diff --git a/DataModels/FollowTargetState.cs b/DataModels/FollowTargetState.cs index 06a4c88..5ce6bdc 100644 --- a/DataModels/FollowTargetState.cs +++ b/DataModels/FollowTargetState.cs @@ -24,17 +24,28 @@ public FollowTargetState(TEntity nullEntity, IEqualityComparer comparer public TEntity AttachmentTarget { get; private set; } + public int AttachmentRevision { get; private set; } + public bool HasSubject => !_comparer.Equals(SelectedSubject, _nullEntity); public void SelectSubject(TEntity subject) { SelectedSubject = subject; - AttachmentTarget = subject; + SetAttachmentTarget(subject); } public void ResolveAttachmentTarget(TEntity target) { - AttachmentTarget = HasSubject ? target : _nullEntity; + SetAttachmentTarget(HasSubject ? target : _nullEntity); + } + + private void SetAttachmentTarget(TEntity target) + { + if (_comparer.Equals(AttachmentTarget, target)) + return; + + AttachmentTarget = target; + AttachmentRevision++; } } } diff --git a/DataModels/TransitStopNameFormatter.cs b/DataModels/TransitStopNameFormatter.cs new file mode 100644 index 0000000..4baabd1 --- /dev/null +++ b/DataModels/TransitStopNameFormatter.cs @@ -0,0 +1,39 @@ +using System; + +namespace FirstPersonCameraContinued.DataModels +{ + public static class TransitStopNameFormatter + { + public const string DefaultStopName = "Stop"; + + public static string ChooseStopName(params string?[] candidates) + { + foreach (string? candidate in candidates) + { + string normalized = NormalizeDisplayName(candidate); + if (!string.IsNullOrEmpty(normalized)) + return normalized; + } + + return DefaultStopName; + } + + public static string NormalizeDisplayName(string? name) + { + if (name == null) + return ""; + + if (string.IsNullOrWhiteSpace(name)) + return ""; + + string trimmedName = name.Trim(); + return IsRawAssetLocalizationName(trimmedName) ? "" : trimmedName; + } + + private static bool IsRawAssetLocalizationName(string name) + { + return name.StartsWith("Assets.NAME[", StringComparison.OrdinalIgnoreCase) + && name.EndsWith("]", StringComparison.Ordinal); + } + } +} diff --git a/MonoBehaviours/FirstPersonCameraController.cs b/MonoBehaviours/FirstPersonCameraController.cs index 0cb9d89..91b2a12 100644 --- a/MonoBehaviours/FirstPersonCameraController.cs +++ b/MonoBehaviours/FirstPersonCameraController.cs @@ -46,7 +46,7 @@ public Entity GetFollowEntity() public Entity GetAttachmentTarget() { - return _model.AttachmentTarget; + return _transformer?.GetEntityFollower().ResolveAttachmentTarget() ?? _model.AttachmentTarget; } public quaternion GetViewRotation() diff --git a/Systems/FirstPersonCameraActivatedUISystem.cs b/Systems/FirstPersonCameraActivatedUISystem.cs index 8216dca..47f5bba 100644 --- a/Systems/FirstPersonCameraActivatedUISystem.cs +++ b/Systems/FirstPersonCameraActivatedUISystem.cs @@ -475,7 +475,7 @@ private void UpdateLineStationInfo(Entity currentEntity) float pos = allWaypoints[i].normalizedPosition; if (pos <= midpointPosition) { - var (streetName, crossStreet) = GetStopStreetAndCrossStreet(allWaypoints[i].stopEntity); + var (streetName, crossStreet) = GetStripMapStopName(allWaypoints[i].stopEntity, isMetroOrTrain); displayedStations.Add((streetName, crossStreet, allWaypoints[i].position, allWaypoints[i].stopEntity)); } } @@ -499,14 +499,14 @@ private void UpdateLineStationInfo(Entity currentEntity) if (include) { - var (streetName, crossStreet) = GetStopStreetAndCrossStreet(allWaypoints[i].stopEntity); + var (streetName, crossStreet) = GetStripMapStopName(allWaypoints[i].stopEntity, isMetroOrTrain); displayedStations.Add((streetName, crossStreet, allWaypoints[i].position, allWaypoints[i].stopEntity)); } } if (firstStationIndex >= 0) { - var (streetName, crossStreet) = GetStopStreetAndCrossStreet(allWaypoints[firstStationIndex].stopEntity); + var (streetName, crossStreet) = GetStripMapStopName(allWaypoints[firstStationIndex].stopEntity, isMetroOrTrain); displayedStations.Add((streetName, crossStreet, allWaypoints[firstStationIndex].position, allWaypoints[firstStationIndex].stopEntity)); } } @@ -786,14 +786,15 @@ private LineStationInfo BuildLineStationResult( private string GetVanillaStopName(Entity stopEntity) { - if (nameSystem.TryGetCustomName(stopEntity, out var customName)) + string customName = GetCustomName(stopEntity); + if (!string.IsNullOrEmpty(customName)) { return customName; } if (BuildingUtils.GetAddress(EntityManager, stopEntity, out var road, out var number)) { - string roadName = AbbreviateSuffix(nameSystem.GetRenderedLabelName(road)); + string roadName = AbbreviateSuffix(SafeGetRenderedLabelName(road)); if (!string.IsNullOrEmpty(roadName)) { return $"{number} {roadName}"; @@ -900,20 +901,26 @@ private string GetLineColor(Entity routeEntity) private string FormatStationName(string streetName, string crossStreet, Dictionary nameCount, Entity stopEntity) { - if (nameSystem.TryGetCustomName(stopEntity, out var customName)) + string customName = GetCustomName(stopEntity); + if (!string.IsNullOrEmpty(customName)) { return customName; } if (EntityManager.TryGetComponent(stopEntity, out var owner) && owner.m_Owner != Entity.Null) { - if (nameSystem.TryGetCustomName(owner.m_Owner, out var ownerCustomName)) + string ownerCustomName = GetCustomName(owner.m_Owner); + if (!string.IsNullOrEmpty(ownerCustomName)) { return ownerCustomName; } } + streetName = TransitStopNameFormatter.NormalizeDisplayName(streetName); string baseName = GetStreetBaseName(streetName); + if (string.IsNullOrEmpty(baseName)) + return TransitStopNameFormatter.DefaultStopName; + if (GetDictionaryValueOrDefault(nameCount, baseName) > 1 && !string.IsNullOrEmpty(crossStreet)) { string crossBase = GetStreetBaseName(crossStreet); @@ -959,8 +966,8 @@ private static int GetDictionaryValueOrDefault(Dictionary dictionar // fallback to stop name if (string.IsNullOrEmpty(streetName)) { - try { streetName = nameSystem.GetRenderedLabelName(stopEntity); } catch { } - if (string.IsNullOrEmpty(streetName)) streetName = "Stop"; + streetName = SafeGetRenderedLabelName(stopEntity); + if (string.IsNullOrEmpty(streetName)) streetName = TransitStopNameFormatter.DefaultStopName; } // find cross street @@ -982,15 +989,59 @@ private string GetRoadName(Entity roadEdge) { if (EntityManager.TryGetComponent(roadEdge, out var aggregated)) { - try - { - return nameSystem.GetRenderedLabelName(aggregated.m_Aggregate); - } - catch { } + return SafeGetRenderedLabelName(aggregated.m_Aggregate); } return ""; } + private (string streetName, string crossStreet) GetStripMapStopName(Entity stopEntity, bool isMetroOrTrain) + { + if (!isMetroOrTrain) + return GetStopStreetAndCrossStreet(stopEntity); + + return ( + TransitStopNameFormatter.ChooseStopName( + GetCustomName(stopEntity), + GetOwnerCustomName(stopEntity)), + ""); + } + + private string GetCustomName(Entity entity) + { + if (entity == Entity.Null) + return ""; + + return nameSystem.TryGetCustomName(entity, out var customName) + ? TransitStopNameFormatter.NormalizeDisplayName(customName) + : ""; + } + + private string GetOwnerCustomName(Entity entity) + { + if (entity == Entity.Null) + return ""; + + if (EntityManager.TryGetComponent(entity, out var owner) && owner.m_Owner != Entity.Null) + return GetCustomName(owner.m_Owner); + + return ""; + } + + private string SafeGetRenderedLabelName(Entity entity) + { + if (entity == Entity.Null) + return ""; + + try + { + return TransitStopNameFormatter.NormalizeDisplayName(nameSystem.GetRenderedLabelName(entity)); + } + catch + { + return ""; + } + } + private float3 GetNodePosition(Entity node) { if (EntityManager.TryGetComponent(node, out var nodeComponent)) diff --git a/Tests/FollowTargetStateTests.cs b/Tests/FollowTargetStateTests.cs index 5af9a14..9116d2c 100644 --- a/Tests/FollowTargetStateTests.cs +++ b/Tests/FollowTargetStateTests.cs @@ -29,4 +29,21 @@ public void ClearingSubjectAlsoClearsAttachmentTarget() Assert.Equal(0, state.SelectedSubject); Assert.Equal(0, state.AttachmentTarget); } + + [Fact] + public void ChangingAttachmentTargetAdvancesAttachmentRevision() + { + var state = new FollowTargetState(0); + + state.SelectSubject(42); + int selectedRevision = state.AttachmentRevision; + + state.ResolveAttachmentTarget(9001); + int vehicleRevision = state.AttachmentRevision; + state.ResolveAttachmentTarget(9001); + + Assert.Equal(42, state.SelectedSubject); + Assert.True(vehicleRevision > selectedRevision); + Assert.Equal(vehicleRevision, state.AttachmentRevision); + } } diff --git a/Tests/FollowTargetStateTests.csproj b/Tests/FollowTargetStateTests.csproj index f4efe8b..64d171f 100644 --- a/Tests/FollowTargetStateTests.csproj +++ b/Tests/FollowTargetStateTests.csproj @@ -13,5 +13,6 @@ + diff --git a/Tests/TransitStopNameFormatterTests.cs b/Tests/TransitStopNameFormatterTests.cs new file mode 100644 index 0000000..bb172a0 --- /dev/null +++ b/Tests/TransitStopNameFormatterTests.cs @@ -0,0 +1,25 @@ +using FirstPersonCameraContinued.DataModels; +using Xunit; + +namespace FirstPersonCameraContinued.Tests; + +public class TransitStopNameFormatterTests +{ + [Fact] + public void ChooseStopNameUsesCustomNameBeforeTrackAssetLabel() + { + string name = TransitStopNameFormatter.ChooseStopName( + "Custom Stop", + "Assets.NAME[Subway Track]"); + + Assert.Equal("Custom Stop", name); + } + + [Fact] + public void ChooseStopNameIgnoresRawAssetLocalizationLabels() + { + string name = TransitStopNameFormatter.ChooseStopName("Assets.NAME[Subway Track]"); + + Assert.Equal("Stop", name); + } +} diff --git a/Transformer/CameraTransformer.cs b/Transformer/CameraTransformer.cs index dbd29c5..9c1e398 100644 --- a/Transformer/CameraTransformer.cs +++ b/Transformer/CameraTransformer.cs @@ -58,7 +58,7 @@ internal CameraTransformer( VirtualCameraRig rig, CameraDataModel model ) _model = model; _manualFinalTransform = new ManualFinalTransform(); _entityFollower = new EntityFollower( _model ); - _followEntityFinalTransform = new FollowEntityFinalTransform( _entityFollower ); + _followEntityFinalTransform = new FollowEntityFinalTransform( _entityFollower, RefreshScope ); _entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; FinalTransform = _manualFinalTransform; AddTransforms( ); @@ -95,9 +95,7 @@ internal CameraTransformer( VirtualCameraRig rig, CameraDataModel model ) } } - _model.Scope = DetermineScope( ); - UpdateEffectToggle( ); - OnScopeChanged?.Invoke( ); // Propagate event to listeners + RefreshScope( ); _model.LastFollowEntity = _model.FollowEntity; }; } @@ -136,25 +134,52 @@ private void UpdateEffectToggle() _model.DisableEffects = _model.Mode == CameraMode.Disabled || _model.Scope != CameraScope.Citizen; } + private Entity GetScopeEntity() + { + return _model.AttachmentTarget != Entity.Null ? _model.AttachmentTarget : _model.FollowEntity; + } + + public void RefreshScope() + { + var previousScope = _model.Scope; + var previousVehicle = _model.ScopeVehicle; + var previousCitizen = _model.ScopeCitizen; + + _model.Scope = DetermineScope( ); + UpdateEffectToggle( ); + + if (previousScope != _model.Scope + || previousVehicle != _model.ScopeVehicle + || previousCitizen != _model.ScopeCitizen) + { + OnScopeChanged?.Invoke( ); + } + } + /// /// Determines camera scope from entity type /// /// public CameraScope DetermineScope( ) { - if ( _model.FollowEntity == Entity.Null ) + var entity = GetScopeEntity( ); + + _model.ScopeCitizen = null; + _model.ScopeVehicle = VehicleType.Unknown; + + if ( entity == Entity.Null ) return CameraScope.Default; - if ( CheckForCitizenScope( out var age ) ) + if ( CheckForCitizenScope( entity, out var age ) ) { _model.ScopeCitizen = age; return CameraScope.Citizen; } - else if ( _entityManager.HasComponent( _model.FollowEntity ) ) + else if ( _entityManager.HasComponent( entity ) ) { return CameraScope.Pet; } - else if ( CheckForVehicleScope( out var vehicleType, out _ ) ) + else if ( CheckForVehicleScope( entity, out var vehicleType, out _ ) ) { var isCar = ( vehicleType & VehicleType.Cars ) != 0; var isVan = ( vehicleType & VehicleType.Vans ) != 0; @@ -167,11 +192,11 @@ public CameraScope DetermineScope( ) return CameraScope.Default; } - private bool CheckForCitizenScope( out CitizenAge citizenAge ) + private bool CheckForCitizenScope( Entity entity, out CitizenAge citizenAge ) { citizenAge = CitizenAge.Teen; - if ( _entityManager.TryGetComponent( _model.FollowEntity, out var resident ) ) + if ( _entityManager.TryGetComponent( entity, out var resident ) ) { if ( resident.m_Citizen != Entity.Null && _entityManager.TryGetComponent( resident.m_Citizen, out var citizen ) ) @@ -187,7 +212,11 @@ private bool CheckForCitizenScope( out CitizenAge citizenAge ) public bool CheckForVehicleScope( out VehicleType vehicleType, out string translatedVehicleType ) { - var entity = _model.FollowEntity; + return CheckForVehicleScope( GetScopeEntity( ), out vehicleType, out translatedVehicleType ); + } + + private bool CheckForVehicleScope( Entity entity, out VehicleType vehicleType, out string translatedVehicleType ) + { var isVehicle = false; vehicleType = VehicleType.Unknown; @@ -343,7 +372,7 @@ public bool CheckForVehicleScope( out VehicleType vehicleType, out string transl if (_entityManager.HasComponent(entity)) { - if (_entityManager.TryGetComponent(_model.FollowEntity, out var prefabRefComponent)) + if (_entityManager.TryGetComponent(entity, out var prefabRefComponent)) { if (_entityManager.TryGetComponent(prefabRefComponent.m_Prefab, out var publicTransportVehicleDataComponent)) { diff --git a/Transformer/FinalTransforms/FollowEntityFinalTransform.cs b/Transformer/FinalTransforms/FollowEntityFinalTransform.cs index 746f533..cd1c2fa 100644 --- a/Transformer/FinalTransforms/FollowEntityFinalTransform.cs +++ b/Transformer/FinalTransforms/FollowEntityFinalTransform.cs @@ -2,6 +2,7 @@ using FirstPersonCameraContinued.DataModels; using FirstPersonCameraContinued.Enums; using Game.Citizens; +using System; using Unity.Entities; using Unity.Mathematics; @@ -14,12 +15,15 @@ internal class FollowEntityFinalTransform : IFinalCameraTransform { private float3 offset; private Entity lastFollow; + private int lastAttachmentRevision = -1; private readonly EntityFollower _entityFollower; + private readonly Action _refreshScope; - public FollowEntityFinalTransform(EntityFollower entityFollower) + public FollowEntityFinalTransform(EntityFollower entityFollower, Action refreshScope) { _entityFollower = entityFollower; + _refreshScope = refreshScope; } /// @@ -32,10 +36,12 @@ public void Apply(VirtualCameraRig rig, CameraDataModel model) if (!_entityFollower.TryGetPosition(out float3 pos, out Bounds3 bounds, out quaternion rot, out bool isTrain)) return; - // When the entity changes get the new offset - if (lastFollow != model.FollowEntity) + _refreshScope( ); + + if (lastFollow != model.FollowEntity || lastAttachmentRevision != model.FollowTarget.AttachmentRevision) { lastFollow = model.FollowEntity; + lastAttachmentRevision = model.FollowTarget.AttachmentRevision; GrabOffset(model); }