diff --git a/Robust.Client/Audio/AudioSystem.cs b/Robust.Client/Audio/AudioSystem.cs index 2eabb9fbb24..2c0a058e776 100644 --- a/Robust.Client/Audio/AudioSystem.cs +++ b/Robust.Client/Audio/AudioSystem.cs @@ -493,7 +493,7 @@ public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(Soun var (entity, component) = CreateAndStartPlayingStream(audioParams, stream); component.Global = true; component.Source.Global = true; - Dirty(entity, component); + DirtyField(entity, component, nameof(AudioComponent.Global)); return (entity, component); } diff --git a/Robust.Client/GameStates/NetGraphOverlay.cs b/Robust.Client/GameStates/NetGraphOverlay.cs index 63be513854e..41cb88cc364 100644 --- a/Robust.Client/GameStates/NetGraphOverlay.cs +++ b/Robust.Client/GameStates/NetGraphOverlay.cs @@ -26,6 +26,7 @@ internal sealed class NetGraphOverlay : Overlay [Dependency] private readonly IClientNetManager _netManager = default!; [Dependency] private readonly IClientGameStateManager _gameStateManager = default!; [Dependency] private readonly IComponentFactory _componentFactory = default!; + [Dependency] private readonly IConsoleHost _host = default!; [Dependency] private readonly IEntityManager _entManager = default!; private const int HistorySize = 60 * 5; // number of ticks to keep in history. @@ -79,7 +80,7 @@ private void HandleGameStateApplied(GameStateAppliedArgs args) string? entStateString = null; string? entDelString = null; - var conShell = IoCManager.Resolve().LocalShell; + var conShell = _host.LocalShell; var entStates = args.AppliedState.EntityStates; if (entStates.HasContents) diff --git a/Robust.Serialization.Generator/Generator.cs b/Robust.Serialization.Generator/Generator.cs index 382adb2ce08..a48862dc263 100644 --- a/Robust.Serialization.Generator/Generator.cs +++ b/Robust.Serialization.Generator/Generator.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -13,6 +14,8 @@ public class Generator : IIncrementalGenerator private const string TypeCopierInterfaceNamespace = "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopier"; private const string TypeCopyCreatorInterfaceNamespace = "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopyCreator"; private const string SerializationHooksNamespace = "Robust.Shared.Serialization.ISerializationHooks"; + private const string AutoStateAttributeName = "Robust.Shared.Analyzers.AutoGenerateComponentStateAttribute"; + private const string ComponentDeltaInterfaceName = "Robust.Shared.GameObjects.IComponentDelta"; public void Initialize(IncrementalGeneratorInitializationContext initContext) { @@ -35,6 +38,7 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) var builder = new StringBuilder(); var containingTypes = new Stack(); var declarationsGenerated = new HashSet(); + var deltaType = compilation.GetTypeByMetadataName(ComponentDeltaInterfaceName)!; foreach (var declaration in declarations) { @@ -106,9 +110,9 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) { {{GetConstructor(definition)}} - {{GetCopyMethods(definition)}} + {{GetCopyMethods(definition, deltaType)}} - {{GetInstantiators(definition)}} + {{GetInstantiators(definition, deltaType)}} } {{containingTypesEnd}} @@ -192,7 +196,7 @@ private static string GetConstructor(DataDefinition definition) return builder.ToString(); } - private static string GetCopyMethods(DataDefinition definition) + private static string GetCopyMethods(DataDefinition definition, ITypeSymbol deltaType) { var builder = new StringBuilder(); @@ -263,7 +267,7 @@ public override void Copy(ref object target, ISerializationManager serialization {{baseCopy}} """); - foreach (var @interface in GetImplicitDataDefinitionInterfaces(definition.Type, true)) + foreach (var @interface in InternalGetImplicitDataDefinitionInterfaces(definition.Type, true, deltaType)) { var interfaceModifiers = baseType != null && baseType.AllInterfaces.Contains(@interface, SymbolEqualityComparer.Default) ? "override " @@ -292,7 +296,7 @@ public override void Copy(ref object target, ISerializationManager serialization return builder.ToString(); } - private static string GetInstantiators(DataDefinition definition) + private static string GetInstantiators(DataDefinition definition, ITypeSymbol deltaType) { var builder = new StringBuilder(); var modifiers = string.Empty; @@ -326,7 +330,7 @@ private static string GetInstantiators(DataDefinition definition) """); } - foreach (var @interface in GetImplicitDataDefinitionInterfaces(definition.Type, false)) + foreach (var @interface in InternalGetImplicitDataDefinitionInterfaces(definition.Type, false, deltaType)) { var interfaceName = @interface.ToDisplayString(); builder.AppendLine($$""" @@ -345,6 +349,25 @@ private static string GetInstantiators(DataDefinition definition) return builder.ToString(); } + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] + private static IEnumerable InternalGetImplicitDataDefinitionInterfaces(ITypeSymbol type, bool all, ITypeSymbol deltaType) + { + var symbols = GetImplicitDataDefinitionInterfaces(type, all); + + // TODO SOURCE GEN + // fix this jank + // The comp-state source generator will add an "IComponentDelta" interface to classes with the auto state + // attribute, and this generator creates methods that those classes then have to implement because + // IComponentDelta a DataDefinition via the ImplicitDataDefinitionForInheritorsAttribute on IComponent. + if (!HasAttribute(type, AutoStateAttributeName)) + return symbols; + + if (symbols.Any(x => x.ToDisplayString() == deltaType.ToDisplayString())) + return symbols; + + return symbols.Append(deltaType); + } + // TODO serveronly? do we care? who knows!! private static StringBuilder CopyDataFields(DataDefinition definition) { diff --git a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs index 69cd47bedd4..af4f386de9c 100644 --- a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs +++ b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs @@ -134,11 +134,45 @@ public class ComponentNetworkGenerator : ISourceGenerator // component.Count = state.Count; var handleStateSetters = new StringBuilder(); + // Implements a switch case to correspond string field names to sourcegenned fields. + var deltaGetFields = new StringBuilder(); + var deltaHandleFields = new StringBuilder(); + + var deltaApply = new StringBuilder(); + var deltaCreate = new StringBuilder(); + var index = -1; + + var fieldDeltas = new StringBuilder(); + foreach (var (type, name) in fields) { + index++; + + deltaApply.Append($@" + case {index}:"); + + deltaGetFields.Append($@" + case {index}:"); + + deltaHandleFields.Append($@" + case {index}:"); + + if (index == 0) + { + fieldDeltas.Append(@$"""{name}"""); + } + else + { + fieldDeltas.Append(@$", ""{name}"""); + } + var typeDisplayStr = type.ToDisplayString(FullNullableFormat); var nullable = type.NullableAnnotation == NullableAnnotation.Annotated; var nullableAnnotation = nullable ? "?" : string.Empty; + string? getField; + string? cast; + // TODO: Uhh I just need casts or something. + var castString = typeDisplayStr.Substring(8); switch (typeDisplayStr) { @@ -147,10 +181,27 @@ public class ComponentNetworkGenerator : ISourceGenerator stateFields.Append($@" public NetEntity{nullableAnnotation} {name} = default!;"); + getField = $"GetNetEntity(component.{name})"; + getStateInit.Append($@" - {name} = GetNetEntity(component.{name}),"); + {name} = {getField},"); + + deltaGetFields.Append($@" + data.Add({getField});"); + + cast = $"(NetEntity{nullableAnnotation})"; + handleStateSetters.Append($@" - component.{name} = EnsureEntity<{componentName}>(state.{name}, uid);"); + component.{name} = EnsureEntity<{componentName}>(state.{name}, uid);"); + + deltaHandleFields.Append($@" + component.{name} = EnsureEntity<{componentName}>({cast} value!, uid);"); + + deltaCreate.Append($@" + {name} = fullState.{name},"); + + deltaApply.Append($@" + fullState.{name} = {cast} value!;"); break; case GlobalEntityCoordinatesName: @@ -158,30 +209,81 @@ public class ComponentNetworkGenerator : ISourceGenerator stateFields.Append($@" public NetCoordinates{nullableAnnotation} {name} = default!;"); + getField = $"GetNetCoordinates(component.{name})"; + getStateInit.Append($@" - {name} = GetNetCoordinates(component.{name}),"); + {name} = {getField},"); + + deltaGetFields.Append($@" + data.Add({getField});"); + + cast = $"(NetCoordinates{nullableAnnotation})"; + handleStateSetters.Append($@" - component.{name} = EnsureCoordinates<{componentName}>(state.{name}, uid);"); + component.{name} = EnsureCoordinates<{componentName}>(state.{name}, uid);"); + + deltaHandleFields.Append($@" + component.{name} = EnsureCoordinates<{componentName}>({cast} value!, uid);"); + + deltaCreate.Append($@" + {name} = fullState.{name},"); + + deltaApply.Append($@" + fullState.{name} = {cast} value!;"); break; case GlobalEntityUidSetName: stateFields.Append($@" public {GlobalNetEntityUidSetName} {name} = default!;"); + getField = $"GetNetEntitySet(component.{name})"; + getStateInit.Append($@" - {name} = GetNetEntitySet(component.{name}),"); + {name} = {getField},"); + + deltaGetFields.Append($@" + data.Add({getField});"); + + cast = $"({GlobalNetEntityUidSetName})"; + handleStateSetters.Append($@" - EnsureEntitySet<{componentName}>(state.{name}, uid, component.{name});"); + EnsureEntitySet<{componentName}>(state.{name}, uid, component.{name});"); + + deltaHandleFields.Append($@" + EnsureEntitySet<{componentName}>({cast} value!, uid, component.{name});"); + + deltaCreate.Append($@" + {name} = new(fullState.{name}),"); + + deltaApply.Append($@" + fullState.{name} = {cast} value!;"); break; case GlobalEntityUidListName: stateFields.Append($@" - public {GlobalNetEntityUidListName} {name} = default!;"); + public {GlobalNetEntityUidListName} {name} = default!;"); + + getField = $"GetNetEntityList(component.{name})"; getStateInit.Append($@" - {name} = GetNetEntityList(component.{name}),"); + {name} = {getField},"); + + deltaGetFields.Append($@" + data.Add({getField});"); + + cast = $"({GlobalNetEntityUidListName})"; + handleStateSetters.Append($@" - EnsureEntityList<{componentName}>(state.{name}, uid, component.{name});"); + EnsureEntityList<{componentName}>(state.{name}, uid, component.{name});"); + + deltaHandleFields.Append($@" + EnsureEntityList<{componentName}>({cast} value!, uid, component.{name});"); + + deltaCreate.Append($@" + {name} = new(fullState.{name}),"); + + deltaApply.Append($@" + fullState.{name} = {cast} value!;"); break; default: @@ -208,20 +310,41 @@ public class ComponentNetworkGenerator : ISourceGenerator stateFields.Append($@" public Dictionary<{key}, {value}> {name} = default!;"); + getField = $"GetNetEntityDictionary(component.{name})"; + getStateInit.Append($@" - {name} = GetNetEntityDictionary(component.{name}),"); + {name} = {getField},"); + + deltaGetFields.Append($@" + data.Add({getField});"); if (valueNullable && value is not GlobalNetEntityName and not GlobalNetEntityNullableName) { + cast = $"(Dictionary<{key}, {value}>)"; + handleStateSetters.Append($@" - EnsureEntityDictionaryNullableValue<{componentName}, {value}>(state.{name}, uid, component.{name});"); + EnsureEntityDictionaryNullableValue<{componentName}, {value}>(state.{name}, uid, component.{name});"); + + deltaHandleFields.Append($@" + EnsureEntityDictionaryNullableValue<{componentName}, {value}>({cast} value!, uid, component.{name});"); } else { + cast = $"({castString})"; + handleStateSetters.Append($@" - EnsureEntityDictionary<{ensureGeneric}>(state.{name}, uid, component.{name});"); + EnsureEntityDictionary<{ensureGeneric}>(state.{name}, uid, component.{name})"); + + deltaHandleFields.Append($@" + EnsureEntityDictionary<{ensureGeneric}>( value!, uid, component.{name});"); } + deltaCreate.Append($@" + {name} = new(fullState.{name}),"); + + deltaApply.Append($@" + fullState.{name} = {cast} value!;"); + break; } @@ -232,10 +355,27 @@ public class ComponentNetworkGenerator : ISourceGenerator stateFields.Append($@" public Dictionary<{key}, {value}> {name} = default!;"); + getField = $"GetNetEntityDictionary(component.{name})"; + getStateInit.Append($@" - {name} = GetNetEntityDictionary(component.{name}),"); + {name} = {getField},"); + + deltaGetFields.Append($@" + data.Add({getField});"); + + cast = $"(Dictionary<{key}, {value}>)"; + handleStateSetters.Append($@" - EnsureEntityDictionary<{componentName}, {key}>(state.{name}, uid, component.{name});"); + EnsureEntityDictionary<{componentName}, {key}>(state.{name}, uid, component.{name});"); + + deltaHandleFields.Append($@" + EnsureEntityDictionary<{componentName}, {key}>({cast} value!, uid, component.{name});"); + + deltaCreate.Append($@" + {name} = new(fullState.{name}),"); + + deltaApply.Append($@" + fullState.{name} = {cast} value!;"); break; } @@ -251,23 +391,79 @@ public class ComponentNetworkGenerator : ISourceGenerator getStateInit.Append($@" {name} = component.{name},"); + deltaGetFields.Append($@" + data.Add(component.{name});"); + + cast = $"({castString})"; + var nullCast = nullable ? castString.Substring(0, castString.Length - 1) : castString; + handleStateSetters.Append($@" if (state.{name} == null) component.{name} = null!; else component.{name} = new(state.{name});"); + + deltaHandleFields.Append($@" + var {name}Value = {cast} value!; + if ({name}Value == null) + component.{name} = null!; + else + component.{name} = new {nullCast}({name}Value);"); + + if (nullable) + { + deltaCreate.Append($@" + {name} = fullState.{name} == null ? null : new(fullState.{name}),"); + } + else + { + deltaCreate.Append($@" + {name} = new(fullState.{name}),"); + } + + deltaApply.Append($@" + if (value == null) + fullState.{name} = null!; + else + fullState.{name} = new {nullCast}(({nullCast}) value);"); } else { getStateInit.Append($@" {name} = component.{name},"); + deltaGetFields.Append($@" + data.Add(component.{name});"); + + cast = $"({castString})"; + handleStateSetters.Append($@" component.{name} = state.{name};"); + + deltaHandleFields.Append($@" + component.{name} = {cast} value!;"); + + deltaCreate.Append($@" + {name} = fullState.{name},"); + + deltaApply.Append($@" + fullState.{name} = {cast} value!;"); } break; } + + /* + * End loop stuff + */ + deltaApply.Append($@" + break;"); + + deltaGetFields.Append($@" + break;"); + + deltaHandleFields.Append($@" + break;"); } var eventRaise = ""; @@ -284,13 +480,23 @@ public class ComponentNetworkGenerator : ISourceGenerator using Robust.Shared.GameStates; using Robust.Shared.GameObjects; using Robust.Shared.Analyzers; +using Robust.Shared.Collections; using Robust.Shared.Serialization; using Robust.Shared.Map; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using System.Collections.Generic; namespace {nameSpace}; -public partial class {componentName} +public partial class {componentName} : IComponentDelta {{ + /// + public GameTick LastFieldUpdate {{ get; set; }} = GameTick.Zero; + + /// + public GameTick[] LastModifiedFields {{ get; set; }} = Array.Empty(); + [System.Serializable, NetSerializable] public sealed class {stateName} : IComponentState {{{stateFields} @@ -301,12 +507,47 @@ public sealed class {componentName}_AutoNetworkSystem : EntitySystem {{ public override void Initialize() {{ + EntityManager.ComponentFactory.RegisterNetworkedFields<{classSymbol}>({fieldDeltas}); SubscribeLocalEvent<{componentName}, ComponentGetState>(OnGetState); SubscribeLocalEvent<{componentName}, ComponentHandleState>(OnHandleState); }} private void OnGetState(EntityUid uid, {componentName} component, ref ComponentGetState args) {{ + // Delta state + var delta = (IComponentDelta)component; + + if (args.FromTick > component.CreationTick && delta.LastFieldUpdate >= args.FromTick) + {{ + var data = new ValueList(); + uint fields = 0; + + for (var i = 0; i < delta.LastModifiedFields.Length; i++) + {{ + var lastUpdate = delta.LastModifiedFields[i]; + + // Field not dirty + if (lastUpdate < args.FromTick) + continue; + + fields |= (uint) (1 << i); + + switch (i) + {{{deltaGetFields} + default: + throw new ArgumentOutOfRangeException(); + }} + }} + + args.State = new {componentName}DeltaFieldComponentState() + {{ + ModifiedFields = fields, + Fields = data.ToArray(), + }}; + + return; + }} + args.State = new {stateName} {{{getStateInit} }}; @@ -314,11 +555,98 @@ private void OnGetState(EntityUid uid, {componentName} component, ref ComponentG private void OnHandleState(EntityUid uid, {componentName} component, ref ComponentHandleState args) {{ + if (args.Current is {componentName}DeltaFieldComponentState deltaState) + {{ + // Don't need CompReg here because we already know the AutoNetworkedField indices in advance. + byte index = 0; + + // So we iterate the bitmask and see if it's flagged, which we need to track independently of the array index. + for (var i = 0; i < {index + 1}; i++) + {{ + var field = 1 << i; + + // Field not modified + if ((deltaState.ModifiedFields & field) == 0x0) + continue; + + var value = deltaState.Fields[index]; + + switch (i) + {{{deltaHandleFields} + default: + throw new ArgumentOutOfRangeException(); + }} + + index++; + }} + + DebugTools.Assert(index == deltaState.Fields.Length); + return; + }} + if (args.Current is not {stateName} state) return; {handleStateSetters}{eventRaise} }} }} + + [Serializable, NetSerializable] + public sealed class {componentName}DeltaFieldComponentState : IComponentDeltaState<{stateName}> + {{ + public uint ModifiedFields = 0; + public object?[] Fields = Array.Empty(); + + public void ApplyToFullState({stateName} fullState) + {{ + byte index = 0; + + for (var i = 0; i < {index + 1}; i++) + {{ + var field = 1 << i; + + if ((ModifiedFields & field) == 0x0) + continue; + + var value = Fields[index]; + + switch (i) + {{{deltaApply} + }} + + index++; + }} + + DebugTools.Assert(index == Fields.Length); + }} + + public {stateName} CreateNewFullState({stateName} fullState) + {{ + var newState = new {stateName} + {{{deltaCreate} + }}; + + byte index = 0; + + for (var i = 0; i < {index + 1}; i++) + {{ + var field = 1 << i; + + if ((ModifiedFields & field) == 0x0) + continue; + + var value = Fields[index]; + + switch (i) + {{{deltaApply} + }} + + index++; + }} + + DebugTools.Assert(index == Fields.Length); + return newState; + }} + }} }} "; } diff --git a/Robust.Shared.CompNetworkGenerator/Properties/launchSettings.json b/Robust.Shared.CompNetworkGenerator/Properties/launchSettings.json new file mode 100644 index 00000000000..ae931baa7b2 --- /dev/null +++ b/Robust.Shared.CompNetworkGenerator/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Comp State Generator": { + "commandName": "DebugRoslynComponent", + "targetProject": "../../Content.Shared/Content.Shared.csproj" + } + } +} diff --git a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs index 7bed97f5ef3..05eda84bd1b 100644 --- a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs +++ b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs @@ -105,6 +105,7 @@ public void SetPlaybackPosition(Entity? nullEntity, float posit if (entity.Comp.PauseTime != null) { entity.Comp.PauseTime = entity.Comp.PauseTime.Value + timeOffset; + DirtyField(entity, nameof(AudioComponent.PauseTime)); // Paused audio doesn't have TimedDespawn so. } @@ -112,6 +113,7 @@ public void SetPlaybackPosition(Entity? nullEntity, float posit { // Bump it back so the actual playback positions moves forward entity.Comp.AudioStart -= timeOffset; + DirtyField(entity, nameof(AudioComponent.AudioStart)); // need to ensure it doesn't despawn too early. if (TryComp(entity.Owner, out TimedDespawnComponent? despawn)) @@ -121,8 +123,6 @@ public void SetPlaybackPosition(Entity? nullEntity, float posit } entity.Comp.PlaybackPosition = position; - // Network the new playback position. - Dirty(entity); } /// @@ -191,6 +191,9 @@ public void SetState(EntityUid? entity, AudioState state, bool force = false, Au var pauseOffset = Timing.CurTime - component.PauseTime; component.AudioStart += pauseOffset ?? TimeSpan.Zero; component.PlaybackPosition = (float) (Timing.CurTime - component.AudioStart).TotalSeconds; + + DirtyField(entity.Value, component, nameof(AudioComponent.AudioStart)); + DirtyField(entity.Value, component, nameof(AudioComponent.PlaybackPosition)); } // If we were stopped then played then restart audiostart to now. @@ -198,6 +201,9 @@ public void SetState(EntityUid? entity, AudioState state, bool force = false, Au { component.AudioStart = Timing.CurTime; component.PauseTime = null; + + DirtyField(entity.Value, component, nameof(AudioComponent.AudioStart)); + DirtyField(entity.Value, component, nameof(AudioComponent.PauseTime)); } switch (state) @@ -205,17 +211,21 @@ public void SetState(EntityUid? entity, AudioState state, bool force = false, Au case AudioState.Stopped: component.AudioStart = Timing.CurTime; component.PauseTime = null; + DirtyField(entity.Value, component, nameof(AudioComponent.AudioStart)); + DirtyField(entity.Value, component, nameof(AudioComponent.PauseTime)); component.StopPlaying(); RemComp(entity.Value); break; case AudioState.Paused: // Set it to current time so we can easily unpause it later. component.PauseTime = Timing.CurTime; + DirtyField(entity.Value, component, nameof(AudioComponent.PauseTime)); component.Pause(); RemComp(entity.Value); break; case AudioState.Playing: component.PauseTime = null; + DirtyField(entity.Value, component, nameof(AudioComponent.PauseTime)); component.StartPlaying(); // Reset TimedDespawn so the audio still gets cleaned up. @@ -230,7 +240,7 @@ public void SetState(EntityUid? entity, AudioState state, bool force = false, Au } component.State = state; - Dirty(entity.Value, component); + DirtyField(entity.Value, component, nameof(AudioComponent.State)); } protected void SetZOffset(float value) @@ -375,7 +385,7 @@ public void SetVolume(EntityUid? entity, float value, AudioComponent? component component.Params.Volume = value; component.Volume = value; - Dirty(entity.Value, component); + DirtyField(entity.Value, component, nameof(AudioComponent.Params)); } #endregion diff --git a/Robust.Shared/GameObjects/ComponentFactory.cs b/Robust.Shared/GameObjects/ComponentFactory.cs index b95c7a1668d..9ae6b914320 100644 --- a/Robust.Shared/GameObjects/ComponentFactory.cs +++ b/Robust.Shared/GameObjects/ComponentFactory.cs @@ -175,6 +175,41 @@ private static string CalculateComponentName(Type type) return name; } + /// + public void RegisterNetworkedFields(params string[] fields) where T : IComponent + { + var compReg = GetRegistration(CompIdx.Index()); + RegisterNetworkedFields(compReg, fields); + } + + /// + public void RegisterNetworkedFields(ComponentRegistration compReg, params string[] fields) + { + // Nothing to do. + if (compReg.NetworkedFields.Length > 0 || fields.Length == 0) + return; + + DebugTools.Assert(fields.Length <= 32); + + if (fields.Length > 32) + { + throw new NotSupportedException( + "Components with more than 32 networked fields unsupported! Consider splitting it up or making a pr for 64-bit flags"); + } + + compReg.NetworkedFields = fields; + var lookup = new Dictionary(fields.Length); + var i = 0; + + foreach (var field in fields) + { + lookup[field] = i; + i++; + } + + compReg.NetworkedFieldLookup = lookup.ToFrozenDictionary(); + } + public void IgnoreMissingComponents(string postfix = "") { if (_ignoreMissingComponentPostfix != null && _ignoreMissingComponentPostfix != postfix) diff --git a/Robust.Shared/GameObjects/ComponentRegistration.cs b/Robust.Shared/GameObjects/ComponentRegistration.cs index 84947c40d5f..4a5ef87fd39 100644 --- a/Robust.Shared/GameObjects/ComponentRegistration.cs +++ b/Robust.Shared/GameObjects/ComponentRegistration.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using Robust.Shared.Collections; using Robust.Shared.GameStates; @@ -39,6 +40,13 @@ public sealed class ComponentRegistration /// public Type Type { get; } + /// + /// Fields that are networked for this component. Used for delta states. + /// + public string[] NetworkedFields = []; + + public FrozenDictionary NetworkedFieldLookup = FrozenDictionary.Empty; + // Internal for sandboxing. // Avoid content passing an instance of this to ComponentFactory to get any type they want instantiated. internal ComponentRegistration(string name, Type type, CompIdx idx, bool unsaved = false) diff --git a/Robust.Shared/GameObjects/Components/CollisionWakeComponent.cs b/Robust.Shared/GameObjects/Components/CollisionWakeComponent.cs index 3e548f705f7..35b3265fd9c 100644 --- a/Robust.Shared/GameObjects/Components/CollisionWakeComponent.cs +++ b/Robust.Shared/GameObjects/Components/CollisionWakeComponent.cs @@ -1,30 +1,15 @@ -using System; using Robust.Shared.GameStates; -using Robust.Shared.IoC; -using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; -namespace Robust.Shared.GameObjects -{ - /// - /// An optimisation component for stuff that should be set as collidable when it's awake and non-collidable when asleep. - /// - [RegisterComponent, NetworkedComponent()] - [Access(typeof(CollisionWakeSystem))] - public sealed partial class CollisionWakeComponent : Component - { - [DataField("enabled")] - public bool Enabled = true; - - [Serializable, NetSerializable] - public sealed class CollisionWakeState : ComponentState - { - public bool Enabled { get; } +namespace Robust.Shared.GameObjects; - public CollisionWakeState(bool enabled) - { - Enabled = enabled; - } - } - } +/// +/// An optimisation component for stuff that should be set as collidable when it's awake and non-collidable when asleep. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(CollisionWakeSystem))] +public sealed partial class CollisionWakeComponent : Component +{ + [DataField, AutoNetworkedField] + public bool Enabled = true; } diff --git a/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs b/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs index b4870fa04ba..467c2ced976 100644 --- a/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs +++ b/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs @@ -24,26 +24,26 @@ public sealed partial class EyeComponent : Component /// that without messing with the main viewport's eye. This is important as there are some overlays that are /// only be drawn if that viewport's eye belongs to the currently controlled entity. /// - [ViewVariables, DataField("target"), AutoNetworkedField] + [DataField, AutoNetworkedField] public EntityUid? Target; - [ViewVariables(VVAccess.ReadWrite), DataField("drawFov"), AutoNetworkedField] + [DataField, AutoNetworkedField] public bool DrawFov = true; [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] public bool DrawLight = true; // yes it's not networked, don't ask. - [ViewVariables(VVAccess.ReadWrite), DataField("rotation")] + [ViewVariables(VVAccess.ReadWrite), DataField] public Angle Rotation; - [ViewVariables(VVAccess.ReadWrite), DataField("zoom")] + [ViewVariables(VVAccess.ReadWrite), DataField] public Vector2 Zoom = Vector2.One; /// /// Eye offset, relative to the map, and not affected by /// - [ViewVariables(VVAccess.ReadWrite), DataField("offset"), AutoNetworkedField] + [DataField, AutoNetworkedField] public Vector2 Offset; /// diff --git a/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs b/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs new file mode 100644 index 00000000000..56abd48a660 --- /dev/null +++ b/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs @@ -0,0 +1,61 @@ +using Robust.Shared.Timing; + +namespace Robust.Shared.GameObjects; + +public abstract partial class EntityManager +{ + public void DirtyField(EntityUid uid, IComponentDelta comp, string fieldName, MetaDataComponent? metadata = null) + { + // TODO + var compReg = ComponentFactory.GetRegistration(comp); + + if (!compReg.NetworkedFieldLookup.TryGetValue(fieldName, out var idx)) + { + _sawmill.Error($"Tried to dirty delta field {fieldName} on {ToPrettyString(uid)} that isn't implemented."); + return; + } + + var curTick = _gameTiming.CurTick; + comp.LastFieldUpdate = curTick; + comp.LastModifiedFields[idx] = curTick; + Dirty(uid, comp, metadata); + } + + public void DirtyField(EntityUid uid, T comp, string fieldName, MetaDataComponent? metadata = null) + where T : IComponentDelta + { + var compReg = ComponentFactory.GetRegistration(CompIdx.Index()); + + if (!compReg.NetworkedFieldLookup.TryGetValue(fieldName, out var idx)) + { + _sawmill.Error($"Tried to dirty delta field {fieldName} on {ToPrettyString(uid)} that isn't implemented."); + return; + } + + var curTick = _gameTiming.CurTick; + comp.LastFieldUpdate = curTick; + comp.LastModifiedFields[idx] = curTick; + Dirty(uid, comp, metadata); + } +} + +/// +/// Indicates this component supports delta states. +/// +public partial interface IComponentDelta : IComponent +{ + // TODO: This isn't entirely robust but not sure how else to handle this? + /// + /// Track last time a field was dirtied. if the full component dirty exceeds this then we send a full state update. + /// + public GameTick LastFieldUpdate { get; set; } + + /// + /// Stores the last modified tick for fields. + /// + public GameTick[] LastModifiedFields + { + get; + set; + } +} diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index 2ab8298cd4e..69371ebb80d 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -390,6 +390,13 @@ private void AddComponentInternal(EntityUid uid, T component, ComponentRegist metadata.NetComponents.Add(netId, component); } + if (component is IComponentDelta delta) + { + var curTick = _gameTiming.CurTick; + delta.LastModifiedFields = new GameTick[reg.NetworkedFields.Length]; + Array.Fill(delta.LastModifiedFields, curTick); + } + component.Networked = reg.NetID != null; var eventArgs = new AddedComponentEventArgs(new ComponentEventArgs(component, uid), reg); @@ -734,7 +741,7 @@ public bool HasComponent(EntityUid uid) where T : IComponent /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasComponent(EntityUid? uid) where T : IComponent + public bool HasComponent([NotNullWhen(true)] EntityUid? uid) where T : IComponent { return uid.HasValue && HasComponent(uid.Value); } @@ -749,7 +756,7 @@ public bool HasComponent(EntityUid uid, Type type) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasComponent(EntityUid? uid, Type type) + public bool HasComponent([NotNullWhen(true)] EntityUid? uid, Type type) { if (!uid.HasValue) { @@ -772,7 +779,7 @@ public bool HasComponent(EntityUid uid, ushort netId, MetaDataComponent? meta = /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasComponent(EntityUid? uid, ushort netId, MetaDataComponent? meta = null) + public bool HasComponent([NotNullWhen(true)] EntityUid? uid, ushort netId, MetaDataComponent? meta = null) { if (!uid.HasValue) { @@ -1596,7 +1603,7 @@ public bool TryComp([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] - public bool HasComp(EntityUid? uid) => HasComponent(uid); + public bool HasComp([NotNullWhen(true)] EntityUid? uid) => HasComponent(uid); [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] @@ -1607,7 +1614,7 @@ public bool HasComponent(EntityUid uid) [MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure] - public bool HasComponent(EntityUid? uid) + public bool HasComponent([NotNullWhen(true)] EntityUid? uid) { return uid != null && HasComponent(uid.Value); } diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index fa9b6bc8730..dc65b0c2af4 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -158,6 +158,29 @@ protected void Dirty(EntityUid uid, IComponent component, MetaDataComponent? met EntityManager.Dirty(uid, component, meta); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void DirtyField(EntityUid uid, IComponentDelta delta, string fieldName, MetaDataComponent? meta = null) + { + EntityManager.DirtyField(uid, delta, fieldName, meta); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void DirtyField(Entity entity, string fieldName, MetaDataComponent? meta = null) + where T : IComponentDelta + { + if (!Resolve(entity.Owner, ref entity.Comp)) + return; + + EntityManager.DirtyField(entity.Owner, entity.Comp, fieldName, meta); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void DirtyField(EntityUid uid, T component, string fieldName, MetaDataComponent? meta = null) + where T : IComponentDelta + { + EntityManager.DirtyField(uid, component, fieldName, meta); + } + /// /// Marks a component as dirty. This also implicitly dirties the entity this component belongs to. /// diff --git a/Robust.Shared/GameObjects/IComponentFactory.cs b/Robust.Shared/GameObjects/IComponentFactory.cs index e060c94d955..4490a549459 100644 --- a/Robust.Shared/GameObjects/IComponentFactory.cs +++ b/Robust.Shared/GameObjects/IComponentFactory.cs @@ -77,6 +77,10 @@ public interface IComponentFactory /// The availability of the component. ComponentAvailability GetComponentAvailability(string componentName, bool ignoreCase = false); + public void RegisterNetworkedFields(params string[] fields) where T : IComponent; + + public void RegisterNetworkedFields(ComponentRegistration compReg, params string[] fields); + /// /// Slow-path for Type -> CompIdx mapping without generics. /// diff --git a/Robust.Shared/GameObjects/IEntityManager.Network.cs b/Robust.Shared/GameObjects/IEntityManager.Network.cs index 5cbeb6fd885..ec5d6054757 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Network.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Network.cs @@ -6,6 +6,11 @@ namespace Robust.Shared.GameObjects; public partial interface IEntityManager { + public void DirtyField(EntityUid uid, IComponentDelta delta, string fieldName, MetaDataComponent? metadata = null); + + public void DirtyField(EntityUid uid, T component, string fieldName, MetaDataComponent? metadata = null) + where T : IComponentDelta; + /// /// Tries to parse a string as a NetEntity and return the relevant EntityUid. /// diff --git a/Robust.Shared/GameObjects/Systems/CollisionWakeSystem.cs b/Robust.Shared/GameObjects/Systems/CollisionWakeSystem.cs index e245644223d..e912d5caba2 100644 --- a/Robust.Shared/GameObjects/Systems/CollisionWakeSystem.cs +++ b/Robust.Shared/GameObjects/Systems/CollisionWakeSystem.cs @@ -1,4 +1,3 @@ -using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Physics; @@ -17,9 +16,6 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnRemove); - SubscribeLocalEvent(OnGetState); - SubscribeLocalEvent(OnHandleState); - SubscribeLocalEvent(OnJointAdd); SubscribeLocalEvent(OnJointRemove); @@ -43,22 +39,6 @@ public void SetEnabled(EntityUid uid, bool enabled, CollisionWakeComponent? comp Dirty(uid, component); } - private void OnHandleState(EntityUid uid, CollisionWakeComponent component, ref ComponentHandleState args) - { - if (args.Current is CollisionWakeComponent.CollisionWakeState state) - component.Enabled = state.Enabled; - - // Note, this explicitly does not update PhysicsComponent.CanCollide. The physics component should perform - // its own state-handling logic. Additionally, if we wanted to set it you would have to ensure that things - // like the join-component and physics component have already handled their states, otherwise CanCollide may - // be set incorrectly and leave the client with a bad state. - } - - private void OnGetState(EntityUid uid, CollisionWakeComponent component, ref ComponentGetState args) - { - args.State = new CollisionWakeComponent.CollisionWakeState(component.Enabled); - } - private void OnRemove(EntityUid uid, CollisionWakeComponent component, ComponentShutdown args) { if (component.Enabled diff --git a/Robust.Shared/GameObjects/Systems/SharedEyeSystem.cs b/Robust.Shared/GameObjects/Systems/SharedEyeSystem.cs index 2091e271485..6ee530c9ec9 100644 --- a/Robust.Shared/GameObjects/Systems/SharedEyeSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedEyeSystem.cs @@ -63,7 +63,7 @@ public void SetOffset(EntityUid uid, Vector2 value, EyeComponent? eyeComponent = eyeComponent.Offset = value; eyeComponent.Eye.Offset = value; - Dirty(uid, eyeComponent); + DirtyField(uid, eyeComponent, nameof(EyeComponent.Offset)); } public void SetDrawFov(EntityUid uid, bool value, EyeComponent? eyeComponent = null) @@ -76,7 +76,7 @@ public void SetDrawFov(EntityUid uid, bool value, EyeComponent? eyeComponent = n eyeComponent.DrawFov = value; eyeComponent.Eye.DrawFov = value; - Dirty(uid, eyeComponent); + DirtyField(uid, eyeComponent, nameof(EyeComponent.DrawFov)); } public void SetDrawLight(Entity entity, bool value) @@ -89,7 +89,7 @@ public void SetDrawLight(Entity entity, bool value) entity.Comp.DrawLight = value; entity.Comp.Eye.DrawLight = value; - Dirty(entity); + DirtyField(entity, nameof(EyeComponent.DrawLight)); } public void SetRotation(EntityUid uid, Angle rotation, EyeComponent? eyeComponent = null) @@ -131,7 +131,7 @@ public void SetTarget(EntityUid uid, EntityUid? value, EyeComponent? eyeComponen } eyeComponent.Target = value; - Dirty(uid, eyeComponent); + DirtyField(uid, eyeComponent, nameof(EyeComponent.Target)); } public void SetZoom(EntityUid uid, Vector2 value, EyeComponent? eyeComponent = null) @@ -172,6 +172,6 @@ public void SetVisibilityMask(EntityUid uid, int value, EyeComponent? eyeCompone return; eyeComponent.VisibilityMask = value; - Dirty(uid, eyeComponent); + DirtyField(uid, eyeComponent, nameof(EyeComponent.VisibilityMask)); } } diff --git a/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs b/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs index 4e620399b2c..79da7944bef 100644 --- a/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs +++ b/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs @@ -30,13 +30,17 @@ using Robust.Shared.Physics.Dynamics.Contacts; using Robust.Shared.Physics.Systems; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Timing; using Robust.Shared.ViewVariables; namespace Robust.Shared.Physics.Components; [RegisterComponent, NetworkedComponent] -public sealed partial class PhysicsComponent : Component +public sealed partial class PhysicsComponent : Component, IComponentDelta { + public GameTick LastFieldUpdate { get; set; } + public GameTick[] LastModifiedFields { get; set; } + /// /// Has this body been added to an island previously in this tick. /// @@ -57,10 +61,10 @@ public sealed partial class PhysicsComponent : Component /// internal readonly LinkedList Contacts = new(); - [DataField("ignorePaused"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public bool IgnorePaused; - [DataField("bodyType"), Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)] + [DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)] public BodyType BodyType = BodyType.Static; // We'll also block Static bodies from ever being awake given they don't need to move. @@ -74,13 +78,11 @@ public sealed partial class PhysicsComponent : Component /// body will be woken. /// /// true if sleeping is allowed; otherwise, false. - [ViewVariables(VVAccess.ReadWrite), DataField("sleepingAllowed"), - Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, + [DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)] public bool SleepingAllowed = true; - [ViewVariables(VVAccess.ReadWrite), DataField("sleepTime"), - Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, + [DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)] public float SleepTime = 0f; @@ -90,8 +92,7 @@ public sealed partial class PhysicsComponent : Component /// /// Also known as Enabled in Box2D /// - [ViewVariables(VVAccess.ReadWrite), DataField("canCollide"), - Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, + [DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)] public bool CanCollide = true; @@ -168,7 +169,7 @@ public sealed partial class PhysicsComponent : Component /// /// Is the body allowed to have angular velocity. /// - [ViewVariables(VVAccess.ReadWrite), DataField("fixedRotation"), + [ViewVariables(VVAccess.ReadWrite), DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)] public bool FixedRotation = true; @@ -189,7 +190,7 @@ public sealed partial class PhysicsComponent : Component /// The force is applied to the center of mass. /// https://en.wikipedia.org/wiki/Force /// - [ViewVariables(VVAccess.ReadWrite), DataField("force"), + [ViewVariables(VVAccess.ReadWrite), DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)] public Vector2 Force; @@ -200,7 +201,7 @@ public sealed partial class PhysicsComponent : Component /// The torque rotates around the Z axis on the object. /// https://en.wikipedia.org/wiki/Torque /// - [ViewVariables(VVAccess.ReadWrite), DataField("torque"), + [ViewVariables(VVAccess.ReadWrite), DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)] public float Torque; @@ -216,7 +217,7 @@ public sealed partial class PhysicsComponent : Component /// This is a set amount that the body's linear velocity is reduced by every tick. /// Combined with the tile friction. /// - [ViewVariables(VVAccess.ReadWrite), DataField("linearDamping"), + [DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)] public float LinearDamping = 0.2f; @@ -226,7 +227,7 @@ public sealed partial class PhysicsComponent : Component /// Combined with the tile friction. /// /// - [ViewVariables(VVAccess.ReadWrite), DataField("angularDamping"), + [DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)] public float AngularDamping = 0.2f; @@ -260,7 +261,7 @@ public sealed partial class PhysicsComponent : Component /// /// The current status of the object /// - [ViewVariables(VVAccess.ReadWrite), DataField("bodyStatus"), Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)] + [DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)] public BodyStatus BodyStatus { get; set; } [ViewVariables, Access(typeof(SharedPhysicsSystem))] diff --git a/Robust.Shared/Physics/Components/PhysicsComponentState.cs b/Robust.Shared/Physics/Components/PhysicsComponentState.cs index 29080b4a0b8..29bb3ed3b99 100644 --- a/Robust.Shared/Physics/Components/PhysicsComponentState.cs +++ b/Robust.Shared/Physics/Components/PhysicsComponentState.cs @@ -1,35 +1,180 @@ using System; +using System.Collections.Generic; using System.Numerics; using Robust.Shared.GameObjects; -using Robust.Shared.Maths; using Robust.Shared.Serialization; namespace Robust.Shared.Physics.Components; +/// +/// Average use-case of only linear velocity update +/// [Serializable, NetSerializable] -public readonly record struct PhysicsComponentState( - bool CanCollide, - bool SleepingAllowed, - bool FixedRotation, - BodyStatus Status, - Vector2 LinearVelocity, - float AngularVelocity, - BodyType BodyType, - float Friction, - float LinearDamping, - float AngularDamping) - : IComponentState +public record struct PhysicsLinearVelocityDeltaState : IComponentDeltaState { - public readonly bool CanCollide = CanCollide; - public readonly bool SleepingAllowed = SleepingAllowed; - public readonly bool FixedRotation = FixedRotation; - public readonly BodyStatus Status = Status; - - public readonly Vector2 LinearVelocity = LinearVelocity; - public readonly float AngularVelocity = AngularVelocity; - public readonly BodyType BodyType = BodyType; - - public readonly float Friction = Friction; - public readonly float LinearDamping = LinearDamping; - public readonly float AngularDamping = AngularDamping; + public Vector2 LinearVelocity; + + public void ApplyToFullState(PhysicsComponentState fullState) + { + fullState.LinearVelocity = LinearVelocity; + } + + public PhysicsComponentState CreateNewFullState(PhysicsComponentState fullState) + { + var copy = new PhysicsComponentState(fullState) + { + LinearVelocity = LinearVelocity, + }; + return copy; + } +} + +/// +/// 2nd-most typical usecase of just velocity updates +/// +[Serializable, NetSerializable] +public record struct PhysicsVelocityDeltaState : IComponentDeltaState +{ + public Vector2 LinearVelocity; + public float AngularVelocity; + + public void ApplyToFullState(PhysicsComponentState fullState) + { + fullState.LinearVelocity = LinearVelocity; + fullState.AngularVelocity = AngularVelocity; + } + + public PhysicsComponentState CreateNewFullState(PhysicsComponentState fullState) + { + var copy = new PhysicsComponentState(fullState) + { + LinearVelocity = LinearVelocity, + AngularVelocity = AngularVelocity + }; + return copy; + } +} + +/// +/// Slower delta state that can include all fields. +/// +[Serializable, NetSerializable] +public record struct PhysicsDeltaState() : IComponentDeltaState +{ + public uint ModifiedFields = 0; + public object?[] Fields = Array.Empty(); + + public void ApplyToFullState(PhysicsComponentState fullState) + { + ApplyTo(fullState); + } + + public PhysicsComponentState CreateNewFullState(PhysicsComponentState fullState) + { + var copy = new PhysicsComponentState(fullState); + ApplyTo(copy); + return copy; + } + + private void ApplyTo(PhysicsComponentState state) + { + byte index = 0; + + for (var i = 0; i < 12; i++) + { + var field = 1 << i; + + if ((ModifiedFields & field) == 0x0) + continue; + + var value = Fields[index]; + + switch (i) + { + // See SharedPhysicsSystem. + // If there's an easy way to get a compreg here then we could debug assert. + case 0: + state.CanCollide = (bool)value!; + break; + case 1: + state.Status = (BodyStatus)value!; + break; + case 2: + state.BodyType = (BodyType)value!; + break; + case 3: + state.SleepingAllowed = (bool)value!; + break; + case 4: + state.FixedRotation = (bool)value!; + break; + case 5: + state.Friction = (float)value!; + break; + case 6: + state.Force = (Vector2)value!; + break; + case 7: + state.Torque = (float)value!; + break; + case 8: + state.LinearDamping = (float)value!; + break; + case 9: + state.AngularDamping = (float)value!; + break; + case 10: + state.AngularVelocity = (float)value!; + break; + case 11: + state.LinearVelocity = (Vector2)value!; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + index++; + } + } +} + +[Serializable, NetSerializable] +public sealed class PhysicsComponentState : IComponentState +{ + public bool CanCollide; + public bool SleepingAllowed; + public bool FixedRotation; + public BodyStatus Status; + + public Vector2 LinearVelocity; + public float AngularVelocity; + public BodyType BodyType; + + public float Friction; + public float LinearDamping; + public float AngularDamping; + + public Vector2 Force; + public float Torque; + + public PhysicsComponentState() {} + + public PhysicsComponentState(PhysicsComponentState existing) + { + CanCollide = existing.CanCollide; + SleepingAllowed = existing.SleepingAllowed; + FixedRotation = existing.FixedRotation; + Status = existing.Status; + + LinearVelocity = existing.LinearVelocity; + AngularVelocity = existing.AngularVelocity; + BodyType = existing.BodyType; + + Friction = existing.Friction; + LinearDamping = existing.LinearDamping; + AngularDamping = existing.AngularDamping; + + Force = existing.Force; + Torque = existing.Torque; + } } diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs index 730f5e18ebf..82163bcc340 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs @@ -23,7 +23,9 @@ */ using System; +using System.Collections.Generic; using System.Numerics; +using Robust.Shared.Collections; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Map; @@ -69,39 +71,219 @@ private void OnPhysicsInit(EntityUid uid, PhysicsComponent component, ComponentI private void OnPhysicsGetState(EntityUid uid, PhysicsComponent component, ref ComponentGetState args) { - args.State = new PhysicsComponentState( - component.CanCollide, - component.SleepingAllowed, - component.FixedRotation, - component.BodyStatus, - component.LinearVelocity, - component.AngularVelocity, - component.BodyType, - component._friction, - component.LinearDamping, - component.AngularDamping); + if (args.FromTick > component.CreationTick && component.LastFieldUpdate >= args.FromTick) + { + var slowPath = false; + + for (var i = 0; i < _angularVelocityIndex; i++) + { + var field = component.LastModifiedFields[i]; + + if (field < args.FromTick) + continue; + + slowPath = true; + break; + } + + // We can do a smaller delta with no list index overhead. + if (!slowPath) + { + var angularDirty = component.LastModifiedFields[_angularVelocityIndex] >= args.FromTick; + + if (angularDirty) + { + args.State = new PhysicsVelocityDeltaState() + { + AngularVelocity = component.AngularVelocity, + LinearVelocity = component.LinearVelocity, + }; + } + else + { + args.State = new PhysicsLinearVelocityDeltaState() + { + LinearVelocity = component.LinearVelocity, + }; + } + + return; + } + + // Slowpath :( + uint fields = 0; + var data = new ValueList(); + + for (byte i = 0; i < component.LastModifiedFields.Length; i++) + { + var lastUpdate = component.LastModifiedFields[i]; + + if (lastUpdate < args.FromTick) + continue; + + fields |= (uint) (1 << i); + + switch (i) + { + case 0: + data.Add(component.CanCollide); + break; + case 1: + data.Add(component.BodyStatus); + break; + case 2: + data.Add(component.BodyType); + break; + case 3: + data.Add(component.SleepingAllowed); + break; + case 4: + data.Add(component.FixedRotation); + break; + case 5: + data.Add(component._friction); + break; + case 6: + data.Add(component.Force); + break; + case 7: + data.Add(component.Torque); + break; + case 8: + data.Add(component.LinearDamping); + break; + case 9: + data.Add(component.AngularDamping); + break; + case 10: + data.Add(component.AngularVelocity); + break; + case 11: + data.Add(component.LinearVelocity); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + args.State = new PhysicsDeltaState() + { + ModifiedFields = fields, + Fields = data.ToArray(), + }; + return; + } + + args.State = new PhysicsComponentState + { + CanCollide = component.CanCollide, + SleepingAllowed = component.SleepingAllowed, + FixedRotation = component.FixedRotation, + Status = component.BodyStatus, + LinearVelocity = component.LinearVelocity, + AngularVelocity = component.AngularVelocity, + BodyType = component.BodyType, + Friction = component._friction, + LinearDamping = component.LinearDamping, + AngularDamping = component.AngularDamping, + Force = component.Force, + Torque = component.Torque, + }; } private void OnPhysicsHandleState(EntityUid uid, PhysicsComponent component, ref ComponentHandleState args) { - if (args.Current is not PhysicsComponentState newState) + if (args.Current == null) return; - SetSleepingAllowed(uid, component, newState.SleepingAllowed); - SetFixedRotation(uid, newState.FixedRotation, body: component); - SetCanCollide(uid, newState.CanCollide, body: component); - component.BodyStatus = newState.Status; - // So transform doesn't apply MapId in the HandleComponentState because ??? so MapId can still be 0. // Fucking kill me, please. You have no idea deep the rabbit hole of shitcode goes to make this work. - TryComp(uid, out var manager); + _fixturesQuery.TryComp(uid, out var manager); + + if (args.Current is PhysicsLinearVelocityDeltaState linearState) + { + SetLinearVelocity(uid, linearState.LinearVelocity, body: component, manager: manager); + } + else if (args.Current is PhysicsVelocityDeltaState velocityState) + { + SetLinearVelocity(uid, velocityState.LinearVelocity, body: component, manager: manager); + SetAngularVelocity(uid, velocityState.AngularVelocity, body: component, manager: manager); + } + else if (args.Current is PhysicsDeltaState deltaState) + { + byte index = 0; + + for (var i = 0; i < 12; i++) + { + var field = 1 << i; + + // Field not dirty + if ((deltaState.ModifiedFields & field) == 0x0) + continue; + + var value = deltaState.Fields[index]; + + switch (i) + { + case 0: + SetCanCollide(uid, (bool)value!, body: component); + break; + case 1: + component.BodyStatus = (BodyStatus)value!; + break; + case 2: + SetBodyType(uid, (BodyType)value!, manager, component); + break; + case 3: + SetSleepingAllowed(uid, component, (bool)value!); + break; + case 4: + SetFixedRotation(uid, (bool)value!, body: component); + break; + case 5: + SetFriction(uid, component, (float)value!); + break; + case 6: + component.Force = (Vector2)value!; + break; + case 7: + component.Torque = (float)value!; + break; + case 8: + SetLinearDamping(uid, component, (float)value!); + break; + case 9: + SetAngularDamping(uid, component, (float)value!); + break; + case 10: + SetAngularVelocity(uid, (float)value!, body: component, manager: manager); + break; + case 11: + SetLinearVelocity(uid, (Vector2)value!, body: component, manager: manager); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + index++; + } + } + else if (args.Current is PhysicsComponentState newState) + { + SetSleepingAllowed(uid, component, newState.SleepingAllowed); + SetFixedRotation(uid, newState.FixedRotation, body: component); + SetCanCollide(uid, newState.CanCollide, body: component); + component.BodyStatus = newState.Status; - SetLinearVelocity(uid, newState.LinearVelocity, body: component, manager: manager); - SetAngularVelocity(uid, newState.AngularVelocity, body: component, manager: manager); - SetBodyType(uid, newState.BodyType, manager, component); - SetFriction(uid, component, newState.Friction); - SetLinearDamping(uid, component, newState.LinearDamping); - SetAngularDamping(uid, component, newState.AngularDamping); + SetLinearVelocity(uid, newState.LinearVelocity, body: component, manager: manager); + SetAngularVelocity(uid, newState.AngularVelocity, body: component, manager: manager); + SetBodyType(uid, newState.BodyType, manager, component); + SetFriction(uid, component, newState.Friction); + SetLinearDamping(uid, component, newState.LinearDamping); + SetAngularDamping(uid, component, newState.AngularDamping); + component.Force = newState.Force; + component.Torque = newState.Torque; + } } #endregion @@ -132,7 +314,6 @@ public void ApplyForce(EntityUid uid, Vector2 force, Vector2 point, FixturesComp body.Force += force; body.Torque += Vector2Helpers.Cross(point - body._localCenter, force); - Dirty(uid, body); } public void ApplyForce(EntityUid uid, Vector2 force, FixturesComponent? manager = null, PhysicsComponent? body = null) @@ -143,7 +324,6 @@ public void ApplyForce(EntityUid uid, Vector2 force, FixturesComponent? manager } body.Force += force; - Dirty(uid, body); } public void ApplyTorque(EntityUid uid, float torque, FixturesComponent? manager = null, PhysicsComponent? body = null) @@ -154,7 +334,7 @@ public void ApplyTorque(EntityUid uid, float torque, FixturesComponent? manager } body.Torque += torque; - Dirty(uid, body); + DirtyField(uid, body, nameof(PhysicsComponent.Torque)); } public void ApplyLinearImpulse(EntityUid uid, Vector2 impulse, FixturesComponent? manager = null, PhysicsComponent? body = null) @@ -212,34 +392,29 @@ public void DestroyContacts(PhysicsComponent body) /// public void ResetDynamics(EntityUid uid, PhysicsComponent body, bool dirty = true) { - var updated = false; - if (body.Torque != 0f) { body.Torque = 0f; - updated = true; + DirtyField(uid, body, nameof(PhysicsComponent.Torque)); } if (body.AngularVelocity != 0f) { body.AngularVelocity = 0f; - updated = true; + DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity)); } if (body.Force != Vector2.Zero) { body.Force = Vector2.Zero; - updated = true; + DirtyField(uid, body, nameof(PhysicsComponent.Force)); } if (body.LinearVelocity != Vector2.Zero) { body.LinearVelocity = Vector2.Zero; - updated = true; + DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity)); } - - if (updated && dirty) - Dirty(uid, body); } [Obsolete("Use overload that takes EntityUid")] @@ -281,7 +456,6 @@ public void ResetMassData(EntityUid uid, FixturesComponent? manager = null, Phys if (((int) body.BodyType & (int) (BodyType.Kinematic | BodyType.Static)) != 0) { body._localCenter = Vector2.Zero; - Dirty(uid, body); return; } @@ -315,8 +489,13 @@ public void ResetMassData(EntityUid uid, FixturesComponent? manager = null, Phys body._localCenter = localCenter; // Update center of mass velocity. - body.LinearVelocity += Vector2Helpers.Cross(body.AngularVelocity, localCenter - oldCenter); - Dirty(uid, body); + var comVelocityDiff = Vector2Helpers.Cross(body.AngularVelocity, localCenter - oldCenter); + + if (comVelocityDiff != Vector2.Zero) + { + body.LinearVelocity += comVelocityDiff; + DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity)); + } if (body._mass == oldMass && body._inertia == oldInertia && oldCenter == localCenter) return; @@ -346,9 +525,7 @@ public bool SetAngularVelocity(EntityUid uid, float value, bool dirty = true, Fi return false; body.AngularVelocity = value; - - if (dirty) - Dirty(uid, body); + DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity)); return true; } @@ -374,10 +551,7 @@ public bool SetLinearVelocity(EntityUid uid, Vector2 velocity, bool dirty = true return false; body.LinearVelocity = velocity; - - if (dirty) - Dirty(uid, body); - + DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity)); return true; } @@ -387,9 +561,7 @@ public void SetAngularDamping(EntityUid uid, PhysicsComponent body, float value, return; body.AngularDamping = value; - - if (dirty) - Dirty(uid, body); + DirtyField(uid, body, nameof(PhysicsComponent.AngularDamping)); } [Obsolete("Use overload that takes EntityUid")] @@ -404,9 +576,7 @@ public void SetLinearDamping(EntityUid uid, PhysicsComponent body, float value, return; body.LinearDamping = value; - - if (dirty) - Dirty(uid, body); + DirtyField(uid, body, nameof(PhysicsComponent.LinearDamping)); } [Obsolete("Use overload that takes EntityUid")] @@ -464,7 +634,6 @@ public void SetAwake(Entity ent, bool value, bool updateSleepT } UpdateMapAwakeState(uid, body); - Dirty(ent); } public void TrySetBodyType(EntityUid uid, BodyType value, FixturesComponent? manager = null, PhysicsComponent? body = null, TransformComponent? xform = null) @@ -492,8 +661,18 @@ public void SetBodyType(EntityUid uid, BodyType value, FixturesComponent? manage if (body.BodyType == BodyType.Static) { SetAwake((uid, body), false); - body.LinearVelocity = Vector2.Zero; - body.AngularVelocity = 0.0f; + + if (body.LinearVelocity != Vector2.Zero) + { + body.LinearVelocity = Vector2.Zero; + DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity)); + } + + if (body.AngularVelocity != 0f) + { + body.AngularVelocity = 0f; + DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity)); + } } // Even if it's dynamic if it can't collide then don't force it awake. else if (body.CanCollide) @@ -501,8 +680,11 @@ public void SetBodyType(EntityUid uid, BodyType value, FixturesComponent? manage SetAwake((uid, body), true); } - body.Force = Vector2.Zero; - body.Torque = 0.0f; + if (body.Torque != 0f) + { + body.Torque = 0f; + DirtyField(uid, body, nameof(PhysicsComponent.Torque)); + } _broadphase.RegenerateContacts(uid, body, manager, xform); @@ -519,9 +701,7 @@ public void SetBodyStatus(EntityUid uid, PhysicsComponent body, BodyStatus statu return; body.BodyStatus = status; - - if (dirty) - Dirty(uid, body); + DirtyField(uid, body, nameof(PhysicsComponent.BodyStatus)); } [Obsolete("Use overload that takes EntityUid")] @@ -578,10 +758,7 @@ public bool SetCanCollide( var ev = new CollisionChangeEvent(uid, body, value); RaiseLocalEvent(ref ev); } - - if (dirty) - Dirty(uid, body); - + DirtyField(uid, body, nameof(PhysicsComponent.CanCollide)); return value; } @@ -591,11 +768,15 @@ public void SetFixedRotation(EntityUid uid, bool value, bool dirty = true, Fixtu return; body.FixedRotation = value; - body.AngularVelocity = 0.0f; - ResetMassData(uid, manager: manager, body: body); + DirtyField(uid, body, nameof(PhysicsComponent.FixedRotation)); + + if (body.AngularVelocity != 0f) + { + body.AngularVelocity = 0.0f; + DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity)); + } - if (dirty) - Dirty(uid, body); + ResetMassData(uid, manager: manager, body: body); } public void SetFriction(EntityUid uid, PhysicsComponent body, float value, bool dirty = true) @@ -604,9 +785,7 @@ public void SetFriction(EntityUid uid, PhysicsComponent body, float value, bool return; body._friction = value; - - if (dirty) - Dirty(uid, body); + DirtyField(uid, body, nameof(PhysicsComponent.Friction)); } [Obsolete("Use overload that takes EntityUid")] @@ -628,9 +807,7 @@ public void SetInertia(EntityUid uid, PhysicsComponent body, float value, bool d body._inertia = value - body.Mass * Vector2.Dot(body._localCenter, body._localCenter); DebugTools.Assert(body._inertia > 0.0f); body.InvI = 1.0f / body._inertia; - - if (dirty) - Dirty(uid, body); + // Not networked } } @@ -647,6 +824,7 @@ public void SetLocalCenter(EntityUid uid, PhysicsComponent body, Vector2 value) if (value.EqualsApprox(body._localCenter)) return; body._localCenter = value; + // Not networked } public void SetSleepingAllowed(EntityUid uid, PhysicsComponent body, bool value, bool dirty = true) @@ -658,9 +836,7 @@ public void SetSleepingAllowed(EntityUid uid, PhysicsComponent body, bool value, SetAwake((uid, body), true); body.SleepingAllowed = value; - - if (dirty) - Dirty(uid, body); + DirtyField(uid, body, nameof(PhysicsComponent.SleepingAllowed)); } public void SetSleepTime(PhysicsComponent body, float value) diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.cs index 7ee351b792b..bb9c4c8109c 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.cs @@ -75,10 +75,32 @@ public abstract partial class SharedPhysicsSystem : EntitySystem protected EntityQuery PhysMapQuery; protected EntityQuery MapQuery; + private ComponentRegistration _physicsReg = default!; + private byte _angularVelocityIndex; + public override void Initialize() { base.Initialize(); + _physicsReg = EntityManager.ComponentFactory.GetRegistration(CompIdx.Index()); + + // If you update this then update the delta state + GetState + HandleState! + EntityManager.ComponentFactory.RegisterNetworkedFields(_physicsReg, + nameof(PhysicsComponent.CanCollide), + nameof(PhysicsComponent.BodyStatus), + nameof(PhysicsComponent.BodyType), + nameof(PhysicsComponent.SleepingAllowed), + nameof(PhysicsComponent.FixedRotation), + nameof(PhysicsComponent.Friction), + nameof(PhysicsComponent.Force), + nameof(PhysicsComponent.Torque), + nameof(PhysicsComponent.LinearDamping), + nameof(PhysicsComponent.AngularDamping), + nameof(PhysicsComponent.AngularVelocity), + nameof(PhysicsComponent.LinearVelocity)); + + _angularVelocityIndex = 10; + _fixturesQuery = GetEntityQuery(); PhysicsQuery = GetEntityQuery(); _xformQuery = GetEntityQuery(); diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.cs b/Robust.Shared/Serialization/Manager/SerializationManager.cs index 90b874a4e0d..89385ec2d30 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.cs @@ -70,7 +70,7 @@ IEnumerable GetImplicitTypes(Type type) { foreach (var child in _reflectionManager.GetAllChildren(type)) { - if (child.IsAbstract || child.IsGenericTypeDefinition) + if (child.IsAbstract || child.IsGenericTypeDefinition || child.IsInterface) continue; yield return child;