Compare commits

..

2 Commits

Author SHA1 Message Date
Pieter-Jan Briers
91d4b32c66 Version: 167.0.1 2024-03-10 20:50:30 +01:00
Pieter-Jan Briers
e788f03a28 Backport 859f150404
(cherry picked from commit 24d5c26fa6)
(cherry picked from commit 688efac67b634c613539b783a9fb6e679948cd53)
2024-03-10 20:50:30 +01:00
265 changed files with 3682 additions and 4793 deletions

3
.github/CODEOWNERS vendored
View File

@@ -7,6 +7,3 @@
**/Toolshed/** @moonheart08
*Command.cs @moonheart08
*Commands.cs @moonheart08
# Physics
**/Robust.Shared/Physics/** @metalgearsloth

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -54,151 +54,7 @@ END TEMPLATE-->
*None yet*
## 176.0.0
### Breaking changes
* Reverted audio rework temporarily until packaging is fixed.
* Changes to Robust.Packaging to facilitate Content.Packaging ports from the python packaging scripts.
### New features
* Add a cvar for max game state buffer size.
* Add an overload for GetEntitiesInRange that takes in a set.
### Bugfixes
* Fix PVS initial list capacity always being 0.
* Fix replay lerp error spam.
## 175.0.0
### Breaking changes
* Removed static SoundSystem.Play methods.
* Moved IPlayingAudioStream onto AudioComponent and entities instead of an abstract stream.
* IResourceCache is in shared and IClientResourceCache is the client version to use for textures.
* Default audio attenuation changed from InverseDistanceClamped to LinearDistanceClamped.
* Removed per-source audio attenuation.
### New features
* Add preliminary support for EFX Reverb presets + auxiliary slots; these are also entities.
* Audio on grid entities is now attached to the grid.
### Bugfixes
* If an audio entity comes into PVS range its track will start at the relevant offset and not the beginning.
* Z-Axis offset is considered for ReferenceDistance / MaxDistance for audio.
* Audio will now pause if the attached entity is paused.
### Other
* Changed audio Z-Axis offset from -5m to -1m.
## 174.0.0
### Breaking changes
* ActorComponent has been moved to `Robust.Shared.Player` (namespace changed).
### New features
* Added `SpriteSystem.GetFrame()` method, which takes in an animated RSI and a time and returns a frame/texture.
* Added `IRobustRandom.NextAngle()`
## 173.1.0
### New features
* Add physics chain shapes from Box2D.
## 173.0.0
### Breaking changes
* Remove GridModifiedEvent in favor of TileChangedEvent.
### Bugfixes
* Fix some grid rendering bugs where chunks don't get destroyed correctly.
## 172.0.0
### Breaking changes
* Remove TryLifestage helper methods.
* Refactor IPlayerManager to remove more IPlayerSession, changed PlayerAttachedEvent etc on client to have the Local prefix, and shuffled namespaces around.
### New features
* Add EnsureComponent(ref Entity<\T?>)
### Bugfixes
* Re-add force ask threshold and fix other PVS bugs.
## 171.0.0
### Breaking changes
* Change PlaceNextTo method names to be more descriptive.
* Rename RefreshRelay for joints to SetRelay to match its behaviour.
### Bugfixes
* Fix PVS error spam for joint relays not being cleaned up.
### Other
* Set EntityLastModifiedTick on entity spawn.
## 170.0.0
### Breaking changes
* Removed obsolete methods and properties in VisibilitySystem, SharedContainerSystem and MetaDataComponent.
### Bugfixes
* Fixed duplicate command error.
* Fixed not being able to delete individual entities with the delete command.
### Other
* FileLogHandler logs can now be deleted while the engine is running.
## 169.0.1
### Other
* The client now knows about registered server-side toolshed commands.
## 169.0.0
### Breaking changes
* Entity<T> has been introduced to hold a component and its owning entity. Some methods that returned and accepted components directly have been removed or obsoleted to reflect this.
### Other
* By-value events may now be subscribed to by-ref.
* The manifest's assemblyPrefix value is now respected on the server.
## 168.0.0
### Breaking changes
* The Component.OnRemove method has been removed. Use SubscribeLocalEvent<TComp, ComponentRemove>(OnRemove) from an EntitySystem instead.
## 167.0.1
## 167.0.0
@@ -245,7 +101,7 @@ END TEMPLATE-->
### New features
* The YAML validator now checks the default values of ProtoId<T> and EntProtoId data fields.
* The YAML validator now checks the default values of ProtoId<T> and EntProtoId data fields.
### Bugfixes

View File

@@ -17,15 +17,15 @@ cmd-error-dir-not-found = Could not find directory: {$dir}.
cmd-failure-no-attached-entity = There is no entity attached to this shell.
## 'help' command
cmd-help-desc = Display general help or help text for a specific command
cmd-help-help = Usage: help [command name]
cmd-oldhelp-desc = Display general help or help text for a specific command
cmd-oldhelp-help = Usage: help [command name]
When no command name is provided, displays general-purpose help text. If a command name is provided, displays help text for that command.
cmd-help-no-args = To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'. To search for commands, use 'list <filter>'.
cmd-help-unknown = Unknown command: { $command }
cmd-help-top = { $command } - { $description }
cmd-help-invalid-args = Invalid amount of arguments.
cmd-help-arg-cmdname = [command name]
cmd-oldhelp-no-args = To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'. To search for commands, use 'list <filter>'.
cmd-oldhelp-unknown = Unknown command: { $command }
cmd-oldhelp-top = { $command } - { $description }
cmd-oldhelp-invalid-args = Invalid amount of arguments.
cmd-oldhelp-arg-cmdname = [command name]
## 'cvar' command
cmd-cvar-desc = Gets or sets a CVar.

View File

@@ -23,6 +23,16 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
"Make sure that methods subscribing to a ref event have the ref keyword for the event argument."
);
private static readonly DiagnosticDescriptor ByValueEventSubscribedByRefRule = new(
Diagnostics.IdValueEventRaisedByRef,
"Value event subscribed to by-ref",
"Tried to subscribe to a value event '{0}' by-ref.",
"Usage",
DiagnosticSeverity.Error,
true,
"Make sure that methods subscribing to value events do not have the ref keyword for the event argument."
);
private static readonly DiagnosticDescriptor ByRefEventRaisedByValueRule = new(
Diagnostics.IdByRefEventRaisedByValue,
"By-ref event raised by value",
@@ -45,6 +55,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
ByRefEventSubscribedByValueRule,
ByValueEventSubscribedByRefRule,
ByRefEventRaisedByValueRule,
ByValueEventRaisedByRefRule
);
@@ -53,9 +64,71 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterOperationAction(CheckEventSubscription, OperationKind.Invocation);
context.RegisterOperationAction(CheckEventRaise, OperationKind.Invocation);
}
private void CheckEventSubscription(OperationAnalysisContext context)
{
if (context.Operation is not IInvocationOperation operation)
return;
var subscribeMethods = context.Compilation
.GetTypeByMetadataName("Robust.Shared.GameObjects.EntitySystem")?
.GetMembers()
.Where(m => m.Name.Contains("SubscribeLocalEvent"))
.Cast<IMethodSymbol>();
if (subscribeMethods == null)
return;
if (!subscribeMethods.Any(m => m.Equals(operation.TargetMethod.OriginalDefinition, Default)))
return;
var typeArguments = operation.TargetMethod.TypeArguments;
if (typeArguments.Length < 1 || typeArguments.Length > 2)
return;
if (operation.Arguments.First().Value is not IDelegateCreationOperation delegateCreation)
return;
if (delegateCreation.Target is not IMethodReferenceOperation methodReference)
return;
var eventParameter = methodReference.Method.Parameters.LastOrDefault();
if (eventParameter == null)
return;
ITypeSymbol eventArgument;
switch (typeArguments.Length)
{
case 1:
eventArgument = typeArguments[0];
break;
case 2:
eventArgument = typeArguments[1];
break;
default:
return;
}
var byRefAttribute = context.Compilation.GetTypeByMetadataName(ByRefAttribute);
if (byRefAttribute == null)
return;
var isByRefEventType = eventArgument
.GetAttributes()
.Any(attribute => attribute.AttributeClass?.Equals(byRefAttribute, Default) ?? false);
var parameterIsRef = eventParameter.RefKind == RefKind.Ref;
if (isByRefEventType != parameterIsRef)
{
var descriptor = isByRefEventType ? ByRefEventSubscribedByValueRule : ByValueEventSubscribedByRefRule;
var diagnostic = Diagnostic.Create(descriptor, operation.Syntax.GetLocation(), eventArgument);
context.ReportDiagnostic(diagnostic);
}
}
private void CheckEventRaise(OperationAnalysisContext context)
{
if (context.Operation is not IInvocationOperation operation)

View File

@@ -18,6 +18,7 @@ public static class Diagnostics
public const string IdInvalidNotNullableFlagType = "RA0011";
public const string IdNotNullableFlagValueType = "RA0012";
public const string IdByRefEventSubscribedByValue = "RA0013";
public const string IdValueEventSubscribedByRef = "RA0014";
public const string IdByRefEventRaisedByValue = "RA0015";
public const string IdValueEventRaisedByRef = "RA0016";
public const string IdDataDefinitionPartial = "RA0017";

View File

@@ -54,7 +54,7 @@ public class RecursiveMoveBenchmark
var mapSys = _entMan.System<SharedMapSystem>();
var mapId = mapMan.CreateMap();
var map = mapMan.GetMapEntityId(mapId);
var gridComp = mapMan.CreateGridEntity(mapId);
var gridComp = mapMan.CreateGrid(mapId);
var grid = gridComp.Owner;
_gridCoords = new EntityCoordinates(grid, .5f, .5f);
_mapCoords = new EntityCoordinates(map, 100, 100);

View File

@@ -1,6 +1,8 @@
using System;
using System.Linq;
using System.Net;
using Robust.Client.Configuration;
using Robust.Client.Debugging;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Player;
@@ -8,12 +10,13 @@ using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -62,12 +65,12 @@ namespace Robust.Client
_configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged, invokeImmediately: true);
_playMan.Initialize(0);
_playMan.Initialize();
_playMan.PlayerListUpdated += OnPlayerListUpdated;
Reset();
}
private void OnPlayerListUpdated()
private void OnPlayerListUpdated(object? sender, EventArgs e)
{
var serverPlayers = _playMan.PlayerCount;
if (_net.ServerChannel != null && GameInfo != null && _net.IsConnected)
@@ -127,10 +130,9 @@ namespace Robust.Client
{
DebugTools.Assert(RunLevel < ClientRunLevel.Connecting);
DebugTools.Assert(!_net.IsConnected);
var name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName);
_playMan.SetupSinglePlayer(name);
_playMan.Startup();
_playMan.LocalPlayer!.Name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName);
OnRunLevelChanged(ClientRunLevel.SinglePlayerGame);
_playMan.JoinGame(_playMan.LocalSession!);
GameStartedSetup();
}
@@ -171,14 +173,22 @@ namespace Robust.Client
info.ServerName = serverName;
}
var channel = _net.ServerChannel!;
var maxPlayers = _configManager.GetCVar<int>("game.maxplayers");
info.ServerMaxPlayers = maxPlayers;
var userName = _net.ServerChannel!.UserName;
var userId = _net.ServerChannel.UserId;
// start up player management
_playMan.SetupMultiplayer(channel);
_playMan.PlayerStatusChanged += OnStatusChanged;
_playMan.Startup();
_playMan.LocalPlayer!.UserId = userId;
_playMan.LocalPlayer.Name = userName;
_playMan.LocalPlayer.StatusChanged += OnLocalStatusChanged;
var serverPlayers = _playMan.PlayerCount;
_discord.Update(info.ServerName, channel.UserName, info.ServerMaxPlayers.ToString(), serverPlayers.ToString());
_discord.Update(info.ServerName, userName, info.ServerMaxPlayers.ToString(), serverPlayers.ToString());
}
@@ -211,8 +221,6 @@ namespace Robust.Client
private void Reset()
{
_configManager.ReceivedInitialNwVars -= OnReceivedClientData;
_playMan.PlayerStatusChanged -= OnStatusChanged;
_configManager.ClearReceivedInitialNwVars();
OnRunLevelChanged(ClientRunLevel.Initialize);
}
@@ -255,17 +263,19 @@ namespace Robust.Client
Reset();
}
private void OnStatusChanged(object? sender, SessionStatusEventArgs e)
private void OnLocalStatusChanged(object? obj, StatusEventArgs eventArgs)
{
if (e.Session != _playMan.LocalSession)
return;
// player finished fully connecting to the server.
// OldStatus is used here because it can go from connecting-> connected or connecting-> ingame
if (e.OldStatus == SessionStatus.Connecting)
OnPlayerJoinedServer(e.Session);
else if (e.NewStatus == SessionStatus.InGame)
OnPlayerJoinedGame(e.Session);
if (eventArgs.OldStatus == SessionStatus.Connecting)
{
OnPlayerJoinedServer(_playMan.LocalPlayer!.Session);
}
if (eventArgs.NewStatus == SessionStatus.InGame)
{
OnPlayerJoinedGame(_playMan.LocalPlayer!.Session);
}
}
private void OnRunLevelChanged(ClientRunLevel newRunLevel)

View File

@@ -37,7 +37,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Replays;

View File

@@ -13,7 +13,7 @@ using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;

View File

@@ -26,7 +26,10 @@ namespace Robust.Client.Console.Commands
var entity = _entityManager.GetEntity(netEntity);
var componentName = args[1];
var component = _componentFactory.GetComponent(componentName);
var component = (Component) _componentFactory.GetComponent(componentName);
component.Owner = entity;
_entityManager.AddComponent(entity, component);
}
}

View File

@@ -1,7 +1,6 @@
#if DEBUG
using System.Numerics;
using System.Text;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.UserInterface;
@@ -9,6 +8,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.Debugging
@@ -19,7 +19,6 @@ namespace Robust.Client.Debugging
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterface = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
private Label? _label;
@@ -71,7 +70,7 @@ namespace Robust.Client.Debugging
return;
}
var tile = _mapSystem.GetTileRef(gridUid, grid, spot);
var tile = grid.GetTileRef(spot);
_label.Position = mouseSpot.Position + new Vector2(32, 0);
if (_hovered?.GridId == gridUid && _hovered?.Tile == tile) return;
@@ -80,7 +79,7 @@ namespace Robust.Client.Debugging
var text = new StringBuilder();
foreach (var ent in _mapSystem.GetAnchoredEntities(gridUid, grid, spot))
foreach (var ent in grid.GetAnchoredEntities(spot))
{
if (EntityManager.TryGetComponent<MetaDataComponent>(ent, out var meta))
{

View File

@@ -46,6 +46,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Input;
@@ -206,7 +207,6 @@ namespace Robust.Client.Debugging
private readonly Font _font;
private HashSet<Joint> _drawnJoints = new();
private List<Entity<MapGridComponent>> _grids = new();
public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IMapManager mapManager, IPlayerManager playerManager, IResourceCache cache, DebugPhysicsSystem system, EntityLookupSystem lookup, SharedPhysicsSystem physicsSystem)
{
@@ -231,33 +231,32 @@ namespace Robust.Client.Debugging
{
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
if (_entityManager.HasComponent<MapGridComponent>(physBody)) continue;
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner)) continue;
var xform = _physicsSystem.GetPhysicsTransform(physBody);
var comp = physBody.Comp;
var xform = _physicsSystem.GetPhysicsTransform(physBody.Owner);
const float AlphaModifier = 0.2f;
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody).Fixtures.Values)
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody.Owner).Fixtures.Values)
{
// Invalid shape - Box2D doesn't check for IsSensor but we will for sanity.
if (comp.BodyType == BodyType.Dynamic && fixture.Density == 0f && fixture.Hard)
if (physBody.BodyType == BodyType.Dynamic && fixture.Density == 0f && fixture.Hard)
{
DrawShape(worldHandle, fixture, xform, Color.Red.WithAlpha(AlphaModifier));
}
else if (!comp.CanCollide)
else if (!physBody.CanCollide)
{
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.5f, 0.3f).WithAlpha(AlphaModifier));
}
else if (comp.BodyType == BodyType.Static)
else if (physBody.BodyType == BodyType.Static)
{
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.9f, 0.5f).WithAlpha(AlphaModifier));
}
else if ((comp.BodyType & (BodyType.Kinematic | BodyType.KinematicController)) != 0x0)
else if ((physBody.BodyType & (BodyType.Kinematic | BodyType.KinematicController)) != 0x0)
{
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.5f, 0.9f).WithAlpha(AlphaModifier));
}
else if (!comp.Awake)
else if (!physBody.Awake)
{
DrawShape(worldHandle, fixture, xform, new Color(0.6f, 0.6f, 0.6f).WithAlpha(AlphaModifier));
}
@@ -276,18 +275,15 @@ namespace Robust.Client.Debugging
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
var color = Color.Purple.WithAlpha(Alpha);
var transform = _physicsSystem.GetPhysicsTransform(physBody);
worldHandle.DrawCircle(Transform.Mul(transform, physBody.Comp.LocalCenter), 0.2f, color);
var transform = _physicsSystem.GetPhysicsTransform(physBody.Owner);
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), 0.2f, color);
}
_grids.Clear();
_mapManager.FindGridsIntersecting(mapId, viewBounds, ref _grids);
foreach (var grid in _grids)
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, viewBounds))
{
var physBody = _entityManager.GetComponent<PhysicsComponent>(grid);
var physBody = _entityManager.GetComponent<PhysicsComponent>(grid.Owner);
var color = Color.Orange.WithAlpha(Alpha);
var transform = _physicsSystem.GetPhysicsTransform(grid);
var transform = _physicsSystem.GetPhysicsTransform(grid.Owner);
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), 1f, color);
}
}
@@ -296,14 +292,14 @@ namespace Robust.Client.Debugging
{
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
if (_entityManager.HasComponent<MapGridComponent>(physBody)) continue;
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner)) continue;
var xform = _physicsSystem.GetPhysicsTransform(physBody);
var xform = _physicsSystem.GetPhysicsTransform(physBody.Owner);
const float AlphaModifier = 0.2f;
Box2? aabb = null;
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody).Fixtures.Values)
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody.Owner).Fixtures.Values)
{
for (var i = 0; i < fixture.Shape.ChildCount; i++)
{
@@ -322,11 +318,10 @@ namespace Robust.Client.Debugging
{
_drawnJoints.Clear();
var query = _entityManager.AllEntityQueryEnumerator<JointComponent>();
while (query.MoveNext(out var uid, out var jointComponent))
foreach (var jointComponent in _entityManager.EntityQuery<JointComponent>(true))
{
if (jointComponent.JointCount == 0 ||
!_entityManager.TryGetComponent(uid, out TransformComponent? xf1) ||
!_entityManager.TryGetComponent(jointComponent.Owner, out TransformComponent? xf1) ||
!viewAABB.Contains(xf1.WorldPosition)) continue;
foreach (var (_, joint) in jointComponent.Joints)
@@ -366,9 +361,6 @@ namespace Robust.Client.Debugging
_debugPhysicsSystem.PointCount = 0;
}
worldHandle.UseShader(null);
worldHandle.SetTransform(Matrix3.Identity);
}
private void DrawScreen(DrawingHandleScreen screenHandle, OverlayDrawArgs args)
@@ -378,31 +370,28 @@ namespace Robust.Client.Debugging
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.ShapeInfo) != 0x0)
{
var hoverBodies = new List<Entity<PhysicsComponent>>();
var hoverBodies = new List<PhysicsComponent>();
var bounds = Box2.UnitCentered.Translated(_eyeManager.PixelToMap(mousePos.Position).Position);
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, bounds))
{
var uid = physBody.Owner;
if (_entityManager.HasComponent<MapGridComponent>(uid)) continue;
hoverBodies.Add((uid, physBody));
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner)) continue;
hoverBodies.Add(physBody);
}
var lineHeight = _font.GetLineHeight(1f);
var drawPos = mousePos.Position + new Vector2(20, 0) + new Vector2(0, -(hoverBodies.Count * 4 * lineHeight / 2f));
int row = 0;
foreach (var bodyEnt in hoverBodies)
foreach (var body in hoverBodies)
{
if (bodyEnt != hoverBodies[0])
if (body != hoverBodies[0])
{
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), "------");
row++;
}
var body = bodyEnt.Comp;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner}");
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {body.Owner}");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
row++;
@@ -441,9 +430,6 @@ namespace Robust.Client.Debugging
}
}
}
screenHandle.UseShader(null);
screenHandle.SetTransform(Matrix3.Identity);
}
protected internal override void Draw(in OverlayDrawArgs args)
@@ -465,26 +451,11 @@ namespace Robust.Client.Debugging
{
switch (fixture.Shape)
{
case ChainShape cShape:
{
var count = cShape.Count;
var vertices = cShape.Vertices;
var v1 = Transform.Mul(xform, vertices[0]);
for (var i = 1; i < count; ++i)
{
var v2 = Transform.Mul(xform, vertices[i]);
worldHandle.DrawLine(v1, v2, color);
v1 = v2;
}
}
break;
case PhysShapeCircle circle:
var center = Transform.Mul(xform, circle.Position);
worldHandle.DrawCircle(center, circle.Radius, color);
break;
case EdgeShape edge:
{
var v1 = Transform.Mul(xform, edge.Vertex1);
var v2 = Transform.Mul(xform, edge.Vertex2);
worldHandle.DrawLine(v1, v2, color);
@@ -494,7 +465,6 @@ namespace Robust.Client.Debugging
worldHandle.DrawCircle(v1, 0.1f, color);
worldHandle.DrawCircle(v2, 0.1f, color);
}
}
break;
case PolygonShape poly:

View File

@@ -3,7 +3,6 @@ using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using Robust.Client.WebViewHook;
using Robust.Shared.ContentPack;
using Robust.Shared.Log;
using Robust.Shared.Utility;

View File

@@ -287,6 +287,78 @@ namespace Robust.Client
return true;
}
private ResourceManifestData LoadResourceManifest()
{
// Parses /manifest.yml for game-specific settings that cannot be exclusively set up by content code.
if (!_resourceCache.TryContentFileRead("/manifest.yml", out var stream))
return ResourceManifestData.Default;
var yamlStream = new YamlStream();
using (stream)
{
using var streamReader = new StreamReader(stream, EncodingHelpers.UTF8);
yamlStream.Load(streamReader);
}
if (yamlStream.Documents.Count == 0)
return ResourceManifestData.Default;
if (yamlStream.Documents.Count != 1 || yamlStream.Documents[0].RootNode is not YamlMappingNode mapping)
{
throw new InvalidOperationException(
"Expected a single YAML document with root mapping for /manifest.yml");
}
var modules = ReadStringArray(mapping, "modules") ?? Array.Empty<string>();
string? assemblyPrefix = null;
if (mapping.TryGetNode("assemblyPrefix", out var prefixNode))
assemblyPrefix = prefixNode.AsString();
string? defaultWindowTitle = null;
if (mapping.TryGetNode("defaultWindowTitle", out var winTitleNode))
defaultWindowTitle = winTitleNode.AsString();
string? windowIconSet = null;
if (mapping.TryGetNode("windowIconSet", out var iconSetNode))
windowIconSet = iconSetNode.AsString();
string? splashLogo = null;
if (mapping.TryGetNode("splashLogo", out var splashNode))
splashLogo = splashNode.AsString();
bool autoConnect = true;
if (mapping.TryGetNode("autoConnect", out var autoConnectNode))
autoConnect = autoConnectNode.AsBool();
var clientAssemblies = ReadStringArray(mapping, "clientAssemblies");
return new ResourceManifestData(
modules,
assemblyPrefix,
defaultWindowTitle,
windowIconSet,
splashLogo,
autoConnect,
clientAssemblies
);
static string[]? ReadStringArray(YamlMappingNode mapping, string key)
{
if (!mapping.TryGetNode(key, out var node))
return null;
var sequence = (YamlSequenceNode)node;
var array = new string[sequence.Children.Count];
for (var i = 0; i < array.Length; i++)
{
array[i] = sequence[i].AsString();
}
return array;
}
}
internal bool StartupSystemSplash(
GameControllerOptions options,
Func<ILogHandler>? logHandlerFactory,
@@ -385,7 +457,7 @@ namespace Robust.Client
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
}
_resourceManifest = ResourceManifestData.LoadResourceManifest(_resourceCache);
_resourceManifest = LoadResourceManifest();
{
// Handle GameControllerOptions implicit CVar overrides.
@@ -632,6 +704,7 @@ namespace Robust.Client
logManager.GetSawmill("ogl.debug.other").Level = LogLevel.Warning;
logManager.GetSawmill("gdparse").Level = LogLevel.Error;
logManager.GetSawmill("discord").Level = LogLevel.Warning;
logManager.GetSawmill("net.predict").Level = LogLevel.Info;
logManager.GetSawmill("szr").Level = LogLevel.Info;
logManager.GetSawmill("loc").Level = LogLevel.Warning;
@@ -713,6 +786,20 @@ namespace Robust.Client
_clydeAudio.Shutdown();
}
private sealed record ResourceManifestData(
string[] Modules,
string? AssemblyPrefix,
string? DefaultWindowTitle,
string? WindowIconSet,
string? SplashLogo,
bool AutoConnect,
string[]? ClientAssemblies
)
{
public static readonly ResourceManifestData Default =
new ResourceManifestData(Array.Empty<string>(), null, null, null, null, true, null);
}
public event Action<FrameEventArgs>? TickUpdateOverride;
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Prometheus;
using Robust.Client.GameStates;
using Robust.Client.Player;
@@ -85,17 +86,11 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc />
public override void Dirty(EntityUid uid, IComponent component, MetaDataComponent? meta = null)
{
Dirty(new Entity<IComponent>(uid, component), meta);
}
/// <inheritdoc />
public override void Dirty<T>(Entity<T> ent, MetaDataComponent? meta = null)
public override void Dirty(EntityUid uid, Component component, MetaDataComponent? meta = null)
{
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.Dirty(ent, meta);
base.Dirty(uid, component, meta);
}
public override EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metaDataComponent = null)
@@ -170,7 +165,7 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc />
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel? channel)
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel channel)
{
throw new NotSupportedException();
}

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using Robust.Client.Animations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using static Robust.Client.Animations.AnimationPlaybackShared;
namespace Robust.Client.GameObjects
@@ -19,5 +21,42 @@ namespace Robust.Client.GameObjects
= new();
internal bool HasPlayingAnimation = false;
/// <summary>
/// Start playing an animation.
/// </summary>
/// <param name="animation">The animation to play.</param>
/// <param name="key">
/// The key for this animation play. This key can be used to stop playback short later.
/// </param>
[Obsolete("Use AnimationPlayerSystem.Play() instead")]
public void Play(Animation animation, string key)
{
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AnimationPlayerSystem>().AddComponent(this);
var playback = new AnimationPlayback(animation);
PlayingAnimations.Add(key, playback);
}
[Obsolete("Use AnimationPlayerSystem.HasRunningAnimation() instead")]
public bool HasRunningAnimation(string key)
{
return PlayingAnimations.ContainsKey(key);
}
[Obsolete("Use AnimationPlayerSystem.Stop() instead")]
public void Stop(string key)
{
PlayingAnimations.Remove(key);
}
[Obsolete("Temporary method until the event is replaced with eventbus")]
internal void AnimationComplete(string key)
{
AnimationCompleted?.Invoke(key);
}
[Obsolete("Use AnimationCompletedEvent instead")]
public event Action<string>? AnimationCompleted;
}
}

View File

@@ -1,19 +1,22 @@
using System;
using System.Collections.Generic;
using Robust.Client.Animations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
public sealed class AnimationPlayerSystem : EntitySystem
public sealed class AnimationPlayerSystem : EntitySystem, IPostInjectInit
{
private readonly List<Entity<AnimationPlayerComponent>> _activeAnimations = new();
private readonly List<AnimationPlayerComponent> _activeAnimations = new();
private EntityQuery<MetaDataComponent> _metaQuery;
[Dependency] private readonly IComponentFactory _compFact = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
public override void Initialize()
{
@@ -35,22 +38,22 @@ namespace Robust.Client.GameObjects
continue;
}
if (!Update(uid, anim.Comp, frameTime))
if (!Update(uid, anim, frameTime))
{
continue;
}
_activeAnimations.RemoveSwap(i);
i--;
anim.Comp.HasPlayingAnimation = false;
anim.HasPlayingAnimation = false;
}
}
internal void AddComponent(Entity<AnimationPlayerComponent> ent)
internal void AddComponent(AnimationPlayerComponent component)
{
if (ent.Comp.HasPlayingAnimation) return;
_activeAnimations.Add(ent);
ent.Comp.HasPlayingAnimation = true;
if (component.HasPlayingAnimation) return;
_activeAnimations.Add(component);
component.HasPlayingAnimation = true;
}
private bool Update(EntityUid uid, AnimationPlayerComponent component, float frameTime)
@@ -75,6 +78,7 @@ namespace Robust.Client.GameObjects
{
component.PlayingAnimations.Remove(key);
EntityManager.EventBus.RaiseLocalEvent(uid, new AnimationCompletedEvent {Uid = uid, Key = key}, true);
component.AnimationComplete(key);
}
return false;
@@ -85,29 +89,22 @@ namespace Robust.Client.GameObjects
/// </summary>
public void Play(EntityUid uid, Animation animation, string key)
{
var component = EnsureComp<AnimationPlayerComponent>(uid);
Play(new Entity<AnimationPlayerComponent>(uid, component), animation, key);
var component = EntityManager.EnsureComponent<AnimationPlayerComponent>(uid);
Play(component, animation, key);
}
[Obsolete("Use Play(EntityUid<AnimationPlayerComponent> ent, Animation animation, string key) instead")]
public void Play(EntityUid uid, AnimationPlayerComponent? component, Animation animation, string key)
{
component ??= EntityManager.EnsureComponent<AnimationPlayerComponent>(uid);
Play(new Entity<AnimationPlayerComponent>(uid, component), animation, key);
Play(component, animation, key);
}
/// <summary>
/// Start playing an animation.
/// </summary>
[Obsolete("Use Play(EntityUid<AnimationPlayerComponent> ent, Animation animation, string key) instead")]
public void Play(AnimationPlayerComponent component, Animation animation, string key)
{
Play(new Entity<AnimationPlayerComponent>(component.Owner, component), animation, key);
}
public void Play(Entity<AnimationPlayerComponent> ent, Animation animation, string key)
{
AddComponent(ent);
AddComponent(component);
var playback = new AnimationPlaybackShared.AnimationPlayback(animation);
#if DEBUG
@@ -119,18 +116,18 @@ namespace Robust.Client.GameObjects
if (compTrack.ComponentType == null)
{
Log.Error("Attempted to play a component animation without any component specified.");
_sawmill.Error("Attempted to play a component animation without any component specified.");
return;
}
if (!EntityManager.TryGetComponent(ent, compTrack.ComponentType, out var animatedComp))
if (!EntityManager.TryGetComponent(component.Owner, compTrack.ComponentType, out var animatedComp))
{
Log.Error(
$"Attempted to play a component animation, but the entity {ToPrettyString(ent)} does not have the component to be animated: {compTrack.ComponentType}.");
_sawmill.Error(
$"Attempted to play a component animation, but the entity {ToPrettyString(component.Owner)} does not have the component to be animated: {compTrack.ComponentType}.");
return;
}
if (IsClientSide(ent) || !animatedComp.NetSyncEnabled)
if (IsClientSide(component.Owner) || !animatedComp.NetSyncEnabled)
continue;
var reg = _compFact.GetRegistration(animatedComp);
@@ -143,13 +140,13 @@ namespace Robust.Client.GameObjects
if (animatedComp.GetType().GetProperty(compTrack.Property) is { } property &&
property.HasCustomAttribute<AutoNetworkedFieldAttribute>())
{
Log.Warning($"Playing a component animation on a networked component {reg.Name} belonging to {ToPrettyString(ent)}");
_sawmill.Warning($"Playing a component animation on a networked component {reg.Name} belonging to {ToPrettyString(component.Owner)}");
}
}
}
#endif
ent.Comp.PlayingAnimations.Add(key, playback);
component.PlayingAnimations.Add(key, playback);
}
public bool HasRunningAnimation(EntityUid uid, string key)
@@ -178,19 +175,20 @@ namespace Robust.Client.GameObjects
public void Stop(EntityUid uid, string key)
{
if (!TryComp<AnimationPlayerComponent>(uid, out var player))
return;
if (!TryComp<AnimationPlayerComponent>(uid, out var player)) return;
player.PlayingAnimations.Remove(key);
}
public void Stop(EntityUid uid, AnimationPlayerComponent? component, string key)
{
if (!Resolve(uid, ref component, false))
return;
if (!Resolve(uid, ref component, false)) return;
component.PlayingAnimations.Remove(key);
}
void IPostInjectInit.PostInject()
{
_sawmill = _logManager.GetSawmill("anim");
}
}
public sealed class AnimationCompletedEvent : EntityEventArgs

View File

@@ -17,6 +17,7 @@ using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Random;
using Robust.Shared.Replays;
using Robust.Shared.Threading;

View File

@@ -1,7 +1,7 @@
using Robust.Client.Graphics;
using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
@@ -15,8 +15,8 @@ public sealed class EyeSystem : SharedEyeSystem
{
base.Initialize();
SubscribeLocalEvent<EyeComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<EyeComponent, LocalPlayerDetachedEvent>(OnEyeDetached);
SubscribeLocalEvent<EyeComponent, LocalPlayerAttachedEvent>(OnEyeAttached);
SubscribeLocalEvent<EyeComponent, PlayerDetachedEvent>(OnEyeDetached);
SubscribeLocalEvent<EyeComponent, PlayerAttachedEvent>(OnEyeAttached);
SubscribeLocalEvent<EyeComponent, AfterAutoHandleStateEvent>(OnEyeAutoState);
// Make sure this runs *after* entities have been moved by interpolation and movement.
@@ -29,7 +29,7 @@ public sealed class EyeSystem : SharedEyeSystem
UpdateEye(component);
}
private void OnEyeAttached(EntityUid uid, EyeComponent component, LocalPlayerAttachedEvent args)
private void OnEyeAttached(EntityUid uid, EyeComponent component, PlayerAttachedEvent args)
{
// TODO: This probably shouldn't be nullable bruv.
if (component._eye != null)
@@ -41,7 +41,7 @@ public sealed class EyeSystem : SharedEyeSystem
RaiseLocalEvent(uid, ref ev, true);
}
private void OnEyeDetached(EntityUid uid, EyeComponent component, LocalPlayerDetachedEvent args)
private void OnEyeDetached(EntityUid uid, EyeComponent component, PlayerDetachedEvent args)
{
_eyeManager.ClearCurrentEye();
}

View File

@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
@@ -59,8 +58,6 @@ namespace Robust.Client.GameObjects
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private List<Entity<MapGridComponent>> _grids = new();
public GridChunkBoundsOverlay(IEntityManager entManager, IEyeManager eyeManager, IMapManager mapManager)
{
_entityManager = entManager;
@@ -74,15 +71,13 @@ namespace Robust.Client.GameObjects
var viewport = args.WorldBounds;
var worldHandle = args.WorldHandle;
_grids.Clear();
_mapManager.FindGridsIntersecting(currentMap, viewport, ref _grids);
foreach (var grid in _grids)
foreach (var grid in _mapManager.FindGridsIntersecting(currentMap, viewport))
{
var worldMatrix = _entityManager.GetComponent<TransformComponent>(grid).WorldMatrix;
var worldMatrix = _entityManager.GetComponent<TransformComponent>(grid.Owner).WorldMatrix;
worldHandle.SetTransform(worldMatrix);
var transform = new Transform(Vector2.Zero, Angle.Zero);
var chunkEnumerator = grid.Comp.GetMapChunks(viewport);
var chunkEnumerator = grid.GetMapChunks(viewport);
while (chunkEnumerator.MoveNext(out var chunk))
{

View File

@@ -3,13 +3,16 @@ using System.Numerics;
using Robust.Client.GameStates;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -128,7 +131,7 @@ namespace Robust.Client.GameObjects
public override void Initialize()
{
SubscribeLocalEvent<LocalPlayerAttachedEvent>(OnAttachedEntityChanged);
SubscribeLocalEvent<PlayerAttachSysMessage>(OnAttachedEntityChanged);
_conHost.RegisterCommand("incmd",
"Inserts an input command into the simulation",
@@ -168,11 +171,11 @@ namespace Robust.Client.GameObjects
HandleInputCommand(localPlayer.Session, keyFunction, message);
}
private void OnAttachedEntityChanged(LocalPlayerAttachedEvent message)
private void OnAttachedEntityChanged(PlayerAttachSysMessage message)
{
if (message.Entity != default) // attach
if (message.AttachedEntity != default) // attach
{
SetEntityContextActive(_inputManager, message.Entity);
SetEntityContextActive(_inputManager, message.AttachedEntity);
}
else // detach
{
@@ -224,4 +227,44 @@ namespace Robust.Client.GameObjects
_sawmillInputContext = _logManager.GetSawmill("input.context");
}
}
/// <summary>
/// Entity system message that is raised when the player changes attached entities.
/// </summary>
public sealed class PlayerAttachSysMessage : EntityEventArgs
{
/// <summary>
/// New entity the player is attached to.
/// </summary>
public EntityUid AttachedEntity { get; }
/// <summary>
/// Creates a new instance of <see cref="PlayerAttachSysMessage"/>.
/// </summary>
/// <param name="attachedEntity">New entity the player is attached to.</param>
public PlayerAttachSysMessage(EntityUid attachedEntity)
{
AttachedEntity = attachedEntity;
}
}
public sealed class PlayerAttachedEvent : EntityEventArgs
{
public PlayerAttachedEvent(EntityUid entity)
{
Entity = entity;
}
public EntityUid Entity { get; }
}
public sealed class PlayerDetachedEvent : EntityEventArgs
{
public PlayerDetachedEvent(EntityUid entity)
{
Entity = entity;
}
public EntityUid Entity { get; }
}
}

View File

@@ -1,16 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.ComponentTrees;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
@@ -18,7 +15,6 @@ using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Robust.Client.GameObjects
@@ -187,48 +183,6 @@ namespace Robust.Client.GameObjects
{
_queuedFrameUpdate.Add(uid);
}
/// <summary>
/// Gets the specified frame for this sprite at the specified time.
/// </summary>
public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime)
{
Texture? sprite = null;
switch (spriteSpec)
{
case SpriteSpecifier.Rsi rsi:
var rsiActual = _resourceCache.GetResource<RSIResource>(rsi.RsiPath).RSI;
rsiActual.TryGetState(rsi.RsiState, out var state);
var frames = state!.GetFrames(RsiDirection.South);
var delays = state.GetDelays();
var totalDelay = delays.Sum();
var time = curTime.TotalSeconds % totalDelay;
var delaySum = 0f;
for (var i = 0; i < delays.Length; i++)
{
var delay = delays[i];
delaySum += delay;
if (time > delaySum)
continue;
sprite = frames[i];
break;
}
sprite ??= Frame0(spriteSpec);
break;
case SpriteSpecifier.Texture texture:
sprite = texture.GetTexture(_resourceCache);
break;
default:
throw new NotImplementedException();
}
return sprite;
}
}
/// <summary>

View File

@@ -28,11 +28,11 @@ namespace Robust.Client.GameObjects
// Only keep track of transforms actively lerping.
// Much faster than iterating 3000+ transforms every frame.
[ViewVariables] private readonly List<Entity<TransformComponent>> _lerpingTransforms = new();
[ViewVariables] private readonly List<TransformComponent> _lerpingTransforms = new();
public void Reset()
{
foreach (var (_, xform) in _lerpingTransforms)
foreach (var xform in _lerpingTransforms)
{
xform.ActivelyLerping = false;
xform.NextPosition = null;
@@ -54,10 +54,9 @@ namespace Robust.Client.GameObjects
// should show the entity lerping.
// - If the client predicts an entity will move while already lerping due to a state-application, it should
// clear the state's lerp, under the assumption that the client predicted the state and already rendered
// the entity in the state's final position.
// the entity in the final position.
// - If the client predicts that an entity moves, then we only lerp if this is the first time that the tick
// was predicted. I.e., we assume the entity was already rendered in the final position that was
// previously predicted.
// was predicted. I.e., we assume the entity was already rendered in it's final of that lerp.
// - If the client predicts that an entity should lerp twice in the same tick, then we need to combine them.
// I.e. moving from a->b then b->c, the client should lerp from a->c.
@@ -78,7 +77,7 @@ namespace Robust.Client.GameObjects
return;
}
_lerpingTransforms.Add((uid, xform));
_lerpingTransforms.Add(xform);
xform.ActivelyLerping = true;
xform.PredictedLerp = false;
xform.LerpParent = xform.ParentUid;
@@ -97,7 +96,7 @@ namespace Robust.Client.GameObjects
if (!xform.ActivelyLerping)
{
_lerpingTransforms.Add((uid, xform));
_lerpingTransforms.Add(xform);
xform.ActivelyLerping = true;
xform.PredictedLerp = true;
xform.PrevRotation = xform._localRotation;
@@ -124,7 +123,8 @@ namespace Robust.Client.GameObjects
for (var i = 0; i < _lerpingTransforms.Count; i++)
{
var (uid, transform) = _lerpingTransforms[i];
var transform = _lerpingTransforms[i];
var uid = transform.Owner;
var found = false;
// Only lerp if parent didn't change.

View File

@@ -26,6 +26,8 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Profiling;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
@@ -35,7 +37,7 @@ namespace Robust.Client.GameStates
{
/// <inheritdoc />
[UsedImplicitly]
public sealed class ClientGameStateManager : IClientGameStateManager
public sealed class ClientGameStateManager : IClientGameStateManager, IPostInjectInit
{
private GameStateProcessor _processor = default!;
@@ -53,7 +55,7 @@ namespace Robust.Client.GameStates
private readonly Dictionary<EntityUid, HashSet<Type>> _pendingReapplyNetStates = new();
private readonly HashSet<NetEntity> _stateEnts = new();
private readonly List<EntityUid> _toDelete = new();
private readonly List<IComponent> _toRemove = new();
private readonly List<Component> _toRemove = new();
private readonly Dictionary<NetEntity, Dictionary<ushort, ComponentState>> _outputData = new();
private readonly List<(EntityUid, TransformComponent)> _queuedBroadphaseUpdates = new();
@@ -80,13 +82,6 @@ namespace Robust.Client.GameStates
private ISawmill _sawmill = default!;
/// <summary>
/// If we are waiting for a full game state from the server, we will automatically re-send full state requests
/// if they do not arrive in time. Ideally this should never happen, this here just in case a client gets
/// stuck waiting for a full state that the server doesn't know the client even wants.
/// </summary>
public static readonly TimeSpan FullStateTimeout = TimeSpan.FromSeconds(10);
/// <inheritdoc />
public int MinBufferSize => _processor.MinBufferSize;
@@ -94,8 +89,7 @@ namespace Robust.Client.GameStates
public int TargetBufferSize => _processor.TargetBufferSize;
/// <inheritdoc />
public int GetApplicableStateCount() => _processor.GetApplicableStateCount();
public int StateCount => _processor.StateCount;
public int CurrentBufferSize => _processor.CalculateBufferSize(_timing.LastRealTick);
public bool IsPredictionEnabled { get; private set; }
public bool PredictionNeedsResetting { get; private set; }
@@ -127,10 +121,7 @@ namespace Robust.Client.GameStates
/// <inheritdoc />
public void Initialize()
{
_sawmill = _logMan.GetSawmill("state");
_sawmill.Level = LogLevel.Info;
_processor = new GameStateProcessor(this, _timing, _sawmill);
_processor = new GameStateProcessor(_timing);
_network.RegisterNetMessage<MsgState>(HandleStateMessage);
_network.RegisterNetMessage<MsgStateLeavePvs>(HandlePvsLeaveMessage);
@@ -146,7 +137,6 @@ namespace Robust.Client.GameStates
_config.OnValueChanged(CVars.NetPredictLagBias, i => PredictLagBias = i, true);
_config.OnValueChanged(CVars.NetStateBufMergeThreshold, i => StateBufferMergeThreshold = i, true);
_config.OnValueChanged(CVars.NetPVSEntityExitBudget, i => _pvsDetachBudget = i, true);
_config.OnValueChanged(CVars.NetMaxBufferSize, i => _processor.MaxBufferSize = i, true);
_processor.Interpolation = _config.GetCVar(CVars.NetInterp);
_processor.BufferSize = _config.GetCVar(CVars.NetBufferSize);
@@ -255,19 +245,9 @@ namespace Robust.Client.GameStates
/// <inheritdoc />
public void ApplyGameState()
{
// If we have been waiting for a full state for a long time, re-request a full state.
if (_processor.WaitingForFull
&& _processor.LastFullStateRequested is {} last
&& DateTime.UtcNow - last.Time > FullStateTimeout)
{
// Re-request a full state.
// We use the previous from-tick, just in case the full state is already on the way,
RequestFullState(null, last.Tick);
}
// Calculate how many states we need to apply this tick.
// Always at least one, but can be more based on StateBufferMergeThreshold.
var curBufSize = GetApplicableStateCount();
var curBufSize = CurrentBufferSize;
var targetBufSize = TargetBufferSize;
var bufferOverflow = curBufSize - targetBufSize - StateBufferMergeThreshold;
@@ -320,9 +300,9 @@ namespace Robust.Client.GameStates
}
// If we were waiting for a new state, we are now applying it.
if (_processor.WaitingForFull)
if (_processor.LastFullStateRequested.HasValue)
{
_processor.OnFullStateReceived();
_processor.LastFullStateRequested = null;
_timing.LastProcessedTick = curState.ToSequence;
DebugTools.Assert(curState.FromSequence == GameTick.Zero);
PartialStateReset(curState, true);
@@ -387,7 +367,7 @@ namespace Robust.Client.GameStates
if (_processor.WaitingForFull)
_timing.TickTimingAdjustment = 0f;
else
_timing.TickTimingAdjustment = (GetApplicableStateCount() - (float)TargetBufferSize) * 0.10f;
_timing.TickTimingAdjustment = (CurrentBufferSize - (float)TargetBufferSize) * 0.10f;
// If we are about to process an another tick in the same frame, lets not bother unnecessarily running prediction ticks
// Really the main-loop ticking just needs to be more specialized for clients.
@@ -432,11 +412,11 @@ namespace Robust.Client.GameStates
}
}
public void RequestFullState(NetEntity? missingEntity = null, GameTick? tick = null)
public void RequestFullState(NetEntity? missingEntity = null)
{
_sawmill.Info("Requesting full server state");
_network.ClientSendMessage(new MsgStateRequestFull { Tick = _timing.LastRealTick , MissingEntity = missingEntity ?? NetEntity.Invalid });
_processor.OnFullStateRequested(tick ?? _timing.LastRealTick);
_processor.RequestFullState();
}
public void PredictTicks(GameTick predictionTarget)
@@ -519,7 +499,7 @@ namespace Robust.Client.GameStates
var countReset = 0;
var system = _entitySystemManager.GetEntitySystem<ClientDirtySystem>();
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
RemQueue<IComponent> toRemove = new();
RemQueue<Component> toRemove = new();
foreach (var entity in system.DirtyEntities)
{
@@ -624,7 +604,7 @@ namespace Robust.Client.GameStates
/// Whenever a new entity is created, the server doesn't send full state data, given that much of the data
/// can simply be obtained from the entity prototype information. This function basically creates a fake
/// initial server state for any newly created entity. It does this by simply using the standard <see
/// cref="IEntityManager.GetComponentState"/>.
/// cref="IEntityManager.GetComponentState(IEventBus, IComponent)"/>.
/// </remarks>
private void MergeImplicitData(IEnumerable<NetEntity> createdEntities)
{
@@ -690,7 +670,7 @@ namespace Robust.Client.GameStates
using (_prof.Group("Player"))
{
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<SessionState>());
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<PlayerState>());
}
using (_prof.Group("Callback"))
@@ -1207,7 +1187,8 @@ namespace Robust.Client.GameStates
{
if (!meta.NetComponents.TryGetValue(id, out var comp))
{
comp = _compFactory.GetComponent(id);
comp = (Component) _compFactory.GetComponent(id);
comp.Owner = uid;
_entityManager.AddComponent(uid, comp, true, metadata: meta);
}
@@ -1220,7 +1201,8 @@ namespace Robust.Client.GameStates
{
if (!meta.NetComponents.TryGetValue(compChange.NetID, out var comp))
{
comp = _compFactory.GetComponent(compChange.NetID);
comp = (Component) _compFactory.GetComponent(compChange.NetID);
comp.Owner = uid;
_entityManager.AddComponent(uid, comp, true, metadata:meta);
}
else if (compChange.LastModifiedTick <= lastApplied && lastApplied != GameTick.Zero)
@@ -1290,9 +1272,7 @@ namespace Robust.Client.GameStates
var handleState = new ComponentHandleState(cur, next);
bus.RaiseComponentEvent(comp, ref handleState);
}
#pragma warning disable CS0168 // Variable is declared but never used
catch (Exception e)
#pragma warning restore CS0168 // Variable is declared but never used
{
#if EXCEPTION_TOLERANCE
_sawmill.Error($"Failed to apply comp state: entity={_entities.ToPrettyString(uid)}, comp={comp.GetType()}");
@@ -1448,7 +1428,8 @@ namespace Robust.Client.GameStates
{
if (!meta.NetComponents.TryGetValue(id, out var comp))
{
comp = _compFactory.GetComponent(id);
comp = (Component) _compFactory.GetComponent(id);
comp.Owner = uid;
_entityManager.AddComponent(uid, comp, true, meta);
}
@@ -1474,6 +1455,11 @@ namespace Robust.Client.GameStates
public bool IsQueuedForDetach(NetEntity entity)
=> _processor.IsQueuedForDetach(entity);
void IPostInjectInit.PostInject()
{
_sawmill = _logMan.GetSawmill(CVars.NetPredict.Name);
}
}
public sealed class GameStateAppliedArgs : EventArgs

View File

@@ -1,33 +1,46 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Robust.Client.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
/// <inheritdoc />
internal sealed class GameStateProcessor : IGameStateProcessor
internal sealed class GameStateProcessor : IGameStateProcessor, IPostInjectInit
{
[Dependency] private ILogManager _logMan = default!;
private readonly IClientGameTiming _timing;
private readonly IClientGameStateManager _state;
private readonly ISawmill _logger;
private readonly List<GameState> _stateBuffer = new();
private readonly Dictionary<GameTick, List<NetEntity>> _pvsDetachMessages = new();
private ISawmill _logger = default!;
private ISawmill _stateLogger = default!;
public GameState? LastFullState { get; private set; }
public bool WaitingForFull => LastFullStateRequested.HasValue;
public (GameTick Tick, DateTime Time)? LastFullStateRequested { get; private set; } = (GameTick.Zero, DateTime.MaxValue);
public GameTick? LastFullStateRequested
{
get => _lastFullStateRequested;
set
{
_lastFullStateRequested = value;
LastFullState = null;
}
}
public GameTick? _lastFullStateRequested = GameTick.Zero;
private int _bufferSize;
private int _maxBufferSize = 512;
public const int MinimumMaxBufferSize = 256;
/// <summary>
/// This dictionary stores the full most recently received server state of any entity. This is used whenever predicted entities get reset.
@@ -48,14 +61,7 @@ namespace Robust.Client.GameStates
public int BufferSize
{
get => _bufferSize;
set => _bufferSize = Math.Max(value, 0);
}
public int MaxBufferSize
{
get => _maxBufferSize;
// We place a lower bound on the maximum size to avoid spamming servers with full game state requests.
set => _maxBufferSize = Math.Max(value, MinimumMaxBufferSize);
set => _bufferSize = value < 0 ? 0 : value;
}
/// <inheritdoc />
@@ -65,12 +71,9 @@ namespace Robust.Client.GameStates
/// Constructs a new instance of <see cref="GameStateProcessor"/>.
/// </summary>
/// <param name="timing">Timing information of the current state.</param>
/// <param name="clientGameStateManager"></param>
public GameStateProcessor(IClientGameStateManager state, IClientGameTiming timing, ISawmill logger)
public GameStateProcessor(IClientGameTiming timing)
{
_timing = timing;
_state = state;
_logger = logger;
}
/// <inheritdoc />
@@ -80,7 +83,7 @@ namespace Robust.Client.GameStates
if (state.ToSequence <= _timing.LastRealTick)
{
if (Logging)
_logger.Debug($"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
_stateLogger.Debug($"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return false;
}
@@ -92,7 +95,7 @@ namespace Robust.Client.GameStates
continue;
if (Logging)
_logger.Debug($"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
_stateLogger.Debug($"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return false;
}
@@ -101,68 +104,34 @@ namespace Robust.Client.GameStates
if (!WaitingForFull)
{
// This is a good state that we will be using.
TryAdd(state);
_stateBuffer.Add(state);
if (Logging)
_logger.Debug($"Received New GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
_stateLogger.Debug($"Received New GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return true;
}
if (LastFullState == null && state.FromSequence == GameTick.Zero)
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value)
{
if (state.ToSequence >= LastFullStateRequested!.Value.Tick)
{
LastFullState = state;
_logger.Info($"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
return true;
}
LastFullState = state;
_logger.Info($"Received a late full game state. Received: {state.ToSequence}. Requested: {LastFullStateRequested.Value.Tick}");
if (Logging)
_logger.Info($"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
return true;
}
if (LastFullState != null && state.ToSequence <= LastFullState.ToSequence)
{
_logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
if (Logging)
_logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
return false;
}
TryAdd(state);
_stateBuffer.Add(state);
return true;
}
public void TryAdd(GameState state)
{
if (_stateBuffer.Count <= MaxBufferSize)
{
_stateBuffer.Add(state);
return;
}
// This can happen if a required state gets dropped somehow and the client keeps receiving future
// game states that they can't apply. I.e., GetApplicableStateCount() is zero, even though there are many
// states in the list.
//
// This can seemingly happen when the server sends ""reliable"" game states while the client is paused?
// For example, when debugging the client, while the server is running:
// - The client stops sending acks for states that the server sends out.
// - Thus the client will exceed the net.force_ack_threshold cvar
// - The server starts sending some packets ""reliably"" and just force updates the clients last ack.
//
// What should happen is that when the client resumes, it receives the reliably sent states and can just
// resume. However, even though the packets are sent ""reliably"", they just seem to get dropped.
// I don't quite understand how/why yet, but this ensures the client doesn't get stuck.
#if FULL_RELEASE
_logger.Warning(@$"Exceeded maximum state buffer size!
Tick: {_timing.CurTick}/{_timing.LastProcessedTick}/{_timing.LastRealTick}
Size: {_stateBuffer.Count}
Applicable states: {GetApplicableStateCount()}
Was waiting for full: {WaitingForFull} {LastFullStateRequested}
Had full state: {LastFullState != null}"
);
#endif
_state.RequestFullState();
}
/// <summary>
/// Attempts to get the current and next states to apply.
/// </summary>
@@ -183,7 +152,7 @@ Had full state: {LastFullState != null}"
"Tried to apply a non-extrapolated state that has too high of a FromSequence!");
if (Logging)
_logger.Debug($"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
_stateLogger.Debug($"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
}
return applyNextState;
@@ -375,20 +344,14 @@ Had full state: {LastFullState != null}"
{
_stateBuffer.Clear();
LastFullState = null;
LastFullStateRequested = (GameTick.Zero, DateTime.MaxValue);
LastFullStateRequested = GameTick.Zero;
}
public void OnFullStateRequested(GameTick tick)
public void RequestFullState()
{
_stateBuffer.Clear();
LastFullState = null;
LastFullStateRequested = (tick, DateTime.UtcNow);
}
public void OnFullStateReceived()
{
LastFullState = null;
LastFullStateRequested = null;
LastFullStateRequested = _timing.LastRealTick;
}
public void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, ComponentState>> implicitData)
@@ -453,11 +416,10 @@ Had full state: {LastFullState != null}"
return false;
}
public int GetApplicableStateCount(GameTick? fromTick = null)
public int CalculateBufferSize(GameTick fromTick)
{
fromTick ??= _timing.LastRealTick;
bool foundState;
var nextTick = fromTick.Value;
var nextTick = fromTick;
do
{
@@ -475,9 +437,13 @@ Had full state: {LastFullState != null}"
}
while (foundState);
return (int) (nextTick.Value - fromTick.Value.Value);
return (int) (nextTick.Value - fromTick.Value);
}
public int StateCount => _stateBuffer.Count;
void IPostInjectInit.PostInject()
{
_logger = _logMan.GetSawmill("net");
_stateLogger = _logMan.GetSawmill("net.state");
}
}
}

View File

@@ -32,15 +32,7 @@ namespace Robust.Client.GameStates
/// <summary>
/// Number of applicable game states currently in the state buffer.
/// </summary>
int GetApplicableStateCount();
[Obsolete("use GetApplicableStateCount()")]
int CurrentBufferSize => GetApplicableStateCount();
/// <summary>
/// Total number of game states currently in the state buffer.
/// </summary>
int StateCount { get; }
int CurrentBufferSize { get; }
/// <summary>
/// If the buffer size is this many states larger than the target buffer size,
@@ -99,7 +91,7 @@ namespace Robust.Client.GameStates
/// <summary>
/// Requests a full state from the server. This should override even implicit entity data.
/// </summary>
void RequestFullState(NetEntity? missingEntity = null, GameTick? tick = null);
void RequestFullState(NetEntity? missingEntity = null);
uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs;

View File

@@ -96,7 +96,7 @@ namespace Robust.Client.GameStates
/// This includes only applicable states. If there is a gap, future buffers are not included.
/// </summary>
/// <param name="fromTick">The tick to calculate from.</param>
int GetApplicableStateCount(GameTick? fromTick);
int CalculateBufferSize(GameTick fromTick);
bool TryGetLastServerStates(NetEntity entity,
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary);

View File

@@ -68,7 +68,7 @@ namespace Robust.Client.GameStates
var lag = _netManager.ServerChannel!.Ping;
// calc interp info
var buffer = _gameStateManager.GetApplicableStateCount();
var buffer = _gameStateManager.CurrentBufferSize;
_totalHistoryPayload += sz;
_history.Add((toSeq, sz, lag, buffer));
@@ -268,7 +268,7 @@ namespace Robust.Client.GameStates
handle.DrawString(_font, new Vector2(LeftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
// buffer text
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.GetApplicableStateCount().ToString()} states");
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
}
protected override void DisposeBehavior()

View File

@@ -4,6 +4,7 @@ using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
@@ -40,28 +41,22 @@ namespace Robust.Client.Graphics.Clyde
gridProgram.SetUniformTextureMaybe(UniILightTexture, TextureUnit.Texture1);
gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1));
var grids = new List<Entity<MapGridComponent>>();
_mapManager.FindGridsIntersecting(mapId, worldBounds, ref grids);
foreach (var mapGrid in grids)
foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
{
if (!_mapChunkData.ContainsKey(mapGrid))
if (!_mapChunkData.ContainsKey(mapGrid.Owner))
continue;
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid);
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid.Owner);
gridProgram.SetUniform(UniIModelMatrix, transform.WorldMatrix);
var enumerator = mapGrid.Comp.GetMapChunks(worldBounds);
var data = _mapChunkData[mapGrid];
var enumerator = mapGrid.GetMapChunks(worldBounds);
while (enumerator.MoveNext(out var chunk))
{
DebugTools.Assert(chunk.FilledTiles > 0);
if (!data.TryGetValue(chunk.Indices, out MapChunkData? datum))
data[chunk.Indices] = datum = _initChunkBuffers(mapGrid, chunk);
if (_isChunkDirty(mapGrid, chunk))
_updateChunkMesh(mapGrid, chunk);
if (datum.Dirty)
_updateChunkMesh(mapGrid, chunk, datum);
var datum = _mapChunkData[mapGrid.Owner][chunk.Indices];
DebugTools.Assert(datum.TileCount > 0);
if (datum.TileCount == 0)
continue;
@@ -73,36 +68,22 @@ namespace Robust.Client.Graphics.Clyde
CheckGlError();
}
}
CullEmptyChunks();
}
private void CullEmptyChunks()
private void _updateChunkMesh(MapGridComponent grid, MapChunk chunk)
{
foreach (var (grid, chunks) in _mapChunkData)
var data = _mapChunkData[grid.Owner];
if (!data.TryGetValue(chunk.Indices, out var datum))
{
var gridComp = _mapManager.GetGridComp(grid);
foreach (var (index, chunk) in chunks)
{
if (!chunk.Dirty || gridComp.Chunks.ContainsKey(index))
{
DebugTools.Assert(gridComp.Chunks[index].FilledTiles > 0);
continue;
}
DeleteChunk(chunk);
chunks.Remove(index);
}
datum = _initChunkBuffers(grid, chunk);
}
}
private void _updateChunkMesh(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
Span<ushort> indexBuffer = stackalloc ushort[_indicesPerChunk(chunk)];
Span<Vertex2D> vertexBuffer = stackalloc Vertex2D[_verticesPerChunk(chunk)];
var i = 0;
var cSz = grid.Comp.ChunkSize;
var cSz = grid.ChunkSize;
var cScaled = chunk.Indices * cSz;
for (ushort x = 0; x < cSz; x++)
{
@@ -149,7 +130,7 @@ namespace Robust.Client.Graphics.Clyde
datum.TileCount = i;
}
private unsafe MapChunkData _initChunkBuffers(Entity<MapGridComponent> grid, MapChunk chunk)
private unsafe MapChunkData _initChunkBuffers(MapGridComponent grid, MapChunk chunk)
{
var vao = GenVertexArray();
BindVertexArray(vao);
@@ -177,22 +158,41 @@ namespace Robust.Client.Graphics.Clyde
Dirty = true
};
_mapChunkData[grid.Owner].Add(chunk.Indices, datum);
return datum;
}
private void DeleteChunk(MapChunkData data)
private bool _isChunkDirty(MapGridComponent grid, MapChunk chunk)
{
DeleteVertexArray(data.VAO);
CheckGlError();
data.VBO.Delete();
data.EBO.Delete();
var data = _mapChunkData[grid.Owner];
return !data.TryGetValue(chunk.Indices, out var datum) || datum.Dirty;
}
public void _setChunkDirty(MapGridComponent grid, Vector2i chunk)
{
var data = _mapChunkData.GetOrNew(grid.Owner);
if (data.TryGetValue(chunk, out var datum))
{
datum.Dirty = true;
}
// Don't need to set it if we don't have an entry since lack of an entry is treated as dirty.
}
private void _updateOnGridModified(GridModifiedEvent args)
{
foreach (var (pos, _) in args.Modified)
{
var grid = args.Grid;
var chunk = grid.GridTileToChunkIndices(pos);
_setChunkDirty(grid, chunk);
}
}
private void _updateTileMapOnUpdate(ref TileChangedEvent args)
{
var gridData = _mapChunkData.GetOrNew(args.Entity);
if (gridData.TryGetValue(args.ChunkIndex, out var data))
data.Dirty = true;
var grid = _mapManager.GetGrid(args.NewTile.GridUid);
var chunk = grid.GridTileToChunkIndices(new Vector2i(args.NewTile.X, args.NewTile.Y));
_setChunkDirty(grid, chunk);
}
private void _updateOnGridCreated(GridStartupEvent ev)
@@ -208,7 +208,10 @@ namespace Robust.Client.Graphics.Clyde
var data = _mapChunkData[gridId];
foreach (var chunkDatum in data.Values)
{
DeleteChunk(chunkDatum);
DeleteVertexArray(chunkDatum.VAO);
CheckGlError();
chunkDatum.VBO.Delete();
chunkDatum.EBO.Delete();
}
_mapChunkData.Remove(gridId);

View File

@@ -350,7 +350,7 @@ namespace Robust.Client.Graphics.Clyde
_renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize));
if (entry.Sprite.RaiseShaderEvent)
_entityManager.EventBus.RaiseLocalEvent(entry.Uid,
_entityManager.EventBus.RaiseLocalEvent(entry.Sprite.Owner,
new BeforePostShaderRenderEvent(entry.Sprite, viewport), false);
}
}

View File

@@ -1,3 +1,11 @@
using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Threading;
using Robust.Shared.Utility;
using System;
using System.Buffers;
using System.Collections.Generic;
@@ -6,15 +14,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using System.Threading.Tasks;
using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Threading;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Clyde;
@@ -260,7 +260,7 @@ internal partial class Clyde
if (cmp != 0)
return cmp;
return a.Uid.CompareTo(b.Uid);
return a.Sprite.Owner.CompareTo(b.Sprite.Owner);
}
}
}

View File

@@ -17,9 +17,11 @@ using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using Color = Robust.Shared.Maths.Color;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
namespace Robust.Client.Graphics.Clyde
@@ -173,6 +175,7 @@ namespace Robust.Client.Graphics.Clyde
_entityManager.EventBus.SubscribeEvent<TileChangedEvent>(EventSource.Local, this, _updateTileMapOnUpdate);
_entityManager.EventBus.SubscribeEvent<GridStartupEvent>(EventSource.Local, this, _updateOnGridCreated);
_entityManager.EventBus.SubscribeEvent<GridRemovalEvent>(EventSource.Local, this, _updateOnGridRemoved);
_entityManager.EventBus.SubscribeEvent<GridModifiedEvent>(EventSource.Local, this, _updateOnGridModified);
}
public void ShutdownGridEcsEvents()
@@ -180,6 +183,7 @@ namespace Robust.Client.Graphics.Clyde
_entityManager.EventBus.UnsubscribeEvent<TileChangedEvent>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<GridStartupEvent>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<GridRemovalEvent>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<GridModifiedEvent>(EventSource.Local, this);
}
private void GLInitBindings(bool gles)

View File

@@ -1,13 +1,12 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Direction = Robust.Shared.Maths.Direction;
namespace Robust.Client.Map;
@@ -23,8 +22,6 @@ public sealed class TileEdgeOverlay : Overlay
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
private List<Entity<MapGridComponent>> _grids = new();
public TileEdgeOverlay(IEntityManager entManager, IMapManager mapManager, IResourceCache resource, ITileDefinitionManager tileDefManager)
{
_entManager = entManager;
@@ -39,18 +36,16 @@ public sealed class TileEdgeOverlay : Overlay
if (args.MapId == MapId.Nullspace)
return;
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
foreach (var grid in _grids)
foreach (var grid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
{
var tileSize = grid.Comp.TileSize;
var tileSize = grid.TileSize;
var tileDimensions = new Vector2(tileSize, tileSize);
var xform = xformQuery.GetComponent(grid);
var xform = xformQuery.GetComponent(grid.Owner);
args.WorldHandle.SetTransform(xform.WorldMatrix);
foreach (var tileRef in grid.Comp.GetTilesIntersecting(args.WorldBounds, false))
foreach (var tileRef in grid.GetTilesIntersecting(args.WorldBounds, false))
{
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
@@ -66,7 +61,7 @@ public sealed class TileEdgeOverlay : Overlay
continue;
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
var neighborTile = grid.Comp.GetTileRef(neighborIndices);
var neighborTile = grid.GetTileRef(neighborIndices);
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
// If it's the same tile then no edge to be drawn.

View File

@@ -1,6 +1,6 @@
using System.Buffers;
using System.Collections.Generic;
using Robust.Client.Player;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
@@ -20,8 +20,8 @@ public sealed partial class PhysicsSystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LocalPlayerAttachedEvent>(OnAttach);
SubscribeLocalEvent<LocalPlayerDetachedEvent>(OnDetach);
SubscribeLocalEvent<PlayerAttachedEvent>(OnAttach);
SubscribeLocalEvent<PlayerDetachedEvent>(OnDetach);
SubscribeLocalEvent<PhysicsComponent, JointAddedEvent>(OnJointAdded);
SubscribeLocalEvent<PhysicsComponent, JointRemovedEvent>(OnJointRemoved);
}
@@ -63,12 +63,12 @@ public sealed partial class PhysicsSystem
UpdateIsPredicted(args.Joint.BodyBUid);
}
private void OnAttach(LocalPlayerAttachedEvent ev)
private void OnAttach(PlayerAttachedEvent ev)
{
UpdateIsPredicted(ev.Entity);
}
private void OnDetach(LocalPlayerDetachedEvent ev)
private void OnDetach(PlayerDetachedEvent ev)
{
UpdateIsPredicted(ev.Entity);
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics;
@@ -25,7 +26,7 @@ namespace Robust.Client.Physics
protected override void Cleanup(PhysicsMapComponent component, float frameTime)
{
var toRemove = new List<Entity<PhysicsComponent>>();
var toRemove = new List<PhysicsComponent>();
// Because we're not predicting 99% of bodies its sleep timer never gets incremented so we'll just do it ourselves.
// (and serializing it over the network isn't necessary?)
@@ -37,13 +38,13 @@ namespace Robust.Client.Physics
body.SleepTime += frameTime;
if (body.SleepTime > TimeToSleep)
{
toRemove.Add(new Entity<PhysicsComponent>(body.Owner, body));
toRemove.Add(body);
}
}
foreach (var body in toRemove)
{
SetAwake(body, false);
SetAwake(body.Owner, body, false);
}
base.Cleanup(component, frameTime);

View File

@@ -8,6 +8,7 @@ using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
@@ -286,17 +287,23 @@ namespace Robust.Client.Placement
}, outsidePrediction: true))
.Register<PlacementManager>();
PlayerManager.LocalPlayerDetached += OnDetached;
var localPlayer = PlayerManager.LocalPlayer;
localPlayer!.EntityAttached += OnEntityAttached;
}
private void TearDownInput()
{
CommandBinds.Unregister<PlacementManager>();
PlayerManager.LocalPlayerDetached -= OnDetached;
if (PlayerManager.LocalPlayer != null)
{
PlayerManager.LocalPlayer.EntityAttached -= OnEntityAttached;
}
}
private void OnDetached(EntityUid obj)
private void OnEntityAttached(EntityAttachedEventArgs eventArgs)
{
// player attached to a new entity, basically disable the editor
Clear();
}

View File

@@ -1,71 +1,44 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Player;
public interface IPlayerManager : ISharedPlayerManager
namespace Robust.Client.Player
{
/// <summary>
/// Invoked when the list of sessions/players gets updated.
/// </summary>
event Action? PlayerListUpdated;
/// <summary>
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets attached to a new entity. See also <see cref="LocalPlayerAttachedEvent"/>
/// </summary>
event Action<EntityUid>? LocalPlayerAttached;
/// <summary>
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets detached from new entity. See also <see cref="LocalPlayerDetachedEvent"/>
/// </summary>
event Action<EntityUid>? LocalPlayerDetached;
void ApplyPlayerStates(IReadOnlyCollection<SessionState> list);
/// <summary>
/// Sets up a single player game. This creates a dummy <see cref="ISharedPlayerManager.LocalSession"/> without an
/// <see cref="INetChannel"/>.
/// </summary>
void SetupSinglePlayer(string name);
/// <summary>
/// Sets up the manager for a multiplayer game. This creates a <see cref="ISharedPlayerManager.LocalSession"/>
/// using the given <see cref="INetChannel"/>.
/// </summary>
void SetupMultiplayer(INetChannel channel);
[Obsolete("Use LocalSession instead")]
LocalPlayer? LocalPlayer { get;}
}
/// <summary>
/// ECS event that gets raised when the local player gets attached to a new entity. The event is both broadcast and
/// raised directed at the new entity.
/// </summary>
public sealed class LocalPlayerAttachedEvent : EntityEventArgs
{
public LocalPlayerAttachedEvent(EntityUid entity)
public interface IPlayerManager : ISharedPlayerManager
{
Entity = entity;
new IEnumerable<ICommonSession> Sessions { get; }
[ViewVariables]
IReadOnlyDictionary<NetUserId, ICommonSession> SessionsDict { get; }
[ViewVariables]
LocalPlayer? LocalPlayer { get; }
/// <summary>
/// Invoked after LocalPlayer is changed
/// </summary>
event Action<LocalPlayerChangedEventArgs>? LocalPlayerChanged;
event EventHandler PlayerListUpdated;
void Initialize();
void Startup();
void Shutdown();
void ApplyPlayerStates(IReadOnlyCollection<PlayerState> list);
}
public EntityUid Entity { get; }
}
/// <summary>
/// ECS event that gets raised when the local player gets detached from an entity. The event is both broadcast and
/// raised directed at the new entity.
/// </summary>
public sealed class LocalPlayerDetachedEvent : EntityEventArgs
{
public LocalPlayerDetachedEvent(EntityUid entity)
public sealed class LocalPlayerChangedEventArgs : EventArgs
{
Entity = entity;
public readonly LocalPlayer? OldPlayer;
public readonly LocalPlayer? NewPlayer;
public LocalPlayerChangedEventArgs(LocalPlayer? oldPlayer, LocalPlayer? newPlayer)
{
OldPlayer = oldPlayer;
NewPlayer = newPlayer;
}
}
public EntityUid Entity { get; }
}
}

View File

@@ -1,6 +1,11 @@
using System;
using Robust.Client.GameObjects;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Player
@@ -10,30 +15,163 @@ namespace Robust.Client.Player
/// </summary>
public sealed class LocalPlayer
{
public LocalPlayer(ICommonSession session)
{
Session = session;
}
/// <summary>
/// An entity has been attached to the local player.
/// </summary>
public event Action<EntityAttachedEventArgs>? EntityAttached;
/// <summary>
/// An entity has been detached from the local player.
/// </summary>
public event Action<EntityDetachedEventArgs>? EntityDetached;
/// <summary>
/// Game entity that the local player is controlling. If this is default, the player is not attached to any
/// entity at all.
/// </summary>
[ViewVariables]
public EntityUid? ControlledEntity => Session.AttachedEntity;
[ViewVariables] public EntityUid? ControlledEntity { get; private set; }
[ViewVariables] public NetUserId UserId { get; set; }
/// <summary>
/// Session of the local client.
/// </summary>
[ViewVariables]
public NetUserId UserId => Session.UserId;
public ICommonSession Session => InternalSession;
internal PlayerSession InternalSession { get; set; } = default!;
/// <summary>
/// OOC name of the local player.
/// </summary>
[ViewVariables]
public string Name => Session.Name;
public string Name { get; set; } = default!;
/// <summary>
/// Session of the local client.
/// The status of the client's session has changed.
/// </summary>
public ICommonSession Session;
public event EventHandler<StatusEventArgs>? StatusChanged;
/// <summary>
/// Attaches a client to an entity.
/// </summary>
/// <param name="entity">Entity to attach the client to.</param>
public void AttachEntity(EntityUid entity, IEntityManager entMan, IBaseClient client)
{
if (ControlledEntity == entity)
return;
// Detach and cleanup first
DetachEntity();
if (!entMan.EntityExists(entity))
{
Logger.Error($"Attempting to attach player to non-existent entity {entity}!");
return;
}
ControlledEntity = entity;
InternalSession.AttachedEntity = entity;
if (!entMan.TryGetComponent<EyeComponent?>(entity, out var eye))
{
eye = entMan.AddComponent<EyeComponent>(entity);
if (client.RunLevel != ClientRunLevel.SinglePlayerGame)
{
Logger.Warning($"Attaching local player to an entity {entMan.ToPrettyString(entity)} without an eye. This eye will not be netsynced and may cause issues.");
}
eye.NetSyncEnabled = false;
}
EntityAttached?.Invoke(new EntityAttachedEventArgs(entity));
// notify ECS Systems
var eventBus = entMan.EventBus;
eventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(entity));
eventBus.RaiseLocalEvent(entity, new PlayerAttachedEvent(entity), true);
}
/// <summary>
/// Detaches the client from an entity.
/// </summary>
public void DetachEntity()
{
var entMan = IoCManager.Resolve<IEntityManager>();
var previous = ControlledEntity;
ControlledEntity = null;
InternalSession.AttachedEntity = null;
if (previous != null)
{
entMan.EventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(default));
entMan.EventBus.RaiseLocalEvent(previous.Value, new PlayerDetachedEvent(previous.Value), true);
EntityDetached?.Invoke(new EntityDetachedEventArgs(previous.Value));
}
}
/// <summary>
/// Changes the state of the session.
/// </summary>
public void SwitchState(SessionStatus newStatus)
{
SwitchState(Session.Status, newStatus);
}
/// <summary>
/// Changes the state of the session. This overload allows you to spoof the oldStatus, use with caution.
/// </summary>
public void SwitchState(SessionStatus oldStatus, SessionStatus newStatus)
{
var args = new StatusEventArgs(oldStatus, newStatus);
Session.Status = newStatus;
StatusChanged?.Invoke(this, args);
}
}
/// <summary>
/// Event arguments for when the status of a session changes.
/// </summary>
public sealed class StatusEventArgs : EventArgs
{
/// <summary>
/// Status that the session switched from.
/// </summary>
public SessionStatus OldStatus { get; }
/// <summary>
/// Status that the session switched to.
/// </summary>
public SessionStatus NewStatus { get; }
/// <summary>
/// Constructs a new instance of the class.
/// </summary>
public StatusEventArgs(SessionStatus oldStatus, SessionStatus newStatus)
{
OldStatus = oldStatus;
NewStatus = newStatus;
}
}
public sealed class EntityDetachedEventArgs : EventArgs
{
public EntityDetachedEventArgs(EntityUid oldEntity)
{
OldEntity = oldEntity;
}
public EntityUid OldEntity { get; }
}
public sealed class EntityAttachedEventArgs : EventArgs
{
public EntityAttachedEventArgs(EntityUid newEntity)
{
NewEntity = newEntity;
}
public EntityUid NewEntity { get; }
}
}

View File

@@ -2,13 +2,16 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Player
{
@@ -17,187 +20,154 @@ namespace Robust.Client.Player
/// Why not just attach the inputs directly? It's messy! This makes the whole thing nicely encapsulated.
/// This class also communicates with the server to let the server control what entity it is attached to.
/// </summary>
internal sealed class PlayerManager : SharedPlayerManager, IPlayerManager
public sealed class PlayerManager : IPlayerManager
{
[Dependency] private readonly IClientNetManager _network = default!;
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly ILogManager _logMan = default!;
/// <summary>
/// Received player states that had an unknown <see cref="NetEntity"/>.
/// Active sessions of connected clients to the server.
/// </summary>
private Dictionary<NetUserId, SessionState> _pendingStates = new ();
private List<SessionState> _pending = new();
private readonly Dictionary<NetUserId, ICommonSession> _sessions = new();
/// <inheritdoc />
public override ICommonSession[] NetworkedSessions
public IEnumerable<ICommonSession> NetworkedSessions
{
get
{
return LocalSession != null
? new [] { LocalSession }
: Array.Empty<ICommonSession>();
if (LocalPlayer is not null)
return new[] {LocalPlayer.Session};
return Enumerable.Empty<ICommonSession>();
}
}
/// <inheritdoc />
public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1;
public LocalPlayer? LocalPlayer { get; set; }
public event Action<SessionStatusEventArgs>? LocalStatusChanged;
public event Action? PlayerListUpdated;
public event Action<EntityUid>? LocalPlayerDetached;
public event Action<EntityUid>? LocalPlayerAttached;
IEnumerable<ICommonSession> ISharedPlayerManager.Sessions => _sessions.Values;
/// <inheritdoc />
public override void Initialize(int maxPlayers)
public int PlayerCount => _sessions.Values.Count;
/// <inheritdoc />
public int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? 0;
public ICommonSession? LocalSession => LocalPlayer?.Session;
/// <inheritdoc />
[ViewVariables]
public LocalPlayer? LocalPlayer
{
base.Initialize(maxPlayers);
get => _localPlayer;
private set
{
if (_localPlayer == value) return;
var oldValue = _localPlayer;
_localPlayer = value;
LocalPlayerChanged?.Invoke(new LocalPlayerChangedEventArgs(oldValue, _localPlayer));
}
}
private LocalPlayer? _localPlayer;
private ISawmill _sawmill = default!;
public event Action<LocalPlayerChangedEventArgs>? LocalPlayerChanged;
/// <inheritdoc />
[ViewVariables]
IEnumerable<ICommonSession> IPlayerManager.Sessions => _sessions.Values;
/// <inheritdoc />
public IReadOnlyDictionary<NetUserId, ICommonSession> SessionsDict => _sessions;
/// <inheritdoc />
public event EventHandler? PlayerListUpdated;
/// <inheritdoc />
public void Initialize()
{
_client.RunLevelChanged += OnRunLevelChanged;
_sawmill = _logMan.GetSawmill("player");
_network.RegisterNetMessage<MsgPlayerListReq>();
_network.RegisterNetMessage<MsgPlayerList>(HandlePlayerList);
PlayerStatusChanged += StatusChanged;
}
private void StatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.Session == LocalPlayer?.Session)
LocalStatusChanged?.Invoke(e);
}
/// <inheritdoc />
public override void Startup()
public void Startup()
{
if (LocalSession == null)
throw new InvalidOperationException("LocalSession cannot be null");
DebugTools.Assert(LocalPlayer == null);
LocalPlayer = new LocalPlayer();
LocalPlayer = new LocalPlayer(LocalSession);
base.Startup();
}
public void SetupSinglePlayer(string name)
{
if (LocalSession != null)
throw new InvalidOperationException($"Player manager already running?");
LocalSession = CreateAndAddSession(default, name);
Startup();
PlayerListUpdated?.Invoke();
}
public void SetupMultiplayer(INetChannel channel)
{
if (LocalSession != null)
throw new InvalidOperationException($"Player manager already running?");
var session = CreateAndAddSession(channel.UserId, channel.UserName);
session.Channel = channel;
LocalSession = session;
Startup();
_network.ClientSendMessage(new MsgPlayerListReq());
var msgList = new MsgPlayerListReq();
// message is empty
_network.ClientSendMessage(msgList);
}
/// <inheritdoc />
public override void Shutdown()
public void Shutdown()
{
if (LocalSession != null)
SetAttachedEntity(LocalSession, null);
LocalPlayer?.DetachEntity();
LocalPlayer = null;
LocalSession = null;
_pendingStates.Clear();
base.Shutdown();
PlayerListUpdated?.Invoke();
_sessions.Clear();
}
public override void SetAttachedEntity(ICommonSession session, EntityUid? uid)
{
if (session.AttachedEntity == uid)
return;
var old = session.AttachedEntity;
base.SetAttachedEntity(session, uid);
if (session != LocalSession)
return;
if (old.HasValue)
{
Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(old)}.");
EntManager.EventBus.RaiseLocalEvent(old.Value, new LocalPlayerDetachedEvent(old.Value), true);
LocalPlayerDetached?.Invoke(old.Value);
}
if (uid == null)
{
Sawmill.Info($"Local player is no longer attached to any entity.");
return;
}
if (!EntManager.EntityExists(uid))
{
Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!");
return;
}
if (!EntManager.EnsureComponent(uid.Value, out EyeComponent eye))
{
if (_client.RunLevel != ClientRunLevel.SinglePlayerGame)
Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues.");
eye.NetSyncEnabled = false;
}
Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(uid)}.");
EntManager.EventBus.RaiseLocalEvent(uid.Value, new LocalPlayerAttachedEvent(uid.Value), true);
LocalPlayerAttached?.Invoke(uid.Value);
}
public void ApplyPlayerStates(IReadOnlyCollection<SessionState> list)
{
var dirty = ApplyStates(list, true);
if (_pendingStates.Count == 0)
{
// This is somewhat inefficient as it might try to re-apply states that failed just a moment ago.
_pending.Clear();
_pending.AddRange(_pendingStates.Values);
_pendingStates.Clear();
dirty |= ApplyStates(_pending, false);
}
if (dirty)
PlayerListUpdated?.Invoke();
}
private bool ApplyStates(IReadOnlyCollection<SessionState> list, bool fullList)
/// <inheritdoc />
public void ApplyPlayerStates(IReadOnlyCollection<PlayerState> list)
{
if (list.Count == 0)
return false;
{
// This happens when the server says "nothing changed!"
return;
}
DebugTools.Assert(_network.IsConnected || _client.RunLevel == ClientRunLevel.SinglePlayerGame // replays use state application.
, "Received player state without being connected?");
DebugTools.Assert(LocalSession != null, "Received player state before Session finished setup.");
DebugTools.Assert(LocalPlayer != null, "Call Startup()");
DebugTools.Assert(LocalPlayer!.Session != null, "Received player state before Session finished setup.");
var state = list.FirstOrDefault(s => s.UserId == LocalSession.UserId);
var myState = list.FirstOrDefault(s => s.UserId == LocalPlayer.UserId);
bool dirty = false;
if (state != null)
if (myState != null)
{
dirty = true;
if (!EntManager.TryGetEntity(state.ControlledEntity, out var uid)
&& state.ControlledEntity is { Valid:true } )
var uid = _entManager.GetEntity(myState.ControlledEntity);
if (myState.ControlledEntity is {Valid: true} && !_entManager.EntityExists(uid))
{
Sawmill.Error($"Received player state for local player with an unknown net entity!");
_pendingStates[state.UserId] = state;
}
else
{
_pendingStates.Remove(state.UserId);
_sawmill.Error($"Received player state for local player with an unknown net entity!");
}
SetAttachedEntity(LocalSession, uid);
SetStatus(LocalSession, state.Status);
UpdateAttachedEntity(uid);
UpdateSessionStatus(myState.Status);
}
return UpdatePlayerList(list, fullList) || dirty;
UpdatePlayerList(list);
}
/// <summary>
/// Compares the server sessionStatus to the client one, and updates if needed.
/// </summary>
private void UpdateSessionStatus(SessionStatus myStateStatus)
{
if (LocalPlayer!.Session.Status != myStateStatus)
LocalPlayer.SwitchState(myStateStatus);
}
/// <summary>
/// Compares the server attachedEntity to the client one, and updates if needed.
/// </summary>
/// <param name="entity">AttachedEntity in the server session.</param>
private void UpdateAttachedEntity(EntityUid? entity)
{
if (LocalPlayer!.ControlledEntity == entity)
{
return;
}
if (entity == null)
{
LocalPlayer.DetachEntity();
return;
}
LocalPlayer.AttachEntity(entity.Value, _entManager, _client);
}
/// <summary>
@@ -205,87 +175,117 @@ namespace Robust.Client.Player
/// </summary>
private void HandlePlayerList(MsgPlayerList msg)
{
ApplyPlayerStates(msg.Plyrs);
UpdatePlayerList(msg.Plyrs);
}
/// <summary>
/// Compares the server player list to the client one, and updates if needed.
/// </summary>
private bool UpdatePlayerList(IEnumerable<SessionState> remotePlayers, bool fullList)
private void UpdatePlayerList(IEnumerable<PlayerState> remotePlayers)
{
var dirty = false;
var users = new List<NetUserId>();
var hitSet = new List<NetUserId>();
foreach (var state in remotePlayers)
{
users.Add(state.UserId);
hitSet.Add(state.UserId);
if (!EntManager.TryGetEntity(state.ControlledEntity, out var controlled)
&& state.ControlledEntity is {Valid: true})
if (_sessions.TryGetValue(state.UserId, out var session))
{
_pendingStates[state.UserId] = state;
var local = (PlayerSession) session;
var controlled = _entManager.GetEntity(state.ControlledEntity);
// Exists, update data.
if (local.Name == state.Name
&& local.Status == state.Status
&& local.Ping == state.Ping
&& local.AttachedEntity == controlled)
{
continue;
}
dirty = true;
local.Name = state.Name;
local.Status = state.Status;
local.Ping = state.Ping;
local.AttachedEntity = controlled;
}
else
{
_pendingStates.Remove(state.UserId);
}
if (!InternalSessions.TryGetValue(state.UserId, out var session))
{
// This is a new userid, so we create a new session.
DebugTools.Assert(state.UserId != LocalPlayer?.UserId);
var newSession = CreateAndAddSession(state.UserId, state.Name);
newSession.Ping = state.Ping;
newSession.Name = state.Name;
SetStatus(newSession, state.Status);
SetAttachedEntity(newSession, controlled);
// New, give him a slot.
dirty = true;
continue;
}
// Check if the data is actually different
if (session.Name == state.Name
&& session.Status == state.Status
&& session.Ping == state.Ping
&& session.AttachedEntity == controlled)
{
continue;
var newSession = new PlayerSession(state.UserId)
{
Name = state.Name,
Status = state.Status,
Ping = state.Ping,
AttachedEntity = _entManager.GetEntity(state.ControlledEntity),
};
_sessions.Add(state.UserId, newSession);
if (state.UserId == LocalPlayer!.UserId)
{
LocalPlayer.InternalSession = newSession;
newSession.ConnectedClient = _network.ServerChannel!;
// We just connected to the server, hurray!
LocalPlayer.SwitchState(SessionStatus.Connecting, newSession.Status);
}
}
dirty = true;
var local = (CommonSession) session;
local.Name = state.Name;
local.Ping = state.Ping;
SetStatus(local, state.Status);
SetAttachedEntity(local, controlled);
}
// Remove old users. This only works if the provided state is a list of all players
if (fullList)
foreach (var existing in _sessions.Keys.ToArray())
{
foreach (var oldUser in InternalSessions.Keys.ToArray())
// clear slot, player left
if (!hitSet.Contains(existing))
{
// clear slot, player left
if (users.Contains(oldUser))
continue;
DebugTools.Assert(oldUser != LocalUser
|| LocalUser == null
|| LocalUser == default(NetUserId),
"Client is still connected to the server but not in the list of players?");
RemoveSession(oldUser);
_pendingStates.Remove(oldUser);
DebugTools.Assert(LocalPlayer!.UserId != existing || _client.RunLevel == ClientRunLevel.SinglePlayerGame, // replays apply player states.
"I'm still connected to the server, but i left?");
_sessions.Remove(existing);
dirty = true;
}
}
return dirty;
if (dirty)
{
PlayerListUpdated?.Invoke(this, EventArgs.Empty);
}
}
public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
private void OnRunLevelChanged(object? sender, RunLevelChangedEventArgs e)
{
if (LocalEntity == uid)
if (e.NewLevel != ClientRunLevel.SinglePlayerGame)
return;
DebugTools.AssertNotNull(LocalPlayer);
// We do some further setup steps for singleplayer here...
// The local player's GUID in singleplayer will always be the default.
var guid = default(NetUserId);
var session = new PlayerSession(guid)
{
session = LocalSession!;
Name = LocalPlayer!.Name,
Ping = 0,
};
LocalPlayer.UserId = guid;
LocalPlayer.InternalSession = session;
// Add the local session to the list.
_sessions.Add(guid, session);
LocalPlayer.SwitchState(SessionStatus.InGame);
PlayerListUpdated?.Invoke(this, EventArgs.Empty);
}
public bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
{
if (LocalPlayer?.ControlledEntity == uid)
{
session = LocalPlayer.Session;
return true;
}

View File

@@ -0,0 +1,60 @@
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Player
{
internal sealed class PlayerSession : ICommonSession
{
internal SessionStatus Status { get; set; } = SessionStatus.Connecting;
/// <inheritdoc />
SessionStatus ICommonSession.Status
{
get => this.Status;
set => this.Status = value;
}
/// <inheritdoc />
[ViewVariables]
public EntityUid? AttachedEntity { get; set; }
/// <inheritdoc />
[ViewVariables]
public NetUserId UserId { get; }
[ViewVariables]
internal string Name { get; set; } = "<Unknown>";
/// <inheritdoc />
string ICommonSession.Name
{
get => this.Name;
set => this.Name = value;
}
[ViewVariables]
internal short Ping { get; set; }
/// <inheritdoc />
[ViewVariables]
public INetChannel ConnectedClient { get; internal set; } = null!;
/// <inheritdoc />
short ICommonSession.Ping
{
get => this.Ping;
set => this.Ping = value;
}
/// <summary>
/// Creates an instance of a PlayerSession.
/// </summary>
public PlayerSession(NetUserId user)
{
UserId = user;
}
}
}

View File

@@ -7,6 +7,7 @@ using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Threading.Tasks;
using Robust.Client.Upload.Commands;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.Replays;
@@ -100,7 +101,7 @@ public sealed partial class ReplayLoadManager
await callback(0, states.Count, LoadingState.ProcessingFiles, true);
var playerSpan = state0.PlayerStates.Value;
Dictionary<NetUserId, SessionState> playerStates = new(playerSpan.Count);
Dictionary<NetUserId, PlayerState> playerStates = new(playerSpan.Count);
foreach (var player in playerSpan)
{
playerStates.Add(player.UserId, player);
@@ -390,7 +391,7 @@ public sealed partial class ReplayLoadManager
return new EntityState(newState.NetEntity, combined, newState.EntityLastModified, newState.NetComponents ?? oldNetComps);
}
private void UpdatePlayerStates(ReadOnlySpan<SessionState> span, Dictionary<NetUserId, SessionState> playerStates)
private void UpdatePlayerStates(ReadOnlySpan<PlayerState> span, Dictionary<NetUserId, PlayerState> playerStates)
{
foreach (var player in span)
{

View File

@@ -1,5 +1,4 @@
using System;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Shared.Utility;
@@ -59,10 +58,6 @@ internal sealed partial class ReplayPlaybackManager
_timing.LastRealTick = _timing.LastProcessedTick = _timing.CurTick = Replay.CurTick;
_gameState.UpdateFullRep(state, cloneDelta: true);
// Clear existing lerps
_entMan.EntitySysManager.GetEntitySystem<TransformSystem>().Reset();
_gameState.ApplyGameState(state, Replay.NextState);
ProcessMessages(Replay.CurMessages, skipEffectEvents);

View File

@@ -9,7 +9,7 @@ using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Replays;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Value;
@@ -138,9 +138,9 @@ internal sealed class ReplayRecordingManager : SharedReplayRecordingManager
return (state, detachMsg);
}
private SessionState GetPlayerState(ICommonSession session)
private PlayerState GetPlayerState(ICommonSession session)
{
return new SessionState
return new PlayerState
{
UserId = session.UserId,
Status = session.Status,

View File

@@ -17,7 +17,7 @@
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.2" Condition="'$(UseSystemSqlite)' != 'True'" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.NFluidsynth" Version="0.1.1" PrivateAssets="compile" />
<PackageReference Include="NVorbis" Version="0.10.1" PrivateAssets="compile" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.7" />
<PackageReference Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" PrivateAssets="compile" />
<PackageReference Include="OpenTK.OpenAL" Version="4.7.5" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.SharpFont" Version="1.0.1" PrivateAssets="compile" />

View File

@@ -7,6 +7,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using Direction = Robust.Shared.Maths.Direction;
namespace Robust.Client.UserInterface.Controls
{
@@ -17,14 +18,18 @@ namespace Robust.Client.UserInterface.Controls
IEntityManager _entMan;
[ViewVariables]
public SpriteComponent? Sprite { get; private set; }
private SpriteComponent? _sprite;
public SpriteComponent? Sprite
{
get => _sprite;
[Obsolete("Use SetEntity()")]
set => SetEntity(value?.Owner);
}
[ViewVariables]
public EntityUid? Entity { get; private set; }
public Entity<SpriteComponent>? Ent => Entity == null || Sprite == null ? null : (Entity.Value, Sprite);
/// <summary>
/// This field configures automatic scaling of the sprite. This automatic scaling is done before
/// applying the explicitly set scale <see cref="SpriteView.Scale"/>.
@@ -119,22 +124,14 @@ namespace Robust.Client.UserInterface.Controls
public SpriteView()
{
_entMan = IoCManager.Resolve<IEntityManager>();
if (_entMan.TryGetComponent(Entity, out SpriteComponent? sprite))
{
Sprite = sprite;
}
_entMan.TryGetComponent(Entity, out _sprite);
RectClipContent = true;
}
public void SetEntity(EntityUid? uid)
{
Entity = uid;
if (_entMan.TryGetComponent(Entity, out SpriteComponent? sprite))
{
Sprite = sprite;
}
_entMan.TryGetComponent(Entity, out _sprite);
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
@@ -146,13 +143,13 @@ namespace Robust.Client.UserInterface.Controls
private void UpdateSize()
{
if (Entity == null || Sprite == null)
if (Entity == null || _sprite == null)
{
_spriteSize = default;
return;
}
var spriteBox = Sprite.CalculateRotatedBoundingBox(default, _worldRotation ?? Angle.Zero, _eyeRotation)
var spriteBox = _sprite.CalculateRotatedBoundingBox(default, _worldRotation ?? Angle.Zero, _eyeRotation)
.CalcBoundingBox();
if (!SpriteOffset)
@@ -194,10 +191,10 @@ namespace Robust.Client.UserInterface.Controls
internal override void DrawInternal(IRenderHandle renderHandle)
{
if (Entity is not {} uid || Sprite == null)
if (Entity is not {} uid || _sprite == null)
return;
if (Sprite.Deleted)
if (_sprite.Deleted)
{
SetEntity(null);
return;
@@ -217,11 +214,11 @@ namespace Robust.Client.UserInterface.Controls
var offset = SpriteOffset
? Vector2.Zero
: - (-_eyeRotation).RotateVec(Sprite.Offset) * new Vector2(1, -1) * EyeManager.PixelsPerMeter;
: - (-_eyeRotation).RotateVec(_sprite.Offset) * new Vector2(1, -1) * EyeManager.PixelsPerMeter;
var position = PixelSize / 2 + offset * stretch * UIScale;
var scale = Scale * UIScale * stretch;
renderHandle.DrawEntity(uid, position, scale, _worldRotation, _eyeRotation, OverrideDirection, Sprite);
renderHandle.DrawEntity(uid, position, scale, _worldRotation, _eyeRotation, OverrideDirection, _sprite);
}
}
}

View File

@@ -117,7 +117,7 @@ Mouse Pos:
{playerWorldOffset}
{playerCoordinates}
Rotation: {playerRotation.Degrees:F2}°
EntId: {controlledEntity}
EntId: {entityTransform.Owner}
GridUid: {entityTransform.GridUid}
Grid Rotation: {gridRotation.Degrees:F2}°");
}

View File

@@ -15,7 +15,7 @@ using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;

View File

@@ -5,7 +5,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Robust.Client.ViewVariables.Editors
@@ -67,13 +67,13 @@ namespace Robust.Client.ViewVariables.Editors
var xVal = float.Parse(x.Text, CultureInfo.InvariantCulture);
var yVal = float.Parse(y.Text, CultureInfo.InvariantCulture);
if (!entityManager.HasComponent<MapGridComponent>(gridVal))
if (!mapManager.TryGetGrid(gridVal, out var grid))
{
ValueChanged(new EntityCoordinates(EntityUid.Invalid, new(xVal, yVal)));
return;
}
ValueChanged(new EntityCoordinates(gridVal, new(xVal, yVal)));
ValueChanged(new EntityCoordinates(grid.Owner, new(xVal, yVal)));
}
if (!ReadOnly)

View File

@@ -120,11 +120,7 @@ namespace Robust.Client.ViewVariables.Instances
};
top.HorizontalExpand = true;
hBox.AddChild(top);
var view = new SpriteView { OverrideDirection = Direction.South };
view.SetEntity(_entity);
hBox.AddChild(view);
hBox.AddChild(new SpriteView {Sprite = sprite, OverrideDirection = Direction.South});
vBoxContainer.AddChild(hBox);
}
else
@@ -435,7 +431,8 @@ namespace Robust.Client.ViewVariables.Instances
try
{
var comp = componentFactory.GetComponent(registration.Type);
var comp = (Component) componentFactory.GetComponent(registration.Type);
comp.Owner = _entity;
_entityManager.AddComponent(_entity, comp);
}
catch (Exception e)

View File

@@ -61,7 +61,7 @@ internal sealed class AssetPassPackRsis : AssetPass
foreach (var (key, dat) in _foundRsis)
{
if (dat.MetaJson == null)
continue;
return;
RunJob(() =>
{

View File

@@ -21,11 +21,11 @@ public sealed class RobustClientAssetGraph
/// </summary>
public IReadOnlyCollection<AssetPass> AllPasses { get; }
public RobustClientAssetGraph(bool parallel = true)
public RobustClientAssetGraph()
{
// The code injecting the list of source files is assumed to be pretty single-threaded.
// We use a parallelizing input to break out all the work on files coming in onto multiple threads.
Input = new AssetPassPipe { Name = "RobustClientAssetGraphInput", Parallelize = parallel };
Input = new AssetPassPipe { Name = "RobustClientAssetGraphInput", Parallelize = true };
PresetPasses = new AssetPassPipe { Name = "RobustClientAssetGraphPresetPasses" };
Output = new AssetPassPipe { Name = "RobustClientAssetGraphOutput", CheckDuplicates = true };
NormalizeText = new AssetPassNormalizeText { Name = "RobustClientAssetGraphNormalizeText" };

View File

@@ -4,10 +4,9 @@ namespace Robust.Packaging;
public sealed class RobustClientPackaging
{
public static IReadOnlySet<string> ClientIgnoredResources { get; } = new HashSet<string>
public static IReadOnlySet<string> ClientIgnoresResources { get; } = new HashSet<string>
{
"Maps",
"ConfigPresets",
// Leaving this here for future archaeologists to ponder at.
"emotes.xml",
"Groups",
@@ -19,8 +18,48 @@ public sealed class RobustClientPackaging
AssetPass pass,
CancellationToken cancel = default)
{
var ignoreSet = ClientIgnoredResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
var ignoreSet = ClientIgnoresResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
await RobustSharedPackaging.DoResourceCopy(Path.Combine(contentDir, "Resources"), pass, ignoreSet, cancel: cancel);
await RobustSharedPackaging.DoResourceCopy(Path.Combine(contentDir, "Resources"), pass, ignoreSet, cancel);
}
public static async Task WriteContentAssemblies(
AssetPass pass,
string contentDir,
string binDir,
IEnumerable<string> contentAssemblies,
CancellationToken cancel = default)
{
await WriteContentAssemblies("Assemblies", pass, contentDir, binDir, contentAssemblies, cancel);
}
public static Task WriteContentAssemblies(
string target,
AssetPass pass,
string contentDir,
string binDir,
IEnumerable<string> contentAssemblies,
CancellationToken cancel = default)
{
var files = new List<string>();
var sourceDir = Path.Combine(contentDir, "bin", binDir);
foreach (var asm in contentAssemblies)
{
files.Add($"{asm}.dll");
var pdbPath = $"{asm}.pdb";
if (File.Exists(Path.Combine(sourceDir, pdbPath)))
files.Add(pdbPath);
}
foreach (var f in files)
{
cancel.ThrowIfCancellationRequested();
pass.InjectFileFromDisk($"{target}/{f}", Path.Combine(sourceDir, f));
}
return Task.CompletedTask;
}
}

View File

@@ -1,33 +0,0 @@
using Robust.Packaging.AssetProcessing;
namespace Robust.Packaging;
public sealed class RobustServerPackaging
{
public static IReadOnlySet<string> ServerIgnoresResources { get; } = new HashSet<string>
{
"Audio",
"Textures",
"Fonts",
"Shaders",
};
public static async Task WriteServerResources(
string contentDir,
AssetPass pass,
CancellationToken cancel = default)
{
var ignoreSet = ServerIgnoresResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
await RobustSharedPackaging.DoResourceCopy(Path.Combine(contentDir, "Resources"),
pass,
ignoreSet,
"Resources",
cancel);
await RobustSharedPackaging.DoResourceCopy(Path.Combine("RobustToolbox", "Resources"),
pass,
ignoreSet,
"Resources",
cancel);
}
}

View File

@@ -15,53 +15,10 @@ public sealed class RobustSharedPackaging
".DS_Store"
};
// IDK what these are supposed to correspond to but targetDir is the target directory.
public static async Task WriteContentAssemblies(
AssetPass pass,
string contentDir,
string binDir,
IEnumerable<string> contentAssemblies,
string targetDir = "Assemblies",
CancellationToken cancel = default)
{
await WriteContentAssemblies(targetDir, pass, contentDir, binDir, contentAssemblies, cancel);
}
public static Task WriteContentAssemblies(
string target,
AssetPass pass,
string contentDir,
string binDir,
IEnumerable<string> contentAssemblies,
CancellationToken cancel = default)
{
var files = new List<string>();
var sourceDir = Path.Combine(contentDir, "bin", binDir);
foreach (var asm in contentAssemblies)
{
files.Add($"{asm}.dll");
var pdbPath = $"{asm}.pdb";
if (File.Exists(Path.Combine(sourceDir, pdbPath)))
files.Add(pdbPath);
}
foreach (var f in files)
{
cancel.ThrowIfCancellationRequested();
pass.InjectFileFromDisk($"{target}/{f}", Path.Combine(sourceDir, f));
}
return Task.CompletedTask;
}
public static Task DoResourceCopy(
string diskSource,
AssetPass pass,
IReadOnlySet<string> ignoreSet,
string targetDir = "",
CancellationToken cancel = default)
{
foreach (var path in Directory.EnumerateFileSystemEntries(diskSource))
@@ -72,7 +29,7 @@ public sealed class RobustSharedPackaging
if (ignoreSet.Contains(filename))
continue;
var targetPath = Path.Combine(targetDir, filename);
var targetPath = filename;
if (Directory.Exists(path))
CopyDirIntoZip(path, targetPath, pass);
else
@@ -87,10 +44,10 @@ public sealed class RobustSharedPackaging
foreach (var file in Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories))
{
var relPath = Path.GetRelativePath(directory, file);
var zipPath = $"{basePath}/{relPath}";
if (Path.DirectorySeparatorChar != '/')
zipPath = zipPath.Replace(Path.DirectorySeparatorChar, '/');
relPath = relPath.Replace(Path.DirectorySeparatorChar, '/');
var zipPath = $"{basePath}/{relPath}";
// Console.WriteLine($"{directory}/{zipPath} -> /{zipPath}");
pass.InjectFileFromDisk(zipPath, file);

View File

@@ -28,7 +28,6 @@ using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
@@ -307,9 +306,7 @@ namespace Robust.Server
_modLoader.SetUseLoadContext(!ContentStart);
_modLoader.SetEnableSandboxing(Options.Sandboxing);
var resourceManifest = ResourceManifestData.LoadResourceManifest(_resources);
if (!_modLoader.TryLoadModulesFrom(Options.AssemblyDirectory, resourceManifest.AssemblyPrefix ?? Options.ContentModulePrefix))
if (!_modLoader.TryLoadModulesFrom(Options.AssemblyDirectory, Options.ContentModulePrefix))
{
_logger.Fatal("Errors while loading content assemblies.");
return true;

View File

@@ -1,4 +1,5 @@
using JetBrains.Annotations;
using System;
using JetBrains.Annotations;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -46,7 +47,11 @@ namespace Robust.Server.Console.Commands
shell.WriteLine($"Entity {_entityManager.GetComponent<MetaDataComponent>(uid.Value).EntityName} already has a {componentName} component.");
}
var component = _componentFactory.GetComponent(registration.Type);
var component = (Component) _componentFactory.GetComponent(registration.Type);
#pragma warning disable CS0618
component.Owner = uid.Value;
#pragma warning restore CS0618
_entityManager.AddComponent(uid.Value, component);
shell.WriteLine($"Added {componentName} component to entity {_entityManager.GetComponent<MetaDataComponent>(uid.Value).EntityName}.");

View File

@@ -0,0 +1,37 @@
using Robust.Server.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Server.Console.Commands
{
public sealed class DeleteCommand : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entityManager = default!;
public override string Command => "delete";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteLine("You should provide exactly one argument.");
return;
}
if (!NetEntity.TryParse(args[0], out var entityNet))
{
shell.WriteLine("Invalid entity UID.");
return;
}
if (!_entityManager.TryGetEntity(entityNet, out var entity) || !_entityManager.EntityExists(entity))
{
shell.WriteLine("That entity does not exist.");
return;
}
_entityManager.DeleteEntity(entity.Value);
}
}
}

View File

@@ -21,7 +21,7 @@ namespace Robust.Server.Console.Commands
var sb = new StringBuilder();
var players = _players.Sessions;
var players = _players.ServerSessions;
sb.AppendLine($"{"Player Name",20} {"Status",12} {"Playing Time",14} {"Ping",9} {"IP EndPoint",20}");
sb.AppendLine("-------------------------------------------------------------------------------");
@@ -50,8 +50,8 @@ namespace Robust.Server.Console.Commands
{
if (args.Length < 1)
{
var player = shell.Player;
var toKickPlayer = player ?? _players.Sessions.FirstOrDefault();
var player = shell.Player as IPlayerSession;
var toKickPlayer = player ?? _players.ServerSessions.FirstOrDefault();
if (toKickPlayer == null)
{
shell.WriteLine("You need to provide a player to kick.");
@@ -79,7 +79,7 @@ namespace Robust.Server.Console.Commands
{
if (args.Length == 1)
{
var options = _players.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
var options = _players.ServerSessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
return CompletionResult.FromHintOptions(options, "<PlayerIndex>");
}

View File

@@ -1,8 +1,9 @@
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
using Robust.Shared.Players;
namespace Robust.Server.Console.Commands
{
@@ -16,7 +17,7 @@ namespace Robust.Server.Console.Commands
{
var session = shell.Player;
if (session is not { } playerSession)
if (session is not IPlayerSession playerSession)
{
shell.WriteError($"Unable to find {nameof(ICommonSession)} for shell");
return;
@@ -53,7 +54,7 @@ namespace Robust.Server.Console.Commands
{
var session = shell.Player;
if (session is not { } playerSession)
if (session is not IPlayerSession playerSession)
{
shell.WriteError($"Unable to find {nameof(ICommonSession)} for shell");
return;

View File

@@ -1,4 +1,6 @@
using Robust.Shared.Player;
using Robust.Server.Player;
using Robust.Shared.Map;
using Robust.Shared.Players;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Errors;
@@ -8,27 +10,27 @@ namespace Robust.Server.Console
{
public IConGroupControllerImplementation? Implementation { get; set; }
public bool CanCommand(ICommonSession session, string cmdName)
public bool CanCommand(IPlayerSession session, string cmdName)
{
return Implementation?.CanCommand(session, cmdName) ?? false;
}
public bool CanAdminPlace(ICommonSession session)
public bool CanAdminPlace(IPlayerSession session)
{
return Implementation?.CanAdminPlace(session) ?? false;
}
public bool CanScript(ICommonSession session)
public bool CanScript(IPlayerSession session)
{
return Implementation?.CanScript(session) ?? false;
}
public bool CanAdminMenu(ICommonSession session)
public bool CanAdminMenu(IPlayerSession session)
{
return Implementation?.CanAdminMenu(session) ?? false;
}
public bool CanAdminReloadPrototypes(ICommonSession session)
public bool CanAdminReloadPrototypes(IPlayerSession session)
{
return Implementation?.CanAdminReloadPrototypes(session) ?? false;
}

View File

@@ -1,14 +1,14 @@
using Robust.Shared.Player;
using Robust.Server.Player;
using Robust.Shared.Toolshed;
namespace Robust.Server.Console
{
public interface IConGroupControllerImplementation : IPermissionController
{
bool CanCommand(ICommonSession session, string cmdName);
bool CanAdminPlace(ICommonSession session);
bool CanScript(ICommonSession session);
bool CanAdminMenu(ICommonSession session);
bool CanAdminReloadPrototypes(ICommonSession session);
bool CanCommand(IPlayerSession session, string cmdName);
bool CanAdminPlace(IPlayerSession session);
bool CanScript(IPlayerSession session);
bool CanAdminMenu(IPlayerSession session);
bool CanAdminReloadPrototypes(IPlayerSession session);
}
}

View File

@@ -5,9 +5,10 @@ using System.Threading.Tasks;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Utility;
@@ -41,7 +42,7 @@ namespace Robust.Server.Console
var msg = new MsgConCmd();
msg.Text = command;
NetManager.ServerSendMessage(msg, session.Channel);
NetManager.ServerSendMessage(msg, ((IPlayerSession)session).ConnectedClient);
}
/// <inheritdoc />
@@ -49,12 +50,18 @@ namespace Robust.Server.Console
{
var msg = new FormattedMessage();
msg.AddText(text);
OutputText(session, msg, false);
if (session is IPlayerSession playerSession)
OutputText(playerSession, msg, false);
else
OutputText(null, msg, false);
}
public override void WriteLine(ICommonSession? session, FormattedMessage msg)
{
OutputText(session, msg, false);
if (session is IPlayerSession playerSession)
OutputText(playerSession, msg, false);
else
OutputText(null, msg, false);
}
/// <inheritdoc />
@@ -62,7 +69,10 @@ namespace Robust.Server.Console
{
var msg = new FormattedMessage();
msg.AddText(text);
OutputText(session, msg, true);
if (session is IPlayerSession playerSession)
OutputText(playerSession, msg, true);
else
OutputText(null, msg, true);
}
public bool IsCmdServer(IConsoleCommand cmd) => true;
@@ -146,7 +156,7 @@ namespace Robust.Server.Console
private bool ShellCanExecute(IConsoleShell shell, string cmdName)
{
return shell.Player == null || _groupController.CanCommand(shell.Player, cmdName);
return shell.Player == null || _groupController.CanCommand((IPlayerSession)shell.Player, cmdName);
}
private void HandleRegistrationRequest(INetChannel senderConnection)
@@ -154,40 +164,16 @@ namespace Robust.Server.Console
var message = new MsgConCmdReg();
var counter = 0;
var toolshedCommands = _toolshed.DefaultEnvironment.AllCommands().ToArray();
message.Commands = new List<MsgConCmdReg.Command>(AvailableCommands.Count + toolshedCommands.Length);
var commands = new HashSet<string>();
message.Commands = new MsgConCmdReg.Command[AvailableCommands.Count];
foreach (var command in AvailableCommands.Values)
{
if (!commands.Add(command.Command))
{
Sawmill.Error($"Duplicate command: {command.Command}");
continue;
}
message.Commands.Add(new MsgConCmdReg.Command
message.Commands[counter++] = new MsgConCmdReg.Command
{
Name = command.Command,
Description = command.Description,
Help = command.Help
});
}
foreach (var spec in toolshedCommands)
{
var name = spec.FullName();
if (!commands.Add(name))
{
Sawmill.Warning($"Duplicate toolshed command: {name}");
continue;
}
message.Commands.Add(new MsgConCmdReg.Command
{
Name = name,
Description = spec.Cmd.Description(spec.SubCommand),
Help = spec.Cmd.GetHelp(spec.SubCommand)
});
};
}
NetManager.ServerSendMessage(message, senderConnection);
@@ -204,7 +190,7 @@ namespace Robust.Server.Console
ExecuteCommand(session, text);
}
private void OutputText(ICommonSession? session, FormattedMessage text, bool error)
private void OutputText(IPlayerSession? session, FormattedMessage text, bool error)
{
if (session != null)
{

View File

@@ -0,0 +1,13 @@
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Robust.Server.GameObjects
{
[RegisterComponent]
public sealed partial class ActorComponent : Component
{
[ViewVariables]
public IPlayerSession PlayerSession { get; internal set; } = default!;
}
}

View File

@@ -1,12 +1,12 @@
using System.Collections.Generic;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
namespace Robust.Server.GameObjects
{
[RegisterComponent]
internal sealed partial class ViewSubscriberComponent : Component
{
internal readonly HashSet<ICommonSession> SubscribedSessions = new();
internal readonly HashSet<IPlayerSession> SubscribedSessions = new();
}
}

View File

@@ -1,5 +1,8 @@
using System;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Server.GameObjects
{
@@ -13,5 +16,13 @@ namespace Robust.Server.GameObjects
/// </summary>
[DataField("layer")]
public int Layer = 1;
[ViewVariables(VVAccess.ReadWrite)]
[Obsolete("Do not access directly, only exists for VV")]
public int LayerVV
{
get => Layer;
set => EntitySystem.Get<VisibilitySystem>().SetLayer(this, value);
}
}
}

View File

@@ -4,7 +4,6 @@ using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Server.GameObjects
@@ -31,7 +30,7 @@ namespace Robust.Server.GameObjects
/// <param name="player">The player to attach to the entity</param>
/// <param name="force">Whether to kick any existing players from the entity</param>
/// <returns>Whether the attach succeeded, or not.</returns>
public bool Attach(EntityUid? uid, ICommonSession player, bool force = false)
public bool Attach(EntityUid? uid, IPlayerSession player, bool force = false)
{
return Attach(uid, player, false, out _);
}
@@ -44,26 +43,25 @@ namespace Robust.Server.GameObjects
/// <param name="force">Whether to kick any existing players from the entity</param>
/// <param name="forceKicked">The player that was forcefully kicked, or null.</param>
/// <returns>Whether the attach succeeded, or not.</returns>
public bool Attach(EntityUid? entity, ICommonSession player, bool force, out ICommonSession? forceKicked)
public bool Attach(EntityUid? entity, IPlayerSession player, bool force, out IPlayerSession? forceKicked)
{
// Null by default.
forceKicked = null;
if (player.AttachedEntity == entity)
{
DebugTools.Assert(entity == null || HasComp<ActorComponent>(entity));
return true;
}
if (entity is not { } uid)
return Detach(player);
// Cannot attach to a deleted, nonexisting or terminating entity.
if (TerminatingOrDeleted(uid))
if (!TryComp(uid, out MetaDataComponent? meta) || meta.EntityLifeStage > EntityLifeStage.MapInitialized)
{
return false;
}
// Check if there was a player attached to the entity already...
if (TryComp(uid, out ActorComponent? actor))
if (EntityManager.TryGetComponent(uid, out ActorComponent? actor))
{
// If we're not forcing the attach, this fails.
if (!force)
@@ -71,22 +69,22 @@ namespace Robust.Server.GameObjects
// Set the event's force-kicked session before detaching it.
forceKicked = actor.PlayerSession;
RemComp(uid, actor);
DebugTools.AssertNull(forceKicked.AttachedEntity);
Detach(uid, actor);
}
// Detach from the currently attached entity.
Detach(player);
if (!Detach(player))
return false;
// We add the actor component.
actor = EntityManager.AddComponent<ActorComponent>(uid);
EntityManager.EnsureComponent<EyeComponent>(uid);
actor.PlayerSession = player;
_playerManager.SetAttachedEntity(player, uid);
DebugTools.Assert(player.AttachedEntity == entity);
player.SetAttachedEntity(uid);
// The player is fully attached now, raise an event!
RaiseLocalEvent(uid, new PlayerAttachedEvent(uid, player, forceKicked), true);
DebugTools.Assert(player.AttachedEntity == entity);
return true;
}
@@ -110,33 +108,21 @@ namespace Robust.Server.GameObjects
/// <param name="player">The player session that will be detached from any attached entities.</param>
/// <returns>Whether the player is now detached from any entities.
/// This returns true if the player wasn't attached to any entity.</returns>
public bool Detach(ICommonSession player, ActorComponent? actor = null)
public bool Detach(IPlayerSession player)
{
var uid = player.AttachedEntity;
if (uid == null)
return true;
if (!Resolve(uid.Value, ref actor, false))
{
Log.Error($"Player {player} was attached to a deleted entity?");
((CommonSession) player).AttachedEntity = null;
return true;
}
RemComp(uid.Value, actor);
DebugTools.AssertNull(player.AttachedEntity);
return false;
return uid == null || Detach(uid.Value);
}
private void OnActorShutdown(EntityUid entity, ActorComponent component, ComponentShutdown args)
{
_playerManager.SetAttachedEntity(component.PlayerSession, null);
component.PlayerSession.SetAttachedEntity(null);
// The player is fully detached now that the component has shut down.
RaiseLocalEvent(entity, new PlayerDetachedEvent(entity, component.PlayerSession), true);
}
public bool TryGetActorFromUserId(NetUserId? userId, [NotNullWhen(true)] out ICommonSession? actor, out EntityUid? actorEntity)
public bool TryGetActorFromUserId(NetUserId? userId, [NotNullWhen(true)] out IPlayerSession? actor, [MaybeNullWhen(true)] out EntityUid? actorEntity)
{
actor = null;
actorEntity = null;
@@ -157,14 +143,14 @@ namespace Robust.Server.GameObjects
public sealed class PlayerAttachedEvent : EntityEventArgs
{
public EntityUid Entity { get; }
public ICommonSession Player { get; }
public IPlayerSession Player { get; }
/// <summary>
/// The player session that was forcefully kicked from the entity, if any.
/// </summary>
public ICommonSession? Kicked { get; }
public IPlayerSession? Kicked { get; }
public PlayerAttachedEvent(EntityUid entity, ICommonSession player, ICommonSession? kicked = null)
public PlayerAttachedEvent(EntityUid entity, IPlayerSession player, IPlayerSession? kicked = null)
{
Entity = entity;
Player = player;
@@ -178,9 +164,9 @@ namespace Robust.Server.GameObjects
public sealed class PlayerDetachedEvent : EntityEventArgs
{
public EntityUid Entity { get; }
public ICommonSession Player { get; }
public IPlayerSession Player { get; }
public PlayerDetachedEvent(EntityUid entity, ICommonSession player)
public PlayerDetachedEvent(EntityUid entity, IPlayerSession player)
{
Entity = entity;
Player = player;

View File

@@ -6,6 +6,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Players;
namespace Robust.Server.GameObjects;
[UsedImplicitly]

View File

@@ -5,7 +5,6 @@ using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Player;
namespace Robust.Server.GameObjects
{
@@ -16,10 +15,10 @@ namespace Robust.Server.GameObjects
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
private readonly Dictionary<ICommonSession, IPlayerCommandStates> _playerInputs = new();
private readonly Dictionary<IPlayerSession, IPlayerCommandStates> _playerInputs = new();
private readonly Dictionary<ICommonSession, uint> _lastProcessedInputCmd = new();
private readonly Dictionary<IPlayerSession, uint> _lastProcessedInputCmd = new();
/// <inheritdoc />
public override void Initialize()
@@ -49,7 +48,7 @@ namespace Robust.Server.GameObjects
if (!Enum.IsDefined(typeof(BoundKeyState), msg.State))
return;
var session = eventArgs.SenderSession;
var session = (IPlayerSession) eventArgs.SenderSession;
if (_lastProcessedInputCmd[session] < msg.InputSequence)
_lastProcessedInputCmd[session] = msg.InputSequence;
@@ -66,12 +65,12 @@ namespace Robust.Server.GameObjects
}
}
public IPlayerCommandStates GetInputStates(ICommonSession session)
public IPlayerCommandStates GetInputStates(IPlayerSession session)
{
return _playerInputs[session];
}
public uint GetLastInputCommand(ICommonSession session)
public uint GetLastInputCommand(IPlayerSession session)
{
return _lastProcessedInputCmd[session];
}

View File

@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Numerics;
using Robust.Server.Maps;
using Robust.Shared.Collections;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -732,7 +733,7 @@ public sealed class MapLoaderSystem : EntitySystem
return;
// get ents that the grids will bind to
var gridComps = new Entity<MapGridComponent>[yamlGrids.Count];
var gridComps = new MapGridComponent[yamlGrids.Count];
var gridQuery = _serverEntityManager.GetEntityQuery<MapGridComponent>();
// linear search for new grid comps
@@ -744,7 +745,7 @@ public sealed class MapLoaderSystem : EntitySystem
// These should actually be new, pre-init
DebugTools.Assert(gridComp.LifeStage == ComponentLifeStage.Added);
gridComps[gridComp.GridIndex] = new Entity<MapGridComponent>(uid, gridComp);
gridComps[gridComp.GridIndex] = gridComp;
}
for (var index = 0; index < yamlGrids.Count; index++)
@@ -763,18 +764,18 @@ public sealed class MapLoaderSystem : EntitySystem
MappingDataNode yamlGridInfo = (MappingDataNode)yamlGrid["settings"];
SequenceDataNode yamlGridChunks = (SequenceDataNode)yamlGrid["chunks"];
AllocateMapGrid(gridComp, yamlGridInfo);
var gridUid = gridComp.Owner;
var grid = AllocateMapGrid(gridComp, yamlGridInfo);
var gridUid = grid.Owner;
foreach (var chunkNode in yamlGridChunks.Cast<MappingDataNode>())
{
var (chunkOffsetX, chunkOffsetY) = _serManager.Read<Vector2i>(chunkNode["ind"]);
_serManager.Read(chunkNode, _context, instanceProvider: () => _mapSystem.GetOrAddChunk(gridUid, gridComp, chunkOffsetX, chunkOffsetY), notNullableOverride: true);
_serManager.Read(chunkNode, _context, instanceProvider: () => _mapSystem.GetOrAddChunk(gridUid, grid, chunkOffsetX, chunkOffsetY), notNullableOverride: true);
}
}
}
private static void AllocateMapGrid(MapGridComponent gridComp, MappingDataNode yamlGridInfo)
private static MapGridComponent AllocateMapGrid(MapGridComponent gridComp, MappingDataNode yamlGridInfo)
{
// sane defaults
ushort csz = 16;
@@ -794,6 +795,8 @@ public sealed class MapLoaderSystem : EntitySystem
gridComp.ChunkSize = csz;
gridComp.TileSize = tsz;
return gridComp;
}
private void StartupEntities(MapData data)

View File

@@ -66,8 +66,9 @@ namespace Robust.Server.GameObjects
private void HandleGridEmpty(EntityUid uid, MapGridComponent component, EmptyGridEvent args)
{
if (!_deleteEmptyGrids || TerminatingOrDeleted(uid) || HasComp<MapComponent>(uid))
return;
if (!_deleteEmptyGrids) return;
if (!EntityManager.EntityExists(uid)) return;
if (EntityManager.GetComponent<MetaDataComponent>(uid).EntityLifeStage >= EntityLifeStage.Terminating) return;
MapManager.DeleteGrid(args.GridId);
}

View File

@@ -7,7 +7,7 @@ using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Utility;
namespace Robust.Server.GameObjects
@@ -194,7 +194,7 @@ namespace Robust.Server.GameObjects
return bui.SubscribedSessions.Count > 0;
}
public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, IPlayerSession session, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
@@ -218,7 +218,7 @@ namespace Robust.Server.GameObjects
public bool TrySetUiState(EntityUid uid,
Enum uiKey,
BoundUserInterfaceState state,
ICommonSession? session = null,
IPlayerSession? session = null,
UserInterfaceComponent? ui = null,
bool clearOverrides = true)
{
@@ -284,7 +284,7 @@ namespace Robust.Server.GameObjects
#region Open
public bool TryOpen(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
public bool TryOpen(EntityUid uid, Enum uiKey, IPlayerSession session, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
@@ -319,7 +319,7 @@ namespace Robust.Server.GameObjects
#endregion
#region Close
public bool TryClose(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
public bool TryClose(EntityUid uid, Enum uiKey, IPlayerSession session, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;

View File

@@ -1,5 +1,5 @@
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
namespace Robust.Server.GameObjects
{
@@ -18,7 +18,7 @@ namespace Robust.Server.GameObjects
/// <summary>
/// Subscribes the session to get PVS updates from the point of view of the specified entity.
/// </summary>
public void AddViewSubscriber(EntityUid uid, ICommonSession session)
public void AddViewSubscriber(EntityUid uid, IPlayerSession session)
{
// If the entity doesn't have the component, it will be added.
var viewSubscriber = EntityManager.EnsureComponent<ViewSubscriberComponent>(uid);
@@ -27,7 +27,7 @@ namespace Robust.Server.GameObjects
return; // Already subscribed, do nothing else.
viewSubscriber.SubscribedSessions.Add(session);
session.ViewSubscriptions.Add(uid);
session.AddViewSubscription(uid);
RaiseLocalEvent(uid, new ViewSubscriberAddedEvent(uid, session), true);
}
@@ -35,7 +35,7 @@ namespace Robust.Server.GameObjects
/// <summary>
/// Unsubscribes the session from getting PVS updates from the point of view of the specified entity.
/// </summary>
public void RemoveViewSubscriber(EntityUid uid, ICommonSession session)
public void RemoveViewSubscriber(EntityUid uid, IPlayerSession session)
{
if(!EntityManager.TryGetComponent(uid, out ViewSubscriberComponent? viewSubscriber))
return; // Entity didn't have any subscriptions, do nothing.
@@ -43,7 +43,7 @@ namespace Robust.Server.GameObjects
if (!viewSubscriber.SubscribedSessions.Remove(session))
return; // Session wasn't subscribed, do nothing.
session.ViewSubscriptions.Remove(uid);
session.RemoveViewSubscription(uid);
RaiseLocalEvent(uid, new ViewSubscriberRemovedEvent(uid, session), true);
}
@@ -51,7 +51,7 @@ namespace Robust.Server.GameObjects
{
foreach (var session in component.SubscribedSessions)
{
session.ViewSubscriptions.Remove(uid);
session.RemoveViewSubscription(uid);
}
}
}
@@ -62,9 +62,9 @@ namespace Robust.Server.GameObjects
public sealed class ViewSubscriberAddedEvent : EntityEventArgs
{
public EntityUid View { get; }
public ICommonSession Subscriber { get; }
public IPlayerSession Subscriber { get; }
public ViewSubscriberAddedEvent(EntityUid view, ICommonSession subscriber)
public ViewSubscriberAddedEvent(EntityUid view, IPlayerSession subscriber)
{
View = view;
Subscriber = subscriber;
@@ -78,9 +78,9 @@ namespace Robust.Server.GameObjects
public sealed class ViewSubscriberRemovedEvent : EntityEventArgs
{
public EntityUid View { get; }
public ICommonSession Subscriber { get; }
public IPlayerSession Subscriber { get; }
public ViewSubscriberRemovedEvent(EntityUid view, ICommonSession subscriber)
public ViewSubscriberRemovedEvent(EntityUid view, IPlayerSession subscriber)
{
View = view;
Subscriber = subscriber;

View File

@@ -1,14 +1,13 @@
using System;
using Robust.Server.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables;
namespace Robust.Server.GameObjects
{
public sealed class VisibilitySystem : EntitySystem
{
[Dependency] private readonly PvsSystem _pvs = default!;
[Dependency] private readonly IViewVariablesManager _vvManager = default!;
private EntityQuery<TransformComponent> _xformQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
@@ -22,15 +21,6 @@ namespace Robust.Server.GameObjects
_visiblityQuery = GetEntityQuery<VisibilityComponent>();
SubscribeLocalEvent<EntParentChangedMessage>(OnParentChange);
EntityManager.EntityInitialized += OnEntityInit;
_vvManager.GetTypeHandler<VisibilityComponent>()
.AddPath(nameof(VisibilityComponent.Layer), (_, comp) => comp.Layer, (uid, value, comp) =>
{
if (!Resolve(uid, ref comp))
return;
SetLayer(uid, comp, value);
});
}
public override void Shutdown()
@@ -50,6 +40,12 @@ namespace Robust.Server.GameObjects
RefreshVisibility(uid, visibilityComponent: component);
}
[Obsolete("Use overload that takes an EntityUid instead")]
public void AddLayer(VisibilityComponent component, int layer, bool refresh = true)
{
AddLayer(component.Owner, component, layer, refresh);
}
public void RemoveLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true)
{
if ((layer & component.Layer) != layer)
@@ -61,6 +57,12 @@ namespace Robust.Server.GameObjects
RefreshVisibility(uid, visibilityComponent: component);
}
[Obsolete("Use overload that takes an EntityUid instead")]
public void RemoveLayer(VisibilityComponent component, int layer, bool refresh = true)
{
RemoveLayer(component.Owner, component, layer, refresh);
}
public void SetLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true)
{
if (component.Layer == layer)
@@ -72,6 +74,12 @@ namespace Robust.Server.GameObjects
RefreshVisibility(uid, visibilityComponent: component);
}
[Obsolete("Use overload that takes an EntityUid instead")]
public void SetLayer(VisibilityComponent component, int layer, bool refresh = true)
{
SetLayer(component.Owner, component, layer, refresh);
}
private void OnParentChange(ref EntParentChangedMessage ev)
{
RefreshVisibility(ev.Entity);
@@ -119,6 +127,12 @@ namespace Robust.Server.GameObjects
}
}
[Obsolete("Use overload that takes an EntityUid instead")]
public void RefreshVisibility(VisibilityComponent visibilityComponent)
{
RefreshVisibility(visibilityComponent.Owner, visibilityComponent);
}
private int GetParentVisibilityMask(EntityUid uid, VisibilityComponent? visibilityComponent = null)
{
int visMask = 1; // apparently some content expects everything to have the first bit/flag set to true.

View File

@@ -1,10 +1,12 @@
using System.Collections.Generic;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Robust.Server.GameObjects
{
public interface IServerEntityNetworkManager : IEntityNetworkManager
{
uint GetLastMessageSequence(ICommonSession session);
uint GetLastMessageSequence(IPlayerSession session);
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Prometheus;
using Robust.Server.Player;
@@ -14,7 +15,6 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
@@ -131,7 +131,7 @@ namespace Robust.Server.GameObjects
private readonly PriorityQueue<MsgEntity> _queue = new(new MessageSequenceComparer());
private readonly Dictionary<ICommonSession, uint> _lastProcessedSequencesCmd =
private readonly Dictionary<IPlayerSession, uint> _lastProcessedSequencesCmd =
new();
private bool _logLateMsgs;
@@ -162,7 +162,7 @@ namespace Robust.Server.GameObjects
EntitiesCount.Set(Entities.Count);
}
public uint GetLastMessageSequence(ICommonSession session)
public uint GetLastMessageSequence(IPlayerSession session)
{
return _lastProcessedSequencesCmd[session];
}

View File

@@ -1,6 +1,6 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Timing;
namespace Robust.Server.GameStates

View File

@@ -8,7 +8,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;

View File

@@ -1,5 +1,5 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Players;
namespace Robust.Server.GameStates;

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;

View File

@@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -68,7 +68,7 @@ namespace Robust.Server.GameStates
return true;
}
public void CleanupDirty(IEnumerable<ICommonSession> sessions)
public void CleanupDirty(IEnumerable<IPlayerSession> sessions)
{
if (!CullingEnabled)
{

View File

@@ -15,7 +15,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -35,16 +35,10 @@ internal sealed partial class PvsSystem : EntitySystem
public const float ChunkSize = 8;
// TODO make this a cvar. Make it in terms of seconds and tie it to tick rate?
// Main issue is that I CBF figuring out the logic for handling it changing mid-game.
public const int DirtyBufferSize = 20;
// Note: If a client has ping higher than TickBuffer / TickRate, then the server will treat every entity as if it
// had entered PVS for the first time. Note that due to the PVS budget, this buffer is easily overwhelmed.
/// <summary>
/// See <see cref="CVars.NetForceAckThreshold"/>.
/// </summary>
public int ForceAckThreshold { get; private set; }
/// <summary>
/// Maximum number of pooled objects
/// </summary>
@@ -145,7 +139,6 @@ internal sealed partial class PvsSystem : EntitySystem
_configManager.OnValueChanged(CVars.NetPVS, SetPvs, true);
_configManager.OnValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged, true);
_configManager.OnValueChanged(CVars.NetForceAckThreshold, OnForceAckChanged, true);
_serverGameStateManager.ClientAck += OnClientAck;
_serverGameStateManager.ClientRequestFull += OnClientRequestFull;
@@ -163,7 +156,6 @@ internal sealed partial class PvsSystem : EntitySystem
_configManager.UnsubValueChanged(CVars.NetPVS, SetPvs);
_configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged);
_configManager.UnsubValueChanged(CVars.NetForceAckThreshold, OnForceAckChanged);
_serverGameStateManager.ClientAck -= OnClientAck;
_serverGameStateManager.ClientRequestFull -= OnClientRequestFull;
@@ -207,7 +199,7 @@ internal sealed partial class PvsSystem : EntitySystem
// return last acked to pool, but only if it is not still in the OverflowDictionary.
if (sessionData.LastAcked != null && !sessionData.SentEntities.ContainsKey(sessionData.LastAcked.Value.Tick))
{
DebugTools.Assert(!sessionData.SentEntities.Values.Contains(sessionData.LastAcked.Value.Data));
DebugTools.Assert(sessionData.SentEntities.Values.Contains(sessionData.LastAcked.Value.Data));
_visSetPool.Return(sessionData.LastAcked.Value.Data);
}
@@ -220,11 +212,6 @@ internal sealed partial class PvsSystem : EntitySystem
_viewSize = obj * 2;
}
private void OnForceAckChanged(int value)
{
ForceAckThreshold = value;
}
private void SetPvs(bool value)
{
_seenAllEnts.Clear();
@@ -433,7 +420,7 @@ internal sealed partial class PvsSystem : EntitySystem
#endregion
public (List<(int, IChunkIndexLocation)> , HashSet<int>[], EntityUid[][] viewers) GetChunks(ICommonSession[] sessions)
public (List<(int, IChunkIndexLocation)> , HashSet<int>[], EntityUid[][] viewers) GetChunks(IPlayerSession[] sessions)
{
var playerChunks = new HashSet<int>[sessions.Length];
var viewerEntities = new EntityUid[sessions.Length][];
@@ -702,7 +689,7 @@ internal sealed partial class PvsSystem : EntitySystem
}
internal (List<EntityState>? updates, List<NetEntity>? deletions, List<NetEntity>? leftPvs, GameTick fromTick)
CalculateEntityStates(ICommonSession session,
CalculateEntityStates(IPlayerSession session,
GameTick fromTick,
GameTick toTick,
(Dictionary<NetEntity, MetaDataComponent> metadata, RobustTree<NetEntity> tree)?[] chunks,
@@ -710,8 +697,8 @@ internal sealed partial class PvsSystem : EntitySystem
EntityUid[] viewers)
{
DebugTools.Assert(session.Status == SessionStatus.InGame);
var newEntityBudget = _netConfigManager.GetClientCVar(session.Channel, CVars.NetPVSEntityBudget);
var enteredEntityBudget = _netConfigManager.GetClientCVar(session.Channel, CVars.NetPVSEntityEnterBudget);
var newEntityBudget = _netConfigManager.GetClientCVar(session.ConnectedClient, CVars.NetPVSEntityBudget);
var enteredEntityBudget = _netConfigManager.GetClientCVar(session.ConnectedClient, CVars.NetPVSEntityEnterBudget);
var newEntityCount = 0;
var enteredEntityCount = 0;
var sessionData = PlayerData[session];
@@ -888,7 +875,7 @@ internal sealed partial class PvsSystem : EntitySystem
return null;
var tick = _gameTiming.CurTick;
var minSize = Math.Max(0, lastSent.Count - visibleEnts.Count);
var minSize = Math.Max(0, lastSent.Count - lastSent.Count);
var leftView = new List<NetEntity>(minSize);
foreach (var netEntity in lastSent.Keys)
@@ -1142,8 +1129,8 @@ internal sealed partial class PvsSystem : EntitySystem
var query = EntityManager.AllEntityQueryEnumerator<MetaDataComponent>();
while (query.MoveNext(out var uid, out var md))
{
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized, $"Entity {ToPrettyString(uid)} has not been initialized");
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating, $"Entity {ToPrettyString(uid)} is/has been terminated");
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating);
if (md.EntityLastModifiedTick <= fromTick)
continue;
@@ -1178,10 +1165,10 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
if (!toSend.Add(uid) || !_metaQuery.TryGetComponent(uid, out var md))
continue;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized, $"Entity {ToPrettyString(uid)} has not been initialized");
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating, $"Entity {ToPrettyString(uid)} is/has been terminated");
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick, $"Entity {ToPrettyString(uid)} last modified tick is less than creation tick");
DebugTools.Assert(md.EntityLastModifiedTick > fromTick, $"Entity {ToPrettyString(uid)} last modified tick is less than from tick");
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating);
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick);
DebugTools.Assert(md.EntityLastModifiedTick > fromTick);
var state = GetEntityState(player, uid, fromTick, md);
@@ -1205,10 +1192,10 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
if (!toSend.Add(uid) || !_metaQuery.TryGetComponent(uid, out var md))
continue;
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized, $"Entity {ToPrettyString(uid)} has not been initialized");
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating, $"Entity {ToPrettyString(uid)} is/has been terminated");
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick, $"Entity {ToPrettyString(uid)} last modified tick is less than creation tick");
DebugTools.Assert(md.EntityLastModifiedTick > fromTick, $"Entity {ToPrettyString(uid)} last modified tick is less than from tick");
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating);
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick);
DebugTools.Assert(md.EntityLastModifiedTick > fromTick);
var state = GetEntityState(player, uid, fromTick, md);
if (!state.Empty)
@@ -1315,11 +1302,11 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
private EntityUid[] GetSessionViewers(ICommonSession session)
{
if (session.Status != SessionStatus.InGame)
if (session.Status != SessionStatus.InGame || session is not IPlayerSession sess)
return Array.Empty<EntityUid>();
// Fast path
if (session.ViewSubscriptions.Count == 0)
if (sess.ViewSubscriptionCount == 0)
{
if (session.AttachedEntity == null)
return Array.Empty<EntityUid>();
@@ -1331,7 +1318,11 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
if (session.AttachedEntity != null)
viewers.Add(session.AttachedEntity.Value);
viewers.UnionWith(session.ViewSubscriptions);
foreach (var uid in sess.ViewSubscriptions)
{
viewers.Add(uid);
}
return viewers.ToArray();
}
@@ -1421,7 +1412,7 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
[ByRefEvent]
public struct ExpandPvsEvent
{
public readonly ICommonSession Session;
public readonly IPlayerSession Session;
/// <summary>
/// List of entities that will get added to this session's PVS set.
@@ -1434,7 +1425,7 @@ public struct ExpandPvsEvent
/// </summary>
public List<EntityUid>? RecursiveEntities;
public ExpandPvsEvent(ICommonSession session)
public ExpandPvsEvent(IPlayerSession session)
{
Session = session;
}

View File

@@ -26,7 +26,7 @@ using Prometheus;
using Robust.Server.Replays;
using Robust.Shared.Console;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Players;
namespace Robust.Server.GameStates
{
@@ -168,7 +168,7 @@ Oldest acked clients: {string.Join(", ", players)}
/// <inheritdoc />
public void SendGameStateUpdate()
{
var players = _playerManager.Sessions.Where(o => o.Status == SessionStatus.InGame).ToArray();
var players = _playerManager.ServerSessions.Where(o => o.Status == SessionStatus.InGame).ToArray();
// Update entity positions in PVS chunks/collections
// TODO disable processing if culling is disabled? Need to check if toggling PVS breaks anything.
@@ -224,7 +224,7 @@ Oldest acked clients: {string.Join(", ", players)}
_pvs.CullDeletionHistory(oldestAck);
}
private GameTick SendStates(ICommonSession[] players, PvsData? pvsData)
private GameTick SendStates(IPlayerSession[] players, PvsData? pvsData)
{
var inputSystem = _systemManager.GetEntitySystem<InputSystem>();
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
@@ -265,7 +265,7 @@ Oldest acked clients: {string.Join(", ", players)}
public (Dictionary<NetEntity, MetaDataComponent> metadata, RobustTree<NetEntity> tree)?[] ChunkCache;
}
private PvsData? GetPVSData(ICommonSession[] players)
private PvsData? GetPVSData(IPlayerSession[] players)
{
var (chunks, playerChunks, viewerEntities) = _pvs.GetChunks(players);
const int ChunkBatchSize = 2;
@@ -319,11 +319,11 @@ Oldest acked clients: {string.Join(", ", players)}
private void SendStateUpdate(int i,
PvsThreadResources resources,
InputSystem inputSystem,
ICommonSession session,
IPlayerSession session,
PvsData? pvsData,
ref uint oldestAckValue)
{
var channel = session.Channel;
var channel = session.ConnectedClient;
var sessionData = _pvs.PlayerData[session];
var lastAck = sessionData.LastReceivedAck;
List<NetEntity>? leftPvs = null;
@@ -347,7 +347,7 @@ Oldest acked clients: {string.Join(", ", players)}
(entStates, deletions, fromTick) = _pvs.GetAllEntityStates(session, lastAck, _gameTiming.CurTick);
}
var playerStates = _playerManager.GetPlayerStates(fromTick);
var playerStates = _playerManager.GetPlayerStates(lastAck);
// lastAck varies with each client based on lag and such, we can't just make 1 global state and send it to everyone
var lastInputCommand = inputSystem.GetLastInputCommand(session);
@@ -368,30 +368,11 @@ Oldest acked clients: {string.Join(", ", players)}
stateUpdateMessage.State = state;
stateUpdateMessage.CompressionContext = resources.CompressionContext;
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
// If the state is too big we let Lidgren send it reliably. This is to avoid a situation where a state is so
// large that it (or part of it) consistently gets dropped. When we send reliably, we immediately update the
// ack so that the next state will not also be huge.
//
// We also do this if the client's last ack is too old. This helps prevent things like the entity deletion
// history from becoming too bloated if a bad client fails to send acks for whatever reason.
if (_gameTiming.CurTick.Value > lastAck.Value + _pvs.ForceAckThreshold)
{
stateUpdateMessage.ForceSendReliably = true;
// Aside from the time shortly after connecting, this shouldn't be common. If it is happening.
// something is probably wrong (or we have a malicious client). Hence we log an error.
// If it is more frequent than I think, this can be downgraded to a warning.
#if FULL_RELEASE
var connectedTime = (DateTime.UtcNow - session.ConnectedTime).TotalMinutes;
if (lastAck > GameTick.Zero && connectedTime > 1)
_logger.Error($"Client {session} exceeded ack-tick threshold. Last ack: {lastAck}. Cur tick: {_gameTiming.CurTick}. Connect time: {connectedTime} minutes");
#endif
}
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
if (stateUpdateMessage.ShouldSendReliably())
{
sessionData.LastReceivedAck = _gameTiming.CurTick;

View File

@@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Robust.Server.Console;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
@@ -14,7 +15,7 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -91,7 +92,9 @@ namespace Robust.Server.Physics
private void OnDebugRequest(RequestGridNodesMessage msg, EntitySessionEventArgs args)
{
if (!_conGroup.CanCommand(args.SenderSession, ShowGridNodesCommand)) return;
var pSession = (PlayerSession) args.SenderSession;
if (!_conGroup.CanCommand(pSession, ShowGridNodesCommand)) return;
AddDebugSubscriber(args.SenderSession);
}
@@ -257,7 +260,7 @@ namespace Robust.Server.Physics
for (var i = 0; i < grids.Count - 1; i++)
{
var group = grids[i];
var newGrid = _mapManager.CreateGridEntity(mapId);
var newGrid = _mapManager.CreateGrid(mapId);
var newGridUid = newGrid.Owner;
var newGridXform = xformQuery.GetComponent(newGridUid);
newGrids[i] = newGridUid;
@@ -284,7 +287,7 @@ namespace Robust.Server.Physics
}
}
newGrid.Comp.SetTiles(tileData);
newGrid.SetTiles(tileData);
DebugTools.Assert(_mapManager.IsGrid(newGridUid), "A split grid had no tiles?");
// Set tiles on new grid + update anchored entities

View File

@@ -15,7 +15,6 @@ using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Placement;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Robust.Server.Placement
@@ -200,11 +199,11 @@ namespace Robust.Server.Placement
}
else if (tileType != 0) // create a new grid
{
var newGrid = _mapManager.CreateGridEntity(coordinates.GetMapId(_entityManager));
var newGridXform = _entityManager.GetComponent<TransformComponent>(newGrid);
newGridXform.WorldPosition = coordinates.Position - newGrid.Comp.TileSizeHalfVector; // assume bottom left tile origin
var tilePos = newGrid.Comp.WorldToTile(coordinates.Position);
newGrid.Comp.SetTile(tilePos, new Tile(tileType));
var newGrid = _mapManager.CreateGrid(coordinates.GetMapId(_entityManager));
var newGridXform = _entityManager.GetComponent<TransformComponent>(newGrid.Owner);
newGridXform.WorldPosition = coordinates.Position - newGrid.TileSizeHalfVector; // assume bottom left tile origin
var tilePos = newGrid.WorldToTile(coordinates.Position);
newGrid.SetTile(tilePos, new Tile(tileType));
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);

View File

@@ -1,12 +1,88 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Enums;
using Robust.Shared.GameStates;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Timing;
namespace Robust.Server.Player;
/// <summary>
/// Manages each players session when connected to the server.
/// </summary>
public interface IPlayerManager : ISharedPlayerManager
namespace Robust.Server.Player
{
BoundKeyMap KeyMap { get; }
}
/// <summary>
/// Manages each players session when connected to the server.
/// </summary>
public interface IPlayerManager : ISharedPlayerManager
{
BoundKeyMap KeyMap { get; }
/// <summary>
/// Same as the common sessions, but the server version.
/// </summary>
IEnumerable<IPlayerSession> ServerSessions { get; }
/// <summary>
/// Raised when the <see cref="SessionStatus" /> of a <see cref="IPlayerSession" /> is changed.
/// </summary>
event EventHandler<SessionStatusEventArgs> PlayerStatusChanged;
/// <summary>
/// Initializes the manager.
/// </summary>
/// <param name="maxPlayers">Maximum number of players that can connect to this server at one time.</param>
void Initialize(int maxPlayers);
void Shutdown();
bool TryGetSessionByUsername(string username, [NotNullWhen(true)] out IPlayerSession? session);
/// <summary>
/// Returns the client session of the networkId.
/// </summary>
/// <returns></returns>
IPlayerSession GetSessionByUserId(NetUserId index);
IPlayerSession GetSessionByChannel(INetChannel channel);
bool TryGetSessionByChannel(INetChannel channel, [NotNullWhen(true)] out IPlayerSession? session);
bool TryGetSessionById(NetUserId userId, [NotNullWhen(true)] out IPlayerSession? session);
/// <summary>
/// Checks to see if a PlayerIndex is a valid session.
/// </summary>
bool ValidSessionId(NetUserId index);
IPlayerData GetPlayerData(NetUserId userId);
bool TryGetPlayerData(NetUserId userId, [NotNullWhen(true)] out IPlayerData? data);
bool TryGetPlayerDataByUsername(string userName, [NotNullWhen(true)] out IPlayerData? data);
bool HasPlayerData(NetUserId userId);
/// <summary>
/// Tries to get the user ID of the user with the specified username.
/// </summary>
/// <remarks>
/// This only works if this user has already connected once before during this server run.
/// It does still work if the user has since disconnected.
/// </remarks>
bool TryGetUserId(string userName, out NetUserId userId);
IEnumerable<IPlayerData> GetAllPlayerData();
[Obsolete]
void DetachAll();
[Obsolete("Use player Filter or Inline me!")]
List<IPlayerSession> GetPlayersInRange(MapCoordinates worldPos, int range);
[Obsolete("Use player Filter or Inline me!")]
List<IPlayerSession> GetPlayersInRange(EntityCoordinates worldPos, int range);
[Obsolete("Use player Filter or Inline me!")]
List<IPlayerSession> GetPlayersBy(Func<IPlayerSession, bool> predicate);
[Obsolete("Use player Filter or Inline me!")]
List<IPlayerSession> GetAllPlayers();
List<PlayerState>? GetPlayerStates(GameTick fromTick);
}
}

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
namespace Robust.Server.Player
{
public interface IPlayerSession : ICommonSession
{
DateTime ConnectedTime { get; }
event EventHandler<SessionStatusEventArgs> PlayerStatusChanged;
void JoinGame();
LoginType AuthType { get; }
/// <summary>
/// Attaches this player to an entity.
/// NOTE: The content pack almost certainly has an alternative for this.
/// Do not call this directly for most content code.
/// </summary>
/// <param name="entity">The entity to attach to.</param>
[Obsolete("Use ActorSystem.Attach() instead.")]
void AttachToEntity(EntityUid? entity);
/// <summary>
/// Attaches this player to an entity.
/// NOTE: The content pack almost certainly has an alternative for this.
/// Do not call this directly for most content code.
/// </summary>
/// <param name="uid">The entity to attach to.</param>
[Obsolete("Use ActorSystem.Attach() instead.")]
void AttachToEntity(EntityUid uid);
/// <summary>
/// Detaches this player from an entity.
/// NOTE: The content pack almost certainly has an alternative for this.
/// Do not call this directly for most content code.
/// </summary>
[Obsolete("Use ActorSystem.Detach() instead.")]
void DetachFromEntity();
void OnConnect();
void OnDisconnect();
IReadOnlySet<EntityUid> ViewSubscriptions { get; }
int ViewSubscriptionCount { get; }
/// <summary>
/// Persistent data for this player.
/// </summary>
IPlayerData Data { get; }
/// <summary>
/// Internal method to set <see cref="ICommonSession.AttachedEntity"/> and update the player's status.
/// Do NOT use this unless you know what you're doing, you probably want <see cref="AttachToEntity"/>
/// and <see cref="DetachFromEntity"/> instead.
/// </summary>
internal void SetAttachedEntity(EntityUid? entity);
/// <summary>
/// Internal method to add an entity Uid to <see cref="ViewSubscriptions"/>.
/// Do NOT use this outside of <see cref="ViewSubscriberSystem"/>.
/// </summary>
internal void AddViewSubscription(EntityUid eye);
/// <summary>
/// Internal method to remove an entity Uid from <see cref="ViewSubscriptions"/>.
/// Do NOT use this outside of <see cref="ViewSubscriberSystem"/>.
/// </summary>
internal void RemoveViewSubscription(EntityUid eye);
}
}

View File

@@ -0,0 +1,24 @@
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
namespace Robust.Server.Player
{
sealed class PlayerData : IPlayerData
{
public PlayerData(NetUserId userId, string userName)
{
UserId = userId;
UserName = userName;
}
[ViewVariables]
public NetUserId UserId { get; }
[ViewVariables]
public string UserName { get; }
[ViewVariables]
public object? ContentDataUncast { get; set; }
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Prometheus;
using Robust.Server.Configuration;
@@ -11,19 +12,22 @@ using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Server.Player
{
/// <summary>
/// This class will manage connected player sessions.
/// </summary>
internal sealed class PlayerManager : SharedPlayerManager, IPlayerManager
public sealed class PlayerManager : IPlayerManager
{
private static readonly Gauge PlayerCountMetric = Metrics
.CreateGauge("robust_player_count", "Number of players on the server.");
@@ -37,13 +41,79 @@ namespace Robust.Server.Player
public BoundKeyMap KeyMap { get; private set; } = default!;
private GameTick _lastStateUpdate;
private readonly ReaderWriterLockSlim _sessionsLock = new();
/// <summary>
/// Active sessions of connected clients to the server.
/// </summary>
[ViewVariables]
private readonly Dictionary<NetUserId, PlayerSession> _sessions = new();
[ViewVariables]
private readonly Dictionary<NetUserId, PlayerData> _playerData = new();
[ViewVariables]
private readonly Dictionary<string, NetUserId> _userIdMap = new();
/// <inheritdoc />
public override void Initialize(int maxPlayers)
public IEnumerable<ICommonSession> NetworkedSessions => Sessions;
/// <inheritdoc />
public IEnumerable<ICommonSession> Sessions
{
get
{
_sessionsLock.EnterReadLock();
try
{
return _sessions.Values;
}
finally
{
_sessionsLock.ExitReadLock();
}
}
}
public IEnumerable<IPlayerSession> ServerSessions => Sessions.Cast<IPlayerSession>();
/// <inheritdoc />
[ViewVariables]
public int PlayerCount
{
get
{
_sessionsLock.EnterReadLock();
try
{
return _sessions.Count;
}
finally
{
_sessionsLock.ExitReadLock();
}
}
}
/// <inheritdoc />
[ViewVariables]
public int MaxPlayers { get; private set; } = 32;
public ICommonSession? LocalSession => null;
/// <inheritdoc />
public event EventHandler<SessionStatusEventArgs>? PlayerStatusChanged;
/// <inheritdoc />
public void Initialize(int maxPlayers)
{
base.Initialize(maxPlayers);
KeyMap = new BoundKeyMap(_reflectionManager);
KeyMap.PopulateKeyFunctionsMap();
MaxPlayers = maxPlayers;
_network.RegisterNetMessage<MsgPlayerListReq>(HandlePlayerListReq);
_network.RegisterNetMessage<MsgPlayerList>();
_network.RegisterNetMessage<MsgSyncTimeBase>();
@@ -53,9 +123,8 @@ namespace Robust.Server.Player
_network.Disconnect += EndSession;
}
public override void Shutdown()
public void Shutdown()
{
base.Shutdown();
KeyMap = default!;
_network.Connecting -= OnConnecting;
@@ -63,6 +132,250 @@ namespace Robust.Server.Player
_network.Disconnect -= EndSession;
}
public bool TryGetSessionByUsername(string username, [NotNullWhen(true)] out IPlayerSession? session)
{
if (!_userIdMap.TryGetValue(username, out var userId))
{
session = null;
return false;
}
_sessionsLock.EnterReadLock();
try
{
if (_sessions.TryGetValue(userId, out var iSession))
{
session = iSession;
return true;
}
}
finally
{
_sessionsLock.ExitReadLock();
}
session = null;
return false;
}
IPlayerSession IPlayerManager.GetSessionByChannel(INetChannel channel) => GetSessionByChannel(channel);
public bool TryGetSessionByChannel(INetChannel channel, [NotNullWhen(true)] out IPlayerSession? session)
{
_sessionsLock.EnterReadLock();
try
{
// Should only be one session per client. Returns that session, in theory.
if (_sessions.TryGetValue(channel.UserId, out var concrete))
{
session = concrete;
return true;
}
session = null;
return false;
}
finally
{
_sessionsLock.ExitReadLock();
}
}
private PlayerSession GetSessionByChannel(INetChannel channel)
{
_sessionsLock.EnterReadLock();
try
{
// Should only be one session per client. Returns that session, in theory.
return _sessions[channel.UserId];
}
finally
{
_sessionsLock.ExitReadLock();
}
}
/// <inheritdoc />
public IPlayerSession GetSessionByUserId(NetUserId index)
{
_sessionsLock.EnterReadLock();
try
{
return _sessions[index];
}
finally
{
_sessionsLock.ExitReadLock();
}
}
public bool ValidSessionId(NetUserId index)
{
_sessionsLock.EnterReadLock();
try
{
return _sessions.ContainsKey(index);
}
finally
{
_sessionsLock.ExitReadLock();
}
}
public bool TryGetSessionById(NetUserId userId, [NotNullWhen(true)] out IPlayerSession? session)
{
_sessionsLock.EnterReadLock();
try
{
if (_sessions.TryGetValue(userId, out var playerSession))
{
session = playerSession;
return true;
}
}
finally
{
_sessionsLock.ExitReadLock();
}
session = default;
return false;
}
/// <summary>
/// Causes all sessions to switch from the lobby to the the game.
/// </summary>
[Obsolete]
public void SendJoinGameToAll()
{
_sessionsLock.EnterReadLock();
try
{
foreach (var s in _sessions.Values)
{
s.JoinGame();
}
}
finally
{
_sessionsLock.ExitReadLock();
}
}
public bool TryGetUserId(string userName, out NetUserId userId)
{
return _userIdMap.TryGetValue(userName, out userId);
}
public IEnumerable<IPlayerData> GetAllPlayerData()
{
return _playerData.Values;
}
/// <summary>
/// Causes all sessions to detach from their entity.
/// </summary>
[Obsolete]
public void DetachAll()
{
_sessionsLock.EnterReadLock();
try
{
foreach (var s in _sessions.Values)
{
s.DetachFromEntity();
}
}
finally
{
_sessionsLock.ExitReadLock();
}
}
/// <summary>
/// Gets all players inside of a circle.
/// </summary>
/// <param name="worldPos">Position of the circle in world-space.</param>
/// <param name="range">Radius of the circle in world units.</param>
/// <returns></returns>
[Obsolete("Use player Filter or Inline me!")]
public List<IPlayerSession> GetPlayersInRange(MapCoordinates worldPos, int range)
{
return Filter.Empty()
.AddInRange(worldPos, range)
.Recipients
.Cast<IPlayerSession>()
.ToList();
}
/// <summary>
/// Gets all players inside of a circle.
/// </summary>
/// <param name="worldPos">Position of the circle in world-space.</param>
/// <param name="range">Radius of the circle in world units.</param>
/// <returns></returns>
[Obsolete("Use player Filter or Inline me!")]
public List<IPlayerSession> GetPlayersInRange(EntityCoordinates worldPos, int range)
{
return Filter.Empty()
.AddInRange(worldPos.ToMap(_entityManager), range)
.Recipients
.Cast<IPlayerSession>()
.ToList();
}
[Obsolete("Use player Filter or Inline me!")]
public List<IPlayerSession> GetPlayersBy(Func<IPlayerSession, bool> predicate)
{
return Filter.Empty()
.AddWhere((session => predicate((IPlayerSession)session)))
.Recipients
.Cast<IPlayerSession>()
.ToList();
}
/// <summary>
/// Gets all players in the server.
/// </summary>
/// <returns></returns>
[Obsolete("Use player Filter or Inline me!")]
public List<IPlayerSession> GetAllPlayers()
{
return ServerSessions.ToList();
}
/// <summary>
/// Gets all player states in the server.
/// </summary>
/// <param name="fromTick"></param>
/// <returns></returns>
public List<PlayerState>? GetPlayerStates(GameTick fromTick)
{
if (_lastStateUpdate < fromTick)
{
return null;
}
_sessionsLock.EnterReadLock();
try
{
#if FULL_RELEASE
return _sessions.Values
.Select(s => s.PlayerState)
.ToList();
#else
// Integration tests need to clone data before "sending" it to the client. Otherwise they reference the
// same object.
return _sessions.Values
.Select(s => s.PlayerState.Clone())
.ToList();
#endif
}
finally
{
_sessionsLock.ExitReadLock();
}
}
private Task OnConnecting(NetConnectingArgs args)
{
if (PlayerCount >= _baseServer.MaxPlayers)
@@ -80,10 +393,30 @@ namespace Robust.Server.Player
/// <param name="args"></param>
private void NewSession(object? sender, NetChannelArgs args)
{
var session = CreateAndAddSession(args.Channel.UserId, args.Channel.UserName);
session.Channel = args.Channel;
if (!_playerData.TryGetValue(args.Channel.UserId, out var data))
{
data = new PlayerData(args.Channel.UserId, args.Channel.UserName);
_playerData.Add(args.Channel.UserId, data);
}
_userIdMap[args.Channel.UserName] = args.Channel.UserId;
var session = new PlayerSession(this, args.Channel, data);
session.PlayerStatusChanged += (_, sessionArgs) => OnPlayerStatusChanged(session, sessionArgs.OldStatus, sessionArgs.NewStatus);
_sessionsLock.EnterWriteLock();
try
{
_sessions.Add(args.Channel.UserId, session);
}
finally
{
_sessionsLock.ExitWriteLock();
}
PlayerCountMetric.Set(PlayerCount);
// Synchronize base time.
var msgTimeBase = new MsgSyncTimeBase();
(msgTimeBase.Time, msgTimeBase.Tick) = _timing.TimeBase;
@@ -92,6 +425,11 @@ namespace Robust.Server.Player
_cfg.SyncConnectingClient(args.Channel);
}
private void OnPlayerStatusChanged(IPlayerSession session, SessionStatus oldStatus, SessionStatus newStatus)
{
PlayerStatusChanged?.Invoke(this, new SessionStatusEventArgs(session, oldStatus, newStatus));
}
/// <summary>
/// Ends a clients session, and disconnects them.
/// </summary>
@@ -103,19 +441,20 @@ namespace Robust.Server.Player
}
// make sure nothing got messed up during the life of the session
DebugTools.Assert(session.Channel == args.Channel);
DebugTools.Assert(session.ConnectedClient == args.Channel);
SetStatus(session, SessionStatus.Disconnected);
if (session.AttachedEntity != null)
EntManager.System<ActorSystem>().Detach(session.AttachedEntity.Value);
var viewSys = EntManager.System<ViewSubscriberSystem>();
foreach (var eye in session.ViewSubscriptions.ToArray())
//Detach the entity and (don't)delete it.
session.OnDisconnect();
_sessionsLock.EnterWriteLock();
try
{
viewSys.RemoveViewSubscriber(eye, session);
_sessions.Remove(session.UserId);
}
finally
{
_sessionsLock.ExitWriteLock();
}
RemoveSession(session.UserId);
PlayerCountMetric.Set(PlayerCount);
Dirty();
}
@@ -130,18 +469,17 @@ namespace Robust.Server.Player
// This is done before the packet is built, so that the client
// can see themselves Connected.
var session = GetSessionByChannel(channel);
session.ConnectedTime = DateTime.UtcNow;
SetStatus(session, SessionStatus.Connected);
session.OnConnect();
var list = new List<SessionState>();
var list = new List<PlayerState>();
foreach (var client in players)
{
var info = new SessionState
var info = new PlayerState
{
UserId = client.UserId,
Name = client.Name,
Status = client.Status,
Ping = client.Channel!.Ping
Ping = client.ConnectedClient.Ping
};
list.Add(info);
}
@@ -151,7 +489,46 @@ namespace Robust.Server.Player
channel.SendMessage(netMsg);
}
public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
public void Dirty()
{
_lastStateUpdate = _timing.CurTick;
}
public IPlayerData GetPlayerData(NetUserId userId)
{
return _playerData[userId];
}
public bool TryGetPlayerData(NetUserId userId, [NotNullWhen(true)] out IPlayerData? data)
{
if (_playerData.TryGetValue(userId, out var playerData))
{
data = playerData;
return true;
}
data = default;
return false;
}
public bool TryGetPlayerDataByUsername(string userName, [NotNullWhen(true)] out IPlayerData? data)
{
if (!_userIdMap.TryGetValue(userName, out var userId))
{
data = null;
return false;
}
// PlayerData is initialized together with the _userIdMap so we can trust that it'll be present.
data = _playerData[userId];
return true;
}
public bool HasPlayerData(NetUserId userId)
{
return _playerData.ContainsKey(userId);
}
public bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
{
if (!_entityManager.TryGetComponent(uid, out ActorComponent? actor))
{
@@ -163,4 +540,18 @@ namespace Robust.Server.Player
return true;
}
}
public sealed class SessionStatusEventArgs : EventArgs
{
public SessionStatusEventArgs(IPlayerSession session, SessionStatus oldStatus, SessionStatus newStatus)
{
Session = session;
OldStatus = oldStatus;
NewStatus = newStatus;
}
public IPlayerSession Session { get; }
public SessionStatus OldStatus { get; }
public SessionStatus NewStatus { get; }
}
}

View File

@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using Robust.Server.GameObjects;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
namespace Robust.Server.Player
{
/// <summary>
/// This is the session of a connected client.
/// </summary>
internal sealed class PlayerSession : IPlayerSession
{
private readonly PlayerManager _playerManager;
public readonly PlayerState PlayerState;
private readonly HashSet<EntityUid> _viewSubscriptions = new();
public PlayerSession(PlayerManager playerManager, INetChannel client, PlayerData data)
{
_playerManager = playerManager;
UserId = client.UserId;
Name = client.UserName;
_data = data;
PlayerState = new PlayerState
{
UserId = client.UserId,
};
ConnectedClient = client;
UpdatePlayerState();
}
[ViewVariables] public IReadOnlySet<EntityUid> ViewSubscriptions => _viewSubscriptions;
public int ViewSubscriptionCount => _viewSubscriptions.Count;
[ViewVariables] public INetChannel ConnectedClient { get; }
/// <inheritdoc />
[ViewVariables] public EntityUid? AttachedEntity { get; set; }
private SessionStatus _status = SessionStatus.Connecting;
[ViewVariables]
internal string Name { get; set; }
/// <inheritdoc />
string ICommonSession.Name
{
get => this.Name;
set => this.Name = value;
}
[ViewVariables]
internal short Ping
{
get => ConnectedClient.Ping;
set => throw new NotSupportedException();
}
short ICommonSession.Ping
{
get => this.Ping;
set => this.Ping = value;
}
[ViewVariables]
internal SessionStatus Status
{
get => _status;
set
{
if (_status == value)
return;
var old = _status;
_status = value;
UpdatePlayerState();
PlayerStatusChanged?.Invoke(this, new SessionStatusEventArgs(this, old, value));
}
}
/// <inheritdoc />
SessionStatus ICommonSession.Status
{
get => this.Status;
set => this.Status = value;
}
/// <inheritdoc />
public DateTime ConnectedTime { get; private set; }
[ViewVariables(VVAccess.ReadWrite)]
public int VisibilityMask { get; set; } = 1;
/// <inheritdoc />
[ViewVariables]
public NetUserId UserId { get; }
private readonly PlayerData _data;
[ViewVariables] public IPlayerData Data => _data;
/// <inheritdoc />
public event EventHandler<SessionStatusEventArgs>? PlayerStatusChanged;
/// <inheritdoc />
[Obsolete("Use ActorSystem.Attach() instead.")]
public void AttachToEntity(EntityUid? entity)
{
EntitySystem.Get<ActorSystem>().Attach(entity, this);
}
/// <inheritdoc />
[Obsolete("Use ActorSystem.Attach() instead.")]
public void AttachToEntity(EntityUid uid)
{
EntitySystem.Get<ActorSystem>().Attach(uid, this);
}
/// <inheritdoc />
[Obsolete("Use ActorSystem.Detach() instead.")]
public void DetachFromEntity()
{
if (AttachedEntity == null)
return;
if (IoCManager.Resolve<IEntityManager>().Deleted(AttachedEntity!.Value))
{
Logger.Error($"Player \"{this}\" was attached to an entity that was deleted. THIS SHOULD NEVER HAPPEN, BUT DOES.");
// We can't contact ActorSystem because trying to fire an entity event would crash.
// Work around it.
AttachedEntity = null;
UpdatePlayerState();
return;
}
EntitySystem.Get<ActorSystem>().Detach(AttachedEntity.Value);
}
/// <inheritdoc />
public void OnConnect()
{
ConnectedTime = DateTime.UtcNow;
Status = SessionStatus.Connected;
UpdatePlayerState();
}
/// <inheritdoc />
public void OnDisconnect()
{
Status = SessionStatus.Disconnected;
UnsubscribeAllViews();
DetachFromEntity();
UpdatePlayerState();
}
/// <summary>
/// Causes the session to switch from the lobby to the game.
/// </summary>
public void JoinGame()
{
if (ConnectedClient == null || Status == SessionStatus.InGame)
return;
Status = SessionStatus.InGame;
UpdatePlayerState();
}
public LoginType AuthType => ConnectedClient.AuthType;
/// <inheritdoc />
void IPlayerSession.SetAttachedEntity(EntityUid? entity)
{
AttachedEntity = entity;
UpdatePlayerState();
}
void IPlayerSession.AddViewSubscription(EntityUid eye)
{
_viewSubscriptions.Add(eye);
}
void IPlayerSession.RemoveViewSubscription(EntityUid eye)
{
_viewSubscriptions.Remove(eye);
}
private void UnsubscribeAllViews()
{
var viewSubscriberSystem = EntitySystem.Get<ViewSubscriberSystem>();
foreach (var eye in _viewSubscriptions)
{
viewSubscriberSystem.RemoveViewSubscriber(eye, this);
}
}
private void UpdatePlayerState()
{
PlayerState.Status = Status;
PlayerState.Name = Name;
PlayerState.ControlledEntity = IoCManager.Resolve<IEntityManager>().GetNetEntity(AttachedEntity);
_playerManager.Dirty();
}
/// <inheritdoc />
public override string ToString()
{
return Name;
}
}
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
@@ -19,7 +20,6 @@ using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
using Robust.Shared.Scripting;
using Robust.Shared.Utility;
@@ -38,7 +38,7 @@ namespace Robust.Server.Scripting
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly ILogManager _logManager = default!;
readonly Dictionary<ICommonSession, Dictionary<int, ScriptInstance>> _instances =
readonly Dictionary<IPlayerSession, Dictionary<int, ScriptInstance>> _instances =
new();
private ISawmill _sawmill = default!;

View File

@@ -22,7 +22,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Replays;

View File

@@ -34,12 +34,12 @@ public sealed class DefaultMagicAczProvider : IMagicAczProvider
var inputPass = graph.Input;
var contentDir = FindContentRootPath(_deps);
await RobustSharedPackaging.WriteContentAssemblies(
await RobustClientPackaging.WriteContentAssemblies(
inputPass,
contentDir,
binFolderPath,
assemblyNames,
cancel: cancel);
cancel);
await RobustClientPackaging.WriteClientResources(contentDir, inputPass, cancel);

View File

@@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Toolshed;
namespace Robust.Server.Toolshed.Commands.Players;
@@ -17,7 +17,7 @@ public sealed class ActorCommand : ToolshedCommand
}
[CommandImplementation("session")]
public IEnumerable<ICommonSession> Session([PipedArgument] IEnumerable<EntityUid> input)
public IEnumerable<IPlayerSession> Session([PipedArgument] IEnumerable<EntityUid> input)
{
return input.Where(HasComp<ActorComponent>).Select(x => Comp<ActorComponent>(x).PlayerSession);
}

View File

@@ -7,7 +7,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
@@ -20,18 +20,18 @@ public sealed class PlayerCommand : ToolshedCommand
[Dependency] private readonly IPlayerManager _playerManager = default!;
[CommandImplementation("list")]
public IEnumerable<ICommonSession> Players()
=> _playerManager.Sessions;
public IEnumerable<IPlayerSession> Players()
=> _playerManager.ServerSessions;
[CommandImplementation("self")]
public ICommonSession Self([CommandInvocationContext] IInvocationContext ctx)
public IPlayerSession Self([CommandInvocationContext] IInvocationContext ctx)
{
if (ctx.Session is null)
{
ctx.ReportError(new NotForServerConsoleError());
}
return ctx.Session!;
return (IPlayerSession)ctx.Session!;
}
[CommandImplementation("imm")]
@@ -59,13 +59,13 @@ public sealed class PlayerCommand : ToolshedCommand
}
[CommandImplementation("entity")]
public IEnumerable<EntityUid> GetPlayerEntity([PipedArgument] IEnumerable<ICommonSession> sessions)
public IEnumerable<EntityUid> GetPlayerEntity([PipedArgument] IEnumerable<IPlayerSession> sessions)
{
return sessions.Select(x => x.AttachedEntity).Where(x => x is not null).Cast<EntityUid>();
}
[CommandImplementation("entity")]
public EntityUid GetPlayerEntity([PipedArgument] ICommonSession sessions)
public EntityUid GetPlayerEntity([PipedArgument] IPlayerSession sessions)
{
return sessions.AttachedEntity ?? default;
}

View File

@@ -5,7 +5,6 @@ using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Upload;
using Robust.Shared.ViewVariables;
@@ -18,7 +17,7 @@ public sealed class NetworkResourceManager : SharedNetworkResourceManager
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IConGroupController _controller = default!;
public event Action<ICommonSession, NetworkResourceUploadMessage>? OnResourceUploaded;
public event Action<IPlayerSession, NetworkResourceUploadMessage>? OnResourceUploaded;
[ViewVariables] public bool Enabled { get; private set; } = true;
[ViewVariables] public float SizeLimit { get; private set; }

View File

@@ -6,13 +6,13 @@ using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Server.ViewVariables
@@ -31,8 +31,6 @@ namespace Robust.Server.ViewVariables
private readonly Dictionary<uint, ViewVariablesSession>
_sessions = new();
private readonly Dictionary<NetUserId, List<uint>> _users = new();
private uint _nextSessionId = 1;
public override void Initialize()
@@ -46,22 +44,6 @@ namespace Robust.Server.ViewVariables
_netManager.RegisterNetMessage<MsgViewVariablesDenySession>();
_netManager.RegisterNetMessage<MsgViewVariablesOpenSession>();
_netManager.RegisterNetMessage<MsgViewVariablesRemoteData>();
_playerManager.PlayerStatusChanged += OnStatusChanged;
}
private void OnStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus != SessionStatus.Disconnected)
return;
if (!_users.TryGetValue(e.Session.UserId, out var vvSessions))
return;
foreach (var id in vvSessions)
{
_closeSession(id, false);
}
}
private void _msgCloseSession(MsgViewVariablesCloseSession message)
@@ -253,13 +235,19 @@ namespace Robust.Server.ViewVariables
_robustSerializer, _entityManager, Sawmill);
_sessions.Add(sessionId, session);
_users.GetOrNew(session.PlayerUser).Add(sessionId);
var allowMsg = new MsgViewVariablesOpenSession();
allowMsg.RequestId = message.RequestId;
allowMsg.SessionId = session.SessionId;
_netManager.ServerSendMessage(allowMsg, message.MsgChannel);
player.PlayerStatusChanged += (_, args) =>
{
if (args.NewStatus == SessionStatus.Disconnected)
{
_closeSession(session.SessionId, false);
}
};
}
private void _closeSession(uint sessionId, bool sendMsg)

View File

@@ -98,7 +98,7 @@ namespace Robust.Server.ViewVariables
// Auto-dirty component. Only works when modifying a field that is directly on a component,
// Does not work for nested objects.
if (Object is Component comp)
EntityManager.Dirty(comp.Owner, comp);
EntityManager.Dirty(comp);
}
public bool TryGetRelativeObject(object[] propertyIndex, out object? value)

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Text;
@@ -9,7 +10,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Errors;
@@ -187,10 +188,10 @@ namespace Robust.Shared.Scripting
}
#region EntityManager proxy methods
public T Comp<T>(EntityUid uid) where T : IComponent
public T Comp<T>(EntityUid uid) where T : Component
=> ent.GetComponent<T>(uid);
public bool TryComp<T>(EntityUid uid, out T? comp) where T : IComponent
public bool TryComp<T>(EntityUid uid, out T? comp) where T : Component
=> ent.TryGetComponent(uid, out comp);
public bool HasComp<T>(EntityUid uid)
@@ -206,7 +207,7 @@ namespace Robust.Shared.Scripting
=> ent.DirtyEntity(uid);
public void Dirty(Component comp)
=> ent.Dirty(comp.Owner, comp);
=> ent.Dirty(comp);
public string Name(EntityUid uid)
=> ent.GetComponent<MetaDataComponent>(uid).EntityName;

View File

@@ -10,7 +10,7 @@ namespace Robust.Shared.Analyzers;
/// component state replication beyond just directly setting variables should not use this attribute.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
[BaseTypeRequired(typeof(IComponent))]
[BaseTypeRequired(typeof(Component))]
public sealed class AutoGenerateComponentStateAttribute : Attribute
{
/// <summary>

Some files were not shown because too many files have changed in this diff Show More