Compare commits

...

58 Commits

Author SHA1 Message Date
Pieter-Jan Briers
b65e0c64ea Version: 226.2.2 2024-08-11 19:54:41 +02:00
Pieter-Jan Briers
4c388bc03d Use absolute path for explorer.exe
frick me

(cherry picked from commit 0284eb0430)
2024-08-11 19:54:41 +02:00
Pieter-Jan Briers
658dee1591 Version: 226.2.1 2024-08-11 17:54:19 +02:00
Pieter-Jan Briers
b6e5cca127 Security updates (#5353)
* Fix security bug in WritableDirProvider.OpenOsWindow()

Reported by @NarryG and @nyeogmi

* Sandbox updates

* Update ImageSharp again

(cherry picked from commit 7d778248ee)
(cherry picked from commit f66cda74e95619ddba2221bda644bf4394619805)
2024-08-11 17:54:19 +02:00
metalgearsloth
da5416a2da Version: 226.2.0 2024-06-20 17:28:11 +10:00
metalgearsloth
021845d956 Add some System.Random methods (#5177)
* Add some System.Random methods

* weh
2024-06-20 17:23:47 +10:00
Leon Friedrich
7fab9f3b8d Fix ContainerSystem debug assert (#5254) 2024-06-20 17:23:38 +10:00
Pieter-Jan Briers
69c1161562 FormattedMessage/DebugConsole performance improvements (#5244)
* Add VisibilityChanged virtual to Control

* Defer updating invisible OutputPanels on UIScale change

DebugConsole falls under this when not hidden, and it significantly improves perf of e.g. resizing the window when there's a lot of stuff in there.

* Avoid redundant UI Scale updates on window resize.

Window resizing can change the UI scale, due to the auto-scaling system. This system had multiple perf issues:

UI scale was set and propagated even if it didn't change (system disabled, not effective, etc). This was just wasted processing.

UI scale was updated for every window resize event. When the game is lagging (due to the aforementioned UI scale updates being expensive...) this means multiple window resize events in a single frame ALL cause a UI scale update, which is useless.

UI scale updates from resizing now avoid doing *nothing* and are deferred until later in the frame for natural batching.

* Reduce allocations/memory usage of various rich-text related things

Just allocate a buncha dictionaries what could possibly go wrong.

I kept to non-breaking-changes which means this couldn't as effective as it should be.

There's some truly repulsive stuff here. Ugh.

* Cap debug console content size.

It's a CVar.

OutputPanel has been switched to use a new RingBufferList datastructure to make removal of the oldest entry efficient.

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2024-06-20 17:22:12 +10:00
metalgearsloth
095fe9d60f Turn broadphase contacts into a job (#5245)
Okay when I said no more physics this was a low-hanging fruit as we can get rid of the mapmanager getmapentityid for every contact so.
2024-06-20 17:19:26 +10:00
Leon Friedrich
14138fbcc2 Separate PVS serialization from compression & sending (#5246) 2024-06-20 17:18:51 +10:00
Pieter-Jan Briers
48ce24e98b Remove race condition invoking ThreadPool.SetMinThreads call
For some reason we call ThreadPool.SetMinThreads on startup of the game server. Calling this function this early seems to put us at high risk of triggering the following deadlock bug in the .NET runtime: https://github.com/dotnet/runtime/issues/93175

Given I have zero trust in whether this manual ThreadPool fuckery is even helpful, I'm just gonna nuke it and call it a day.
2024-06-20 03:12:01 +02:00
Pieter-Jan Briers
9cde21a7b3 Lower default MTU again.
Yet more reports of people running into issues with the current default.
2024-06-20 00:15:27 +02:00
Pieter-Jan Briers
ae1051e813 Cache non-existence of ResourceCache TryGetResource.
Many patterns (both in engine and content) make use of regular TryGetResource returning null. The problem is that if the resource doesn't exist, it won't be cached and the code attempts to load it from disk *every single time*.

For example, opening an inventory in SS14 would hang the client for ages on some UI themes due to the UITheme texture fallback system constantly trying to load a texture that doesn't exist.
2024-06-19 22:50:09 +02:00
Leon Friedrich
a3f80ac7dd Increase default value of res.rsi_atlas_size (#5250) 2024-06-19 22:09:39 +02:00
CaasGit
f98ef78a21 Update LoaderApi to the latest commit. (#5256) 2024-06-19 19:52:49 +02:00
metalgearsloth
bf8054b181 Version: 226.1.0 2024-06-18 21:50:18 +10:00
metalgearsloth
6b875e6676 Add local entities APIs (#5178)
Need for some vgroid stuff
2024-06-18 21:41:38 +10:00
Vasilis
a687c0a6c0 Change "to" to "from" on advert error (#5247)
It's a message FROM the hub

Currently, if you get "You are banned from the hub, if you believe this is an error contact us" it may confuse someone that they have to visit the hub URL where they will be met with a 404 because it's not an actual website. Seems it looks like "contact us to website"

Similarly, with "Failed to contact status address" makes it look like it's an error message coming from robust failing to connect to the hub server. When it's actually coming from the hub, telling you probably don't have your ports open.

I believe changing it to "from" will get the message acros that this is a message from the HUB and not robust.
2024-06-17 15:15:30 +02:00
Pieter-Jan Briers
0580cf3ff7 Drop SQL exporter in Robust.Benchmarks to fix compilation.
It was using an old Npgsql version, which broke compilation. Updating it breaks some of the custom JSON mapping code.

Comment out the entire thing, it's not being used anymore anyways.
2024-06-17 02:00:11 +02:00
Pieter-Jan Briers
590964d5bf Update SpaceWizards.HttpListener to 0.1.1
This fixes an EXTREMELY RARE crash on server startup due to a race condition. Yes, it did cause a crash in practice that's how I noticed it.
2024-06-16 21:34:15 +02:00
Pieter-Jan Briers
ceda39813d Fix MsgPlayerList being capped to 255
WHY WAS THIS A BYTE.

This prevented having more than 255 people on a server, beyond that the game might get stuck as people's player states wouldn't necessarily get sent.
2024-06-16 21:31:57 +02:00
metalgearsloth
a3a8912f42 Version: 226.0.0 2024-06-17 01:50:04 +10:00
metalgearsloth
b40973157d Animation player fixes (#5238)
Ensures the event always goes out even if the animation is stopped.
2024-06-17 01:47:31 +10:00
Leon Friedrich
1de8731465 Reduce uses of IComponentFactory.GetIndex(Type) (#5242)
* Update `RaiseComponentEvent` & component lifestatge methods

* Fix ComponentNetworkGenerator

* a

* A
2024-06-17 01:46:51 +10:00
Leon Friedrich
3a479cb5f4 Add ComponentEventAttribute to AfterAutoHandleStateEvent (#5243) 2024-06-15 17:32:19 +02:00
slarticodefast
76eeebf439 Allow RequestScreenTexture to be set in overlays (#5234) 2024-06-15 23:35:10 +10:00
metalgearsloth
2fa83181e2 Version: 225.0.0 2024-06-15 16:46:54 +10:00
Leon Friedrich
36f02b4a18 Fix IComponentFactory mock in tests (#5240) 2024-06-15 16:38:05 +10:00
metalgearsloth
e842142dd7 Minor API niceties (#5219)
* Minor API niceties

* weh
2024-06-15 16:26:01 +10:00
Leon Friedrich
2eb740cea8 Try prevent eventbus loops (#5166)
* Add test

* Try prevent event bus linked list loops

* Eh, add an upper limit anyways
2024-06-15 12:20:48 +10:00
metalgearsloth
a044f04e3b Remove CompIdx locks (#5231)
* Remove CompIdx locks

So GetComponentState in PVS calls RaiseComponentEvent which in turn calls this. When you start getting a significant number of players it seems to run into lock contention considering every single compstate get will lock this.

Instead we'll just update the dictionary whenever RegisterClass is called instead.

* Fix thread-safety issues
2024-06-15 01:30:45 +10:00
Leon Friedrich
a4723d1f62 Avoid read lock in GetEntityQuery (#5236) 2024-06-15 01:19:29 +10:00
Pieter-Jan Briers
627c1eb054 Rewrite HappyEyeballsHttp
This makes the game use HTTPS more when available.

Implementation is just taken from my work on the launcher, nothing special here.
2024-06-14 11:27:34 +02:00
Pieter-Jan Briers
836aec0b87 Changelog for Toolshed ent change
Forgot this in 5c83678c78. Oops.
2024-06-14 03:03:33 +02:00
Pieter-Jan Briers
9116e64291 Implement info query ?can_skip_build=1
The hub has been adding this parameter for a while, instructing the game server that it doesn't need to run ACZ. This fixes the (relatively common) issue where the first publish fails because ACZ takes longer than the hub status timeout.

I apparently already committed some code for this once on accident. Whoops.
2024-06-14 03:02:12 +02:00
metalgearsloth
a6bfb5f557 Fix lookupflags oversight (#5233)
The ONE codepath CM-14 used and I forgot to add it.
2024-06-14 09:59:47 +10:00
Pieter-Jan Briers
5c83678c78 Fix "ent" toolshed command
Makes it use NetEntity instead of EntityUid.
2024-06-13 00:22:22 +02:00
Pieter-Jan Briers
eac94b1032 Allow Eye position to be set directly.
Eye is not a well-designed API, but we've got it so here we go. It was originally designed to have some form of support for non-entity eyes through the FixedEye type, by overriding the Position property in a child type. #1016 broke this however.

This PR just makes the property writable so this is possible again.

Co-authored-by: moonheart08 <moony@hellomouse.net>
2024-06-12 23:56:29 +02:00
metalgearsloth
efd870d070 Mark System<T> as pure (#5225) 2024-06-11 03:43:20 +02:00
Leon Friedrich
94f98073b0 Make PrototypeManager.TryIndex log errors when using invalid id structs (#5203)
* Make `PrototypeManager.TryIndex` log errors when using id structs

* A
2024-06-08 22:15:21 +10:00
DrSmugleaf
5aa9378de0 Add an overload of TerminatingOrDeleted with a nullable EntityUid (#5214) 2024-06-08 20:51:05 +10:00
Leon Friedrich
850e9ab695 Try optimize NetEntities console completion helper (#5217)
* Try optimize `NetEntities` completion options

* Actually just remove it

* a
2024-06-08 20:44:21 +10:00
Tayrtahn
7319f3a241 Raise an event when an entity's name is changed (#5216) 2024-06-08 10:45:28 +10:00
DrSmugleaf
b6252c9e4f Make Entity<T> work as a loc parameter (#5215) 2024-06-07 15:48:00 +02:00
ElectroJr
fcd507d1f9 Version: 224.1.1 2024-06-06 00:58:26 +12:00
Leon Friedrich
1eb874f4c3 Fix storage key-not-found exception (#5213) 2024-06-05 22:56:50 +10:00
ElectroJr
a628d31c4b Version: 224.1.0 2024-06-05 20:19:18 +12:00
Leon Friedrich
2b0ecd7166 Fix cvar type errors (#5212) 2024-06-05 18:18:49 +10:00
Tom Leys
bde650689b Perf: Avoid a copy of ComponentChanges every tick within Checkpoints (#5146)
* Perf: Avoid a copy of ComponentChanges every tick within Checkpoints

- Also remove temporary Dictionary created every tick * every change
- Reduces GC load, 6GB less temporary allocations on typical replays.

* perf: Checkpoints: Apply state changes in-place when possible

- Avoids >1GB of gas tile allocations.

* Revert "perf: Checkpoints: Apply state changes in-place when possible"

This reverts commit 1a478944a6.

* Fix delta state merge issues

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2024-06-05 18:06:38 +10:00
Tom Leys
87d8d74d8c Perf: Improve replay playback responsiveness (#5152)
* Perf: Improve replay playback responsiveness

- new CVAR ReplayMaxScrubTime
- There is a time budget when applying replay ticks updates of only 10 ms
- Ensure we don't apply checkpoints that move us backwards in time by accident
- Prevent double-lookup of checkpoints.

* Fix merge error

* Fix it again, but for real this time

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2024-06-05 17:38:36 +10:00
Nemanja
dddf13a19a EntityPrototypeView (#5185)
* ent proto view

* pee jay bee rahvew

* Fix EnteredTree() not respawning the entity

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2024-06-05 17:04:49 +10:00
Leon Friedrich
75626a86a3 Add dummy sessions for integration tests (#5202)
* Add dummy sessions

* if FULL_RELEASE
2024-06-05 16:50:06 +10:00
Tornado Tech
3e3cd0e257 Fixed incmd command (#5192)
* Fixed incmd command

* Change `HandleInputCommand` argument type

* Localize console errors

* Why is input code even like this
2024-06-05 16:32:56 +10:00
deltanedas
a3a90154a4 add SetUi to shared ui system (#5092)
* re-add AddUi

* rename to SetUi, add if missing

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
2024-06-05 15:25:49 +10:00
Ygg01
9240c94e59 Write Errors when a duplicate localization key is found. (#4885)
* Update Linguini to v0.8.1

* Add tests and verify desired behavior has been reached.

* Remove duplicate messages.

* Minor fix to message output. Add Wrapper for Fluent errors.

* Restart the test pipeline.

* Restart the test pipeline.

* Make so test don't do an early bailout.

* Ensure all errors get written rather than bailing on first.

* Fix text breakage.

* Remove obsolete // TODO LINGUINI

* line wrapping conventions

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2024-06-05 15:23:41 +10:00
ShadowCommander
a95ba9f181 Fix Toolshed EnumTypeParse on non-lowercase enum values (#5211) 2024-06-05 15:22:02 +10:00
Leon Friedrich
074a4faa92 Try fix client-side BUI error spam (#5208) 2024-06-05 15:21:27 +10:00
metalgearsloth
6b4d74f46e Maybe fix bad resolve logging (#5207)
I'm not entirely sure what happens, I couldn't repro it locally even when I tried to force it to use threadpool threads. The only info I have is it happens and no other info has been provided so.
2024-06-05 09:00:41 +10:00
114 changed files with 3193 additions and 1021 deletions

View File

@@ -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" />

View File

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

View File

@@ -54,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

View File

@@ -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...]

View File

@@ -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();

View File

@@ -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>
{

View File

@@ -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
}

View File

@@ -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.")]

View File

@@ -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; }

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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"))

View File

@@ -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();
}
}

View File

@@ -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);

View 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);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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]

View File

@@ -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;

View File

@@ -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>

View File

@@ -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()

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -216,6 +216,8 @@ namespace Robust.Client.UserInterface
{
foreach (var root in _roots)
{
CheckRootUIScaleUpdate(root);
using (_prof.Group("Root"))
{
var totalUpdated = root.DoFrameUpdateRecursive(args);

View File

@@ -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()

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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]];
}
}
}
}

View 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;
}
}

View 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();
}
}

View File

@@ -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;
}

View File

@@ -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)
{

View File

@@ -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;
}
}
}

View File

@@ -39,8 +39,6 @@ namespace Robust.Server
return;
}
ThreadPool.SetMinThreads(Environment.ProcessorCount * 2, Environment.ProcessorCount);
ParsedMain(parsed, contentStart, options);
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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.

View File

@@ -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 />

View File

@@ -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);

View File

@@ -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.

View 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()
{
}
}
}

View File

@@ -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
{

View File

@@ -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];

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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:

View File

@@ -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

View File

@@ -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)
{

View File

@@ -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;
}
}
}

View File

@@ -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();

View File

@@ -5,7 +5,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects;
[RegisterComponent, NetworkedComponent]
[RegisterComponent]
public sealed partial class ActiveUserInterfaceComponent : Component
{
}

View File

@@ -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.

View File

@@ -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();
}

View File

@@ -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)
{

View File

@@ -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];

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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>();

View File

@@ -586,7 +586,7 @@ namespace Robust.Shared.GameObjects
{
try
{
LifeShutdown(component);
LifeShutdown(uid, component, _componentFactory.GetIndex(component.GetType()));
}
catch (Exception e)
{

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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>

View File

@@ -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.

View File

@@ -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>

View File

@@ -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));

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -28,7 +28,7 @@ namespace Robust.Shared.Graphics
public virtual MapCoordinates Position
{
get => _coords;
internal set => _coords = value;
set => _coords = value;
}
[ViewVariables(VVAccess.ReadWrite)]

View File

@@ -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

View File

@@ -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}";
}
}

View File

@@ -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;
}

View File

@@ -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; }
};
}

View File

@@ -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());
}
}
}

View File

@@ -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));
}
}

View File

@@ -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)
{

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameStates;
using Robust.Shared.Timing;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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