mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a761fbc09e | ||
|
|
f69440b3f2 | ||
|
|
b459d2ce21 | ||
|
|
202182e3d4 | ||
|
|
96cb52e5d2 | ||
|
|
82e0c0baeb | ||
|
|
54d6552164 | ||
|
|
c21b6c993c | ||
|
|
2fe4a8b859 | ||
|
|
8325966dbb | ||
|
|
2459a9d688 | ||
|
|
2cd2d1edd6 | ||
|
|
b982350851 | ||
|
|
4a50bc2154 | ||
|
|
4c85e205b9 | ||
|
|
0b447d9d82 | ||
|
|
ceb205ad52 | ||
|
|
a48ff3dbf1 | ||
|
|
2b85fa88c1 | ||
|
|
19564a421b | ||
|
|
b3f0e467ee | ||
|
|
216292c849 | ||
|
|
68753d15e0 | ||
|
|
2a357051ae | ||
|
|
58e0b62145 | ||
|
|
14cc273997 | ||
|
|
93f4428635 | ||
|
|
164bf68aca | ||
|
|
773b87672b | ||
|
|
eecf834039 | ||
|
|
325fe46aa3 | ||
|
|
2f6c29ab43 | ||
|
|
aab1a2dba9 | ||
|
|
f36fbd9c83 | ||
|
|
126c863f45 | ||
|
|
618a8491bf | ||
|
|
2743b64a2b | ||
|
|
28cc91934c | ||
|
|
eadfcd4c09 | ||
|
|
7871b0010e | ||
|
|
3da04ed17e | ||
|
|
170d192791 | ||
|
|
dcd9939554 | ||
|
|
98ef58eca6 | ||
|
|
ab1e99a0df | ||
|
|
499c236798 | ||
|
|
8dc2345ceb | ||
|
|
9b04270178 | ||
|
|
d75dbc901f | ||
|
|
19a3e82848 | ||
|
|
911abf2693 | ||
|
|
f5874ea402 | ||
|
|
b486ef885c | ||
|
|
9d55d77e48 | ||
|
|
5af3cb969c | ||
|
|
429bc806dc | ||
|
|
81484699a8 | ||
|
|
7cad8d5ba3 | ||
|
|
3aa04a3c86 | ||
|
|
9750b113c8 | ||
|
|
5a6c4220fc | ||
|
|
b2d389f184 | ||
|
|
ad0cb05dd6 | ||
|
|
ad134d9e4e | ||
|
|
be33bc2219 | ||
|
|
aa2fd2107d | ||
|
|
554e0777b1 | ||
|
|
21b7c5f93e | ||
|
|
9e5c1e9c95 | ||
|
|
6825f09fb9 | ||
|
|
58e3a4eb4a | ||
|
|
9a342f0d11 | ||
|
|
f754ddb96d | ||
|
|
7feede0d95 | ||
|
|
ea152366e3 | ||
|
|
ab47d4e009 | ||
|
|
81b2a3825e | ||
|
|
56d850f389 | ||
|
|
b737ecf9b3 | ||
|
|
ed5223b592 | ||
|
|
f87012e681 | ||
|
|
54529fdbe3 | ||
|
|
1745a12e5a | ||
|
|
d201d787b7 | ||
|
|
904ddea274 | ||
|
|
6b6ec844e8 | ||
|
|
f24d18f470 | ||
|
|
77654a1628 | ||
|
|
f3af813b57 | ||
|
|
0623baedcf | ||
|
|
2ade6c04c5 | ||
|
|
a9df9097c1 | ||
|
|
755dac719f | ||
|
|
7095a58685 | ||
|
|
16e68a4351 | ||
|
|
0152f9d1d8 | ||
|
|
16d916796a | ||
|
|
e865157432 | ||
|
|
24d5ce4bd4 | ||
|
|
662195e4ff | ||
|
|
d00fd6f736 | ||
|
|
2d58c1071d | ||
|
|
a9db89d023 | ||
|
|
684cabf3e6 | ||
|
|
a4f51f0cd9 | ||
|
|
a8ddd837c8 | ||
|
|
82aace7997 | ||
|
|
01ce244b7b | ||
|
|
58aa6e5c75 | ||
|
|
4818c3aab4 | ||
|
|
3b6adeb5ff | ||
|
|
889b8351be | ||
|
|
ac37b0a131 | ||
|
|
f6f1fc425a | ||
|
|
7476628840 | ||
|
|
668cdbe76b | ||
|
|
a0a6e9b111 | ||
|
|
06d28f04e6 | ||
|
|
57897161d0 | ||
|
|
c4c528478e |
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -7,3 +7,6 @@
|
||||
**/Toolshed/** @moonheart08
|
||||
*Command.cs @moonheart08
|
||||
*Commands.cs @moonheart08
|
||||
|
||||
# Physics
|
||||
**/Robust.Shared/Physics/** @metalgearsloth
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
344
RELEASE-NOTES.md
344
RELEASE-NOTES.md
@@ -54,6 +54,350 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 180.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add EnsureEntity variants that take in collections.
|
||||
* Add more MapSystem helper methods.
|
||||
|
||||
### Internal
|
||||
|
||||
* Cache some more PVS data to avoid re-allocating every tick.
|
||||
|
||||
|
||||
## 180.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add the map name to lsmap.
|
||||
* Add net.pool_size to CVars to control the message data pool size in Lidgren and to also toggle pooling.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix physics contraints causing enormous heap allocations.
|
||||
* Fix potential error when writing a runtime log.
|
||||
* Fix shape lookups for non-hard fixtures in EntityLookupSystem from 180.0.0
|
||||
|
||||
|
||||
## 180.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Removed some obsolete methods from EntityLookupSystem.
|
||||
|
||||
### New features
|
||||
|
||||
* PhysicsSystem.TryGetNearest now supports chain shapes.
|
||||
* Add IPhysShape methods to EntityLookupSystem rather than relying on AABB checks.
|
||||
* Add some more helper methods to SharedTransformSystem.
|
||||
* Add GetOrNew dictionary extension that also returns a bool on whether the key existed.
|
||||
* Add a GetAnchoredEntities overload that takes in a list.
|
||||
|
||||
### Other
|
||||
|
||||
* Use NetEntities for the F3 debug panel to align with command usage.
|
||||
|
||||
|
||||
## 179.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* EyeComponent.Eye is no longer nullable
|
||||
|
||||
### New features
|
||||
|
||||
* Light rendering can now be enabled or disable per eye.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Deserializing old maps with empty grid chunks should now just ignore those chunks.
|
||||
|
||||
### Other
|
||||
|
||||
* UnknownPrototypeException now also tells you the prototype kind instead of just the unkown ID.
|
||||
* Adding or removing networked components while resetting predicted entities now results in a more informative exception.
|
||||
|
||||
|
||||
## 178.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Most methods in ActorSystem have been moved to ISharedPlayerManager.
|
||||
* Several actor/player related components and events have been moved to shared.
|
||||
|
||||
### New features
|
||||
|
||||
* Added `NetListAsArray<T>.Value` to the sandbox whitelist
|
||||
|
||||
|
||||
## 177.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Removed toInsertXform and added containerXform in SharedContainerSystem.CanInsert.
|
||||
* Removed EntityQuery parameters from SharedContainerSystem.IsEntityOrParentInContainer.
|
||||
* Changed the signature of ContainsEntity in SharedTransformSystem to use Entity<T>.
|
||||
* Removed one obsoleted SharedTransformSystem.AnchorEntity method.
|
||||
* Changed signature of SharedTransformSystem.SetCoordinates to use Entity<T>.
|
||||
|
||||
### New features
|
||||
|
||||
* Added more Entity<T> query methods.
|
||||
* Added BeforeApplyState event to replay playback.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed inverted GetAllMapGrids map id check.
|
||||
* Fixed transform test warnings.
|
||||
* Fixed PlacementManager warnings.
|
||||
* Fixed reparenting bug for entities that are being deleted.
|
||||
|
||||
### Other
|
||||
|
||||
* Changed VerticalAlignment of RichTextLabel to Center to be consistent with Label.
|
||||
* Changed PVS error log to be a warning instead.
|
||||
* Marked insert and remove container methods as obsolete, added container system methods to replace them.
|
||||
* Marked TransformComponent.MapPosition as obsolete, added GetMapCoordinates system method to replace it.
|
||||
|
||||
### Internal
|
||||
|
||||
* Moved TryGetUi/TryToggleUi/ToggleUi/TryOpen/OpenUi/TryClose/CloseUi methods from UserInterfaceSystem to SharedUserInterfaceSystem.
|
||||
|
||||
|
||||
## 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.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Remove ComponentExtensions.
|
||||
* Remove ContainerHelpers.
|
||||
* Change some TransformSystem methods to fix clientside lerping.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed PVS bugs from dropped entity states.
|
||||
|
||||
### Other
|
||||
|
||||
* Add more joint debug asserts.
|
||||
|
||||
|
||||
## 166.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* EntityUid-NetEntity conversion methods now return null when given a null value, rather than returning an invalid id.
|
||||
* ExpandPvsEvent now defaults to using null lists to reduce allocations.
|
||||
* Various component lifestage related methods have been moved from the `Component` class to `EntityManager`.
|
||||
* Session/client specific PVS overrides are now always recursive, which means that all children of the overriden entity will also get sent.
|
||||
|
||||
### New features
|
||||
|
||||
* Added a SortedSet yaml serializer.
|
||||
|
||||
### Other
|
||||
|
||||
* AddComponentUninitialized is now marked as obsolete and will be removed in the future.
|
||||
* DebugTools.AssertOwner() now accepts null components.
|
||||
|
||||
|
||||
## 165.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The arguments of `SplitContainer`s resize-finished event have changed.
|
||||
|
||||
### New features
|
||||
|
||||
* The YAML validator now checks the default values of ProtoId<T> and EntProtoId data fields.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* The minimum draggable area of split containers now blocks mouse inputs.
|
||||
|
||||
|
||||
## 164.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Make automatic component states infer cloneData.
|
||||
* Removed cloneData from AutoNetworkedFieldAttribute. This is now automatically inferred.
|
||||
|
||||
### Internal
|
||||
|
||||
* Reduce Transform GetComponents in RecursiveDeleteEntity.
|
||||
|
||||
|
||||
## 163.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Moved TimedDespawn to engine for a component that deletes the attached entity after a timer has elapsed.
|
||||
|
||||
### New features
|
||||
|
||||
* Add ExecuteCommand for integration tests.
|
||||
* Allow adding / removing widgets of cub-controls.
|
||||
* Give maps / grids a default name to help with debugging.
|
||||
* Use ToPrettyString in component resolve errors to help with debugging.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix console backspace exception.
|
||||
* Fix rendering invalid maps spamming exceptions every frame.
|
||||
|
||||
### Internal
|
||||
|
||||
* Move ClientGameStatemanager local variables to fields to avoid re-allocating every tick.
|
||||
|
||||
|
||||
## 162.2.1
|
||||
|
||||
|
||||
|
||||
@@ -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-oldhelp-desc = Display general help or help text for a specific command
|
||||
cmd-oldhelp-help = Usage: help [command name]
|
||||
cmd-help-desc = Display general help or help text for a specific command
|
||||
cmd-help-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-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]
|
||||
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]
|
||||
|
||||
## 'cvar' command
|
||||
cmd-cvar-desc = Gets or sets a CVar.
|
||||
|
||||
@@ -23,16 +23,6 @@ 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",
|
||||
@@ -55,7 +45,6 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
||||
ByRefEventSubscribedByValueRule,
|
||||
ByValueEventSubscribedByRefRule,
|
||||
ByRefEventRaisedByValueRule,
|
||||
ByValueEventRaisedByRefRule
|
||||
);
|
||||
@@ -64,71 +53,9 @@ 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)
|
||||
|
||||
@@ -18,7 +18,6 @@ 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";
|
||||
|
||||
@@ -54,7 +54,7 @@ public class RecursiveMoveBenchmark
|
||||
var mapSys = _entMan.System<SharedMapSystem>();
|
||||
var mapId = mapMan.CreateMap();
|
||||
var map = mapMan.GetMapEntityId(mapId);
|
||||
var gridComp = mapMan.CreateGrid(mapId);
|
||||
var gridComp = mapMan.CreateGridEntity(mapId);
|
||||
var grid = gridComp.Owner;
|
||||
_gridCoords = new EntityCoordinates(grid, .5f, .5f);
|
||||
_mapCoords = new EntityCoordinates(map, 100, 100);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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;
|
||||
@@ -10,13 +8,12 @@ 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.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -65,12 +62,12 @@ namespace Robust.Client
|
||||
|
||||
_configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged, invokeImmediately: true);
|
||||
|
||||
_playMan.Initialize();
|
||||
_playMan.Initialize(0);
|
||||
_playMan.PlayerListUpdated += OnPlayerListUpdated;
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void OnPlayerListUpdated(object? sender, EventArgs e)
|
||||
private void OnPlayerListUpdated()
|
||||
{
|
||||
var serverPlayers = _playMan.PlayerCount;
|
||||
if (_net.ServerChannel != null && GameInfo != null && _net.IsConnected)
|
||||
@@ -130,9 +127,10 @@ namespace Robust.Client
|
||||
{
|
||||
DebugTools.Assert(RunLevel < ClientRunLevel.Connecting);
|
||||
DebugTools.Assert(!_net.IsConnected);
|
||||
_playMan.Startup();
|
||||
_playMan.LocalPlayer!.Name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName);
|
||||
var name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName);
|
||||
_playMan.SetupSinglePlayer(name);
|
||||
OnRunLevelChanged(ClientRunLevel.SinglePlayerGame);
|
||||
_playMan.JoinGame(_playMan.LocalSession!);
|
||||
GameStartedSetup();
|
||||
}
|
||||
|
||||
@@ -173,22 +171,14 @@ namespace Robust.Client
|
||||
info.ServerName = serverName;
|
||||
}
|
||||
|
||||
var maxPlayers = _configManager.GetCVar<int>("game.maxplayers");
|
||||
info.ServerMaxPlayers = maxPlayers;
|
||||
|
||||
var userName = _net.ServerChannel!.UserName;
|
||||
var userId = _net.ServerChannel.UserId;
|
||||
var channel = _net.ServerChannel!;
|
||||
|
||||
// start up player management
|
||||
_playMan.Startup();
|
||||
|
||||
_playMan.LocalPlayer!.UserId = userId;
|
||||
_playMan.LocalPlayer.Name = userName;
|
||||
|
||||
_playMan.LocalPlayer.StatusChanged += OnLocalStatusChanged;
|
||||
_playMan.SetupMultiplayer(channel);
|
||||
_playMan.PlayerStatusChanged += OnStatusChanged;
|
||||
|
||||
var serverPlayers = _playMan.PlayerCount;
|
||||
_discord.Update(info.ServerName, userName, info.ServerMaxPlayers.ToString(), serverPlayers.ToString());
|
||||
_discord.Update(info.ServerName, channel.UserName, info.ServerMaxPlayers.ToString(), serverPlayers.ToString());
|
||||
|
||||
}
|
||||
|
||||
@@ -221,6 +211,8 @@ namespace Robust.Client
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_configManager.ReceivedInitialNwVars -= OnReceivedClientData;
|
||||
_playMan.PlayerStatusChanged -= OnStatusChanged;
|
||||
_configManager.ClearReceivedInitialNwVars();
|
||||
OnRunLevelChanged(ClientRunLevel.Initialize);
|
||||
}
|
||||
@@ -263,19 +255,17 @@ namespace Robust.Client
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void OnLocalStatusChanged(object? obj, StatusEventArgs eventArgs)
|
||||
private void OnStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
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 (eventArgs.OldStatus == SessionStatus.Connecting)
|
||||
{
|
||||
OnPlayerJoinedServer(_playMan.LocalPlayer!.Session);
|
||||
}
|
||||
|
||||
if (eventArgs.NewStatus == SessionStatus.InGame)
|
||||
{
|
||||
OnPlayerJoinedGame(_playMan.LocalPlayer!.Session);
|
||||
}
|
||||
if (e.OldStatus == SessionStatus.Connecting)
|
||||
OnPlayerJoinedServer(e.Session);
|
||||
else if (e.NewStatus == SessionStatus.InGame)
|
||||
OnPlayerJoinedGame(e.Session);
|
||||
}
|
||||
|
||||
private void OnRunLevelChanged(ClientRunLevel newRunLevel)
|
||||
|
||||
@@ -37,7 +37,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Replays;
|
||||
|
||||
@@ -13,7 +13,7 @@ using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
@@ -26,10 +26,7 @@ namespace Robust.Client.Console.Commands
|
||||
var entity = _entityManager.GetEntity(netEntity);
|
||||
var componentName = args[1];
|
||||
|
||||
var component = (Component) _componentFactory.GetComponent(componentName);
|
||||
|
||||
component.Owner = entity;
|
||||
|
||||
var component = _componentFactory.GetComponent(componentName);
|
||||
_entityManager.AddComponent(entity, component);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#if DEBUG
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
@@ -8,7 +9,6 @@ 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,6 +19,7 @@ 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;
|
||||
|
||||
@@ -70,7 +71,7 @@ namespace Robust.Client.Debugging
|
||||
return;
|
||||
}
|
||||
|
||||
var tile = grid.GetTileRef(spot);
|
||||
var tile = _mapSystem.GetTileRef(gridUid, grid, spot);
|
||||
_label.Position = mouseSpot.Position + new Vector2(32, 0);
|
||||
|
||||
if (_hovered?.GridId == gridUid && _hovered?.Tile == tile) return;
|
||||
@@ -79,7 +80,7 @@ namespace Robust.Client.Debugging
|
||||
|
||||
var text = new StringBuilder();
|
||||
|
||||
foreach (var ent in grid.GetAnchoredEntities(spot))
|
||||
foreach (var ent in _mapSystem.GetAnchoredEntities(gridUid, grid, spot))
|
||||
{
|
||||
if (EntityManager.TryGetComponent<MetaDataComponent>(ent, out var meta))
|
||||
{
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
@@ -207,6 +206,7 @@ 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,32 +231,33 @@ namespace Robust.Client.Debugging
|
||||
{
|
||||
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
|
||||
{
|
||||
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner)) continue;
|
||||
if (_entityManager.HasComponent<MapGridComponent>(physBody)) continue;
|
||||
|
||||
var xform = _physicsSystem.GetPhysicsTransform(physBody.Owner);
|
||||
var xform = _physicsSystem.GetPhysicsTransform(physBody);
|
||||
var comp = physBody.Comp;
|
||||
|
||||
const float AlphaModifier = 0.2f;
|
||||
|
||||
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody.Owner).Fixtures.Values)
|
||||
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody).Fixtures.Values)
|
||||
{
|
||||
// Invalid shape - Box2D doesn't check for IsSensor but we will for sanity.
|
||||
if (physBody.BodyType == BodyType.Dynamic && fixture.Density == 0f && fixture.Hard)
|
||||
if (comp.BodyType == BodyType.Dynamic && fixture.Density == 0f && fixture.Hard)
|
||||
{
|
||||
DrawShape(worldHandle, fixture, xform, Color.Red.WithAlpha(AlphaModifier));
|
||||
}
|
||||
else if (!physBody.CanCollide)
|
||||
else if (!comp.CanCollide)
|
||||
{
|
||||
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.5f, 0.3f).WithAlpha(AlphaModifier));
|
||||
}
|
||||
else if (physBody.BodyType == BodyType.Static)
|
||||
else if (comp.BodyType == BodyType.Static)
|
||||
{
|
||||
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.9f, 0.5f).WithAlpha(AlphaModifier));
|
||||
}
|
||||
else if ((physBody.BodyType & (BodyType.Kinematic | BodyType.KinematicController)) != 0x0)
|
||||
else if ((comp.BodyType & (BodyType.Kinematic | BodyType.KinematicController)) != 0x0)
|
||||
{
|
||||
DrawShape(worldHandle, fixture, xform, new Color(0.5f, 0.5f, 0.9f).WithAlpha(AlphaModifier));
|
||||
}
|
||||
else if (!physBody.Awake)
|
||||
else if (!comp.Awake)
|
||||
{
|
||||
DrawShape(worldHandle, fixture, xform, new Color(0.6f, 0.6f, 0.6f).WithAlpha(AlphaModifier));
|
||||
}
|
||||
@@ -275,15 +276,18 @@ namespace Robust.Client.Debugging
|
||||
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
|
||||
{
|
||||
var color = Color.Purple.WithAlpha(Alpha);
|
||||
var transform = _physicsSystem.GetPhysicsTransform(physBody.Owner);
|
||||
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), 0.2f, color);
|
||||
var transform = _physicsSystem.GetPhysicsTransform(physBody);
|
||||
worldHandle.DrawCircle(Transform.Mul(transform, physBody.Comp.LocalCenter), 0.2f, color);
|
||||
}
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, viewBounds))
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(mapId, viewBounds, ref _grids);
|
||||
|
||||
foreach (var grid in _grids)
|
||||
{
|
||||
var physBody = _entityManager.GetComponent<PhysicsComponent>(grid.Owner);
|
||||
var physBody = _entityManager.GetComponent<PhysicsComponent>(grid);
|
||||
var color = Color.Orange.WithAlpha(Alpha);
|
||||
var transform = _physicsSystem.GetPhysicsTransform(grid.Owner);
|
||||
var transform = _physicsSystem.GetPhysicsTransform(grid);
|
||||
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), 1f, color);
|
||||
}
|
||||
}
|
||||
@@ -292,14 +296,14 @@ namespace Robust.Client.Debugging
|
||||
{
|
||||
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
|
||||
{
|
||||
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner)) continue;
|
||||
if (_entityManager.HasComponent<MapGridComponent>(physBody)) continue;
|
||||
|
||||
var xform = _physicsSystem.GetPhysicsTransform(physBody.Owner);
|
||||
var xform = _physicsSystem.GetPhysicsTransform(physBody);
|
||||
|
||||
const float AlphaModifier = 0.2f;
|
||||
Box2? aabb = null;
|
||||
|
||||
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody.Owner).Fixtures.Values)
|
||||
foreach (var fixture in _entityManager.GetComponent<FixturesComponent>(physBody).Fixtures.Values)
|
||||
{
|
||||
for (var i = 0; i < fixture.Shape.ChildCount; i++)
|
||||
{
|
||||
@@ -318,10 +322,11 @@ namespace Robust.Client.Debugging
|
||||
{
|
||||
_drawnJoints.Clear();
|
||||
|
||||
foreach (var jointComponent in _entityManager.EntityQuery<JointComponent>(true))
|
||||
var query = _entityManager.AllEntityQueryEnumerator<JointComponent>();
|
||||
while (query.MoveNext(out var uid, out var jointComponent))
|
||||
{
|
||||
if (jointComponent.JointCount == 0 ||
|
||||
!_entityManager.TryGetComponent(jointComponent.Owner, out TransformComponent? xf1) ||
|
||||
!_entityManager.TryGetComponent(uid, out TransformComponent? xf1) ||
|
||||
!viewAABB.Contains(xf1.WorldPosition)) continue;
|
||||
|
||||
foreach (var (_, joint) in jointComponent.Joints)
|
||||
@@ -361,6 +366,9 @@ namespace Robust.Client.Debugging
|
||||
|
||||
_debugPhysicsSystem.PointCount = 0;
|
||||
}
|
||||
|
||||
worldHandle.UseShader(null);
|
||||
worldHandle.SetTransform(Matrix3.Identity);
|
||||
}
|
||||
|
||||
private void DrawScreen(DrawingHandleScreen screenHandle, OverlayDrawArgs args)
|
||||
@@ -370,28 +378,31 @@ namespace Robust.Client.Debugging
|
||||
|
||||
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.ShapeInfo) != 0x0)
|
||||
{
|
||||
var hoverBodies = new List<PhysicsComponent>();
|
||||
var hoverBodies = new List<Entity<PhysicsComponent>>();
|
||||
var bounds = Box2.UnitCentered.Translated(_eyeManager.PixelToMap(mousePos.Position).Position);
|
||||
|
||||
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, bounds))
|
||||
{
|
||||
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner)) continue;
|
||||
hoverBodies.Add(physBody);
|
||||
var uid = physBody.Owner;
|
||||
if (_entityManager.HasComponent<MapGridComponent>(uid)) continue;
|
||||
hoverBodies.Add((uid, 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 body in hoverBodies)
|
||||
foreach (var bodyEnt in hoverBodies)
|
||||
{
|
||||
if (body != hoverBodies[0])
|
||||
if (bodyEnt != hoverBodies[0])
|
||||
{
|
||||
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), "------");
|
||||
row++;
|
||||
}
|
||||
|
||||
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {body.Owner}");
|
||||
var body = bodyEnt.Comp;
|
||||
|
||||
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner}");
|
||||
row++;
|
||||
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
|
||||
row++;
|
||||
@@ -430,6 +441,9 @@ namespace Robust.Client.Debugging
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenHandle.UseShader(null);
|
||||
screenHandle.SetTransform(Matrix3.Identity);
|
||||
}
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
@@ -451,11 +465,26 @@ 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);
|
||||
@@ -465,6 +494,7 @@ namespace Robust.Client.Debugging
|
||||
worldHandle.DrawCircle(v1, 0.1f, color);
|
||||
worldHandle.DrawCircle(v2, 0.1f, color);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case PolygonShape poly:
|
||||
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
|
||||
|
||||
@@ -287,78 +287,6 @@ 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,
|
||||
@@ -457,7 +385,7 @@ namespace Robust.Client
|
||||
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
|
||||
}
|
||||
|
||||
_resourceManifest = LoadResourceManifest();
|
||||
_resourceManifest = ResourceManifestData.LoadResourceManifest(_resourceCache);
|
||||
|
||||
{
|
||||
// Handle GameControllerOptions implicit CVar overrides.
|
||||
@@ -704,7 +632,6 @@ 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;
|
||||
|
||||
@@ -786,20 +713,6 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Prometheus;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Player;
|
||||
@@ -43,9 +42,9 @@ namespace Robust.Client.GameObjects
|
||||
base.FlushEntities();
|
||||
}
|
||||
|
||||
EntityUid IClientEntityManagerInternal.CreateEntity(string? prototypeName)
|
||||
EntityUid IClientEntityManagerInternal.CreateEntity(string? prototypeName, out MetaDataComponent metadata)
|
||||
{
|
||||
return base.CreateEntity(prototypeName);
|
||||
return base.CreateEntity(prototypeName, out metadata);
|
||||
}
|
||||
|
||||
void IClientEntityManagerInternal.InitializeEntity(EntityUid entity, MetaDataComponent? meta)
|
||||
@@ -86,21 +85,23 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dirty(EntityUid uid, Component component, MetaDataComponent? meta = null)
|
||||
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)
|
||||
{
|
||||
// Client only dirties during prediction
|
||||
if (_gameTiming.InPrediction)
|
||||
base.Dirty(uid, component, meta);
|
||||
base.Dirty(ent, meta);
|
||||
}
|
||||
|
||||
[return: NotNullIfNotNull("uid")]
|
||||
public override EntityStringRepresentation? ToPrettyString(EntityUid? uid)
|
||||
public override EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metaDataComponent = null)
|
||||
{
|
||||
if (uid == null)
|
||||
return null;
|
||||
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity == uid)
|
||||
return base.ToPrettyString(uid).Value with { Session = _playerManager.LocalPlayer.Session };
|
||||
return base.ToPrettyString(uid) with { Session = _playerManager.LocalPlayer.Session };
|
||||
|
||||
return base.ToPrettyString(uid);
|
||||
}
|
||||
@@ -169,7 +170,7 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel channel)
|
||||
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel? channel)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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
|
||||
@@ -21,42 +19,5 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
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;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
public sealed class AnimationPlayerSystem : EntitySystem, IPostInjectInit
|
||||
public sealed class AnimationPlayerSystem : EntitySystem
|
||||
{
|
||||
private readonly List<AnimationPlayerComponent> _activeAnimations = new();
|
||||
private readonly List<Entity<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()
|
||||
{
|
||||
@@ -39,22 +35,22 @@ namespace Robust.Client.GameObjects
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Update(uid, anim, frameTime))
|
||||
if (!Update(uid, anim.Comp, frameTime))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_activeAnimations.RemoveSwap(i);
|
||||
i--;
|
||||
anim.HasPlayingAnimation = false;
|
||||
anim.Comp.HasPlayingAnimation = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal void AddComponent(AnimationPlayerComponent component)
|
||||
internal void AddComponent(Entity<AnimationPlayerComponent> ent)
|
||||
{
|
||||
if (component.HasPlayingAnimation) return;
|
||||
_activeAnimations.Add(component);
|
||||
component.HasPlayingAnimation = true;
|
||||
if (ent.Comp.HasPlayingAnimation) return;
|
||||
_activeAnimations.Add(ent);
|
||||
ent.Comp.HasPlayingAnimation = true;
|
||||
}
|
||||
|
||||
private bool Update(EntityUid uid, AnimationPlayerComponent component, float frameTime)
|
||||
@@ -79,7 +75,6 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
component.PlayingAnimations.Remove(key);
|
||||
EntityManager.EventBus.RaiseLocalEvent(uid, new AnimationCompletedEvent {Uid = uid, Key = key}, true);
|
||||
component.AnimationComplete(key);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -90,22 +85,29 @@ namespace Robust.Client.GameObjects
|
||||
/// </summary>
|
||||
public void Play(EntityUid uid, Animation animation, string key)
|
||||
{
|
||||
var component = EntityManager.EnsureComponent<AnimationPlayerComponent>(uid);
|
||||
Play(component, animation, key);
|
||||
var component = EnsureComp<AnimationPlayerComponent>(uid);
|
||||
Play(new Entity<AnimationPlayerComponent>(uid, 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(component, animation, key);
|
||||
Play(new Entity<AnimationPlayerComponent>(uid, 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)
|
||||
{
|
||||
AddComponent(component);
|
||||
Play(new Entity<AnimationPlayerComponent>(component.Owner, component), animation, key);
|
||||
}
|
||||
|
||||
public void Play(Entity<AnimationPlayerComponent> ent, Animation animation, string key)
|
||||
{
|
||||
AddComponent(ent);
|
||||
var playback = new AnimationPlaybackShared.AnimationPlayback(animation);
|
||||
|
||||
#if DEBUG
|
||||
@@ -117,18 +119,18 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
if (compTrack.ComponentType == null)
|
||||
{
|
||||
_sawmill.Error($"Attempted to play a component animation without any component specified.");
|
||||
Log.Error("Attempted to play a component animation without any component specified.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent(component.Owner, compTrack.ComponentType, out var animatedComp))
|
||||
if (!EntityManager.TryGetComponent(ent, compTrack.ComponentType, out var animatedComp))
|
||||
{
|
||||
_sawmill.Error(
|
||||
$"Attempted to play a component animation, but the entity {ToPrettyString(component.Owner)} does not have the component to be animated: {compTrack.ComponentType}.");
|
||||
Log.Error(
|
||||
$"Attempted to play a component animation, but the entity {ToPrettyString(ent)} does not have the component to be animated: {compTrack.ComponentType}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsClientSide(component.Owner) || !animatedComp.NetSyncEnabled)
|
||||
if (IsClientSide(ent) || !animatedComp.NetSyncEnabled)
|
||||
continue;
|
||||
|
||||
var reg = _compFact.GetRegistration(animatedComp);
|
||||
@@ -136,12 +138,18 @@ namespace Robust.Client.GameObjects
|
||||
// In principle there is nothing wrong with this, as long as the property of the component being
|
||||
// animated is not part of the networked state and setting it does not dirty the component. Hence only a
|
||||
// warning in debug mode.
|
||||
if (reg.NetID != null)
|
||||
_sawmill.Warning($"Playing a component animation on a networked component {reg.Name} belonging to {ToPrettyString(component.Owner)}");
|
||||
if (reg.NetID != null && compTrack.Property != null)
|
||||
{
|
||||
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)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
component.PlayingAnimations.Add(key, playback);
|
||||
ent.Comp.PlayingAnimations.Add(key, playback);
|
||||
}
|
||||
|
||||
public bool HasRunningAnimation(EntityUid uid, string key)
|
||||
@@ -170,19 +178,18 @@ 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;
|
||||
component.PlayingAnimations.Remove(key);
|
||||
}
|
||||
if (!Resolve(uid, ref component, false))
|
||||
return;
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("anim");
|
||||
component.PlayingAnimations.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ 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;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Physics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
@@ -15,8 +14,8 @@ public sealed class EyeSystem : SharedEyeSystem
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<EyeComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<EyeComponent, PlayerDetachedEvent>(OnEyeDetached);
|
||||
SubscribeLocalEvent<EyeComponent, PlayerAttachedEvent>(OnEyeAttached);
|
||||
SubscribeLocalEvent<EyeComponent, LocalPlayerDetachedEvent>(OnEyeDetached);
|
||||
SubscribeLocalEvent<EyeComponent, LocalPlayerAttachedEvent>(OnEyeAttached);
|
||||
SubscribeLocalEvent<EyeComponent, AfterAutoHandleStateEvent>(OnEyeAutoState);
|
||||
|
||||
// Make sure this runs *after* entities have been moved by interpolation and movement.
|
||||
@@ -26,35 +25,25 @@ public sealed class EyeSystem : SharedEyeSystem
|
||||
|
||||
private void OnEyeAutoState(EntityUid uid, EyeComponent component, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
UpdateEye(component);
|
||||
UpdateEye((uid, component));
|
||||
}
|
||||
|
||||
private void OnEyeAttached(EntityUid uid, EyeComponent component, PlayerAttachedEvent args)
|
||||
private void OnEyeAttached(EntityUid uid, EyeComponent component, LocalPlayerAttachedEvent args)
|
||||
{
|
||||
// TODO: This probably shouldn't be nullable bruv.
|
||||
if (component._eye != null)
|
||||
{
|
||||
_eyeManager.CurrentEye = component._eye;
|
||||
}
|
||||
|
||||
UpdateEye((uid, component));
|
||||
_eyeManager.CurrentEye = component.Eye;
|
||||
var ev = new EyeAttachedEvent(uid, component);
|
||||
RaiseLocalEvent(uid, ref ev, true);
|
||||
}
|
||||
|
||||
private void OnEyeDetached(EntityUid uid, EyeComponent component, PlayerDetachedEvent args)
|
||||
private void OnEyeDetached(EntityUid uid, EyeComponent component, LocalPlayerDetachedEvent args)
|
||||
{
|
||||
_eyeManager.ClearCurrentEye();
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, EyeComponent component, ComponentInit args)
|
||||
{
|
||||
component._eye = new Eye
|
||||
{
|
||||
Position = Transform(uid).MapPosition,
|
||||
Zoom = component.Zoom,
|
||||
DrawFov = component.DrawFov,
|
||||
Rotation = component.Rotation,
|
||||
};
|
||||
UpdateEye((uid, component));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -64,7 +53,7 @@ public sealed class EyeSystem : SharedEyeSystem
|
||||
|
||||
while (query.MoveNext(out var uid, out var eyeComponent))
|
||||
{
|
||||
if (eyeComponent._eye == null)
|
||||
if (eyeComponent.Eye == null)
|
||||
continue;
|
||||
|
||||
if (!TryComp<TransformComponent>(eyeComponent.Target, out var xform))
|
||||
@@ -73,7 +62,7 @@ public sealed class EyeSystem : SharedEyeSystem
|
||||
eyeComponent.Target = null;
|
||||
}
|
||||
|
||||
eyeComponent._eye.Position = xform.MapPosition;
|
||||
eyeComponent.Eye.Position = xform.MapPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
@@ -58,6 +59,8 @@ 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;
|
||||
@@ -71,13 +74,15 @@ namespace Robust.Client.GameObjects
|
||||
var viewport = args.WorldBounds;
|
||||
var worldHandle = args.WorldHandle;
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(currentMap, viewport))
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(currentMap, viewport, ref _grids);
|
||||
foreach (var grid in _grids)
|
||||
{
|
||||
var worldMatrix = _entityManager.GetComponent<TransformComponent>(grid.Owner).WorldMatrix;
|
||||
var worldMatrix = _entityManager.GetComponent<TransformComponent>(grid).WorldMatrix;
|
||||
worldHandle.SetTransform(worldMatrix);
|
||||
var transform = new Transform(Vector2.Zero, Angle.Zero);
|
||||
|
||||
var chunkEnumerator = grid.GetMapChunks(viewport);
|
||||
var chunkEnumerator = grid.Comp.GetMapChunks(viewport);
|
||||
|
||||
while (chunkEnumerator.MoveNext(out var chunk))
|
||||
{
|
||||
|
||||
@@ -3,16 +3,13 @@ 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.Maths;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -131,7 +128,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<PlayerAttachSysMessage>(OnAttachedEntityChanged);
|
||||
SubscribeLocalEvent<LocalPlayerAttachedEvent>(OnAttachedEntityChanged);
|
||||
|
||||
_conHost.RegisterCommand("incmd",
|
||||
"Inserts an input command into the simulation",
|
||||
@@ -171,11 +168,11 @@ namespace Robust.Client.GameObjects
|
||||
HandleInputCommand(localPlayer.Session, keyFunction, message);
|
||||
}
|
||||
|
||||
private void OnAttachedEntityChanged(PlayerAttachSysMessage message)
|
||||
private void OnAttachedEntityChanged(LocalPlayerAttachedEvent message)
|
||||
{
|
||||
if (message.AttachedEntity != default) // attach
|
||||
if (message.Entity != default) // attach
|
||||
{
|
||||
SetEntityContextActive(_inputManager, message.AttachedEntity);
|
||||
SetEntityContextActive(_inputManager, message.Entity);
|
||||
}
|
||||
else // detach
|
||||
{
|
||||
@@ -227,44 +224,4 @@ 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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
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;
|
||||
@@ -15,6 +18,7 @@ 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
|
||||
@@ -183,6 +187,48 @@ 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>
|
||||
|
||||
@@ -1,51 +1,41 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed partial class TransformSystem
|
||||
{
|
||||
public override void SetLocalPosition(TransformComponent xform, Vector2 value)
|
||||
public override void SetLocalPosition(EntityUid uid, Vector2 value, TransformComponent? xform = null)
|
||||
{
|
||||
xform.PrevPosition = xform._localPosition;
|
||||
if (!XformQuery.Resolve(uid, ref xform))
|
||||
return;
|
||||
|
||||
xform.NextPosition = value;
|
||||
xform.LerpParent = xform.ParentUid;
|
||||
base.SetLocalPosition(xform, value);
|
||||
ActivateLerp(xform);
|
||||
ActivateLerp(uid, xform);
|
||||
base.SetLocalPosition(uid, value, xform);
|
||||
}
|
||||
|
||||
public override void SetLocalPositionNoLerp(TransformComponent xform, Vector2 value)
|
||||
public override void SetLocalRotation(EntityUid uid, Angle value, TransformComponent? xform = null)
|
||||
{
|
||||
xform.NextPosition = null;
|
||||
xform.LerpParent = EntityUid.Invalid;
|
||||
base.SetLocalPositionNoLerp(xform, value);
|
||||
if (!XformQuery.Resolve(uid, ref xform))
|
||||
return;
|
||||
|
||||
xform.NextRotation = value;
|
||||
ActivateLerp(uid, xform);
|
||||
base.SetLocalRotation(uid, value, xform);
|
||||
}
|
||||
|
||||
public override void SetLocalRotationNoLerp(TransformComponent xform, Angle angle)
|
||||
public override void SetLocalPositionRotation(EntityUid uid, Vector2 pos, Angle rot, TransformComponent? xform = null)
|
||||
{
|
||||
xform.NextRotation = null;
|
||||
xform.LerpParent = EntityUid.Invalid;
|
||||
base.SetLocalRotationNoLerp(xform, angle);
|
||||
}
|
||||
if (!XformQuery.Resolve(uid, ref xform))
|
||||
return;
|
||||
|
||||
public override void SetLocalRotation(TransformComponent xform, Angle angle)
|
||||
{
|
||||
xform.PrevRotation = xform._localRotation;
|
||||
xform.NextRotation = angle;
|
||||
xform.LerpParent = xform.ParentUid;
|
||||
base.SetLocalRotation(xform, angle);
|
||||
ActivateLerp(xform);
|
||||
}
|
||||
|
||||
public override void SetLocalPositionRotation(TransformComponent xform, Vector2 pos, Angle rot)
|
||||
{
|
||||
xform.PrevPosition = xform._localPosition;
|
||||
xform.NextPosition = pos;
|
||||
xform.PrevRotation = xform._localRotation;
|
||||
xform.NextRotation = rot;
|
||||
xform.LerpParent = xform.ParentUid;
|
||||
base.SetLocalPositionRotation(xform, pos, rot);
|
||||
ActivateLerp(xform);
|
||||
ActivateLerp(uid, xform);
|
||||
base.SetLocalPositionRotation(uid, pos, rot, xform);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using JetBrains.Annotations;
|
||||
@@ -25,20 +24,15 @@ namespace Robust.Client.GameObjects
|
||||
private const float MinInterpolationDistance = 0.001f;
|
||||
private const float MinInterpolationDistanceSquared = MinInterpolationDistance * MinInterpolationDistance;
|
||||
|
||||
private const double MinInterpolationAngle = Math.PI / 720;
|
||||
|
||||
// 45 degrees.
|
||||
private const double MaxInterpolationAngle = Math.PI / 4;
|
||||
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
// Only keep track of transforms actively lerping.
|
||||
// Much faster than iterating 3000+ transforms every frame.
|
||||
[ViewVariables] private readonly List<TransformComponent> _lerpingTransforms = new();
|
||||
[ViewVariables] private readonly List<Entity<TransformComponent>> _lerpingTransforms = new();
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
foreach (var xform in _lerpingTransforms)
|
||||
foreach (var (_, xform) in _lerpingTransforms)
|
||||
{
|
||||
xform.ActivelyLerping = false;
|
||||
xform.NextPosition = null;
|
||||
@@ -48,21 +42,78 @@ namespace Robust.Client.GameObjects
|
||||
_lerpingTransforms.Clear();
|
||||
}
|
||||
|
||||
public override void ActivateLerp(TransformComponent xform)
|
||||
public override void ActivateLerp(EntityUid uid, TransformComponent xform)
|
||||
{
|
||||
if (xform.ActivelyLerping)
|
||||
// This lerping logic is pretty convoluted and generally assumes that the client does not mispredict.
|
||||
// A more foolproof solution would be to just cache the coordinates at which any given entity was most
|
||||
// recently rendered and using that as the lerp origin. However that'd require enumerating over all entities
|
||||
// every tick which is pretty icky.
|
||||
|
||||
// The general considerations are:
|
||||
// - If the client receives a server state for an entity moving from a->b and predicts nothing else, then it
|
||||
// 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.
|
||||
// - 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.
|
||||
// - 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.
|
||||
|
||||
// If the client predicts an entity moves while already lerping, it should clear the
|
||||
// predict a->b, lerp a->b
|
||||
// predicted a->b, then predict b->c. Lerp b->c
|
||||
// predicted a->b, then predict b->c. Lerp b->c
|
||||
// predicted a->b, predicted b->c, then predict c->d. Lerp c->d
|
||||
// server state a->b, then predicted b->c, lerp b->c
|
||||
// server state a->b, then predicted b->c, then predict d, lerp b->c
|
||||
|
||||
if (_gameTiming.ApplyingState)
|
||||
{
|
||||
if (xform.ActivelyLerping)
|
||||
{
|
||||
// This should not happen, but can happen if some bad component state application code modifies an entity's coordinates.
|
||||
Log.Error($"Entity {(ToPrettyString(uid))} tried to lerp twice while applying component states.");
|
||||
return;
|
||||
}
|
||||
|
||||
_lerpingTransforms.Add((uid, xform));
|
||||
xform.ActivelyLerping = true;
|
||||
xform.PredictedLerp = false;
|
||||
xform.LerpParent = xform.ParentUid;
|
||||
xform.PrevRotation = xform._localRotation;
|
||||
xform.PrevPosition = xform._localPosition;
|
||||
xform.LastLerp = _gameTiming.CurTick;
|
||||
return;
|
||||
}
|
||||
|
||||
xform.ActivelyLerping = true;
|
||||
_lerpingTransforms.Add(xform);
|
||||
}
|
||||
xform.LastLerp = _gameTiming.CurTick;
|
||||
if (!_gameTiming.IsFirstTimePredicted)
|
||||
{
|
||||
xform.ActivelyLerping = false;
|
||||
return;
|
||||
}
|
||||
|
||||
public override void DeactivateLerp(TransformComponent component)
|
||||
{
|
||||
// this should cause the lerp to do nothing
|
||||
component.NextPosition = null;
|
||||
component.NextRotation = null;
|
||||
component.LerpParent = EntityUid.Invalid;
|
||||
if (!xform.ActivelyLerping)
|
||||
{
|
||||
_lerpingTransforms.Add((uid, xform));
|
||||
xform.ActivelyLerping = true;
|
||||
xform.PredictedLerp = true;
|
||||
xform.PrevRotation = xform._localRotation;
|
||||
xform.PrevPosition = xform._localPosition;
|
||||
xform.LerpParent = xform.ParentUid;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!xform.PredictedLerp || xform.LerpParent != xform.ParentUid)
|
||||
{
|
||||
// Existing lerp was not due to prediction, but due to state application. That lerp should already
|
||||
// have been rendered, so we will start a new lerp from the current position.
|
||||
xform.PrevRotation = xform._localRotation;
|
||||
xform.PrevPosition = xform._localPosition;
|
||||
xform.LerpParent = xform.ParentUid;
|
||||
}
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
@@ -73,12 +124,13 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
for (var i = 0; i < _lerpingTransforms.Count; i++)
|
||||
{
|
||||
var transform = _lerpingTransforms[i];
|
||||
var (uid, transform) = _lerpingTransforms[i];
|
||||
var found = false;
|
||||
|
||||
// Only lerp if parent didn't change.
|
||||
// E.g. entering lockers would do it.
|
||||
if (transform.LerpParent == transform.ParentUid
|
||||
if (transform.ActivelyLerping
|
||||
&& transform.LerpParent == transform.ParentUid
|
||||
&& transform.ParentUid.IsValid()
|
||||
&& !transform.Deleted)
|
||||
{
|
||||
@@ -90,8 +142,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
if (distance is > MinInterpolationDistanceSquared and < MaxInterpolationDistanceSquared)
|
||||
{
|
||||
transform.LocalPosition = Vector2.Lerp(lerpSource, lerpDest, step);
|
||||
// Setting LocalPosition clears LerpPosition so fix that.
|
||||
SetLocalPositionNoLerp(uid, Vector2.Lerp(lerpSource, lerpDest, step), transform);
|
||||
transform.NextPosition = lerpDest;
|
||||
found = true;
|
||||
}
|
||||
@@ -101,15 +152,9 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
var lerpDest = transform.NextRotation.Value;
|
||||
var lerpSource = transform.PrevRotation;
|
||||
var distance = Math.Abs(Angle.ShortestDistance(lerpDest, lerpSource));
|
||||
|
||||
if (distance is > MinInterpolationAngle and < MaxInterpolationAngle)
|
||||
{
|
||||
transform.LocalRotation = Angle.Lerp(lerpSource, lerpDest, step);
|
||||
// Setting LocalRotation clears LerpAngle so fix that.
|
||||
transform.NextRotation = lerpDest;
|
||||
found = true;
|
||||
}
|
||||
SetLocalRotationNoLerp(uid, Angle.Lerp(lerpSource, lerpDest, step), transform);
|
||||
transform.NextRotation = lerpDest;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
// These methods are used by the Game State Manager.
|
||||
|
||||
EntityUid CreateEntity(string? prototypeName);
|
||||
EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata);
|
||||
|
||||
void InitializeEntity(EntityUid entity, MetaDataComponent? meta = null);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Physics;
|
||||
@@ -25,8 +26,6 @@ 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;
|
||||
@@ -36,7 +35,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[UsedImplicitly]
|
||||
public sealed class ClientGameStateManager : IClientGameStateManager, IPostInjectInit
|
||||
public sealed class ClientGameStateManager : IClientGameStateManager
|
||||
{
|
||||
private GameStateProcessor _processor = default!;
|
||||
|
||||
@@ -52,6 +51,14 @@ namespace Robust.Client.GameStates
|
||||
private readonly Dictionary<NetEntity, EntityState> _toCreate = new();
|
||||
private readonly Dictionary<ushort, (IComponent Component, ComponentState? curState, ComponentState? nextState)> _compStateWork = new();
|
||||
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 Dictionary<NetEntity, Dictionary<ushort, ComponentState>> _outputData = new();
|
||||
private readonly List<(EntityUid, TransformComponent)> _queuedBroadphaseUpdates = new();
|
||||
|
||||
private readonly ObjectPool<Dictionary<ushort, ComponentState>> _compDataPool =
|
||||
new DefaultObjectPool<Dictionary<ushort, ComponentState>>(new DictPolicy<ushort, ComponentState>(), 256);
|
||||
|
||||
private uint _metaCompNetId;
|
||||
|
||||
@@ -73,6 +80,13 @@ 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;
|
||||
|
||||
@@ -80,7 +94,8 @@ namespace Robust.Client.GameStates
|
||||
public int TargetBufferSize => _processor.TargetBufferSize;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentBufferSize => _processor.CalculateBufferSize(_timing.LastRealTick);
|
||||
public int GetApplicableStateCount() => _processor.GetApplicableStateCount();
|
||||
public int StateCount => _processor.StateCount;
|
||||
|
||||
public bool IsPredictionEnabled { get; private set; }
|
||||
public bool PredictionNeedsResetting { get; private set; }
|
||||
@@ -102,10 +117,22 @@ namespace Robust.Client.GameStates
|
||||
|
||||
public event Action<MsgStateLeavePvs>? PvsLeave;
|
||||
|
||||
#if DEBUG
|
||||
/// <summary>
|
||||
/// If true, this will cause received game states to be ignored. Used by integration tests.
|
||||
/// </summary>
|
||||
public bool DropStates;
|
||||
#endif
|
||||
|
||||
private bool _resettingPredictedEntities;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
{
|
||||
_processor = new GameStateProcessor(_timing);
|
||||
_sawmill = _logMan.GetSawmill("state");
|
||||
_sawmill.Level = LogLevel.Info;
|
||||
|
||||
_processor = new GameStateProcessor(this, _timing, _sawmill);
|
||||
|
||||
_network.RegisterNetMessage<MsgState>(HandleStateMessage);
|
||||
_network.RegisterNetMessage<MsgStateLeavePvs>(HandlePvsLeaveMessage);
|
||||
@@ -121,6 +148,7 @@ 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);
|
||||
@@ -135,6 +163,8 @@ namespace Robust.Client.GameStates
|
||||
_conHost.RegisterCommand("localdelete", Loc.GetString("cmd-local-delete-desc"), Loc.GetString("cmd-local-delete-help"), LocalDeleteEntCommand);
|
||||
_conHost.RegisterCommand("fullstatereset", Loc.GetString("cmd-full-state-reset-desc"), Loc.GetString("cmd-full-state-reset-help"), (_,_,_) => RequestFullState());
|
||||
|
||||
_entities.ComponentAdded += OnComponentAdded;
|
||||
|
||||
var metaId = _compFactory.GetRegistration(typeof(MetaDataComponent)).NetID;
|
||||
if (!metaId.HasValue)
|
||||
throw new InvalidOperationException("MetaDataComponent does not have a NetId.");
|
||||
@@ -142,6 +172,23 @@ namespace Robust.Client.GameStates
|
||||
_metaCompNetId = metaId.Value;
|
||||
}
|
||||
|
||||
private void OnComponentAdded(AddedComponentEventArgs args)
|
||||
{
|
||||
if (_resettingPredictedEntities)
|
||||
{
|
||||
var comp = args.ComponentType;
|
||||
|
||||
if (comp.NetID == null)
|
||||
return;
|
||||
|
||||
_sawmill.Error($"""
|
||||
Added component {comp.Name} with net id {comp.NetID} while resetting predicted entities.
|
||||
Stack trace:
|
||||
{Environment.StackTrace}
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
{
|
||||
@@ -194,6 +241,10 @@ namespace Robust.Client.GameStates
|
||||
|
||||
private void HandleStateMessage(MsgState message)
|
||||
{
|
||||
#if DEBUG
|
||||
if (DropStates)
|
||||
return;
|
||||
#endif
|
||||
// We ONLY ack states that are definitely going to get applied. Otherwise the sever might assume that we
|
||||
// applied a state containing entity-creation information, which it would then no longer send to us when
|
||||
// we re-encounter this entity
|
||||
@@ -225,9 +276,19 @@ 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 = CurrentBufferSize;
|
||||
var curBufSize = GetApplicableStateCount();
|
||||
var targetBufSize = TargetBufferSize;
|
||||
|
||||
var bufferOverflow = curBufSize - targetBufSize - StateBufferMergeThreshold;
|
||||
@@ -268,24 +329,21 @@ namespace Robust.Client.GameStates
|
||||
continue;
|
||||
}
|
||||
|
||||
if (PredictionNeedsResetting)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
ResetPredictedEntities();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// avoid exception spam from repeatedly trying to reset the same entity.
|
||||
_entitySystemManager.GetEntitySystem<ClientDirtySystem>().Reset();
|
||||
_runtimeLog.LogException(e, "ResetPredictedEntities");
|
||||
}
|
||||
ResetPredictedEntities();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// avoid exception spam from repeatedly trying to reset the same entity.
|
||||
_entitySystemManager.GetEntitySystem<ClientDirtySystem>().Reset();
|
||||
_runtimeLog.LogException(e, "ResetPredictedEntities");
|
||||
}
|
||||
|
||||
// If we were waiting for a new state, we are now applying it.
|
||||
if (_processor.LastFullStateRequested.HasValue)
|
||||
if (_processor.WaitingForFull)
|
||||
{
|
||||
_processor.LastFullStateRequested = null;
|
||||
_processor.OnFullStateReceived();
|
||||
_timing.LastProcessedTick = curState.ToSequence;
|
||||
DebugTools.Assert(curState.FromSequence == GameTick.Zero);
|
||||
PartialStateReset(curState, true);
|
||||
@@ -350,7 +408,7 @@ namespace Robust.Client.GameStates
|
||||
if (_processor.WaitingForFull)
|
||||
_timing.TickTimingAdjustment = 0f;
|
||||
else
|
||||
_timing.TickTimingAdjustment = (CurrentBufferSize - (float)TargetBufferSize) * 0.10f;
|
||||
_timing.TickTimingAdjustment = (GetApplicableStateCount() - (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.
|
||||
@@ -395,11 +453,11 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
}
|
||||
|
||||
public void RequestFullState(NetEntity? missingEntity = null)
|
||||
public void RequestFullState(NetEntity? missingEntity = null, GameTick? tick = null)
|
||||
{
|
||||
_sawmill.Info("Requesting full server state");
|
||||
_network.ClientSendMessage(new MsgStateRequestFull { Tick = _timing.LastRealTick , MissingEntity = missingEntity ?? NetEntity.Invalid });
|
||||
_processor.RequestFullState();
|
||||
_processor.OnFullStateRequested(tick ?? _timing.LastRealTick);
|
||||
}
|
||||
|
||||
public void PredictTicks(GameTick predictionTarget)
|
||||
@@ -468,19 +526,21 @@ namespace Robust.Client.GameStates
|
||||
|
||||
public void ResetPredictedEntities()
|
||||
{
|
||||
PredictionNeedsResetting = false;
|
||||
|
||||
using var _ = _prof.Group("ResetPredictedEntities");
|
||||
using var __ = _timing.StartStateApplicationArea();
|
||||
|
||||
// This is terrible, and I hate it. This also needs to run even when prediction is disabled.
|
||||
_entitySystemManager.GetEntitySystem<SharedGridTraversalSystem>().QueuedEvents.Clear();
|
||||
_entitySystemManager.GetEntitySystem<TransformSystem>().Reset();
|
||||
|
||||
if (!PredictionNeedsResetting)
|
||||
return;
|
||||
|
||||
PredictionNeedsResetting = false;
|
||||
var countReset = 0;
|
||||
var system = _entitySystemManager.GetEntitySystem<ClientDirtySystem>();
|
||||
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
|
||||
RemQueue<Component> toRemove = new();
|
||||
|
||||
// This is terrible, and I hate it.
|
||||
_entitySystemManager.GetEntitySystem<SharedGridTraversalSystem>().QueuedEvents.Clear();
|
||||
_entitySystemManager.GetEntitySystem<TransformSystem>().Reset();
|
||||
RemQueue<IComponent> toRemove = new();
|
||||
|
||||
foreach (var entity in system.DirtyEntities)
|
||||
{
|
||||
@@ -498,40 +558,50 @@ namespace Robust.Client.GameStates
|
||||
|
||||
countReset += 1;
|
||||
|
||||
foreach (var (netId, comp) in meta.NetComponents)
|
||||
try
|
||||
{
|
||||
if (!comp.NetSyncEnabled)
|
||||
continue;
|
||||
_resettingPredictedEntities = true;
|
||||
|
||||
// Was this component added during prediction?
|
||||
if (comp.CreationTick > _timing.LastRealTick)
|
||||
foreach (var (netId, comp) in meta.NetComponents)
|
||||
{
|
||||
if (last.ContainsKey(netId))
|
||||
if (!comp.NetSyncEnabled)
|
||||
continue;
|
||||
|
||||
// Was this component added during prediction?
|
||||
if (comp.CreationTick > _timing.LastRealTick)
|
||||
{
|
||||
// Component was probably removed and then re-addedd during a single prediction run
|
||||
// Just reset state as normal.
|
||||
comp.ClearCreationTick();
|
||||
if (last.ContainsKey(netId))
|
||||
{
|
||||
// Component was probably removed and then re-addedd during a single prediction run
|
||||
// Just reset state as normal.
|
||||
comp.ClearCreationTick();
|
||||
}
|
||||
else
|
||||
{
|
||||
toRemove.Add(comp);
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A new component was added: {comp.GetType()}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (comp.LastModifiedTick <= _timing.LastRealTick ||
|
||||
!last.TryGetValue(netId, out var compState))
|
||||
{
|
||||
toRemove.Add(comp);
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A new component was added: {comp.GetType()}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
|
||||
|
||||
var handleState = new ComponentHandleState(compState, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
|
||||
comp.LastModifiedTick = _timing.LastRealTick;
|
||||
}
|
||||
|
||||
if (comp.LastModifiedTick <= _timing.LastRealTick || !last.TryGetValue(netId, out var compState))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_sawmill.Level <= LogLevel.Debug)
|
||||
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
|
||||
|
||||
var handleState = new ComponentHandleState(compState, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
|
||||
comp.LastModifiedTick = _timing.LastRealTick;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_resettingPredictedEntities = false;
|
||||
}
|
||||
|
||||
// Remove predicted component additions
|
||||
@@ -585,18 +655,17 @@ 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(IEventBus, IComponent)"/>.
|
||||
/// cref="IEntityManager.GetComponentState"/>.
|
||||
/// </remarks>
|
||||
private void MergeImplicitData(IEnumerable<NetEntity> createdEntities)
|
||||
{
|
||||
var outputData = new Dictionary<NetEntity, Dictionary<ushort, ComponentState>>();
|
||||
var bus = _entityManager.EventBus;
|
||||
|
||||
foreach (var netEntity in createdEntities)
|
||||
{
|
||||
var (createdEntity, meta) = _entityManager.GetEntityData(netEntity);
|
||||
var compData = new Dictionary<ushort, ComponentState>();
|
||||
outputData.Add(netEntity, compData);
|
||||
var compData = _compDataPool.Get();
|
||||
_outputData.Add(netEntity, compData);
|
||||
|
||||
foreach (var (netId, component) in meta.NetComponents)
|
||||
{
|
||||
@@ -609,7 +678,14 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
}
|
||||
|
||||
_processor.MergeImplicitData(outputData);
|
||||
_processor.MergeImplicitData(_outputData);
|
||||
|
||||
foreach (var data in _outputData.Values)
|
||||
{
|
||||
_compDataPool.Return(data);
|
||||
}
|
||||
|
||||
_outputData.Clear();
|
||||
}
|
||||
|
||||
private void AckGameState(GameTick sequence)
|
||||
@@ -645,7 +721,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
using (_prof.Group("Player"))
|
||||
{
|
||||
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<PlayerState>());
|
||||
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<SessionState>());
|
||||
}
|
||||
|
||||
using (_prof.Group("Callback"))
|
||||
@@ -687,12 +763,10 @@ namespace Robust.Client.GameStates
|
||||
if (metaState == null)
|
||||
throw new MissingMetadataException(es.NetEntity);
|
||||
|
||||
var uid = _entities.CreateEntity(metaState.PrototypeId);
|
||||
var uid = _entities.CreateEntity(metaState.PrototypeId, out var newMeta);
|
||||
_toCreate.Add(es.NetEntity, es);
|
||||
var newMeta = metas.GetComponent(uid);
|
||||
_toApply.Add(uid, (es.NetEntity, newMeta, false, GameTick.Zero, es, null));
|
||||
|
||||
|
||||
// Client creates a client-side net entity for the newly created entity.
|
||||
// We need to clear this mapping before assigning the real net id.
|
||||
// TODO NetEntity Jank: prevent the client from creating this in the first place.
|
||||
@@ -722,9 +796,7 @@ namespace Robust.Client.GameStates
|
||||
if (_toCreate.ContainsKey(es.NetEntity))
|
||||
continue;
|
||||
|
||||
var uid = _entityManager.GetEntity(es.NetEntity);
|
||||
|
||||
if (!metas.TryGetComponent(uid, out var meta))
|
||||
if (!_entityManager.TryGetEntityData(es.NetEntity, out var uid, out var meta))
|
||||
continue;
|
||||
|
||||
bool isEnteringPvs = (meta.Flags & MetaDataFlags.Detached) != 0;
|
||||
@@ -739,7 +811,7 @@ namespace Robust.Client.GameStates
|
||||
continue;
|
||||
}
|
||||
|
||||
_toApply.Add(uid, (es.NetEntity, meta, isEnteringPvs, meta.LastStateApplied, es, null));
|
||||
_toApply.Add(uid.Value, (es.NetEntity, meta, isEnteringPvs, meta.LastStateApplied, es, null));
|
||||
meta.LastStateApplied = curState.ToSequence;
|
||||
}
|
||||
|
||||
@@ -785,7 +857,7 @@ namespace Robust.Client.GameStates
|
||||
state = (meta.NetEntity, meta, false, GameTick.Zero, null, null);
|
||||
}
|
||||
|
||||
var queuedBroadphaseUpdates = new List<(EntityUid, TransformComponent)>(enteringPvs);
|
||||
_queuedBroadphaseUpdates.Clear();
|
||||
|
||||
// Apply entity states.
|
||||
using (_prof.Group("Apply States"))
|
||||
@@ -805,7 +877,7 @@ namespace Robust.Client.GameStates
|
||||
DebugTools.Assert(xform.Broadphase == BroadphaseData.Invalid);
|
||||
xform.Broadphase = null;
|
||||
if (!_toApply.TryGetValue(xform.ParentUid, out var parent) || !parent.EnteringPvs)
|
||||
queuedBroadphaseUpdates.Add((entity, xform));
|
||||
_queuedBroadphaseUpdates.Add((entity, xform));
|
||||
}
|
||||
|
||||
_prof.WriteValue("Count", ProfData.Int32(_toApply.Count));
|
||||
@@ -816,7 +888,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var (uid, xform) in queuedBroadphaseUpdates)
|
||||
foreach (var (uid, xform) in _queuedBroadphaseUpdates)
|
||||
{
|
||||
lookupSys.FindAndAddToEntityTree(uid, true, xform);
|
||||
}
|
||||
@@ -833,7 +905,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessDeletions(delSpan, xforms, metas, xformSys);
|
||||
ProcessDeletions(delSpan, xforms, xformSys);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -870,17 +942,17 @@ namespace Robust.Client.GameStates
|
||||
_sawmill.Info($"Resetting all entity states to tick {state.ToSequence}.");
|
||||
|
||||
// Construct hashset for set.Contains() checks.
|
||||
_stateEnts.Clear();
|
||||
var entityStates = state.EntityStates.Span;
|
||||
var stateEnts = new HashSet<NetEntity>();
|
||||
foreach (var entState in entityStates)
|
||||
{
|
||||
stateEnts.Add(entState.NetEntity);
|
||||
_stateEnts.Add(entState.NetEntity);
|
||||
}
|
||||
|
||||
var xforms = _entities.GetEntityQuery<TransformComponent>();
|
||||
var xformSys = _entitySystemManager.GetEntitySystem<SharedTransformSystem>();
|
||||
|
||||
var toDelete = new List<EntityUid>(Math.Max(64, _entities.EntityCount - stateEnts.Count));
|
||||
_toDelete.Clear();
|
||||
|
||||
// Client side entities won't need the transform, but that should always be a tiny minority of entities
|
||||
var metaQuery = _entityManager.AllEntityQueryEnumerator<MetaDataComponent, TransformComponent>();
|
||||
@@ -891,14 +963,16 @@ namespace Robust.Client.GameStates
|
||||
if (metadata.NetEntity.IsClientSide())
|
||||
{
|
||||
if (deleteClientEntities)
|
||||
toDelete.Add(ent);
|
||||
_toDelete.Add(ent);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stateEnts.Contains(netEnt))
|
||||
if (_stateEnts.Contains(netEnt))
|
||||
{
|
||||
if (resetAllEntities || metadata.LastStateApplied > state.ToSequence)
|
||||
metadata.LastStateApplied = GameTick.Zero; // TODO track last-state-applied for individual components? Is it even worth it?
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -916,13 +990,14 @@ namespace Robust.Client.GameStates
|
||||
&& !deleteClientEntities // don't add duplicates
|
||||
&& _entities.IsClientSide(child.Value))
|
||||
{
|
||||
toDelete.Add(child.Value);
|
||||
_toDelete.Add(child.Value);
|
||||
}
|
||||
}
|
||||
toDelete.Add(ent);
|
||||
|
||||
_toDelete.Add(ent);
|
||||
}
|
||||
|
||||
foreach (var ent in toDelete)
|
||||
foreach (var ent in _toDelete)
|
||||
{
|
||||
_entities.DeleteEntity(ent);
|
||||
}
|
||||
@@ -931,7 +1006,6 @@ namespace Robust.Client.GameStates
|
||||
private void ProcessDeletions(
|
||||
ReadOnlySpan<NetEntity> delSpan,
|
||||
EntityQuery<TransformComponent> xforms,
|
||||
EntityQuery<MetaDataComponent> metas,
|
||||
SharedTransformSystem xformSys)
|
||||
{
|
||||
// Processing deletions is non-trivial, because by default deletions will also delete all child entities.
|
||||
@@ -1002,6 +1076,7 @@ namespace Robust.Client.GameStates
|
||||
// things like container insertion and ejection.
|
||||
|
||||
using var _ = _prof.Group("Leave PVS");
|
||||
detached.EnsureCapacity(toDetach.Count);
|
||||
|
||||
foreach (var (tick, ents) in toDetach)
|
||||
{
|
||||
@@ -1024,9 +1099,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
foreach (var netEntity in entities)
|
||||
{
|
||||
var ent = _entityManager.GetEntity(netEntity);
|
||||
|
||||
if (!metas.TryGetComponent(ent, out var meta))
|
||||
if (!_entityManager.TryGetEntityData(netEntity, out var ent, out var meta))
|
||||
continue;
|
||||
|
||||
if (meta.LastStateApplied > maxTick)
|
||||
@@ -1042,10 +1115,10 @@ namespace Robust.Client.GameStates
|
||||
if (lastStateApplied.HasValue)
|
||||
meta.LastStateApplied = lastStateApplied.Value;
|
||||
|
||||
var xform = xforms.GetComponent(ent);
|
||||
var xform = xforms.GetComponent(ent.Value);
|
||||
if (xform.ParentUid.IsValid())
|
||||
{
|
||||
lookupSys.RemoveFromEntityTree(ent, xform);
|
||||
lookupSys.RemoveFromEntityTree(ent.Value, xform);
|
||||
xform.Broadphase = BroadphaseData.Invalid;
|
||||
|
||||
// In some cursed scenarios an entity inside of a container can leave PVS without the container itself leaving PVS.
|
||||
@@ -1054,13 +1127,13 @@ namespace Robust.Client.GameStates
|
||||
if ((meta.Flags & MetaDataFlags.InContainer) != 0 &&
|
||||
metas.TryGetComponent(xform.ParentUid, out var containerMeta) &&
|
||||
(containerMeta.Flags & MetaDataFlags.Detached) == 0 &&
|
||||
containerSys.TryGetContainingContainer(xform.ParentUid, ent, out container, null, true))
|
||||
containerSys.TryGetContainingContainer(xform.ParentUid, ent.Value, out container, null, true))
|
||||
{
|
||||
container.Remove(ent, _entities, xform, meta, false, true);
|
||||
container.Remove(ent.Value, _entities, xform, meta, false, true);
|
||||
}
|
||||
|
||||
meta._flags |= MetaDataFlags.Detached;
|
||||
xformSys.DetachParentToNull(ent, xform);
|
||||
xformSys.DetachParentToNull(ent.Value, xform);
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0);
|
||||
|
||||
if (container != null)
|
||||
@@ -1140,14 +1213,15 @@ namespace Robust.Client.GameStates
|
||||
// First remove any deleted components
|
||||
if (curState?.NetComponents != null)
|
||||
{
|
||||
RemQueue<Component> toRemove = new();
|
||||
_toRemove.Clear();
|
||||
|
||||
foreach (var (id, comp) in meta.NetComponents)
|
||||
{
|
||||
if (comp.NetSyncEnabled && !curState.NetComponents.Contains(id))
|
||||
toRemove.Add(comp);
|
||||
_toRemove.Add(comp);
|
||||
}
|
||||
|
||||
foreach (var comp in toRemove)
|
||||
foreach (var comp in _toRemove)
|
||||
{
|
||||
_entities.RemoveComponent(uid, comp, meta);
|
||||
}
|
||||
@@ -1164,8 +1238,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
if (!meta.NetComponents.TryGetValue(id, out var comp))
|
||||
{
|
||||
comp = (Component) _compFactory.GetComponent(id);
|
||||
comp.Owner = uid;
|
||||
comp = _compFactory.GetComponent(id);
|
||||
_entityManager.AddComponent(uid, comp, true, metadata: meta);
|
||||
}
|
||||
|
||||
@@ -1178,8 +1251,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
if (!meta.NetComponents.TryGetValue(compChange.NetID, out var comp))
|
||||
{
|
||||
comp = (Component) _compFactory.GetComponent(compChange.NetID);
|
||||
comp.Owner = uid;
|
||||
comp = _compFactory.GetComponent(compChange.NetID);
|
||||
_entityManager.AddComponent(uid, comp, true, metadata:meta);
|
||||
}
|
||||
else if (compChange.LastModifiedTick <= lastApplied && lastApplied != GameTick.Zero)
|
||||
@@ -1249,7 +1321,9 @@ 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()}");
|
||||
@@ -1405,8 +1479,7 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
if (!meta.NetComponents.TryGetValue(id, out var comp))
|
||||
{
|
||||
comp = (Component) _compFactory.GetComponent(id);
|
||||
comp.Owner = uid;
|
||||
comp = _compFactory.GetComponent(id);
|
||||
_entityManager.AddComponent(uid, comp, true, meta);
|
||||
}
|
||||
|
||||
@@ -1415,24 +1488,23 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
|
||||
// ensure we don't have any extra components
|
||||
RemQueue<Component> toRemove = new();
|
||||
_toRemove.Clear();
|
||||
|
||||
foreach (var (id, comp) in meta.NetComponents)
|
||||
{
|
||||
if (comp.NetSyncEnabled && !lastState.ContainsKey(id))
|
||||
toRemove.Add(comp);
|
||||
_toRemove.Add(comp);
|
||||
}
|
||||
|
||||
foreach (var comp in toRemove)
|
||||
foreach (var comp in _toRemove)
|
||||
{
|
||||
_entities.RemoveComponent(uid, comp);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_sawmill = _logMan.GetSawmill(CVars.NetPredict.Name);
|
||||
}
|
||||
public bool IsQueuedForDetach(NetEntity entity)
|
||||
=> _processor.IsQueuedForDetach(entity);
|
||||
}
|
||||
|
||||
public sealed class GameStateAppliedArgs : EventArgs
|
||||
|
||||
@@ -1,46 +1,33 @@
|
||||
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, IPostInjectInit
|
||||
internal sealed class GameStateProcessor : IGameStateProcessor
|
||||
{
|
||||
[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? LastFullStateRequested
|
||||
{
|
||||
get => _lastFullStateRequested;
|
||||
set
|
||||
{
|
||||
_lastFullStateRequested = value;
|
||||
LastFullState = null;
|
||||
}
|
||||
}
|
||||
|
||||
public GameTick? _lastFullStateRequested = GameTick.Zero;
|
||||
public (GameTick Tick, DateTime Time)? LastFullStateRequested { get; private set; } = (GameTick.Zero, DateTime.MaxValue);
|
||||
|
||||
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.
|
||||
@@ -61,7 +48,14 @@ namespace Robust.Client.GameStates
|
||||
public int BufferSize
|
||||
{
|
||||
get => _bufferSize;
|
||||
set => _bufferSize = value < 0 ? 0 : value;
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -71,9 +65,12 @@ namespace Robust.Client.GameStates
|
||||
/// Constructs a new instance of <see cref="GameStateProcessor"/>.
|
||||
/// </summary>
|
||||
/// <param name="timing">Timing information of the current state.</param>
|
||||
public GameStateProcessor(IClientGameTiming timing)
|
||||
/// <param name="clientGameStateManager"></param>
|
||||
public GameStateProcessor(IClientGameStateManager state, IClientGameTiming timing, ISawmill logger)
|
||||
{
|
||||
_timing = timing;
|
||||
_state = state;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -83,7 +80,7 @@ namespace Robust.Client.GameStates
|
||||
if (state.ToSequence <= _timing.LastRealTick)
|
||||
{
|
||||
if (Logging)
|
||||
_stateLogger.Debug($"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
_logger.Debug($"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -95,7 +92,7 @@ namespace Robust.Client.GameStates
|
||||
continue;
|
||||
|
||||
if (Logging)
|
||||
_stateLogger.Debug($"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
_logger.Debug($"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -104,34 +101,68 @@ namespace Robust.Client.GameStates
|
||||
if (!WaitingForFull)
|
||||
{
|
||||
// This is a good state that we will be using.
|
||||
_stateBuffer.Add(state);
|
||||
TryAdd(state);
|
||||
if (Logging)
|
||||
_stateLogger.Debug($"Received New GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
|
||||
_logger.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 && state.ToSequence >= LastFullStateRequested!.Value)
|
||||
if (LastFullState == null && state.FromSequence == GameTick.Zero)
|
||||
{
|
||||
LastFullState = state;
|
||||
|
||||
if (Logging)
|
||||
if (state.ToSequence >= LastFullStateRequested!.Value.Tick)
|
||||
{
|
||||
LastFullState = state;
|
||||
_logger.Info($"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
_logger.Info($"Received a late full game state. Received: {state.ToSequence}. Requested: {LastFullStateRequested.Value.Tick}");
|
||||
}
|
||||
|
||||
if (LastFullState != null && state.ToSequence <= 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}");
|
||||
|
||||
_logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
|
||||
return false;
|
||||
}
|
||||
|
||||
_stateBuffer.Add(state);
|
||||
TryAdd(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>
|
||||
@@ -152,7 +183,7 @@ namespace Robust.Client.GameStates
|
||||
"Tried to apply a non-extrapolated state that has too high of a FromSequence!");
|
||||
|
||||
if (Logging)
|
||||
_stateLogger.Debug($"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
|
||||
_logger.Debug($"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
|
||||
}
|
||||
|
||||
return applyNextState;
|
||||
@@ -344,14 +375,20 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
_stateBuffer.Clear();
|
||||
LastFullState = null;
|
||||
LastFullStateRequested = GameTick.Zero;
|
||||
LastFullStateRequested = (GameTick.Zero, DateTime.MaxValue);
|
||||
}
|
||||
|
||||
public void RequestFullState()
|
||||
public void OnFullStateRequested(GameTick tick)
|
||||
{
|
||||
_stateBuffer.Clear();
|
||||
LastFullState = null;
|
||||
LastFullStateRequested = _timing.LastRealTick;
|
||||
LastFullStateRequested = (tick, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
public void OnFullStateReceived()
|
||||
{
|
||||
LastFullState = null;
|
||||
LastFullStateRequested = null;
|
||||
}
|
||||
|
||||
public void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, ComponentState>> implicitData)
|
||||
@@ -404,10 +441,23 @@ namespace Robust.Client.GameStates
|
||||
return _lastStateFullRep.TryGetValue(entity, out dictionary);
|
||||
}
|
||||
|
||||
public int CalculateBufferSize(GameTick fromTick)
|
||||
public bool IsQueuedForDetach(NetEntity entity)
|
||||
{
|
||||
// This isn't fast, but its just meant for use in tests & debug asserts.
|
||||
foreach (var msg in _pvsDetachMessages.Values)
|
||||
{
|
||||
if (msg.Contains(entity))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public int GetApplicableStateCount(GameTick? fromTick = null)
|
||||
{
|
||||
fromTick ??= _timing.LastRealTick;
|
||||
bool foundState;
|
||||
var nextTick = fromTick;
|
||||
var nextTick = fromTick.Value;
|
||||
|
||||
do
|
||||
{
|
||||
@@ -425,13 +475,9 @@ namespace Robust.Client.GameStates
|
||||
}
|
||||
while (foundState);
|
||||
|
||||
return (int) (nextTick.Value - fromTick.Value);
|
||||
return (int) (nextTick.Value - fromTick.Value.Value);
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_logger = _logMan.GetSawmill("net");
|
||||
_stateLogger = _logMan.GetSawmill("net.state");
|
||||
}
|
||||
public int StateCount => _stateBuffer.Count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,15 @@ namespace Robust.Client.GameStates
|
||||
/// <summary>
|
||||
/// Number of applicable game states currently in the state buffer.
|
||||
/// </summary>
|
||||
int CurrentBufferSize { get; }
|
||||
int GetApplicableStateCount();
|
||||
|
||||
[Obsolete("use GetApplicableStateCount()")]
|
||||
int CurrentBufferSize => GetApplicableStateCount();
|
||||
|
||||
/// <summary>
|
||||
/// Total number of game states currently in the state buffer.
|
||||
/// </summary>
|
||||
int StateCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If the buffer size is this many states larger than the target buffer size,
|
||||
@@ -91,7 +99,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);
|
||||
void RequestFullState(NetEntity? missingEntity = null, GameTick? tick = null);
|
||||
|
||||
uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs;
|
||||
|
||||
|
||||
@@ -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 CalculateBufferSize(GameTick fromTick);
|
||||
int GetApplicableStateCount(GameTick? fromTick);
|
||||
|
||||
bool TryGetLastServerStates(NetEntity entity,
|
||||
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary);
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace Robust.Client.GameStates
|
||||
var lag = _netManager.ServerChannel!.Ping;
|
||||
|
||||
// calc interp info
|
||||
var buffer = _gameStateManager.CurrentBufferSize;
|
||||
var buffer = _gameStateManager.GetApplicableStateCount();
|
||||
|
||||
_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.CurrentBufferSize.ToString()} states");
|
||||
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.GetApplicableStateCount().ToString()} states");
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
using System;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.GameStates
|
||||
{
|
||||
internal sealed class NetInterpOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
@@ -24,6 +24,11 @@ namespace Robust.Client.GameStates
|
||||
private readonly SharedContainerSystem _container;
|
||||
private readonly SharedTransformSystem _xform;
|
||||
|
||||
/// <summary>
|
||||
/// When an entity stops lerping the overlay will continue to draw a box around the entity for this amount of time.
|
||||
/// </summary>
|
||||
public static readonly TimeSpan Delay = TimeSpan.FromSeconds(2f);
|
||||
|
||||
public NetInterpOverlay(EntityLookupSystem lookup)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
@@ -40,8 +45,8 @@ namespace Robust.Client.GameStates
|
||||
var worldHandle = (DrawingHandleWorld) handle;
|
||||
var viewport = args.WorldAABB;
|
||||
|
||||
var query = _entityManager.AllEntityQueryEnumerator<PhysicsComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var uid, out var physics, out var transform))
|
||||
var query = _entityManager.AllEntityQueryEnumerator<TransformComponent>();
|
||||
while (query.MoveNext(out var uid, out var transform))
|
||||
{
|
||||
// if not on the same map, continue
|
||||
if (transform.MapID != _eyeManager.CurrentMap || _container.IsEntityInContainer(uid))
|
||||
@@ -50,8 +55,8 @@ namespace Robust.Client.GameStates
|
||||
if (transform.GridUid == uid)
|
||||
continue;
|
||||
|
||||
// This entity isn't lerping, no need to draw debug info for it
|
||||
if(transform.NextPosition == null)
|
||||
var delta = (_timing.CurTick.Value - transform.LastLerp.Value) * _timing.TickPeriod;
|
||||
if(!transform.ActivelyLerping && delta > Delay)
|
||||
continue;
|
||||
|
||||
var aabb = _lookup.GetWorldAABB(uid);
|
||||
@@ -61,7 +66,9 @@ namespace Robust.Client.GameStates
|
||||
continue;
|
||||
|
||||
var (pos, rot) = _xform.GetWorldPositionRotation(transform, _entityManager.GetEntityQuery<TransformComponent>());
|
||||
var boxOffset = transform.NextPosition.Value - transform.LocalPosition;
|
||||
var boxOffset = transform.NextPosition != null
|
||||
? transform.NextPosition.Value - transform.LocalPosition
|
||||
: default;
|
||||
var worldOffset = (rot - transform.LocalRotation).RotateVec(boxOffset);
|
||||
|
||||
var nextPos = pos + worldOffset;
|
||||
|
||||
@@ -4,7 +4,6 @@ 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;
|
||||
@@ -41,22 +40,28 @@ namespace Robust.Client.Graphics.Clyde
|
||||
gridProgram.SetUniformTextureMaybe(UniILightTexture, TextureUnit.Texture1);
|
||||
gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1));
|
||||
|
||||
foreach (var mapGrid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
|
||||
var grids = new List<Entity<MapGridComponent>>();
|
||||
_mapManager.FindGridsIntersecting(mapId, worldBounds, ref grids);
|
||||
foreach (var mapGrid in grids)
|
||||
{
|
||||
if (!_mapChunkData.ContainsKey(mapGrid.Owner))
|
||||
if (!_mapChunkData.ContainsKey(mapGrid))
|
||||
continue;
|
||||
|
||||
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid.Owner);
|
||||
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid);
|
||||
gridProgram.SetUniform(UniIModelMatrix, transform.WorldMatrix);
|
||||
var enumerator = mapGrid.GetMapChunks(worldBounds);
|
||||
var enumerator = mapGrid.Comp.GetMapChunks(worldBounds);
|
||||
var data = _mapChunkData[mapGrid];
|
||||
|
||||
while (enumerator.MoveNext(out var chunk))
|
||||
{
|
||||
if (_isChunkDirty(mapGrid, chunk))
|
||||
_updateChunkMesh(mapGrid, chunk);
|
||||
DebugTools.Assert(chunk.FilledTiles > 0);
|
||||
if (!data.TryGetValue(chunk.Indices, out MapChunkData? datum))
|
||||
data[chunk.Indices] = datum = _initChunkBuffers(mapGrid, chunk);
|
||||
|
||||
var datum = _mapChunkData[mapGrid.Owner][chunk.Indices];
|
||||
if (datum.Dirty)
|
||||
_updateChunkMesh(mapGrid, chunk, datum);
|
||||
|
||||
DebugTools.Assert(datum.TileCount > 0);
|
||||
if (datum.TileCount == 0)
|
||||
continue;
|
||||
|
||||
@@ -68,22 +73,36 @@ namespace Robust.Client.Graphics.Clyde
|
||||
CheckGlError();
|
||||
}
|
||||
}
|
||||
|
||||
CullEmptyChunks();
|
||||
}
|
||||
|
||||
private void _updateChunkMesh(MapGridComponent grid, MapChunk chunk)
|
||||
private void CullEmptyChunks()
|
||||
{
|
||||
var data = _mapChunkData[grid.Owner];
|
||||
|
||||
if (!data.TryGetValue(chunk.Indices, out var datum))
|
||||
foreach (var (grid, chunks) in _mapChunkData)
|
||||
{
|
||||
datum = _initChunkBuffers(grid, chunk);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.ChunkSize;
|
||||
var cSz = grid.Comp.ChunkSize;
|
||||
var cScaled = chunk.Indices * cSz;
|
||||
for (ushort x = 0; x < cSz; x++)
|
||||
{
|
||||
@@ -130,7 +149,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
datum.TileCount = i;
|
||||
}
|
||||
|
||||
private unsafe MapChunkData _initChunkBuffers(MapGridComponent grid, MapChunk chunk)
|
||||
private unsafe MapChunkData _initChunkBuffers(Entity<MapGridComponent> grid, MapChunk chunk)
|
||||
{
|
||||
var vao = GenVertexArray();
|
||||
BindVertexArray(vao);
|
||||
@@ -158,41 +177,22 @@ namespace Robust.Client.Graphics.Clyde
|
||||
Dirty = true
|
||||
};
|
||||
|
||||
_mapChunkData[grid.Owner].Add(chunk.Indices, datum);
|
||||
return datum;
|
||||
}
|
||||
|
||||
private bool _isChunkDirty(MapGridComponent grid, MapChunk chunk)
|
||||
private void DeleteChunk(MapChunkData data)
|
||||
{
|
||||
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);
|
||||
}
|
||||
DeleteVertexArray(data.VAO);
|
||||
CheckGlError();
|
||||
data.VBO.Delete();
|
||||
data.EBO.Delete();
|
||||
}
|
||||
|
||||
private void _updateTileMapOnUpdate(ref TileChangedEvent args)
|
||||
{
|
||||
var grid = _mapManager.GetGrid(args.NewTile.GridUid);
|
||||
var chunk = grid.GridTileToChunkIndices(new Vector2i(args.NewTile.X, args.NewTile.Y));
|
||||
_setChunkDirty(grid, chunk);
|
||||
var gridData = _mapChunkData.GetOrNew(args.Entity);
|
||||
if (gridData.TryGetValue(args.ChunkIndex, out var data))
|
||||
data.Dirty = true;
|
||||
}
|
||||
|
||||
private void _updateOnGridCreated(GridStartupEvent ev)
|
||||
@@ -208,10 +208,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var data = _mapChunkData[gridId];
|
||||
foreach (var chunkDatum in data.Values)
|
||||
{
|
||||
DeleteVertexArray(chunkDatum.VAO);
|
||||
CheckGlError();
|
||||
chunkDatum.VBO.Delete();
|
||||
chunkDatum.EBO.Delete();
|
||||
DeleteChunk(chunkDatum);
|
||||
}
|
||||
|
||||
_mapChunkData.Remove(gridId);
|
||||
|
||||
@@ -350,7 +350,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize));
|
||||
|
||||
if (entry.Sprite.RaiseShaderEvent)
|
||||
_entityManager.EventBus.RaiseLocalEvent(entry.Sprite.Owner,
|
||||
_entityManager.EventBus.RaiseLocalEvent(entry.Uid,
|
||||
new BeforePostShaderRenderEvent(entry.Sprite, viewport), false);
|
||||
}
|
||||
}
|
||||
@@ -512,7 +512,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldAABB, worldBounds);
|
||||
}
|
||||
|
||||
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
|
||||
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawLight && eye.DrawFov)
|
||||
{
|
||||
ApplyFovToBuffer(viewport, eye);
|
||||
}
|
||||
|
||||
@@ -332,16 +332,18 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 worldAABB, IEye eye)
|
||||
{
|
||||
if (!_lightManager.Enabled)
|
||||
if (!_lightManager.Enabled || !eye.DrawLight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mapId = eye.Position.MapId;
|
||||
if (mapId == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
// If this map has lighting disabled, return
|
||||
var mapUid = _mapManager.GetMapEntityId(mapId);
|
||||
if (!_entityManager.GetComponent<MapComponent>(mapUid).LightingEnabled)
|
||||
if (!_entityManager.TryGetComponent<MapComponent>(mapUid, out var map) || !map.LightingEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
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;
|
||||
@@ -14,7 +6,15 @@ 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.Sprite.Owner.CompareTo(b.Sprite.Owner);
|
||||
return a.Uid.CompareTo(b.Uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,9 @@ 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
|
||||
@@ -175,7 +173,6 @@ 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()
|
||||
@@ -183,7 +180,6 @@ 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)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
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;
|
||||
|
||||
@@ -22,6 +23,8 @@ 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;
|
||||
@@ -36,16 +39,18 @@ public sealed class TileEdgeOverlay : Overlay
|
||||
if (args.MapId == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
foreach (var grid in _grids)
|
||||
{
|
||||
var tileSize = grid.TileSize;
|
||||
var tileSize = grid.Comp.TileSize;
|
||||
var tileDimensions = new Vector2(tileSize, tileSize);
|
||||
var xform = xformQuery.GetComponent(grid.Owner);
|
||||
var xform = xformQuery.GetComponent(grid);
|
||||
args.WorldHandle.SetTransform(xform.WorldMatrix);
|
||||
|
||||
foreach (var tileRef in grid.GetTilesIntersecting(args.WorldBounds, false))
|
||||
foreach (var tileRef in grid.Comp.GetTilesIntersecting(args.WorldBounds, false))
|
||||
{
|
||||
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
|
||||
|
||||
@@ -61,7 +66,7 @@ public sealed class TileEdgeOverlay : Overlay
|
||||
continue;
|
||||
|
||||
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
|
||||
var neighborTile = grid.GetTileRef(neighborIndices);
|
||||
var neighborTile = grid.Comp.GetTileRef(neighborIndices);
|
||||
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
|
||||
|
||||
// If it's the same tile then no edge to be drawn.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
@@ -8,6 +7,7 @@ using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Dynamics.Contacts;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Physics;
|
||||
@@ -20,8 +20,8 @@ public sealed partial class PhysicsSystem
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<PlayerAttachedEvent>(OnAttach);
|
||||
SubscribeLocalEvent<PlayerDetachedEvent>(OnDetach);
|
||||
SubscribeLocalEvent<LocalPlayerAttachedEvent>(OnAttach);
|
||||
SubscribeLocalEvent<LocalPlayerDetachedEvent>(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(PlayerAttachedEvent ev)
|
||||
private void OnAttach(LocalPlayerAttachedEvent ev)
|
||||
{
|
||||
UpdateIsPredicted(ev.Entity);
|
||||
}
|
||||
|
||||
private void OnDetach(PlayerDetachedEvent ev)
|
||||
private void OnDetach(LocalPlayerDetachedEvent ev)
|
||||
{
|
||||
UpdateIsPredicted(ev.Entity);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Physics;
|
||||
@@ -26,7 +25,7 @@ namespace Robust.Client.Physics
|
||||
|
||||
protected override void Cleanup(PhysicsMapComponent component, float frameTime)
|
||||
{
|
||||
var toRemove = new List<PhysicsComponent>();
|
||||
var toRemove = new List<Entity<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?)
|
||||
@@ -38,13 +37,13 @@ namespace Robust.Client.Physics
|
||||
body.SleepTime += frameTime;
|
||||
if (body.SleepTime > TimeToSleep)
|
||||
{
|
||||
toRemove.Add(body);
|
||||
toRemove.Add(new Entity<PhysicsComponent>(body.Owner, body));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var body in toRemove)
|
||||
{
|
||||
SetAwake(body.Owner, body, false);
|
||||
SetAwake(body, false);
|
||||
}
|
||||
|
||||
base.Cleanup(component, frameTime);
|
||||
@@ -82,11 +81,8 @@ namespace Robust.Client.Physics
|
||||
continue;
|
||||
}
|
||||
|
||||
xform.PrevPosition = position;
|
||||
xform.PrevRotation = rotation;
|
||||
xform.LerpParent = parentUid;
|
||||
xform.NextPosition = xform.LocalPosition;
|
||||
xform.NextRotation = xform.LocalRotation;
|
||||
// Transform system will handle lerping.
|
||||
_transform.SetLocalPositionRotation(uid, xform.LocalPosition, xform.LocalRotation, xform);
|
||||
}
|
||||
|
||||
component.LerpData.Clear();
|
||||
|
||||
@@ -8,7 +8,6 @@ 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;
|
||||
@@ -287,23 +286,17 @@ namespace Robust.Client.Placement
|
||||
}, outsidePrediction: true))
|
||||
.Register<PlacementManager>();
|
||||
|
||||
var localPlayer = PlayerManager.LocalPlayer;
|
||||
localPlayer!.EntityAttached += OnEntityAttached;
|
||||
PlayerManager.LocalPlayerDetached += OnDetached;
|
||||
}
|
||||
|
||||
private void TearDownInput()
|
||||
{
|
||||
CommandBinds.Unregister<PlacementManager>();
|
||||
|
||||
if (PlayerManager.LocalPlayer != null)
|
||||
{
|
||||
PlayerManager.LocalPlayer.EntityAttached -= OnEntityAttached;
|
||||
}
|
||||
PlayerManager.LocalPlayerDetached -= OnDetached;
|
||||
}
|
||||
|
||||
private void OnEntityAttached(EntityAttachedEventArgs eventArgs)
|
||||
private void OnDetached(EntityUid obj)
|
||||
{
|
||||
// player attached to a new entity, basically disable the editor
|
||||
Clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Client.Player
|
||||
namespace Robust.Client.Player;
|
||||
|
||||
public interface IPlayerManager : ISharedPlayerManager
|
||||
{
|
||||
public interface IPlayerManager : ISharedPlayerManager
|
||||
{
|
||||
new IEnumerable<ICommonSession> Sessions { get; }
|
||||
/// <summary>
|
||||
/// Invoked when the list of sessions/players gets updated.
|
||||
/// </summary>
|
||||
event Action? PlayerListUpdated;
|
||||
|
||||
[ViewVariables]
|
||||
IReadOnlyDictionary<NetUserId, ICommonSession> SessionsDict { get; }
|
||||
/// <summary>
|
||||
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets attached to a new entity, or when the local
|
||||
/// session gets updated. See also <see cref="LocalPlayerAttachedEvent"/>
|
||||
/// </summary>
|
||||
event Action<EntityUid>? LocalPlayerAttached;
|
||||
|
||||
[ViewVariables]
|
||||
LocalPlayer? LocalPlayer { get; }
|
||||
/// <summary>
|
||||
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets detached from an entity, or when the local
|
||||
/// session gets updated. See also <see cref="LocalPlayerDetachedEvent"/>
|
||||
/// </summary>
|
||||
event Action<EntityUid>? LocalPlayerDetached;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked after LocalPlayer is changed
|
||||
/// </summary>
|
||||
event Action<LocalPlayerChangedEventArgs>? LocalPlayerChanged;
|
||||
/// <summary>
|
||||
/// Invoked whenever <see cref="ISharedPlayerManager.LocalSession"/> changes.
|
||||
/// </summary>
|
||||
event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged;
|
||||
|
||||
event EventHandler PlayerListUpdated;
|
||||
void ApplyPlayerStates(IReadOnlyCollection<SessionState> list);
|
||||
|
||||
void Initialize();
|
||||
void Startup();
|
||||
void Shutdown();
|
||||
/// <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);
|
||||
|
||||
void ApplyPlayerStates(IReadOnlyCollection<PlayerState> list);
|
||||
}
|
||||
/// <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);
|
||||
|
||||
public sealed class LocalPlayerChangedEventArgs : EventArgs
|
||||
{
|
||||
public readonly LocalPlayer? OldPlayer;
|
||||
public readonly LocalPlayer? NewPlayer;
|
||||
public LocalPlayerChangedEventArgs(LocalPlayer? oldPlayer, LocalPlayer? newPlayer)
|
||||
{
|
||||
OldPlayer = oldPlayer;
|
||||
NewPlayer = newPlayer;
|
||||
}
|
||||
}
|
||||
void SetLocalSession(ICommonSession session);
|
||||
|
||||
[Obsolete("Use LocalSession instead")]
|
||||
LocalPlayer? LocalPlayer { get;}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
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.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.Player
|
||||
@@ -15,163 +10,30 @@ namespace Robust.Client.Player
|
||||
/// </summary>
|
||||
public sealed class LocalPlayer
|
||||
{
|
||||
/// <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;
|
||||
public LocalPlayer(ICommonSession session)
|
||||
{
|
||||
Session = session;
|
||||
}
|
||||
|
||||
/// <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 { get; private set; }
|
||||
|
||||
[ViewVariables] public NetUserId UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Session of the local client.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public ICommonSession Session => InternalSession;
|
||||
public EntityUid? ControlledEntity => Session.AttachedEntity;
|
||||
|
||||
internal PlayerSession InternalSession { get; set; } = default!;
|
||||
[ViewVariables]
|
||||
public NetUserId UserId => Session.UserId;
|
||||
|
||||
/// <summary>
|
||||
/// OOC name of the local player.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string Name { get; set; } = default!;
|
||||
public string Name => Session.Name;
|
||||
|
||||
/// <summary>
|
||||
/// The status of the client's session has changed.
|
||||
/// Session of the local client.
|
||||
/// </summary>
|
||||
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; }
|
||||
public ICommonSession Session;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,13 @@ 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.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.Player
|
||||
{
|
||||
@@ -20,154 +17,212 @@ 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>
|
||||
public sealed class PlayerManager : IPlayerManager
|
||||
internal sealed class PlayerManager : SharedPlayerManager, 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>
|
||||
/// Active sessions of connected clients to the server.
|
||||
/// Received player states that had an unknown <see cref="NetEntity"/>.
|
||||
/// </summary>
|
||||
private readonly Dictionary<NetUserId, ICommonSession> _sessions = new();
|
||||
private Dictionary<NetUserId, SessionState> _pendingStates = new ();
|
||||
private List<SessionState> _pending = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ICommonSession> NetworkedSessions
|
||||
public override ICommonSession[] NetworkedSessions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (LocalPlayer is not null)
|
||||
return new[] {LocalPlayer.Session};
|
||||
|
||||
return Enumerable.Empty<ICommonSession>();
|
||||
return LocalSession != null
|
||||
? new [] { LocalSession }
|
||||
: Array.Empty<ICommonSession>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerable<ICommonSession> ISharedPlayerManager.Sessions => _sessions.Values;
|
||||
public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1;
|
||||
|
||||
public LocalPlayer? LocalPlayer { get; private set; }
|
||||
|
||||
public event Action<SessionStatusEventArgs>? LocalStatusChanged;
|
||||
public event Action? PlayerListUpdated;
|
||||
public event Action<EntityUid>? LocalPlayerDetached;
|
||||
public event Action<EntityUid>? LocalPlayerAttached;
|
||||
public event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int PlayerCount => _sessions.Values.Count;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? 0;
|
||||
|
||||
public ICommonSession? LocalSession => LocalPlayer?.Session;
|
||||
|
||||
/// <inheritdoc />
|
||||
[ViewVariables]
|
||||
public LocalPlayer? LocalPlayer
|
||||
public override void Initialize(int 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");
|
||||
base.Initialize(maxPlayers);
|
||||
_network.RegisterNetMessage<MsgPlayerListReq>();
|
||||
_network.RegisterNetMessage<MsgPlayerList>(HandlePlayerList);
|
||||
PlayerStatusChanged += StatusChanged;
|
||||
}
|
||||
|
||||
private void StatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
if (e.Session == LocalPlayer?.Session)
|
||||
LocalStatusChanged?.Invoke(e);
|
||||
}
|
||||
|
||||
public void SetupSinglePlayer(string name)
|
||||
{
|
||||
if (LocalSession != null)
|
||||
throw new InvalidOperationException($"Player manager already running?");
|
||||
|
||||
var session = CreateAndAddSession(default, name);
|
||||
session.ClientSide = true;
|
||||
SetLocalSession(session);
|
||||
Startup();
|
||||
PlayerListUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public void SetupMultiplayer(INetChannel channel)
|
||||
{
|
||||
if (LocalSession != null)
|
||||
throw new InvalidOperationException($"Player manager already running?");
|
||||
|
||||
SetLocalSession(CreateAndAddSession(channel));
|
||||
Startup();
|
||||
_network.ClientSendMessage(new MsgPlayerListReq());
|
||||
}
|
||||
|
||||
public void SetLocalSession(ICommonSession? session)
|
||||
{
|
||||
if (session == LocalSession)
|
||||
return;
|
||||
|
||||
var old = LocalSession;
|
||||
|
||||
if (old?.AttachedEntity is {} oldUid)
|
||||
{
|
||||
LocalSession = null;
|
||||
LocalPlayer = null;
|
||||
Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(oldUid)}.");
|
||||
EntManager.EventBus.RaiseLocalEvent(oldUid, new LocalPlayerDetachedEvent(oldUid), true);
|
||||
LocalPlayerDetached?.Invoke(oldUid);
|
||||
}
|
||||
|
||||
LocalSession = session;
|
||||
LocalPlayer = session == null ? null : new LocalPlayer(session);
|
||||
Sawmill.Info($"Changing local session from {old?.ToString() ?? "null"} to {session?.ToString() ?? "null"}.");
|
||||
LocalSessionChanged?.Invoke((old, LocalSession));
|
||||
|
||||
if (session?.AttachedEntity is {} newUid)
|
||||
{
|
||||
Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(newUid)}.");
|
||||
EntManager.EventBus.RaiseLocalEvent(newUid, new LocalPlayerAttachedEvent(newUid), true);
|
||||
LocalPlayerAttached?.Invoke(newUid);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Startup()
|
||||
public override void Shutdown()
|
||||
{
|
||||
DebugTools.Assert(LocalPlayer == null);
|
||||
LocalPlayer = new LocalPlayer();
|
||||
|
||||
var msgList = new MsgPlayerListReq();
|
||||
// message is empty
|
||||
_network.ClientSendMessage(msgList);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Shutdown()
|
||||
{
|
||||
LocalPlayer?.DetachEntity();
|
||||
SetAttachedEntity(LocalSession, null, out _);
|
||||
LocalPlayer = null;
|
||||
_sessions.Clear();
|
||||
LocalSession = null;
|
||||
_pendingStates.Clear();
|
||||
base.Shutdown();
|
||||
PlayerListUpdated?.Invoke();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyPlayerStates(IReadOnlyCollection<PlayerState> list)
|
||||
public override bool SetAttachedEntity(ICommonSession? session, EntityUid? uid, out ICommonSession? kicked, bool force = false)
|
||||
{
|
||||
kicked = null;
|
||||
if (session == null)
|
||||
return false;
|
||||
|
||||
if (session.AttachedEntity == uid)
|
||||
return true;
|
||||
|
||||
var old = session.AttachedEntity;
|
||||
if (!base.SetAttachedEntity(session, uid, out kicked, force))
|
||||
return false;
|
||||
|
||||
if (session != LocalSession)
|
||||
return true;
|
||||
|
||||
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 true;
|
||||
}
|
||||
|
||||
if (!EntManager.EntityExists(uid))
|
||||
{
|
||||
Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!");
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (list.Count == 0)
|
||||
{
|
||||
// This happens when the server says "nothing changed!"
|
||||
return;
|
||||
}
|
||||
return false;
|
||||
|
||||
DebugTools.Assert(_network.IsConnected || _client.RunLevel == ClientRunLevel.SinglePlayerGame // replays use state application.
|
||||
, "Received player state without being connected?");
|
||||
DebugTools.Assert(LocalPlayer != null, "Call Startup()");
|
||||
DebugTools.Assert(LocalPlayer!.Session != null, "Received player state before Session finished setup.");
|
||||
DebugTools.Assert(LocalSession != null, "Received player state before Session finished setup.");
|
||||
|
||||
var myState = list.FirstOrDefault(s => s.UserId == LocalPlayer.UserId);
|
||||
var state = list.FirstOrDefault(s => s.UserId == LocalSession.UserId);
|
||||
|
||||
if (myState != null)
|
||||
bool dirty = false;
|
||||
if (state != null)
|
||||
{
|
||||
var uid = _entManager.GetEntity(myState.ControlledEntity);
|
||||
if (myState.ControlledEntity is {Valid: true} && !_entManager.EntityExists(uid))
|
||||
dirty = true;
|
||||
if (!EntManager.TryGetEntity(state.ControlledEntity, out var uid)
|
||||
&& state.ControlledEntity is { Valid:true } )
|
||||
{
|
||||
_sawmill.Error($"Received player state for local player with an unknown net entity!");
|
||||
Sawmill.Error($"Received player state for local player with an unknown net entity!");
|
||||
_pendingStates[state.UserId] = state;
|
||||
}
|
||||
else
|
||||
{
|
||||
_pendingStates.Remove(state.UserId);
|
||||
}
|
||||
|
||||
UpdateAttachedEntity(uid);
|
||||
UpdateSessionStatus(myState.Status);
|
||||
SetAttachedEntity(LocalSession, uid, out _, true);
|
||||
SetStatus(LocalSession, state.Status);
|
||||
}
|
||||
|
||||
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);
|
||||
return UpdatePlayerList(list, fullList) || dirty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -175,117 +230,88 @@ namespace Robust.Client.Player
|
||||
/// </summary>
|
||||
private void HandlePlayerList(MsgPlayerList msg)
|
||||
{
|
||||
UpdatePlayerList(msg.Plyrs);
|
||||
ApplyPlayerStates(msg.Plyrs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the server player list to the client one, and updates if needed.
|
||||
/// </summary>
|
||||
private void UpdatePlayerList(IEnumerable<PlayerState> remotePlayers)
|
||||
private bool UpdatePlayerList(IEnumerable<SessionState> remotePlayers, bool fullList)
|
||||
{
|
||||
var dirty = false;
|
||||
|
||||
var hitSet = new List<NetUserId>();
|
||||
|
||||
var users = new List<NetUserId>();
|
||||
foreach (var state in remotePlayers)
|
||||
{
|
||||
hitSet.Add(state.UserId);
|
||||
users.Add(state.UserId);
|
||||
|
||||
if (_sessions.TryGetValue(state.UserId, out var session))
|
||||
if (!EntManager.TryGetEntity(state.ControlledEntity, out var controlled)
|
||||
&& state.ControlledEntity is {Valid: true})
|
||||
{
|
||||
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;
|
||||
_pendingStates[state.UserId] = state;
|
||||
}
|
||||
else
|
||||
{
|
||||
// New, give him a slot.
|
||||
dirty = true;
|
||||
|
||||
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);
|
||||
}
|
||||
_pendingStates.Remove(state.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var existing in _sessions.Keys.ToArray())
|
||||
{
|
||||
// clear slot, player left
|
||||
if (!hitSet.Contains(existing))
|
||||
if (!InternalSessions.TryGetValue(state.UserId, out var session))
|
||||
{
|
||||
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);
|
||||
// This is a new userid, so we create a new session.
|
||||
DebugTools.Assert(state.UserId != LocalPlayer?.UserId);
|
||||
var newSession = (CommonSession) CreateAndAddSession(state.UserId, state.Name);
|
||||
newSession.Ping = state.Ping;
|
||||
SetStatus(newSession, state.Status);
|
||||
SetAttachedEntity(newSession, controlled, out _, true);
|
||||
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;
|
||||
}
|
||||
|
||||
dirty = true;
|
||||
var local = (CommonSession) session;
|
||||
local.Name = state.Name;
|
||||
local.Ping = state.Ping;
|
||||
SetStatus(local, state.Status);
|
||||
SetAttachedEntity(local, controlled, out _, true);
|
||||
}
|
||||
|
||||
// Remove old users. This only works if the provided state is a list of all players
|
||||
if (fullList)
|
||||
{
|
||||
foreach (var oldUser in InternalSessions.Keys.ToArray())
|
||||
{
|
||||
if (users.Contains(oldUser))
|
||||
continue;
|
||||
|
||||
if (InternalSessions[oldUser].ClientSide)
|
||||
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);
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
{
|
||||
PlayerListUpdated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
return dirty;
|
||||
}
|
||||
|
||||
private void OnRunLevelChanged(object? sender, RunLevelChangedEventArgs e)
|
||||
public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
|
||||
{
|
||||
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)
|
||||
if (LocalEntity == uid)
|
||||
{
|
||||
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;
|
||||
session = LocalSession!;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ 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;
|
||||
@@ -101,7 +100,7 @@ public sealed partial class ReplayLoadManager
|
||||
|
||||
await callback(0, states.Count, LoadingState.ProcessingFiles, true);
|
||||
var playerSpan = state0.PlayerStates.Value;
|
||||
Dictionary<NetUserId, PlayerState> playerStates = new(playerSpan.Count);
|
||||
Dictionary<NetUserId, SessionState> playerStates = new(playerSpan.Count);
|
||||
foreach (var player in playerSpan)
|
||||
{
|
||||
playerStates.Add(player.UserId, player);
|
||||
@@ -391,7 +390,7 @@ public sealed partial class ReplayLoadManager
|
||||
return new EntityState(newState.NetEntity, combined, newState.EntityLastModified, newState.NetComponents ?? oldNetComps);
|
||||
}
|
||||
|
||||
private void UpdatePlayerStates(ReadOnlySpan<PlayerState> span, Dictionary<NetUserId, PlayerState> playerStates)
|
||||
private void UpdatePlayerStates(ReadOnlySpan<SessionState> span, Dictionary<NetUserId, SessionState> playerStates)
|
||||
{
|
||||
foreach (var player in span)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Replays;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
@@ -128,6 +129,11 @@ public interface IReplayPlaybackManager
|
||||
/// </summary>
|
||||
event Action? ReplayUnpaused;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked just before a replay applies a game state.
|
||||
/// </summary>
|
||||
event Action<(GameState Current, GameState? Next)>? BeforeApplyState;
|
||||
|
||||
/// <summary>
|
||||
/// If currently replaying a client-side recording, this is the user that recorded the replay.
|
||||
/// Useful for setting default observer spawn positions.
|
||||
@@ -137,5 +143,5 @@ public interface IReplayPlaybackManager
|
||||
/// <summary>
|
||||
/// Fetches the entity that the <see cref="Recorder"/> is currently attached to.
|
||||
/// </summary>
|
||||
public bool TryGetRecorderEntity([NotNullWhen(true)] out EntityUid? uid);
|
||||
bool TryGetRecorderEntity([NotNullWhen(true)] out EntityUid? uid);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ internal sealed partial class ReplayPlaybackManager
|
||||
_gameState.ClearDetachQueue();
|
||||
EnsureDetachedExist(checkpoint);
|
||||
_gameState.DetachImmediate(checkpoint.Detached);
|
||||
BeforeApplyState?.Invoke((checkpoint.State, next));
|
||||
_gameState.ApplyGameState(checkpoint.State, next);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -58,7 +59,13 @@ internal sealed partial class ReplayPlaybackManager
|
||||
|
||||
_timing.LastRealTick = _timing.LastProcessedTick = _timing.CurTick = Replay.CurTick;
|
||||
_gameState.UpdateFullRep(state, cloneDelta: true);
|
||||
_gameState.ApplyGameState(state, Replay.NextState);
|
||||
|
||||
// Clear existing lerps
|
||||
_entMan.EntitySysManager.GetEntitySystem<TransformSystem>().Reset();
|
||||
|
||||
var next = Replay.NextState;
|
||||
BeforeApplyState?.Invoke((state, next));
|
||||
_gameState.ApplyGameState(state, next);
|
||||
ProcessMessages(Replay.CurMessages, skipEffectEvents);
|
||||
|
||||
// TODO REPLAYS block audio
|
||||
|
||||
@@ -42,7 +42,9 @@ internal sealed partial class ReplayPlaybackManager
|
||||
{
|
||||
var state = Replay.CurState;
|
||||
_gameState.UpdateFullRep(state, cloneDelta: true);
|
||||
_gameState.ApplyGameState(state, Replay.NextState);
|
||||
var next = Replay.NextState;
|
||||
BeforeApplyState?.Invoke((state, next));
|
||||
_gameState.ApplyGameState(state, next);
|
||||
DebugTools.Assert(Replay.LastApplied >= state.FromSequence);
|
||||
DebugTools.Assert(Replay.LastApplied + 1 <= state.ToSequence);
|
||||
Replay.LastApplied = state.ToSequence;
|
||||
|
||||
@@ -12,6 +12,7 @@ using Robust.Client.Upload;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
@@ -44,6 +45,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
|
||||
public event Action? ReplayPlaybackStopped;
|
||||
public event Action? ReplayPaused;
|
||||
public event Action? ReplayUnpaused;
|
||||
public event Action<(GameState Current, GameState? Next)>? BeforeApplyState;
|
||||
|
||||
public ReplayData? Replay { get; private set; }
|
||||
public NetUserId? Recorder => Replay?.Recorder;
|
||||
|
||||
@@ -9,7 +9,7 @@ using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
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 PlayerState GetPlayerState(ICommonSession session)
|
||||
private SessionState GetPlayerState(ICommonSession session)
|
||||
{
|
||||
return new PlayerState
|
||||
return new SessionState
|
||||
{
|
||||
UserId = session.UserId,
|
||||
Status = session.Status,
|
||||
|
||||
12
Robust.Client/Spawners/TimedDespawnSystem.cs
Normal file
12
Robust.Client/Spawners/TimedDespawnSystem.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Spawners;
|
||||
|
||||
namespace Robust.Client.Spawners;
|
||||
|
||||
public sealed class TimedDespawnSystem : SharedTimedDespawnSystem
|
||||
{
|
||||
protected override bool CanDelete(EntityUid uid)
|
||||
{
|
||||
return IsClientSide(uid);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public RichTextLabel()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
VerticalAlignment = VAlignment.Center;
|
||||
}
|
||||
|
||||
public void SetMessage(FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null)
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controls
|
||||
@@ -14,7 +15,17 @@ namespace Robust.Client.UserInterface.Controls
|
||||
/// for each enum value to see how the different options work.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public SplitResizeMode ResizeMode { get; set; }
|
||||
public SplitResizeMode ResizeMode
|
||||
{
|
||||
get => _resizeMode;
|
||||
set
|
||||
{
|
||||
_resizeMode = value;
|
||||
_splitDragArea.Visible = value != SplitResizeMode.NotResizable;
|
||||
}
|
||||
}
|
||||
|
||||
private SplitResizeMode _resizeMode = SplitResizeMode.RespectChildrenMinSize;
|
||||
|
||||
/// <summary>
|
||||
/// Width of the split in virtual pixels
|
||||
@@ -30,7 +41,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
}
|
||||
|
||||
private float _splitWidth;
|
||||
private float _splitWidth = 10;
|
||||
|
||||
/// <summary>
|
||||
/// This width determines the minimum size of the draggable area around the split. This has no effect if it
|
||||
@@ -38,11 +49,13 @@ namespace Robust.Client.UserInterface.Controls
|
||||
/// </summary>
|
||||
public float MinDraggableWidth = 10f;
|
||||
|
||||
public float DraggableWidth => MathF.Max(MinDraggableWidth, _splitWidth);
|
||||
|
||||
/// <summary>
|
||||
/// Virtual pixel offset from the edge beyond which the split cannot be moved.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float SplitEdgeSeparation { get; set; }
|
||||
public float SplitEdgeSeparation { get; set; } = 10;
|
||||
|
||||
private float _splitStart;
|
||||
|
||||
@@ -58,6 +71,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_splitStart = value - _splitWidth / 2;
|
||||
ClampSplitCenter();
|
||||
InvalidateMeasure();
|
||||
OnSplitResized?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,13 +95,51 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
}
|
||||
|
||||
private SplitState _splitState;
|
||||
private bool _dragging;
|
||||
private SplitState _splitState = SplitState.Auto;
|
||||
private SplitOrientation _orientation;
|
||||
private SplitStretchDirection _stretchDirection;
|
||||
private SplitStretchDirection _stretchDirection = SplitStretchDirection.BottomRight;
|
||||
private bool _dragging;
|
||||
private float _dragOffset;
|
||||
|
||||
private bool Vertical => Orientation == SplitOrientation.Vertical;
|
||||
|
||||
private readonly SplitDragControl _splitDragArea = new();
|
||||
|
||||
/// <summary>
|
||||
/// The upper/left control in the split container.
|
||||
/// </summary>
|
||||
public Control? First
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ChildCount < 3)
|
||||
return null;
|
||||
|
||||
DebugTools.AssertNotEqual(GetChild(0), _splitDragArea);
|
||||
return GetChild(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The lower/right control in the split container.
|
||||
/// </summary>
|
||||
public Control? Second
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ChildCount < 3)
|
||||
return null;
|
||||
|
||||
DebugTools.AssertNotEqual(GetChild(1), _splitDragArea);
|
||||
return GetChild(1);
|
||||
}
|
||||
}
|
||||
|
||||
public (Control First, Control Second)? Splits => ChildCount < 3 ? null : (GetChild(0), GetChild(1));
|
||||
|
||||
public event Action? OnSplitResizeFinished;
|
||||
public event Action? OnSplitResized;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the split position should be set manually or automatically.
|
||||
/// </summary>
|
||||
@@ -131,73 +183,53 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public SplitContainer()
|
||||
{
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
_splitState = SplitState.Auto;
|
||||
_stretchDirection = SplitStretchDirection.BottomRight;
|
||||
_dragging = false;
|
||||
ResizeMode = SplitResizeMode.RespectChildrenMinSize;
|
||||
SplitWidth = 10;
|
||||
SplitEdgeSeparation = 10;
|
||||
AddChild(_splitDragArea);
|
||||
_splitDragArea.Visible = _resizeMode != SplitResizeMode.NotResizable;
|
||||
_splitDragArea.DefaultCursorShape = Vertical ? CursorShape.VResize : CursorShape.HResize;
|
||||
_splitDragArea.OnMouseUp += StopDragging;
|
||||
_splitDragArea.OnMouseDown += StartDragging;
|
||||
_splitDragArea.OnMouseMove += OnMove;
|
||||
}
|
||||
|
||||
protected internal override void MouseMove(GUIMouseMoveEventArgs args)
|
||||
private void OnMove(GUIMouseMoveEventArgs args)
|
||||
{
|
||||
base.MouseMove(args);
|
||||
if (ResizeMode == SplitResizeMode.NotResizable)
|
||||
return;
|
||||
|
||||
if (ResizeMode == SplitResizeMode.NotResizable) return;
|
||||
if (!_dragging)
|
||||
return;
|
||||
|
||||
if (_dragging)
|
||||
{
|
||||
var newOffset = Vertical ? args.RelativePosition.Y : args.RelativePosition.X;
|
||||
|
||||
SplitCenter = newOffset;
|
||||
DefaultCursorShape = Vertical ? CursorShape.VResize : CursorShape.HResize;
|
||||
}
|
||||
else
|
||||
{
|
||||
// on mouseover, check if they are over the split and change the cursor accordingly
|
||||
var cursor = CursorShape.Arrow;
|
||||
if (CanDragAt(args.RelativePosition))
|
||||
{
|
||||
cursor = Vertical ? CursorShape.VResize : CursorShape.HResize;
|
||||
}
|
||||
|
||||
DefaultCursorShape = cursor;
|
||||
}
|
||||
// Source control might be either the container, or the separator.
|
||||
// So we manually calculate the relative coordinates wrt the container.
|
||||
var relative = args.GlobalPosition - GlobalPosition;
|
||||
SplitCenter = Vertical ? relative.Y - _dragOffset : relative.X + _dragOffset;
|
||||
}
|
||||
|
||||
|
||||
protected internal override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
protected override void ChildAdded(Control newChild)
|
||||
{
|
||||
base.KeyBindDown(args);
|
||||
|
||||
if (ResizeMode == SplitResizeMode.NotResizable) return;
|
||||
|
||||
if (_dragging || args.Function != EngineKeyFunctions.UIClick) return;
|
||||
|
||||
if (CanDragAt(args.RelativePosition))
|
||||
{
|
||||
_dragging = true;
|
||||
_splitState = SplitState.Manual;
|
||||
}
|
||||
base.ChildAdded(newChild);
|
||||
_splitDragArea.SetPositionLast();
|
||||
}
|
||||
|
||||
protected internal override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
public void StartDragging(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindUp(args);
|
||||
if (ResizeMode == SplitResizeMode.NotResizable || _dragging)
|
||||
return;
|
||||
|
||||
if (args.Function != EngineKeyFunctions.UIClick) return;
|
||||
_dragging = true;
|
||||
_dragOffset = DraggableWidth / 2 - (Vertical ? args.RelativePosition.Y : args.RelativePosition.X);
|
||||
_splitState = SplitState.Manual;
|
||||
DefaultCursorShape = Vertical ? CursorShape.VResize : CursorShape.HResize;
|
||||
}
|
||||
|
||||
private void StopDragging(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (!_dragging)
|
||||
return;
|
||||
|
||||
_dragging = false;
|
||||
DefaultCursorShape = CursorShape.Arrow;
|
||||
}
|
||||
|
||||
private bool CanDragAt(Vector2 relativePosition)
|
||||
{
|
||||
var distance = Vertical
|
||||
? Math.Abs(relativePosition.Y - SplitCenter)
|
||||
: Math.Abs(relativePosition.X - SplitCenter);
|
||||
|
||||
return distance <= _splitWidth || distance <= MinDraggableWidth;
|
||||
OnSplitResizeFinished?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -218,7 +250,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
desiredSplit += Vertical ? desiredSize.Value.Y - Size.Y : desiredSize.Value.X - Size.X;
|
||||
_splitStart = MathHelper.Clamp(desiredSplit, SplitEdgeSeparation, splitMax);
|
||||
|
||||
if (ResizeMode == SplitResizeMode.RespectChildrenMinSize && ChildCount == 2)
|
||||
if (ResizeMode == SplitResizeMode.RespectChildrenMinSize && ChildCount == 3)
|
||||
{
|
||||
var first = GetChild(0);
|
||||
var second = GetChild(1);
|
||||
@@ -234,7 +266,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
if (ChildCount != 2)
|
||||
if (ChildCount < 3)
|
||||
{
|
||||
return finalSize;
|
||||
}
|
||||
@@ -287,15 +319,23 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
}
|
||||
|
||||
// location & size of the draggable area may be larger than the split area.
|
||||
var dragCenter = _splitStart + _splitWidth / 2;
|
||||
var dragWidth = DraggableWidth;
|
||||
var dragStart = dragCenter - dragWidth / 2;
|
||||
var dragEnd = dragCenter + dragWidth / 2;
|
||||
|
||||
if (Vertical)
|
||||
{
|
||||
first.Arrange(new UIBox2(0, 0, finalSize.X, _splitStart));
|
||||
second.Arrange(new UIBox2(0, _splitStart + _splitWidth, finalSize.X, finalSize.Y));
|
||||
_splitDragArea.Arrange(new UIBox2(0, dragStart, finalSize.X, dragEnd));
|
||||
}
|
||||
else
|
||||
{
|
||||
first.Arrange(new UIBox2(0, 0, _splitStart, finalSize.Y));
|
||||
second.Arrange(new UIBox2(_splitStart + _splitWidth, 0, finalSize.X, finalSize.Y));
|
||||
_splitDragArea.Arrange(new UIBox2(dragStart, 0, dragEnd, finalSize.Y));
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
@@ -303,7 +343,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
if (ChildCount != 2)
|
||||
if (ChildCount < 3)
|
||||
{
|
||||
return Vector2.Zero;
|
||||
}
|
||||
@@ -420,5 +460,40 @@ namespace Robust.Client.UserInterface.Controls
|
||||
/// </summary>
|
||||
TopLeft,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple control to intercept mous events and redirect them to the parent.
|
||||
/// </summary>
|
||||
private sealed class SplitDragControl : Control
|
||||
{
|
||||
public event Action<GUIBoundKeyEventArgs>? OnMouseDown;
|
||||
public event Action<GUIBoundKeyEventArgs>? OnMouseUp;
|
||||
public event Action<GUIMouseMoveEventArgs>? OnMouseMove;
|
||||
|
||||
public SplitDragControl()
|
||||
{
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
}
|
||||
|
||||
protected internal override void MouseMove(GUIMouseMoveEventArgs args)
|
||||
{
|
||||
base.MouseMove(args);
|
||||
OnMouseMove?.Invoke(args);
|
||||
}
|
||||
|
||||
protected internal override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindDown(args);
|
||||
if (args.Function == EngineKeyFunctions.UIClick)
|
||||
OnMouseDown?.Invoke(args);
|
||||
}
|
||||
|
||||
protected internal override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindUp(args);
|
||||
if (args.Function == EngineKeyFunctions.UIClick)
|
||||
OnMouseUp?.Invoke(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ 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
|
||||
{
|
||||
@@ -18,18 +17,14 @@ namespace Robust.Client.UserInterface.Controls
|
||||
IEntityManager _entMan;
|
||||
|
||||
[ViewVariables]
|
||||
private SpriteComponent? _sprite;
|
||||
public SpriteComponent? Sprite
|
||||
{
|
||||
get => _sprite;
|
||||
[Obsolete("Use SetEntity()")]
|
||||
set => SetEntity(value?.Owner);
|
||||
}
|
||||
public SpriteComponent? Sprite { get; private set; }
|
||||
|
||||
|
||||
[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"/>.
|
||||
@@ -124,14 +119,22 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public SpriteView()
|
||||
{
|
||||
_entMan = IoCManager.Resolve<IEntityManager>();
|
||||
_entMan.TryGetComponent(Entity, out _sprite);
|
||||
if (_entMan.TryGetComponent(Entity, out SpriteComponent? sprite))
|
||||
{
|
||||
Sprite = sprite;
|
||||
}
|
||||
|
||||
RectClipContent = true;
|
||||
}
|
||||
|
||||
public void SetEntity(EntityUid? uid)
|
||||
{
|
||||
Entity = uid;
|
||||
_entMan.TryGetComponent(Entity, out _sprite);
|
||||
|
||||
if (_entMan.TryGetComponent(Entity, out SpriteComponent? sprite))
|
||||
{
|
||||
Sprite = sprite;
|
||||
}
|
||||
}
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
@@ -143,13 +146,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)
|
||||
@@ -191,10 +194,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;
|
||||
@@ -214,11 +217,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
|
||||
return;
|
||||
|
||||
var mapSystem = _entityManager.System<SharedMapSystem>();
|
||||
var xformSystem = _entityManager.System<SharedTransformSystem>();
|
||||
|
||||
if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid))
|
||||
{
|
||||
@@ -80,7 +81,7 @@ namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
|
||||
{
|
||||
mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
|
||||
mouseWorldMap.Position);
|
||||
tile = new TileRef(EntityUid.Invalid, mouseGridPos.ToVector2i(_entityManager, _mapManager), Tile.Empty);
|
||||
tile = new TileRef(EntityUid.Invalid, mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty);
|
||||
}
|
||||
|
||||
var controlHovered = UserInterfaceManager.CurrentlyHovered;
|
||||
@@ -90,35 +91,35 @@ Screen Size: {screenSize} (scale: {screenScale})
|
||||
Mouse Pos:
|
||||
Screen: {mouseScreenPos}
|
||||
{mouseWorldMap}
|
||||
{mouseGridPos}
|
||||
{_entityManager.GetNetCoordinates(mouseGridPos)}
|
||||
{tile}
|
||||
GUI: {controlHovered}");
|
||||
|
||||
_textBuilder.AppendLine("\nAttached Entity:");
|
||||
var controlledEntity = _playerManager?.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
|
||||
_textBuilder.AppendLine("\nAttached NetEntity:");
|
||||
var controlledEntity = _playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid;
|
||||
|
||||
if (controlledEntity == EntityUid.Invalid)
|
||||
{
|
||||
_textBuilder.AppendLine("No attached entity.");
|
||||
_textBuilder.AppendLine("No attached netentity.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var entityTransform = _entityManager.GetComponent<TransformComponent>(controlledEntity);
|
||||
var playerWorldOffset = entityTransform.MapPosition;
|
||||
var playerWorldOffset = xformSystem.GetMapCoordinates(entityTransform);
|
||||
var playerScreen = _eyeManager.WorldToScreen(playerWorldOffset.Position);
|
||||
|
||||
var playerCoordinates = entityTransform.Coordinates;
|
||||
var playerRotation = entityTransform.WorldRotation;
|
||||
var playerRotation = xformSystem.GetWorldRotation(entityTransform);
|
||||
var gridRotation = entityTransform.GridUid != null
|
||||
? _entityManager.GetComponent<TransformComponent>(entityTransform.GridUid.Value)
|
||||
.WorldRotation
|
||||
? xformSystem.GetWorldRotation(entityTransform.GridUid.Value)
|
||||
: Angle.Zero;
|
||||
|
||||
_textBuilder.Append($@" Screen: {playerScreen}
|
||||
{playerWorldOffset}
|
||||
{playerCoordinates}
|
||||
{_entityManager.GetNetCoordinates(playerCoordinates)}
|
||||
Rotation: {playerRotation.Degrees:F2}°
|
||||
EntId: {entityTransform.Owner}
|
||||
GridUid: {entityTransform.GridUid}
|
||||
NEntId: {_entityManager.GetNetEntity(controlledEntity)}
|
||||
Grid NEntId: {_entityManager.GetNetEntity(entityTransform.GridUid)}
|
||||
Grid Rotation: {gridRotation.Degrees:F2}°");
|
||||
}
|
||||
|
||||
|
||||
@@ -69,12 +69,11 @@ public abstract class UIScreen : LayoutContainer
|
||||
|
||||
public void RemoveWidget<T>() where T : UIWidget, new()
|
||||
{
|
||||
if (_widgets.TryGetValue(typeof(T), out var widget))
|
||||
{
|
||||
RemoveChild(widget);
|
||||
}
|
||||
if (!_widgets.Remove(typeof(T), out var widget))
|
||||
return;
|
||||
|
||||
_widgets.Remove(typeof(T));
|
||||
widget.Parent?.RemoveChild(widget);
|
||||
RemoveChildren(widget);
|
||||
}
|
||||
|
||||
internal void OnRemoved()
|
||||
@@ -103,6 +102,14 @@ public abstract class UIScreen : LayoutContainer
|
||||
AddChild(widget);
|
||||
}
|
||||
|
||||
public void AddWidgetDirect(UIWidget widget)
|
||||
{
|
||||
if (!_widgets.TryAdd(widget.GetType(), widget))
|
||||
throw new Exception("Tried to add duplicate widget to screen!");
|
||||
|
||||
RegisterChildren(widget);
|
||||
}
|
||||
|
||||
public T? GetWidget<T>() where T : UIWidget, new()
|
||||
{
|
||||
return (T?) _widgets.GetValueOrDefault(typeof(T));
|
||||
|
||||
@@ -15,7 +15,7 @@ using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
@@ -5,7 +5,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.Map.Components;
|
||||
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 (!mapManager.TryGetGrid(gridVal, out var grid))
|
||||
if (!entityManager.HasComponent<MapGridComponent>(gridVal))
|
||||
{
|
||||
ValueChanged(new EntityCoordinates(EntityUid.Invalid, new(xVal, yVal)));
|
||||
return;
|
||||
}
|
||||
|
||||
ValueChanged(new EntityCoordinates(grid.Owner, new(xVal, yVal)));
|
||||
ValueChanged(new EntityCoordinates(gridVal, new(xVal, yVal)));
|
||||
}
|
||||
|
||||
if (!ReadOnly)
|
||||
|
||||
@@ -120,7 +120,11 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
};
|
||||
top.HorizontalExpand = true;
|
||||
hBox.AddChild(top);
|
||||
hBox.AddChild(new SpriteView {Sprite = sprite, OverrideDirection = Direction.South});
|
||||
|
||||
var view = new SpriteView { OverrideDirection = Direction.South };
|
||||
view.SetEntity(_entity);
|
||||
hBox.AddChild(view);
|
||||
|
||||
vBoxContainer.AddChild(hBox);
|
||||
}
|
||||
else
|
||||
@@ -270,7 +274,7 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
button.OnPressed += _ =>
|
||||
{
|
||||
ViewVariablesManager.OpenVV(
|
||||
new ViewVariablesComponentSelector(_entityManager.GetNetEntity(_entity), componentType.FullName));
|
||||
new ViewVariablesComponentSelector(_netEntity, componentType.FullName));
|
||||
};
|
||||
removeButton.OnPressed += _ =>
|
||||
{
|
||||
@@ -431,8 +435,7 @@ namespace Robust.Client.ViewVariables.Instances
|
||||
|
||||
try
|
||||
{
|
||||
var comp = (Component) componentFactory.GetComponent(registration.Type);
|
||||
comp.Owner = _entity;
|
||||
var comp = componentFactory.GetComponent(registration.Type);
|
||||
_entityManager.AddComponent(_entity, comp);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@@ -61,7 +61,7 @@ internal sealed class AssetPassPackRsis : AssetPass
|
||||
foreach (var (key, dat) in _foundRsis)
|
||||
{
|
||||
if (dat.MetaJson == null)
|
||||
return;
|
||||
continue;
|
||||
|
||||
RunJob(() =>
|
||||
{
|
||||
|
||||
@@ -21,11 +21,11 @@ public sealed class RobustClientAssetGraph
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<AssetPass> AllPasses { get; }
|
||||
|
||||
public RobustClientAssetGraph()
|
||||
public RobustClientAssetGraph(bool parallel = true)
|
||||
{
|
||||
// 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 = true };
|
||||
Input = new AssetPassPipe { Name = "RobustClientAssetGraphInput", Parallelize = parallel };
|
||||
PresetPasses = new AssetPassPipe { Name = "RobustClientAssetGraphPresetPasses" };
|
||||
Output = new AssetPassPipe { Name = "RobustClientAssetGraphOutput", CheckDuplicates = true };
|
||||
NormalizeText = new AssetPassNormalizeText { Name = "RobustClientAssetGraphNormalizeText" };
|
||||
|
||||
@@ -4,9 +4,10 @@ namespace Robust.Packaging;
|
||||
|
||||
public sealed class RobustClientPackaging
|
||||
{
|
||||
public static IReadOnlySet<string> ClientIgnoresResources { get; } = new HashSet<string>
|
||||
public static IReadOnlySet<string> ClientIgnoredResources { get; } = new HashSet<string>
|
||||
{
|
||||
"Maps",
|
||||
"ConfigPresets",
|
||||
// Leaving this here for future archaeologists to ponder at.
|
||||
"emotes.xml",
|
||||
"Groups",
|
||||
@@ -18,48 +19,8 @@ public sealed class RobustClientPackaging
|
||||
AssetPass pass,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
var ignoreSet = ClientIgnoresResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
|
||||
var ignoreSet = ClientIgnoredResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
|
||||
|
||||
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;
|
||||
await RobustSharedPackaging.DoResourceCopy(Path.Combine(contentDir, "Resources"), pass, ignoreSet, cancel: cancel);
|
||||
}
|
||||
}
|
||||
|
||||
33
Robust.Packaging/RobustServerPackaging.cs
Normal file
33
Robust.Packaging/RobustServerPackaging.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,53 @@ 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))
|
||||
@@ -29,7 +72,7 @@ public sealed class RobustSharedPackaging
|
||||
if (ignoreSet.Contains(filename))
|
||||
continue;
|
||||
|
||||
var targetPath = filename;
|
||||
var targetPath = Path.Combine(targetDir, filename);
|
||||
if (Directory.Exists(path))
|
||||
CopyDirIntoZip(path, targetPath, pass);
|
||||
else
|
||||
@@ -44,11 +87,11 @@ public sealed class RobustSharedPackaging
|
||||
foreach (var file in Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relPath = Path.GetRelativePath(directory, file);
|
||||
if (Path.DirectorySeparatorChar != '/')
|
||||
relPath = relPath.Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
var zipPath = $"{basePath}/{relPath}";
|
||||
|
||||
if (Path.DirectorySeparatorChar != '/')
|
||||
zipPath = zipPath.Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
// Console.WriteLine($"{directory}/{zipPath} -> /{zipPath}");
|
||||
pass.InjectFileFromDisk(zipPath, file);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ 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;
|
||||
@@ -306,7 +307,9 @@ namespace Robust.Server
|
||||
_modLoader.SetUseLoadContext(!ContentStart);
|
||||
_modLoader.SetEnableSandboxing(Options.Sandboxing);
|
||||
|
||||
if (!_modLoader.TryLoadModulesFrom(Options.AssemblyDirectory, Options.ContentModulePrefix))
|
||||
var resourceManifest = ResourceManifestData.LoadResourceManifest(_resources);
|
||||
|
||||
if (!_modLoader.TryLoadModulesFrom(Options.AssemblyDirectory, resourceManifest.AssemblyPrefix ?? Options.ContentModulePrefix))
|
||||
{
|
||||
_logger.Fatal("Errors while loading content assemblies.");
|
||||
return true;
|
||||
@@ -657,10 +660,14 @@ namespace Robust.Server
|
||||
{
|
||||
// Write down exception log
|
||||
var logPath = _config.GetCVar(CVars.LogPath);
|
||||
var relPath = PathHelpers.ExecutableRelativeFile(logPath);
|
||||
Directory.CreateDirectory(relPath);
|
||||
var pathToWrite = Path.Combine(relPath,
|
||||
if (!Path.IsPathRooted(logPath))
|
||||
{
|
||||
logPath = PathHelpers.ExecutableRelativeFile(logPath);
|
||||
}
|
||||
|
||||
var pathToWrite = Path.Combine(logPath,
|
||||
"Runtime-" + DateTime.Now.ToString("yyyy-MM-dd-THH-mm-ss") + ".txt");
|
||||
Directory.CreateDirectory(logPath);
|
||||
File.WriteAllText(pathToWrite, _runtimeLog.Display(), EncodingHelpers.UTF8);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -47,11 +46,7 @@ namespace Robust.Server.Console.Commands
|
||||
shell.WriteLine($"Entity {_entityManager.GetComponent<MetaDataComponent>(uid.Value).EntityName} already has a {componentName} component.");
|
||||
}
|
||||
|
||||
var component = (Component) _componentFactory.GetComponent(registration.Type);
|
||||
|
||||
#pragma warning disable CS0618
|
||||
component.Owner = uid.Value;
|
||||
#pragma warning restore CS0618
|
||||
var component = _componentFactory.GetComponent(registration.Type);
|
||||
_entityManager.AddComponent(uid.Value, component);
|
||||
|
||||
shell.WriteLine($"Added {componentName} component to entity {_entityManager.GetComponent<MetaDataComponent>(uid.Value).EntityName}.");
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace Robust.Server.Console.Commands
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var players = _players.ServerSessions;
|
||||
var players = _players.Sessions;
|
||||
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 as IPlayerSession;
|
||||
var toKickPlayer = player ?? _players.ServerSessions.FirstOrDefault();
|
||||
var player = shell.Player;
|
||||
var toKickPlayer = player ?? _players.Sessions.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.ServerSessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
|
||||
var options = _players.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
|
||||
|
||||
return CompletionResult.FromHintOptions(options, "<PlayerIndex>");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Server.Console.Commands
|
||||
{
|
||||
@@ -17,7 +16,7 @@ namespace Robust.Server.Console.Commands
|
||||
{
|
||||
var session = shell.Player;
|
||||
|
||||
if (session is not IPlayerSession playerSession)
|
||||
if (session is not { } playerSession)
|
||||
{
|
||||
shell.WriteError($"Unable to find {nameof(ICommonSession)} for shell");
|
||||
return;
|
||||
@@ -54,7 +53,7 @@ namespace Robust.Server.Console.Commands
|
||||
{
|
||||
var session = shell.Player;
|
||||
|
||||
if (session is not IPlayerSession playerSession)
|
||||
if (session is not { } playerSession)
|
||||
{
|
||||
shell.WriteError($"Unable to find {nameof(ICommonSession)} for shell");
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Toolshed.Errors;
|
||||
|
||||
@@ -10,27 +8,27 @@ namespace Robust.Server.Console
|
||||
{
|
||||
public IConGroupControllerImplementation? Implementation { get; set; }
|
||||
|
||||
public bool CanCommand(IPlayerSession session, string cmdName)
|
||||
public bool CanCommand(ICommonSession session, string cmdName)
|
||||
{
|
||||
return Implementation?.CanCommand(session, cmdName) ?? false;
|
||||
}
|
||||
|
||||
public bool CanAdminPlace(IPlayerSession session)
|
||||
public bool CanAdminPlace(ICommonSession session)
|
||||
{
|
||||
return Implementation?.CanAdminPlace(session) ?? false;
|
||||
}
|
||||
|
||||
public bool CanScript(IPlayerSession session)
|
||||
public bool CanScript(ICommonSession session)
|
||||
{
|
||||
return Implementation?.CanScript(session) ?? false;
|
||||
}
|
||||
|
||||
public bool CanAdminMenu(IPlayerSession session)
|
||||
public bool CanAdminMenu(ICommonSession session)
|
||||
{
|
||||
return Implementation?.CanAdminMenu(session) ?? false;
|
||||
}
|
||||
|
||||
public bool CanAdminReloadPrototypes(IPlayerSession session)
|
||||
public bool CanAdminReloadPrototypes(ICommonSession session)
|
||||
{
|
||||
return Implementation?.CanAdminReloadPrototypes(session) ?? false;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Toolshed;
|
||||
|
||||
namespace Robust.Server.Console
|
||||
{
|
||||
public interface IConGroupControllerImplementation : IPermissionController
|
||||
{
|
||||
bool CanCommand(IPlayerSession session, string cmdName);
|
||||
bool CanAdminPlace(IPlayerSession session);
|
||||
bool CanScript(IPlayerSession session);
|
||||
bool CanAdminMenu(IPlayerSession session);
|
||||
bool CanAdminReloadPrototypes(IPlayerSession session);
|
||||
bool CanCommand(ICommonSession session, string cmdName);
|
||||
bool CanAdminPlace(ICommonSession session);
|
||||
bool CanScript(ICommonSession session);
|
||||
bool CanAdminMenu(ICommonSession session);
|
||||
bool CanAdminReloadPrototypes(ICommonSession session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ 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.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Toolshed.Syntax;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -42,7 +41,7 @@ namespace Robust.Server.Console
|
||||
|
||||
var msg = new MsgConCmd();
|
||||
msg.Text = command;
|
||||
NetManager.ServerSendMessage(msg, ((IPlayerSession)session).ConnectedClient);
|
||||
NetManager.ServerSendMessage(msg, session.Channel);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -50,18 +49,12 @@ namespace Robust.Server.Console
|
||||
{
|
||||
var msg = new FormattedMessage();
|
||||
msg.AddText(text);
|
||||
if (session is IPlayerSession playerSession)
|
||||
OutputText(playerSession, msg, false);
|
||||
else
|
||||
OutputText(null, msg, false);
|
||||
OutputText(session, msg, false);
|
||||
}
|
||||
|
||||
public override void WriteLine(ICommonSession? session, FormattedMessage msg)
|
||||
{
|
||||
if (session is IPlayerSession playerSession)
|
||||
OutputText(playerSession, msg, false);
|
||||
else
|
||||
OutputText(null, msg, false);
|
||||
OutputText(session, msg, false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -69,10 +62,7 @@ namespace Robust.Server.Console
|
||||
{
|
||||
var msg = new FormattedMessage();
|
||||
msg.AddText(text);
|
||||
if (session is IPlayerSession playerSession)
|
||||
OutputText(playerSession, msg, true);
|
||||
else
|
||||
OutputText(null, msg, true);
|
||||
OutputText(session, msg, true);
|
||||
}
|
||||
|
||||
public bool IsCmdServer(IConsoleCommand cmd) => true;
|
||||
@@ -156,7 +146,7 @@ namespace Robust.Server.Console
|
||||
|
||||
private bool ShellCanExecute(IConsoleShell shell, string cmdName)
|
||||
{
|
||||
return shell.Player == null || _groupController.CanCommand((IPlayerSession)shell.Player, cmdName);
|
||||
return shell.Player == null || _groupController.CanCommand(shell.Player, cmdName);
|
||||
}
|
||||
|
||||
private void HandleRegistrationRequest(INetChannel senderConnection)
|
||||
@@ -164,16 +154,40 @@ namespace Robust.Server.Console
|
||||
var message = new MsgConCmdReg();
|
||||
|
||||
var counter = 0;
|
||||
message.Commands = new MsgConCmdReg.Command[AvailableCommands.Count];
|
||||
var toolshedCommands = _toolshed.DefaultEnvironment.AllCommands().ToArray();
|
||||
message.Commands = new List<MsgConCmdReg.Command>(AvailableCommands.Count + toolshedCommands.Length);
|
||||
var commands = new HashSet<string>();
|
||||
|
||||
foreach (var command in AvailableCommands.Values)
|
||||
{
|
||||
message.Commands[counter++] = new MsgConCmdReg.Command
|
||||
if (!commands.Add(command.Command))
|
||||
{
|
||||
Sawmill.Error($"Duplicate command: {command.Command}");
|
||||
continue;
|
||||
}
|
||||
message.Commands.Add(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);
|
||||
@@ -190,7 +204,7 @@ namespace Robust.Server.Console
|
||||
ExecuteCommand(session, text);
|
||||
}
|
||||
|
||||
private void OutputText(IPlayerSession? session, FormattedMessage text, bool error)
|
||||
private void OutputText(ICommonSession? session, FormattedMessage text, bool error)
|
||||
{
|
||||
if (session != null)
|
||||
{
|
||||
|
||||
@@ -196,7 +196,7 @@ namespace Robust.Server.Console
|
||||
break;
|
||||
|
||||
case ConsoleKey.Backspace:
|
||||
if (currentBuffer.Length > 0)
|
||||
if (currentBuffer.Length > 0 && internalCursor > 0)
|
||||
{
|
||||
currentBuffer = currentBuffer.Remove(internalCursor - 1, 1);
|
||||
internalCursor--;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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!;
|
||||
}
|
||||
}
|
||||
@@ -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<IPlayerSession> SubscribedSessions = new();
|
||||
internal readonly HashSet<ICommonSession> SubscribedSessions = new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
@@ -16,13 +13,5 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// System that handles players being attached/detached from entities.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class ActorSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ActorComponent, ComponentShutdown>(OnActorShutdown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a player session to an entity, optionally kicking any sessions already attached to it.
|
||||
/// </summary>
|
||||
/// <param name="uid">The entity to attach the player to</param>
|
||||
/// <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, IPlayerSession player, bool force = false)
|
||||
{
|
||||
return Attach(uid, player, false, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a player session to an entity, optionally kicking any sessions already attached to it.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity to attach the player to</param>
|
||||
/// <param name="player">The player to attach to the entity</param>
|
||||
/// <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, IPlayerSession player, bool force, out IPlayerSession? forceKicked)
|
||||
{
|
||||
// Null by default.
|
||||
forceKicked = null;
|
||||
|
||||
if (player.AttachedEntity == entity)
|
||||
return true;
|
||||
|
||||
if (entity is not { } uid)
|
||||
return Detach(player);
|
||||
|
||||
// Cannot attach to a deleted, nonexisting or terminating entity.
|
||||
if (!TryComp(uid, out MetaDataComponent? meta) || meta.EntityLifeStage > EntityLifeStage.MapInitialized)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if there was a player attached to the entity already...
|
||||
if (EntityManager.TryGetComponent(uid, out ActorComponent? actor))
|
||||
{
|
||||
// If we're not forcing the attach, this fails.
|
||||
if (!force)
|
||||
return false;
|
||||
|
||||
// Set the event's force-kicked session before detaching it.
|
||||
forceKicked = actor.PlayerSession;
|
||||
Detach(uid, actor);
|
||||
}
|
||||
|
||||
// Detach from the currently attached entity.
|
||||
if (!Detach(player))
|
||||
return false;
|
||||
|
||||
// We add the actor component.
|
||||
actor = EntityManager.AddComponent<ActorComponent>(uid);
|
||||
EntityManager.EnsureComponent<EyeComponent>(uid);
|
||||
actor.PlayerSession = player;
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches an attached session from the entity, if any.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity player sessions will be detached from.</param>
|
||||
/// <returns>Whether any player session was detached.</returns>
|
||||
public bool Detach(EntityUid uid, ActorComponent? actor = null)
|
||||
{
|
||||
if (!Resolve(uid, ref actor, false))
|
||||
return false;
|
||||
|
||||
RemComp(uid, actor);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches this player from its attached entity, if any.
|
||||
/// </summary>
|
||||
/// <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(IPlayerSession player)
|
||||
{
|
||||
var uid = player.AttachedEntity;
|
||||
return uid == null || Detach(uid.Value);
|
||||
}
|
||||
|
||||
private void OnActorShutdown(EntityUid entity, ActorComponent component, ComponentShutdown args)
|
||||
{
|
||||
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 IPlayerSession? actor, [MaybeNullWhen(true)] out EntityUid? actorEntity)
|
||||
{
|
||||
actor = null;
|
||||
actorEntity = null;
|
||||
if (userId != null)
|
||||
{
|
||||
if (!_playerManager.TryGetSessionById(userId.Value, out actor))
|
||||
return false;
|
||||
actorEntity = actor.AttachedEntity;
|
||||
}
|
||||
|
||||
return actor != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event for when a player has been attached to an entity.
|
||||
/// </summary>
|
||||
public sealed class PlayerAttachedEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Entity { get; }
|
||||
public IPlayerSession Player { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The player session that was forcefully kicked from the entity, if any.
|
||||
/// </summary>
|
||||
public IPlayerSession? Kicked { get; }
|
||||
|
||||
public PlayerAttachedEvent(EntityUid entity, IPlayerSession player, IPlayerSession? kicked = null)
|
||||
{
|
||||
Entity = entity;
|
||||
Player = player;
|
||||
Kicked = kicked;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event for when a player has been detached from an entity.
|
||||
/// </summary>
|
||||
public sealed class PlayerDetachedEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Entity { get; }
|
||||
public IPlayerSession Player { get; }
|
||||
|
||||
public PlayerDetachedEvent(EntityUid entity, IPlayerSession player)
|
||||
{
|
||||
Entity = entity;
|
||||
Player = player;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ 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]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Robust.Server.GameObjects;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
@@ -15,10 +16,10 @@ namespace Robust.Server.GameObjects
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
private readonly Dictionary<IPlayerSession, IPlayerCommandStates> _playerInputs = new();
|
||||
private readonly Dictionary<ICommonSession, IPlayerCommandStates> _playerInputs = new();
|
||||
|
||||
|
||||
private readonly Dictionary<IPlayerSession, uint> _lastProcessedInputCmd = new();
|
||||
private readonly Dictionary<ICommonSession, uint> _lastProcessedInputCmd = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
@@ -48,7 +49,7 @@ namespace Robust.Server.GameObjects
|
||||
if (!Enum.IsDefined(typeof(BoundKeyState), msg.State))
|
||||
return;
|
||||
|
||||
var session = (IPlayerSession) eventArgs.SenderSession;
|
||||
var session = eventArgs.SenderSession;
|
||||
|
||||
if (_lastProcessedInputCmd[session] < msg.InputSequence)
|
||||
_lastProcessedInputCmd[session] = msg.InputSequence;
|
||||
@@ -65,12 +66,12 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
public IPlayerCommandStates GetInputStates(IPlayerSession session)
|
||||
public IPlayerCommandStates GetInputStates(ICommonSession session)
|
||||
{
|
||||
return _playerInputs[session];
|
||||
}
|
||||
|
||||
public uint GetLastInputCommand(IPlayerSession session)
|
||||
public uint GetLastInputCommand(ICommonSession session)
|
||||
{
|
||||
return _lastProcessedInputCmd[session];
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ 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;
|
||||
@@ -291,6 +290,9 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
|
||||
ReadGrids(data);
|
||||
|
||||
// grids prior to engine v175 might've been serialized with empty chunks which now throw debug asserts.
|
||||
RemoveEmptyChunks(data);
|
||||
|
||||
// Then, go hierarchically in order and do the entity things.
|
||||
StartupEntities(data);
|
||||
|
||||
@@ -306,6 +308,25 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RemoveEmptyChunks(MapData data)
|
||||
{
|
||||
var gridQuery = _serverEntityManager.GetEntityQuery<MapGridComponent>();
|
||||
foreach (var uid in data.EntitiesToDeserialize.Keys)
|
||||
{
|
||||
if (!gridQuery.TryGetComponent(uid, out var gridComp))
|
||||
continue;
|
||||
|
||||
foreach (var (index, chunk) in gridComp.Chunks)
|
||||
{
|
||||
if (chunk.FilledTiles > 0)
|
||||
continue;
|
||||
|
||||
Log.Warning($"Encountered empty chunk while deserializing map. Grid: {ToPrettyString(uid)}. Chunk index: {index}");
|
||||
gridComp.Chunks.Remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool VerifyEntitiesExist(MapData data, BeforeEntityReadEvent ev)
|
||||
{
|
||||
_stopwatch.Restart();
|
||||
@@ -733,7 +754,7 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
return;
|
||||
|
||||
// get ents that the grids will bind to
|
||||
var gridComps = new MapGridComponent[yamlGrids.Count];
|
||||
var gridComps = new Entity<MapGridComponent>[yamlGrids.Count];
|
||||
var gridQuery = _serverEntityManager.GetEntityQuery<MapGridComponent>();
|
||||
|
||||
// linear search for new grid comps
|
||||
@@ -745,7 +766,7 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
// These should actually be new, pre-init
|
||||
DebugTools.Assert(gridComp.LifeStage == ComponentLifeStage.Added);
|
||||
|
||||
gridComps[gridComp.GridIndex] = gridComp;
|
||||
gridComps[gridComp.GridIndex] = new Entity<MapGridComponent>(uid, gridComp);
|
||||
}
|
||||
|
||||
for (var index = 0; index < yamlGrids.Count; index++)
|
||||
@@ -764,18 +785,18 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
MappingDataNode yamlGridInfo = (MappingDataNode)yamlGrid["settings"];
|
||||
SequenceDataNode yamlGridChunks = (SequenceDataNode)yamlGrid["chunks"];
|
||||
|
||||
var grid = AllocateMapGrid(gridComp, yamlGridInfo);
|
||||
var gridUid = grid.Owner;
|
||||
AllocateMapGrid(gridComp, yamlGridInfo);
|
||||
var gridUid = gridComp.Owner;
|
||||
|
||||
foreach (var chunkNode in yamlGridChunks.Cast<MappingDataNode>())
|
||||
{
|
||||
var (chunkOffsetX, chunkOffsetY) = _serManager.Read<Vector2i>(chunkNode["ind"]);
|
||||
_serManager.Read(chunkNode, _context, instanceProvider: () => _mapSystem.GetOrAddChunk(gridUid, grid, chunkOffsetX, chunkOffsetY), notNullableOverride: true);
|
||||
_serManager.Read(chunkNode, _context, instanceProvider: () => _mapSystem.GetOrAddChunk(gridUid, gridComp, chunkOffsetX, chunkOffsetY), notNullableOverride: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static MapGridComponent AllocateMapGrid(MapGridComponent gridComp, MappingDataNode yamlGridInfo)
|
||||
private static void AllocateMapGrid(MapGridComponent gridComp, MappingDataNode yamlGridInfo)
|
||||
{
|
||||
// sane defaults
|
||||
ushort csz = 16;
|
||||
@@ -795,8 +816,6 @@ public sealed class MapLoaderSystem : EntitySystem
|
||||
|
||||
gridComp.ChunkSize = csz;
|
||||
gridComp.TileSize = tsz;
|
||||
|
||||
return gridComp;
|
||||
}
|
||||
|
||||
private void StartupEntities(MapData data)
|
||||
|
||||
@@ -66,9 +66,8 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
private void HandleGridEmpty(EntityUid uid, MapGridComponent component, EmptyGridEvent args)
|
||||
{
|
||||
if (!_deleteEmptyGrids) return;
|
||||
if (!EntityManager.EntityExists(uid)) return;
|
||||
if (EntityManager.GetComponent<MetaDataComponent>(uid).EntityLifeStage >= EntityLifeStage.Terminating) return;
|
||||
if (!_deleteEmptyGrids || TerminatingOrDeleted(uid) || HasComp<MapComponent>(uid))
|
||||
return;
|
||||
|
||||
MapManager.DeleteGrid(args.GridId);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Server.GameObjects;
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
@@ -21,8 +20,6 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
private readonly List<ICommonSession> _sessionCache = new();
|
||||
|
||||
private readonly Dictionary<ICommonSession, List<PlayerBoundUserInterface>> _openInterfaces = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -46,7 +43,7 @@ namespace Robust.Server.GameObjects
|
||||
if (args.NewStatus != SessionStatus.Disconnected)
|
||||
return;
|
||||
|
||||
if (!_openInterfaces.TryGetValue(args.Session, out var buis))
|
||||
if (!OpenInterfaces.TryGetValue(args.Session, out var buis))
|
||||
return;
|
||||
|
||||
foreach (var bui in buis.ToArray())
|
||||
@@ -92,7 +89,7 @@ namespace Robust.Server.GameObjects
|
||||
/// <summary>
|
||||
/// Verify that the subscribed clients are still in range of the interface.
|
||||
/// </summary>
|
||||
private void CheckRange(EntityUid uid, Shared.GameObjects.ActiveUserInterfaceComponent activeUis, PlayerBoundUserInterface ui, TransformComponent transform, EntityQuery<TransformComponent> query)
|
||||
private void CheckRange(EntityUid uid, ActiveUserInterfaceComponent activeUis, PlayerBoundUserInterface ui, TransformComponent transform, EntityQuery<TransformComponent> query)
|
||||
{
|
||||
if (ui.InteractionRange <= 0)
|
||||
return;
|
||||
@@ -139,11 +136,6 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateInterface(PlayerBoundUserInterface ui)
|
||||
{
|
||||
EnsureComp<ActiveUserInterfaceComponent>(ui.Owner).Interfaces.Add(ui);
|
||||
}
|
||||
|
||||
#region Get BUI
|
||||
|
||||
public bool HasUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
|
||||
@@ -168,20 +160,14 @@ namespace Robust.Server.GameObjects
|
||||
? bui
|
||||
: null;
|
||||
}
|
||||
public bool TryGetUi(EntityUid uid, Enum uiKey, [NotNullWhen(true)] out PlayerBoundUserInterface? bui, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
bui = null;
|
||||
|
||||
return Resolve(uid, ref ui, false) && ui.Interfaces.TryGetValue(uiKey, out bui);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return UIs a session has open.
|
||||
/// Null if empty.
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
public List<PlayerBoundUserInterface>? GetAllUIsForSession(ICommonSession session)
|
||||
{
|
||||
_openInterfaces.TryGetValue(session, out var value);
|
||||
OpenInterfaces.TryGetValue(session, out var value);
|
||||
return value;
|
||||
}
|
||||
#endregion
|
||||
@@ -194,7 +180,7 @@ namespace Robust.Server.GameObjects
|
||||
return bui.SubscribedSessions.Count > 0;
|
||||
}
|
||||
|
||||
public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, IPlayerSession session, UserInterfaceComponent? ui = null)
|
||||
public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
@@ -218,7 +204,7 @@ namespace Robust.Server.GameObjects
|
||||
public bool TrySetUiState(EntityUid uid,
|
||||
Enum uiKey,
|
||||
BoundUserInterfaceState state,
|
||||
IPlayerSession? session = null,
|
||||
ICommonSession? session = null,
|
||||
UserInterfaceComponent? ui = null,
|
||||
bool clearOverrides = true)
|
||||
{
|
||||
@@ -259,94 +245,14 @@ namespace Robust.Server.GameObjects
|
||||
bui.StateDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches between closed and open for a specific client.
|
||||
/// </summary>
|
||||
public bool TryToggleUi(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
ToggleUi(bui, session);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches between closed and open for a specific client.
|
||||
/// </summary>
|
||||
public void ToggleUi(PlayerBoundUserInterface bui, ICommonSession session)
|
||||
{
|
||||
if (bui._subscribedSessions.Contains(session))
|
||||
CloseUi(bui, session);
|
||||
else
|
||||
OpenUi(bui, session);
|
||||
}
|
||||
|
||||
#region Open
|
||||
|
||||
public bool TryOpen(EntityUid uid, Enum uiKey, IPlayerSession session, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
return OpenUi(bui, session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens this interface for a specific client.
|
||||
/// </summary>
|
||||
public bool OpenUi(PlayerBoundUserInterface bui, ICommonSession session)
|
||||
{
|
||||
if (session.Status == SessionStatus.Connecting || session.Status == SessionStatus.Disconnected)
|
||||
return false;
|
||||
|
||||
if (!bui._subscribedSessions.Add(session))
|
||||
return false;
|
||||
|
||||
_openInterfaces.GetOrNew(session).Add(bui);
|
||||
RaiseLocalEvent(bui.Owner, new BoundUIOpenedEvent(bui.UiKey, bui.Owner, session));
|
||||
|
||||
RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), new OpenBoundInterfaceMessage(), bui.UiKey), session.ConnectedClient);
|
||||
|
||||
// Fun fact, clients needs to have BUIs open before they can receive the state.....
|
||||
if (bui.LastStateMsg != null)
|
||||
RaiseNetworkEvent(bui.LastStateMsg, session.ConnectedClient);
|
||||
|
||||
ActivateInterface(bui);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Close
|
||||
public bool TryClose(EntityUid uid, Enum uiKey, IPlayerSession session, UserInterfaceComponent? ui = null)
|
||||
{
|
||||
if (!TryGetUi(uid, uiKey, out var bui, ui))
|
||||
return false;
|
||||
|
||||
return CloseUi(bui, session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close this interface for a specific client.
|
||||
/// </summary>
|
||||
public bool CloseUi(PlayerBoundUserInterface bui, ICommonSession session, ActiveUserInterfaceComponent? activeUis = null)
|
||||
{
|
||||
if (!bui._subscribedSessions.Remove(session))
|
||||
return false;
|
||||
|
||||
RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), new CloseBoundInterfaceMessage(), bui.UiKey), session.ConnectedClient);
|
||||
CloseShared(bui, session, activeUis);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void CloseShared(PlayerBoundUserInterface bui, ICommonSession session, ActiveUserInterfaceComponent? activeUis = null)
|
||||
{
|
||||
var owner = bui.Owner;
|
||||
bui._subscribedSessions.Remove(session);
|
||||
bui.PlayerStateOverrides.Remove(session);
|
||||
|
||||
if (_openInterfaces.TryGetValue(session, out var buis))
|
||||
if (OpenInterfaces.TryGetValue(session, out var buis))
|
||||
buis.Remove(bui);
|
||||
|
||||
RaiseLocalEvent(owner, new BoundUIClosedEvent(bui.UiKey, owner, session));
|
||||
|
||||
@@ -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, IPlayerSession session)
|
||||
public void AddViewSubscriber(EntityUid uid, ICommonSession 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.AddViewSubscription(uid);
|
||||
session.ViewSubscriptions.Add(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, IPlayerSession session)
|
||||
public void RemoveViewSubscriber(EntityUid uid, ICommonSession 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.RemoveViewSubscription(uid);
|
||||
session.ViewSubscriptions.Remove(uid);
|
||||
RaiseLocalEvent(uid, new ViewSubscriberRemovedEvent(uid, session), true);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Robust.Server.GameObjects
|
||||
{
|
||||
foreach (var session in component.SubscribedSessions)
|
||||
{
|
||||
session.RemoveViewSubscription(uid);
|
||||
session.ViewSubscriptions.Remove(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,9 +62,9 @@ namespace Robust.Server.GameObjects
|
||||
public sealed class ViewSubscriberAddedEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid View { get; }
|
||||
public IPlayerSession Subscriber { get; }
|
||||
public ICommonSession Subscriber { get; }
|
||||
|
||||
public ViewSubscriberAddedEvent(EntityUid view, IPlayerSession subscriber)
|
||||
public ViewSubscriberAddedEvent(EntityUid view, ICommonSession subscriber)
|
||||
{
|
||||
View = view;
|
||||
Subscriber = subscriber;
|
||||
@@ -78,9 +78,9 @@ namespace Robust.Server.GameObjects
|
||||
public sealed class ViewSubscriberRemovedEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid View { get; }
|
||||
public IPlayerSession Subscriber { get; }
|
||||
public ICommonSession Subscriber { get; }
|
||||
|
||||
public ViewSubscriberRemovedEvent(EntityUid view, IPlayerSession subscriber)
|
||||
public ViewSubscriberRemovedEvent(EntityUid view, ICommonSession subscriber)
|
||||
{
|
||||
View = view;
|
||||
Subscriber = subscriber;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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;
|
||||
@@ -21,6 +22,15 @@ 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()
|
||||
@@ -40,12 +50,6 @@ 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)
|
||||
@@ -57,12 +61,6 @@ 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)
|
||||
@@ -74,12 +72,6 @@ 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);
|
||||
@@ -127,12 +119,6 @@ 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.
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
public interface IServerEntityNetworkManager : IEntityNetworkManager
|
||||
{
|
||||
uint GetLastMessageSequence(IPlayerSession session);
|
||||
uint GetLastMessageSequence(ICommonSession session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
using Prometheus;
|
||||
using Robust.Server.Player;
|
||||
@@ -15,6 +14,7 @@ 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;
|
||||
@@ -42,6 +42,7 @@ namespace Robust.Server.GameObjects
|
||||
#endif
|
||||
|
||||
private ISawmill _netEntSawmill = default!;
|
||||
private EntityQuery<ActorComponent> _actorQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -53,6 +54,12 @@ namespace Robust.Server.GameObjects
|
||||
base.Initialize();
|
||||
}
|
||||
|
||||
public override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
_actorQuery = GetEntityQuery<ActorComponent>();
|
||||
}
|
||||
|
||||
EntityUid IServerEntityManagerInternal.AllocEntity(EntityPrototype? prototype)
|
||||
{
|
||||
return AllocEntity(prototype, out _);
|
||||
@@ -78,15 +85,15 @@ namespace Robust.Server.GameObjects
|
||||
StartEntity(entity);
|
||||
}
|
||||
|
||||
private protected override EntityUid CreateEntity(string? prototypeName, IEntityLoadContext? context = null)
|
||||
private protected override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
|
||||
{
|
||||
if (prototypeName == null)
|
||||
return base.CreateEntity(prototypeName, context);
|
||||
return base.CreateEntity(prototypeName, out metadata, context);
|
||||
|
||||
if (!PrototypeManager.TryIndex<EntityPrototype>(prototypeName, out var prototype))
|
||||
throw new EntityCreationException($"Attempted to spawn an entity with an invalid prototype: {prototypeName}");
|
||||
|
||||
var entity = base.CreateEntity(prototype, context);
|
||||
var entity = base.CreateEntity(prototype, out metadata, context);
|
||||
|
||||
// At this point in time, all data configure on the entity *should* be purely from the prototype.
|
||||
// As such, we can reset the modified ticks to Zero,
|
||||
@@ -109,15 +116,10 @@ namespace Robust.Server.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
[return: NotNullIfNotNull("uid")]
|
||||
public override EntityStringRepresentation? ToPrettyString(EntityUid? uid)
|
||||
public override EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metadata = null)
|
||||
{
|
||||
if (uid == null)
|
||||
return null;
|
||||
|
||||
TryGetComponent(uid, out ActorComponent? actor);
|
||||
|
||||
return base.ToPrettyString(uid).Value with { Session = actor?.PlayerSession };
|
||||
_actorQuery.TryGetComponent(uid, out ActorComponent? actor);
|
||||
return base.ToPrettyString(uid) with { Session = actor?.PlayerSession };
|
||||
}
|
||||
|
||||
#region IEntityNetworkManager impl
|
||||
@@ -129,7 +131,7 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
private readonly PriorityQueue<MsgEntity> _queue = new(new MessageSequenceComparer());
|
||||
|
||||
private readonly Dictionary<IPlayerSession, uint> _lastProcessedSequencesCmd =
|
||||
private readonly Dictionary<ICommonSession, uint> _lastProcessedSequencesCmd =
|
||||
new();
|
||||
|
||||
private bool _logLateMsgs;
|
||||
@@ -160,7 +162,7 @@ namespace Robust.Server.GameObjects
|
||||
EntitiesCount.Set(Entities.Count);
|
||||
}
|
||||
|
||||
public uint GetLastMessageSequence(IPlayerSession session)
|
||||
public uint GetLastMessageSequence(ICommonSession session)
|
||||
{
|
||||
return _lastProcessedSequencesCmd[session];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Server.GameStates
|
||||
|
||||
@@ -8,7 +8,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -95,7 +95,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
|
||||
/// <summary>
|
||||
/// List of <see cref="TIndex"/> that should always get sent to a certain <see cref="ICommonSession"/>.
|
||||
/// </summary>
|
||||
private readonly Dictionary<ICommonSession, HashSet<TIndex>> _localOverrides = new();
|
||||
private readonly Dictionary<ICommonSession, HashSet<TIndex>> _sessionOverrides = new();
|
||||
|
||||
/// <summary>
|
||||
/// Which <see cref="TIndex"/> where last seen/sent to a certain <see cref="ICommonSession"/>.
|
||||
@@ -207,7 +207,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
|
||||
public bool TryGetChunk(EntityUid gridId, Vector2i chunkIndices, [NotNullWhen(true)] out HashSet<TIndex>? indices) =>
|
||||
_gridChunkContents[gridId].TryGetValue(chunkIndices, out indices);
|
||||
|
||||
public HashSet<TIndex>.Enumerator GetElementsForSession(ICommonSession session) => _localOverrides[session].GetEnumerator();
|
||||
public HashSet<TIndex>.Enumerator GetSessionOverrides(ICommonSession session) => _sessionOverrides[session].GetEnumerator();
|
||||
|
||||
private void AddIndexInternal(TIndex index, IIndexLocation location, HashSet<IChunkIndexLocation> dirtyChunks)
|
||||
{
|
||||
@@ -226,10 +226,10 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
|
||||
gridLoc.Add(index);
|
||||
dirtyChunks.Add(gridChunkLocation);
|
||||
break;
|
||||
case LocalOverride localOverride:
|
||||
// might be gone due to disconnects
|
||||
if(!_localOverrides.ContainsKey(localOverride.Session)) return;
|
||||
_localOverrides[localOverride.Session].Add(index);
|
||||
case SessionOverride sessionOverride:
|
||||
if (!_sessionOverrides.TryGetValue(sessionOverride.Session, out var set))
|
||||
return;
|
||||
set.Add(index);
|
||||
break;
|
||||
case MapChunkLocation mapChunkLocation:
|
||||
// might be gone due to map-deletions
|
||||
@@ -253,16 +253,14 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
|
||||
switch (location)
|
||||
{
|
||||
case GlobalOverride global:
|
||||
if (global.Recursive)
|
||||
_globalRecursiveOverrides.Remove(index);
|
||||
else
|
||||
_globalOverrides.Remove(index);
|
||||
var set = global.Recursive ? _globalRecursiveOverrides : _globalOverrides;
|
||||
set.Remove(index);
|
||||
break;
|
||||
case GridChunkLocation gridChunkLocation:
|
||||
_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Remove(index);
|
||||
break;
|
||||
case LocalOverride localOverride:
|
||||
_localOverrides[localOverride.Session].Remove(index);
|
||||
case SessionOverride sessionOverride:
|
||||
_sessionOverrides.GetValueOrDefault(sessionOverride.Session)?.Remove(index);
|
||||
break;
|
||||
case MapChunkLocation mapChunkLocation:
|
||||
_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices].Remove(index);
|
||||
@@ -276,7 +274,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
|
||||
/// <inheritdoc />
|
||||
public bool AddPlayer(ICommonSession session)
|
||||
{
|
||||
return _localOverrides.TryAdd(session, new()) & _lastSeen.TryAdd(session, new());
|
||||
return _sessionOverrides.TryAdd(session, new()) & _lastSeen.TryAdd(session, new());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -292,7 +290,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
|
||||
/// <inheritdoc />
|
||||
public bool RemovePlayer(ICommonSession session)
|
||||
{
|
||||
if (_localOverrides.Remove(session, out var indices))
|
||||
if (_sessionOverrides.Remove(session, out var indices))
|
||||
{
|
||||
foreach (var index in indices)
|
||||
{
|
||||
@@ -412,7 +410,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
|
||||
return;
|
||||
}
|
||||
|
||||
if (!removeFromOverride && oldLocation is LocalOverride)
|
||||
if (!removeFromOverride && oldLocation is SessionOverride)
|
||||
return;
|
||||
|
||||
if (oldLocation is GlobalOverride global &&
|
||||
@@ -426,28 +424,29 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
|
||||
|
||||
/// <summary>
|
||||
/// Updates an <see cref="TIndex"/> to be sent to a specific <see cref="ICommonSession"/> at all times.
|
||||
/// This will always also send all children of the given entity.
|
||||
/// </summary>
|
||||
/// <param name="index">The <see cref="TIndex"/> to update.</param>
|
||||
/// <param name="session">The <see cref="ICommonSession"/> receiving the object.</param>
|
||||
/// <param name="removeFromOverride">An index at an override position will not be updated unless you set this flag.</param>
|
||||
public void UpdateIndex(TIndex index, ICommonSession session, bool removeFromOverride = false)
|
||||
public void AddSessionOverride(TIndex index, ICommonSession session, bool removeFromOverride)
|
||||
{
|
||||
if (!TryGetLocation(index, out var oldLocation))
|
||||
{
|
||||
RegisterUpdate(index, new LocalOverride(session));
|
||||
RegisterUpdate(index, new SessionOverride(session));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!removeFromOverride || oldLocation is GlobalOverride)
|
||||
if (!removeFromOverride && oldLocation is GlobalOverride)
|
||||
return;
|
||||
|
||||
if (oldLocation is LocalOverride local &&
|
||||
if (oldLocation is SessionOverride local &&
|
||||
(!removeFromOverride || local.Session == session))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RegisterUpdate(index, new LocalOverride(session));
|
||||
RegisterUpdate(index, new SessionOverride(session));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -460,7 +459,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
|
||||
{
|
||||
if (!removeFromOverride
|
||||
&& TryGetLocation(index, out var oldLocation)
|
||||
&& oldLocation is GlobalOverride or LocalOverride)
|
||||
&& oldLocation is GlobalOverride or SessionOverride)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -510,7 +509,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
|
||||
_indexLocations.TryGetValue(index, out var oldLocation);
|
||||
|
||||
//removeFromOverride is false 99% of the time.
|
||||
if ((bufferedLocation ?? oldLocation) is GlobalOverride or LocalOverride && !removeFromOverride)
|
||||
if ((bufferedLocation ?? oldLocation) is GlobalOverride or SessionOverride && !removeFromOverride)
|
||||
return;
|
||||
|
||||
if (oldLocation is GridChunkLocation oldGrid &&
|
||||
@@ -541,7 +540,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
|
||||
_indexLocations.TryGetValue(index, out var oldLocation);
|
||||
|
||||
//removeFromOverride is false 99% of the time.
|
||||
if ((bufferedLocation ?? oldLocation) is GlobalOverride or LocalOverride && !removeFromOverride)
|
||||
if ((bufferedLocation ?? oldLocation) is GlobalOverride or SessionOverride && !removeFromOverride)
|
||||
return;
|
||||
|
||||
// Is this entity just returning to its old location?
|
||||
@@ -641,14 +640,14 @@ public struct GlobalOverride : IIndexLocation
|
||||
}
|
||||
}
|
||||
|
||||
public struct LocalOverride : IIndexLocation
|
||||
public struct SessionOverride : IIndexLocation
|
||||
{
|
||||
public LocalOverride(ICommonSession session)
|
||||
public SessionOverride(ICommonSession session)
|
||||
{
|
||||
Session = session;
|
||||
}
|
||||
|
||||
public ICommonSession Session { get; init; }
|
||||
public readonly ICommonSession Session;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Server.GameStates;
|
||||
|
||||
@@ -22,13 +22,14 @@ public sealed class PvsOverrideSystem : EntitySystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to ensure that an entity is always sent to a specific client. Overrides any global or pre-existing
|
||||
/// client-specific overrides.
|
||||
/// Used to ensure that an entity is always sent to a specific client. By default this overrides any global or pre-existing
|
||||
/// client-specific overrides. Unlike global overrides, this is always recursive.
|
||||
/// </summary>
|
||||
/// <param name="removeExistingOverride">Whether or not to supersede existing overrides.</param>
|
||||
public void AddSessionOverride(EntityUid uid, ICommonSession session,bool removeExistingOverride = true)
|
||||
/// <param name="recursive">If true, this will also recursively send any children of the given index.</param>
|
||||
public void AddSessionOverride(EntityUid uid, ICommonSession session, bool removeExistingOverride = true)
|
||||
{
|
||||
_pvs.EntityPVSCollection.UpdateIndex(GetNetEntity(uid), session, removeExistingOverride);
|
||||
_pvs.EntityPVSCollection.AddSessionOverride(GetNetEntity(uid), session, removeExistingOverride);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
|
||||
@@ -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<IPlayerSession> sessions)
|
||||
public void CleanupDirty(IEnumerable<ICommonSession> sessions)
|
||||
{
|
||||
if (!CullingEnabled)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -15,7 +16,7 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -35,10 +36,16 @@ 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>
|
||||
@@ -139,6 +146,7 @@ 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;
|
||||
@@ -156,6 +164,7 @@ 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;
|
||||
@@ -188,6 +197,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
Log.Warning(sb.ToString());
|
||||
|
||||
sessionData.LastSeenAt.Clear();
|
||||
sessionData.LastLeftView.Clear();
|
||||
|
||||
if (sessionData.Overflow != null)
|
||||
{
|
||||
@@ -198,7 +208,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);
|
||||
}
|
||||
|
||||
@@ -211,6 +221,11 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
_viewSize = obj * 2;
|
||||
}
|
||||
|
||||
private void OnForceAckChanged(int value)
|
||||
{
|
||||
ForceAckThreshold = value;
|
||||
}
|
||||
|
||||
private void SetPvs(bool value)
|
||||
{
|
||||
_seenAllEnts.Clear();
|
||||
@@ -256,6 +271,8 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
foreach (var sessionData in PlayerData.Values)
|
||||
{
|
||||
sessionData.LastSeenAt.Remove(metadata.NetEntity);
|
||||
sessionData.LastLeftView.Remove(metadata.NetEntity);
|
||||
|
||||
if (sessionData.SentEntities.TryGetValue(previousTick, out var ents))
|
||||
ents.Remove(metadata.NetEntity);
|
||||
}
|
||||
@@ -418,10 +435,15 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
|
||||
#endregion
|
||||
|
||||
public (List<(int, IChunkIndexLocation)> , HashSet<int>[], EntityUid[][] viewers) GetChunks(IPlayerSession[] sessions)
|
||||
public List<(int, IChunkIndexLocation)> GetChunks(
|
||||
ICommonSession[] sessions,
|
||||
ref HashSet<int>[] playerChunks,
|
||||
ref EntityUid[][] viewerEntities)
|
||||
{
|
||||
var playerChunks = new HashSet<int>[sessions.Length];
|
||||
var viewerEntities = new EntityUid[sessions.Length][];
|
||||
// Pass these in to avoid allocating new ones every tick, 99% of the time sessions length is going to be the same size.
|
||||
// These values will get overridden here and the old values have already been returned to the pool by this point.
|
||||
Array.Resize(ref playerChunks, sessions.Length);
|
||||
Array.Resize(ref viewerEntities, sessions.Length);
|
||||
|
||||
_chunkList.Clear();
|
||||
// Keep track of the index of each chunk we use for a faster index lookup.
|
||||
@@ -444,8 +466,8 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
var session = sessions[i];
|
||||
playerChunks[i] = _playerChunkPool.Get();
|
||||
|
||||
var viewers = GetSessionViewers(session);
|
||||
viewerEntities[i] = viewers;
|
||||
ref var viewers = ref viewerEntities[i];
|
||||
GetSessionViewers(session, ref viewers);
|
||||
|
||||
for (var j = 0; j < viewers.Length; j++)
|
||||
{
|
||||
@@ -537,7 +559,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
return (_chunkList, playerChunks, viewerEntities);
|
||||
return _chunkList;
|
||||
}
|
||||
|
||||
public void RegisterNewPreviousChunkTrees(
|
||||
@@ -554,23 +576,20 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
_reusedTrees.Add(chunks[i]);
|
||||
}
|
||||
|
||||
var previousIndices = _previousTrees.Keys.ToArray();
|
||||
for (var i = 0; i < previousIndices.Length; i++)
|
||||
foreach (var (index, chunk) in _previousTrees)
|
||||
{
|
||||
var index = previousIndices[i];
|
||||
// ReSharper disable once InconsistentlySynchronizedField
|
||||
if (_reusedTrees.Contains(index)) continue;
|
||||
var chunk = _previousTrees[index];
|
||||
if (chunk.HasValue)
|
||||
if (_reusedTrees.Contains(index))
|
||||
continue;
|
||||
|
||||
if (chunk != null)
|
||||
{
|
||||
_chunkCachePool.Return(chunk.Value.metadata);
|
||||
_treePool.Return(chunk.Value.tree);
|
||||
}
|
||||
|
||||
if (!chunks.Contains(index))
|
||||
{
|
||||
_previousTrees.Remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
_previousTrees.EnsureCapacity(chunks.Count);
|
||||
@@ -687,7 +706,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
}
|
||||
|
||||
internal (List<EntityState>? updates, List<NetEntity>? deletions, List<NetEntity>? leftPvs, GameTick fromTick)
|
||||
CalculateEntityStates(IPlayerSession session,
|
||||
CalculateEntityStates(ICommonSession session,
|
||||
GameTick fromTick,
|
||||
GameTick toTick,
|
||||
(Dictionary<NetEntity, MetaDataComponent> metadata, RobustTree<NetEntity> tree)?[] chunks,
|
||||
@@ -695,14 +714,15 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
EntityUid[] viewers)
|
||||
{
|
||||
DebugTools.Assert(session.Status == SessionStatus.InGame);
|
||||
var newEntityBudget = _netConfigManager.GetClientCVar(session.ConnectedClient, CVars.NetPVSEntityBudget);
|
||||
var enteredEntityBudget = _netConfigManager.GetClientCVar(session.ConnectedClient, CVars.NetPVSEntityEnterBudget);
|
||||
var newEntityBudget = _netConfigManager.GetClientCVar(session.Channel, CVars.NetPVSEntityBudget);
|
||||
var enteredEntityBudget = _netConfigManager.GetClientCVar(session.Channel, CVars.NetPVSEntityEnterBudget);
|
||||
var newEntityCount = 0;
|
||||
var enteredEntityCount = 0;
|
||||
var sessionData = PlayerData[session];
|
||||
sessionData.SentEntities.TryGetValue(toTick - 1, out var lastSent);
|
||||
var lastAcked = sessionData.LastAcked?.Data;
|
||||
var lastSeen = sessionData.LastSeenAt;
|
||||
var lastLeft = sessionData.LastLeftView;
|
||||
var visibleEnts = _visSetPool.Get();
|
||||
|
||||
if (visibleEnts.Count != 0)
|
||||
@@ -730,7 +750,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
|
||||
foreach (var rootNode in cache.Value.tree.RootNodes)
|
||||
{
|
||||
RecursivelyAddTreeNode(in rootNode, cache.Value.tree, lastAcked, lastSent, visibleEnts, lastSeen, cache.Value.metadata, stack, in fromTick,
|
||||
RecursivelyAddTreeNode(in rootNode, cache.Value.tree, lastAcked, lastSent, visibleEnts, lastSeen, lastLeft, cache.Value.metadata, stack, in fromTick,
|
||||
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget);
|
||||
}
|
||||
}
|
||||
@@ -741,7 +761,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
{
|
||||
var netEntity = globalEnumerator.Current;
|
||||
var uid = GetEntity(netEntity);
|
||||
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, lastSeen, in fromTick,
|
||||
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, lastSeen, lastLeft, in fromTick,
|
||||
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget);
|
||||
}
|
||||
globalEnumerator.Dispose();
|
||||
@@ -751,39 +771,45 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
{
|
||||
var netEntity = globalRecursiveEnumerator.Current;
|
||||
var uid = GetEntity(netEntity);
|
||||
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, lastSeen, in fromTick,
|
||||
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, lastSeen, lastLeft, in fromTick,
|
||||
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget, true);
|
||||
}
|
||||
globalRecursiveEnumerator.Dispose();
|
||||
|
||||
var localEnumerator = _entityPvsCollection.GetElementsForSession(session);
|
||||
while (localEnumerator.MoveNext())
|
||||
var sessionOverrides = _entityPvsCollection.GetSessionOverrides(session);
|
||||
while (sessionOverrides.MoveNext())
|
||||
{
|
||||
var netEntity = localEnumerator.Current;
|
||||
var netEntity = sessionOverrides.Current;
|
||||
var uid = GetEntity(netEntity);
|
||||
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, lastSeen,in fromTick,
|
||||
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget);
|
||||
RecursivelyAddOverride(in uid, lastAcked, lastSent, visibleEnts, lastSeen, lastLeft, in fromTick,
|
||||
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget, true);
|
||||
}
|
||||
localEnumerator.Dispose();
|
||||
sessionOverrides.Dispose();
|
||||
|
||||
foreach (var viewerEntity in viewers)
|
||||
{
|
||||
RecursivelyAddOverride(in viewerEntity, lastAcked, lastSent, visibleEnts, lastSeen, in fromTick,
|
||||
RecursivelyAddOverride(in viewerEntity, lastAcked, lastSent, visibleEnts, lastSeen, lastLeft, in fromTick,
|
||||
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget);
|
||||
}
|
||||
|
||||
var expandEvent = new ExpandPvsEvent(session);
|
||||
RaiseLocalEvent(ref expandEvent);
|
||||
foreach (var entityUid in expandEvent.Entities)
|
||||
if (expandEvent.Entities != null)
|
||||
{
|
||||
RecursivelyAddOverride(in entityUid, lastAcked, lastSent, visibleEnts, lastSeen, in fromTick,
|
||||
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget);
|
||||
foreach (var entityUid in expandEvent.Entities)
|
||||
{
|
||||
RecursivelyAddOverride(in entityUid, lastAcked, lastSent, visibleEnts, lastSeen, lastLeft, in fromTick,
|
||||
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entityUid in expandEvent.RecursiveEntities)
|
||||
if (expandEvent.RecursiveEntities != null)
|
||||
{
|
||||
RecursivelyAddOverride(in entityUid, lastAcked, lastSent, visibleEnts, lastSeen,in fromTick,
|
||||
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget, true);
|
||||
foreach (var entityUid in expandEvent.RecursiveEntities)
|
||||
{
|
||||
RecursivelyAddOverride(in entityUid, lastAcked, lastSent, visibleEnts, lastSeen, lastLeft, in fromTick,
|
||||
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget, true);
|
||||
}
|
||||
}
|
||||
|
||||
var entityStates = new List<EntityState>(entStateCount);
|
||||
@@ -820,7 +846,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
}
|
||||
|
||||
// tell a client to detach entities that have left their view
|
||||
var leftView = ProcessLeavePVS(visibleEnts, lastSent);
|
||||
var leftView = ProcessLeavePvs(visibleEnts, lastSent, lastLeft);
|
||||
|
||||
if (sessionData.SentEntities.Add(toTick, visibleEnts, out var oldEntry))
|
||||
{
|
||||
@@ -857,18 +883,26 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
/// Figure out what entities are no longer visible to the client. These entities are sent reliably to the client
|
||||
/// in a separate net message.
|
||||
/// </summary>
|
||||
private List<NetEntity>? ProcessLeavePVS(
|
||||
private List<NetEntity>? ProcessLeavePvs(
|
||||
Dictionary<NetEntity, PvsEntityVisibility> visibleEnts,
|
||||
Dictionary<NetEntity, PvsEntityVisibility>? lastSent)
|
||||
Dictionary<NetEntity, PvsEntityVisibility>? lastSent,
|
||||
Dictionary<NetEntity, GameTick> lastLeft)
|
||||
{
|
||||
if (lastSent == null)
|
||||
return null;
|
||||
|
||||
var leftView = new List<NetEntity>();
|
||||
var tick = _gameTiming.CurTick;
|
||||
var minSize = Math.Max(0, lastSent.Count - visibleEnts.Count);
|
||||
var leftView = new List<NetEntity>(minSize);
|
||||
|
||||
foreach (var netEntity in lastSent.Keys)
|
||||
{
|
||||
if (!visibleEnts.ContainsKey(netEntity))
|
||||
{
|
||||
leftView.Add(netEntity);
|
||||
lastLeft[netEntity] = tick;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return leftView.Count > 0 ? leftView : null;
|
||||
@@ -880,6 +914,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
Dictionary<NetEntity, PvsEntityVisibility>? lastSent,
|
||||
Dictionary<NetEntity, PvsEntityVisibility> toSend,
|
||||
Dictionary<NetEntity, GameTick> lastSeen,
|
||||
Dictionary<NetEntity, GameTick> lastLeft,
|
||||
Dictionary<NetEntity, MetaDataComponent> metaDataCache,
|
||||
Stack<NetEntity> stack,
|
||||
in GameTick fromTick,
|
||||
@@ -902,7 +937,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(toSend, currentNodeIndex, out var exists);
|
||||
if (!exists)
|
||||
{
|
||||
var (entered, shouldAdd) = ProcessEntry(in currentNodeIndex, lastAcked, lastSent, lastSeen,
|
||||
var (entered, shouldAdd) = ProcessEntry(in currentNodeIndex, lastAcked, lastSent, lastSeen, lastLeft, fromTick,
|
||||
ref newEntityCount, ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
|
||||
|
||||
if (!shouldAdd)
|
||||
@@ -932,6 +967,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
Dictionary<NetEntity, PvsEntityVisibility>? lastSent,
|
||||
Dictionary<NetEntity, PvsEntityVisibility> toSend,
|
||||
Dictionary<NetEntity, GameTick> lastSeen,
|
||||
Dictionary<NetEntity, GameTick> lastLeft,
|
||||
in GameTick fromTick,
|
||||
ref int newEntityCount,
|
||||
ref int enteredEntityCount,
|
||||
@@ -947,7 +983,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
|
||||
var xform = _xformQuery.GetComponent(uid);
|
||||
var parent = xform.ParentUid;
|
||||
if (parent.IsValid() && !RecursivelyAddOverride(in parent, lastAcked, lastSent, toSend, lastSeen, in fromTick,
|
||||
if (parent.IsValid() && !RecursivelyAddOverride(in parent, lastAcked, lastSent, toSend, lastSeen, lastLeft, in fromTick,
|
||||
ref newEntityCount, ref enteredEntityCount, ref entStateCount, in newEntityBudget,
|
||||
in enteredEntityBudget))
|
||||
{
|
||||
@@ -957,7 +993,6 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
var metadata = _metaQuery.GetComponent(uid);
|
||||
var netEntity = metadata.NetEntity;
|
||||
|
||||
//did we already get added?
|
||||
// Note that we check this AFTER adding parents. This is because while this entity may already have been added
|
||||
// to the toSend set, it doesn't guarantee that its parents have been. E.g., if a player ghost just teleported
|
||||
// to follow a far away entity, the player's own entity is still being sent, but we need to ensure that we also
|
||||
@@ -965,17 +1000,13 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(toSend, netEntity, out var exists);
|
||||
if (!exists)
|
||||
{
|
||||
// TODO PERFORMANCE.
|
||||
// ProcessEntry() unnecessarily checks lastSent.ContainsKey() and maybe lastSeen.Contains(). Given that at this
|
||||
// point the budgets are just ignored, this should just bypass those checks. But then again 99% of the time this
|
||||
// is just the player's own entity + maybe a singularity. So currently not all that performance intensive.
|
||||
var (entered, _) = ProcessEntry(in netEntity, lastAcked, lastSent, lastSeen, ref newEntityCount, ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
|
||||
var (entered, _) = ProcessEntry(in netEntity, lastAcked, lastSent, lastSeen, lastLeft, fromTick, ref newEntityCount, ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
|
||||
AddToSendSet(in netEntity, metadata, ref value, toSend, fromTick, in entered, ref entStateCount);
|
||||
}
|
||||
|
||||
if (addChildren)
|
||||
{
|
||||
RecursivelyAddChildren(xform, lastAcked, lastSent, toSend, lastSeen, fromTick, ref newEntityCount,
|
||||
RecursivelyAddChildren(xform, lastAcked, lastSent, toSend, lastSeen, lastLeft, fromTick, ref newEntityCount,
|
||||
ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget);
|
||||
}
|
||||
|
||||
@@ -987,6 +1018,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
Dictionary<NetEntity, PvsEntityVisibility>? lastSent,
|
||||
Dictionary<NetEntity, PvsEntityVisibility> toSend,
|
||||
Dictionary<NetEntity, GameTick> lastSeen,
|
||||
Dictionary<NetEntity, GameTick> lastLeft,
|
||||
in GameTick fromTick,
|
||||
ref int newEntityCount,
|
||||
ref int enteredEntityCount,
|
||||
@@ -1005,22 +1037,23 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(toSend, childNetEntity, out var exists);
|
||||
if (!exists)
|
||||
{
|
||||
var (entered, _) = ProcessEntry(in childNetEntity, lastAcked, lastSent, lastSeen, ref newEntityCount,
|
||||
var (entered, _) = ProcessEntry(in childNetEntity, lastAcked, lastSent, lastSeen, lastLeft, fromTick, ref newEntityCount,
|
||||
ref enteredEntityCount, newEntityBudget, enteredEntityBudget);
|
||||
|
||||
AddToSendSet(in childNetEntity, metadata, ref value, toSend, fromTick, in entered, ref entStateCount);
|
||||
}
|
||||
|
||||
RecursivelyAddChildren(childXform, lastAcked, lastSent, toSend, lastSeen, fromTick, ref newEntityCount,
|
||||
RecursivelyAddChildren(childXform, lastAcked, lastSent, toSend, lastSeen, lastLeft, fromTick, ref newEntityCount,
|
||||
ref enteredEntityCount, ref entStateCount, in newEntityBudget, in enteredEntityBudget);
|
||||
}
|
||||
}
|
||||
|
||||
private (bool Entered, bool ShouldAdd) ProcessEntry(
|
||||
in NetEntity netEntity,
|
||||
private (bool Entered, bool ShouldAdd) ProcessEntry(in NetEntity netEntity,
|
||||
Dictionary<NetEntity, PvsEntityVisibility>? lastAcked,
|
||||
Dictionary<NetEntity, PvsEntityVisibility>? lastSent,
|
||||
Dictionary<NetEntity, GameTick> lastSeen,
|
||||
Dictionary<NetEntity, GameTick> lastLeft,
|
||||
GameTick fromTick,
|
||||
ref int newEntityCount,
|
||||
ref int enteredEntityCount,
|
||||
in int newEntityBudget,
|
||||
@@ -1028,16 +1061,15 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
{
|
||||
var enteredSinceLastSent = lastSent == null || !lastSent.ContainsKey(netEntity);
|
||||
|
||||
var entered = enteredSinceLastSent || // OR, entered since last ack:
|
||||
lastAcked == null || !lastAcked.ContainsKey(netEntity);
|
||||
var entered = enteredSinceLastSent
|
||||
|| lastAcked == null
|
||||
|| !lastAcked.ContainsKey(netEntity) // entered since last acked
|
||||
|| lastLeft.GetValueOrDefault(netEntity) >= fromTick; // Just in case a packet was lost. I love dictionary lookups
|
||||
|
||||
// If the entity is entering, but we already sent this entering entity in the last message, we won't add it to
|
||||
// the budget. Chances are the packet will arrive in a nice and orderly fashion, and the client will stick to
|
||||
// their requested budget. However this can cause issues if a packet gets dropped, because a player may create
|
||||
// 2x or more times the normal entity creation budget.
|
||||
//
|
||||
// The fix for that would be to just also give the PVS budget a client-side aspect that controls entity creation
|
||||
// rate.
|
||||
if (enteredSinceLastSent)
|
||||
{
|
||||
if (newEntityCount >= newEntityBudget || enteredEntityCount >= enteredEntityBudget)
|
||||
@@ -1114,8 +1146,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);
|
||||
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating);
|
||||
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");
|
||||
if (md.EntityLastModifiedTick <= fromTick)
|
||||
continue;
|
||||
|
||||
@@ -1150,10 +1182,10 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
|
||||
if (!toSend.Add(uid) || !_metaQuery.TryGetComponent(uid, out var md))
|
||||
continue;
|
||||
|
||||
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
|
||||
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating);
|
||||
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick);
|
||||
DebugTools.Assert(md.EntityLastModifiedTick > fromTick);
|
||||
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");
|
||||
|
||||
var state = GetEntityState(player, uid, fromTick, md);
|
||||
|
||||
@@ -1177,10 +1209,10 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
|
||||
if (!toSend.Add(uid) || !_metaQuery.TryGetComponent(uid, out var md))
|
||||
continue;
|
||||
|
||||
DebugTools.Assert(md.EntityLifeStage >= EntityLifeStage.Initialized);
|
||||
DebugTools.Assert(md.EntityLifeStage < EntityLifeStage.Terminating);
|
||||
DebugTools.Assert(md.EntityLastModifiedTick >= md.CreationTick);
|
||||
DebugTools.Assert(md.EntityLastModifiedTick > fromTick);
|
||||
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");
|
||||
|
||||
var state = GetEntityState(player, uid, fromTick, md);
|
||||
if (!state.Empty)
|
||||
@@ -1285,30 +1317,44 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
|
||||
return entState;
|
||||
}
|
||||
|
||||
private EntityUid[] GetSessionViewers(ICommonSession session)
|
||||
private void GetSessionViewers(ICommonSession session, [NotNull] ref EntityUid[]? viewers)
|
||||
{
|
||||
if (session.Status != SessionStatus.InGame || session is not IPlayerSession sess)
|
||||
return Array.Empty<EntityUid>();
|
||||
if (session.Status != SessionStatus.InGame)
|
||||
{
|
||||
viewers = Array.Empty<EntityUid>();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fast path
|
||||
if (sess.ViewSubscriptionCount == 0)
|
||||
if (session.ViewSubscriptions.Count == 0)
|
||||
{
|
||||
if (session.AttachedEntity == null)
|
||||
return Array.Empty<EntityUid>();
|
||||
{
|
||||
viewers = Array.Empty<EntityUid>();
|
||||
return;
|
||||
}
|
||||
|
||||
return new[] { session.AttachedEntity.Value };
|
||||
Array.Resize(ref viewers, 1);
|
||||
viewers[0] = session.AttachedEntity.Value;
|
||||
return;
|
||||
}
|
||||
|
||||
var viewers = new HashSet<EntityUid>();
|
||||
if (session.AttachedEntity != null)
|
||||
viewers.Add(session.AttachedEntity.Value);
|
||||
|
||||
foreach (var uid in sess.ViewSubscriptions)
|
||||
int i = 0;
|
||||
if (session.AttachedEntity is { } local)
|
||||
{
|
||||
viewers.Add(uid);
|
||||
DebugTools.Assert(!session.ViewSubscriptions.Contains(local));
|
||||
Array.Resize(ref viewers, session.ViewSubscriptions.Count + 1);
|
||||
viewers[i++] = local;
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Resize(ref viewers, session.ViewSubscriptions.Count);
|
||||
}
|
||||
|
||||
return viewers.ToArray();
|
||||
foreach (var ent in session.ViewSubscriptions)
|
||||
{
|
||||
viewers[i++] = ent;
|
||||
}
|
||||
}
|
||||
|
||||
// Read Safe
|
||||
@@ -1368,6 +1414,11 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
|
||||
/// </summary>
|
||||
public readonly Dictionary<NetEntity, GameTick> LastSeenAt = new();
|
||||
|
||||
/// <summary>
|
||||
/// Tick at which an entity last left a player's PVS view.
|
||||
/// </summary>
|
||||
public readonly Dictionary<NetEntity, GameTick> LastLeftView = new();
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="SentEntities"/> overflow in case a player's last ack is more than <see cref="DirtyBufferSize"/> ticks behind the current tick.
|
||||
/// </summary>
|
||||
@@ -1390,22 +1441,22 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public readonly struct ExpandPvsEvent
|
||||
public struct ExpandPvsEvent
|
||||
{
|
||||
public readonly IPlayerSession Session;
|
||||
public readonly ICommonSession Session;
|
||||
|
||||
/// <summary>
|
||||
/// List of entities that will get added to this session's PVS set.
|
||||
/// </summary>
|
||||
public readonly List<EntityUid> Entities = new();
|
||||
public List<EntityUid>? Entities;
|
||||
|
||||
/// <summary>
|
||||
/// List of entities that will get added to this session's PVS set. Unlike <see cref="Entities"/> this will also
|
||||
/// recursively add all children of the given entity.
|
||||
/// </summary>
|
||||
public readonly List<EntityUid> RecursiveEntities = new();
|
||||
public List<EntityUid>? RecursiveEntities;
|
||||
|
||||
public ExpandPvsEvent(IPlayerSession session)
|
||||
public ExpandPvsEvent(ICommonSession session)
|
||||
{
|
||||
Session = session;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ using Prometheus;
|
||||
using Robust.Server.Replays;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Server.GameStates
|
||||
{
|
||||
@@ -37,6 +37,9 @@ namespace Robust.Server.GameStates
|
||||
// Mapping of net UID of clients -> last known acked state.
|
||||
private GameTick _lastOldestAck = GameTick.Zero;
|
||||
|
||||
private HashSet<int>[] _playerChunks = Array.Empty<HashSet<int>>();
|
||||
private EntityUid[][] _viewerEntities = Array.Empty<EntityUid[]>();
|
||||
|
||||
private PvsSystem _pvs = default!;
|
||||
|
||||
[Dependency] private readonly EntityManager _entityManager = default!;
|
||||
@@ -168,7 +171,7 @@ Oldest acked clients: {string.Join(", ", players)}
|
||||
/// <inheritdoc />
|
||||
public void SendGameStateUpdate()
|
||||
{
|
||||
var players = _playerManager.ServerSessions.Where(o => o.Status == SessionStatus.InGame).ToArray();
|
||||
var players = _playerManager.Sessions.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 +227,7 @@ Oldest acked clients: {string.Join(", ", players)}
|
||||
_pvs.CullDeletionHistory(oldestAck);
|
||||
}
|
||||
|
||||
private GameTick SendStates(IPlayerSession[] players, PvsData? pvsData)
|
||||
private GameTick SendStates(ICommonSession[] players, PvsData? pvsData)
|
||||
{
|
||||
var inputSystem = _systemManager.GetEntitySystem<InputSystem>();
|
||||
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
|
||||
@@ -265,9 +268,9 @@ Oldest acked clients: {string.Join(", ", players)}
|
||||
public (Dictionary<NetEntity, MetaDataComponent> metadata, RobustTree<NetEntity> tree)?[] ChunkCache;
|
||||
}
|
||||
|
||||
private PvsData? GetPVSData(IPlayerSession[] players)
|
||||
private PvsData? GetPVSData(ICommonSession[] players)
|
||||
{
|
||||
var (chunks, playerChunks, viewerEntities) = _pvs.GetChunks(players);
|
||||
var chunks= _pvs.GetChunks(players, ref _playerChunks, ref _viewerEntities);
|
||||
const int ChunkBatchSize = 2;
|
||||
var chunksCount = chunks.Count;
|
||||
var chunkBatches = (int)MathF.Ceiling((float)chunksCount / ChunkBatchSize);
|
||||
@@ -310,8 +313,8 @@ Oldest acked clients: {string.Join(", ", players)}
|
||||
ArrayPool<bool>.Shared.Return(reuse);
|
||||
return new PvsData()
|
||||
{
|
||||
PlayerChunks = playerChunks,
|
||||
ViewerEntities = viewerEntities,
|
||||
PlayerChunks = _playerChunks,
|
||||
ViewerEntities = _viewerEntities,
|
||||
ChunkCache = chunkCache,
|
||||
};
|
||||
}
|
||||
@@ -319,11 +322,11 @@ Oldest acked clients: {string.Join(", ", players)}
|
||||
private void SendStateUpdate(int i,
|
||||
PvsThreadResources resources,
|
||||
InputSystem inputSystem,
|
||||
IPlayerSession session,
|
||||
ICommonSession session,
|
||||
PvsData? pvsData,
|
||||
ref uint oldestAckValue)
|
||||
{
|
||||
var channel = session.ConnectedClient;
|
||||
var channel = session.Channel;
|
||||
var sessionData = _pvs.PlayerData[session];
|
||||
var lastAck = sessionData.LastReceivedAck;
|
||||
List<NetEntity>? leftPvs = null;
|
||||
@@ -347,7 +350,7 @@ Oldest acked clients: {string.Join(", ", players)}
|
||||
(entStates, deletions, fromTick) = _pvs.GetAllEntityStates(session, lastAck, _gameTiming.CurTick);
|
||||
}
|
||||
|
||||
var playerStates = _playerManager.GetPlayerStates(lastAck);
|
||||
var playerStates = _playerManager.GetPlayerStates(fromTick);
|
||||
|
||||
// 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,11 +371,25 @@ 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;
|
||||
#if FULL_RELEASE
|
||||
var connectedTime = (DateTime.UtcNow - session.ConnectedTime).TotalMinutes;
|
||||
if (lastAck > GameTick.Zero && connectedTime > 1)
|
||||
_logger.Warning($"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;
|
||||
|
||||
@@ -12,6 +12,7 @@ using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Validation;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.Maps;
|
||||
|
||||
@@ -93,6 +94,7 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
|
||||
IDependencyCollection dependencies, bool alwaysWrite = false,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
DebugTools.Assert(value.FilledTiles > 0, "Attempting to write an empty chunk");
|
||||
var root = new MappingDataNode();
|
||||
var ind = new ValueDataNode($"{value.X},{value.Y}");
|
||||
root.Add("ind", ind);
|
||||
|
||||
@@ -3,7 +3,6 @@ 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;
|
||||
@@ -15,7 +14,7 @@ using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -92,9 +91,7 @@ namespace Robust.Server.Physics
|
||||
|
||||
private void OnDebugRequest(RequestGridNodesMessage msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var pSession = (PlayerSession) args.SenderSession;
|
||||
|
||||
if (!_conGroup.CanCommand(pSession, ShowGridNodesCommand)) return;
|
||||
if (!_conGroup.CanCommand(args.SenderSession, ShowGridNodesCommand)) return;
|
||||
|
||||
AddDebugSubscriber(args.SenderSession);
|
||||
}
|
||||
@@ -260,7 +257,7 @@ namespace Robust.Server.Physics
|
||||
for (var i = 0; i < grids.Count - 1; i++)
|
||||
{
|
||||
var group = grids[i];
|
||||
var newGrid = _mapManager.CreateGrid(mapId);
|
||||
var newGrid = _mapManager.CreateGridEntity(mapId);
|
||||
var newGridUid = newGrid.Owner;
|
||||
var newGridXform = xformQuery.GetComponent(newGridUid);
|
||||
newGrids[i] = newGridUid;
|
||||
@@ -287,7 +284,7 @@ namespace Robust.Server.Physics
|
||||
}
|
||||
}
|
||||
|
||||
newGrid.SetTiles(tileData);
|
||||
newGrid.Comp.SetTiles(tileData);
|
||||
DebugTools.Assert(_mapManager.IsGrid(newGridUid), "A split grid had no tiles?");
|
||||
|
||||
// Set tiles on new grid + update anchored entities
|
||||
|
||||
@@ -15,6 +15,7 @@ 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
|
||||
@@ -30,6 +31,10 @@ namespace Robust.Server.Placement
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
private EntityLookupSystem _lookup => _entityManager.System<EntityLookupSystem>();
|
||||
private SharedMapSystem _maps => _entityManager.System<SharedMapSystem>();
|
||||
private SharedTransformSystem _xformSystem => _entityManager.System<SharedTransformSystem>();
|
||||
|
||||
//TO-DO: Expand for multiple permission per mob?
|
||||
// Add support for multi-use placeables (tiles etc.).
|
||||
public List<PlacementInformation> BuildPermissions { get; set; } = new();
|
||||
@@ -44,6 +49,7 @@ namespace Robust.Server.Placement
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// Someday PlacementManagerSystem my beloved.
|
||||
_sawmill = _logManager.GetSawmill("placement");
|
||||
|
||||
_networkManager.RegisterNetMessage<MsgPlacement>(HandleNetMessage);
|
||||
@@ -142,7 +148,7 @@ namespace Robust.Server.Placement
|
||||
if (_entityManager.TryGetComponent<MapGridComponent>(gridUid, out var grid))
|
||||
{
|
||||
var replacementQuery = _entityManager.GetEntityQuery<PlacementReplacementComponent>();
|
||||
var anc = grid.GetAnchoredEntitiesEnumerator(grid.LocalToTile(coordinates));
|
||||
var anc = _maps.GetAnchoredEntitiesEnumerator(gridUid.Value, grid, _maps.LocalToTile(gridUid.Value, grid, coordinates));
|
||||
var toDelete = new ValueList<EntityUid>();
|
||||
|
||||
while (anc.MoveNext(out var ent))
|
||||
@@ -185,25 +191,22 @@ namespace Robust.Server.Placement
|
||||
|
||||
MapGridComponent? grid;
|
||||
|
||||
_mapManager.TryGetGrid(coordinates.EntityId, out grid);
|
||||
|
||||
if (grid == null)
|
||||
_mapManager.TryFindGridAt(coordinates.ToMap(_entityManager), out _, out grid);
|
||||
|
||||
if (grid != null) // stick to existing grid
|
||||
EntityUid gridId = coordinates.EntityId;
|
||||
if (_entityManager.TryGetComponent(coordinates.EntityId, out grid)
|
||||
|| _mapManager.TryFindGridAt(coordinates.ToMap(_entityManager, _xformSystem), out gridId, out grid))
|
||||
{
|
||||
grid.SetTile(coordinates, new Tile(tileType));
|
||||
_maps.SetTile(gridId, grid, coordinates, new Tile(tileType));
|
||||
|
||||
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
|
||||
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
|
||||
}
|
||||
else if (tileType != 0) // create a new grid
|
||||
{
|
||||
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 newGrid = _mapManager.CreateGridEntity(coordinates.GetMapId(_entityManager));
|
||||
var newGridXform = _entityManager.GetComponent<TransformComponent>(newGrid);
|
||||
_xformSystem.SetWorldPosition(newGridXform, coordinates.Position - newGrid.Comp.TileSizeHalfVector); // assume bottom left tile origin
|
||||
var tilePos = newGrid.Comp.WorldToTile(coordinates.Position);
|
||||
_maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType));
|
||||
|
||||
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
|
||||
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
|
||||
@@ -227,11 +230,16 @@ namespace Robust.Server.Placement
|
||||
{
|
||||
EntityCoordinates start = _entityManager.GetCoordinates(msg.NetCoordinates);
|
||||
Vector2 rectSize = msg.RectSize;
|
||||
foreach (EntityUid entity in EntitySystem.Get<EntityLookupSystem>().GetEntitiesIntersecting(start.GetMapId(_entityManager),
|
||||
foreach (var entity in _lookup.GetEntitiesIntersecting(start.GetMapId(_entityManager),
|
||||
new Box2(start.Position, start.Position + rectSize)))
|
||||
{
|
||||
if (_entityManager.Deleted(entity) || _entityManager.HasComponent<MapGridComponent>(entity) || _entityManager.HasComponent<ActorComponent>(entity))
|
||||
if (_entityManager.Deleted(entity) ||
|
||||
_entityManager.HasComponent<MapGridComponent>(entity) ||
|
||||
_entityManager.HasComponent<ActorComponent>(entity))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var placementEraseEvent = new PlacementEntityEvent(entity, _entityManager.GetComponent<TransformComponent>(entity).Coordinates, PlacementEventAction.Erase, msg.MsgChannel.UserId);
|
||||
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
|
||||
_entityManager.DeleteEntity(entity);
|
||||
@@ -246,16 +254,16 @@ namespace Robust.Server.Placement
|
||||
if (!_entityManager.TryGetComponent<ActorComponent?>(mob, out var actor))
|
||||
return;
|
||||
|
||||
var playerConnection = actor.PlayerSession.ConnectedClient;
|
||||
if (playerConnection == null)
|
||||
return;
|
||||
var playerConnection = actor.PlayerSession.Channel;
|
||||
|
||||
var message = new MsgPlacement();
|
||||
message.PlaceType = PlacementManagerMessage.StartPlacement;
|
||||
message.Range = range;
|
||||
message.IsTile = false;
|
||||
message.ObjType = objectType;
|
||||
message.AlignOption = alignOption;
|
||||
var message = new MsgPlacement
|
||||
{
|
||||
PlaceType = PlacementManagerMessage.StartPlacement,
|
||||
Range = range,
|
||||
IsTile = false,
|
||||
ObjType = objectType,
|
||||
AlignOption = alignOption
|
||||
};
|
||||
_networkManager.ServerSendMessage(message, playerConnection);
|
||||
}
|
||||
|
||||
@@ -267,16 +275,16 @@ namespace Robust.Server.Placement
|
||||
if (!_entityManager.TryGetComponent<ActorComponent?>(mob, out var actor))
|
||||
return;
|
||||
|
||||
var playerConnection = actor.PlayerSession.ConnectedClient;
|
||||
if (playerConnection == null)
|
||||
return;
|
||||
var playerConnection = actor.PlayerSession.Channel;
|
||||
|
||||
var message = new MsgPlacement();
|
||||
message.PlaceType = PlacementManagerMessage.StartPlacement;
|
||||
message.Range = range;
|
||||
message.IsTile = true;
|
||||
message.ObjType = tileType;
|
||||
message.AlignOption = alignOption;
|
||||
var message = new MsgPlacement
|
||||
{
|
||||
PlaceType = PlacementManagerMessage.StartPlacement,
|
||||
Range = range,
|
||||
IsTile = true,
|
||||
ObjType = tileType,
|
||||
AlignOption = alignOption
|
||||
};
|
||||
_networkManager.ServerSendMessage(message, playerConnection);
|
||||
}
|
||||
|
||||
@@ -288,12 +296,12 @@ namespace Robust.Server.Placement
|
||||
if (!_entityManager.TryGetComponent<ActorComponent?>(mob, out var actor))
|
||||
return;
|
||||
|
||||
var playerConnection = actor.PlayerSession.ConnectedClient;
|
||||
if (playerConnection == null)
|
||||
return;
|
||||
var playerConnection = actor.PlayerSession.Channel;
|
||||
|
||||
var message = new MsgPlacement();
|
||||
message.PlaceType = PlacementManagerMessage.CancelPlacement;
|
||||
var message = new MsgPlacement
|
||||
{
|
||||
PlaceType = PlacementManagerMessage.CancelPlacement
|
||||
};
|
||||
_networkManager.ServerSendMessage(message, playerConnection);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user