mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b65e0c64ea | ||
|
|
4c388bc03d | ||
|
|
658dee1591 | ||
|
|
b6e5cca127 | ||
|
|
da5416a2da | ||
|
|
021845d956 | ||
|
|
7fab9f3b8d | ||
|
|
69c1161562 | ||
|
|
095fe9d60f | ||
|
|
14138fbcc2 | ||
|
|
48ce24e98b | ||
|
|
9cde21a7b3 | ||
|
|
ae1051e813 | ||
|
|
a3f80ac7dd | ||
|
|
f98ef78a21 | ||
|
|
bf8054b181 | ||
|
|
6b875e6676 | ||
|
|
a687c0a6c0 | ||
|
|
0580cf3ff7 | ||
|
|
590964d5bf | ||
|
|
ceda39813d | ||
|
|
a3a8912f42 | ||
|
|
b40973157d | ||
|
|
1de8731465 | ||
|
|
3a479cb5f4 | ||
|
|
76eeebf439 | ||
|
|
2fa83181e2 | ||
|
|
36f02b4a18 | ||
|
|
e842142dd7 | ||
|
|
2eb740cea8 | ||
|
|
a044f04e3b | ||
|
|
a4723d1f62 | ||
|
|
627c1eb054 | ||
|
|
836aec0b87 | ||
|
|
9116e64291 | ||
|
|
a6bfb5f557 | ||
|
|
5c83678c78 | ||
|
|
eac94b1032 | ||
|
|
efd870d070 | ||
|
|
94f98073b0 | ||
|
|
5aa9378de0 | ||
|
|
850e9ab695 | ||
|
|
7319f3a241 | ||
|
|
b6252c9e4f | ||
|
|
fcd507d1f9 | ||
|
|
1eb874f4c3 | ||
|
|
a628d31c4b | ||
|
|
2b0ecd7166 | ||
|
|
bde650689b | ||
|
|
87d8d74d8c | ||
|
|
dddf13a19a | ||
|
|
75626a86a3 | ||
|
|
3e3cd0e257 | ||
|
|
a3a90154a4 | ||
|
|
9240c94e59 | ||
|
|
a95ba9f181 | ||
|
|
074a4faa92 | ||
|
|
6b4d74f46e |
@@ -15,10 +15,10 @@
|
||||
<PackageVersion Include="ILReader.Core" Version="1.0.0.4" />
|
||||
<PackageVersion Include="JetBrains.Annotations" Version="2023.3.0" />
|
||||
<PackageVersion Include="JetBrains.Profiler.Api" Version="1.4.0" />
|
||||
<PackageVersion Include="Linguini.Bundle" Version="0.1.3" />
|
||||
<PackageVersion Include="Linguini.Bundle" Version="0.8.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.1"/>
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.1"/>
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.8.0" />
|
||||
@@ -43,7 +43,7 @@
|
||||
<PackageVersion Include="NUnit.Analyzers" Version="3.10.0" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageVersion Include="Nett" Version="0.15.0" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<PackageVersion Include="OpenTK.OpenAL" Version="4.7.7" />
|
||||
<PackageVersion Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" />
|
||||
<PackageVersion Include="Pidgin" Version="3.2.2" />
|
||||
@@ -55,8 +55,8 @@
|
||||
<PackageVersion Include="Serilog" Version="3.1.1" />
|
||||
<PackageVersion Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
|
||||
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.3" />
|
||||
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.0" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
|
||||
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
121
RELEASE-NOTES.md
121
RELEASE-NOTES.md
@@ -54,6 +54,127 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 226.2.2
|
||||
|
||||
|
||||
## 226.2.1
|
||||
|
||||
|
||||
## 226.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* `Control.VisibilityChanged()` virtual function.
|
||||
* Add some System.Random methods for NextFloat and NextPolarVector2.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixes ContainerSystem failing client-side debug asserts when an entity gets unanchored & inserted into a container on the same tick.
|
||||
* Remove potential race condition on server startup from invoking ThreadPool.SetMinThreads.
|
||||
|
||||
### Other
|
||||
|
||||
* Increase default value of res.rsi_atlas_size.
|
||||
* Fix internal networking logic.
|
||||
* Updates of `OutputPanel` contents caused by change in UI scale are now deferred until visible. Especially important to avoid updates from debug console.
|
||||
* Debug console is now limited to only keep `con.max_entries` entries.
|
||||
* Non-existent resources are cached by `IResourceCache.TryGetResource`. This avoids the game constantly trying to re-load non-existent resources in common patterns such as UI theme texture fallbacks.
|
||||
* Default IPv4 MTU has been lowered to 700.
|
||||
* Update Robust.LoaderApi.
|
||||
|
||||
### Internal
|
||||
|
||||
* Split out PVS serialization from compression and sending game states.
|
||||
* Turn broadphase contacts into an IParallelRobustJob and remove unnecessary GetMapEntityIds for every contact.
|
||||
|
||||
|
||||
## 226.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add some GetLocalEntitiesIntersecting methods for `Entity<T>`.
|
||||
|
||||
### Other
|
||||
|
||||
* Fix internal networking logic
|
||||
|
||||
|
||||
## 226.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `IEventBus.RaiseComponentEvent` now requires an EntityUid argument.
|
||||
* The `AddedComponentEventArgs` and `RemovedComponentEventArgs` constructors are now internal
|
||||
|
||||
### New features
|
||||
|
||||
* Allow RequestScreenTexture to be set in overlays.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix AnimationCompletedEvent not always going out.
|
||||
|
||||
|
||||
## 225.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `NetEntity.Parse` and `TryParse` will now fail to parse empty strings.
|
||||
* Try to prevent EventBus looping. This also caps the amount of directed component subscriptions for a particular component to 256.
|
||||
|
||||
### New features
|
||||
|
||||
* `IPrototypeManager.TryIndex` will now default to logging errors if passed an invalid prototype id struct (i,e., `EntProtoId` or `ProtoId<T>`). There is a new optional bool argument to disable logging errors.
|
||||
* `Eye` now allows its `Position` to be set directly. Please only do this with the `FixedEye` child type constructed manually.
|
||||
* Engine now respects the hub's `can_skip_build` parameter on info query, fixing an issue where the first hub advertisement fails due to ACZ taking too long.
|
||||
* Add GetSession & TryGetSession to ActorSystem.
|
||||
* Raise an event when an entity's name is changed.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* The `ent` toolshed command now takes `NetEntity` values, fixing parsing in practical uses.
|
||||
* Fix ComponentFactory test mocks.
|
||||
* Fix LookupFlags missing from a couple of EntityLookupSystem methods.
|
||||
|
||||
### Other
|
||||
|
||||
* Improved engine's Happy Eyeballs implementation, should result in more usage of IPv6 for HTTP APIs when available.
|
||||
* Remove CompIdx locks to improve performance inside Pvs at higher player counts.
|
||||
* Avoid a read lock in GetEntityQuery to also improve performance.
|
||||
* Mark `EntityManager.System<T>` as Pure.
|
||||
|
||||
|
||||
## 224.1.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed UserInterfaceSystem sometimes throwing a key-not-found exception when trying to close UIs.
|
||||
|
||||
|
||||
## 224.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* `ServerIntegrationInstance` has new methods for adding dummy player sessions for tests that require multiple players.
|
||||
* Linguini has been updated to v0.8.1. Errors will now be logged when a duplicate localization key is found.
|
||||
* Added `UserInterfaceSystem.SetUi()` for modifying the `InterfaceData` associated with some BUI.
|
||||
* Added the `EntityPrototypeView` control for spawning & rendering an entity prototype.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix `UserInterfaceSystem` spamming client side errors when entities with UIs open are deleted while outside of PVS range.
|
||||
* Fix Toolshed's EnumTypeParse not working enum values with upercase characters.
|
||||
* Fixed `incmd` command not working due to an invalid cast.
|
||||
|
||||
### Other
|
||||
|
||||
* There have been various performance improvements to replay loading & playback.
|
||||
|
||||
### Internal
|
||||
|
||||
* Added `DummySession` and `DummyChannel` classes for use in integration tests and benchmarks to fool the server into thinking that there are multiple players connected.
|
||||
* Added `ICommonSessionInternal` and updated `CommonSession` so that the internal setters now go through that interface.
|
||||
|
||||
## 224.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
@@ -9,6 +9,7 @@ cmd-parse-failure-float = {$arg} is not a valid float.
|
||||
cmd-parse-failure-bool = {$arg} is not a valid bool.
|
||||
cmd-parse-failure-uid = {$arg} is not a valid entity UID.
|
||||
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
|
||||
cmd-parse-failure-enum = {$arg} is not a {$enum} Enum.
|
||||
cmd-parse-failure-grid = {$arg} is not a valid grid.
|
||||
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
|
||||
cmd-parse-failure-session = There is no session with username: {$username}
|
||||
@@ -252,9 +253,6 @@ cmd-bind-arg-command = <InputCommand>
|
||||
cmd-net-draw-interp-desc = Toggles the debug drawing of the network interpolation.
|
||||
cmd-net-draw-interp-help = Usage: net_draw_interp
|
||||
|
||||
cmd-net-draw-interp-desc = Toggles the debug drawing of the network interpolation.
|
||||
cmd-net-draw-interp-help = Usage: net_draw_interp
|
||||
|
||||
cmd-net-watch-ent-desc = Dumps all network updates for an EntityId to the console.
|
||||
cmd-net-watch-ent-help = Usage: net_watchent <0|EntityUid>
|
||||
|
||||
@@ -306,16 +304,9 @@ cmd-savegrid-help = savegrid <gridID> <Path>
|
||||
cmd-testbed-desc = Loads a physics testbed on the specified map.
|
||||
cmd-testbed-help = testbed <mapid> <test>
|
||||
|
||||
cmd-saveconfig-desc = Saves the client configuration to the config file.
|
||||
cmd-saveconfig-help = saveconfig
|
||||
|
||||
## 'flushcookies' command
|
||||
# Note: the flushcookies command is from Robust.Client.WebView, it's not in the main engine code.
|
||||
|
||||
cmd-flushcookies-desc = Flush CEF cookie storage to disk
|
||||
cmd-flushcookies-help = This ensure cookies are properly saved to disk in the event of unclean shutdowns.
|
||||
Note that the actual operation is asynchronous.
|
||||
|
||||
## 'addcomp' command
|
||||
cmd-addcomp-desc = Adds a component to an entity.
|
||||
cmd-addcomp-help = addcomp <uid> <componentName>
|
||||
@@ -391,9 +382,9 @@ cmd-tp-desc = Teleports a player to any location in the round.
|
||||
cmd-tp-help = tp <x> <y> [<mapID>]
|
||||
|
||||
cmd-tpto-desc = Teleports the current player or the specified players/entities to the location of the first player/entity.
|
||||
cmd-tpto-help = tpto <username|uid> [username|uid]...
|
||||
cmd-tpto-destination-hint = destination (uid or username)
|
||||
cmd-tpto-victim-hint = entity to teleport (uid or username)
|
||||
cmd-tpto-help = tpto <username|uid> [username|NetEntity]...
|
||||
cmd-tpto-destination-hint = destination (NetEntity or username)
|
||||
cmd-tpto-victim-hint = entity to teleport (NetEntity or username)
|
||||
cmd-tpto-parse-error = Cant resolve entity or player: {$str}
|
||||
|
||||
cmd-listplayers-desc = Lists all players currently connected.
|
||||
@@ -453,9 +444,6 @@ cmd-showanchored-help = Usage: showanchored
|
||||
cmd-dmetamem-desc = Dumps a type's members in a format suitable for the sandbox configuration file.
|
||||
cmd-dmetamem-help = Usage: dmetamem <type>
|
||||
|
||||
cmd-dmetamem-desc = Displays chunk bounds for the purposes of rendering.
|
||||
cmd-dmetamem-help = Usage: showchunkbb <type>
|
||||
|
||||
cmd-launchauth-desc = Load authentication tokens from launcher data to aid in testing of live servers.
|
||||
cmd-launchauth-help = Usage: launchauth <account name>
|
||||
|
||||
@@ -522,9 +510,6 @@ cmd-profsnap-help = Usage: profsnap
|
||||
cmd-devwindow-desc = Dev Window
|
||||
cmd-devwindow-help = Usage: devwindow
|
||||
|
||||
cmd-devwindow-desc = Open file
|
||||
cmd-devwindow-help = Usage: testopenfile
|
||||
|
||||
cmd-scene-desc = Immediately changes the UI scene/state.
|
||||
cmd-scene-help = Usage: scene <className>
|
||||
|
||||
@@ -535,14 +520,11 @@ cmd-hwid-desc = Returns the current HWID (HardWare ID).
|
||||
cmd-hwid-help = Usage: hwid
|
||||
|
||||
cmd-vvread-desc = Retrieve a path's value using VV (View Variables).
|
||||
cmd-vvread-desc = Usage: vvread <path>
|
||||
cmd-vvread-help = Usage: vvread <path>
|
||||
|
||||
cmd-vvwrite-desc = Modify a path's value using VV (View Variables).
|
||||
cmd-vvwrite-help = Usage: vvwrite <path>
|
||||
|
||||
cmd-vv-desc = Opens View Variables (VV).
|
||||
cmd-vv-help = Usage: vv <path|entity ID|guihover>
|
||||
|
||||
cmd-vvinvoke-desc = Invoke/Call a path with arguments using VV.
|
||||
cmd-vvinvoke-help = Usage: vvinvoke <path> [arguments...]
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ public sealed class DefaultSQLConfig : IConfig
|
||||
|
||||
public IEnumerable<IExporter> GetExporters()
|
||||
{
|
||||
yield return SQLExporter.Default;
|
||||
//yield return SQLExporter.Default;
|
||||
yield break;
|
||||
}
|
||||
|
||||
public IEnumerable<IColumnProvider> GetColumnProviders() => DefaultConfig.Instance.GetColumnProviders();
|
||||
|
||||
@@ -15,11 +15,10 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Npgsql;
|
||||
using Npgsql.Internal;
|
||||
using Npgsql.Internal.TypeHandlers;
|
||||
using Npgsql.Internal.TypeHandling;
|
||||
|
||||
namespace Robust.Benchmarks.Exporters;
|
||||
|
||||
/*
|
||||
public sealed class SQLExporter : IExporter
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions
|
||||
@@ -98,7 +97,9 @@ public sealed class SQLExporter : IExporter
|
||||
|
||||
public string Name => "sql";
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
// https://github.com/npgsql/efcore.pg/issues/1107#issuecomment-945126627
|
||||
class JsonOverrideTypeHandlerResolverFactory : TypeHandlerResolverFactory
|
||||
{
|
||||
@@ -138,6 +139,7 @@ class JsonOverrideTypeHandlerResolverFactory : TypeHandlerResolverFactory
|
||||
=> null; // Let the built-in resolver do this
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
public sealed class DesignTimeContextFactoryPostgres : IDesignTimeDbContextFactory<BenchmarkContext>
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Robust.Client.Console.Commands
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var type = Type.GetType(args[0]);
|
||||
var type = GetType(args[0]);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
@@ -25,6 +25,17 @@ namespace Robust.Client.Console.Commands
|
||||
shell.WriteLine(sig);
|
||||
}
|
||||
}
|
||||
|
||||
private Type? GetType(string name)
|
||||
{
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (assembly.GetType(name) is { } type)
|
||||
return type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1379,7 +1379,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
|
||||
var ev = new QueueSpriteTreeUpdateEvent(entities.GetComponent<TransformComponent>(Owner));
|
||||
entities.EventBus.RaiseComponentEvent(this, ref ev);
|
||||
entities.EventBus.RaiseComponentEvent(Owner, this, ref ev);
|
||||
}
|
||||
|
||||
private void QueueUpdateIsInert()
|
||||
@@ -1389,7 +1389,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
|
||||
var ev = new SpriteUpdateInertEvent();
|
||||
entities.EventBus.RaiseComponentEvent(this, ref ev);
|
||||
entities.EventBus.RaiseComponentEvent(Owner, this, ref ev);
|
||||
}
|
||||
|
||||
[Obsolete("Use SpriteSystem instead.")]
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
private readonly List<Entity<AnimationPlayerComponent>> _activeAnimations = new();
|
||||
|
||||
private EntityQuery<AnimationPlayerComponent> _playerQuery;
|
||||
private EntityQuery<MetaDataComponent> _metaQuery;
|
||||
|
||||
[Dependency] private readonly IComponentFactory _compFact = default!;
|
||||
@@ -18,6 +19,7 @@ namespace Robust.Client.GameObjects
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_playerQuery = GetEntityQuery<AnimationPlayerComponent>();
|
||||
_metaQuery = GetEntityQuery<MetaDataComponent>();
|
||||
}
|
||||
|
||||
@@ -171,28 +173,32 @@ namespace Robust.Client.GameObjects
|
||||
return component.PlayingAnimations.ContainsKey(key);
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
public void Stop(AnimationPlayerComponent component, string key)
|
||||
{
|
||||
component.PlayingAnimations.Remove(key);
|
||||
Stop((component.Owner, component), key);
|
||||
}
|
||||
|
||||
public void Stop(EntityUid uid, string key)
|
||||
public void Stop(Entity<AnimationPlayerComponent?> entity, string key)
|
||||
{
|
||||
if (!TryComp<AnimationPlayerComponent>(uid, out var player))
|
||||
if (!_playerQuery.Resolve(entity.Owner, ref entity.Comp, false) ||
|
||||
!entity.Comp.PlayingAnimations.Remove(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
player.PlayingAnimations.Remove(key);
|
||||
EntityManager.EventBus.RaiseLocalEvent(entity.Owner, new AnimationCompletedEvent {Uid = entity.Owner, Key = key}, true);
|
||||
}
|
||||
|
||||
public void Stop(EntityUid uid, AnimationPlayerComponent? component, string key)
|
||||
{
|
||||
if (!Resolve(uid, ref component, false))
|
||||
return;
|
||||
|
||||
component.PlayingAnimations.Remove(key);
|
||||
Stop((uid, component), key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised whenever an animation stops, either due to running its course or being stopped manually.
|
||||
/// </summary>
|
||||
public sealed class AnimationCompletedEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Uid { get; init; }
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace Robust.Client.GameObjects
|
||||
/// </summary>
|
||||
public sealed class InputSystem : SharedInputSystem, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClientGameStateManager _stateManager = default!;
|
||||
@@ -82,18 +83,35 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
// send it off to the server
|
||||
var clientMsg = (ClientFullInputCmdMessage)message;
|
||||
var fullMsg = new FullInputCmdMessage(
|
||||
clientMsg.Tick,
|
||||
clientMsg.SubTick,
|
||||
(int)clientMsg.InputSequence,
|
||||
clientMsg.InputFunctionId,
|
||||
clientMsg.State,
|
||||
GetNetCoordinates(clientMsg.Coordinates),
|
||||
clientMsg.ScreenCoordinates)
|
||||
var clientMsg = message switch
|
||||
{
|
||||
Uid = GetNetEntity(clientMsg.Uid)
|
||||
ClientFullInputCmdMessage clientInput => clientInput,
|
||||
FullInputCmdMessage fullInput => new ClientFullInputCmdMessage(
|
||||
fullInput.Tick,
|
||||
fullInput.SubTick,
|
||||
fullInput.InputFunctionId,
|
||||
GetCoordinates(fullInput.Coordinates),
|
||||
fullInput.ScreenCoordinates,
|
||||
fullInput.State,
|
||||
GetEntity(fullInput.Uid)),
|
||||
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
var fullMsg = message switch
|
||||
{
|
||||
FullInputCmdMessage fullInput => fullInput,
|
||||
ClientFullInputCmdMessage client => new FullInputCmdMessage(
|
||||
client.Tick,
|
||||
client.SubTick,
|
||||
client.InputFunctionId,
|
||||
clientMsg.State,
|
||||
GetNetCoordinates(client.Coordinates),
|
||||
clientMsg.ScreenCoordinates,
|
||||
GetNetEntity(clientMsg.Uid)
|
||||
),
|
||||
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
DispatchInputCommand(clientMsg, fullMsg);
|
||||
@@ -131,7 +149,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
_conHost.RegisterCommand("incmd",
|
||||
"Inserts an input command into the simulation",
|
||||
"incmd <KeyFunction> <d|u KeyState> <wxPos> <wyPos>",
|
||||
"incmd <KeyFunction> <KeyState> [wxPos] [wyPos]",
|
||||
GenerateInputCommand);
|
||||
}
|
||||
|
||||
@@ -147,17 +165,47 @@ namespace Robust.Client.GameObjects
|
||||
if (_playerManager.LocalEntity is not { } pent)
|
||||
return;
|
||||
|
||||
BoundKeyFunction keyFunction = new BoundKeyFunction(args[0]);
|
||||
BoundKeyState state = args[1] == "u" ? BoundKeyState.Up: BoundKeyState.Down;
|
||||
if (args.Length is not (2 or 4))
|
||||
{
|
||||
shell.WriteLine(Loc.GetString($"cmd-invalid-arg-number-error"));
|
||||
return;
|
||||
}
|
||||
|
||||
var pxform = Transform(pent);
|
||||
var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3]));
|
||||
var coords = EntityCoordinates.FromMap(pent, new MapCoordinates(wPos, pxform.MapID), _transform, EntityManager);
|
||||
var keyFunction = new BoundKeyFunction(args[0]);
|
||||
if (!Enum.TryParse<BoundKeyState>(args[1], out var state))
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-parse-failure-enum", ("arg", args[1]), ("enum", nameof(BoundKeyState))));
|
||||
return;
|
||||
}
|
||||
|
||||
var wOffset = Vector2.Zero;
|
||||
if (args.Length == 4)
|
||||
{
|
||||
if (!float.TryParse(args[2], out var wX))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[2])));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!float.TryParse(args[3], out var wY))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[3])));
|
||||
return;
|
||||
}
|
||||
|
||||
wOffset = new Vector2(wX, wY);
|
||||
}
|
||||
|
||||
var coords = EntityCoordinates.FromMap(pent, _transform.GetMapCoordinates(pent).Offset(wOffset), _transform, EntityManager);
|
||||
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);
|
||||
|
||||
var message = new FullInputCmdMessage(_timing.CurTick, _timing.TickFraction, funcId, state,
|
||||
GetNetCoordinates(coords), new ScreenCoordinates(0, 0, default), NetEntity.Invalid);
|
||||
var message = new ClientFullInputCmdMessage(_timing.CurTick,
|
||||
_timing.TickFraction,
|
||||
funcId,
|
||||
coords,
|
||||
new ScreenCoordinates(0, 0, default),
|
||||
state,
|
||||
EntityUid.Invalid);
|
||||
|
||||
HandleInputCommand(_playerManager.LocalSession, keyFunction, message);
|
||||
}
|
||||
|
||||
@@ -603,7 +603,7 @@ namespace Robust.Client.GameStates
|
||||
if (compState != null)
|
||||
{
|
||||
var handleState = new ComponentHandleState(compState, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
|
||||
_entities.EventBus.RaiseComponentEvent(entity, comp, ref handleState);
|
||||
}
|
||||
|
||||
comp.LastModifiedTick = _timing.LastRealTick;
|
||||
@@ -640,7 +640,7 @@ namespace Robust.Client.GameStates
|
||||
if (state != null)
|
||||
{
|
||||
var stateEv = new ComponentHandleState(state, null);
|
||||
_entities.EventBus.RaiseComponentEvent(comp, ref stateEv);
|
||||
_entities.EventBus.RaiseComponentEvent(entity, comp, ref stateEv);
|
||||
}
|
||||
|
||||
comp.ClearCreationTick(); // don't undo the re-adding.
|
||||
@@ -1361,7 +1361,7 @@ namespace Robust.Client.GameStates
|
||||
continue;
|
||||
|
||||
var handleState = new ComponentHandleState(cur, next);
|
||||
bus.RaiseComponentEvent(comp, ref handleState);
|
||||
bus.RaiseComponentEvent(uid, comp, ref handleState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1516,7 +1516,7 @@ namespace Robust.Client.GameStates
|
||||
continue;
|
||||
|
||||
var handleState = new ComponentHandleState(state, null);
|
||||
_entityManager.EventBus.RaiseComponentEvent(comp, ref handleState);
|
||||
_entityManager.EventBus.RaiseComponentEvent(uid, comp, ref handleState);
|
||||
}
|
||||
|
||||
// ensure we don't have any extra components
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Robust.Client.Graphics
|
||||
/// If set to true, <see cref="ScreenTexture"/> will be set to the current frame (at the moment before the overlay is rendered). This can be costly to performance, but
|
||||
/// some shaders will require it as a passed in uniform to operate.
|
||||
/// </summary>
|
||||
public virtual bool RequestScreenTexture => false;
|
||||
public virtual bool RequestScreenTexture { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="RequestScreenTexture"> is true, then this will be set to the texture corresponding to the current frame. If false, it will always be null.
|
||||
|
||||
@@ -261,8 +261,8 @@ namespace Robust.Client.Player
|
||||
{
|
||||
// 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;
|
||||
var newSession = (ICommonSessionInternal)CreateAndAddSession(state.UserId, state.Name);
|
||||
newSession.SetPing(state.Ping);
|
||||
SetStatus(newSession, state.Status);
|
||||
SetAttachedEntity(newSession, controlled, out _, true);
|
||||
dirty = true;
|
||||
@@ -279,9 +279,9 @@ namespace Robust.Client.Player
|
||||
}
|
||||
|
||||
dirty = true;
|
||||
var local = (CommonSession) session;
|
||||
local.Name = state.Name;
|
||||
local.Ping = state.Ping;
|
||||
var local = (ICommonSessionInternal)session;
|
||||
local.SetName(state.Name);
|
||||
local.SetPing(state.Ping);
|
||||
SetStatus(local, state.Status);
|
||||
SetAttachedEntity(local, controlled, out _, true);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,31 @@ namespace Robust.Client.Replays.Loading;
|
||||
// so that when jumping to tick 1001 the client only has to apply states for tick 1000 and 1001, instead of 0, 1, 2, ...
|
||||
public sealed partial class ReplayLoadManager
|
||||
{
|
||||
// Scratch data used by UpdateEntityStates.
|
||||
// Avoids copying changes for every change to an entity between checkpoints, instead copies once per checkpoint on
|
||||
// first change. We can also use this to avoid building a dictionary of ComponentChange inside the inner loop.
|
||||
private class UpdateScratchData
|
||||
{
|
||||
public Dictionary<ushort, ComponentChange> Changes;
|
||||
public EntityState lastChange;
|
||||
public HashSet<ushort>? netComps;
|
||||
|
||||
public UpdateScratchData(EntityState oldEntState)
|
||||
{
|
||||
Changes = oldEntState.ComponentChanges.Value.ToDictionary(x => x.NetID);
|
||||
lastChange = oldEntState;
|
||||
netComps = oldEntState.NetComponents;
|
||||
}
|
||||
|
||||
public EntityState BakeChanges()
|
||||
{
|
||||
return new EntityState(lastChange.NetEntity,
|
||||
Changes.Values.ToList(),
|
||||
lastChange.EntityLastModified,
|
||||
netComps);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(CheckpointState[], TimeSpan[])> GenerateCheckpointsAsync(
|
||||
ReplayMessage? initMessages,
|
||||
HashSet<string> initialCvars,
|
||||
@@ -138,6 +163,7 @@ public sealed partial class ReplayLoadManager
|
||||
var stats_due_spawned = 0;
|
||||
var stats_due_state = 0;
|
||||
|
||||
var modifiedEntities = new Dictionary<NetEntity, UpdateScratchData>();
|
||||
for (var i = 1; i < states.Count; i++)
|
||||
{
|
||||
if (i % 10 == 0)
|
||||
@@ -148,10 +174,10 @@ public sealed partial class ReplayLoadManager
|
||||
DebugTools.Assert(curState.FromSequence <= lastState.ToSequence);
|
||||
|
||||
UpdatePlayerStates(curState.PlayerStates.Span, playerStates);
|
||||
UpdateEntityStates(curState.EntityStates.Span, entStates, ref spawnedTracker, ref stateTracker, detached);
|
||||
UpdateEntityStates(curState.EntityStates.Span, entStates, modifiedEntities, ref spawnedTracker, ref stateTracker, detached);
|
||||
UpdateMessages(messages[i], uploadedFiles, prototypes, cvars, detachQueue, ref timeBase);
|
||||
ProcessQueue(curState.ToSequence, detachQueue, detached, entStates);
|
||||
UpdateDeletions(curState.EntityDeletions, entStates, detached);
|
||||
UpdateDeletions(curState.EntityDeletions, entStates, detached, modifiedEntities);
|
||||
serverTime[i] = GetTime(curState.ToSequence) - initialTime;
|
||||
ticksSinceLastCheckpoint++;
|
||||
|
||||
@@ -182,6 +208,8 @@ public sealed partial class ReplayLoadManager
|
||||
ticksSinceLastCheckpoint = 0;
|
||||
spawnedTracker = 0;
|
||||
stateTracker = 0;
|
||||
ApplyModifiedEntities(entStates, modifiedEntities);
|
||||
|
||||
var newState = new GameState(GameTick.Zero,
|
||||
curState.ToSequence,
|
||||
default,
|
||||
@@ -339,16 +367,18 @@ public sealed partial class ReplayLoadManager
|
||||
}
|
||||
|
||||
private void UpdateDeletions(NetListAsArray<NetEntity> entityDeletions,
|
||||
Dictionary<NetEntity, EntityState> entStates, HashSet<NetEntity> detached)
|
||||
Dictionary<NetEntity, EntityState> entStates, HashSet<NetEntity> detached, Dictionary<NetEntity, UpdateScratchData> modifiedEntities)
|
||||
{
|
||||
foreach (var ent in entityDeletions.Span)
|
||||
{
|
||||
entStates.Remove(ent);
|
||||
detached.Remove(ent);
|
||||
modifiedEntities.Remove(ent);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateEntityStates(ReadOnlySpan<EntityState> span, Dictionary<NetEntity, EntityState> entStates,
|
||||
Dictionary<NetEntity, UpdateScratchData> modified,
|
||||
ref int spawnedTracker, ref int stateTracker, HashSet<NetEntity> detached)
|
||||
{
|
||||
foreach (var entState in span)
|
||||
@@ -369,9 +399,22 @@ public sealed partial class ReplayLoadManager
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get scratch versions (with write access) for entities modified since last checkpoint
|
||||
UpdateScratchData? scratch;
|
||||
if (!modified.TryGetValue(entState.NetEntity, out scratch))
|
||||
{
|
||||
scratch = new UpdateScratchData(oldEntState);
|
||||
modified[entState.NetEntity] = scratch;
|
||||
}
|
||||
|
||||
stateTracker++;
|
||||
DebugTools.Assert(oldEntState.NetEntity == entState.NetEntity);
|
||||
entStates[entState.NetEntity] = MergeStates(entState, oldEntState.ComponentChanges.Value, oldEntState.NetComponents);
|
||||
// Note this does not change entStates, that change occurs later in ApplyModifiedEntities (to avoid early copies)
|
||||
UpdateScratch(entState, scratch.Changes);
|
||||
if (entState.NetComponents != null)
|
||||
scratch.netComps = entState.NetComponents;
|
||||
scratch.lastChange = entState;
|
||||
|
||||
|
||||
#if DEBUG
|
||||
foreach (var state in entStates[entState.NetEntity].ComponentChanges.Span)
|
||||
@@ -382,6 +425,53 @@ public sealed partial class ReplayLoadManager
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyModifiedEntities(Dictionary<NetEntity, EntityState> entStates, Dictionary<NetEntity, UpdateScratchData> modifiedEntities)
|
||||
{
|
||||
foreach (var modified in modifiedEntities)
|
||||
{
|
||||
entStates[modified.Key] = modified.Value.BakeChanges();
|
||||
}
|
||||
|
||||
modifiedEntities.Clear();
|
||||
}
|
||||
|
||||
private void UpdateScratch(
|
||||
EntityState newState,
|
||||
Dictionary<ushort, ComponentChange> oldState)
|
||||
{
|
||||
// remove any deleted components
|
||||
if (newState.NetComponents != null)
|
||||
{
|
||||
foreach (var change in oldState.Values)
|
||||
{
|
||||
if (!newState.NetComponents.Contains(change.NetID))
|
||||
oldState.Remove(change.NetID);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var newCompState in newState.ComponentChanges.Value)
|
||||
{
|
||||
if (!oldState.TryGetValue(newCompState.NetID, out var existing))
|
||||
{
|
||||
// This is a new component
|
||||
// I'm not 100% sure about this, but I think delta states should always be full states here?
|
||||
DebugTools.Assert(newCompState.State is not IComponentDeltaState newDelta);
|
||||
oldState[newCompState.NetID] = newCompState;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Modify or replace existing component
|
||||
if (newCompState.State is not IComponentDeltaState delta)
|
||||
{
|
||||
oldState[newCompState.NetID] = newCompState;
|
||||
continue;
|
||||
}
|
||||
|
||||
DebugTools.Assert(existing.State != null && existing.State is not IComponentDeltaState);
|
||||
oldState[newCompState.NetID] = new ComponentChange(existing.NetID, delta.CreateNewFullState(existing.State!), newCompState.LastModifiedTick);
|
||||
}
|
||||
}
|
||||
|
||||
private EntityState MergeStates(
|
||||
EntityState newState,
|
||||
IReadOnlyCollection<ComponentChange> oldState,
|
||||
@@ -420,7 +510,7 @@ public sealed partial class ReplayLoadManager
|
||||
foreach (var compChange in newCompStates.Values)
|
||||
{
|
||||
// I'm not 100% sure about this, but I think delta states should always be full states here?
|
||||
DebugTools.Assert(compChange.State is not IComponentDeltaState delta);
|
||||
DebugTools.Assert(compChange.State is not IComponentDeltaState);
|
||||
combined.Add(compChange);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ internal sealed partial class ReplayPlaybackManager
|
||||
/// </summary>
|
||||
/// <param name="index">The target tick/index. The actual checkpoint will have an index less than or equal to this.</param>
|
||||
/// <param name="flushEntities">Whether to delete all entities</param>
|
||||
public void ResetToNearestCheckpoint(int index, bool flushEntities)
|
||||
public void ResetToNearestCheckpoint(int index, bool flushEntities, CheckpointState? checkpoint = null)
|
||||
{
|
||||
if (Replay == null)
|
||||
throw new Exception("Not currently playing a replay");
|
||||
@@ -25,7 +25,8 @@ internal sealed partial class ReplayPlaybackManager
|
||||
if (flushEntities)
|
||||
_entMan.FlushEntities();
|
||||
|
||||
var checkpoint = GetLastCheckpoint(Replay, index);
|
||||
// Look up the desired checkpoint, unless our caller kindly provided one to us.
|
||||
checkpoint ??= GetLastCheckpoint(Replay, index);
|
||||
|
||||
_sawmill.Info($"Resetting to checkpoint. From {Replay.CurrentIndex} to {checkpoint.Index}");
|
||||
var st = new Stopwatch();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Replays.Playback;
|
||||
@@ -27,14 +28,19 @@ internal sealed partial class ReplayPlaybackManager
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Playing &= !pausePlayback;
|
||||
value = Math.Clamp(value, 0, Replay.States.Count - 1);
|
||||
if (value == Replay.CurrentIndex)
|
||||
{
|
||||
ScrubbingTarget = null;
|
||||
return;
|
||||
}
|
||||
|
||||
BeforeSetTick?.Invoke();
|
||||
|
||||
// Begin timing replay processing so we can abort when we run out of time (_replayMaxScrubTime)
|
||||
var st = RStopwatch.StartNew();
|
||||
|
||||
bool skipEffectEvents = value > Replay.CurrentIndex + _visualEventThreshold;
|
||||
if (value < Replay.CurrentIndex)
|
||||
{
|
||||
@@ -45,9 +51,12 @@ internal sealed partial class ReplayPlaybackManager
|
||||
{
|
||||
// If we are skipping many ticks into the future, we try to skip directly to a checkpoint instead of
|
||||
// applying every tick.
|
||||
var nextCheckpoint = GetNextCheckpoint(Replay, Replay.CurrentIndex);
|
||||
if (nextCheckpoint.Index < value && nextCheckpoint.Index > Replay.CurrentIndex)
|
||||
ResetToNearestCheckpoint(value, false);
|
||||
|
||||
var nextCheckpoint = GetLastCheckpoint(Replay, value);
|
||||
// Sanity-Check that the checkpoint is actually BEFORE the desired position.
|
||||
// Also check that it gets us closer to goal position than we already are.
|
||||
if (nextCheckpoint.Index <= value && nextCheckpoint.Index > Replay.CurrentIndex)
|
||||
ResetToNearestCheckpoint(value, false, nextCheckpoint);
|
||||
}
|
||||
|
||||
_entMan.EntitySysManager.GetEntitySystem<ClientDirtySystem>().Reset();
|
||||
@@ -75,6 +84,23 @@ internal sealed partial class ReplayPlaybackManager
|
||||
DebugTools.Assert(Replay.LastApplied >= state.FromSequence);
|
||||
DebugTools.Assert(Replay.LastApplied + 1 <= state.ToSequence);
|
||||
Replay.LastApplied = state.ToSequence;
|
||||
|
||||
if (st.Elapsed.TotalMilliseconds > _replayMaxScrubTime)
|
||||
{
|
||||
// Out of time to advance replay this tick
|
||||
// Note: We check at end of loop so we always advance at least 1 tick.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Use ScrubbingTarget to force a later invocation to continue moving towards the target tick
|
||||
if (Replay.CurrentIndex < value)
|
||||
{
|
||||
ScrubbingTarget = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
ScrubbingTarget = null;
|
||||
}
|
||||
|
||||
AfterSetTick?.Invoke();
|
||||
|
||||
@@ -52,6 +52,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
|
||||
public ReplayData? Replay { get; private set; }
|
||||
public NetUserId? Recorder => Replay?.Recorder;
|
||||
private int _checkpointMinInterval;
|
||||
private int _replayMaxScrubTime;
|
||||
private int _visualEventThreshold;
|
||||
public uint? AutoPauseCountdown { get; set; }
|
||||
public int? ScrubbingTarget { get; set; }
|
||||
@@ -94,6 +95,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
|
||||
_sawmill = _logMan.GetSawmill("replay");
|
||||
_metaId = _factory.GetRegistration(typeof(MetaDataComponent)).NetID!.Value;
|
||||
_confMan.OnValueChanged(CVars.CheckpointMinInterval, (value) => _checkpointMinInterval = value, true);
|
||||
_confMan.OnValueChanged(CVars.ReplayMaxScrubTime, (value) => _replayMaxScrubTime = value, true);
|
||||
_confMan.OnValueChanged(CVars.ReplaySkipThreshold, (value) => _visualEventThreshold = value, true);
|
||||
_client.RunLevelChanged += OnRunLevelChanged;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
sawmill.Debug("Preloading textures...");
|
||||
var sw = Stopwatch.StartNew();
|
||||
var resList = GetTypeDict<TextureResource>();
|
||||
var resList = GetTypeData<TextureResource>().Resources;
|
||||
|
||||
var texList = _manager.ContentFindFiles("/Textures/")
|
||||
// Skip PNG files inside RSIs.
|
||||
@@ -119,7 +119,7 @@ namespace Robust.Client.ResourceManagement
|
||||
private void PreloadRsis(ISawmill sawmill)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var resList = GetTypeDict<RSIResource>();
|
||||
var resList = GetTypeData<RSIResource>().Resources;
|
||||
|
||||
var rsiList = _manager.ContentFindFiles("/Textures/")
|
||||
.Where(p => p.ToString().EndsWith(".rsi/meta.json"))
|
||||
|
||||
@@ -17,9 +17,7 @@ namespace Robust.Client.ResourceManagement;
|
||||
/// </summary>
|
||||
internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
|
||||
{
|
||||
private readonly Dictionary<Type, Dictionary<ResPath, BaseResource>> _cachedResources =
|
||||
new();
|
||||
|
||||
private readonly Dictionary<Type, TypeData> _cachedResources = new();
|
||||
private readonly Dictionary<Type, BaseResource> _fallbacks = new();
|
||||
|
||||
public T GetResource<T>(string path, bool useFallback = true) where T : BaseResource, new()
|
||||
@@ -29,8 +27,8 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
|
||||
|
||||
public T GetResource<T>(ResPath path, bool useFallback = true) where T : BaseResource, new()
|
||||
{
|
||||
var cache = GetTypeDict<T>();
|
||||
if (cache.TryGetValue(path, out var cached))
|
||||
var cache = GetTypeData<T>();
|
||||
if (cache.Resources.TryGetValue(path, out var cached))
|
||||
{
|
||||
return (T) cached;
|
||||
}
|
||||
@@ -40,7 +38,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
|
||||
{
|
||||
var dependencies = IoCManager.Instance!;
|
||||
resource.Load(dependencies, path);
|
||||
cache[path] = resource;
|
||||
cache.Resources[path] = resource;
|
||||
return resource;
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -67,24 +65,31 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
|
||||
|
||||
public bool TryGetResource<T>(ResPath path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
|
||||
{
|
||||
var cache = GetTypeDict<T>();
|
||||
if (cache.TryGetValue(path, out var cached))
|
||||
var cache = GetTypeData<T>();
|
||||
if (cache.Resources.TryGetValue(path, out var cached))
|
||||
{
|
||||
resource = (T) cached;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cache.NonExistent.Contains(path))
|
||||
{
|
||||
resource = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var _resource = new T();
|
||||
try
|
||||
{
|
||||
var dependencies = IoCManager.Instance!;
|
||||
_resource.Load(dependencies, path);
|
||||
resource = _resource;
|
||||
cache[path] = resource;
|
||||
cache.Resources[path] = resource;
|
||||
return true;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
cache.NonExistent.Add(path);
|
||||
resource = null;
|
||||
return false;
|
||||
}
|
||||
@@ -109,9 +114,9 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
|
||||
|
||||
public void ReloadResource<T>(ResPath path) where T : BaseResource, new()
|
||||
{
|
||||
var cache = GetTypeDict<T>();
|
||||
var cache = GetTypeData<T>();
|
||||
|
||||
if (!cache.TryGetValue(path, out var res))
|
||||
if (!cache.Resources.TryGetValue(path, out var res))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -145,7 +150,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
|
||||
|
||||
public void CacheResource<T>(ResPath path, T resource) where T : BaseResource, new()
|
||||
{
|
||||
GetTypeDict<T>()[path] = resource;
|
||||
GetTypeData<T>().Resources[path] = resource;
|
||||
}
|
||||
|
||||
public T GetFallback<T>() where T : BaseResource, new()
|
||||
@@ -168,7 +173,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
|
||||
|
||||
public IEnumerable<KeyValuePair<ResPath, T>> GetAllResources<T>() where T : BaseResource, new()
|
||||
{
|
||||
return GetTypeDict<T>().Select(p => new KeyValuePair<ResPath, T>(p.Key, (T) p.Value));
|
||||
return GetTypeData<T>().Resources.Select(p => new KeyValuePair<ResPath, T>(p.Key, (T) p.Value));
|
||||
}
|
||||
|
||||
public event Action<TextureLoadedEventArgs>? OnRawTextureLoaded;
|
||||
@@ -193,7 +198,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Values))
|
||||
foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Resources.Values))
|
||||
{
|
||||
res.Dispose();
|
||||
}
|
||||
@@ -210,15 +215,9 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
|
||||
#endregion IDisposable Members
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected Dictionary<ResPath, BaseResource> GetTypeDict<T>()
|
||||
private TypeData GetTypeData<T>()
|
||||
{
|
||||
if (!_cachedResources.TryGetValue(typeof(T), out var ret))
|
||||
{
|
||||
ret = new Dictionary<ResPath, BaseResource>();
|
||||
_cachedResources.Add(typeof(T), ret);
|
||||
}
|
||||
|
||||
return ret;
|
||||
return _cachedResources.GetOrNew(typeof(T));
|
||||
}
|
||||
|
||||
public void TextureLoaded(TextureLoadedEventArgs eventArgs)
|
||||
@@ -230,4 +229,13 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
|
||||
{
|
||||
OnRsiLoaded?.Invoke(eventArgs);
|
||||
}
|
||||
|
||||
private sealed class TypeData
|
||||
{
|
||||
public readonly Dictionary<ResPath, BaseResource> Resources = new();
|
||||
|
||||
// List of resources which DON'T exist.
|
||||
// Needed to avoid innocuous TryGet calls repeatedly trying to re-load non-existent resources from disk.
|
||||
public readonly HashSet<ResPath> NonExistent = new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,9 +212,18 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when this control's visibility in the control tree changed.
|
||||
/// </summary>
|
||||
protected virtual void VisibilityChanged(bool newVisible)
|
||||
{
|
||||
}
|
||||
|
||||
private void _propagateVisibilityChanged(bool newVisible)
|
||||
{
|
||||
VisibilityChanged(newVisible);
|
||||
OnVisibilityChanged?.Invoke(this);
|
||||
|
||||
if (!VisibleInTree)
|
||||
{
|
||||
UserInterfaceManagerInternal.ControlHidden(this);
|
||||
|
||||
64
Robust.Client/UserInterface/Controls/EntityPrototypeView.cs
Normal file
64
Robust.Client/UserInterface/Controls/EntityPrototypeView.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controls;
|
||||
|
||||
[Virtual]
|
||||
public class EntityPrototypeView : SpriteView
|
||||
{
|
||||
private string? _currentPrototype;
|
||||
private EntityUid? _ourEntity;
|
||||
|
||||
public EntityPrototypeView()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public EntityPrototypeView(EntProtoId? entProto, IEntityManager entMan) : base(entMan)
|
||||
{
|
||||
SetPrototype(entProto);
|
||||
}
|
||||
|
||||
public void SetPrototype(EntProtoId? entProto)
|
||||
{
|
||||
SpriteSystem ??= EntMan.System<SpriteSystem>();
|
||||
|
||||
if (entProto == _currentPrototype
|
||||
&& EntMan.TryGetComponent(Entity?.Owner, out MetaDataComponent? meta)
|
||||
&& meta.EntityPrototype?.ID == _currentPrototype)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_currentPrototype = entProto;
|
||||
SetEntity(null);
|
||||
if (_ourEntity != null)
|
||||
{
|
||||
EntMan.DeleteEntity(_ourEntity);
|
||||
}
|
||||
|
||||
if (_currentPrototype != null)
|
||||
{
|
||||
_ourEntity = EntMan.Spawn(_currentPrototype);
|
||||
SpriteSystem.ForceUpdate(_ourEntity.Value);
|
||||
SetEntity(_ourEntity);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
if (_currentPrototype != null)
|
||||
SetPrototype(_currentPrototype);
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
if (!EntMan.Deleted(_ourEntity))
|
||||
EntMan.QueueDeleteEntity(_ourEntity);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -20,7 +19,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public const string StylePropertyStyleBox = "stylebox";
|
||||
|
||||
private readonly List<RichTextEntry> _entries = new();
|
||||
private readonly RingBufferList<RichTextEntry> _entries = new();
|
||||
private bool _isAtBottom = true;
|
||||
|
||||
private int _totalContentHeight;
|
||||
@@ -30,6 +29,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public bool ScrollFollowing { get; set; } = true;
|
||||
|
||||
private bool _invalidOnVisible;
|
||||
|
||||
public OutputPanel()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
@@ -45,6 +46,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
|
||||
}
|
||||
|
||||
public int EntryCount => _entries.Count;
|
||||
|
||||
public StyleBox? StyleBoxOverride
|
||||
{
|
||||
get => _styleBoxOverride;
|
||||
@@ -91,7 +94,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
var entry = new RichTextEntry(message, this, _tagManager, null);
|
||||
|
||||
entry.Update(_getFont(), _getContentBox().Width, UIScale);
|
||||
entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale);
|
||||
|
||||
_entries.Add(entry);
|
||||
var font = _getFont();
|
||||
@@ -134,7 +137,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
// So when a new color tag gets hit this stack gets the previous color pushed on.
|
||||
var context = new MarkupDrawingContext(2);
|
||||
|
||||
foreach (ref var entry in CollectionsMarshal.AsSpan(_entries))
|
||||
foreach (ref var entry in _entries)
|
||||
{
|
||||
if (entryOffset + entry.Height < 0)
|
||||
{
|
||||
@@ -147,7 +150,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
break;
|
||||
}
|
||||
|
||||
entry.Draw(handle, font, contentBox, entryOffset, context, UIScale);
|
||||
entry.Draw(_tagManager, handle, font, contentBox, entryOffset, context, UIScale);
|
||||
|
||||
entryOffset += entry.Height + font.GetLineSeparation(UIScale);
|
||||
}
|
||||
@@ -185,9 +188,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_totalContentHeight = 0;
|
||||
var font = _getFont();
|
||||
var sizeX = _getContentBox().Width;
|
||||
foreach (ref var entry in CollectionsMarshal.AsSpan(_entries))
|
||||
foreach (ref var entry in _entries)
|
||||
{
|
||||
entry.Update(font, sizeX, UIScale);
|
||||
entry.Update(_tagManager, font, sizeX, UIScale);
|
||||
_totalContentHeight += entry.Height + font.GetLineSeparation(UIScale);
|
||||
}
|
||||
|
||||
@@ -239,7 +242,13 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
protected internal override void UIScaleChanged()
|
||||
{
|
||||
_invalidateEntries();
|
||||
// If this control isn't visible, don't invalidate entries immediately.
|
||||
// This saves invalidating the debug console if it's hidden,
|
||||
// which is a huge boon as auto-scaling changes UI scale a lot in that scenario.
|
||||
if (!VisibleInTree)
|
||||
_invalidOnVisible = true;
|
||||
else
|
||||
_invalidateEntries();
|
||||
|
||||
base.UIScaleChanged();
|
||||
}
|
||||
@@ -257,5 +266,14 @@ namespace Robust.Client.UserInterface.Controls
|
||||
// existing ones were valid when the UI scale was set.
|
||||
_invalidateEntries();
|
||||
}
|
||||
|
||||
protected override void VisibilityChanged(bool newVisible)
|
||||
{
|
||||
if (newVisible && _invalidOnVisible)
|
||||
{
|
||||
_invalidateEntries();
|
||||
_invalidOnVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
|
||||
var font = _getFont();
|
||||
_entry.Update(font, availableSize.X * UIScale, UIScale, LineHeightScale);
|
||||
_entry.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
|
||||
|
||||
return new Vector2(_entry.Width / UIScale, _entry.Height / UIScale);
|
||||
}
|
||||
@@ -82,7 +82,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return;
|
||||
}
|
||||
|
||||
_entry.Draw(handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
|
||||
_entry.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
|
||||
@@ -14,9 +14,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
[Virtual]
|
||||
public class SpriteView : Control
|
||||
{
|
||||
private SpriteSystem? _sprite;
|
||||
protected SpriteSystem? SpriteSystem;
|
||||
private SharedTransformSystem? _transform;
|
||||
private readonly IEntityManager _entMan;
|
||||
protected readonly IEntityManager EntMan;
|
||||
|
||||
[ViewVariables]
|
||||
public SpriteComponent? Sprite => Entity?.Comp1;
|
||||
@@ -120,20 +120,26 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public SpriteView()
|
||||
{
|
||||
IoCManager.Resolve(ref _entMan);
|
||||
IoCManager.Resolve(ref EntMan);
|
||||
RectClipContent = true;
|
||||
}
|
||||
|
||||
public SpriteView(IEntityManager entMan)
|
||||
{
|
||||
EntMan = entMan;
|
||||
RectClipContent = true;
|
||||
}
|
||||
|
||||
public SpriteView(EntityUid? uid, IEntityManager entMan)
|
||||
{
|
||||
_entMan = entMan;
|
||||
EntMan = entMan;
|
||||
RectClipContent = true;
|
||||
SetEntity(uid);
|
||||
}
|
||||
|
||||
public SpriteView(NetEntity uid, IEntityManager entMan)
|
||||
{
|
||||
_entMan = entMan;
|
||||
EntMan = entMan;
|
||||
RectClipContent = true;
|
||||
SetEntity(uid);
|
||||
}
|
||||
@@ -154,8 +160,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
if (Entity?.Owner == uid)
|
||||
return;
|
||||
|
||||
if (!_entMan.TryGetComponent(uid, out SpriteComponent? sprite)
|
||||
|| !_entMan.TryGetComponent(uid, out TransformComponent? xform))
|
||||
if (!EntMan.TryGetComponent(uid, out SpriteComponent? sprite)
|
||||
|| !EntMan.TryGetComponent(uid, out TransformComponent? xform))
|
||||
{
|
||||
Entity = null;
|
||||
NetEnt = null;
|
||||
@@ -163,7 +169,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
|
||||
Entity = new(uid.Value, sprite, xform);
|
||||
NetEnt = _entMan.GetNetEntity(uid);
|
||||
NetEnt = EntMan.GetNetEntity(uid);
|
||||
}
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
@@ -223,11 +229,11 @@ namespace Robust.Client.UserInterface.Controls
|
||||
if (!ResolveEntity(out var uid, out var sprite, out var xform))
|
||||
return;
|
||||
|
||||
_sprite ??= _entMan.System<SpriteSystem>();
|
||||
_transform ??= _entMan.System<TransformSystem>();
|
||||
SpriteSystem ??= EntMan.System<SpriteSystem>();
|
||||
_transform ??= EntMan.System<TransformSystem>();
|
||||
|
||||
// Ensure the sprite is animated despite possible not being visible in any viewport.
|
||||
_sprite.ForceUpdate(uid);
|
||||
SpriteSystem.ForceUpdate(uid);
|
||||
|
||||
var stretchVec = Stretch switch
|
||||
{
|
||||
@@ -258,13 +264,13 @@ namespace Robust.Client.UserInterface.Controls
|
||||
[NotNullWhen(true)] out SpriteComponent? sprite,
|
||||
[NotNullWhen(true)] out TransformComponent? xform)
|
||||
{
|
||||
if (NetEnt != null && Entity == null && _entMan.TryGetEntity(NetEnt, out var ent))
|
||||
if (NetEnt != null && Entity == null && EntMan.TryGetEntity(NetEnt, out var ent))
|
||||
SetEntity(ent);
|
||||
|
||||
if (Entity != null)
|
||||
{
|
||||
(uid, sprite, xform) = Entity.Value;
|
||||
return !_entMan.Deleted(uid);
|
||||
return !EntMan.Deleted(uid);
|
||||
}
|
||||
|
||||
sprite = null;
|
||||
|
||||
@@ -13,6 +13,12 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
public override float UIScale => UIScaleSet;
|
||||
internal float UIScaleSet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set after the window is resized, to batch up UI scale updates on window resizes.
|
||||
/// </summary>
|
||||
internal bool UIScaleUpdateNeeded { get; set; }
|
||||
|
||||
public override IClydeWindow Window { get; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,6 +7,7 @@ using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Input;
|
||||
@@ -51,6 +52,8 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
private readonly ConcurrentQueue<FormattedMessage> _messageQueue = new();
|
||||
private readonly ISawmill _logger;
|
||||
|
||||
private int _maxEntries;
|
||||
|
||||
public DebugConsole()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
@@ -78,6 +81,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
_consoleHost.AddString += OnAddString;
|
||||
_consoleHost.AddFormatted += OnAddFormatted;
|
||||
_consoleHost.ClearText += OnClearText;
|
||||
_cfg.OnValueChanged(CVars.ConMaxEntries, MaxEntriesChanged, true);
|
||||
|
||||
UserInterfaceManager.ModalRoot.AddChild(_compPopup);
|
||||
}
|
||||
@@ -89,10 +93,17 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
_consoleHost.AddString -= OnAddString;
|
||||
_consoleHost.AddFormatted -= OnAddFormatted;
|
||||
_consoleHost.ClearText -= OnClearText;
|
||||
_cfg.UnsubValueChanged(CVars.ConMaxEntries, MaxEntriesChanged);
|
||||
|
||||
UserInterfaceManager.ModalRoot.RemoveChild(_compPopup);
|
||||
}
|
||||
|
||||
private void MaxEntriesChanged(int value)
|
||||
{
|
||||
_maxEntries = value;
|
||||
TrimExtraOutputEntries();
|
||||
}
|
||||
|
||||
private void OnClearText(object? _, EventArgs args)
|
||||
{
|
||||
Clear();
|
||||
@@ -165,6 +176,15 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
private void _addFormattedLineInternal(FormattedMessage message)
|
||||
{
|
||||
Output.AddMessage(message);
|
||||
TrimExtraOutputEntries();
|
||||
}
|
||||
|
||||
private void TrimExtraOutputEntries()
|
||||
{
|
||||
while (Output.EntryCount > _maxEntries)
|
||||
{
|
||||
Output.RemoveEntry(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void _flushQueue()
|
||||
|
||||
@@ -17,7 +17,6 @@ namespace Robust.Client.UserInterface
|
||||
internal struct RichTextEntry
|
||||
{
|
||||
private readonly Color _defaultColor;
|
||||
private readonly MarkupTagManager _tagManager;
|
||||
private readonly Type[]? _tagsAllowed;
|
||||
|
||||
public readonly FormattedMessage Message;
|
||||
@@ -37,7 +36,7 @@ namespace Robust.Client.UserInterface
|
||||
/// </summary>
|
||||
public ValueList<int> LineBreaks;
|
||||
|
||||
private readonly Dictionary<int, Control> _tagControls = new();
|
||||
private readonly Dictionary<int, Control>? _tagControls;
|
||||
|
||||
public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null)
|
||||
{
|
||||
@@ -46,23 +45,26 @@ namespace Robust.Client.UserInterface
|
||||
Width = 0;
|
||||
LineBreaks = default;
|
||||
_defaultColor = defaultColor ?? new(200, 200, 200);
|
||||
_tagManager = tagManager;
|
||||
_tagsAllowed = tagsAllowed;
|
||||
Dictionary<int, Control>? tagControls = null;
|
||||
|
||||
var nodeIndex = -1;
|
||||
foreach (var node in Message.Nodes)
|
||||
foreach (var node in Message)
|
||||
{
|
||||
nodeIndex++;
|
||||
|
||||
if (node.Name == null)
|
||||
continue;
|
||||
|
||||
if (!_tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
|
||||
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
|
||||
continue;
|
||||
|
||||
parent.Children.Add(control);
|
||||
_tagControls.Add(nodeIndex, control);
|
||||
tagControls ??= new Dictionary<int, Control>();
|
||||
tagControls.Add(nodeIndex, control);
|
||||
}
|
||||
|
||||
_tagControls = tagControls;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -72,7 +74,7 @@ namespace Robust.Client.UserInterface
|
||||
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
|
||||
/// <param name="uiScale"></param>
|
||||
/// <param name="lineHeightScale"></param>
|
||||
public void Update(Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
|
||||
public void Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
|
||||
{
|
||||
// This method is gonna suck due to complexity.
|
||||
// Bear with me here.
|
||||
@@ -91,10 +93,10 @@ namespace Robust.Client.UserInterface
|
||||
// Nodes can change the markup drawing context and return additional text.
|
||||
// It's also possible for nodes to return inline controls. They get treated as one large rune.
|
||||
var nodeIndex = -1;
|
||||
foreach (var node in Message.Nodes)
|
||||
foreach (var node in Message)
|
||||
{
|
||||
nodeIndex++;
|
||||
var text = ProcessNode(node, context);
|
||||
var text = ProcessNode(tagManager, node, context);
|
||||
|
||||
if (!context.Font.TryPeek(out var font))
|
||||
font = defaultFont;
|
||||
@@ -113,7 +115,7 @@ namespace Robust.Client.UserInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_tagControls.TryGetValue(nodeIndex, out var control))
|
||||
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
|
||||
continue;
|
||||
|
||||
if (ProcessRune(ref this, new Rune(' '), out breakLine))
|
||||
@@ -166,6 +168,7 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
|
||||
public readonly void Draw(
|
||||
MarkupTagManager tagManager,
|
||||
DrawingHandleScreen handle,
|
||||
Font defaultFont,
|
||||
UIBox2 drawBox,
|
||||
@@ -184,10 +187,10 @@ namespace Robust.Client.UserInterface
|
||||
var controlYAdvance = 0f;
|
||||
|
||||
var nodeIndex = -1;
|
||||
foreach (var node in Message.Nodes)
|
||||
foreach (var node in Message)
|
||||
{
|
||||
nodeIndex++;
|
||||
var text = ProcessNode(node, context);
|
||||
var text = ProcessNode(tagManager, node, context);
|
||||
if (!context.Color.TryPeek(out var color) || !context.Font.TryPeek(out var font))
|
||||
{
|
||||
color = _defaultColor;
|
||||
@@ -210,7 +213,7 @@ namespace Robust.Client.UserInterface
|
||||
globalBreakCounter += 1;
|
||||
}
|
||||
|
||||
if (!_tagControls.TryGetValue(nodeIndex, out var control))
|
||||
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
|
||||
continue;
|
||||
|
||||
var invertedScale = 1f / uiScale;
|
||||
@@ -223,24 +226,22 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
private readonly string ProcessNode(MarkupNode node, MarkupDrawingContext context)
|
||||
private readonly string ProcessNode(MarkupTagManager tagManager, MarkupNode node, MarkupDrawingContext context)
|
||||
{
|
||||
// If a nodes name is null it's a text node.
|
||||
if (node.Name == null)
|
||||
return node.Value.StringValue ?? "";
|
||||
|
||||
//Skip the node if there is no markup tag for it.
|
||||
if (!_tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
|
||||
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
|
||||
return "";
|
||||
|
||||
if (!node.Closing)
|
||||
{
|
||||
context.Tags.Add(tag);
|
||||
tag.PushDrawContext(node, context);
|
||||
return tag.TextBefore(node);
|
||||
}
|
||||
|
||||
context.Tags.Remove(tag);
|
||||
tag.PopDrawContext(node, context);
|
||||
return tag.TextAfter(node);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,12 @@ internal partial class UserInterfaceManager
|
||||
|
||||
private void UpdateUIScale(WindowRoot root)
|
||||
{
|
||||
root.UIScaleSet = CalculateAutoScale(root);
|
||||
var newScale = CalculateAutoScale(root);
|
||||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
||||
if (newScale == root.UIScaleSet)
|
||||
return;
|
||||
|
||||
root.UIScaleSet = newScale;
|
||||
_propagateUIScaleChanged(root);
|
||||
root.InvalidateMeasure();
|
||||
}
|
||||
@@ -142,7 +147,21 @@ internal partial class UserInterfaceManager
|
||||
{
|
||||
if (!_windowsToRoot.TryGetValue(windowResizedEventArgs.Window.Id, out var root))
|
||||
return;
|
||||
UpdateUIScale(root);
|
||||
|
||||
root.UIScaleUpdateNeeded = true;
|
||||
root.InvalidateMeasure();
|
||||
}
|
||||
|
||||
private void CheckRootUIScaleUpdate(WindowRoot root)
|
||||
{
|
||||
if (!root.UIScaleUpdateNeeded)
|
||||
return;
|
||||
|
||||
using (_prof.Group("UIScaleUpdate"))
|
||||
{
|
||||
UpdateUIScale(root);
|
||||
}
|
||||
|
||||
root.UIScaleUpdateNeeded = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +216,8 @@ namespace Robust.Client.UserInterface
|
||||
{
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
CheckRootUIScaleUpdate(root);
|
||||
|
||||
using (_prof.Group("Root"))
|
||||
{
|
||||
var totalUpdated = root.DoFrameUpdateRecursive(args);
|
||||
|
||||
Submodule Robust.LoaderApi updated: 99a2f4b880...86a02eef16
@@ -1,15 +1,10 @@
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Robust.Server.GameObjects;
|
||||
|
||||
public sealed class ServerMetaDataSystem : MetaDataSystem
|
||||
{
|
||||
[Dependency] private readonly PvsSystem _pvsSystem = default!;
|
||||
private EntityQuery<MetaDataComponent> _mQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -18,7 +13,6 @@ public sealed class ServerMetaDataSystem : MetaDataSystem
|
||||
|
||||
EntityManager.ComponentAdded += OnComponentAdded;
|
||||
EntityManager.ComponentRemoved += OnComponentRemoved;
|
||||
_mQuery = GetEntityQuery<MetaDataComponent>();
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -115,6 +117,16 @@ internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion<P
|
||||
/// </summary>
|
||||
public GameState? State;
|
||||
|
||||
/// <summary>
|
||||
/// The serialized <see cref="State"/> object.
|
||||
/// </summary>
|
||||
public MemoryStream? StateStream;
|
||||
|
||||
/// <summary>
|
||||
/// Whether we should force reliable sending of the <see cref="MsgState"/>.
|
||||
/// </summary>
|
||||
public bool ForceSendReliably { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Clears all stored game state data. This should only be used after the game state has been serialized.
|
||||
/// </summary>
|
||||
|
||||
@@ -75,15 +75,15 @@ namespace Robust.Server.GameStates
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CleanupDirty(ICommonSession[] sessions)
|
||||
private void CleanupDirty()
|
||||
{
|
||||
using var _ = Histogram.WithLabels("Clean Dirty").NewTimer();
|
||||
if (!CullingEnabled)
|
||||
{
|
||||
_seenAllEnts.Clear();
|
||||
foreach (var player in sessions)
|
||||
foreach (var player in _sessions)
|
||||
{
|
||||
_seenAllEnts.Add(player);
|
||||
_seenAllEnts.Add(player.Session);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,12 @@ internal sealed partial class PvsSystem
|
||||
{
|
||||
private WaitHandle? _leaveTask;
|
||||
|
||||
private void ProcessLeavePvs(ICommonSession[] sessions)
|
||||
private void ProcessLeavePvs()
|
||||
{
|
||||
if (!CullingEnabled || sessions.Length == 0)
|
||||
if (!CullingEnabled || _sessions.Length == 0)
|
||||
return;
|
||||
|
||||
DebugTools.AssertNull(_leaveTask);
|
||||
_leaveJob.Setup(sessions);
|
||||
|
||||
if (_async)
|
||||
{
|
||||
@@ -76,29 +75,19 @@ internal sealed partial class PvsSystem
|
||||
{
|
||||
public int BatchSize => 2;
|
||||
private PvsSystem _pvs = _pvs;
|
||||
public int Count => _sessions.Length;
|
||||
private PvsSession[] _sessions;
|
||||
public int Count => _pvs._sessions.Length;
|
||||
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
_pvs.ProcessLeavePvs(_sessions[index]);
|
||||
_pvs.ProcessLeavePvs(_pvs._sessions[index]);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_pvs.Log.Log(LogLevel.Error, e, $"Caught exception while processing pvs-leave messages.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(ICommonSession[] sessions)
|
||||
{
|
||||
// Copy references to PvsSession, in case players disconnect while the job is running.
|
||||
Array.Resize(ref _sessions, sessions.Length);
|
||||
for (var i = 0; i < sessions.Length; i++)
|
||||
{
|
||||
_sessions[i] = _pvs.PlayerData[sessions[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
85
Robust.Server/GameStates/PvsSystem.Send.cs
Normal file
85
Robust.Server/GameStates/PvsSystem.Send.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Prometheus;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.GameStates;
|
||||
|
||||
internal sealed partial class PvsSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Compress and send game states to connected clients.
|
||||
/// </summary>
|
||||
private void SendStates()
|
||||
{
|
||||
// TODO PVS make this async
|
||||
// AFAICT ForEachAsync doesn't support using a threadlocal PvsThreadResources.
|
||||
// Though if it is getting pooled, does it really matter?
|
||||
|
||||
// If this does get run async, then ProcessDisconnections() has to ensure that the job has finished before modifying
|
||||
// the sessions array
|
||||
|
||||
using var _ = Histogram.WithLabels("Send States").NewTimer();
|
||||
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
|
||||
Parallel.ForEach(_sessions, opts, _threadResourcesPool.Get, SendSessionState, _threadResourcesPool.Return);
|
||||
}
|
||||
|
||||
private PvsThreadResources SendSessionState(PvsSession data, ParallelLoopState state, PvsThreadResources resource)
|
||||
{
|
||||
try
|
||||
{
|
||||
SendSessionState(data, resource.CompressionContext);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Log(LogLevel.Error, e, $"Caught exception while sending mail for {data.Session}.");
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private void SendSessionState(PvsSession data, ZStdCompressionContext ctx)
|
||||
{
|
||||
DebugTools.AssertEqual(data.State, null);
|
||||
|
||||
// PVS benchmarks use dummy sessions.
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||
if (data.Session.Channel is not DummyChannel)
|
||||
{
|
||||
DebugTools.AssertNotEqual(data.StateStream, null);
|
||||
var msg = new MsgState
|
||||
{
|
||||
StateStream = data.StateStream,
|
||||
ForceSendReliably = data.ForceSendReliably,
|
||||
CompressionContext = ctx
|
||||
};
|
||||
|
||||
_netMan.ServerSendMessage(msg, data.Session.Channel);
|
||||
if (msg.ShouldSendReliably())
|
||||
{
|
||||
data.RequestedFull = false;
|
||||
data.LastReceivedAck = _gameTiming.CurTick;
|
||||
lock (PendingAcks)
|
||||
{
|
||||
PendingAcks.Add(data.Session);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Always "ack" dummy sessions.
|
||||
data.LastReceivedAck = _gameTiming.CurTick;
|
||||
data.RequestedFull = false;
|
||||
lock (PendingAcks)
|
||||
{
|
||||
PendingAcks.Add(data.Session);
|
||||
}
|
||||
}
|
||||
|
||||
data.StateStream?.Dispose();
|
||||
data.StateStream = null;
|
||||
}
|
||||
}
|
||||
73
Robust.Server/GameStates/PvsSystem.Serialize.cs
Normal file
73
Robust.Server/GameStates/PvsSystem.Serialize.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Prometheus;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.GameStates;
|
||||
|
||||
internal sealed partial class PvsSystem
|
||||
{
|
||||
[Dependency] private readonly IRobustSerializer _serializer = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Get and serialize <see cref="GameState"/> objects for each player. Compressing & sending the states is done later.
|
||||
/// </summary>
|
||||
private void SerializeStates()
|
||||
{
|
||||
using var _ = Histogram.WithLabels("Serialize States").NewTimer();
|
||||
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
|
||||
_oldestAck = GameTick.MaxValue.Value;
|
||||
Parallel.For(-1, _sessions.Length, opts, SerializeState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get and serialize a <see cref="GameState"/> for a single session (or the current replay).
|
||||
/// </summary>
|
||||
private void SerializeState(int i)
|
||||
{
|
||||
try
|
||||
{
|
||||
var guid = i >= 0 ? _sessions[i].Session.UserId.UserId : default;
|
||||
ServerGameStateManager.PvsEventSource.Log.WorkStart(_gameTiming.CurTick.Value, i, guid);
|
||||
|
||||
if (i >= 0)
|
||||
SerializeSessionState(_sessions[i]);
|
||||
else
|
||||
_replay.Update();
|
||||
|
||||
ServerGameStateManager.PvsEventSource.Log.WorkStop(_gameTiming.CurTick.Value, i, guid);
|
||||
}
|
||||
catch (Exception e) // Catch EVERY exception
|
||||
{
|
||||
var source = i >= 0 ? _sessions[i].Session.ToString() : "replays";
|
||||
Log.Log(LogLevel.Error, e, $"Caught exception while serializing game state for {source}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get and serialize a <see cref="GameState"/> for a single session.
|
||||
/// </summary>
|
||||
private void SerializeSessionState(PvsSession data)
|
||||
{
|
||||
ComputeSessionState(data);
|
||||
InterlockedHelper.Min(ref _oldestAck, data.FromTick.Value);
|
||||
DebugTools.AssertEqual(data.StateStream, null);
|
||||
|
||||
// PVS benchmarks use dummy sessions.
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||
if (data.Session.Channel is not DummyChannel)
|
||||
{
|
||||
data.StateStream = RobustMemoryManager.GetMemoryStream();
|
||||
_serializer.SerializeDirect(data.StateStream, data.State);
|
||||
}
|
||||
|
||||
data.ClearState();
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -26,49 +25,6 @@ internal sealed partial class PvsSystem
|
||||
|
||||
private List<ICommonSession> _disconnected = new();
|
||||
|
||||
private void SendStateUpdate(ICommonSession session, PvsThreadResources resources)
|
||||
{
|
||||
var data = GetOrNewPvsSession(session);
|
||||
ComputeSessionState(data);
|
||||
|
||||
InterlockedHelper.Min(ref _oldestAck, data.FromTick.Value);
|
||||
|
||||
// actually send the state
|
||||
var msg = new MsgState
|
||||
{
|
||||
State = data.State,
|
||||
CompressionContext = resources.CompressionContext
|
||||
};
|
||||
|
||||
// PVS benchmarks use dummy sessions.
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||
if (session.Channel != null)
|
||||
{
|
||||
_netMan.ServerSendMessage(msg, session.Channel);
|
||||
if (msg.ShouldSendReliably())
|
||||
{
|
||||
data.RequestedFull = false;
|
||||
data.LastReceivedAck = _gameTiming.CurTick;
|
||||
lock (PendingAcks)
|
||||
{
|
||||
PendingAcks.Add(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Always "ack" dummy sessions.
|
||||
data.LastReceivedAck = _gameTiming.CurTick;
|
||||
data.RequestedFull = false;
|
||||
lock (PendingAcks)
|
||||
{
|
||||
PendingAcks.Add(session);
|
||||
}
|
||||
}
|
||||
|
||||
data.ClearState();
|
||||
}
|
||||
|
||||
private PvsSession GetOrNewPvsSession(ICommonSession session)
|
||||
{
|
||||
if (!PlayerData.TryGetValue(session, out var pvsSession))
|
||||
@@ -103,7 +59,7 @@ internal sealed partial class PvsSystem
|
||||
session.PlayerStates,
|
||||
_deletedEntities);
|
||||
|
||||
session.State.ForceSendReliably = session.RequestedFull
|
||||
session.ForceSendReliably = session.RequestedFull
|
||||
|| _gameTiming.CurTick > session.LastReceivedAck + (uint) ForceAckThreshold;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Prometheus;
|
||||
using Robust.Server.Configuration;
|
||||
@@ -16,9 +14,6 @@ using Robust.Server.Replays;
|
||||
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.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
@@ -99,6 +94,10 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
/// </summary>
|
||||
private readonly List<GameTick> _deletedTick = new();
|
||||
|
||||
/// <summary>
|
||||
/// The sessions that are currently being processed. Note that this is in general used by parallel & async tasks.
|
||||
/// Hence player disconnection processing is deferred and only run via <see cref="ProcessDisconnections"/>.
|
||||
/// </summary>
|
||||
private PvsSession[] _sessions = default!;
|
||||
|
||||
private bool _async;
|
||||
@@ -183,52 +182,25 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
/// </summary>
|
||||
internal void SendGameStates(ICommonSession[] players)
|
||||
{
|
||||
// Wait for pending jobs and process disconnected players
|
||||
ProcessDisconnections();
|
||||
|
||||
// Ensure each session has a PvsSession entry before starting any parallel jobs.
|
||||
CacheSessionData(players);
|
||||
|
||||
// Get visible chunks, and update any dirty chunks.
|
||||
BeforeSendState();
|
||||
BeforeSerializeStates();
|
||||
|
||||
// Construct & send the game state to each player.
|
||||
SendStates(players);
|
||||
// Construct & serialize the game state for each player (and for the replay).
|
||||
SerializeStates();
|
||||
|
||||
// Compress & send the states.
|
||||
SendStates();
|
||||
|
||||
// Cull deletion history
|
||||
AfterSendState(players);
|
||||
AfterSerializeStates();
|
||||
|
||||
ProcessLeavePvs(players);
|
||||
}
|
||||
|
||||
private void SendStates(ICommonSession[] players)
|
||||
{
|
||||
using var _ = Histogram.WithLabels("Send States").NewTimer();
|
||||
|
||||
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
|
||||
_oldestAck = GameTick.MaxValue.Value;
|
||||
|
||||
// Replays process game states in parallel with players
|
||||
Parallel.For(-1, players.Length, opts, _threadResourcesPool.Get, SendPlayer, _threadResourcesPool.Return);
|
||||
|
||||
PvsThreadResources SendPlayer(int i, ParallelLoopState state, PvsThreadResources resource)
|
||||
{
|
||||
try
|
||||
{
|
||||
var guid = i >= 0 ? players[i].UserId.UserId : default;
|
||||
ServerGameStateManager.PvsEventSource.Log.WorkStart(_gameTiming.CurTick.Value, i, guid);
|
||||
|
||||
if (i >= 0)
|
||||
SendStateUpdate(players[i], resource);
|
||||
else
|
||||
_replay.Update();
|
||||
|
||||
ServerGameStateManager.PvsEventSource.Log.WorkStop(_gameTiming.CurTick.Value, i, guid);
|
||||
}
|
||||
catch (Exception e) // Catch EVERY exception
|
||||
{
|
||||
var source = i >= 0 ? players[i].ToString() : "replays";
|
||||
Log.Log(LogLevel.Error, e, $"Caught exception while generating mail for {source}.");
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
ProcessLeavePvs();
|
||||
}
|
||||
|
||||
private void ResetParallelism(int _) => ResetParallelism();
|
||||
@@ -414,23 +386,11 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void BeforeSendState()
|
||||
private void BeforeSerializeStates()
|
||||
{
|
||||
DebugTools.Assert(_chunks.Values.All(x => Exists(x.Map) && Exists(x.Root)));
|
||||
DebugTools.Assert(_chunkSets.Keys.All(Exists));
|
||||
|
||||
_leaveTask?.WaitOne();
|
||||
_leaveTask = null;
|
||||
|
||||
foreach (var session in _disconnected)
|
||||
{
|
||||
if (PlayerData.Remove(session, out var pvsSession))
|
||||
{
|
||||
ClearSendHistory(pvsSession);
|
||||
FreeSessionDataMemory(pvsSession);
|
||||
}
|
||||
}
|
||||
|
||||
var ackJob = ProcessQueuedAcks();
|
||||
|
||||
// Figure out what chunks players can see and cache some chunk data.
|
||||
@@ -443,6 +403,21 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
ackJob?.WaitOne();
|
||||
}
|
||||
|
||||
internal void ProcessDisconnections()
|
||||
{
|
||||
_leaveTask?.WaitOne();
|
||||
_leaveTask = null;
|
||||
|
||||
foreach (var session in _disconnected)
|
||||
{
|
||||
if (PlayerData.Remove(session, out var pvsSession))
|
||||
{
|
||||
ClearSendHistory(pvsSession);
|
||||
FreeSessionDataMemory(pvsSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void CacheSessionData(ICommonSession[] players)
|
||||
{
|
||||
Array.Resize(ref _sessions, players.Length);
|
||||
@@ -452,9 +427,9 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void AfterSendState(ICommonSession[] players)
|
||||
private void AfterSerializeStates()
|
||||
{
|
||||
CleanupDirty(players);
|
||||
CleanupDirty();
|
||||
|
||||
if (_oldestAck == GameTick.MaxValue.Value)
|
||||
{
|
||||
|
||||
@@ -90,18 +90,18 @@ namespace Robust.Server.Player
|
||||
_cfg.SyncConnectingClient(args.Channel);
|
||||
}
|
||||
|
||||
private void EndSession(object? sender, NetChannelArgs args)
|
||||
{
|
||||
EndSession(args.Channel.UserId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends a clients session, and disconnects them.
|
||||
/// </summary>
|
||||
private void EndSession(object? sender, NetChannelArgs args)
|
||||
internal void EndSession(NetUserId user)
|
||||
{
|
||||
if (!TryGetSessionByChannel(args.Channel, out var session))
|
||||
{
|
||||
if (!TryGetSessionById(user, out var session))
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure nothing got messed up during the life of the session
|
||||
DebugTools.Assert(session.Channel == args.Channel);
|
||||
|
||||
SetStatus(session, SessionStatus.Disconnected);
|
||||
SetAttachedEntity(session, null, out _, true);
|
||||
@@ -143,7 +143,6 @@ namespace Robust.Server.Player
|
||||
list.Add(info);
|
||||
}
|
||||
netMsg.Plyrs = list;
|
||||
netMsg.PlyCount = (byte)list.Count;
|
||||
|
||||
channel.SendMessage(netMsg);
|
||||
}
|
||||
@@ -159,5 +158,32 @@ namespace Robust.Server.Player
|
||||
session = actor.PlayerSession;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal ICommonSession AddDummySession(NetUserId user, string name)
|
||||
{
|
||||
#if FULL_RELEASE
|
||||
// Lets not make it completely trivial to fake player counts.
|
||||
throw new NotSupportedException();
|
||||
#endif
|
||||
Lock.EnterWriteLock();
|
||||
DummySession session;
|
||||
try
|
||||
{
|
||||
UserIdMap[name] = user;
|
||||
if (!PlayerData.TryGetValue(user, out var data))
|
||||
PlayerData[user] = data = new(user, name);
|
||||
|
||||
session = new DummySession(user, name, data);
|
||||
InternalSessions.Add(user, session);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Lock.ExitWriteLock();
|
||||
}
|
||||
|
||||
UpdateState(session);
|
||||
|
||||
return session;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,6 @@ namespace Robust.Server
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadPool.SetMinThreads(Environment.ProcessorCount * 2, Environment.ProcessorCount);
|
||||
|
||||
ParsedMain(parsed, contentStart, options);
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ internal sealed class HubManager
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorText = await response.Content.ReadAsStringAsync();
|
||||
_sawmill.Error("Error status while advertising server: [{StatusCode}] {ErrorText}, to {HubUrl}",
|
||||
_sawmill.Error("Error status while advertising server: [{StatusCode}] {ErrorText}, from {HubUrl}",
|
||||
response.StatusCode,
|
||||
errorText,
|
||||
hubUrl);
|
||||
|
||||
@@ -312,14 +312,14 @@ namespace Robust.Server.ServerStatus
|
||||
}
|
||||
|
||||
// Only call this if the download URL is not available!
|
||||
private async Task<AczManifestInfo?> PrepareAcz()
|
||||
private async Task<AczManifestInfo?> PrepareAcz(bool optional = false)
|
||||
{
|
||||
// Take the ACZ lock asynchronously
|
||||
await _aczLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// Setting this now ensures that it won't fail repeatedly on exceptions/etc.
|
||||
if (_aczPrepareAttempted)
|
||||
if (_aczPrepareAttempted || optional)
|
||||
return _aczPrepared;
|
||||
|
||||
_aczPrepareAttempted = true;
|
||||
|
||||
@@ -80,8 +80,7 @@ namespace Robust.Server.ServerStatus
|
||||
if (string.IsNullOrEmpty(downloadUrl))
|
||||
{
|
||||
var query = HttpUtility.ParseQueryString(context.Url.Query);
|
||||
var optional = query.Keys;
|
||||
buildInfo = await PrepareACZBuildInfo();
|
||||
buildInfo = await PrepareACZBuildInfo(optional: query.Get("can_skip_build") == "1");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -129,9 +128,9 @@ namespace Robust.Server.ServerStatus
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<JsonObject?> PrepareACZBuildInfo()
|
||||
private async Task<JsonObject?> PrepareACZBuildInfo(bool optional)
|
||||
{
|
||||
var acm = await PrepareAcz();
|
||||
var acm = await PrepareAcz(optional);
|
||||
if (acm == null) return null;
|
||||
|
||||
// Fork ID is an interesting case, we don't want to cause too many redownloads but we also don't want to pollute disk.
|
||||
|
||||
@@ -275,7 +275,7 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
{
|
||||
eventRaise = @"
|
||||
var ev = new AfterAutoHandleStateEvent(args.Current);
|
||||
EntityManager.EventBus.RaiseComponentEvent(component, ref ev);";
|
||||
EntityManager.EventBus.RaiseComponentEvent(uid, component, ref ev);";
|
||||
}
|
||||
|
||||
return $@"// <auto-generated />
|
||||
|
||||
@@ -39,5 +39,5 @@ public sealed class AutoNetworkedFieldAttribute : Attribute
|
||||
/// <see cref="AutoGenerateComponentStateAttribute.RaiseAfterAutoHandleState"/> is true, so that other systems
|
||||
/// can have effects after handling state without having to redefine all replication.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
[ByRefEvent, ComponentEvent]
|
||||
public record struct AfterAutoHandleStateEvent(IComponentState State);
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace Robust.Shared
|
||||
/// <seealso cref="NetMtuExpand"/>
|
||||
/// <seealso cref="NetMtuIpv6"/>
|
||||
public static readonly CVarDef<int> NetMtu =
|
||||
CVarDef.Create("net.mtu", 900, CVar.ARCHIVE);
|
||||
CVarDef.Create("net.mtu", 700, CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum UDP payload size to send by default, for IPv6.
|
||||
@@ -1374,7 +1374,7 @@ namespace Robust.Shared
|
||||
/// the purpose of using an atlas if it gets too small.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> ResRSIAtlasSize =
|
||||
CVarDef.Create("res.rsi_atlas_size", 8192, CVar.CLIENTONLY);
|
||||
CVarDef.Create("res.rsi_atlas_size", 12288, CVar.CLIENTONLY);
|
||||
|
||||
// TODO: Currently unimplemented.
|
||||
/// <summary>
|
||||
@@ -1560,6 +1560,12 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<int> ConCompletionMargin =
|
||||
CVarDef.Create("con.completion_margin", 3, CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum amount of entries stored by the debug console.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> ConMaxEntries =
|
||||
CVarDef.Create("con.max_entries", 3_000, CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* THREAD
|
||||
*/
|
||||
@@ -1641,6 +1647,13 @@ namespace Robust.Shared
|
||||
true,
|
||||
CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// How many milliseconds we will spend moving forward from the nearest checkpoint or current position.
|
||||
/// We will spend this time when scrubbing the timeline per game tick. This limits CPU usage / locking up and
|
||||
/// improves responsiveness
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> ReplayMaxScrubTime = CVarDef.Create("replay.max_scrub_time", 10);
|
||||
|
||||
/// <summary>
|
||||
/// Determines the threshold before visual events (muzzle flashes, chat pop-ups, etc) are suppressed when
|
||||
/// jumping forward in time. Jumps larger than this will simply skip directly to the target tick.
|
||||
|
||||
304
Robust.Shared/Collections/RingBufferList.cs
Normal file
304
Robust.Shared/Collections/RingBufferList.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Robust.Shared.Utility;
|
||||
using ArgumentNullException = System.ArgumentNullException;
|
||||
|
||||
namespace Robust.Shared.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Datastructure that acts like a <see cref="List{T}"/>, but is actually stored as a ring buffer internally.
|
||||
/// This facilitates efficient removal from the start.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of item contained in the collection.</typeparam>
|
||||
internal sealed class RingBufferList<T> : IList<T>
|
||||
{
|
||||
private T[] _items;
|
||||
private int _read;
|
||||
private int _write;
|
||||
|
||||
public RingBufferList(int capacity)
|
||||
{
|
||||
_items = new T[capacity];
|
||||
}
|
||||
|
||||
public RingBufferList()
|
||||
{
|
||||
_items = [];
|
||||
}
|
||||
|
||||
public int Capacity => _items.Length;
|
||||
|
||||
private bool IsFull => _items.Length == 0 || NextIndex(_write) == _read;
|
||||
|
||||
public void Add(T item)
|
||||
{
|
||||
if (IsFull)
|
||||
Expand();
|
||||
|
||||
DebugTools.Assert(!IsFull);
|
||||
|
||||
_items[_write] = item;
|
||||
_write = NextIndex(_write);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_read = 0;
|
||||
_write = 0;
|
||||
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
|
||||
Array.Clear(_items);
|
||||
}
|
||||
|
||||
public bool Contains(T item)
|
||||
{
|
||||
return IndexOf(item) >= 0;
|
||||
}
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(array);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
|
||||
|
||||
CopyTo(array.AsSpan(arrayIndex));
|
||||
}
|
||||
|
||||
private void CopyTo(Span<T> dest)
|
||||
{
|
||||
if (dest.Length < Count)
|
||||
throw new ArgumentException("Not enough elements in destination!");
|
||||
|
||||
var i = 0;
|
||||
foreach (var item in this)
|
||||
{
|
||||
dest[i++] = item;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(T item)
|
||||
{
|
||||
var index = IndexOf(item);
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
RemoveAt(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
var length = _write - _read;
|
||||
if (length >= 0)
|
||||
return length;
|
||||
|
||||
return length + _items.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public int IndexOf(T item)
|
||||
{
|
||||
var i = 0;
|
||||
foreach (var containedItem in this)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(item, containedItem))
|
||||
return i;
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Insert(int index, T item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
var length = Count;
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, length);
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
|
||||
_items[_read] = default!;
|
||||
|
||||
_read = NextIndex(_read);
|
||||
}
|
||||
else if (index == length - 1)
|
||||
{
|
||||
_write = WrapInv(_write - 1);
|
||||
|
||||
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
|
||||
_items[_write] = default!;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If past me had better foresight I wouldn't be spending so much effort writing this right now.
|
||||
|
||||
var realIdx = RealIndex(index);
|
||||
var origValue = _items[realIdx];
|
||||
T result;
|
||||
|
||||
if (realIdx < _read)
|
||||
{
|
||||
// Scenario one: to-remove index is after break.
|
||||
// One shift is needed.
|
||||
// v
|
||||
// X X X O X X
|
||||
// W R
|
||||
DebugTools.Assert(_write < _read);
|
||||
|
||||
result = ShiftDown(_items.AsSpan()[realIdx.._write], default!);
|
||||
}
|
||||
else if (_write < _read)
|
||||
{
|
||||
// Scenario two: to-remove index is before break, but write is after.
|
||||
// Two shifts are needed.
|
||||
// v
|
||||
// X O X X X X
|
||||
// W R
|
||||
|
||||
var fromEnd = ShiftDown(_items.AsSpan(0, _write), default!);
|
||||
result = ShiftDown(_items.AsSpan(realIdx), fromEnd);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Scenario two: array is contiguous.
|
||||
// One shift is needed.
|
||||
// v
|
||||
// X X X X O O
|
||||
// R W
|
||||
|
||||
result = ShiftDown(_items.AsSpan()[realIdx.._write], default!);
|
||||
}
|
||||
|
||||
// Just make sure we didn't bulldozer something.
|
||||
DebugTools.Assert(EqualityComparer<T>.Default.Equals(origValue, result));
|
||||
|
||||
_write = WrapInv(_write - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static T ShiftDown(Span<T> span, T substitution)
|
||||
{
|
||||
if (span.Length == 0)
|
||||
return substitution;
|
||||
|
||||
var first = span[0];
|
||||
span[1..].CopyTo(span[..^1]);
|
||||
span[^1] = substitution!;
|
||||
return first;
|
||||
}
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
get => GetSlot(index);
|
||||
set => GetSlot(index) = value;
|
||||
}
|
||||
|
||||
private ref T GetSlot(int index)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count);
|
||||
|
||||
return ref _items[RealIndex(index)];
|
||||
}
|
||||
|
||||
private int RealIndex(int index)
|
||||
{
|
||||
return Wrap(index + _read);
|
||||
}
|
||||
|
||||
private int NextIndex(int index) => Wrap(index + 1);
|
||||
|
||||
private int Wrap(int index)
|
||||
{
|
||||
if (index >= _items.Length)
|
||||
index -= _items.Length;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private int WrapInv(int index)
|
||||
{
|
||||
if (index < 0)
|
||||
index = _items.Length - 1;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private void Expand()
|
||||
{
|
||||
var prevSize = _items.Length;
|
||||
var newSize = Math.Max(4, prevSize * 2);
|
||||
Array.Resize(ref _items, newSize);
|
||||
|
||||
if (_write >= _read)
|
||||
return;
|
||||
|
||||
// Write is behind read pointer, so we need to copy the items to be after the read pointer.
|
||||
var toCopy = _items.AsSpan(0, _write);
|
||||
var copyDest = _items.AsSpan(prevSize);
|
||||
toCopy.CopyTo(copyDest);
|
||||
|
||||
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
|
||||
toCopy.Clear();
|
||||
|
||||
_write += prevSize;
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator()
|
||||
{
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly RingBufferList<T> _ringBufferList;
|
||||
private int _readPos;
|
||||
|
||||
internal Enumerator(RingBufferList<T> ringBufferList)
|
||||
{
|
||||
_ringBufferList = ringBufferList;
|
||||
_readPos = _ringBufferList._read - 1;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_readPos = _ringBufferList.NextIndex(_readPos);
|
||||
return _readPos != _ringBufferList._write;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this = new Enumerator(_ringBufferList);
|
||||
}
|
||||
|
||||
public ref T Current => ref _ringBufferList._items[_readPos];
|
||||
|
||||
T IEnumerator<T>.Current => Current;
|
||||
object? IEnumerator.Current => Current;
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,8 +97,23 @@ namespace Robust.Shared.Configuration
|
||||
{
|
||||
// overwrite the value with the saved one
|
||||
var oldValue = GetConfigVarValue(cfgVar);
|
||||
changedInvokes.Add(SetupInvokeValueChanged(cfgVar, value, oldValue));
|
||||
cfgVar.Value = value;
|
||||
|
||||
var convertedValue = value;
|
||||
if (cfgVar.Type != value.GetType())
|
||||
{
|
||||
try
|
||||
{
|
||||
convertedValue = ConvertToCVarType(value, cfgVar.Type!);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_sawmill.Error($"TOML parsed cvar does not match registered cvar type. Name: {cvar}. Code Type: {cfgVar.Type}. Toml type: {value.GetType()}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
changedInvokes.Add(SetupInvokeValueChanged(cfgVar, convertedValue, oldValue));
|
||||
cfgVar.Value = convertedValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ internal sealed class DumpEventTablesCommand : LocalizedCommands
|
||||
{
|
||||
shell.WriteLine($"{evType}:");
|
||||
|
||||
var idx = comps;
|
||||
var idx = comps.Start;
|
||||
while (idx != -1)
|
||||
{
|
||||
ref var entry = ref table.ComponentLists[idx];
|
||||
|
||||
@@ -174,12 +174,7 @@ public sealed class TeleportToCommand : LocalizedCommands
|
||||
|
||||
var hint = args.Length == 1 ? "cmd-tpto-destination-hint" : "cmd-tpto-victim-hint";
|
||||
hint = Loc.GetString(hint);
|
||||
|
||||
var opts = CompletionResult.FromHintOptions(users, hint);
|
||||
if (last != string.Empty && !NetEntity.TryParse(last, out _))
|
||||
return opts;
|
||||
|
||||
return CompletionResult.FromHintOptions(opts.Options.Concat(CompletionHelper.NetEntities(last, _entities)), hint);
|
||||
return CompletionResult.FromHintOptions(users, hint);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -189,27 +189,45 @@ public static class CompletionHelper
|
||||
return Components<MapComponent>(string.Empty, entManager);
|
||||
}
|
||||
|
||||
public static IEnumerable<CompletionOption> NetEntities(string text, IEntityManager? entManager = null)
|
||||
/// <summary>
|
||||
/// Return all existing entities as possible completions. You should generally avoid using this unless you need to.
|
||||
/// </summary>
|
||||
public static IEnumerable<CompletionOption> NetEntities(string text, IEntityManager? entManager = null, int limit = 20)
|
||||
{
|
||||
return Components<MetaDataComponent>(text, entManager);
|
||||
}
|
||||
if (!NetEntity.TryParse(text, out _))
|
||||
yield break;
|
||||
|
||||
public static IEnumerable<CompletionOption> Components<T>(string text, IEntityManager? entManager = null) where T : IComponent
|
||||
{
|
||||
IoCManager.Resolve(ref entManager);
|
||||
var query = entManager.AllEntityQueryEnumerator<MetaDataComponent>();
|
||||
|
||||
var query = entManager.AllEntityQueryEnumerator<T, MetaDataComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out _, out var metadata))
|
||||
var i = 0;
|
||||
while (i < limit && query.MoveNext(out var metadata))
|
||||
{
|
||||
if (!entManager.TryGetNetEntity(uid, out var netEntity, metadata: metadata))
|
||||
continue;
|
||||
|
||||
var netString = netEntity.Value.ToString();
|
||||
|
||||
var netString = metadata.NetEntity.ToString();
|
||||
if (!netString.StartsWith(text))
|
||||
continue;
|
||||
|
||||
i++;
|
||||
yield return new CompletionOption(netString, metadata.EntityName);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<CompletionOption> Components<T>(string text, IEntityManager? entManager = null, int limit = 20) where T : IComponent
|
||||
{
|
||||
if (!NetEntity.TryParse(text, out _))
|
||||
yield break;
|
||||
|
||||
IoCManager.Resolve(ref entManager);
|
||||
var query = entManager.AllEntityQueryEnumerator<T, MetaDataComponent>();
|
||||
|
||||
var i = 0;
|
||||
while (i < limit && query.MoveNext(out _, out var metadata))
|
||||
{
|
||||
var netString = metadata.NetEntity.ToString();
|
||||
if (!netString.StartsWith(text))
|
||||
continue;
|
||||
|
||||
i++;
|
||||
yield return new CompletionOption(netString, metadata.EntityName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ public abstract partial class SharedContainerSystem
|
||||
|
||||
DebugTools.AssertOwner(container.Owner, containerXform);
|
||||
DebugTools.AssertOwner(toInsert, physics);
|
||||
DebugTools.Assert(!container.ExpectedEntities.Contains(GetNetEntity(toInsert)));
|
||||
DebugTools.Assert(container.Manager.Containers.ContainsKey(container.ID));
|
||||
DebugTools.Assert(!container.ExpectedEntities.Contains(GetNetEntity(toInsert)), "entity is expected");
|
||||
DebugTools.Assert(container.Manager.Containers.ContainsKey(container.ID), "manager does not own the container");
|
||||
|
||||
// If someone is attempting to insert an entity into a container that is getting deleted, then we will
|
||||
// automatically delete that entity. I.e., the insertion automatically "succeeds" and both entities get deleted.
|
||||
@@ -82,14 +82,14 @@ public abstract partial class SharedContainerSystem
|
||||
}
|
||||
|
||||
// Update metadata first, so that parent change events can check IsInContainer.
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0);
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0, "invalid metadata flags before insertion");
|
||||
meta.Flags |= MetaDataFlags.InContainer;
|
||||
|
||||
// Remove the entity and any children from broadphases.
|
||||
// This is done before changing can collide to avoid unecceary updates.
|
||||
// TODO maybe combine with RecursivelyUpdatePhysics to avoid fetching components and iterating parents twice?
|
||||
_lookup.RemoveFromEntityTree(toInsert, transform);
|
||||
DebugTools.Assert(transform.Broadphase == null || !transform.Broadphase.Value.IsValid());
|
||||
DebugTools.Assert(transform.Broadphase == null || !transform.Broadphase.Value.IsValid(), "invalid broadphase");
|
||||
|
||||
// Avoid unnecessary broadphase updates while unanchoring, changing physics collision, and re-parenting.
|
||||
var old = transform.Broadphase;
|
||||
@@ -111,7 +111,7 @@ public abstract partial class SharedContainerSystem
|
||||
transform.Broadphase = old;
|
||||
|
||||
// the transform.AttachParent() could previously result in the flag being unset, so check that this hasn't happened.
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0);
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0, "invalid metadata flags after insertion");
|
||||
|
||||
// Implementation specific insert logic
|
||||
container.InternalInsert(toInsert, EntityManager);
|
||||
@@ -125,11 +125,11 @@ public abstract partial class SharedContainerSystem
|
||||
RaiseLocalEvent(toInsert, new EntGotInsertedIntoContainerMessage(toInsert, container), true);
|
||||
|
||||
// The sheer number of asserts tells you about how little I trust container and parenting code.
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0);
|
||||
DebugTools.Assert(!transform.Anchored);
|
||||
DebugTools.Assert(transform.LocalPosition == Vector2.Zero);
|
||||
DebugTools.Assert(MathHelper.CloseTo(transform.LocalRotation.Theta, Angle.Zero));
|
||||
DebugTools.Assert(!PhysicsQuery.TryGetComponent(toInsert, out var phys) || (!phys.Awake && !phys.CanCollide));
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0, "invalid metadata flags after events");
|
||||
DebugTools.Assert(!transform.Anchored, "entity is anchored");
|
||||
DebugTools.AssertEqual(transform.LocalPosition, Vector2.Zero);
|
||||
DebugTools.Assert(MathHelper.CloseTo(transform.LocalRotation.Theta, Angle.Zero), "Angle is not zero");
|
||||
DebugTools.Assert(!PhysicsQuery.TryGetComponent(toInsert, out var phys) || (!phys.Awake && !phys.CanCollide), "Invalid physics");
|
||||
|
||||
Dirty(container.Owner, container.Manager);
|
||||
return true;
|
||||
|
||||
@@ -41,7 +41,7 @@ public abstract partial class SharedContainerSystem
|
||||
return false;
|
||||
|
||||
DebugTools.AssertNotNull(container.Manager);
|
||||
DebugTools.Assert(Exists(toRemove));
|
||||
DebugTools.Assert(Exists(toRemove), "toRemove does not exist");
|
||||
|
||||
if (!force && !CanRemove(toRemove, container))
|
||||
return false;
|
||||
@@ -60,11 +60,11 @@ public abstract partial class SharedContainerSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
DebugTools.Assert(meta.EntityLifeStage < EntityLifeStage.Terminating || (force && !reparent));
|
||||
DebugTools.Assert(xform.Broadphase == null || !xform.Broadphase.Value.IsValid());
|
||||
DebugTools.Assert(!xform.Anchored);
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0x0);
|
||||
DebugTools.Assert(!TryComp(toRemove, out PhysicsComponent? phys) || (!phys.Awake && !phys.CanCollide));
|
||||
DebugTools.Assert(meta.EntityLifeStage < EntityLifeStage.Terminating || (force && !reparent), "Entity is terminating");
|
||||
DebugTools.Assert(xform.Broadphase == null || !xform.Broadphase.Value.IsValid(), "broadphase is invalid");
|
||||
DebugTools.Assert(!xform.Anchored || _timing.ApplyingState, "anchor is invalid");
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0x0, "metadata is invalid");
|
||||
DebugTools.Assert(!TryComp(toRemove, out PhysicsComponent? phys) || (!phys.Awake && !phys.CanCollide), "physics is invalid");
|
||||
|
||||
// Unset flag (before parent change events are raised).
|
||||
meta.Flags &= ~MetaDataFlags.InContainer;
|
||||
@@ -104,7 +104,7 @@ public abstract partial class SharedContainerSystem
|
||||
RaiseLocalEvent(container.Owner, new EntRemovedFromContainerMessage(toRemove, container), true);
|
||||
RaiseLocalEvent(toRemove, new EntGotRemovedFromContainerMessage(toRemove, container), false);
|
||||
|
||||
DebugTools.Assert(destination == null || xform.Coordinates.Equals(destination.Value));
|
||||
DebugTools.Assert(destination == null || xform.Coordinates.Equals(destination.Value), "failed to set destination");
|
||||
|
||||
Dirty(container.Owner, container.Manager);
|
||||
return true;
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Robust.Shared.ContentPack
|
||||
String("short").ThenReturn(PrimitiveTypeCode.Int16);
|
||||
|
||||
private static readonly Parser<char, PrimitiveTypeCode> UInt16TypeParser =
|
||||
String("ushort").ThenReturn(PrimitiveTypeCode.UInt32);
|
||||
String("ushort").ThenReturn(PrimitiveTypeCode.UInt16);
|
||||
|
||||
private static readonly Parser<char, PrimitiveTypeCode> Int32TypeParser =
|
||||
String("int").ThenReturn(PrimitiveTypeCode.Int32);
|
||||
|
||||
@@ -84,12 +84,146 @@ Types:
|
||||
- "bool get_HasContents()"
|
||||
Lidgren.Network:
|
||||
NetBuffer:
|
||||
All: True
|
||||
Methods:
|
||||
- "byte[] get_Data()"
|
||||
- "void set_Data(byte[])"
|
||||
- "int get_LengthBytes()"
|
||||
- "void set_LengthBytes(int)"
|
||||
- "int get_LengthBits()"
|
||||
- "void set_LengthBits(int)"
|
||||
- "long get_Position()"
|
||||
- "void set_Position(long)"
|
||||
- "int get_PositionInBytes()"
|
||||
- "byte[] PeekDataBuffer()"
|
||||
- "bool PeekBoolean()"
|
||||
- "byte PeekByte()"
|
||||
- "sbyte PeekSByte()"
|
||||
- "byte PeekByte(int)"
|
||||
- "System.Span`1<byte> PeekBytes(System.Span`1<byte>)"
|
||||
- "byte[] PeekBytes(int)"
|
||||
- "void PeekBytes(byte[], int, int)"
|
||||
- "short PeekInt16()"
|
||||
- "ushort PeekUInt16()"
|
||||
- "int PeekInt32()"
|
||||
- "int PeekInt32(int)"
|
||||
- "uint PeekUInt32()"
|
||||
- "uint PeekUInt32(int)"
|
||||
- "ulong PeekUInt64()"
|
||||
- "long PeekInt64()"
|
||||
- "ulong PeekUInt64(int)"
|
||||
- "long PeekInt64(int)"
|
||||
- "float PeekFloat()"
|
||||
- "System.Half PeekHalf()"
|
||||
- "float PeekSingle()"
|
||||
- "double PeekDouble()"
|
||||
- "string PeekString()"
|
||||
- "int PeekStringSize()"
|
||||
- "bool ReadBoolean()"
|
||||
- "byte ReadByte()"
|
||||
- "bool ReadByte(ref byte)"
|
||||
- "sbyte ReadSByte()"
|
||||
- "byte ReadByte(int)"
|
||||
- "System.Span`1<byte> ReadBytes(System.Span`1<byte>)"
|
||||
- "byte[] ReadBytes(int)"
|
||||
- "bool ReadBytes(int, ref byte[])"
|
||||
- "bool TryReadBytes(System.Span`1<byte>)"
|
||||
- "void ReadBytes(byte[], int, int)"
|
||||
- "void ReadBits(System.Span`1<byte>, int)"
|
||||
- "void ReadBits(byte[], int, int)"
|
||||
- "short ReadInt16()"
|
||||
- "ushort ReadUInt16()"
|
||||
- "int ReadInt32()"
|
||||
- "bool ReadInt32(ref int)"
|
||||
- "int ReadInt32(int)"
|
||||
- "uint ReadUInt32()"
|
||||
- "bool ReadUInt32(ref uint)"
|
||||
- "uint ReadUInt32(int)"
|
||||
- "ulong ReadUInt64()"
|
||||
- "long ReadInt64()"
|
||||
- "ulong ReadUInt64(int)"
|
||||
- "long ReadInt64(int)"
|
||||
- "float ReadFloat()"
|
||||
- "System.Half ReadHalf()"
|
||||
- "float ReadSingle()"
|
||||
- "bool ReadSingle(ref float)"
|
||||
- "double ReadDouble()"
|
||||
- "uint ReadVariableUInt32()"
|
||||
- "bool ReadVariableUInt32(ref uint)"
|
||||
- "int ReadVariableInt32()"
|
||||
- "long ReadVariableInt64()"
|
||||
- "ulong ReadVariableUInt64()"
|
||||
- "float ReadSignedSingle(int)"
|
||||
- "float ReadUnitSingle(int)"
|
||||
- "float ReadRangedSingle(float, float, int)"
|
||||
- "int ReadRangedInteger(int, int)"
|
||||
- "long ReadRangedInteger(long, long)"
|
||||
- "string ReadString()"
|
||||
- "bool ReadString(ref string)"
|
||||
- "double ReadTime(Lidgren.Network.NetConnection, bool)"
|
||||
- "System.Net.IPEndPoint ReadIPEndPoint()"
|
||||
- "void SkipPadBits()"
|
||||
- "void ReadPadBits()"
|
||||
- "void SkipPadBits(int)"
|
||||
- "void EnsureBufferSize(int)"
|
||||
- "void Write(bool)"
|
||||
- "void Write(byte)"
|
||||
- "void WriteAt(int, byte)"
|
||||
- "void Write(sbyte)"
|
||||
- "void Write(byte, int)"
|
||||
- "void Write(byte[])"
|
||||
- "void Write(System.ReadOnlySpan`1<byte>)"
|
||||
- "void Write(byte[], int, int)"
|
||||
- "void Write(ushort)"
|
||||
- "void WriteAt(int, ushort)"
|
||||
- "void Write(ushort, int)"
|
||||
- "void Write(short)"
|
||||
- "void WriteAt(int, short)"
|
||||
- "void Write(int)"
|
||||
- "void WriteAt(int, int)"
|
||||
- "void Write(uint)"
|
||||
- "void WriteAt(int, uint)"
|
||||
- "void Write(uint, int)"
|
||||
- "void Write(int, int)"
|
||||
- "void Write(ulong)"
|
||||
- "void WriteAt(int, ulong)"
|
||||
- "void Write(ulong, int)"
|
||||
- "void Write(long)"
|
||||
- "void Write(long, int)"
|
||||
- "void Write(System.Half)"
|
||||
- "void Write(float)"
|
||||
- "void Write(double)"
|
||||
- "int WriteVariableUInt32(uint)"
|
||||
- "int WriteVariableInt32(int)"
|
||||
- "int WriteVariableInt64(long)"
|
||||
- "int WriteVariableUInt64(ulong)"
|
||||
- "void WriteSignedSingle(float, int)"
|
||||
- "void WriteUnitSingle(float, int)"
|
||||
- "void WriteRangedSingle(float, float, float, int)"
|
||||
- "int WriteRangedInteger(int, int, int)"
|
||||
- "int WriteRangedInteger(long, long, long)"
|
||||
- "void Write(string)"
|
||||
- "void Write(System.Net.IPEndPoint)"
|
||||
- "void WriteTime(bool)"
|
||||
- "void WriteTime(double, bool)"
|
||||
- "void WritePadBits()"
|
||||
- "void WritePadBits(int)"
|
||||
- "void Write(Lidgren.Network.NetBuffer)"
|
||||
- "void Zero(int)"
|
||||
- "void .ctor()"
|
||||
NetDeliveryMethod: { }
|
||||
NetIncomingMessage:
|
||||
All: True
|
||||
Methods:
|
||||
- "Lidgren.Network.NetIncomingMessageType get_MessageType()"
|
||||
- "Lidgren.Network.NetDeliveryMethod get_DeliveryMethod()"
|
||||
- "int get_SequenceChannel()"
|
||||
- "System.Net.IPEndPoint get_SenderEndPoint()"
|
||||
- "Lidgren.Network.NetConnection get_SenderConnection()"
|
||||
- "double get_ReceiveTime()"
|
||||
- "double ReadTime(bool)"
|
||||
- "string ToString()"
|
||||
NetOutgoingMessage:
|
||||
All: True
|
||||
Methods:
|
||||
- "string ToString()"
|
||||
Nett:
|
||||
CommentLocation: { } # Enum
|
||||
Toml:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.ContentPack
|
||||
@@ -135,11 +136,37 @@ namespace Robust.Shared.ContentPack
|
||||
path = path.Directory;
|
||||
|
||||
var fullPath = GetFullPath(path);
|
||||
Process.Start(new ProcessStartInfo
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
UseShellExecute = true,
|
||||
FileName = fullPath,
|
||||
});
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = $"{Environment.GetEnvironmentVariable("SystemRoot")}\\explorer.exe",
|
||||
Arguments = ".",
|
||||
WorkingDirectory = fullPath,
|
||||
});
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "open",
|
||||
Arguments = ".",
|
||||
WorkingDirectory = fullPath,
|
||||
});
|
||||
}
|
||||
else if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD())
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "xdg-open",
|
||||
Arguments = ".",
|
||||
WorkingDirectory = fullPath,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Opening OS windows not supported on this OS");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -9,36 +9,19 @@ namespace Robust.Shared.GameObjects;
|
||||
|
||||
public readonly struct CompIdx : IEquatable<CompIdx>
|
||||
{
|
||||
private static readonly ReaderWriterLockSlim SlowStoreLock = new();
|
||||
private static readonly Dictionary<Type, CompIdx> SlowStore = new();
|
||||
|
||||
internal readonly int Value;
|
||||
|
||||
internal static CompIdx Index<T>() => Store<T>.Index;
|
||||
|
||||
internal static CompIdx Index(Type t)
|
||||
{
|
||||
using (SlowStoreLock.ReadGuard())
|
||||
{
|
||||
if (SlowStore.TryGetValue(t, out var idx))
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Doesn't exist in the store, get a write lock and add it.
|
||||
using (SlowStoreLock.WriteGuard())
|
||||
{
|
||||
var idx = (CompIdx)typeof(Store<>)
|
||||
.MakeGenericType(t)
|
||||
.GetField(nameof(Store<int>.Index), BindingFlags.Static | BindingFlags.Public)!
|
||||
.GetValue(null)!;
|
||||
|
||||
SlowStore[t] = idx;
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
|
||||
internal static int ArrayIndex<T>() => Index<T>().Value;
|
||||
internal static int ArrayIndex(Type type) => Index(type).Value;
|
||||
|
||||
internal static CompIdx GetIndex(Type type)
|
||||
{
|
||||
return (CompIdx)typeof(Store<>)
|
||||
.MakeGenericType(type)
|
||||
.GetField(nameof(Store<int>.Index), BindingFlags.Static | BindingFlags.Public)!
|
||||
.GetValue(null)!;
|
||||
}
|
||||
|
||||
internal static void AssignArray<T>(ref T[] array, CompIdx idx, T value)
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Robust.Shared.GameObjects
|
||||
public readonly ComponentEventArgs BaseArgs;
|
||||
public readonly ComponentRegistration ComponentType;
|
||||
|
||||
public AddedComponentEventArgs(ComponentEventArgs baseArgs, ComponentRegistration componentType)
|
||||
internal AddedComponentEventArgs(ComponentEventArgs baseArgs, ComponentRegistration componentType)
|
||||
{
|
||||
BaseArgs = baseArgs;
|
||||
ComponentType = componentType;
|
||||
@@ -48,11 +48,14 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
public readonly MetaDataComponent Meta;
|
||||
|
||||
public RemovedComponentEventArgs(ComponentEventArgs baseArgs, bool terminating, MetaDataComponent meta)
|
||||
public readonly CompIdx Idx;
|
||||
|
||||
internal RemovedComponentEventArgs(ComponentEventArgs baseArgs, bool terminating, MetaDataComponent meta, CompIdx idx)
|
||||
{
|
||||
BaseArgs = baseArgs;
|
||||
Terminating = terminating;
|
||||
Meta = meta;
|
||||
Idx = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
// Bunch of dictionaries to allow lookups in all directions.
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// Mapping of component name to type.
|
||||
/// </summary>
|
||||
private FrozenDictionary<string, ComponentRegistration> _names
|
||||
@@ -63,6 +62,11 @@ namespace Robust.Shared.GameObjects
|
||||
private FrozenDictionary<CompIdx, Type> _idxToType
|
||||
= FrozenDictionary<CompIdx, Type>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Slow-path for Type -> CompIdx mapping without generics.
|
||||
/// </summary>
|
||||
private FrozenDictionary<Type, CompIdx> _typeToIdx = FrozenDictionary<Type, CompIdx>.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<ComponentRegistration[]>? ComponentsAdded;
|
||||
|
||||
@@ -78,6 +82,7 @@ namespace Robust.Shared.GameObjects
|
||||
private IEnumerable<ComponentRegistration> AllRegistrations => _types.Values;
|
||||
|
||||
private ComponentRegistration Register(Type type,
|
||||
CompIdx idx,
|
||||
Dictionary<string, ComponentRegistration> names,
|
||||
Dictionary<string, string> lowerCaseNames,
|
||||
Dictionary<Type, ComponentRegistration> types,
|
||||
@@ -123,8 +128,6 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
var unsaved = type.HasCustomAttribute<UnsavedComponentAttribute>();
|
||||
|
||||
var idx = CompIdx.Index(type);
|
||||
|
||||
var registration = new ComponentRegistration(name, type, idx, unsaved);
|
||||
|
||||
idxToType[idx] = type;
|
||||
@@ -399,6 +402,20 @@ namespace Robust.Shared.GameObjects
|
||||
RegisterTypesInternal(types, false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Pure]
|
||||
public CompIdx GetIndex(Type type)
|
||||
{
|
||||
return _typeToIdx[type];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Pure]
|
||||
public int GetArrayIndex(Type type)
|
||||
{
|
||||
return _typeToIdx[type].Value;
|
||||
}
|
||||
|
||||
private void RegisterTypesInternal(Type[] types, bool overwrite)
|
||||
{
|
||||
var names = _names.ToDictionary();
|
||||
@@ -408,12 +425,19 @@ namespace Robust.Shared.GameObjects
|
||||
var ignored = _ignored.ToHashSet();
|
||||
|
||||
var added = new ComponentRegistration[types.Length];
|
||||
var typeToidx = _typeToIdx.ToDictionary();
|
||||
|
||||
for (int i = 0; i < types.Length; i++)
|
||||
{
|
||||
added[i] = Register(types[i], names, lowerCaseNames, typesDict, idxToType, ignored, overwrite);
|
||||
var type = types[i];
|
||||
var idx = CompIdx.GetIndex(type);
|
||||
typeToidx[type] = idx;
|
||||
|
||||
added[i] = Register(type, idx, names, lowerCaseNames, typesDict, idxToType, ignored, overwrite);
|
||||
}
|
||||
|
||||
var st = RStopwatch.StartNew();
|
||||
_typeToIdx = typeToidx.ToFrozenDictionary();
|
||||
_names = names.ToFrozenDictionary();
|
||||
_lowerCaseNames = lowerCaseNames.ToFrozenDictionary();
|
||||
_types = typesDict.ToFrozenDictionary();
|
||||
|
||||
@@ -5,7 +5,7 @@ using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[RegisterComponent]
|
||||
public sealed partial class ActiveUserInterfaceComponent : Component
|
||||
{
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// Actors that currently have interfaces open.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<Enum, List<EntityUid>> Actors = new();
|
||||
public Dictionary<Enum, HashSet<EntityUid>> Actors = new();
|
||||
|
||||
/// <summary>
|
||||
/// Legacy data, new BUIs should be using comp states.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
@@ -9,17 +7,13 @@ namespace Robust.Shared.GameObjects;
|
||||
/// <summary>
|
||||
/// Stores data about this entity and what BUIs they have open.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
/// <remarks>
|
||||
/// This component is implicitly networked via <see cref="UserInterfaceComponent"/>.
|
||||
/// I.e., the other component is authoritative about what UIs are open
|
||||
/// </remarks>
|
||||
[RegisterComponent]
|
||||
public sealed partial class UserInterfaceUserComponent : Component
|
||||
{
|
||||
public override bool SessionSpecific => true;
|
||||
|
||||
[DataField]
|
||||
public Dictionary<EntityUid, List<Enum>> OpenInterfaces = new();
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
internal sealed class UserInterfaceUserComponentState : IComponentState
|
||||
{
|
||||
public Dictionary<NetEntity, List<Enum>> OpenInterfaces = new();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
public record struct Entity<T>
|
||||
public record struct Entity<T> : IFluentEntityUid
|
||||
where T : IComponent?
|
||||
{
|
||||
public EntityUid Owner;
|
||||
public T Comp;
|
||||
EntityUid IFluentEntityUid.FluentOwner => Owner;
|
||||
|
||||
public Entity(EntityUid owner, T comp)
|
||||
{
|
||||
@@ -45,12 +47,13 @@ public record struct Entity<T>
|
||||
public override int GetHashCode() => Owner.GetHashCode();
|
||||
}
|
||||
|
||||
public record struct Entity<T1, T2>
|
||||
public record struct Entity<T1, T2> : IFluentEntityUid
|
||||
where T1 : IComponent? where T2 : IComponent?
|
||||
{
|
||||
public EntityUid Owner;
|
||||
public T1 Comp1;
|
||||
public T2 Comp2;
|
||||
EntityUid IFluentEntityUid.FluentOwner => Owner;
|
||||
|
||||
public Entity(EntityUid owner, T1 comp1, T2 comp2)
|
||||
{
|
||||
@@ -110,13 +113,14 @@ public record struct Entity<T1, T2>
|
||||
}
|
||||
}
|
||||
|
||||
public record struct Entity<T1, T2, T3>
|
||||
public record struct Entity<T1, T2, T3> : IFluentEntityUid
|
||||
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent?
|
||||
{
|
||||
public EntityUid Owner;
|
||||
public T1 Comp1;
|
||||
public T2 Comp2;
|
||||
public T3 Comp3;
|
||||
EntityUid IFluentEntityUid.FluentOwner => Owner;
|
||||
|
||||
public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3)
|
||||
{
|
||||
@@ -211,7 +215,7 @@ public record struct Entity<T1, T2, T3>
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record struct Entity<T1, T2, T3, T4>
|
||||
public record struct Entity<T1, T2, T3, T4> : IFluentEntityUid
|
||||
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent?
|
||||
{
|
||||
public EntityUid Owner;
|
||||
@@ -219,6 +223,7 @@ public record struct Entity<T1, T2, T3, T4>
|
||||
public T2 Comp2;
|
||||
public T3 Comp3;
|
||||
public T4 Comp4;
|
||||
EntityUid IFluentEntityUid.FluentOwner => Owner;
|
||||
|
||||
public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4)
|
||||
{
|
||||
@@ -336,7 +341,7 @@ public record struct Entity<T1, T2, T3, T4>
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record struct Entity<T1, T2, T3, T4, T5>
|
||||
public record struct Entity<T1, T2, T3, T4, T5> : IFluentEntityUid
|
||||
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent?
|
||||
{
|
||||
public EntityUid Owner;
|
||||
@@ -345,6 +350,7 @@ public record struct Entity<T1, T2, T3, T4, T5>
|
||||
public T3 Comp3;
|
||||
public T4 Comp4;
|
||||
public T5 Comp5;
|
||||
EntityUid IFluentEntityUid.FluentOwner => Owner;
|
||||
|
||||
public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4, T5 comp5)
|
||||
{
|
||||
@@ -485,7 +491,7 @@ public record struct Entity<T1, T2, T3, T4, T5>
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record struct Entity<T1, T2, T3, T4, T5, T6>
|
||||
public record struct Entity<T1, T2, T3, T4, T5, T6> : IFluentEntityUid
|
||||
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent?
|
||||
{
|
||||
public EntityUid Owner;
|
||||
@@ -495,6 +501,7 @@ public record struct Entity<T1, T2, T3, T4, T5, T6>
|
||||
public T4 Comp4;
|
||||
public T5 Comp5;
|
||||
public T6 Comp6;
|
||||
EntityUid IFluentEntityUid.FluentOwner => Owner;
|
||||
|
||||
public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4, T5 comp5, T6 comp6)
|
||||
{
|
||||
@@ -658,7 +665,7 @@ public record struct Entity<T1, T2, T3, T4, T5, T6>
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record struct Entity<T1, T2, T3, T4, T5, T6, T7>
|
||||
public record struct Entity<T1, T2, T3, T4, T5, T6, T7> : IFluentEntityUid
|
||||
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? where T7 : IComponent?
|
||||
{
|
||||
public EntityUid Owner;
|
||||
@@ -669,6 +676,7 @@ public record struct Entity<T1, T2, T3, T4, T5, T6, T7>
|
||||
public T5 Comp5;
|
||||
public T6 Comp6;
|
||||
public T7 Comp7;
|
||||
EntityUid IFluentEntityUid.FluentOwner => Owner;
|
||||
|
||||
public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4, T5 comp5, T6 comp6, T7 comp7)
|
||||
{
|
||||
@@ -855,7 +863,7 @@ public record struct Entity<T1, T2, T3, T4, T5, T6, T7>
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record struct Entity<T1, T2, T3, T4, T5, T6, T7, T8>
|
||||
public record struct Entity<T1, T2, T3, T4, T5, T6, T7, T8> : IFluentEntityUid
|
||||
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? where T7 : IComponent? where T8 : IComponent?
|
||||
{
|
||||
public EntityUid Owner;
|
||||
@@ -867,6 +875,7 @@ public record struct Entity<T1, T2, T3, T4, T5, T6, T7, T8>
|
||||
public T6 Comp6;
|
||||
public T7 Comp7;
|
||||
public T8 Comp8;
|
||||
EntityUid IFluentEntityUid.FluentOwner => Owner;
|
||||
|
||||
public Entity(EntityUid owner, T1 comp1, T2 comp2, T3 comp3, T4 comp4, T5 comp5, T6 comp6, T7 comp7, T8 comp8)
|
||||
{
|
||||
|
||||
@@ -69,39 +69,29 @@ namespace Robust.Shared.GameObjects
|
||||
/// DO NOT USE THIS IN CONTENT UNLESS YOU KNOW WHAT YOU'RE DOING, the only reason it's not internal
|
||||
/// is because of the component network source generator.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TEvent">Event to dispatch.</typeparam>
|
||||
/// <param name="component">Component receiving the event.</param>
|
||||
/// <param name="args">Event arguments for the event.</param>
|
||||
public void RaiseComponentEvent<TEvent>(IComponent component, TEvent args)
|
||||
public void RaiseComponentEvent<TEvent, TComponent>(EntityUid uid, TComponent component, TEvent args)
|
||||
where TEvent : notnull
|
||||
where TComponent : IComponent;
|
||||
|
||||
/// <inheritdoc cref="RaiseComponentEvent{TEvent,TComponent}(Robust.Shared.GameObjects.EntityUid,TComponent,TEvent)"/>
|
||||
public void RaiseComponentEvent<TEvent>(EntityUid uid, IComponent component, TEvent args)
|
||||
where TEvent : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an event directly to a specific component.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This has a very specific purpose, and has massive potential to be abused.
|
||||
/// DO NOT USE THIS IN CONTENT UNLESS YOU KNOW WHAT YOU'RE DOING, the only reason it's not internal
|
||||
/// is because of the component network source generator.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TEvent">Event to dispatch.</typeparam>
|
||||
/// <param name="component">Component receiving the event.</param>
|
||||
/// <param name="idx">Type of the component, for faster lookups.</param>
|
||||
/// <param name="args">Event arguments for the event.</param>
|
||||
public void RaiseComponentEvent<TEvent>(IComponent component, CompIdx idx, TEvent args)
|
||||
/// <inheritdoc cref="RaiseComponentEvent{TEvent,TComponent}(Robust.Shared.GameObjects.EntityUid,TComponent,TEvent)"/>
|
||||
public void RaiseComponentEvent<TEvent>(EntityUid uid, IComponent component, CompIdx idx, TEvent args)
|
||||
where TEvent : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an event directly to a specific component, by-ref.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This has a very specific purpose, and has massive potential to be abused.
|
||||
/// DO NOT USE THIS IN CONTENT UNLESS YOU KNOW WHAT YOU'RE DOING, the only reason it's not internal
|
||||
/// is because of the component network source generator.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TEvent">Event to dispatch.</typeparam>
|
||||
/// <param name="component">Component receiving the event.</param>
|
||||
/// <param name="args">Event arguments for the event.</param>
|
||||
public void RaiseComponentEvent<TEvent>(IComponent component, ref TEvent args)
|
||||
/// <inheritdoc cref="RaiseComponentEvent{TEvent,TComponent}(Robust.Shared.GameObjects.EntityUid,TComponent,TEvent)"/>
|
||||
public void RaiseComponentEvent<TEvent>(EntityUid uid, IComponent component, ref TEvent args)
|
||||
where TEvent : notnull;
|
||||
|
||||
/// <inheritdoc cref="RaiseComponentEvent{TEvent,TComponent}(Robust.Shared.GameObjects.EntityUid,TComponent,TEvent)"/>
|
||||
public void RaiseComponentEvent<TEvent, TComponent>(EntityUid uid, TComponent component, ref TEvent args)
|
||||
where TEvent : notnull
|
||||
where TComponent : IComponent;
|
||||
|
||||
/// <inheritdoc cref="RaiseComponentEvent{TEvent,TComponent}(Robust.Shared.GameObjects.EntityUid,TComponent,TEvent)"/>
|
||||
public void RaiseComponentEvent<TEvent>(EntityUid uid, IComponent component, CompIdx idx, ref TEvent args)
|
||||
where TEvent : notnull;
|
||||
|
||||
public void OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare();
|
||||
@@ -114,6 +104,15 @@ namespace Robust.Shared.GameObjects
|
||||
private delegate void DirectedEventHandler<TEvent>(EntityUid uid, IComponent comp, ref TEvent args)
|
||||
where TEvent : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Max size of a components event subscription linked list.
|
||||
/// Used to limit the stackalloc in <see cref="EntDispatch"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// SS14 currently requires only 18, I doubt it will ever need to exceed 256.
|
||||
/// </remarks>
|
||||
private const int MaxEventLinkedListSize = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of <see cref="EntityEventBus"/>.
|
||||
/// </summary>
|
||||
@@ -131,37 +130,58 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IDirectedEventBus.RaiseComponentEvent<TEvent>(IComponent component, TEvent args)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RaiseComponentEvent<TEvent>(EntityUid uid, IComponent component, TEvent args)
|
||||
where TEvent : notnull
|
||||
{
|
||||
ref var unitRef = ref Unsafe.As<TEvent, Unit>(ref args);
|
||||
|
||||
DispatchComponent<TEvent>(
|
||||
component.Owner,
|
||||
component,
|
||||
CompIdx.Index(component.GetType()),
|
||||
ref unitRef);
|
||||
}
|
||||
|
||||
void IDirectedEventBus.RaiseComponentEvent<TEvent>(IComponent component, CompIdx type, TEvent args)
|
||||
{
|
||||
ref var unitRef = ref Unsafe.As<TEvent, Unit>(ref args);
|
||||
|
||||
DispatchComponent<TEvent>(
|
||||
component.Owner,
|
||||
component,
|
||||
type,
|
||||
ref unitRef);
|
||||
RaiseComponentEvent(uid, component, _comFac.GetIndex(component.GetType()), ref args);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IDirectedEventBus.RaiseComponentEvent<TEvent>(IComponent component, ref TEvent args)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RaiseComponentEvent<TEvent, TComponent>(EntityUid uid, TComponent component, TEvent args)
|
||||
where TEvent : notnull
|
||||
where TComponent : IComponent
|
||||
{
|
||||
RaiseComponentEvent(uid, component, CompIdx.Index<TComponent>(), ref args);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RaiseComponentEvent<TEvent>(EntityUid uid, IComponent component, CompIdx type, TEvent args)
|
||||
where TEvent : notnull
|
||||
{
|
||||
RaiseComponentEvent(uid, component, type, ref args);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RaiseComponentEvent<TEvent>(EntityUid uid, IComponent component, ref TEvent args)
|
||||
where TEvent : notnull
|
||||
{
|
||||
RaiseComponentEvent(uid, component, _comFac.GetIndex(component.GetType()), ref args);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RaiseComponentEvent<TEvent, TComponent>(EntityUid uid, TComponent component, ref TEvent args)
|
||||
where TEvent : notnull
|
||||
where TComponent : IComponent
|
||||
{
|
||||
RaiseComponentEvent(uid, component, CompIdx.Index<TComponent>(), ref args);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RaiseComponentEvent<TEvent>(EntityUid uid, IComponent component, CompIdx type, ref TEvent args)
|
||||
where TEvent : notnull
|
||||
{
|
||||
ref var unitRef = ref Unsafe.As<TEvent, Unit>(ref args);
|
||||
|
||||
DispatchComponent<TEvent>(
|
||||
component.Owner,
|
||||
uid,
|
||||
component,
|
||||
CompIdx.Index(component.GetType()),
|
||||
type,
|
||||
ref unitRef);
|
||||
}
|
||||
|
||||
@@ -390,7 +410,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
public void OnComponentRemoved(in RemovedComponentEventArgs e)
|
||||
{
|
||||
EntRemoveComponent(e.BaseArgs.Owner, CompIdx.Index(e.BaseArgs.Component.GetType()));
|
||||
EntRemoveComponent(e.BaseArgs.Owner, e.Idx);
|
||||
}
|
||||
|
||||
private void EntAddSubscription(
|
||||
@@ -488,7 +508,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
DebugTools.Assert(eventTable.Free >= 0);
|
||||
|
||||
ref var eventStartIdx = ref CollectionsMarshal.GetValueRefOrAddDefault(
|
||||
ref var indices = ref CollectionsMarshal.GetValueRefOrAddDefault(
|
||||
eventTable.EventIndices,
|
||||
evType,
|
||||
out var exists);
|
||||
@@ -500,10 +520,13 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
// Set it up
|
||||
entry.Component = compType;
|
||||
entry.Next = exists ? eventStartIdx : -1;
|
||||
entry.Next = exists ? indices.Start : -1;
|
||||
|
||||
// Assign new list entry to EventIndices dictionary.
|
||||
eventStartIdx = entryIdx;
|
||||
indices.Start = entryIdx;
|
||||
indices.Count++;
|
||||
if (indices.Count > MaxEventLinkedListSize)
|
||||
throw new NotSupportedException($"Exceeded maximum event linked list size. Need to implement stackalloc fallback.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,40 +564,37 @@ namespace Robust.Shared.GameObjects
|
||||
foreach (var evType in compSubs.Keys)
|
||||
{
|
||||
DebugTools.Assert(!_eventData[evType].ComponentEvent);
|
||||
ref var dictIdx = ref CollectionsMarshal.GetValueRefOrNullRef(eventTable.EventIndices, evType);
|
||||
if (Unsafe.IsNullRef(ref dictIdx))
|
||||
ref var indices = ref CollectionsMarshal.GetValueRefOrNullRef(eventTable.EventIndices, evType);
|
||||
if (Unsafe.IsNullRef(ref indices))
|
||||
{
|
||||
DebugTools.Assert("This should not be possible. Were the events for this component never added?");
|
||||
continue;
|
||||
}
|
||||
|
||||
ref var updateNext = ref dictIdx;
|
||||
var entryIdx = indices.Start;
|
||||
ref var entry = ref eventTable.ComponentLists[entryIdx];
|
||||
|
||||
// Go over linked list to find index of component.
|
||||
var entryIdx = dictIdx;
|
||||
ref var entry = ref Unsafe.NullRef<EventTableListEntry>();
|
||||
while (true)
|
||||
{
|
||||
entry = ref eventTable.ComponentLists[entryIdx];
|
||||
if (entry.Component == compType)
|
||||
{
|
||||
// Found
|
||||
break;
|
||||
}
|
||||
|
||||
entryIdx = entry.Next;
|
||||
updateNext = ref entry.Next;
|
||||
}
|
||||
|
||||
if (entry.Next == -1 && Unsafe.AreSame(ref dictIdx, ref updateNext))
|
||||
if (indices.Count == 1)
|
||||
{
|
||||
// Last entry for this event type, remove from dict.
|
||||
DebugTools.AssertEqual(entry.Next, -1);
|
||||
eventTable.EventIndices.Remove(evType);
|
||||
}
|
||||
else
|
||||
{
|
||||
ref var updateNext = ref indices.Start;
|
||||
|
||||
// Go over linked list to find index of component.
|
||||
while (entry.Component != compType)
|
||||
{
|
||||
updateNext = ref entry.Next;
|
||||
entryIdx = entry.Next;
|
||||
entry = ref eventTable.ComponentLists[entryIdx];
|
||||
}
|
||||
|
||||
// Rewrite previous index to point to next in chain.
|
||||
updateNext = entry.Next;
|
||||
indices.Count--;
|
||||
}
|
||||
|
||||
// Push entry back onto free list.
|
||||
@@ -585,15 +605,33 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
private void EntDispatch(EntityUid euid, Type eventType, ref Unit args)
|
||||
{
|
||||
if (!EntTryGetSubscriptions(eventType, euid, out var enumerator))
|
||||
if (!_entEventTables.TryGetValue(euid, out var eventTable))
|
||||
return;
|
||||
|
||||
while (enumerator.MoveNext(out var component, out var reg))
|
||||
{
|
||||
if (component.Deleted)
|
||||
continue;
|
||||
if (!eventTable.EventIndices.TryGetValue(eventType, out var indices))
|
||||
return;
|
||||
|
||||
reg.Handler(euid, component, ref args);
|
||||
DebugTools.Assert(indices.Count > 0);
|
||||
DebugTools.Assert(indices.Start >= 0);
|
||||
|
||||
// First, collect all subscribing components.
|
||||
// This is to avoid infinite loops over the linked list if subscription handlers add or remove components.
|
||||
Span<CompIdx> compIds = stackalloc CompIdx[indices.Count];
|
||||
var idx = indices.Start;
|
||||
for (var index = 0; index < compIds.Length; index++)
|
||||
{
|
||||
DebugTools.Assert(idx >= 0);
|
||||
ref var entry = ref eventTable.ComponentLists[idx];
|
||||
idx = entry.Next;
|
||||
compIds[index] = entry.Component;
|
||||
}
|
||||
|
||||
foreach (var compIdx in compIds)
|
||||
{
|
||||
if (!_entMan.TryGetComponent(euid, compIdx, out var comp))
|
||||
continue;
|
||||
var compSubs = _eventSubs[compIdx.Value];
|
||||
compSubs[eventType].Handler(euid, comp, ref args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,16 +640,30 @@ namespace Robust.Shared.GameObjects
|
||||
Type eventType,
|
||||
ref ValueList<OrderedEventDispatch> found)
|
||||
{
|
||||
if (!EntTryGetSubscriptions(eventType, euid, out var enumerator))
|
||||
if (!_entEventTables.TryGetValue(euid, out var eventTable))
|
||||
return;
|
||||
|
||||
while (enumerator.MoveNext(out var component, out var reg))
|
||||
if (!eventTable.EventIndices.TryGetValue(eventType, out var indices))
|
||||
return;
|
||||
|
||||
DebugTools.Assert(indices.Count > 0);
|
||||
DebugTools.Assert(indices.Start >= 0);
|
||||
var idx = indices.Start;
|
||||
while (idx != -1)
|
||||
{
|
||||
found.Add(new OrderedEventDispatch((ref Unit ev) =>
|
||||
{
|
||||
if (!component.Deleted)
|
||||
reg.Handler(euid, component, ref ev);
|
||||
}, reg.Order));
|
||||
ref var entry = ref eventTable.ComponentLists[idx];
|
||||
idx = entry.Next;
|
||||
var comp = _entMan.GetComponentInternal(euid, entry.Component);
|
||||
var compSubs = _eventSubs[entry.Component.Value];
|
||||
var reg = compSubs[eventType];
|
||||
|
||||
found.Add(new OrderedEventDispatch(
|
||||
(ref Unit ev) =>
|
||||
{
|
||||
if (!comp.Deleted)
|
||||
reg.Handler(euid, comp, ref ev);
|
||||
},
|
||||
reg.Order));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,28 +678,6 @@ namespace Robust.Shared.GameObjects
|
||||
reg.Handler(euid, component, ref args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates all subscriptions for an event on a specific entity, returning the component instances and registrations.
|
||||
/// </summary>
|
||||
private bool EntTryGetSubscriptions(Type eventType, EntityUid euid, out SubscriptionsEnumerator enumerator)
|
||||
{
|
||||
if (!_entEventTables.TryGetValue(euid, out var eventTable))
|
||||
{
|
||||
enumerator = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
// No subscriptions to this event type, return null.
|
||||
if (!eventTable.EventIndices.TryGetValue(eventType, out var startEntry))
|
||||
{
|
||||
enumerator = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
enumerator = new(eventType, startEntry, eventTable.ComponentLists, _eventSubs, euid, _entMan);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ClearSubscriptions()
|
||||
{
|
||||
_subscriptionLock = false;
|
||||
@@ -678,59 +708,6 @@ namespace Robust.Shared.GameObjects
|
||||
_eventSubsInv = null!;
|
||||
}
|
||||
|
||||
private struct SubscriptionsEnumerator
|
||||
{
|
||||
private readonly Type _eventType;
|
||||
private readonly EntityUid _uid;
|
||||
private readonly FrozenDictionary<Type, DirectedRegistration>[] _subscriptions;
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly EventTableListEntry[] _list;
|
||||
private int _idx;
|
||||
|
||||
public SubscriptionsEnumerator(
|
||||
Type eventType,
|
||||
int startEntry,
|
||||
EventTableListEntry[] list,
|
||||
FrozenDictionary<Type, DirectedRegistration>[] subscriptions,
|
||||
EntityUid uid,
|
||||
IEntityManager entityManager)
|
||||
{
|
||||
_eventType = eventType;
|
||||
_list = list;
|
||||
_subscriptions = subscriptions;
|
||||
_idx = startEntry;
|
||||
_entityManager = entityManager;
|
||||
_uid = uid;
|
||||
}
|
||||
|
||||
public bool MoveNext(
|
||||
[NotNullWhen(true)] out IComponent? component,
|
||||
[NotNullWhen(true)] out DirectedRegistration? registration)
|
||||
{
|
||||
if (_idx == -1)
|
||||
{
|
||||
component = null;
|
||||
registration = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
ref var entry = ref _list[_idx];
|
||||
_idx = entry.Next;
|
||||
|
||||
var compType = entry.Component;
|
||||
var compSubs = _subscriptions[compType.Value];
|
||||
|
||||
if (!compSubs.TryGetValue(_eventType, out registration))
|
||||
{
|
||||
component = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
component = _entityManager.GetComponentInternal(_uid, compType);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DirectedRegistration : OrderedRegistration
|
||||
{
|
||||
public readonly Delegate Original;
|
||||
@@ -760,7 +737,7 @@ namespace Robust.Shared.GameObjects
|
||||
// Free contains the first free linked list node, or -1 if there is none.
|
||||
// Free nodes form their own linked list.
|
||||
// ComponentList is the actual region of memory containing linked list nodes.
|
||||
public readonly Dictionary<Type, int> EventIndices = new();
|
||||
public readonly Dictionary<Type, (int Start, int Count)> EventIndices = new();
|
||||
public int Free;
|
||||
public EventTableListEntry[] ComponentLists = new EventTableListEntry[InitialListSize];
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ namespace Robust.Shared.GameObjects
|
||||
if (broadcast)
|
||||
CollectBroadcastOrdered(EventSource.Local, subs, ref found);
|
||||
|
||||
// TODO PERF
|
||||
// consider ordering the linked list itself?
|
||||
// Then make broadcast events always a lower priority and replace the valuelist with stackalloc?
|
||||
EntCollectOrdered(uid, eventType, ref found);
|
||||
|
||||
DispatchOrderedEvents(ref unitRef, ref found);
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Log;
|
||||
@@ -124,8 +125,8 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
foreach (var comp in comps)
|
||||
{
|
||||
if (comp is { LifeStage: ComponentLifeStage.Added })
|
||||
LifeInitialize(comp, CompIdx.Index(comp.GetType()));
|
||||
if (comp is {LifeStage: ComponentLifeStage.Added})
|
||||
LifeInitialize(uid, comp, _componentFactory.GetIndex(comp.GetType()));
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@@ -158,19 +159,19 @@ namespace Robust.Shared.GameObjects
|
||||
// Init transform first, we always have it.
|
||||
var transform = TransformQuery.GetComponent(uid);
|
||||
if (transform.LifeStage == ComponentLifeStage.Initialized)
|
||||
LifeStartup(transform);
|
||||
LifeStartup(uid, transform, CompIdx.Index<TransformComponent>());
|
||||
|
||||
// Init physics second if it exists.
|
||||
if (_physicsQuery.TryComp(uid, out var phys) && phys.LifeStage == ComponentLifeStage.Initialized)
|
||||
{
|
||||
LifeStartup(phys);
|
||||
LifeStartup(uid, phys, CompIdx.Index<PhysicsComponent>());
|
||||
}
|
||||
|
||||
// Do rest of components.
|
||||
foreach (var comp in comps)
|
||||
{
|
||||
if (comp is { LifeStage: ComponentLifeStage.Initialized })
|
||||
LifeStartup(comp);
|
||||
LifeStartup(uid, comp, _componentFactory.GetIndex(comp.GetType()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,10 +267,10 @@ namespace Robust.Shared.GameObjects
|
||||
return;
|
||||
|
||||
if (!Comp.Initialized)
|
||||
((EntityManager) _entMan).LifeInitialize(Comp, CompType);
|
||||
((EntityManager) _entMan).LifeInitialize(_owner, Comp, CompType);
|
||||
|
||||
if (metadata.EntityInitialized && !Comp.Running)
|
||||
((EntityManager) _entMan).LifeStartup(Comp);
|
||||
((EntityManager) _entMan).LifeStartup(_owner, Comp, CompType);
|
||||
}
|
||||
|
||||
public static implicit operator T(CompInitializeHandle<T> handle)
|
||||
@@ -354,7 +355,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
// This will invalidate the comp ref as it removes the key from the dictionary.
|
||||
// This is inefficient, but component overriding rarely ever happens.
|
||||
RemoveComponentImmediate(comp!, uid, false, metadata);
|
||||
RemoveComponentImmediate(uid, comp!, type, false, metadata);
|
||||
dict.Add(uid, component);
|
||||
}
|
||||
else
|
||||
@@ -381,7 +382,7 @@ namespace Robust.Shared.GameObjects
|
||||
ComponentAdded?.Invoke(eventArgs);
|
||||
_eventBus.OnComponentAdded(eventArgs);
|
||||
|
||||
LifeAddToEntity(component, reg.Idx);
|
||||
LifeAddToEntity(uid, component, reg.Idx);
|
||||
|
||||
if (skipInit)
|
||||
return;
|
||||
@@ -394,20 +395,24 @@ namespace Robust.Shared.GameObjects
|
||||
if (component.Networked)
|
||||
DirtyEntity(uid, metadata);
|
||||
|
||||
LifeInitialize(component, reg.Idx);
|
||||
LifeInitialize(uid, component, reg.Idx);
|
||||
|
||||
if (metadata.EntityInitialized)
|
||||
LifeStartup(component);
|
||||
LifeStartup(uid, component, reg.Idx);
|
||||
|
||||
if (metadata.EntityLifeStage >= EntityLifeStage.MapInitialized)
|
||||
EventBus.RaiseComponentEvent(component, MapInitEventInstance);
|
||||
EventBus.RaiseComponentEvent(uid, component, reg.Idx, MapInitEventInstance);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool RemoveComponent<T>(EntityUid uid, MetaDataComponent? meta = null)
|
||||
public bool RemoveComponent<T>(EntityUid uid, MetaDataComponent? meta = null) where T : IComponent
|
||||
{
|
||||
return RemoveComponent(uid, typeof(T), meta);
|
||||
if (!TryGetComponent(uid, out T? comp))
|
||||
return false;
|
||||
|
||||
RemoveComponentImmediate(uid, comp, CompIdx.Index<T>(), false, meta);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -417,7 +422,7 @@ namespace Robust.Shared.GameObjects
|
||||
if (!TryGetComponent(uid, type, out var comp))
|
||||
return false;
|
||||
|
||||
RemoveComponentImmediate(comp, uid, false, meta);
|
||||
RemoveComponentImmediate(uid, comp, _componentFactory.GetIndex(type), false, meta);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -431,7 +436,8 @@ namespace Robust.Shared.GameObjects
|
||||
if (!TryGetComponent(uid, netId, out var comp, meta))
|
||||
return false;
|
||||
|
||||
RemoveComponentImmediate(comp, uid, false, meta);
|
||||
var idx = _componentFactory.GetIndex(comp.GetType());
|
||||
RemoveComponentImmediate(uid, comp, idx, false, meta);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -439,7 +445,8 @@ namespace Robust.Shared.GameObjects
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RemoveComponent(EntityUid uid, IComponent component, MetaDataComponent? meta = null)
|
||||
{
|
||||
RemoveComponentImmediate(component, uid, false, meta);
|
||||
var idx = _componentFactory.GetIndex(component.GetType());
|
||||
RemoveComponentImmediate(uid, component, idx, false, meta);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -509,7 +516,8 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
foreach (var comp in InSafeOrder(_entCompIndex[uid]))
|
||||
{
|
||||
RemoveComponentImmediate(comp, uid, false, meta);
|
||||
var idx = _componentFactory.GetIndex(comp.GetType());
|
||||
RemoveComponentImmediate(uid, comp, idx, false, meta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,7 +531,8 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
try
|
||||
{
|
||||
RemoveComponentImmediate(comp, uid, true, meta);
|
||||
var idx = _componentFactory.GetIndex(comp.GetType());
|
||||
RemoveComponentImmediate(uid, comp, idx, true, meta);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -536,14 +545,16 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
private void RemoveComponentDeferred(IComponent component, EntityUid uid, bool terminating)
|
||||
{
|
||||
if (component == null) throw new ArgumentNullException(nameof(component));
|
||||
if (component == null)
|
||||
throw new ArgumentNullException(nameof(component));
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (component.Owner != uid)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
throw new InvalidOperationException("Component is not owned by entity.");
|
||||
|
||||
if (component.Deleted) return;
|
||||
if (component.Deleted)
|
||||
return;
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
@@ -563,7 +574,7 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
|
||||
if (component.LifeStage >= ComponentLifeStage.Initialized && component.LifeStage <= ComponentLifeStage.Running)
|
||||
LifeShutdown(component);
|
||||
LifeShutdown(uid, component, _componentFactory.GetIndex(component.GetType()));
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -574,7 +585,11 @@ namespace Robust.Shared.GameObjects
|
||||
#endif
|
||||
}
|
||||
|
||||
private void RemoveComponentImmediate(IComponent component, EntityUid uid, bool terminating,
|
||||
private void RemoveComponentImmediate(
|
||||
EntityUid uid,
|
||||
IComponent component,
|
||||
CompIdx idx,
|
||||
bool terminating,
|
||||
MetaDataComponent? meta)
|
||||
{
|
||||
if (component.Deleted)
|
||||
@@ -595,10 +610,10 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
|
||||
if (component.Running)
|
||||
LifeShutdown(component);
|
||||
LifeShutdown(uid, component, idx);
|
||||
|
||||
if (component.LifeStage != ComponentLifeStage.PreAdd)
|
||||
LifeRemoveFromEntity(component); // Sets delete
|
||||
LifeRemoveFromEntity(uid, component, idx); // Sets delete
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
@@ -608,7 +623,7 @@ namespace Robust.Shared.GameObjects
|
||||
_runtimeLog.LogException(e, nameof(RemoveComponentImmediate));
|
||||
}
|
||||
#endif
|
||||
DeleteComponent(uid, component, terminating, meta);
|
||||
DeleteComponent(uid, component, idx, terminating, meta);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -619,6 +634,7 @@ namespace Robust.Shared.GameObjects
|
||||
if (component.Deleted)
|
||||
continue;
|
||||
var uid = component.Owner;
|
||||
var idx = _componentFactory.GetIndex(component.GetType());
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
try
|
||||
@@ -629,11 +645,11 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
// TODO add options to cancel deferred deletion?
|
||||
_sawmill.Warning($"Found a running component while culling deferred deletions, owner={ToPrettyString(uid)}, type={component.GetType()}");
|
||||
LifeShutdown(component);
|
||||
LifeShutdown(uid, component, idx);
|
||||
}
|
||||
|
||||
if (component.LifeStage != ComponentLifeStage.PreAdd)
|
||||
LifeRemoveFromEntity(component);
|
||||
LifeRemoveFromEntity(uid, component, idx);
|
||||
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
@@ -644,43 +660,49 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
#endif
|
||||
var meta = MetaQuery.GetComponent(uid);
|
||||
DeleteComponent(uid, component, false, meta);
|
||||
DeleteComponent(uid, component, idx, false, meta);
|
||||
}
|
||||
|
||||
_deleteSet.Clear();
|
||||
}
|
||||
|
||||
private void DeleteComponent(EntityUid entityUid, IComponent component, bool terminating, MetaDataComponent? metadata)
|
||||
private void DeleteComponent(
|
||||
EntityUid entityUid,
|
||||
IComponent component,
|
||||
CompIdx idx,
|
||||
bool terminating,
|
||||
MetaDataComponent? metadata)
|
||||
{
|
||||
if (!MetaQuery.ResolveInternal(entityUid, ref metadata))
|
||||
return;
|
||||
|
||||
var eventArgs = new RemovedComponentEventArgs(new ComponentEventArgs(component, entityUid), false, metadata);
|
||||
var eventArgs = new RemovedComponentEventArgs(new ComponentEventArgs(component, entityUid), false, metadata, idx);
|
||||
ComponentRemoved?.Invoke(eventArgs);
|
||||
_eventBus.OnComponentRemoved(eventArgs);
|
||||
|
||||
var reg = _componentFactory.GetRegistration(component);
|
||||
DebugTools.Assert(component.Networked == (reg.NetID != null));
|
||||
|
||||
if (!terminating && reg.NetID != null)
|
||||
if (!terminating)
|
||||
{
|
||||
if (!metadata.NetComponents.Remove(reg.NetID.Value))
|
||||
_sawmill.Error($"Entity {ToPrettyString(entityUid, metadata)} did not have {component.GetType().Name} in its networked component dictionary during component deletion.");
|
||||
|
||||
if (component.NetSyncEnabled)
|
||||
var reg = _componentFactory.GetRegistration(component);
|
||||
DebugTools.Assert(component.Networked == (reg.NetID != null));
|
||||
if (reg.NetID != null)
|
||||
{
|
||||
DirtyEntity(entityUid, metadata);
|
||||
metadata.LastComponentRemoved = _gameTiming.CurTick;
|
||||
if (!metadata.NetComponents.Remove(reg.NetID.Value))
|
||||
_sawmill.Error($"Entity {ToPrettyString(entityUid, metadata)} did not have {component.GetType().Name} in its networked component dictionary during component deletion.");
|
||||
|
||||
if (component.NetSyncEnabled)
|
||||
{
|
||||
DirtyEntity(entityUid, metadata);
|
||||
metadata.LastComponentRemoved = _gameTiming.CurTick;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_entTraitArray[reg.Idx.Value].Remove(entityUid);
|
||||
_entTraitArray[idx.Value].Remove(entityUid);
|
||||
|
||||
// TODO if terminating the entity, maybe defer this?
|
||||
// _entCompIndex.Remove(uid) gets called later on anyways.
|
||||
_entCompIndex.Remove(entityUid, component);
|
||||
|
||||
|
||||
DebugTools.Assert(_netMan.IsClient // Client side prediction can set LastComponentRemoved to some future tick,
|
||||
|| metadata.EntityLastModifiedTick >= metadata.LastComponentRemoved);
|
||||
}
|
||||
@@ -998,7 +1020,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
public EntityQuery<IComponent> GetEntityQuery(Type type)
|
||||
{
|
||||
var comps = _entTraitArray[CompIdx.ArrayIndex(type)];
|
||||
var comps = _entTraitDict[type];
|
||||
DebugTools.Assert(comps != null, $"Unknown component: {type.Name}");
|
||||
return new EntityQuery<IComponent>(comps, _resolveSawmill);
|
||||
}
|
||||
@@ -1406,7 +1428,7 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
DebugTools.Assert(component.NetSyncEnabled, $"Attempting to get component state for an un-synced component: {component.GetType()}");
|
||||
var getState = new ComponentGetState(session, fromTick);
|
||||
eventBus.RaiseComponentEvent(component, ref getState);
|
||||
eventBus.RaiseComponentEvent(component.Owner, component, ref getState);
|
||||
|
||||
return getState.State;
|
||||
}
|
||||
@@ -1414,7 +1436,7 @@ namespace Robust.Shared.GameObjects
|
||||
public bool CanGetComponentState(IEventBus eventBus, IComponent component, ICommonSession player)
|
||||
{
|
||||
var attempt = new ComponentGetStateAttemptEvent(player);
|
||||
eventBus.RaiseComponentEvent(component, ref attempt);
|
||||
eventBus.RaiseComponentEvent(component.Owner, component, ref attempt);
|
||||
return !attempt.Cancelled;
|
||||
}
|
||||
|
||||
@@ -1563,7 +1585,9 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
|
||||
if (logMissing)
|
||||
_sawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {uid}!\n{new StackTrace(1, true)}");
|
||||
{
|
||||
_sawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {uid}!\n{Environment.StackTrace}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public partial class EntityManager
|
||||
/// Increases the life stage from <see cref="ComponentLifeStage.PreAdd" /> to <see cref="ComponentLifeStage.Added" />,
|
||||
/// after raising a <see cref="ComponentAdd"/> event.
|
||||
/// </summary>
|
||||
internal void LifeAddToEntity(IComponent component, CompIdx type)
|
||||
internal void LifeAddToEntity(EntityUid uid, IComponent component, CompIdx idx)
|
||||
{
|
||||
DebugTools.Assert(component.LifeStage == ComponentLifeStage.PreAdd);
|
||||
|
||||
@@ -23,7 +23,7 @@ public partial class EntityManager
|
||||
component.CreationTick = CurrentTick;
|
||||
// networked components are assumed to be dirty when added to entities. See also: ClearTicks()
|
||||
component.LastModifiedTick = CurrentTick;
|
||||
EventBus.RaiseComponentEvent(component, type, CompAddInstance);
|
||||
EventBus.RaiseComponentEvent(uid, component, idx, CompAddInstance);
|
||||
component.LifeStage = ComponentLifeStage.Added;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
@@ -32,12 +32,12 @@ public partial class EntityManager
|
||||
/// Increases the life stage from <see cref="ComponentLifeStage.Added" /> to <see cref="ComponentLifeStage.Initialized" />,
|
||||
/// calling <see cref="Initialize" />.
|
||||
/// </summary>
|
||||
internal void LifeInitialize(IComponent component, CompIdx type)
|
||||
internal void LifeInitialize(EntityUid uid, IComponent component, CompIdx idx)
|
||||
{
|
||||
DebugTools.Assert(component.LifeStage == ComponentLifeStage.Added);
|
||||
|
||||
component.LifeStage = ComponentLifeStage.Initializing;
|
||||
EventBus.RaiseComponentEvent(component, type, CompInitInstance);
|
||||
EventBus.RaiseComponentEvent(uid, component, idx, CompInitInstance);
|
||||
component.LifeStage = ComponentLifeStage.Initialized;
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@ public partial class EntityManager
|
||||
/// Increases the life stage from <see cref="ComponentLifeStage.Initialized" /> to
|
||||
/// <see cref="ComponentLifeStage.Running" />, calling <see cref="Startup" />.
|
||||
/// </summary>
|
||||
internal void LifeStartup(IComponent component)
|
||||
internal void LifeStartup(EntityUid uid, IComponent component, CompIdx idx)
|
||||
{
|
||||
DebugTools.Assert(component.LifeStage == ComponentLifeStage.Initialized);
|
||||
|
||||
component.LifeStage = ComponentLifeStage.Starting;
|
||||
EventBus.RaiseComponentEvent(component, CompStartupInstance);
|
||||
EventBus.RaiseComponentEvent(uid, component, idx, CompStartupInstance);
|
||||
component.LifeStage = ComponentLifeStage.Running;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public partial class EntityManager
|
||||
/// <remarks>
|
||||
/// Components are allowed to remove themselves in their own Startup function.
|
||||
/// </remarks>
|
||||
internal void LifeShutdown(IComponent component)
|
||||
internal void LifeShutdown(EntityUid uid, IComponent component, CompIdx idx)
|
||||
{
|
||||
DebugTools.Assert(component.LifeStage is >= ComponentLifeStage.Initializing and < ComponentLifeStage.Stopping);
|
||||
|
||||
@@ -73,7 +73,7 @@ public partial class EntityManager
|
||||
}
|
||||
|
||||
component.LifeStage = ComponentLifeStage.Stopping;
|
||||
EventBus.RaiseComponentEvent(component, CompShutdownInstance);
|
||||
EventBus.RaiseComponentEvent(uid, component, idx, CompShutdownInstance);
|
||||
component.LifeStage = ComponentLifeStage.Stopped;
|
||||
}
|
||||
|
||||
@@ -81,13 +81,13 @@ public partial class EntityManager
|
||||
/// Increases the life stage from <see cref="ComponentLifeStage.Stopped" /> to <see cref="ComponentLifeStage.Deleted" />,
|
||||
/// calling <see cref="Component.OnRemove" />.
|
||||
/// </summary>
|
||||
internal void LifeRemoveFromEntity(IComponent component)
|
||||
internal void LifeRemoveFromEntity(EntityUid uid, IComponent component, CompIdx idx)
|
||||
{
|
||||
// can be called at any time after PreAdd, including inside other life stage events.
|
||||
DebugTools.Assert(component.LifeStage != ComponentLifeStage.PreAdd);
|
||||
|
||||
component.LifeStage = ComponentLifeStage.Removing;
|
||||
EventBus.RaiseComponentEvent(component, CompRemoveInstance);
|
||||
EventBus.RaiseComponentEvent(uid, component, idx, CompRemoveInstance);
|
||||
component.LifeStage = ComponentLifeStage.Deleted;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
public partial class EntityManager
|
||||
{
|
||||
[Pure]
|
||||
public T System<T>() where T : IEntitySystem
|
||||
{
|
||||
return _entitySystemManager.GetEntitySystem<T>();
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public T? SystemOrNull<T>() where T : IEntitySystem
|
||||
{
|
||||
return _entitySystemManager.GetEntitySystemOrNull<T>();
|
||||
|
||||
@@ -586,7 +586,7 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
try
|
||||
{
|
||||
LifeShutdown(component);
|
||||
LifeShutdown(uid, component, _componentFactory.GetIndex(component.GetType()));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,6 @@ using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
@@ -76,6 +75,15 @@ public partial class EntitySystem
|
||||
return LifeStage(uid, metaData) >= EntityLifeStage.Terminating;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the entity is being or has been deleted (or never existed in the first place).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected bool TerminatingOrDeleted(EntityUid? uid, MetaDataComponent? metaData = null)
|
||||
{
|
||||
return !uid.HasValue || TerminatingOrDeleted(uid.Value, metaData);
|
||||
}
|
||||
|
||||
[Obsolete("Use override without the EntityQuery")]
|
||||
protected bool Deleted(EntityUid uid, EntityQuery<MetaDataComponent> metaQuery) => Deleted(uid);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
@@ -26,8 +27,10 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
var found = EntityManager.TryGetComponent(uid, out component);
|
||||
|
||||
if(logMissing && !found)
|
||||
Log.Error($"Can't resolve \"{typeof(TComp)}\" on entity {ToPrettyString(uid)}!\n{new StackTrace(1, true)}");
|
||||
if (logMissing && !found)
|
||||
{
|
||||
Log.Error($"Can't resolve \"{typeof(TComp)}\" on entity {ToPrettyString(uid)}!\n{Environment.StackTrace}");
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed on an entity when its name is changed.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct EntityRenamedEvent(string NewName);
|
||||
@@ -77,6 +77,18 @@ namespace Robust.Shared.GameObjects
|
||||
/// <returns>The availability of the component.</returns>
|
||||
ComponentAvailability GetComponentAvailability(string componentName, bool ignoreCase = false);
|
||||
|
||||
/// <summary>
|
||||
/// Slow-path for Type -> CompIdx mapping without generics.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
CompIdx GetIndex(Type type);
|
||||
|
||||
/// <summary>
|
||||
/// Slow-path to get the component index for a specified type.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
int GetArrayIndex(Type type);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a component class with the factory.
|
||||
/// </summary>
|
||||
|
||||
@@ -100,7 +100,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The component reference type to remove.</typeparam>
|
||||
/// <param name="uid">Entity UID to modify.</param>
|
||||
bool RemoveComponent<T>(EntityUid uid, MetaDataComponent? meta = null);
|
||||
bool RemoveComponent<T>(EntityUid uid, MetaDataComponent? meta = null) where T : IComponent;
|
||||
|
||||
/// <summary>
|
||||
/// Removes the component with a specified type.
|
||||
@@ -294,7 +294,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// <param name="uid">Entity UID to check.</param>
|
||||
/// <param name="component">Component of the specified type (if exists).</param>
|
||||
/// <returns>If the component existed in the entity.</returns>
|
||||
bool TryGetComponent<T>(EntityUid uid, [NotNullWhen(true)] out T? component) where T : IComponent?;
|
||||
bool TryGetComponent<T>(EntityUid uid, [NotNullWhen(true)] out T? component) where T : IComponent?;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the component of a specific type.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
@@ -9,6 +10,7 @@ public partial interface IEntityManager
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of entity system to find.</typeparam>
|
||||
/// <returns>The <see cref="IEntitySystem"/> instance matching the specified type.</returns>
|
||||
[Pure]
|
||||
T System<T>() where T : IEntitySystem;
|
||||
|
||||
/// <summary>
|
||||
@@ -16,6 +18,7 @@ public partial interface IEntityManager
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of entity system to find.</typeparam>
|
||||
/// <returns>The <see cref="IEntitySystem"/> instance matching the specified type, or null.</returns>
|
||||
[Pure]
|
||||
T? SystemOrNull<T>() where T : IEntitySystem;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -49,7 +49,7 @@ public readonly struct NetEntity : IEquatable<NetEntity>, IComparable<NetEntity>
|
||||
public static NetEntity Parse(ReadOnlySpan<char> uid)
|
||||
{
|
||||
if (uid.Length == 0)
|
||||
return default;
|
||||
throw new FormatException($"An empty string is not a valid NetEntity");
|
||||
|
||||
if (uid[0] != 'c')
|
||||
return new NetEntity(int.Parse(uid));
|
||||
|
||||
@@ -614,16 +614,16 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
#region EntityCoordinates
|
||||
|
||||
public void GetEntitiesInRange<T>(EntityCoordinates coordinates, float range, HashSet<Entity<T>> entities) where T : IComponent
|
||||
public void GetEntitiesInRange<T>(EntityCoordinates coordinates, float range, HashSet<Entity<T>> entities, LookupFlags flags = DefaultFlags) where T : IComponent
|
||||
{
|
||||
var mapPos = coordinates.ToMap(EntityManager, _transform);
|
||||
GetEntitiesInRange(mapPos, range, entities);
|
||||
GetEntitiesInRange(mapPos, range, entities, flags);
|
||||
}
|
||||
|
||||
public HashSet<Entity<T>> GetEntitiesInRange<T>(EntityCoordinates coordinates, float range) where T : IComponent
|
||||
public HashSet<Entity<T>> GetEntitiesInRange<T>(EntityCoordinates coordinates, float range, LookupFlags flags = DefaultFlags) where T : IComponent
|
||||
{
|
||||
var entities = new HashSet<Entity<T>>();
|
||||
GetEntitiesInRange(coordinates, range, entities);
|
||||
GetEntitiesInRange(coordinates, range, entities, flags);
|
||||
return entities;
|
||||
}
|
||||
|
||||
@@ -739,6 +739,47 @@ public sealed partial class EntityLookupSystem
|
||||
|
||||
#endregion
|
||||
|
||||
#region Local
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entities intersecting the specified broadphase entity using a local AABB.
|
||||
/// </summary>
|
||||
public void GetLocalEntitiesIntersecting<T>(
|
||||
EntityUid gridUid,
|
||||
Vector2i localTile,
|
||||
HashSet<Entity<T>> intersecting,
|
||||
float enlargement = TileEnlargementRadius,
|
||||
LookupFlags flags = DefaultFlags,
|
||||
MapGridComponent? gridComp = null) where T : IComponent
|
||||
{
|
||||
ushort tileSize = 1;
|
||||
|
||||
if (_gridQuery.Resolve(gridUid, ref gridComp))
|
||||
{
|
||||
tileSize = gridComp.TileSize;
|
||||
}
|
||||
|
||||
var localAABB = GetLocalBounds(localTile, tileSize);
|
||||
localAABB = localAABB.Enlarged(TileEnlargementRadius);
|
||||
GetLocalEntitiesIntersecting(gridUid, localAABB, intersecting, flags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entities intersecting the specified broadphase entity using a local AABB.
|
||||
/// </summary>
|
||||
public void GetLocalEntitiesIntersecting<T>(
|
||||
EntityUid gridUid,
|
||||
Box2 localAABB,
|
||||
HashSet<Entity<T>> intersecting,
|
||||
LookupFlags flags = DefaultFlags) where T : IComponent
|
||||
{
|
||||
var query = GetEntityQuery<T>();
|
||||
AddLocalEntitiesIntersecting(gridUid, intersecting, localAABB, flags, query);
|
||||
AddContained(intersecting, flags, query);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Gets entities with the specified component with the specified parent.
|
||||
/// </summary>
|
||||
|
||||
@@ -42,12 +42,19 @@ public abstract class MetaDataSystem : EntitySystem
|
||||
component.PauseTime = state.PauseTime;
|
||||
}
|
||||
|
||||
public void SetEntityName(EntityUid uid, string value, MetaDataComponent? metadata = null)
|
||||
public void SetEntityName(EntityUid uid, string value, MetaDataComponent? metadata = null, bool raiseEvents = true)
|
||||
{
|
||||
if (!_metaQuery.Resolve(uid, ref metadata) || value.Equals(metadata.EntityName))
|
||||
return;
|
||||
|
||||
metadata._entityName = value;
|
||||
|
||||
if (raiseEvents)
|
||||
{
|
||||
var ev = new EntityRenamedEvent(value);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
|
||||
Dirty(uid, metadata, metadata);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Reflection;
|
||||
@@ -67,9 +67,6 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
|
||||
|
||||
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentShutdown>(OnActorShutdown);
|
||||
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentGetStateAttemptEvent>(OnGetStateAttempt);
|
||||
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentGetState>(OnActorGetState);
|
||||
SubscribeLocalEvent<UserInterfaceUserComponent, ComponentHandleState>(OnActorHandleState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -133,42 +130,6 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
CloseUserUis((ent.Owner, ent.Comp));
|
||||
}
|
||||
|
||||
private void OnGetStateAttempt(Entity<UserInterfaceUserComponent> ent, ref ComponentGetStateAttemptEvent args)
|
||||
{
|
||||
if (args.Cancelled || args.Player?.AttachedEntity != ent.Owner)
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnActorGetState(Entity<UserInterfaceUserComponent> ent, ref ComponentGetState args)
|
||||
{
|
||||
var interfaces = new Dictionary<NetEntity, List<Enum>>();
|
||||
|
||||
foreach (var (buid, data) in ent.Comp.OpenInterfaces)
|
||||
{
|
||||
interfaces[GetNetEntity(buid)] = data;
|
||||
}
|
||||
|
||||
args.State = new UserInterfaceUserComponentState()
|
||||
{
|
||||
OpenInterfaces = interfaces,
|
||||
};
|
||||
}
|
||||
|
||||
private void OnActorHandleState(Entity<UserInterfaceUserComponent> ent, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not UserInterfaceUserComponentState state)
|
||||
return;
|
||||
|
||||
// TODO: Allocate less.
|
||||
ent.Comp.OpenInterfaces.Clear();
|
||||
|
||||
foreach (var (nent, data) in state.OpenInterfaces)
|
||||
{
|
||||
var openEnt = EnsureEntity<ActorComponent>(nent, ent.Owner);
|
||||
ent.Comp.OpenInterfaces[openEnt] = data;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void OnPlayerAttached(PlayerAttachedEvent ev)
|
||||
@@ -182,6 +143,9 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (!_uiQuery.TryGetComponent(uid, out var uiComp))
|
||||
continue;
|
||||
|
||||
// Player can now receive information about open UIs
|
||||
Dirty(uid, uiComp);
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (!uiComp.Interfaces.TryGetValue(key, out var data))
|
||||
@@ -203,6 +167,9 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (!_uiQuery.TryGetComponent(uid, out var uiComp))
|
||||
continue;
|
||||
|
||||
// Player can no longer receive information about open UIs
|
||||
Dirty(uid, uiComp);
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (!uiComp.ClientOpenInterfaces.Remove(key, out var cBui))
|
||||
@@ -215,30 +182,34 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
|
||||
private void OnUserInterfaceClosed(Entity<UserInterfaceComponent> ent, ref CloseBoundInterfaceMessage args)
|
||||
{
|
||||
CloseUi(ent, args.Actor, args.UiKey);
|
||||
CloseUiInternal(ent!, args.UiKey, args.Actor);
|
||||
}
|
||||
|
||||
private void CloseUi(Entity<UserInterfaceComponent> ent, EntityUid actor, Enum key)
|
||||
private void CloseUiInternal(Entity<UserInterfaceComponent?> ent, Enum key, EntityUid actor)
|
||||
{
|
||||
var actors = ent.Comp.Actors[key];
|
||||
actors.Remove(actor);
|
||||
if (!_uiQuery.Resolve(ent.Owner, ref ent.Comp, false))
|
||||
return;
|
||||
|
||||
if (!ent.Comp.Actors.TryGetValue(key, out var actors))
|
||||
return;
|
||||
|
||||
actors.Remove(actor);
|
||||
if (actors.Count == 0)
|
||||
ent.Comp.Actors.Remove(key);
|
||||
|
||||
Dirty(ent);
|
||||
|
||||
// If the actor is also deleting then don't worry about updating what they have open.
|
||||
if (!TerminatingOrDeleted(actor) && _userQuery.TryComp(actor, out var actorComp))
|
||||
if (!TerminatingOrDeleted(actor)
|
||||
&& _userQuery.TryComp(actor, out var actorComp)
|
||||
&& actorComp.OpenInterfaces.TryGetValue(ent.Owner, out var keys))
|
||||
{
|
||||
if (actorComp.OpenInterfaces.TryGetValue(ent.Owner, out var keys))
|
||||
keys.Remove(key);
|
||||
if (keys.Count == 0)
|
||||
{
|
||||
keys.Remove(key);
|
||||
|
||||
if (keys.Count == 0)
|
||||
actorComp.OpenInterfaces.Remove(ent.Owner);
|
||||
|
||||
Dirty(actor, actorComp);
|
||||
actorComp.OpenInterfaces.Remove(ent.Owner);
|
||||
if (actorComp.OpenInterfaces.Count == 0)
|
||||
RemCompDeferred<UserInterfaceUserComponent>(actor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,23 +228,29 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
|
||||
private void OnUserInterfaceOpen(Entity<UserInterfaceComponent> ent, ref OpenBoundInterfaceMessage args)
|
||||
{
|
||||
OpenUiInternal(ent!, args.UiKey, args.Actor);
|
||||
}
|
||||
|
||||
private void OpenUiInternal(Entity<UserInterfaceComponent?> ent, Enum key, EntityUid actor)
|
||||
{
|
||||
if (!_uiQuery.Resolve(ent.Owner, ref ent.Comp, false))
|
||||
return;
|
||||
|
||||
// Similar to the close method this handles actually opening a UI, it just gets relayed here
|
||||
EnsureComp<ActiveUserInterfaceComponent>(ent.Owner);
|
||||
|
||||
var actor = args.Actor;
|
||||
var actorComp = EnsureComp<UserInterfaceUserComponent>(actor);
|
||||
|
||||
// Let state handling open the UI clientside.
|
||||
actorComp.OpenInterfaces.GetOrNew(ent.Owner).Add(args.UiKey);
|
||||
ent.Comp.Actors.GetOrNew(args.UiKey).Add(actor);
|
||||
actorComp.OpenInterfaces.GetOrNew(ent.Owner).Add(key);
|
||||
ent.Comp.Actors.GetOrNew(key).Add(actor);
|
||||
Dirty(ent);
|
||||
Dirty(actor, actorComp);
|
||||
|
||||
var ev = new BoundUIOpenedEvent(args.UiKey, ent.Owner, args.Actor);
|
||||
var ev = new BoundUIOpenedEvent(key, ent.Owner, actor);
|
||||
RaiseLocalEvent(ent.Owner, ev);
|
||||
|
||||
// If we're client we want this handled immediately.
|
||||
EnsureClientBui(ent, args.UiKey, ent.Comp.Interfaces[args.UiKey]);
|
||||
EnsureClientBui(ent!, key, ent.Comp.Interfaces[key]);
|
||||
}
|
||||
|
||||
private void OnUserInterfaceStartup(Entity<UserInterfaceComponent> ent, ref ComponentStartup args)
|
||||
@@ -299,7 +276,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
actors.AddRange(acts);
|
||||
foreach (var actor in actors)
|
||||
{
|
||||
CloseUi(ent, actor, key);
|
||||
CloseUiInternal(ent!, key, actor);
|
||||
DebugTools.Assert(!acts.Contains(actor));
|
||||
}
|
||||
|
||||
@@ -311,20 +288,29 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
|
||||
private void OnUserInterfaceGetState(Entity<UserInterfaceComponent> ent, ref ComponentGetState args)
|
||||
{
|
||||
// TODO delta states.
|
||||
// I.e., don't resend the whole BUI state just because a new user opened it.
|
||||
|
||||
var actors = new Dictionary<Enum, List<NetEntity>>();
|
||||
var states = new Dictionary<Enum, BoundUserInterfaceState>();
|
||||
args.State = new UserInterfaceComponent.UserInterfaceComponentState(actors, ent.Comp.States);
|
||||
|
||||
foreach (var (key, acts) in ent.Comp.Actors)
|
||||
// Ensure that only the player that currently has the UI open gets to know what they have it open.
|
||||
if (args.ReplayState)
|
||||
{
|
||||
actors[key] = GetNetEntityList(acts);
|
||||
foreach (var (key, acts) in ent.Comp.Actors)
|
||||
{
|
||||
actors[key] = GetNetEntityList(acts);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (key, state) in ent.Comp.States)
|
||||
else if (args.Player.AttachedEntity is { } player)
|
||||
{
|
||||
states[key] = state;
|
||||
var netPlayer = new List<NetEntity> { GetNetEntity(player) };
|
||||
foreach (var (key, acts) in ent.Comp.Actors)
|
||||
{
|
||||
if (acts.Contains(player))
|
||||
actors[key] = netPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
args.State = new UserInterfaceComponent.UserInterfaceComponentState(actors, states);
|
||||
}
|
||||
|
||||
private void OnUserInterfaceHandleState(Entity<UserInterfaceComponent> ent, ref ComponentHandleState args)
|
||||
@@ -332,48 +318,50 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (args.Current is not UserInterfaceComponent.UserInterfaceComponentState state)
|
||||
return;
|
||||
|
||||
var toRemove = new ValueList<Enum>();
|
||||
|
||||
foreach (var (key, actors) in state.Actors)
|
||||
{
|
||||
ref var existing = ref CollectionsMarshal.GetValueRefOrAddDefault(ent.Comp.Actors, key, out _);
|
||||
|
||||
existing ??= new List<EntityUid>();
|
||||
|
||||
existing.Clear();
|
||||
existing.AddRange(EnsureEntityList<UserInterfaceComponent>(actors, ent.Owner));
|
||||
}
|
||||
|
||||
foreach (var key in ent.Comp.Actors.Keys)
|
||||
{
|
||||
if (state.Actors.ContainsKey(key))
|
||||
continue;
|
||||
|
||||
toRemove.Add(key);
|
||||
if (!state.Actors.ContainsKey(key))
|
||||
CloseUi(ent!, key);
|
||||
}
|
||||
|
||||
foreach (var key in toRemove)
|
||||
var toRemoveActors = new ValueList<EntityUid>();
|
||||
var newSet = new HashSet<EntityUid>();
|
||||
foreach (var (key, stateActors) in state.Actors)
|
||||
{
|
||||
ent.Comp.Actors.Remove(key);
|
||||
var actors = ent.Comp.Actors.GetOrNew(key);
|
||||
|
||||
newSet.Clear();
|
||||
foreach (var netEntity in stateActors)
|
||||
{
|
||||
var uid = EnsureEntity<UserInterfaceComponent>(netEntity, ent.Owner);
|
||||
if (uid.IsValid())
|
||||
newSet.Add(uid);
|
||||
}
|
||||
|
||||
foreach (var actor in newSet)
|
||||
{
|
||||
if (!actors.Contains(actor))
|
||||
OpenUiInternal(ent!, key, actor);
|
||||
}
|
||||
|
||||
foreach (var actor in actors)
|
||||
{
|
||||
if (!newSet.Contains(actor))
|
||||
toRemoveActors.Add(actor);
|
||||
}
|
||||
|
||||
foreach (var actor in toRemoveActors)
|
||||
{
|
||||
CloseUiInternal(ent!, key, actor);
|
||||
}
|
||||
}
|
||||
|
||||
toRemove.Clear();
|
||||
|
||||
// State handling
|
||||
foreach (var key in ent.Comp.States.Keys)
|
||||
{
|
||||
if (state.States.ContainsKey(key))
|
||||
continue;
|
||||
|
||||
toRemove.Add(key);
|
||||
if (!state.States.ContainsKey(key))
|
||||
ent.Comp.States.Remove(key);
|
||||
}
|
||||
|
||||
foreach (var key in toRemove)
|
||||
{
|
||||
ent.Comp.States.Remove(key);
|
||||
}
|
||||
|
||||
toRemove.Clear();
|
||||
var attachedEnt = _player.LocalEntity;
|
||||
|
||||
// Check if the UI is open by us, otherwise dispose of it.
|
||||
@@ -386,11 +374,6 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
}
|
||||
|
||||
bui.Dispose();
|
||||
toRemove.Add(key);
|
||||
}
|
||||
|
||||
foreach (var key in toRemove)
|
||||
{
|
||||
ent.Comp.ClientOpenInterfaces.Remove(key);
|
||||
}
|
||||
|
||||
@@ -519,16 +502,14 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
|
||||
return;
|
||||
|
||||
if (!entity.Comp.Actors.TryGetValue(key, out var actors))
|
||||
if (!entity.Comp.Actors.TryGetValue(key, out var actorSet))
|
||||
return;
|
||||
|
||||
for (var i = actors.Count - 1; i >= 0; i--)
|
||||
var actors = actorSet.ToArray();
|
||||
foreach (var actor in actors)
|
||||
{
|
||||
var actor = actors[i];
|
||||
CloseUi(entity, key, actor);
|
||||
CloseUiInternal(entity, key, actor);
|
||||
}
|
||||
|
||||
DebugTools.Assert(actors.Count == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -563,19 +544,16 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
return;
|
||||
|
||||
// Rely upon the client telling us.
|
||||
if (predicted)
|
||||
if (!predicted)
|
||||
{
|
||||
if (_timing.IsFirstTimePredicted)
|
||||
{
|
||||
// Not guaranteed to open so rely upon the event handling it.
|
||||
// Also lets client request it to be opened remotely too.
|
||||
EntityManager.RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
OnMessageReceived(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key), actor.Value);
|
||||
CloseUiInternal(entity, key, actor.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
EntityManager.RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -800,17 +778,15 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (actor.Comp.OpenInterfaces.Count == 0)
|
||||
return;
|
||||
|
||||
var copied = new Dictionary<EntityUid, List<Enum>>(actor.Comp.OpenInterfaces);
|
||||
var enumCopy = new ValueList<Enum>();
|
||||
|
||||
foreach (var (uid, enums) in copied)
|
||||
foreach (var (uid, enums) in actor.Comp.OpenInterfaces)
|
||||
{
|
||||
enumCopy.Clear();
|
||||
enumCopy.AddRange(enums);
|
||||
|
||||
foreach (var key in enumCopy)
|
||||
{
|
||||
CloseUi(uid, key, actor.Owner);
|
||||
CloseUiInternal(uid, key, actor.Owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -823,9 +799,16 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (!_uiQuery.Resolve(entity.Owner, ref entity.Comp, false))
|
||||
return;
|
||||
|
||||
entity.Comp.Actors.Clear();
|
||||
entity.Comp.States.Clear();
|
||||
Dirty(entity);
|
||||
var toClose = new ValueList<EntityUid>();
|
||||
foreach (var (key, actors) in entity.Comp.Actors)
|
||||
{
|
||||
toClose.Clear();
|
||||
toClose.AddRange(actors);
|
||||
foreach (var actor in toClose)
|
||||
{
|
||||
CloseUiInternal(entity, key, actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -838,7 +821,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
|
||||
foreach (var key in entity.Comp.Interfaces.Keys)
|
||||
{
|
||||
CloseUi(entity, key, actor);
|
||||
CloseUiInternal(entity, key, actor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -978,6 +961,18 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a UI after an entity has been created.
|
||||
/// </summary>
|
||||
public void SetUi(Entity<UserInterfaceComponent?> ent, Enum key, InterfaceData data)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
ent.Comp = AddComp<UserInterfaceComponent>(ent);
|
||||
|
||||
ent.Comp.Interfaces[key] = data;
|
||||
Dirty(ent, ent.Comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that the subscribed clients are still in range of the interface.
|
||||
/// </summary>
|
||||
@@ -985,9 +980,9 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
Entity<TransformComponent> UiEnt,
|
||||
Enum key,
|
||||
InterfaceData data,
|
||||
Entity<TransformComponent?> actor)
|
||||
Entity<TransformComponent> actor)
|
||||
{
|
||||
if (!_xformQuery.Resolve(actor, ref actor.Comp) || actor.Comp.MapID != UiEnt.Comp.MapID)
|
||||
if (actor.Comp.MapID != UiEnt.Comp.MapID)
|
||||
return false;
|
||||
|
||||
// Handle pluggable BoundUserInterfaceCheckRangeEvent
|
||||
@@ -1007,7 +1002,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
|
||||
DebugTools.Assert(checkRangeEvent.Result == BoundUserInterfaceRangeResult.Default);
|
||||
|
||||
return _transforms.InRange(UiEnt!, actor, data.InteractionRange);
|
||||
return _transforms.InRange(UiEnt!, (actor.Owner, actor.Comp), data.InteractionRange);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1023,13 +1018,14 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
{
|
||||
var data = ActorRanges[index];
|
||||
|
||||
if (!XformQuery.TryComp(data.Ui, out var uiXform))
|
||||
if (!XformQuery.TryComp(data.Ui, out var uiXform) ||
|
||||
!XformQuery.TryComp(data.Actor, out var actorXform))
|
||||
{
|
||||
data.Result = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
data.Result = System.CheckRange((data.Ui, uiXform), data.Key, data.Data, data.Actor);
|
||||
data.Result = System.CheckRange((data.Ui, uiXform), data.Key, data.Data, (data.Actor, actorXform));
|
||||
}
|
||||
|
||||
ActorRanges[index] = data;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -36,6 +37,7 @@ namespace Robust.Shared.GameStates
|
||||
/// If true, this state is intended for replays or some other server spectator entity, not for specific
|
||||
/// clients.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(false, nameof(Player))]
|
||||
public bool ReplayState => Player == null;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Robust.Shared.Graphics
|
||||
public virtual MapCoordinates Position
|
||||
{
|
||||
get => _coords;
|
||||
internal set => _coords = value;
|
||||
set => _coords = value;
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
|
||||
@@ -173,6 +173,23 @@ namespace Robust.Shared.Input
|
||||
public ClientFullInputCmdMessage(GameTick tick, ushort subTick, KeyFunctionId inputFunctionId) : base(tick, subTick, inputFunctionId)
|
||||
{
|
||||
}
|
||||
|
||||
public ClientFullInputCmdMessage(
|
||||
GameTick tick,
|
||||
ushort subTick,
|
||||
KeyFunctionId inputFunctionId,
|
||||
EntityCoordinates coordinates,
|
||||
ScreenCoordinates screenCoordinates,
|
||||
BoundKeyState state,
|
||||
EntityUid uid) : base(tick,
|
||||
subTick,
|
||||
inputFunctionId)
|
||||
{
|
||||
Coordinates = coordinates;
|
||||
ScreenCoordinates = screenCoordinates;
|
||||
State = state;
|
||||
Uid = uid;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IFullInputCmdMessage
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using Linguini.Bundle;
|
||||
using Linguini.Bundle.Errors;
|
||||
using Linguini.Syntax.Ast;
|
||||
using Linguini.Syntax.Parser.Error;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -17,6 +21,24 @@ internal static class LocHelper
|
||||
return FormatErrors(self.Message, span, resource, newLine);
|
||||
}
|
||||
|
||||
public static bool InsertResourcesAndReport(this FluentBundle bundle, Resource resource,
|
||||
ResPath path, [NotNullWhen(false)] out List<LocError>? errors)
|
||||
{
|
||||
if (!bundle.AddResource(resource, out var parseErrors))
|
||||
{
|
||||
errors = new List<LocError>();
|
||||
foreach (var fluentError in parseErrors)
|
||||
{
|
||||
errors.Add(new LocError(path, fluentError));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
errors = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string FormatErrors(string message, ErrorSpan span, ReadOnlyMemory<char> resource, string? newLine)
|
||||
{
|
||||
newLine ??= Environment.NewLine;
|
||||
@@ -69,3 +91,28 @@ internal static class LocHelper
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Wrapper around Fluent Error, that adds path to the list of values.
|
||||
/// Work in progress, FluentErrors need to be modified to be more accessible.
|
||||
/// </summary>
|
||||
internal record LocError
|
||||
{
|
||||
public readonly ResPath Path;
|
||||
public readonly FluentError Error;
|
||||
|
||||
/// <summary>
|
||||
/// Basic constructor.
|
||||
/// </summary>
|
||||
/// <param name="path">path of resource being added.</param>
|
||||
/// <param name="fluentError">FluentError encountered.</param>
|
||||
public LocError(ResPath path, FluentError fluentError)
|
||||
{
|
||||
Path = path;
|
||||
Error = fluentError;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{Path.CanonPath}]: {Error}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Linguini.Bundle;
|
||||
using Linguini.Bundle.Errors;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components.Localization;
|
||||
@@ -92,14 +93,14 @@ namespace Robust.Shared.Localization
|
||||
|
||||
var allErrors = new List<FluentError>();
|
||||
if (desc == null
|
||||
&& !bundle.TryGetMsg(locId, "desc", null, out var err1, out desc))
|
||||
&& !bundle.TryGetMessage(locId, "desc", null, out var err1, out desc))
|
||||
{
|
||||
desc = null;
|
||||
allErrors.AddRange(err1);
|
||||
}
|
||||
|
||||
if (suffix == null
|
||||
&& !bundle.TryGetMsg(locId, "suffix", null, out var err, out suffix))
|
||||
&& !bundle.TryGetMessage(locId, "suffix", null, out var err, out suffix))
|
||||
{
|
||||
suffix = null;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Linguini.Bundle;
|
||||
using Linguini.Bundle.Types;
|
||||
using Linguini.Shared.Types.Bundle;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components.Localization;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Shared.Localization
|
||||
@@ -326,8 +323,7 @@ namespace Robust.Shared.Localization
|
||||
|
||||
private void AddCtxFunction(FluentBundle ctx, string name, LocFunction function)
|
||||
{
|
||||
ctx.AddFunction(name, (args, options)
|
||||
=> CallFunction(function, ctx, args, options), out _, InsertBehavior.Overriding);
|
||||
ctx.AddFunctionOverriding(name, (args, options) => CallFunction(function, ctx, args, options));
|
||||
}
|
||||
|
||||
private IFluentType CallFunction(
|
||||
@@ -356,8 +352,8 @@ namespace Robust.Shared.Localization
|
||||
{
|
||||
var bundle = _contexts[culture];
|
||||
|
||||
bundle.AddFunction(name, (args, options)
|
||||
=> CallFunction(function, bundle, args, options), out _, InsertBehavior.Overriding);
|
||||
bundle.AddFunctionOverriding(name, (args, options)
|
||||
=> CallFunction(function, bundle, args, options));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,6 +373,32 @@ namespace Robust.Shared.Localization
|
||||
return WrappedValue.Format(_context);
|
||||
}
|
||||
|
||||
public bool IsError()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Matches(IFluentType other, IScope scope)
|
||||
{
|
||||
if (other is FluentLocWrapperType otherWrapper)
|
||||
{
|
||||
return (WrappedValue, otherWrapper.WrappedValue) switch
|
||||
{
|
||||
(LocValueNone, LocValueNone) => true,
|
||||
(LocValueDateTime l, LocValueDateTime d) => l.Value.Equals(d.Value),
|
||||
(LocValueTimeSpan l, LocValueTimeSpan d) => l.Value.Equals(d.Value),
|
||||
(LocValueNumber l, LocValueNumber d) => l.Value.Equals(d.Value),
|
||||
(LocValueString l, LocValueString d) => l.Value.Equals(d.Value),
|
||||
(LocValueEntity l, LocValueEntity d) => l.Value.Equals(d.Value),
|
||||
({ } l, { } d) => Equals(l, d),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public IFluentType Copy()
|
||||
{
|
||||
return this;
|
||||
@@ -403,6 +425,7 @@ namespace Robust.Shared.Localization
|
||||
{
|
||||
ILocValue wrap => new FluentLocWrapperType(wrap, context),
|
||||
EntityUid entity => new FluentLocWrapperType(new LocValueEntity(entity), context),
|
||||
IFluentEntityUid entity => new FluentLocWrapperType(new LocValueEntity(entity.FluentOwner), context),
|
||||
DateTime dateTime => new FluentLocWrapperType(new LocValueDateTime(dateTime), context),
|
||||
TimeSpan timeSpan => new FluentLocWrapperType(new LocValueTimeSpan(timeSpan), context),
|
||||
Color color => (FluentString)color.ToHex(),
|
||||
@@ -433,4 +456,9 @@ namespace Robust.Shared.Localization
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal interface IFluentEntityUid
|
||||
{
|
||||
internal EntityUid FluentOwner { get; }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Linguini.Bundle;
|
||||
using Linguini.Bundle.Builder;
|
||||
using Linguini.Bundle.Errors;
|
||||
@@ -47,7 +48,8 @@ namespace Robust.Shared.Localization
|
||||
|
||||
if (!TryGetString(messageId, out var msg))
|
||||
{
|
||||
_logSawmill.Debug("Unknown messageId ({culture}): {messageId}", _defaultCulture.Value.Item1.Name, messageId);
|
||||
_logSawmill.Debug("Unknown messageId ({culture}): {messageId}", _defaultCulture.Value.Item1.Name,
|
||||
messageId);
|
||||
msg = messageId;
|
||||
}
|
||||
|
||||
@@ -64,8 +66,9 @@ namespace Robust.Shared.Localization
|
||||
if (TryGetString(messageId, out var argMsg, arg))
|
||||
return argMsg;
|
||||
|
||||
_logSawmill.Debug("Unknown messageId ({culture}): {messageId}", _defaultCulture.Value.Item1.Name, messageId);
|
||||
return messageId;
|
||||
_logSawmill.Debug("Unknown messageId ({culture}): {messageId}", _defaultCulture.Value.Item1.Name,
|
||||
messageId);
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public string GetString(string messageId, (string, object) arg1, (string, object) arg2)
|
||||
@@ -76,8 +79,9 @@ namespace Robust.Shared.Localization
|
||||
if (TryGetString(messageId, out var argMsg, arg1, arg2))
|
||||
return argMsg;
|
||||
|
||||
_logSawmill.Debug("Unknown messageId ({culture}): {messageId}", _defaultCulture.Value.Item1.Name, messageId);
|
||||
return messageId;
|
||||
_logSawmill.Debug("Unknown messageId ({culture}): {messageId}", _defaultCulture.Value.Item1.Name,
|
||||
messageId);
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public string GetString(string messageId, params (string, object)[] args)
|
||||
@@ -88,8 +92,9 @@ namespace Robust.Shared.Localization
|
||||
if (TryGetString(messageId, out var argMsg, args))
|
||||
return argMsg;
|
||||
|
||||
_logSawmill.Debug("Unknown messageId ({culture}): {messageId}", _defaultCulture.Value.Item1.Name, messageId);
|
||||
return messageId;
|
||||
_logSawmill.Debug("Unknown messageId ({culture}): {messageId}", _defaultCulture.Value.Item1.Name,
|
||||
messageId);
|
||||
return messageId;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -101,7 +106,7 @@ namespace Robust.Shared.Localization
|
||||
|
||||
#region TryGetString
|
||||
|
||||
public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value)
|
||||
public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
if (_defaultCulture == null)
|
||||
{
|
||||
@@ -112,7 +117,7 @@ public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value
|
||||
if (TryGetString(messageId, _defaultCulture.Value, out value))
|
||||
return true;
|
||||
|
||||
foreach (var fallback in _fallbackCultures)
|
||||
foreach (var fallback in _fallbackCultures)
|
||||
{
|
||||
if (TryGetString(messageId, fallback, out value))
|
||||
return true;
|
||||
@@ -122,18 +127,24 @@ public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetString(string messageId, (CultureInfo, FluentBundle) bundle, [NotNullWhen(true)] out string? value)
|
||||
public bool TryGetString(string messageId,
|
||||
(CultureInfo, FluentBundle) bundle,
|
||||
[NotNullWhen(true)] out string? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO LINGUINI error list nullable.
|
||||
var result = bundle.Item2.TryGetAttrMsg(messageId, null, out var errs, out value);
|
||||
foreach (var err in errs)
|
||||
if (bundle.Item2.TryGetAttrMessage(messageId, null, out var errors, out value))
|
||||
return true;
|
||||
|
||||
if (errors != null)
|
||||
{
|
||||
_logSawmill.Error("{culture}/{messageId}: {error}", bundle.Item1.Name, messageId, err);
|
||||
foreach (var err in errors)
|
||||
{
|
||||
_logSawmill.Error("{culture}/{messageId}: {error}", bundle.Item1.Name, messageId, err);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -143,7 +154,8 @@ public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value,
|
||||
public bool TryGetString(string messageId,
|
||||
[NotNullWhen(true)] out string? value,
|
||||
(string, object) arg)
|
||||
{
|
||||
// TODO LINGUINI add try-get-message variant that takes in a (string, object)[]
|
||||
@@ -165,8 +177,10 @@ public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value
|
||||
return TryGetString(messageId, out value, args, bundle, info);
|
||||
}
|
||||
|
||||
public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value,
|
||||
(string, object) arg1, (string, object) arg2)
|
||||
public bool TryGetString(string messageId,
|
||||
[NotNullWhen(true)] out string? value,
|
||||
(string, object) arg1,
|
||||
(string, object) arg2)
|
||||
{
|
||||
// TODO LINGUINI add try-get-message variant that takes in a (string, object)[]
|
||||
// I.e., have it automatically call FluentFromObject(context) with the right context if the message exists
|
||||
@@ -188,7 +202,8 @@ public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value
|
||||
return TryGetString(messageId, out value, args, bundle, info);
|
||||
}
|
||||
|
||||
public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value,
|
||||
public bool TryGetString(string messageId,
|
||||
[NotNullWhen(true)] out string? value,
|
||||
params (string, object)[] keyArgs)
|
||||
{
|
||||
// TODO LINGUINI add try-get-message variant that takes in a (string, object)[]
|
||||
@@ -216,10 +231,13 @@ public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = bundle.TryGetAttrMsg(messageId, args, out var errs, out value);
|
||||
foreach (var err in errs)
|
||||
var result = bundle.TryGetAttrMessage(messageId, args, out var errs, out value);
|
||||
if (errs != null)
|
||||
{
|
||||
_logSawmill.Error("{culture}/{messageId}: {error}", culture.Name, messageId, err);
|
||||
foreach (var err in errs)
|
||||
{
|
||||
_logSawmill.Error("{culture}/{messageId}: {error}", culture.Name, messageId, err);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -245,14 +263,14 @@ public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value
|
||||
}
|
||||
|
||||
var idx = messageId.IndexOf('.');
|
||||
if (idx != -1 )
|
||||
if (idx != -1)
|
||||
messageId = messageId.Remove(idx);
|
||||
|
||||
culture = _defaultCulture;
|
||||
if (culture.Value.Item2.HasMessage(messageId))
|
||||
return true;
|
||||
|
||||
foreach (var fallback in _fallbackCultures)
|
||||
foreach (var fallback in _fallbackCultures)
|
||||
{
|
||||
culture = fallback;
|
||||
if (culture.Value.Item2.HasMessage(messageId))
|
||||
@@ -279,7 +297,7 @@ public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value
|
||||
if (bundle.TryGetAstMessage(messageId, out message))
|
||||
return true;
|
||||
|
||||
foreach (var fallback in _fallbackCultures)
|
||||
foreach (var fallback in _fallbackCultures)
|
||||
{
|
||||
bundle = fallback.Item2;
|
||||
if (bundle.TryGetAstMessage(messageId, out message))
|
||||
@@ -331,13 +349,13 @@ public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value
|
||||
_contexts.Add(culture, bundle);
|
||||
AddBuiltInFunctions(bundle);
|
||||
|
||||
_loadData(_res, culture, bundle);
|
||||
_initData(_res, culture, bundle);
|
||||
DefaultCulture ??= culture;
|
||||
}
|
||||
|
||||
public void SetFallbackCluture(params CultureInfo[] cultures)
|
||||
{
|
||||
_fallbackCultures = Array.Empty<(CultureInfo, FluentBundle)>();;
|
||||
_fallbackCultures = Array.Empty<(CultureInfo, FluentBundle)>();
|
||||
var tuples = new (CultureInfo, FluentBundle)[cultures.Length];
|
||||
var i = 0;
|
||||
foreach (var culture in cultures)
|
||||
@@ -376,6 +394,41 @@ public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value
|
||||
}
|
||||
|
||||
private void _loadData(IResourceManager resourceManager, CultureInfo culture, FluentBundle context)
|
||||
{
|
||||
var resources = ReadLocaleFolder(resourceManager, culture);
|
||||
|
||||
foreach (var (path, resource, data) in resources)
|
||||
{
|
||||
var errors = resource.Errors;
|
||||
context.AddResourceOverriding(resource);
|
||||
WriteWarningForErrs(path, errors, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void _initData(IResourceManager resourceManager, CultureInfo culture, FluentBundle context)
|
||||
{
|
||||
var resources = ReadLocaleFolder(resourceManager, culture);
|
||||
|
||||
var resErrors = new List<LocError>();
|
||||
foreach (var (path, resource, data) in resources)
|
||||
{
|
||||
var errors = resource.Errors;
|
||||
WriteWarningForErrs(path, errors, data);
|
||||
if (!context.InsertResourcesAndReport(resource, path, out var errs))
|
||||
{
|
||||
resErrors.AddRange(errs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (resErrors.Count > 0)
|
||||
{
|
||||
WriteLocErrors(resErrors);
|
||||
}
|
||||
}
|
||||
|
||||
private static ParallelQuery<(ResPath path, Resource resource, string contents)> ReadLocaleFolder(
|
||||
IResourceManager resourceManager, CultureInfo culture)
|
||||
{
|
||||
// Load data from .ftl files.
|
||||
// Data is loaded from /Locale/<language-code>/*
|
||||
@@ -400,13 +453,7 @@ public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value
|
||||
var resource = parser.Parse();
|
||||
return (path, resource, contents);
|
||||
});
|
||||
|
||||
foreach (var (path, resource, data) in resources)
|
||||
{
|
||||
var errors = resource.Errors;
|
||||
context.AddResourceOverriding(resource);
|
||||
WriteWarningForErrs(path, errors, data);
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
private void WriteWarningForErrs(ResPath path, List<ParseError> errs, string resource)
|
||||
@@ -417,12 +464,24 @@ public bool TryGetString(string messageId, [NotNullWhen(true)] out string? value
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteWarningForErrs(IList<FluentError> errs, string locId)
|
||||
private void WriteWarningForErrs(IList<FluentError>? errs, string locId)
|
||||
{
|
||||
if (errs == null) return;
|
||||
foreach (var err in errs)
|
||||
{
|
||||
_logSawmill.Error("Error extracting `{locId}`\n{e1}", locId, err);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteLocErrors(IList<LocError>? errs)
|
||||
{
|
||||
if (errs == null) return;
|
||||
var sbErr = new StringBuilder();
|
||||
foreach (var err in errs)
|
||||
{
|
||||
sbErr.Append(err).AppendLine();
|
||||
}
|
||||
_logSawmill.Error(sbErr.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
@@ -9,79 +14,87 @@ namespace Robust.Shared.Network;
|
||||
|
||||
internal static class HappyEyeballsHttp
|
||||
{
|
||||
private const int ConnectionAttemptDelay = 250;
|
||||
|
||||
#if DEBUG
|
||||
|
||||
private const int SlowIpv6 = 0;
|
||||
private const bool BrokenIpv6 = false;
|
||||
|
||||
#endif
|
||||
|
||||
// .NET does not implement Happy Eyeballs at the time of writing.
|
||||
// https://github.com/space-wizards/SS14.Launcher/issues/38
|
||||
// This is the workaround.
|
||||
//
|
||||
// Implementation taken from https://github.com/ppy/osu-framework/pull/4191/files
|
||||
|
||||
public static SocketsHttpHandler CreateHttpHandler()
|
||||
// What's Happy Eyeballs? It makes the launcher try both IPv6 and IPv4,
|
||||
// the former with priority, so that if IPv6 is broken your launcher still works.
|
||||
//
|
||||
// Implementation originally based on,
|
||||
// rewritten as to be nigh-impossible to recognize https://github.com/ppy/osu-framework/pull/4191/files
|
||||
//
|
||||
// This is a simple implementation. It does not fully implement RFC 8305:
|
||||
// * We do not separately handle parallel A and AAAA DNS requests as optimization.
|
||||
// * We don't sort IPs as specified in RFC 6724. I can't tell if GetHostEntryAsync does.
|
||||
// * Look I wanted to keep this simple OK?
|
||||
// We don't do any fancy shit like statefulness or incremental sorting
|
||||
// or incremental DNS updates who cares about that.
|
||||
public static SocketsHttpHandler CreateHttpHandler(bool autoRedirect = true)
|
||||
{
|
||||
return new SocketsHttpHandler
|
||||
{
|
||||
ConnectCallback = OnConnect,
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
AllowAutoRedirect = autoRedirect,
|
||||
// PooledConnectionLifetime = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether IPv6 should be preferred. Value may change based on runtime failures.
|
||||
/// </summary>
|
||||
private static bool _useIPv6 = Socket.OSSupportsIPv6;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the initial IPv6 check has been performed (to determine whether v6 is available or not).
|
||||
/// </summary>
|
||||
private static bool _hasResolvedIPv6Availability;
|
||||
|
||||
private const int FirstTryTimeout = 2000;
|
||||
|
||||
private static async ValueTask<Stream> OnConnect(
|
||||
SocketsHttpConnectionContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_useIPv6)
|
||||
{
|
||||
try
|
||||
{
|
||||
var localToken = cancellationToken;
|
||||
// Get IPs via DNS.
|
||||
// Note that we do not attempt to exclude IPv6 if the user doesn't have IPv6.
|
||||
// According to the docs, GetHostEntryAsync will not return them if there's no address.
|
||||
// BUT! I tested and that's a lie at least on Linux.
|
||||
// Regardless, if you don't have IPv6,
|
||||
// an attempt to connect to an IPv6 socket *should* immediately give a "network unreachable" socket error.
|
||||
// This will cause the code to immediately try the next address,
|
||||
// so IPv6 just gets "skipped over" if you don't have it.
|
||||
// I could find no other robust way to check "is there a chance in hell IPv6 works" other than "try it",
|
||||
// so... try it we will.
|
||||
var endPoint = context.DnsEndPoint;
|
||||
var resolvedAddresses = await GetIpsForHost(endPoint, cancellationToken).ConfigureAwait(false);
|
||||
if (resolvedAddresses.Length == 0)
|
||||
throw new Exception($"Host {context.DnsEndPoint.Host} resolved to no IPs!");
|
||||
|
||||
if (!_hasResolvedIPv6Availability)
|
||||
{
|
||||
// to make things move fast, use a very low timeout for the initial ipv6 attempt.
|
||||
var quickFailCts = new CancellationTokenSource(FirstTryTimeout);
|
||||
var linkedTokenSource =
|
||||
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token);
|
||||
// Sort as specified in the RFC, interleaving.
|
||||
var ips = SortInterleaved(resolvedAddresses);
|
||||
|
||||
localToken = linkedTokenSource.Token;
|
||||
}
|
||||
Debug.Assert(ips.Length > 0);
|
||||
|
||||
return await AttemptConnection(AddressFamily.InterNetworkV6, context, localToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
|
||||
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
|
||||
// but in the interest of keeping this implementation simple, this is acceptable.
|
||||
_useIPv6 = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_hasResolvedIPv6Availability = true;
|
||||
}
|
||||
}
|
||||
var (socket, index) = await ParallelTask(
|
||||
ips.Length,
|
||||
(i, cancel) => AttemptConnection(i, ips[i], endPoint.Port, cancel),
|
||||
TimeSpan.FromMilliseconds(ConnectionAttemptDelay),
|
||||
cancellationToken);
|
||||
|
||||
// fallback to IPv4.
|
||||
return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken);
|
||||
// Log.Verbose("Successfully connected {EndPoint} to address: {Address}", endPoint, ips[index]);
|
||||
|
||||
return new NetworkStream(socket, ownsSocket: true);
|
||||
}
|
||||
|
||||
private static async ValueTask<Stream> AttemptConnection(
|
||||
AddressFamily addressFamily,
|
||||
SocketsHttpConnectionContext context,
|
||||
CancellationToken cancellationToken)
|
||||
private static async Task<Socket> AttemptConnection(
|
||||
int index,
|
||||
IPAddress address,
|
||||
int port,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
// Log.Verbose("Trying IP {Address} for happy eyeballs [{Index}]", address, index);
|
||||
|
||||
// The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
|
||||
var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
|
||||
var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
|
||||
{
|
||||
// Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
|
||||
NoDelay = true
|
||||
@@ -89,15 +102,155 @@ internal static class HappyEyeballsHttp
|
||||
|
||||
try
|
||||
{
|
||||
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
|
||||
// The stream should take the ownership of the underlying socket,
|
||||
// closing it when it's disposed.
|
||||
return new NetworkStream(socket, ownsSocket: true);
|
||||
#if DEBUG
|
||||
if (address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
await Task.Delay(SlowIpv6, cancel).ConfigureAwait(false);
|
||||
|
||||
if (BrokenIpv6)
|
||||
throw new Exception("Oh no I can't reach the network this is SO SAD.");
|
||||
}
|
||||
#endif
|
||||
|
||||
await socket.ConnectAsync(new IPEndPoint(address, port), cancel).ConfigureAwait(false);
|
||||
return socket;
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
// Log.Verbose(e, "Happy Eyeballs to {Address} [{Index}] failed", address, index);
|
||||
socket.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IPAddress[]> GetIpsForHost(DnsEndPoint endPoint, CancellationToken cancel)
|
||||
{
|
||||
if (IPAddress.TryParse(endPoint.Host, out var ip))
|
||||
return [ip];
|
||||
|
||||
var entry = await Dns.GetHostEntryAsync(endPoint.Host, cancel).ConfigureAwait(false);
|
||||
return entry.AddressList;
|
||||
}
|
||||
|
||||
private static IPAddress[] SortInterleaved(IPAddress[] addresses)
|
||||
{
|
||||
// Interleave returned addresses so that they are IPv6 -> IPv4 -> IPv6 -> IPv4.
|
||||
// Assuming we have multiple addresses of the same type that is.
|
||||
// As described in the RFC.
|
||||
|
||||
var ipv6 = addresses.Where(x => x.AddressFamily == AddressFamily.InterNetworkV6).ToArray();
|
||||
var ipv4 = addresses.Where(x => x.AddressFamily == AddressFamily.InterNetwork).ToArray();
|
||||
|
||||
var commonLength = Math.Min(ipv6.Length, ipv4.Length);
|
||||
|
||||
var result = new IPAddress[addresses.Length];
|
||||
for (var i = 0; i < commonLength; i++)
|
||||
{
|
||||
result[i * 2] = ipv6[i];
|
||||
result[1 + i * 2] = ipv4[i];
|
||||
}
|
||||
|
||||
if (ipv4.Length > ipv6.Length)
|
||||
{
|
||||
ipv4.AsSpan(commonLength).CopyTo(result.AsSpan(commonLength * 2));
|
||||
}
|
||||
else if (ipv6.Length > ipv4.Length)
|
||||
{
|
||||
ipv4.AsSpan(commonLength).CopyTo(result.AsSpan(commonLength * 2));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[SuppressMessage("Usage", "RA0004:Risk of deadlock from accessing Task<T>.Result")]
|
||||
internal static async Task<(T, int)> ParallelTask<T>(
|
||||
int candidateCount,
|
||||
Func<int, CancellationToken, Task<T>> taskBuilder,
|
||||
TimeSpan delay,
|
||||
CancellationToken cancel) where T : IDisposable
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(candidateCount);
|
||||
|
||||
using var successCts = CancellationTokenSource.CreateLinkedTokenSource(cancel);
|
||||
|
||||
// All tasks we have ever tried.
|
||||
var allTasks = new List<Task<T>>();
|
||||
// Tasks we are still waiting on.
|
||||
var tasks = new List<Task<T>>();
|
||||
|
||||
// The general loop here is as follows:
|
||||
// 1. Add a new task for the next IP to try.
|
||||
// 2. Wait until any task completes OR the delay happens.
|
||||
// If an error occurs, we stop checking that task and continue checking the next.
|
||||
// Every iteration we add another task, until we're full on them.
|
||||
// We keep looping until we have SUCCESS, or we run out of attempt tasks entirely.
|
||||
|
||||
Task<T>? successTask = null;
|
||||
while (successTask == null && (allTasks.Count < candidateCount || tasks.Count > 0))
|
||||
{
|
||||
if (allTasks.Count < candidateCount)
|
||||
{
|
||||
// We have to queue another task this iteration.
|
||||
var newTask = taskBuilder(allTasks.Count, successCts.Token);
|
||||
tasks.Add(newTask);
|
||||
allTasks.Add(newTask);
|
||||
}
|
||||
|
||||
var whenAnyDone = Task.WhenAny(tasks);
|
||||
Task<T> completedTask;
|
||||
|
||||
if (allTasks.Count < candidateCount)
|
||||
{
|
||||
// Log.Verbose("Waiting on ConnectionAttemptDelay");
|
||||
// If we have another one to queue, wait for a timeout instead of *just* waiting for a connection task.
|
||||
var timeoutTask = Task.Delay(delay, successCts.Token);
|
||||
var whenAnyOrTimeout = await Task.WhenAny(whenAnyDone, timeoutTask).ConfigureAwait(false);
|
||||
if (whenAnyOrTimeout != whenAnyDone)
|
||||
{
|
||||
// Timeout finished. Go to next iteration so we queue another one.
|
||||
continue;
|
||||
}
|
||||
|
||||
completedTask = whenAnyDone.Result;
|
||||
}
|
||||
else
|
||||
{
|
||||
completedTask = await whenAnyDone.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (completedTask.IsCompletedSuccessfully)
|
||||
{
|
||||
// We did it. We have success.
|
||||
successTask = completedTask;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Faulted. Remove it.
|
||||
tasks.Remove(completedTask);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Assert(allTasks.Count > 0);
|
||||
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
await successCts.CancelAsync().ConfigureAwait(false);
|
||||
|
||||
if (successTask == null)
|
||||
{
|
||||
// We didn't get a single successful connection. Well heck.
|
||||
throw new AggregateException(
|
||||
allTasks.Where(x => x.IsFaulted).SelectMany(x => x.Exception!.InnerExceptions));
|
||||
}
|
||||
|
||||
// I don't know if this is possible but MAKE SURE that we don't get two sockets completing at once.
|
||||
// Just a safety measure.
|
||||
foreach (var task in allTasks)
|
||||
{
|
||||
if (task.IsCompletedSuccessfully && task != successTask)
|
||||
task.Result.Dispose();
|
||||
}
|
||||
|
||||
return (successTask.Result, allTasks.IndexOf(successTask));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,13 @@ namespace Robust.Shared.Network.Messages
|
||||
{
|
||||
public override MsgGroups MsgGroup => MsgGroups.Core;
|
||||
|
||||
public byte PlyCount { get; set; }
|
||||
public List<SessionState> Plyrs { get; set; }
|
||||
|
||||
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
|
||||
{
|
||||
Plyrs = new List<SessionState>();
|
||||
PlyCount = buffer.ReadByte();
|
||||
for (var i = 0; i < PlyCount; i++)
|
||||
var playerCount = buffer.ReadInt32();
|
||||
Plyrs = new List<SessionState>(playerCount);
|
||||
for (var i = 0; i < playerCount; i++)
|
||||
{
|
||||
var plyNfo = new SessionState
|
||||
{
|
||||
@@ -34,7 +33,7 @@ namespace Robust.Shared.Network.Messages
|
||||
|
||||
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
|
||||
{
|
||||
buffer.Write(PlyCount);
|
||||
buffer.Write(Plyrs.Count);
|
||||
|
||||
foreach (var ply in Plyrs)
|
||||
{
|
||||
|
||||
@@ -17,15 +17,21 @@ namespace Robust.Shared.Network.Messages
|
||||
// Ideally we would peg this to the actual configured MTU instead of the default constant, but oh well...
|
||||
public const int ReliableThreshold = NetPeerConfiguration.kDefaultMTU - 20;
|
||||
|
||||
// If a state is larger than this, compress it with deflate.
|
||||
// If a state is larger than this, we will compress it
|
||||
// TODO PVS make this a cvar
|
||||
// TODO PVS figure out optimal value
|
||||
public const int CompressionThreshold = 256;
|
||||
|
||||
public override MsgGroups MsgGroup => MsgGroups.Entity;
|
||||
|
||||
public GameState State;
|
||||
public MemoryStream StateStream;
|
||||
|
||||
public ZStdCompressionContext CompressionContext;
|
||||
|
||||
internal bool _hasWritten;
|
||||
internal bool HasWritten;
|
||||
|
||||
internal bool ForceSendReliably;
|
||||
|
||||
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
|
||||
{
|
||||
@@ -60,26 +66,19 @@ namespace Robust.Shared.Network.Messages
|
||||
|
||||
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
|
||||
{
|
||||
using var stateStream = RobustMemoryManager.GetMemoryStream();
|
||||
serializer.SerializeDirect(stateStream, State);
|
||||
buffer.WriteVariableInt32((int)stateStream.Length);
|
||||
buffer.WriteVariableInt32((int)StateStream.Length);
|
||||
|
||||
// We compress the state.
|
||||
if (stateStream.Length > CompressionThreshold)
|
||||
if (StateStream.Length > CompressionThreshold)
|
||||
{
|
||||
// var sw = Stopwatch.StartNew();
|
||||
stateStream.Position = 0;
|
||||
var buf = ArrayPool<byte>.Shared.Rent(ZStd.CompressBound((int)stateStream.Length));
|
||||
var length = CompressionContext.Compress2(buf, stateStream.AsSpan());
|
||||
StateStream.Position = 0;
|
||||
var buf = ArrayPool<byte>.Shared.Rent(ZStd.CompressBound((int)StateStream.Length));
|
||||
var length = CompressionContext.Compress2(buf, StateStream.AsSpan());
|
||||
|
||||
buffer.WriteVariableInt32(length);
|
||||
|
||||
buffer.Write(buf.AsSpan(0, length));
|
||||
|
||||
// var elapsed = sw.Elapsed;
|
||||
// System.Console.WriteLine(
|
||||
// $"From: {State.FromSequence} To: {State.ToSequence} Size: {length} B Before: {stateStream.Length} B time: {elapsed}");
|
||||
|
||||
ArrayPool<byte>.Shared.Return(buf);
|
||||
}
|
||||
// The state is sent as is.
|
||||
@@ -87,10 +86,10 @@ namespace Robust.Shared.Network.Messages
|
||||
{
|
||||
// 0 means that the state isn't compressed.
|
||||
buffer.WriteVariableInt32(0);
|
||||
buffer.Write(stateStream.AsSpan());
|
||||
buffer.Write(StateStream.AsSpan());
|
||||
}
|
||||
|
||||
_hasWritten = true;
|
||||
HasWritten = true;
|
||||
MsgSize = buffer.LengthBytes;
|
||||
}
|
||||
|
||||
@@ -101,21 +100,12 @@ namespace Robust.Shared.Network.Messages
|
||||
/// <returns></returns>
|
||||
public bool ShouldSendReliably()
|
||||
{
|
||||
DebugTools.Assert(_hasWritten, "Attempted to determine sending method before determining packet size.");
|
||||
return State.ForceSendReliably || MsgSize > ReliableThreshold;
|
||||
DebugTools.Assert(HasWritten, "Attempted to determine sending method before determining packet size.");
|
||||
return ForceSendReliably || MsgSize > ReliableThreshold;
|
||||
}
|
||||
|
||||
public override NetDeliveryMethod DeliveryMethod
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ShouldSendReliably())
|
||||
{
|
||||
return NetDeliveryMethod.ReliableUnordered;
|
||||
}
|
||||
|
||||
return base.DeliveryMethod;
|
||||
}
|
||||
}
|
||||
public override NetDeliveryMethod DeliveryMethod => ShouldSendReliably()
|
||||
? NetDeliveryMethod.ReliableUnordered
|
||||
: base.DeliveryMethod;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using Prometheus;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Profiling;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -31,11 +31,14 @@ namespace Robust.Shared.Physics.Systems
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
private EntityQuery<BroadphaseComponent> _broadphaseQuery;
|
||||
private EntityQuery<FixturesComponent> _fixturesQuery;
|
||||
private EntityQuery<MapGridComponent> _gridQuery;
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
private EntityQuery<PhysicsMapComponent> _mapQuery;
|
||||
|
||||
private float _broadphaseExpand;
|
||||
|
||||
/*
|
||||
* Okay so Box2D has its own "MoveProxy" stuff so you can easily find new contacts when required.
|
||||
* Our problem is that we have nested broadphases (rather than being on separate maps) which makes this
|
||||
@@ -43,23 +46,21 @@ namespace Robust.Shared.Physics.Systems
|
||||
* Hence we need to check which broadphases it does intersect and checkar for colliding bodies.
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// How much to expand bounds by to check cross-broadphase collisions.
|
||||
/// Ideally you want to set this to your largest body size.
|
||||
/// This only has a noticeable performance impact where multiple broadphases are in close proximity.
|
||||
/// </summary>
|
||||
private float _broadphaseExpand;
|
||||
|
||||
private const int PairBufferParallel = 8;
|
||||
|
||||
private ObjectPool<List<FixtureProxy>> _bufferPool =
|
||||
new DefaultObjectPool<List<FixtureProxy>>(new ListPolicy<FixtureProxy>(), 2048);
|
||||
private BroadphaseContactJob _contactJob;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_contactJob = new()
|
||||
{
|
||||
_mapManager = _mapManager,
|
||||
System = this,
|
||||
BroadphaseExpand = _broadphaseExpand,
|
||||
};
|
||||
|
||||
_broadphaseQuery = GetEntityQuery<BroadphaseComponent>();
|
||||
_fixturesQuery = GetEntityQuery<FixturesComponent>();
|
||||
_gridQuery = GetEntityQuery<MapGridComponent>();
|
||||
_physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
@@ -71,7 +72,11 @@ namespace Robust.Shared.Physics.Systems
|
||||
Subs.CVar(_cfg, CVars.BroadphaseExpand, SetBroadphaseExpand, true);
|
||||
}
|
||||
|
||||
private void SetBroadphaseExpand(float value) => _broadphaseExpand = value;
|
||||
private void SetBroadphaseExpand(float value)
|
||||
{
|
||||
_contactJob.BroadphaseExpand = value;
|
||||
_broadphaseExpand = value;
|
||||
}
|
||||
|
||||
#region Find Contacts
|
||||
|
||||
@@ -176,65 +181,34 @@ namespace Robust.Shared.Physics.Systems
|
||||
if (moveBuffer.Count == 0)
|
||||
return;
|
||||
|
||||
var count = moveBuffer.Count;
|
||||
var contactBuffer = ArrayPool<List<FixtureProxy>>.Shared.Rent(count);
|
||||
var pMoveBuffer = ArrayPool<(FixtureProxy Proxy, Box2 AABB)>.Shared.Rent(count);
|
||||
|
||||
var idx = 0;
|
||||
_contactJob.MapUid = _mapManager.GetMapEntityIdOrThrow(mapId);
|
||||
_contactJob.MoveBuffer.Clear();
|
||||
|
||||
foreach (var (proxy, aabb) in moveBuffer)
|
||||
{
|
||||
contactBuffer[idx] = _bufferPool.Get();
|
||||
pMoveBuffer[idx++] = (proxy, aabb);
|
||||
_contactJob.MoveBuffer.Add((proxy, aabb));
|
||||
}
|
||||
|
||||
var options = new ParallelOptions
|
||||
for (var i = _contactJob.ContactBuffer.Count; i < _contactJob.MoveBuffer.Count; i++)
|
||||
{
|
||||
MaxDegreeOfParallelism = _parallel.ParallelProcessCount,
|
||||
};
|
||||
_contactJob.ContactBuffer.Add(new List<FixtureProxy>());
|
||||
}
|
||||
|
||||
var batches = (int)MathF.Ceiling((float) count / PairBufferParallel);
|
||||
var count = moveBuffer.Count;
|
||||
|
||||
Parallel.For(0, batches, options, i =>
|
||||
{
|
||||
var start = i * PairBufferParallel;
|
||||
var end = Math.Min(start + PairBufferParallel, count);
|
||||
|
||||
for (var j = start; j < end; j++)
|
||||
{
|
||||
var (proxy, worldAABB) = pMoveBuffer[j];
|
||||
var buffer = contactBuffer[j];
|
||||
|
||||
var proxyBody = proxy.Body;
|
||||
DebugTools.Assert(!proxyBody.Deleted);
|
||||
|
||||
var state = (this, proxy, worldAABB, buffer);
|
||||
|
||||
// Get every broadphase we may be intersecting.
|
||||
_mapManager.FindGridsIntersecting(mapId, worldAABB.Enlarged(_broadphaseExpand), ref state,
|
||||
static (EntityUid uid, MapGridComponent _, ref (
|
||||
SharedBroadphaseSystem system,
|
||||
FixtureProxy proxy,
|
||||
Box2 worldAABB,
|
||||
List<FixtureProxy> pairBuffer) tuple) =>
|
||||
{
|
||||
ref var buffer = ref tuple.pairBuffer;
|
||||
tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, uid, buffer);
|
||||
return true;
|
||||
}, approx: true, includeMap: false);
|
||||
|
||||
// Struct ref moment, I have no idea what's fastest.
|
||||
buffer = state.buffer;
|
||||
FindPairs(proxy, worldAABB, _mapManager.GetMapEntityId(mapId), buffer);
|
||||
}
|
||||
});
|
||||
_parallel.ProcessNow(_contactJob, count);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var proxyA = pMoveBuffer[i].Proxy;
|
||||
var proxies = contactBuffer[i];
|
||||
var proxies = _contactJob.ContactBuffer[i];
|
||||
|
||||
if (proxies.Count == 0)
|
||||
continue;
|
||||
|
||||
var proxyA = _contactJob.MoveBuffer[i].Proxy;
|
||||
var proxyABody = proxyA.Body;
|
||||
FixturesComponent? manager = null;
|
||||
|
||||
_fixturesQuery.TryGetComponent(proxyA.Entity, out var manager);
|
||||
|
||||
foreach (var other in proxies)
|
||||
{
|
||||
@@ -253,13 +227,8 @@ namespace Robust.Shared.Physics.Systems
|
||||
|
||||
_physicsSystem.AddPair(proxyA.FixtureId, other.FixtureId, proxyA, other);
|
||||
}
|
||||
|
||||
_bufferPool.Return(contactBuffer[i]);
|
||||
pMoveBuffer[i] = default;
|
||||
}
|
||||
|
||||
ArrayPool<List<FixtureProxy>>.Shared.Return(contactBuffer);
|
||||
ArrayPool<(FixtureProxy Proxy, Box2 AABB)>.Shared.Return(pMoveBuffer);
|
||||
moveBuffer.Clear();
|
||||
movedGrids.Clear();
|
||||
}
|
||||
@@ -516,5 +485,51 @@ namespace Robust.Shared.Physics.Systems
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record struct BroadphaseContactJob() : IParallelRobustJob
|
||||
{
|
||||
public SharedBroadphaseSystem System = default!;
|
||||
public IMapManager _mapManager = default!;
|
||||
|
||||
public float BroadphaseExpand;
|
||||
|
||||
public EntityUid MapUid;
|
||||
|
||||
public List<List<FixtureProxy>> ContactBuffer = new();
|
||||
public List<(FixtureProxy Proxy, Box2 WorldAABB)> MoveBuffer = new();
|
||||
|
||||
public int BatchSize => 8;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
var (proxy, worldAABB) = MoveBuffer[index];
|
||||
var buffer = ContactBuffer[index];
|
||||
buffer.Clear();
|
||||
|
||||
var proxyBody = proxy.Body;
|
||||
DebugTools.Assert(!proxyBody.Deleted);
|
||||
|
||||
var state = (System, proxy, worldAABB, buffer);
|
||||
|
||||
// Get every broadphase we may be intersecting.
|
||||
_mapManager.FindGridsIntersecting(MapUid, worldAABB.Enlarged(BroadphaseExpand), ref state,
|
||||
static (EntityUid uid, MapGridComponent _, ref (
|
||||
SharedBroadphaseSystem system,
|
||||
FixtureProxy proxy,
|
||||
Box2 worldAABB,
|
||||
List<FixtureProxy> pairBuffer) tuple) =>
|
||||
{
|
||||
ref var buffer = ref tuple.pairBuffer;
|
||||
tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, uid, buffer);
|
||||
return true;
|
||||
},
|
||||
approx: true,
|
||||
includeMap: false);
|
||||
|
||||
// Struct ref moment, I have no idea what's fastest.
|
||||
buffer = state.buffer;
|
||||
System.FindPairs(proxy, worldAABB, MapUid, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
@@ -20,4 +21,29 @@ public sealed class ActorSystem : EntitySystem
|
||||
{
|
||||
_playerManager.SetAttachedEntity(component.PlayerSession, null);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public bool TryGetSession(EntityUid? uid, out ICommonSession? session)
|
||||
{
|
||||
if (TryComp(uid, out ActorComponent? actorComp))
|
||||
{
|
||||
session = actorComp.PlayerSession;
|
||||
return true;
|
||||
}
|
||||
|
||||
session = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
[Pure]
|
||||
public ICommonSession? GetSession(EntityUid? uid)
|
||||
{
|
||||
if (TryComp(uid, out ActorComponent? actorComp))
|
||||
{
|
||||
return actorComp.PlayerSession;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Shared.Player;
|
||||
|
||||
internal sealed class CommonSession : ICommonSession
|
||||
internal sealed class CommonSession : ICommonSessionInternal
|
||||
{
|
||||
[ViewVariables]
|
||||
public EntityUid? AttachedEntity { get; set; }
|
||||
@@ -17,10 +17,10 @@ internal sealed class CommonSession : ICommonSession
|
||||
public NetUserId UserId { get; }
|
||||
|
||||
[ViewVariables]
|
||||
public string Name { get; internal set; } = "<Unknown>";
|
||||
public string Name { get; set; } = "<Unknown>";
|
||||
|
||||
[ViewVariables]
|
||||
public short Ping { get; internal set; }
|
||||
public short Ping { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public DateTime ConnectedTime { get; set; }
|
||||
@@ -42,9 +42,6 @@ internal sealed class CommonSession : ICommonSession
|
||||
[ViewVariables]
|
||||
public HashSet<EntityUid> ViewSubscriptions { get; } = new();
|
||||
|
||||
[ViewVariables]
|
||||
public int VisibilityMask { get; set; } = 1;
|
||||
|
||||
[ViewVariables]
|
||||
public LoginType AuthType => Channel?.AuthType ?? default;
|
||||
|
||||
@@ -56,4 +53,29 @@ internal sealed class CommonSession : ICommonSession
|
||||
Name = name;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public void SetStatus(SessionStatus status)
|
||||
{
|
||||
Status = status;
|
||||
}
|
||||
|
||||
public void SetAttachedEntity(EntityUid? uid)
|
||||
{
|
||||
AttachedEntity = uid;
|
||||
}
|
||||
|
||||
public void SetPing(short ping)
|
||||
{
|
||||
Ping = ping;
|
||||
}
|
||||
|
||||
public void SetName(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public void SetChannel(INetChannel channel)
|
||||
{
|
||||
Channel = channel;
|
||||
}
|
||||
}
|
||||
|
||||
127
Robust.Shared/Player/DummySession.cs
Normal file
127
Robust.Shared/Player/DummySession.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Net;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Robust.Shared.Player;
|
||||
|
||||
/// <summary>
|
||||
/// This is a mock session for use with integration tests and benchmarks. It uses a <see cref="DummyChannel"/> as
|
||||
/// its <see cref="INetChannel"/>, which doesn't support actually sending any messages.
|
||||
/// </summary>
|
||||
internal sealed class DummySession : ICommonSessionInternal
|
||||
{
|
||||
public EntityUid? AttachedEntity {get; set; }
|
||||
public SessionStatus Status { get; set; } = SessionStatus.Connecting;
|
||||
public NetUserId UserId => UserData.UserId;
|
||||
public string Name => UserData.UserName;
|
||||
|
||||
public short Ping { get; set; }
|
||||
|
||||
public INetChannel Channel
|
||||
{
|
||||
get => DummyChannel;
|
||||
[Obsolete]
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public LoginType AuthType { get; set; } = LoginType.GuestAssigned;
|
||||
public HashSet<EntityUid> ViewSubscriptions { get; } = new();
|
||||
public DateTime ConnectedTime { get; set; }
|
||||
public SessionState State { get; set; } = new();
|
||||
public SessionData Data { get; set; }
|
||||
public bool ClientSide { get; set; }
|
||||
public NetUserData UserData { get; set; }
|
||||
|
||||
public DummyChannel DummyChannel;
|
||||
|
||||
public DummySession(NetUserId userId, string userName, SessionData data)
|
||||
{
|
||||
Data = data;
|
||||
UserData = new(userId, userName)
|
||||
{
|
||||
HWId = ImmutableArray<byte>.Empty
|
||||
};
|
||||
DummyChannel = new(this);
|
||||
}
|
||||
|
||||
public void SetStatus(SessionStatus status)
|
||||
{
|
||||
Status = status;
|
||||
}
|
||||
|
||||
public void SetAttachedEntity(EntityUid? uid)
|
||||
{
|
||||
AttachedEntity = uid;
|
||||
}
|
||||
|
||||
public void SetPing(short ping)
|
||||
{
|
||||
Ping = ping;
|
||||
}
|
||||
|
||||
public void SetName(string name)
|
||||
{
|
||||
UserData = new(UserData.UserId, name)
|
||||
{
|
||||
HWId = UserData.HWId
|
||||
};
|
||||
}
|
||||
|
||||
public void SetChannel(INetChannel channel)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A mock NetChannel for use in integration tests and benchmarks.
|
||||
/// </summary>
|
||||
internal sealed class DummyChannel(DummySession session) : INetChannel
|
||||
{
|
||||
public readonly DummySession Session = session;
|
||||
public NetUserData UserData => Session.UserData;
|
||||
public short Ping => Session.Ping;
|
||||
public string UserName => Session.Name;
|
||||
public LoginType AuthType => Session.AuthType;
|
||||
public NetUserId UserId => Session.UserId;
|
||||
|
||||
public int CurrentMtu { get; set; } = default;
|
||||
public long ConnectionId { get; set; } = default;
|
||||
public TimeSpan RemoteTimeOffset { get; set; } = default;
|
||||
public TimeSpan RemoteTime { get; set; } = default;
|
||||
public bool IsConnected { get; set; } = true;
|
||||
public bool IsHandshakeComplete { get; set; } = true;
|
||||
|
||||
// This is just pilfered from IntegrationNetChannel
|
||||
public IPEndPoint RemoteEndPoint { get; } = new(IPAddress.Loopback, 1212);
|
||||
|
||||
// Only used on server, contains the encryption to use for this channel.
|
||||
public NetEncryption? Encryption { get; set; }
|
||||
|
||||
public INetManager NetPeer => throw new NotImplementedException();
|
||||
|
||||
public T CreateNetMessage<T>() where T : NetMessage, new()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SendMessage(NetMessage message)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Disconnect(string reason)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Disconnect(string reason, bool sendBye)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -43,9 +43,8 @@ public interface ICommonSession
|
||||
/// <remarks>
|
||||
/// On the Server every player has a network channel,
|
||||
/// on the Client only the LocalPlayer has a network channel, and that channel points to the server.
|
||||
/// Unless you know what you are doing, you shouldn't be modifying this directly.
|
||||
/// </remarks>
|
||||
INetChannel Channel { get; set; }
|
||||
INetChannel Channel { get; [Obsolete] set; }
|
||||
|
||||
LoginType AuthType { get; }
|
||||
|
||||
@@ -75,3 +74,12 @@ public interface ICommonSession
|
||||
/// </summary>
|
||||
bool ClientSide { get; set; }
|
||||
}
|
||||
|
||||
internal interface ICommonSessionInternal : ICommonSession
|
||||
{
|
||||
public void SetStatus(SessionStatus status);
|
||||
public void SetAttachedEntity(EntityUid? uid);
|
||||
public void SetPing(short ping);
|
||||
public void SetName(string name);
|
||||
void SetChannel(INetChannel channel);
|
||||
}
|
||||
|
||||
@@ -101,8 +101,8 @@ internal abstract partial class SharedPlayerManager
|
||||
|
||||
public ICommonSession CreateAndAddSession(INetChannel channel)
|
||||
{
|
||||
var session = CreateAndAddSession(channel.UserId, channel.UserName);
|
||||
session.Channel = channel;
|
||||
var session = (ICommonSessionInternal)CreateAndAddSession(channel.UserId, channel.UserName);
|
||||
session.SetChannel(channel);
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ internal abstract partial class SharedPlayerManager
|
||||
if (session.AttachedEntity is not {} uid)
|
||||
return;
|
||||
|
||||
((CommonSession) session).AttachedEntity = null;
|
||||
((ICommonSessionInternal) session).SetAttachedEntity(null);
|
||||
UpdateState(session);
|
||||
|
||||
if (EntManager.TryGetComponent(uid, out ActorComponent? actor) && actor.LifeStage <= ComponentLifeStage.Running)
|
||||
@@ -215,7 +215,7 @@ internal abstract partial class SharedPlayerManager
|
||||
if (session.AttachedEntity != null)
|
||||
Detach(session);
|
||||
|
||||
((CommonSession) session).AttachedEntity = uid;
|
||||
((ICommonSessionInternal) session).SetAttachedEntity(uid);
|
||||
actor.PlayerSession = session;
|
||||
UpdateState(session);
|
||||
EntManager.EventBus.RaiseLocalEvent(uid, new PlayerAttachedEvent(uid, session), true);
|
||||
@@ -228,7 +228,7 @@ internal abstract partial class SharedPlayerManager
|
||||
return;
|
||||
|
||||
var old = session.Status;
|
||||
((CommonSession) session).Status = status;
|
||||
((ICommonSessionInternal) session).SetStatus(status);
|
||||
|
||||
UpdateState(session);
|
||||
PlayerStatusChanged?.Invoke(this, new SessionStatusEventArgs(session, old, status));
|
||||
@@ -236,13 +236,13 @@ internal abstract partial class SharedPlayerManager
|
||||
|
||||
public void SetPing(ICommonSession session, short ping)
|
||||
{
|
||||
((CommonSession) session).Ping = ping;
|
||||
((ICommonSessionInternal) session).SetPing(ping);
|
||||
UpdateState(session);
|
||||
}
|
||||
|
||||
public void SetName(ICommonSession session, string name)
|
||||
{
|
||||
((CommonSession) session).Name = name;
|
||||
((ICommonSessionInternal) session).SetName(name);
|
||||
UpdateState(session);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
|
||||
@@ -143,17 +143,20 @@ public interface IPrototypeManager
|
||||
/// </summary>
|
||||
FrozenDictionary<string, T> GetInstances<T>() where T : IPrototype;
|
||||
|
||||
/// <inheritdoc cref="TryIndex{T}(string, out T)"/>
|
||||
bool TryIndex(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype);
|
||||
/// <inheritdoc cref="TryIndex{T}(ProtoId{T}, out T, bool)"/>
|
||||
bool TryIndex(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true);
|
||||
|
||||
/// <inheritdoc cref="TryIndex{T}(string, out T)"/>
|
||||
bool TryIndex<T>(ProtoId<T> id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype;
|
||||
/// <summary>
|
||||
/// Attempt to retrieve the prototype corresponding to the given prototype id.
|
||||
/// Unless otherwise specified, this will log an error if the id does not match any known prototype.
|
||||
/// </summary>
|
||||
bool TryIndex<T>(ProtoId<T> id, [NotNullWhen(true)] out T? prototype, bool logError = true) where T : class, IPrototype;
|
||||
|
||||
/// <inheritdoc cref="TryIndex{T}(string, out T)"/>
|
||||
bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype);
|
||||
/// <inheritdoc cref="TryIndex{T}(ProtoId{T}, out T, bool)"/>
|
||||
bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true);
|
||||
|
||||
/// <inheritdoc cref="TryIndex{T}(string, out T)"/>
|
||||
bool TryIndex<T>(ProtoId<T>? id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype;
|
||||
/// <inheritdoc cref="TryIndex{T}(ProtoId{T}, out T, bool)"/>
|
||||
bool TryIndex<T>(ProtoId<T>? id, [NotNullWhen(true)] out T? prototype, bool logError = true) where T : class, IPrototype;
|
||||
|
||||
bool HasMapping<T>(string id);
|
||||
bool TryGetMapping(Type kind, string id, [NotNullWhen(true)] out MappingDataNode? mappings);
|
||||
|
||||
@@ -108,7 +108,7 @@ public abstract partial class PrototypeManager : IPrototypeManagerInternal
|
||||
}
|
||||
}
|
||||
|
||||
DebugTools.Assert(!TryIndex(id, out var instance)
|
||||
DebugTools.Assert(!TryIndex(id, out var instance, false)
|
||||
|| instance.CategoriesInternal == null
|
||||
|| instance.CategoriesInternal.All(x =>
|
||||
set.Any(y => y.ID == x)));
|
||||
@@ -124,7 +124,7 @@ public abstract partial class PrototypeManager : IPrototypeManagerInternal
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryIndex(id, out var protoInstance))
|
||||
if (!TryIndex(id, out var protoInstance, false))
|
||||
{
|
||||
// Prototype is abstract
|
||||
cache.Add(id, set);
|
||||
|
||||
@@ -744,19 +744,29 @@ namespace Robust.Shared.Prototypes
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryIndex(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype)
|
||||
public bool TryIndex(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true)
|
||||
{
|
||||
return TryIndex(id.Id, out prototype);
|
||||
if (TryIndex(id.Id, out prototype))
|
||||
return true;
|
||||
|
||||
if (logError)
|
||||
Sawmill.Error($"Attempted to resolve invalid {nameof(EntProtoId)}: {id.Id}");
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryIndex<T>(ProtoId<T> id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype
|
||||
public bool TryIndex<T>(ProtoId<T> id, [NotNullWhen(true)] out T? prototype, bool logError = true) where T : class, IPrototype
|
||||
{
|
||||
return TryIndex(id.Id, out prototype);
|
||||
if (TryIndex(id.Id, out prototype))
|
||||
return true;
|
||||
|
||||
if (logError)
|
||||
Sawmill.Error($"Attempted to resolve invalid ProtoId<{typeof(T).Name}>: {id.Id}");
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype)
|
||||
public bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true)
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
@@ -764,11 +774,11 @@ namespace Robust.Shared.Prototypes
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryIndex(id.Value, out prototype);
|
||||
return TryIndex(id.Value, out prototype, logError);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryIndex<T>(ProtoId<T>? id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype
|
||||
public bool TryIndex<T>(ProtoId<T>? id, [NotNullWhen(true)] out T? prototype, bool logError = true) where T : class, IPrototype
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
@@ -776,7 +786,7 @@ namespace Robust.Shared.Prototypes
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryIndex(id.Value, out prototype);
|
||||
return TryIndex(id.Value, out prototype, logError);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user