From bff73a0a1c5476180a91530b0854edca191e9351 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 13 Jun 2026 21:18:47 -0400 Subject: [PATCH] Fix follow subject retention through vehicles --- DataModels/CameraDataModel.cs | 22 ++- DataModels/FollowTargetState.cs | 40 ++++++ EntityFollower.cs | 129 +++++++++--------- FirstPersonCameraContinued.csproj | 5 + MonoBehaviours/FirstPersonCameraController.cs | 5 + Systems/FirstPersonCameraActivatedUISystem.cs | 11 +- Systems/FirstPersonCameraPIPSystem.cs | 2 +- Tests/FollowTargetStateTests.cs | 32 +++++ Tests/FollowTargetStateTests.csproj | 17 +++ 9 files changed, 194 insertions(+), 69 deletions(-) create mode 100644 DataModels/FollowTargetState.cs create mode 100644 Tests/FollowTargetStateTests.cs create mode 100644 Tests/FollowTargetStateTests.csproj diff --git a/DataModels/CameraDataModel.cs b/DataModels/CameraDataModel.cs index 67230f8..c0a5e0e 100644 --- a/DataModels/CameraDataModel.cs +++ b/DataModels/CameraDataModel.cs @@ -28,14 +28,28 @@ public Entity LastFollowEntity set; } + public FollowTargetState FollowTarget + { + get; + } = new FollowTargetState(Entity.Null); + /// - /// The entity we may be following + /// The entity the user intentionally selected to follow. /// public Entity FollowEntity { - get; - set; - } = Entity.Null; + get => FollowTarget.SelectedSubject; + set => FollowTarget.SelectSubject(value); + } + + /// + /// The entity currently used for camera position and rotation. + /// + public Entity AttachmentTarget + { + get => FollowTarget.AttachmentTarget; + set => FollowTarget.ResolveAttachmentTarget(value); + } /// /// Is the player sprinting? diff --git a/DataModels/FollowTargetState.cs b/DataModels/FollowTargetState.cs new file mode 100644 index 0000000..06a4c88 --- /dev/null +++ b/DataModels/FollowTargetState.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace FirstPersonCameraContinued.DataModels +{ + public class FollowTargetState + { + private readonly TEntity _nullEntity; + private readonly IEqualityComparer _comparer; + + public FollowTargetState(TEntity nullEntity) + : this(nullEntity, EqualityComparer.Default) + { + } + + public FollowTargetState(TEntity nullEntity, IEqualityComparer comparer) + { + _nullEntity = nullEntity; + _comparer = comparer; + SelectedSubject = nullEntity; + AttachmentTarget = nullEntity; + } + + public TEntity SelectedSubject { get; private set; } + + public TEntity AttachmentTarget { get; private set; } + + public bool HasSubject => !_comparer.Equals(SelectedSubject, _nullEntity); + + public void SelectSubject(TEntity subject) + { + SelectedSubject = subject; + AttachmentTarget = subject; + } + + public void ResolveAttachmentTarget(TEntity target) + { + AttachmentTarget = HasSubject ? target : _nullEntity; + } + } +} diff --git a/EntityFollower.cs b/EntityFollower.cs index c5b766a..a89e6df 100644 --- a/EntityFollower.cs +++ b/EntityFollower.cs @@ -28,75 +28,65 @@ internal EntityFollower( CameraDataModel model ) } /// - /// Filter the current entity + /// Resolve the current camera attachment target without changing the selected follow subject. /// - private void Filter( ) + public Entity ResolveAttachmentTarget() { + var target = _model.FollowEntity; + + if (target == Entity.Null || !_entityManager.Exists(target)) + { + _model.AttachmentTarget = Entity.Null; + return Entity.Null; + } + // Check and update entity based on TargetElement buffer - if ( _entityManager.TryGetBuffer( _model.FollowEntity, true, out var buffer ) && buffer.Length > 0 ) + if (_entityManager.TryGetBuffer(target, true, out var buffer) && buffer.Length > 0) { - _model.FollowEntity = buffer[0].m_Entity; + target = buffer[0].m_Entity; } // Check and update entity based on CurrentTransport component - if ( TryGetComponent( out var transport ) ) + if ( TryGetComponent(target, out CurrentTransport transport) ) { - _model.FollowEntity = transport.m_CurrentTransport; + target = transport.m_CurrentTransport; } // Further processing if entity has Unspawned component - if ( HasComponent( ) ) + if ( HasComponent(target) ) { // Check and update entity based on CurrentVehicle component - if ( TryGetComponent( out var vehicle ) ) + if ( TryGetComponent(target, out CurrentVehicle vehicle) ) { - _model.FollowEntity = vehicle.m_Vehicle; + target = vehicle.m_Vehicle; } // Check and update entity based on Resident component and its CurrentBuilding - else if ( TryGetComponent( out var residentComponent ) && + else if ( TryGetComponent(target, out Game.Creatures.Resident residentComponent) && TryGetComponent( residentComponent.m_Citizen, out var houseResident ) ) { - _model.FollowEntity = houseResident.m_CurrentBuilding; + target = houseResident.m_CurrentBuilding; } // Check and update entity based on Pet component and its CurrentBuilding - else if ( TryGetComponent( out var petComponent ) && + else if ( TryGetComponent(target, out Game.Creatures.Pet petComponent) && TryGetComponent( petComponent.m_HouseholdPet, out var housePet ) ) { - _model.FollowEntity = housePet.m_CurrentBuilding; + target = housePet.m_CurrentBuilding; } } - - // switch back to following cim entity after exiting vehicle - if (_entityManager.TryGetComponent(_model.LastFollowEntity, out var lastEntityResident) && lastEntityResident.m_Flags.HasFlag(ResidentFlags.Disembarking)) + if (target != Entity.Null && !_entityManager.Exists(target)) { - //Mod.log.Info("Cim Disembarking " + _model.FollowEntity); - _model.FollowEntity = _model.LastFollowEntity; + target = Entity.Null; } + _model.AttachmentTarget = target; + return target; } - /// - /// Shortcut for checking if a component exists - /// - /// - /// - private bool HasComponent( ) - where T : unmanaged, IComponentData - { - return _entityManager.HasComponent( _model.FollowEntity ); - } - - /// - /// Shortcut for getting a component - /// - /// - /// - /// - private bool TryGetComponent( out T component ) + private bool HasComponent( Entity entity ) where T : unmanaged, IComponentData { - return _entityManager.TryGetComponent( _model.FollowEntity, out component ); + return entity != Entity.Null && _entityManager.Exists(entity) && _entityManager.HasComponent( entity ); } /// @@ -109,7 +99,8 @@ private bool TryGetComponent( out T component ) private bool TryGetComponent( Entity entity, out T component ) where T : unmanaged, IComponentData { - return _entityManager.TryGetComponent( entity, out component ); + component = default; + return entity != Entity.Null && _entityManager.Exists(entity) && _entityManager.TryGetComponent( entity, out component ); } /// @@ -198,45 +189,51 @@ public bool TryGetPosition( out float3 position, out Bounds3 bounds, out quatern rotation = default; isTrain = false; - Filter(); - if (_entityManager.TryGetComponent(_model.FollowEntity, out var trainNavigationComponent)) + var target = ResolveAttachmentTarget(); + if (target == Entity.Null) + return false; + + if (_entityManager.TryGetComponent(target, out var trainNavigationComponent)) { - if (_entityManager.TryGetComponent(_model.FollowEntity, out var interpolatedTransformComponent)) + if (_entityManager.TryGetComponent(target, out var interpolatedTransformComponent)) { position = interpolatedTransformComponent.m_Position; rotation = interpolatedTransformComponent.m_Rotation; isTrain = true; + return true; } } else { - if (_entityManager.TryGetBuffer(_model.FollowEntity, true, out var buffer1)) + if (_entityManager.TryGetBuffer(target, true, out var buffer1)) { - var interpolatedPosition = GetInterpolatedPosition(_model.FollowEntity, buffer1, out bounds); + var interpolatedPosition = GetInterpolatedPosition(target, buffer1, out bounds); position = interpolatedPosition.m_Position; rotation = interpolatedPosition.m_Rotation; + return true; } else { - if (_entityManager.TryGetComponent(_model.FollowEntity, out var component1)) + if (_entityManager.TryGetComponent(target, out var component1)) { - var relativePosition = GetRelativePosition(_model.FollowEntity, component1, out bounds); + var relativePosition = GetRelativePosition(target, component1, out bounds); position = relativePosition.m_Position; rotation = relativePosition.m_Rotation; + return true; } else { - if (_entityManager.TryGetComponent(_model.FollowEntity, out var component2)) + if (_entityManager.TryGetComponent(target, out var component2)) { - var objectPosition = GetObjectPosition(_model.FollowEntity, component2, out bounds); + var objectPosition = GetObjectPosition(target, component2, out bounds); position = objectPosition.m_Position; rotation = objectPosition.m_Rotation; + return true; } } } - isTrain = false; } - return true; + return false; } /// @@ -248,34 +245,39 @@ public bool TryGetPosition( out float3 position ) { position = default; - Filter( ); + var target = ResolveAttachmentTarget(); + if (target == Entity.Null) + return false; //if ( _entityManager.TryGetComponent( _model.FollowEntity, out var interpolatedTransform ) ) //{ // position = interpolatedTransform.m_Position; //} - if ( _entityManager.TryGetBuffer( _model.FollowEntity, true, out var buffer1 ) ) + if ( _entityManager.TryGetBuffer( target, true, out var buffer1 ) ) { - var interpolatedPosition = GetInterpolatedPosition( _model.FollowEntity, buffer1, out _ ); + var interpolatedPosition = GetInterpolatedPosition( target, buffer1, out _ ); position = interpolatedPosition.m_Position; + return true; } else { - if ( _entityManager.TryGetComponent( _model.FollowEntity, out var component1 ) ) + if ( _entityManager.TryGetComponent( target, out var component1 ) ) { - var relativePosition = GetRelativePosition( _model.FollowEntity, component1, out _ ); + var relativePosition = GetRelativePosition( target, component1, out _ ); position = relativePosition.m_Position; + return true; } else { - if ( _entityManager.TryGetComponent( _model.FollowEntity, out var component2 ) ) + if ( _entityManager.TryGetComponent( target, out var component2 ) ) { - var objectPosition = GetObjectPosition( _model.FollowEntity, component2, out _ ); + var objectPosition = GetObjectPosition( target, component2, out _ ); position = objectPosition.m_Position; + return true; } } } - return true; + return false; } /// @@ -288,18 +290,21 @@ public bool TryGetPosition( out float3 position ) { rotation = default; - var workingEntity = entity != default ? entity : _model.FollowEntity; + var workingEntity = entity != default ? entity : ResolveAttachmentTarget(); - Filter( ); + if (workingEntity == Entity.Null) + return false; - if ( _entityManager.TryGetComponent( _model.FollowEntity, out var interpolatedTransform ) ) + if ( _entityManager.TryGetComponent( workingEntity, out var interpolatedTransform ) ) { rotation = interpolatedTransform.m_Rotation; + return true; } else if ( _entityManager.TryGetBuffer( workingEntity, true, out var buffer1 ) ) { var interpolatedPosition = GetInterpolatedPosition( workingEntity, buffer1, out _ ); rotation = interpolatedPosition.m_Rotation; + return true; } else { @@ -307,6 +312,7 @@ public bool TryGetPosition( out float3 position ) { var relativePosition = GetRelativePosition( workingEntity, component1, out _ ); rotation = relativePosition.m_Rotation; + return true; } else { @@ -314,10 +320,11 @@ public bool TryGetPosition( out float3 position ) { var objectPosition = GetObjectPosition( workingEntity, component2, out _ ); rotation = objectPosition.m_Rotation; + return true; } } } - return true; + return false; } } } diff --git a/FirstPersonCameraContinued.csproj b/FirstPersonCameraContinued.csproj index f60bf4d..3091bbc 100644 --- a/FirstPersonCameraContinued.csproj +++ b/FirstPersonCameraContinued.csproj @@ -87,6 +87,7 @@ false + $(ManagedPath)\Newtonsoft.Json.dll False @@ -150,6 +151,10 @@ + + + + diff --git a/MonoBehaviours/FirstPersonCameraController.cs b/MonoBehaviours/FirstPersonCameraController.cs index fd31029..0cb9d89 100644 --- a/MonoBehaviours/FirstPersonCameraController.cs +++ b/MonoBehaviours/FirstPersonCameraController.cs @@ -44,6 +44,11 @@ public Entity GetFollowEntity() return _model.FollowEntity; } + public Entity GetAttachmentTarget() + { + return _model.AttachmentTarget; + } + public quaternion GetViewRotation() { return _model.Rotation; diff --git a/Systems/FirstPersonCameraActivatedUISystem.cs b/Systems/FirstPersonCameraActivatedUISystem.cs index 5d24251..8216dca 100644 --- a/Systems/FirstPersonCameraActivatedUISystem.cs +++ b/Systems/FirstPersonCameraActivatedUISystem.cs @@ -143,7 +143,7 @@ protected override void OnUpdate() { return; } - Entity currentEntity = CameraController.GetFollowEntity(); + Entity currentEntity = CameraController.GetAttachmentTarget(); if (currentEntity != Entity.Null) { @@ -749,7 +749,7 @@ private LineStationInfo BuildLineStationResult( foreach (var station in stations) { string baseName = GetStreetBaseName(station.streetName); - nameCount[baseName] = nameCount.GetValueOrDefault(baseName, 0) + 1; + nameCount[baseName] = GetDictionaryValueOrDefault(nameCount, baseName) + 1; } if (goingInbound) @@ -914,7 +914,7 @@ private string FormatStationName(string streetName, string crossStreet, Dictiona } string baseName = GetStreetBaseName(streetName); - if (nameCount.GetValueOrDefault(baseName, 0) > 1 && !string.IsNullOrEmpty(crossStreet)) + if (GetDictionaryValueOrDefault(nameCount, baseName) > 1 && !string.IsNullOrEmpty(crossStreet)) { string crossBase = GetStreetBaseName(crossStreet); return $"{baseName}/\n{crossBase}"; @@ -922,6 +922,11 @@ private string FormatStationName(string streetName, string crossStreet, Dictiona return AbbreviateSuffix(streetName); } + private static int GetDictionaryValueOrDefault(Dictionary dictionary, string key) + { + return dictionary.TryGetValue(key, out int value) ? value : 0; + } + private (string streetName, string crossStreet) GetStopStreetAndCrossStreet(Entity stopEntity) { Entity roadEdge = Entity.Null; diff --git a/Systems/FirstPersonCameraPIPSystem.cs b/Systems/FirstPersonCameraPIPSystem.cs index bc70e00..f175d8e 100644 --- a/Systems/FirstPersonCameraPIPSystem.cs +++ b/Systems/FirstPersonCameraPIPSystem.cs @@ -103,7 +103,7 @@ protected override void OnUpdate() var positon = CameraController.transform.position; var rotation = CameraController.GetViewRotation(); - Entity currentEntity = CameraController.GetFollowEntity(); + Entity currentEntity = CameraController.GetAttachmentTarget(); if (currentEntity != _lastPipEntity) { _lastPipEntity = currentEntity; diff --git a/Tests/FollowTargetStateTests.cs b/Tests/FollowTargetStateTests.cs new file mode 100644 index 0000000..5af9a14 --- /dev/null +++ b/Tests/FollowTargetStateTests.cs @@ -0,0 +1,32 @@ +using FirstPersonCameraContinued.DataModels; +using Xunit; + +namespace FirstPersonCameraContinued.Tests; + +public class FollowTargetStateTests +{ + [Fact] + public void ResolvingAttachmentTargetDoesNotChangeSelectedSubject() + { + var state = new FollowTargetState(0); + + state.SelectSubject(42); + state.ResolveAttachmentTarget(9001); + + Assert.Equal(42, state.SelectedSubject); + Assert.Equal(9001, state.AttachmentTarget); + } + + [Fact] + public void ClearingSubjectAlsoClearsAttachmentTarget() + { + var state = new FollowTargetState(0); + + state.SelectSubject(42); + state.ResolveAttachmentTarget(9001); + state.SelectSubject(0); + + Assert.Equal(0, state.SelectedSubject); + Assert.Equal(0, state.AttachmentTarget); + } +} diff --git a/Tests/FollowTargetStateTests.csproj b/Tests/FollowTargetStateTests.csproj new file mode 100644 index 0000000..f4efe8b --- /dev/null +++ b/Tests/FollowTargetStateTests.csproj @@ -0,0 +1,17 @@ + + + net8.0 + false + enable + + + + + + + + + + + +