Compare commits

...

70 Commits

Author SHA1 Message Date
Pieter-Jan Briers
07aa0ff230 Version: 214.2.3 2024-08-11 19:54:48 +02:00
Pieter-Jan Briers
2b47b798eb Use absolute path for explorer.exe
frick me

(cherry picked from commit 0284eb0430)
2024-08-11 19:54:47 +02:00
Pieter-Jan Briers
c2a1521c95 Version: 214.2.2 2024-08-11 19:32:36 +02:00
Pieter-Jan Briers
a26b48414b Compile compat fixes
(cherry picked from commit 025d90d281)
(cherry picked from commit 799702b814)
(cherry picked from commit 4600ee8e5788891f1b610e2d5141fb4e1228d323)
2024-08-11 19:32:36 +02:00
Pieter-Jan Briers
614a03036b Version: 214.2.1 2024-08-11 17:56:10 +02:00
Pieter-Jan Briers
8c8a3c0e17 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)
(cherry picked from commit db8ba83866c523e08e4fba0b80cd954f4f190613)
2024-08-11 17:56:10 +02:00
ElectroJr
b1f9d011ce Version: 214.2.0 2024-03-16 16:17:59 -04:00
Leon Friedrich
a2d0504368 Replace PVS dictionaries with memory magic (#4795)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-03-17 06:57:13 +11:00
metalgearsloth
7aa951ca48 Add undetachable PVS flag (#4889)
Useful in some rare cases, mainly for grid-related activities.
Specifically:
- Audio entity where we never want it detached.
- FTL previs effects to show impending squish.
2024-03-16 14:58:08 +11:00
metalgearsloth
75a80b7a8a Fix tooltips underflowing left side of screen (#4952)
* Fix tooltips underflowing left side of screen

If the tooltip is so large it would clip the right side then it would underflow completely off-screen. This just clamps it instead.

* Better

* rubb
2024-03-16 14:45:17 +11:00
metalgearsloth
69706b0257 Fix global audio (#4964)
* Fix global audio

* Better
2024-03-16 11:59:57 +11:00
Pieter-Jan Briers
10b191dff8 Version: 214.1.1 2024-03-16 01:13:47 +01:00
Pieter-Jan Briers
92ab3fb64b Fix connection denials always redialling
Bug caused by changes to connection denial.

Fixes #4963
2024-03-16 01:13:34 +01:00
metalgearsloth
92a0c14383 Version: 214.1.0 2024-03-15 20:20:20 +11:00
metalgearsloth
5aaf6d0994 Fix VV for entity prototypes (#4956)
* Fix VV for entity prototypes

* Fix ProtoId
2024-03-15 20:16:09 +11:00
metalgearsloth
15f4da5e4b Audio limit fix (#4962)
I screm. See https://github.com/space-wizards/RobustToolbox/issues/4961
2024-03-15 20:14:49 +11:00
Leon Friedrich
a528e87f3d Add pvs_override_info command (#4958) 2024-03-15 14:32:23 +11:00
Pieter-Jan Briers
4af67b1394 Version: 214.0.0 2024-03-14 20:42:17 +01:00
metalgearsloth
e8de9b98d3 Add basic audio limits (#4921)
* Add basic audio limits

* RN

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-03-14 11:54:43 +01:00
Pieter-Jan Briers
a0ffeff4e5 Release notes for last commit 2024-03-14 08:10:54 +01:00
Pieter-Jan Briers
07654564f3 TextEdit fixes
Fixed being able to position the cursor vertically if placeholder text was visible and multi-line. This is because the code was using line break info for the place holder. On top of not being correct behavior, this caused further exceptions since the cursor would get outside the editable text rope.

Fixed index exception if you try to move left in an empty text edit.

Has regression tests.

Fixes #4957, fixes #4953
2024-03-14 08:09:28 +01:00
Pieter-Jan Briers
7fbf8d05eb Add ability to add structured deny data to NetConnectingArgs. (#4487)
* Add ability to add structured deny data to NetConnectingArgs.

Builds on the (horrifying) NetStructuredDisconnectMessages so that content can do more stuff.

To be used by SS14 to throttle people when they try to connect to a full server.

* Completely rewrite NetStructuredDisconnectMessages

So this class was a mess, and it was so bad it wasn't usable from content! System.Text.Json isn't sandbox safe (and I don't want to look into that right now), so the previous API surface of "pass the JsonNode around everywhere" just didn't work at all for content.

I decided the easiest solution would be to completely rewrite the entire thing to be a layer over a Dictionary<string, object> instead. This warranted a complete rewrite of the API, which should be fine as I doubt anybody was using it anyways.

Also, fully tested.
2024-03-14 07:27:22 +01:00
ShadowCommander
c12971cb9b Add decimal variable to Range Control rounding (#4954)
* Add decimal variable to Range Control rounding

* Remove unnecessary virtual and add ViewVariables
2024-03-13 00:43:27 +01:00
metalgearsloth
2b6381c332 Version: 213.0.0 2024-03-11 14:36:45 +11:00
TemporalOroboros
8149a3aaad Removes Obsolete BaseContainer methods. (#4843) 2024-03-11 14:35:31 +11:00
Kot
4b39bf1f2d Check entity for existence before drawing it in the SpriteView (#4886)
* Check entity for existence before drawing it in the SpriteView

* Slightly refactor ResolveEntity to be more straightforward
2024-03-11 14:34:44 +11:00
metalgearsloth
53394fff44 Add GetEntitiesInRange for sets (#4951)
* Add GetEntitiesInRange for sets

Need it for an old method.

* rn

* Fix SO
2024-03-11 13:43:13 +11:00
metalgearsloth
4bed20e070 Add RaiseSharedEvent (#4950)
Used in some rare cases on content (popups + pickup prediction). I was too lazy to make system proxy methods because it's very infrequent.
2024-03-10 19:33:45 +01:00
metalgearsloth
e4b6af09f1 Version: 212.2.0 2024-03-11 02:08:08 +11:00
metalgearsloth
1ef29ae781 Add some physics helpers (#4946)
Thought I had these but couldn't find them.
2024-03-11 02:05:50 +11:00
Vasilis
5686950421 Increase replay compressed size (#4925)
They tend to get cut off (or well did on wizden before pjb changed it to this exact value bigger along with another patch). Before you got around 2ish hours in a replay before it stopped. I doupt most servers will reach 6ish hours before this takes effect. But those servers can increase this value of needed.
2024-03-08 12:24:41 +01:00
Pieter-Jan Briers
2b54aa8984 Version: 212.1.0 2024-03-07 21:29:29 +01:00
Pieter-Jan Briers
859f150404 YIPPEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE IMAGESHARP VULNERABILITY 2024-03-07 21:29:00 +01:00
metalgearsloth
558f4b5b16 ScrollContainer niceties (#4940)
- If scroll is not visible we don't handle it. This means nested containers don't interfere with their parents anymore.
- Fallback to Y-scrolling for H-scroll only containers.
2024-03-05 23:32:22 +01:00
metalgearsloth
108366152b Fix TextureRect KeepCentered (#4937)
Easiest way to repro is set a non-1.0 UIScale and open the main menu up, the logo will be fonky.
I checked the control dimensions and this aligned with my expectations.
2024-03-05 23:28:28 +01:00
I.K
c55327e1d1 Set a minimum of 0.05 for the light resolution (#4942) 2024-03-05 23:27:39 +01:00
metalgearsloth
370e0fa0d0 Add nullable versions for protomanager (#4938) 2024-03-05 15:24:01 +11:00
metalgearsloth
4f9f82c20c Version: 212.0.1 2024-03-03 19:42:48 +11:00
metalgearsloth
43670a8ddd Pass array by-ref (#4936)
Yeah idk how to fix this otherwise but using this would be nicer.
2024-03-03 19:41:29 +11:00
metalgearsloth
250313e1ed Version: 212.0.0 2024-03-03 18:34:22 +11:00
metalgearsloth
18d511d4b6 Minor fixes (#4935)
- Swear I pushed this array change
- Update changelog
2024-03-03 18:32:51 +11:00
metalgearsloth
da9e5fb370 Add grid tile to Vector2 methods (#4851)
* Add grid tile to Vector2 methods

Avoids me having to do it on content.

* Release note

* Engine

* Collapsible

* Add entitylookup methods for parent / map

Content's done it a bunch so make it reusable.

* Add MaxDimension property to Box2

Sometimes I want to pretend it's a circle radius.

* Add GetLocalPosition to controls

In my case I want the mouse's position inside of the control to show something under it unless there's a better way.

* Add global rectangles for controls

Like my other PR used to check if mouse is inbounds on the control without doing some skrunkly caching with mousemove.

* Add dotted line drawing to screen handle

Probably needs anti-aliasing but idk an easy way to do it.

* weh

* weh

* a

* weh

* weh

* Optimise ChunkEnumerator

It never unioned the AABB passed in with the grid's AABB so it might inadvertantly iterate a lot more dummy chunks than it needs to.

This helps speedup FindGridsIntersecting.

* weh

* Add DrawPrimitives overload for List<Vector2>

Storing ValueList in a field seems sussy so this is the next best thing.

* weh

* Bump pool size

* oop wrong method

* Add drawing methods for lists

Content may be using it over a valuelist for whatever reason.

* Add more ValueList conveniences

* Add more CollectionExtension methods

Maybe array.resize is bad for sandbox coin, in which case I'd also settle for changing it to a list instead.

* Add ToMapCoordinates method for NetCoordinates

* fr

* mraow

* Release notes
2024-03-03 18:29:35 +11:00
Tayrtahn
e3bac382ce Add some helper methods to PVS Filters (#4933) 2024-03-03 11:51:40 +11:00
DrSmugleaf
179c6790b6 Add support for automatically networking component dictionary fields with entity keys and values (#4932)
* Add support for automatically networking component dictionary fields with entity keys and values

* Fix using

* Fix order

* Add support for both key and value being entity uid
2024-03-03 11:51:23 +11:00
metalgearsloth
a7db5634df Add more CollectionExtension methods (#4910)
Maybe array.resize is bad for sandbox coin, in which case I'd also settle for changing it to a list instead.
2024-03-03 11:51:13 +11:00
deltanedas
2daa86ff59 add PushMarkup to FormattedMessage (#4924)
Co-authored-by: deltanedas <@deltanedas:kde.org>
2024-03-03 11:49:41 +11:00
metalgearsloth
d6803f5294 Add DrawPrimitives overload for List<Vector2> (#4900)
* Add DrawPrimitives overload for List<Vector2>

Storing ValueList in a field seems sussy so this is the next best thing.

* weh
2024-03-02 21:34:42 +01:00
metalgearsloth
bdcc0f7b9d Add more ValueList conveniences (#4911)
* Add more ValueList conveniences

* Review

* a
2024-03-02 22:15:18 +11:00
metalgearsloth
ce49aa47cf Add ToMapCoordinates method for NetCoordinates (#4914) 2024-03-02 21:47:05 +11:00
metalgearsloth
c7d48b2526 Remove ISerHooks obsoletion (#4928)
Still needed in rare cases so not really deprecated and we already discourage coders from using it where possible.
2024-02-28 19:01:08 +01:00
rene-descartes2021
6bb7f5b4ef Allow for use of new .NET 8 MSBuild property <UseArtifactsOutput> (#4929) 2024-02-28 19:00:51 +01:00
Brandon Hu
2974310450 chore: Remove some typos (#4927)
* chore: Remove some typos

* Apply suggestions from code review

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2024-02-27 12:31:29 +01:00
metalgearsloth
2694dce076 Version: 211.0.2 2024-02-25 14:14:45 +11:00
metalgearsloth
8960d1d995 Fix TextureRect scaling (#4923)
From moony my git patch didn't apply so done manually
2024-02-25 14:13:54 +11:00
metalgearsloth
0a4683d33e Version: 211.0.1 2024-02-23 19:32:16 +11:00
metalgearsloth
379bcfabe0 Fix Map Grid chunk enumerators (#4920)
They have empty AABBs so always returned early.
2024-02-23 19:31:20 +11:00
metalgearsloth
1d91838166 Version: 211.0.0 2024-02-23 18:01:07 +11:00
metalgearsloth
a5d4b8096f Move chunk enumerators to engine (#4901)
* Move chunk enumerators to engine

* notes

* Cleanup
2024-02-23 17:51:34 +11:00
Pieter-Jan Briers
a77eee5658 Fix async console command completions on ServerConsoleHost
Fixes #4828

Asynchronous console command completions were just not being run on the server, the wrong function was being called. Hooray.

This caused sudo to break because it actually uses an async command completion (as other command completions it invokes might in turn be async).
2024-02-23 00:22:58 +01:00
metalgearsloth
156187a0dd Optimise ChunkEnumerator (#4899)
* Optimise ChunkEnumerator

It never unioned the AABB passed in with the grid's AABB so it might inadvertantly iterate a lot more dummy chunks than it needs to.

This helps speedup FindGridsIntersecting.

* weh

* oop wrong method

* Update RELEASE-NOTES.md
2024-02-22 13:26:04 +11:00
metalgearsloth
852f002f59 Make collinear vertices check public (#4913) 2024-02-21 21:08:23 +11:00
metalgearsloth
9dc49c1904 Make physics constants public (#4912) 2024-02-20 22:02:28 -08:00
Moony
1995b13e5d Fix TextureRect. (#4908)
Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com>
2024-02-20 15:41:01 -08:00
Pieter-Jan Briers
f985d10ed9 In which I spend too much time SIMDizing PadVerticesV2 2024-02-20 12:27:20 +01:00
Pieter-Jan Briers
ae6cebbfbb Source gen reorganizations + component unpause generator. (#4896)
* Source gen reorganizations + component unpause generator.

This commit (and subsequent commits) aims to clean up our Roslyn plugin (source gens + analyzers) stack to more sanely re-use common code

I also built a new source-gen that automatically generates unpausing implementations for components, incrementing attributed TimeSpan field when unpaused.

* Fix warnings in all Roslyn projects
2024-02-20 10:15:32 +01:00
Pieter-Jan Briers
ef0bc1a2e4 Version: 210.1.1 2024-02-17 22:17:52 +01:00
Pieter-Jan Briers
72ba484f5b Changelog for key binding fix PRs 2024-02-17 22:12:33 +01:00
Pieter-Jan Briers
a70e511fcb Change default of ButtonGroup.IsNoneSetAllowed to true.
This brings default ButtonGroup behavior back to before #4841.

The original comments in the code *did* clearly intend for the other behavior to be the default, but the code was blatantly bugged (whoops) so this didn't happen. Content relied on this A LOT and it's quite sane behavior regardless so just change the default back call it a day.
2024-02-17 22:09:23 +01:00
Errant
e7f9e95525 fix default keybinds not knowing their place (#4903)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-02-17 22:03:36 +01:00
nikthechampiongr
bd908f9db6 Invalid keybinds will no longer mess up your game. (#4902)
* Fix issues when saving invalid keybinds.

* Fix horrible thing I forgot to fix.

* Change error log to debug

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2024-02-17 21:54:35 +01:00
118 changed files with 4686 additions and 610 deletions

View File

@@ -33,10 +33,10 @@ jobs:
mkdir "release/${{ steps.parse_version.outputs.version }}"
mv release/*.zip "release/${{ steps.parse_version.outputs.version }}"
- name: Upload files to centcomm
- name: Upload files to Suns
uses: appleboy/scp-action@master
with:
host: centcomm.spacestation14.io
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
source: "release/${{ steps.parse_version.outputs.version }}"
@@ -46,7 +46,7 @@ jobs:
- name: Update manifest JSON
uses: appleboy/ssh-action@master
with:
host: centcomm.spacestation14.io
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }}

View File

@@ -1,4 +1,14 @@
<Project>
<PropertyGroup>
<!--
We actually set ManagePackageVersionsCentrally manually in another import file.
Since .NET SDK 8.0.300, ManagePackageVersionsCentrally is automatically set if Directory.Packages.props exists.
https://github.com/NuGet/NuGet.Client/pull/5572
We actively negate this here, as we have some packages in tree we don't want such automatic behavior for.
We use Directory.Build.props to get copy the state *after* our MSBuild config but before Nuget's config.
-->
<ManagePackageVersionsCentrally />
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
@@ -45,17 +55,20 @@
<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.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.0" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="System.Numerics.Vectors" Version="4.5.0" />
<PackageVersion Include="System.Memory" Version="4.5.5" />
<PackageVersion Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.22621.5" />
<PackageVersion Include="TerraFX.Interop.Xlib" Version="6.4.0" />
<PackageVersion Include="VorbisPizza" Version="1.3.0" />
<PackageVersion Include="YamlDotNet" Version="13.7.1" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="PolySharp" Version="1.14.1" />
</ItemGroup>
</Project>

View File

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

View File

@@ -24,12 +24,16 @@
<RobustInjectorsConfiguration>$(Configuration)</RobustInjectorsConfiguration>
<RobustInjectorsConfiguration Condition="'$(Configuration)' == 'DebugOpt'">Debug</RobustInjectorsConfiguration>
<RobustInjectorsConfiguration Condition="'$(Configuration)' == 'Tools'">Release</RobustInjectorsConfiguration>
<RobustInjectorsConfiguration Condition="'$(UseArtifactsOutput)' == 'true' And '$(RuntimeIdentifier)' != ''">$(RobustInjectorsConfiguration)_$(RuntimeIdentifier)</RobustInjectorsConfiguration>
<RobustInjectorsConfiguration Condition="'$(UseArtifactsOutput)' == 'true'">$(RobustInjectorsConfiguration.ToLower())</RobustInjectorsConfiguration>
<CompileRobustXamlTaskAssemblyFile Condition="'$(UseArtifactsOutput)' != 'true'">$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\bin\$(RobustInjectorsConfiguration)\netstandard2.0\Robust.Client.Injectors.dll</CompileRobustXamlTaskAssemblyFile>
<CompileRobustXamlTaskAssemblyFile Condition="'$(UseArtifactsOutput)' == 'true'">$(MSBuildThisFileDirectory)\..\..\artifacts\bin\Robust.Client.Injectors\$(RobustInjectorsConfiguration)\Robust.Client.Injectors.dll</CompileRobustXamlTaskAssemblyFile>
</PropertyGroup>
<UsingTask
Condition="'$(_RobustUseExternalMSBuild)' != 'true' And $(DesignTimeBuild) != true"
TaskName="CompileRobustXamlTask"
AssemblyFile="$(MSBuildThisFileDirectory)\..\Robust.Client.Injectors\bin\$(RobustInjectorsConfiguration)\netstandard2.0\Robust.Client.Injectors.dll"/>
AssemblyFile="$(CompileRobustXamlTaskAssemblyFile)"/>
<Target
Name="CompileRobustXaml"
Condition="Exists('@(IntermediateAssembly)')"

View File

@@ -54,6 +54,180 @@ END TEMPLATE-->
*None yet*
## 214.2.3
## 214.2.2
## 214.2.1
## 214.2.0
### New features
* Added a `Undetachable` entity metadata flag, which stops the client from moving an entity to nullspace when it moves out of PVS range.
### Bugfixes
* Fix tooltips not clamping to the left side of the viewport.
* Fix global audio property not being properly set.
### Internal
* The server game state / PVS code has been rewritten. It should be somewhat faster now, albeit at the cost of using more memory. The current engine version may be unstable.
## 214.1.1
### Bugfixes
* Fixed connection denial always causing redial.
## 214.1.0
### New features
* Added the `pvs_override_info` command for debugging PVS overrides.
### Bugfixes
* Fix VV for prototype structs.
* Fix audio limits for clientside audio.
## 214.0.0
### Breaking changes
* `NetStructuredDisconnectMessages` has received a complete overhaul and has been moved to `NetDisconnectMessage`. The API is no longer designed such that consumers must pass around JSON nodes, as they are not in sandbox (and clunky).
### New features
* Add a basic default concurrent audio limit of 16 for a single filepath to avoid overflowing audio sources.
* `NetConnectingArgs.Deny()` can now pass along structured data that will be received by the client.
### Bugfixes
* Fixed cursor position bugs when an empty `TextEdit` has a multi-line place holder.
* Fixed empty `TextEdit` throwing exception if cursor is moved left.
## 213.0.0
### Breaking changes
* Remove obsoleted BaseContainer methods.
### New features
* Add EntityManager.RaiseSharedEvent where the event won't go to the attached client but will be predicted locally on their end.
* Add GetEntitiesInRange override that takes in EntityCoordinates and an EntityUid hashset.
### Bugfixes
* Check if a sprite entity is deleted before drawing in SpriteView.
## 212.2.0
### New features
* Add IsHardCollidable to SharedPhysicsSystem to determine if 2 entities would collide.
### Other
* Double the default maximum replay size.
## 212.1.0
### New features
* Add nullable methods for TryIndex / HasIndex on IPrototypeManager.
### Bugfixes
* Fix TextureRect alignment where the strech mode is KeepCentered.
## 212.0.1
### Bugfixes
* Fix passing array by `this` instead of by `ref`.
## 212.0.0
### Breaking changes
* Change Collapsible controls default orientations to Vertical.
### New features
* Expose the Label control for Collapsible controls.
* Add GetGridPosition that considers physics center-of-mass.
* Add TileToVector methods to get the LocalPosition of tile-coords (taking into account tile size).
* Add some more helper methods to PVS filters around EntityUids.
* Add support for Dictionary AutoNetworkedFields.
* Add EnsureLength method for arrays.
* Add PushMarkup to FormattedMessage.
* Add DrawPrimitives overload for `List<Vector2>`
* Add more ValueList ctors that are faster.
* Add ToMapCoordinates method for NetCoordinates.
### Other
* Remove ISerializationHooks obsoletion as they are useful in some rare cases.
### Internal
* Bump max pool size for robust jobs.
## 211.0.2
### Bugfixes
* Fix TextureRect scaling not handling UIScale correctly.
## 211.0.1
### Bugfixes
* Fix GridChunkEnumerator on maps.
## 211.0.0
### Breaking changes
* Moved ChunkIndicesEnumerator to engine and to a re-useable namespace at Robust.Shared/Maps.
### New features
* Added an Enlarged method for Box2Rotated.
### Internal
* Significantly optimise ChunkEnumerator / FindGridsIntersecting in certain use cases by intersecting the grid's AABB with the local AABB to avoid iterating dummy chunks.
## 210.1.1
### Bugfixes
* Fixed multiple recent bugs with key binding storage.
### Other
* Change default of `ButtonGroup.IsNoneSetAllowed` to `true`. This makes it default again to the previous (unintentional) behavior.
## 210.1.0
### New features

View File

@@ -490,7 +490,7 @@ cmd-net_entityreport-help = Usage: net_entityreport
cmd-net_refresh-desc = Requests a full server state.
cmd-net_refresh-help = Usage: net_refresh
cmd-net_graph-desc = Toggles the net statistics pannel.
cmd-net_graph-desc = Toggles the net statistics panel.
cmd-net_graph-help = Usage: net_graph
cmd-net_watchent-desc = Dumps all network updates for an EntityId to the console.
@@ -566,3 +566,9 @@ cmd-reloadtiletextures-help = Usage: reloadtiletextures
cmd-audio_length-desc = Shows the length of an audio file
cmd-audio_length-help = Usage: audio_length { cmd-audio_length-arg-file-name }
cmd-audio_length-arg-file-name = <file name>
## PVS
cmd-pvs-override-info-desc = Prints information about any PVS overrides associated with an entity.
cmd-pvs-override-info-empty = Entity {$nuid} has no PVS overrides.
cmd-pvs-override-info-global = Entity {$nuid} has a global override.
cmd-pvs-override-info-clients = Entity {$nuid} has a session override for {$clients}.

View File

@@ -70,9 +70,9 @@ command-description-ls-in =
command-description-methods-get =
Returns all methods associated with the input type.
command-description-methods-overrides =
Returns all methods overriden on the input type.
Returns all methods overridden on the input type.
command-description-methods-overridesfrom =
Returns all methods overriden from the given type on the input type.
Returns all methods overridden from the given type on the input type.
command-description-cmd-moo =
Asks the important questions.
command-description-cmd-descloc =
@@ -418,6 +418,6 @@ command-description-tee =
This essentially lets you have a branch in your code to do multiple operations on one value.
command-description-cmd-info =
Returns a CommandSpec for the given command.
On it's own, this means it'll print the comamnd's help message.
On its own, this means it'll print the command's help message.
command-description-comp-rm =
Removes the given component from the entity.

View File

@@ -0,0 +1,8 @@
// OH BOY. TURNS OUT IT GETS EVEN MORE CURSED.
//
// So because we're compiling a copy of Robust.Roslyn.Shared into every analyzer project,
// the test project sees multiple copies of it. This would make it impossible to use.
// UNLESS you use this obscure C# feature called "extern alias"
// that I guarantee you you've never heard of before, and are now concerned about.
extern alias SerializationGenerator;

View File

@@ -0,0 +1,340 @@
extern alias SerializationGenerator;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using NUnit.Framework;
using SerializationGenerator::Robust.Roslyn.Shared;
using SerializationGenerator::Robust.Serialization.Generator;
namespace Robust.Analyzers.Tests;
[TestFixture]
[TestOf(typeof(ComponentPauseGenerator))]
[Parallelizable(ParallelScope.All)]
public sealed class ComponentPauseGeneratorTest
{
private const string TypesCode = """
global using System;
global using Robust.Shared.Analyzers;
global using Robust.Shared.GameObjects;
namespace Robust.Shared.Analyzers
{
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class AutoGenerateComponentPauseAttribute : Attribute
{
public bool Dirty = false;
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class AutoPausedFieldAttribute : Attribute;
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class AutoNetworkedFieldAttribute : Attribute
{
}
}
namespace Robust.Shared.GameObjects
{
public interface IComponent;
}
""";
[Test]
public void TestBasic()
{
var result = RunGenerator("""
[AutoGenerateComponentPause]
public sealed partial class FooComponent : IComponent
{
[AutoPausedField]
public TimeSpan Foo;
}
""");
ExpectNoDiagnostics(result);
ExpectSource(
result,
"""
// <auto-generated />
using Robust.Shared.GameObjects;
public partial class FooComponent
{
[RobustAutoGenerated]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused);
}
private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
{
component.Foo += args.PausedTime;
}
}
}
""");
}
[Test]
public void TestNullable()
{
var result = RunGenerator("""
[AutoGenerateComponentPause]
public sealed partial class FooComponent : IComponent
{
[AutoPausedField]
public TimeSpan? Foo;
}
""");
ExpectNoDiagnostics(result);
ExpectSource(
result,
"""
// <auto-generated />
using Robust.Shared.GameObjects;
public partial class FooComponent
{
[RobustAutoGenerated]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused);
}
private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
{
if (component.Foo.HasValue)
component.Foo = component.Foo.Value + args.PausedTime;
}
}
}
""");
}
[Test]
public void TestAutoState()
{
var result = RunGenerator("""
[AutoGenerateComponentPause]
public sealed partial class FooComponent : IComponent
{
[AutoPausedField, AutoNetworkedField]
public TimeSpan Foo;
}
""");
ExpectNoDiagnostics(result);
ExpectSource(
result,
"""
// <auto-generated />
using Robust.Shared.GameObjects;
public partial class FooComponent
{
[RobustAutoGenerated]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused);
}
private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
{
component.Foo += args.PausedTime;
Dirty(uid, component);
}
}
}
""");
}
[Test]
public void TestExplicitDirty()
{
var result = RunGenerator("""
[AutoGenerateComponentPause(Dirty = true)]
public sealed partial class FooComponent : IComponent
{
[AutoPausedField]
public TimeSpan Foo;
}
""");
ExpectNoDiagnostics(result);
ExpectSource(
result,
"""
// <auto-generated />
using Robust.Shared.GameObjects;
public partial class FooComponent
{
[RobustAutoGenerated]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(OnEntityUnpaused);
}
private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
{
component.Foo += args.PausedTime;
Dirty(uid, component);
}
}
}
""");
}
[Test]
public void TestDiagnosticNotIComponent()
{
var result = RunGenerator("""
[AutoGenerateComponentPause]
public sealed partial class FooComponent
{
[AutoPausedField]
public TimeSpan Foo;
}
""");
ExpectNoSource(result);
ExpectDiagnostics(result, [
(Diagnostics.IdComponentPauseNotComponent, new LinePositionSpan(new LinePosition(1, 28), new LinePosition(1, 40)))
]);
}
[Test]
public void TestDiagnosticNoFields()
{
var result = RunGenerator("""
[AutoGenerateComponentPause]
public sealed partial class FooComponent : IComponent
{
public TimeSpan Foo;
}
""");
ExpectNoSource(result);
ExpectDiagnostics(result, [
(Diagnostics.IdComponentPauseNoFields, new LinePositionSpan(new LinePosition(1, 28), new LinePosition(1, 40)))
]);
}
[Test]
public void TestDiagnosticNoParentAttribute()
{
var result = RunGenerator("""
public sealed partial class FooComponent : IComponent
{
[AutoPausedField]
public TimeSpan Foo, Fooz;
[AutoPausedField]
public TimeSpan Bar { get; set; }
}
""");
ExpectNoSource(result);
ExpectDiagnostics(result, [
(Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(3, 20), new LinePosition(3, 23))),
(Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(3, 25), new LinePosition(3, 29))),
(Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(6, 20), new LinePosition(6, 23)))
]);
}
[Test]
public void TestDiagnosticWrongType()
{
var result = RunGenerator("""
[AutoGenerateComponentPause]
public sealed partial class FooComponent : IComponent
{
[AutoPausedField]
public int Foo, Fooz;
[AutoPausedField]
public int Bar { get; set; }
}
""");
ExpectNoSource(result);
ExpectDiagnostics(result, [
(Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(4, 15), new LinePosition(4, 18))),
(Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(4, 20), new LinePosition(4, 24))),
(Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(7, 15), new LinePosition(7, 18)))
]);
}
private static void ExpectSource(GeneratorRunResult result, string expected)
{
Assert.That(result.GeneratedSources, Has.Length.EqualTo(1));
var source = result.GeneratedSources[0];
Assert.That(source.SourceText.ToString(), Is.EqualTo(expected));
}
private static void ExpectNoSource(GeneratorRunResult result)
{
Assert.That(result.GeneratedSources, Is.Empty);
}
private static void ExpectNoDiagnostics(GeneratorRunResult result)
{
Assert.That(result.Diagnostics, Is.Empty);
}
private static void ExpectDiagnostics(GeneratorRunResult result, (string code, LinePositionSpan span)[] diagnostics)
{
Assert.Multiple(() =>
{
Assert.That(result.Diagnostics, Has.Length.EqualTo(diagnostics.Length));
foreach (var (code, span) in diagnostics)
{
Assert.That(
result.Diagnostics.Any(x => x.Id == code && x.Location.GetLineSpan().Span == span),
$"Expected diagnostic with code {code} and location {span}");
}
});
}
private static GeneratorRunResult RunGenerator(string source)
{
var compilation = (Compilation)CSharpCompilation.Create("compilation",
new[]
{
CSharpSyntaxTree.ParseText(source, path: "Source.cs"),
CSharpSyntaxTree.ParseText(TypesCode, path: "Types.cs")
},
new[] { MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) },
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var generator = new ComponentPauseGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out _);
var result = driver.GetRunResult();
return result.Results[0];
}
}

View File

@@ -1,4 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<SkipRobustAnalyzer>true</SkipRobustAnalyzer>
</PropertyGroup>
<Import Project="..\MSBuild\Robust.Properties.targets"/>
<Import Project="..\MSBuild\Robust.Engine.props"/>
@@ -23,5 +27,6 @@
<ItemGroup>
<ProjectReference Include="..\Robust.Analyzers\Robust.Analyzers.csproj"/>
<ProjectReference Include="..\Robust.Serialization.Generator\Robust.Serialization.Generator.csproj" Aliases="SerializationGenerator" />
</ItemGroup>
</Project>

View File

@@ -5,6 +5,7 @@ using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
using Robust.Shared.Analyzers.Implementation;
namespace Robust.Analyzers

View File

@@ -4,6 +4,7 @@ using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
using static Microsoft.CodeAnalysis.SymbolEqualityComparer;
namespace Robust.Analyzers;
@@ -16,7 +17,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor ByRefEventSubscribedByValueRule = new(
Diagnostics.IdByRefEventSubscribedByValue,
"By-ref event subscribed to by value",
"Tried to subscribe to a by-ref event '{0}' by value.",
"Tried to subscribe to a by-ref event '{0}' by value",
"Usage",
DiagnosticSeverity.Error,
true,
@@ -26,7 +27,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor ByRefEventRaisedByValueRule = new(
Diagnostics.IdByRefEventRaisedByValue,
"By-ref event raised by value",
"Tried to raise a by-ref event '{0}' by value.",
"Tried to raise a by-ref event '{0}' by value",
"Usage",
DiagnosticSeverity.Error,
true,
@@ -36,7 +37,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor ByValueEventRaisedByRefRule = new(
Diagnostics.IdValueEventRaisedByRef,
"Value event raised by-ref",
"Tried to raise a value event '{0}' by-ref.",
"Tried to raise a value event '{0}' by-ref",
"Usage",
DiagnosticSeverity.Error,
true,

View File

@@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
@@ -18,7 +19,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
Diagnostics.IdDataDefinitionPartial,
"Type must be partial",
"Type {0} is a DataDefinition but is not partial.",
"Type {0} is a DataDefinition but is not partial",
"Usage",
DiagnosticSeverity.Error,
true,
@@ -28,7 +29,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
Diagnostics.IdNestedDataDefinitionPartial,
"Type must be partial",
"Type {0} contains nested data definition {1} but is not partial.",
"Type {0} contains nested data definition {1} but is not partial",
"Usage",
DiagnosticSeverity.Error,
true,
@@ -38,7 +39,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor DataFieldWritableRule = new(
Diagnostics.IdDataFieldWritable,
"Data field must not be readonly",
"Data field {0} in data definition {1} is readonly.",
"Data field {0} in data definition {1} is readonly",
"Usage",
DiagnosticSeverity.Error,
true,
@@ -48,7 +49,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
Diagnostics.IdDataFieldPropertyWritable,
"Data field property must have a setter",
"Data field property {0} in data definition {1} does not have a setter.",
"Data field property {0} in data definition {1} does not have a setter",
"Usage",
DiagnosticSeverity.Error,
true,

View File

@@ -9,7 +9,7 @@ using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxKind;
using static Robust.Analyzers.Diagnostics;
using static Robust.Roslyn.Shared.Diagnostics;
namespace Robust.Analyzers;

View File

@@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
using Document = Microsoft.CodeAnalysis.Document;
namespace Robust.Analyzers

View File

@@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;

View File

@@ -2,6 +2,7 @@ using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers
{

View File

@@ -3,6 +3,7 @@ using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
@@ -31,7 +32,7 @@ public sealed class NotNullableFlagAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor InvalidNotNullableImplementationRule = new (
Diagnostics.IdInvalidNotNullableFlagImplementation,
"Invalid NotNullable flag implementation.",
"Invalid NotNullable flag implementation",
"NotNullable flag is either not typed as bool, or does not have a default value equaling false",
"Usage",
DiagnosticSeverity.Error,
@@ -41,7 +42,7 @@ public sealed class NotNullableFlagAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor InvalidNotNullableTypeRule = new (
Diagnostics.IdInvalidNotNullableFlagType,
"Failed to resolve type parameter",
"Failed to resolve type parameter \"{0}\".",
"Failed to resolve type parameter \"{0}\"",
"Usage",
DiagnosticSeverity.Error,
true,
@@ -49,7 +50,7 @@ public sealed class NotNullableFlagAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor NotNullableFlagValueTypeRule = new (
Diagnostics.IdNotNullableFlagValueType,
"NotNullable flag not supported for value types.",
"NotNullable flag not supported for value types",
"Value types as generic arguments are not supported for NotNullable flags",
"Usage",
DiagnosticSeverity.Error,

View File

@@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;

View File

@@ -1,17 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>10</LangVersion>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
</ItemGroup>
<ItemGroup>
<!-- Needed for NotNullableFlagAnalyzer. -->
<Compile Include="..\Robust.Shared\Analyzers\NotNullableFlagAttribute.cs" />
@@ -28,4 +16,10 @@
<Compile Include="..\Robust.Shared\Analyzers\PreferGenericVariantAttribute.cs" />
</ItemGroup>
<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />
<PropertyGroup>
<Nullable>disable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers
{

View File

@@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;

View File

@@ -1,19 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Compile Link="XamlX\filename" Include="../XamlX/src/XamlX/**/*.cs" />
<Compile Remove="../XamlX/src/XamlX/**/SreTypeSystem.cs" />
<Compile Remove="../XamlX/src/XamlX/obj/**" />
<Compile Include="..\Robust.Client\UserInterface\ControlPropertyAccess.cs" />
</ItemGroup>
<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />
<PropertyGroup>
<!-- XamlX doesn't do NRTs. -->
<Nullable>disable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Robust.Shared;
using Robust.Shared.GameObjects;
namespace Robust.Client.Audio;
public sealed partial class AudioSystem
{
/*
* Handles limiting concurrent sounds for audio to avoid blowing the source budget on one sound getting spammed.
*/
private readonly Dictionary<string, int> _playingCount = new();
private int _maxConcurrent;
private void InitializeLimit()
{
Subs.CVar(CfgManager, CVars.AudioDefaultConcurrent, SetConcurrentLimit, true);
}
private void SetConcurrentLimit(int obj)
{
_maxConcurrent = obj;
}
private bool TryAudioLimit(string sound)
{
if (string.IsNullOrEmpty(sound))
return true;
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_playingCount, sound, out _);
if (count >= _maxConcurrent)
return false;
count++;
return true;
}
private void RemoveAudioLimit(string sound)
{
if (!_playingCount.TryGetValue(sound, out var count))
return;
count--;
if (count <= 0)
{
_playingCount.Remove(sound);
return;
}
_playingCount[sound] = count;
}
}

View File

@@ -41,6 +41,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
[Dependency] private readonly IParallelManager _parMan = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
[Dependency] private readonly IAudioInternal _audio = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
@@ -51,6 +52,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
private EntityUid? _listenerGrid;
private UpdateAudioJob _updateAudioJob;
private EntityQuery<PhysicsComponent> _physicsQuery;
private float _maxRayLength;
@@ -108,6 +110,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
Subs.CVar(CfgManager, CVars.AudioAttenuation, OnAudioAttenuation, true);
Subs.CVar(CfgManager, CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
InitializeLimit();
}
private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAutoHandleStateEvent args)
@@ -163,26 +166,37 @@ public sealed partial class AudioSystem : SharedAudioSystem
return;
}
SetupSource(component, audioResource);
SetupSource((uid, component), audioResource);
component.Loaded = true;
}
private void SetupSource(AudioComponent component, AudioResource audioResource, TimeSpan? length = null)
private void SetupSource(Entity<AudioComponent> entity, AudioResource audioResource, TimeSpan? length = null)
{
var source = _audio.CreateAudioSource(audioResource);
if (source == null)
var component = entity.Comp;
if (TryAudioLimit(component.FileName))
{
Log.Error($"Error creating audio source for {audioResource}");
DebugTools.Assert(false);
source = component.Source;
var newSource = _audio.CreateAudioSource(audioResource);
if (newSource == null)
{
Log.Error($"Error creating audio source for {audioResource}");
DebugTools.Assert(false);
}
else
{
component.Source = newSource;
}
}
component.Source = source;
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
{
_metadata.SetFlag(entity.Owner, MetaDataFlags.Undetachable, true);
}
// Need to set all initial data for first frame.
ApplyAudioParams(component.Params, component);
source.Global = component.Global;
component.Source.Global = component.Global;
// Don't play until first frame so occlusion etc. are correct.
component.Gain = 0f;
@@ -202,6 +216,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
// Breaks with prediction?
component.Source.Dispose();
RemoveAudioLimit(component.FileName);
}
private void OnAudioAttenuation(int obj)
@@ -576,13 +592,13 @@ public sealed partial class AudioSystem : SharedAudioSystem
return PlayGlobal(filename, audioParams);
}
public override void LoadStream<T>(AudioComponent component, T stream)
public override void LoadStream<T>(Entity<AudioComponent> entity, T stream)
{
if (stream is AudioStream audioStream)
{
TryGetAudio(audioStream, out var audio);
SetupSource(component, audio!, audioStream.Length);
component.Loaded = true;
SetupSource(entity, audio!, audioStream.Length);
entity.Comp.Loaded = true;
}
}
@@ -621,7 +637,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
var audioP = audioParams ?? AudioParams.Default;
var entity = EntityManager.CreateEntityUninitialized("Audio", MapCoordinates.Nullspace);
var comp = SetupAudio(entity, null, audioP, stream.Length);
LoadStream(comp, stream);
LoadStream((entity, comp), stream);
EntityManager.InitializeAndStartEntity(entity);
var source = comp.Source;

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

@@ -165,7 +165,7 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
{
var textEdit = new TextEdit
{
Placeholder = new Rope.Leaf("You deleted the lipsum OwO")
Placeholder = new Rope.Leaf("You deleted the lipsum\nOwO")
};
TabContainer.SetTabTitle(textEdit, "TextEdit");

View File

@@ -8,6 +8,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Replays;
using Robust.Shared.Utility;
@@ -134,6 +135,24 @@ namespace Robust.Client.GameObjects
EventBus.RaiseEvent(EventSource.Local, new EntitySessionMessage<T>(eventArgs, msg));
}
/// <inheritdoc />
public override void RaiseSharedEvent<T>(T message, EntityUid? user = null)
{
if (user == null || user != _playerManager.LocalEntity || !_gameTiming.IsFirstTimePredicted)
return;
EventBus.RaiseEvent(EventSource.Local, ref message);
}
/// <inheritdoc />
public override void RaiseSharedEvent<T>(T message, ICommonSession? user = null)
{
if (user == null || user != _playerManager.LocalSession || !_gameTiming.IsFirstTimePredicted)
return;
EventBus.RaiseEvent(EventSource.Local, ref message);
}
#region IEntityNetworkManager impl
public override IEntityNetworkManager EntityNetManager => this;

View File

@@ -1120,7 +1120,7 @@ namespace Robust.Client.GameStates
continue;
}
if ((meta.Flags & MetaDataFlags.Detached) != 0)
if ((meta.Flags & (MetaDataFlags.Detached | MetaDataFlags.Undetachable)) != 0)
continue;
if (lastStateApplied.HasValue)

View File

@@ -1201,7 +1201,7 @@ namespace Robust.Client.Graphics.Clyde
private void LightResolutionScaleChanged(float newValue)
{
_lightResolutionScale = newValue;
_lightResolutionScale = newValue > 0.05f ? newValue : 0.05f;
RegenAllLightRts();
}

View File

@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.Graphics;
using System.Runtime.Intrinsics;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
@@ -63,6 +64,19 @@ namespace Robust.Client.Graphics
// ---- DrawPrimitives: Vector2 API ----
/// <summary>
/// Draws arbitrary geometry primitives with a flat color.
/// </summary>
/// <param name="primitiveTopology">The topology of the primitives to draw.</param>
/// <param name="vertices">The list of vertices to render.</param>
/// <param name="color">The color to draw with.</param>
public void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, List<Vector2> vertices,
Color color)
{
var span = CollectionsMarshal.AsSpan(vertices);
DrawPrimitives(primitiveTopology, span, color);
}
/// <summary>
/// Draws arbitrary geometry primitives with a flat color.
/// </summary>
@@ -100,12 +114,43 @@ namespace Robust.Client.Graphics
DrawPrimitives(primitiveTopology, White, indices, drawVertices);
}
private void PadVerticesV2(ReadOnlySpan<Vector2> input, Span<DrawVertexUV2DColor> output, Color color)
private static void PadVerticesV2(ReadOnlySpan<Vector2> input, Span<DrawVertexUV2DColor> output, Color color)
{
Color colorLinear = Color.FromSrgb(color);
for (var i = 0; i < output.Length; i++)
if (input.Length == 0)
return;
if (input.Length != output.Length)
{
output[i] = new DrawVertexUV2DColor(input[i], new Vector2(0.5f, 0.5f), colorLinear);
throw new InvalidOperationException("Invalid lengths!");
}
var colorLinear = Color.FromSrgb(color);
var colorVec = Unsafe.As<Color, Vector128<float>>(ref colorLinear);
var uvVec = Vector128.Create(0, 0, 0.5f, 0.5f);
var maskVec = Vector128.Create(0xFFFFFFFF, 0xFFFFFFFF, 0, 0).AsSingle();
var simdVectors = (nuint)(input.Length / 2);
ref readonly var srcBase = ref Unsafe.As<Vector2, float>(ref Unsafe.AsRef(in input[0]));
ref var dstBase = ref Unsafe.As<DrawVertexUV2DColor, float>(ref output[0]);
for (nuint i = 0; i < simdVectors; i++)
{
var positions = Vector128.LoadUnsafe(in srcBase, i * 4);
var posColorLower = (positions & maskVec) | uvVec;
var posColorUpper = (Vector128.Shuffle(positions, Vector128.Create(2, 3, 0, 0)) & maskVec) | uvVec;
posColorLower.StoreUnsafe(ref dstBase, i * 16);
colorVec.StoreUnsafe(ref dstBase, i * 16 + 4);
posColorUpper.StoreUnsafe(ref dstBase, i * 16 + 8);
colorVec.StoreUnsafe(ref dstBase, i * 16 + 12);
}
var lastPos = (int)simdVectors * 2;
if (lastPos != output.Length)
{
// Odd number of vertices. Handle the last manually.
output[lastPos] = new DrawVertexUV2DColor(input[lastPos], new Vector2(0.5f, 0.5f), colorLinear);
}
}

View File

@@ -40,7 +40,7 @@ namespace Robust.Client.Input
void KeyDown(KeyEventArgs e);
void KeyUp(KeyEventArgs e);
IKeyBinding RegisterBinding(in KeyBindingRegistration reg, bool markModified=true);
IKeyBinding RegisterBinding(in KeyBindingRegistration reg, bool markModified=true, bool invalid=false);
void RemoveBinding(IKeyBinding binding, bool markModified=true);

View File

@@ -523,13 +523,16 @@ namespace Robust.Client.Input
{
var baseKeyRegs = _serialization.Read<KeyBindingRegistration[]>(BaseKeyRegsNode, notNullableOverride: true);
foreach (var reg in baseKeyRegs)
{
var invalid = false;
if (reg.Type != KeyBindingType.Command && !NetworkBindMap.FunctionExists(reg.Function.FunctionName))
{
Logger.ErrorS("input", "Key function in {0} does not exist: '{1}'", file,
Logger.DebugS("input", "Key function in {0} does not exist: '{1}'.", file,
reg.Function);
continue;
invalid = true;
}
if (defaultRegistration)
@@ -544,7 +547,7 @@ namespace Robust.Client.Input
}
}
RegisterBinding(reg, markModified: defaultRegistration);
RegisterBinding(reg, markModified: !defaultRegistration, invalid);
}
}
@@ -552,11 +555,16 @@ namespace Robust.Client.Input
{
var leaveEmpty = _serialization.Read<BoundKeyFunction[]>(node, notNullableOverride: true);
if (leaveEmpty.Length > 0)
foreach (var bind in leaveEmpty)
{
// Adding to _modifiedKeyFunctions means that these keybinds won't be loaded from the base file.
// Because they've been explicitly cleared.
_modifiedKeyFunctions.UnionWith(leaveEmpty);
_modifiedKeyFunctions.Add(bind);
// Adding to bindingsByFunction because if the keybind is not valid(For example if it's from another
// server then we will have problems saving the file)
_bindingsByFunction.GetOrNew(bind);
}
}
}
@@ -584,7 +592,7 @@ namespace Robust.Client.Input
return binding;
}
public IKeyBinding RegisterBinding(in KeyBindingRegistration reg, bool markModified = true)
public IKeyBinding RegisterBinding(in KeyBindingRegistration reg, bool markModified = true, bool invalid = false)
{
var binding = new KeyBinding(this, reg.Function.FunctionName, reg.Type, reg.BaseKey, reg.CanFocus, reg.CanRepeat,
reg.AllowSubCombs, reg.Priority, reg.Mod1, reg.Mod2, reg.Mod3);
@@ -615,7 +623,7 @@ namespace Robust.Client.Input
public void InputModeChanged() => OnInputModeChanged?.Invoke();
private void RegisterBinding(KeyBinding binding, bool markModified = true)
private void RegisterBinding(KeyBinding binding, bool markModified = true, bool invalid = false)
{
// we sort larger combos first so they take priority over smaller (single key) combos,
// so they get processed first in KeyDown and such.
@@ -630,7 +638,8 @@ namespace Robust.Client.Input
_modifiedKeyFunctions.Add(binding.Function);
}
_bindings.Insert(pos, binding);
if (!invalid)
_bindings.Insert(pos, binding);
_bindingsByFunction.GetOrNew(binding.Function).Add(binding);
OnKeyBindingAdded?.Invoke(binding);
}

View File

@@ -468,7 +468,7 @@ namespace Robust.Client.UserInterface.Controls
/// Create a new <see cref="ButtonGroup"/>
/// </summary>
/// <param name="isNoneSetAllowed">The value of <see cref="IsNoneSetAllowed"/> on the new button group.</param>
public ButtonGroup(bool isNoneSetAllowed = false)
public ButtonGroup(bool isNoneSetAllowed = true)
{
IsNoneSetAllowed = isNoneSetAllowed;
}

View File

@@ -29,9 +29,11 @@ namespace Robust.Client.UserInterface.Controls
}
public Collapsible()
{}
{
Orientation = LayoutOrientation.Vertical;
}
public Collapsible(CollapsibleHeading header, CollapsibleBody body)
public Collapsible(CollapsibleHeading header, CollapsibleBody body) : this()
{
AddChild(header);
AddChild(body);
@@ -39,12 +41,9 @@ namespace Robust.Client.UserInterface.Controls
Initialize();
}
public Collapsible(string title, CollapsibleBody body)
public Collapsible(string title, CollapsibleBody body) : this(new CollapsibleHeading(title), body)
{
AddChild(new CollapsibleHeading(title));
AddChild(body);
Initialize();
}
protected internal override void Draw(DrawingHandleScreen handle)
@@ -105,11 +104,15 @@ namespace Robust.Client.UserInterface.Controls
set => _chevron.Margin = value;
}
private Label _title = new();
/// <summary>
/// Exposes the label for this heading.
/// </summary>
public Label Label { get; }
public string? Title
{
get => _title.Text;
set => _title.Text = value;
get => Label.Text;
set => Label.Text = value;
}
public CollapsibleHeading()
@@ -118,8 +121,8 @@ namespace Robust.Client.UserInterface.Controls
var box = new BoxContainer();
AddChild(box);
box.AddChild(_chevron);
_title = new Label();
box.AddChild(_title);
Label = new Label();
box.AddChild(Label);
}
public CollapsibleHeading(string title) : this()

View File

@@ -13,6 +13,7 @@ namespace Robust.Client.UserInterface.Controls
private float _value;
private float _page;
private bool _rounded;
private int _roundingDecimals = 0;
public event Action<Range>? OnValueChanged;
@@ -86,6 +87,17 @@ namespace Robust.Client.UserInterface.Controls
}
}
[ViewVariables]
public int RoundingDecimals
{
get => _roundingDecimals;
set
{
_roundingDecimals = value;
_ensureValueClamped();
}
}
public virtual void SetValueWithoutEvent(float newValue)
{
newValue = ClampValue(newValue);
@@ -107,7 +119,7 @@ namespace Robust.Client.UserInterface.Controls
{
if (_rounded)
{
value = MathF.Round(value);
value = MathF.Round(value, _roundingDecimals);
}
return MathHelper.Clamp(value, _minValue, _maxValue-_page);
}

View File

@@ -20,6 +20,11 @@ namespace Robust.Client.UserInterface.Controls
private bool _suppressScrollValueChanged;
/// <summary>
/// If true then if we have a y-axis scroll it will convert it to an x-axis scroll.
/// </summary>
public bool FallbackDeltaScroll { get; set; } = true;
public int ScrollSpeedX { get; set; } = 50;
public int ScrollSpeedY { get; set; } = 50;
@@ -246,9 +251,19 @@ namespace Robust.Client.UserInterface.Controls
if (_hScrollEnabled)
{
_hScrollBar.ValueTarget += args.Delta.X * ScrollSpeedX;
var delta =
args.Delta.X == 0f &&
!_vScrollEnabled &&
FallbackDeltaScroll ?
-args.Delta.Y :
args.Delta.X;
_hScrollBar.ValueTarget += delta * ScrollSpeedX;
}
if (!_vScrollVisible && !_hScrollVisible)
return;
args.Handle();
}

View File

@@ -16,7 +16,7 @@ namespace Robust.Client.UserInterface.Controls
{
private SpriteSystem? _sprite;
private SharedTransformSystem? _transform;
IEntityManager _entMan;
private readonly IEntityManager _entMan;
[ViewVariables]
public SpriteComponent? Sprite => Entity?.Comp1;
@@ -143,6 +143,8 @@ namespace Robust.Client.UserInterface.Controls
if (netEnt == NetEnt)
return;
// The Entity is getting set later in the ResolveEntity method
// because the client may not have received it yet.
Entity = null;
NetEnt = netEnt;
}
@@ -256,28 +258,19 @@ 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))
SetEntity(ent);
if (Entity != null)
{
(uid, sprite, xform) = Entity.Value;
return true;
return !_entMan.Deleted(uid);
}
sprite = null;
xform = null;
uid = default;
if (NetEnt == null)
return false;
if (!_entMan.TryGetEntity(NetEnt, out var ent))
return false;
SetEntity(ent);
if (Entity == null)
return false;
(uid, sprite, xform) = Entity.Value;
return true;
return false;
}
}
}

View File

@@ -576,7 +576,7 @@ public sealed class TextEdit : Control
var newPos = CursorShiftedLeft();
// Explicit newlines work kinda funny with bias, so keep it at top there.
var bias = Rope.Index(TextRope, newPos) == '\n'
var bias = _cursorPosition.Index == TextLength || Rope.Index(TextRope, newPos) == '\n'
? LineBreakBias.Top
: LineBreakBias.Bottom;
@@ -940,6 +940,13 @@ public sealed class TextEdit : Control
private CursorPos GetIndexAtHorizontalPos(int line, float horizontalPos)
{
// If the placeholder is visible, this function does not return correct results because it looks at TextRope,
// but _lineBreaks is configured for the display rope.
// Bail out early in this case, the function is not currently used in any situation in any location
// where something else is desired if the placeholder is visible.
if (IsPlaceholderVisible)
return default;
var contentBox = PixelSizeBox;
var font = GetFont();
var uiScale = UIScale;

View File

@@ -17,6 +17,9 @@ namespace Robust.Client.UserInterface.Controls
{
public const string StylePropertyTexture = "texture";
public const string StylePropertyShader = "shader";
public const string StylePropertyTextureStretch = "texture-stretch";
public const string StylePropertyTextureScale = "texture-scale";
public const string StylePropertyTextureSizeTarget = "texture-size-target";
private bool _canShrink;
private Texture? _texture;
@@ -29,7 +32,18 @@ namespace Robust.Client.UserInterface.Controls
/// </summary>
public Texture? Texture
{
get => _texture;
get
{
if (_texture is null)
{
if (TryGetStyleProperty(StylePropertyTexture, out Texture? texture))
{
return texture;
}
}
return _texture;
}
set
{
var oldSize = _texture?.Size;
@@ -43,6 +57,7 @@ namespace Robust.Client.UserInterface.Controls
}
private string? _texturePath;
private StretchMode _stretch = StretchMode.Keep;
public string TexturePath
{
@@ -54,21 +69,45 @@ namespace Robust.Client.UserInterface.Controls
}
protected override void StylePropertiesChanged()
{
base.StylePropertiesChanged();
InvalidateMeasure();
}
protected override void OnThemeUpdated()
{
if (_texturePath != null) Texture = Theme.ResolveTexture(_texturePath);
base.OnThemeUpdated();
}
public Vector2 TextureSizeTarget
{
get
{
if (!TryGetStyleProperty(StylePropertyTextureSizeTarget, out Vector2 target))
target = _textureScale * Texture?.Size ?? Vector2.Zero;
return target;
}
}
/// <summary>
/// Scales the texture displayed.
/// </summary>
/// <remarks>
/// This does not apply to the following stretch modes: <see cref="StretchMode.Scale"/>.
/// This additionally does not apply if a size target is set.
/// </remarks>
public Vector2 TextureScale
{
get => _textureScale;
get
{
if (!TryGetStyleProperty(StylePropertyTextureScale, out Vector2 scale))
scale = _textureScale;
return scale;
}
set
{
_textureScale = value;
@@ -96,23 +135,27 @@ namespace Robust.Client.UserInterface.Controls
/// <summary>
/// Controls how the texture should be drawn if the control is larger than the size of the texture.
/// </summary>
public StretchMode Stretch { get; set; } = StretchMode.Keep;
public StretchMode Stretch
{
get
{
if (!TryGetStyleProperty(StylePropertyTextureStretch, out StretchMode stretch))
stretch = _stretch;
return stretch;
}
set => _stretch = value;
}
protected internal override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
var texture = _texture;
ShaderInstance? shader = null;
var texture = Texture;
if (texture == null)
{
TryGetStyleProperty(StylePropertyTexture, out texture);
if (texture == null)
{
return;
}
}
if (texture is null)
return;
ShaderInstance? shader = null;
if (ShaderOverride != null)
{
@@ -167,17 +210,17 @@ namespace Robust.Client.UserInterface.Controls
case StretchMode.Tile:
// TODO: Implement Tile.
case StretchMode.Keep:
return UIBox2.FromDimensions(Vector2.Zero, texture.Size * _textureScale * UIScale);
return UIBox2.FromDimensions(Vector2.Zero, TextureSizeTarget * UIScale);
case StretchMode.KeepCentered:
{
var position = (PixelSize - texture.Size * _textureScale * UIScale) / 2;
return UIBox2.FromDimensions(position, texture.Size * _textureScale * UIScale);
var position = (Size - TextureSizeTarget) / 2;
return UIBox2.FromDimensions(position, TextureSizeTarget * UIScale);
}
case StretchMode.KeepAspect:
case StretchMode.KeepAspectCentered:
{
var (texWidth, texHeight) = texture.Size * _textureScale;
var (texWidth, texHeight) = TextureSizeTarget;
var width = texWidth * (PixelSize.Y / texHeight);
var height = (float)PixelSize.Y;
if (width > PixelSize.X)
@@ -197,7 +240,7 @@ namespace Robust.Client.UserInterface.Controls
}
case StretchMode.KeepAspectCovered:
var texSize = texture.Size * _textureScale;
var texSize = TextureSizeTarget;
// Calculate the scale necessary to fit width and height to control size.
var (scaleX, scaleY) = PixelSize / texSize;
// Use whichever scale is greater.
@@ -259,19 +302,10 @@ namespace Robust.Client.UserInterface.Controls
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var texture = _texture;
if (texture == null)
{
TryGetStyleProperty(StylePropertyTexture, out texture);
}
if (texture == null || CanShrink)
{
if (CanShrink || Texture == null)
return Vector2.Zero;
}
return texture.Size * TextureScale;
return TextureSizeTarget;
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using System;
using System.Numerics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
@@ -41,20 +42,16 @@ namespace Robust.Client.UserInterface
tooltip.Measure(Vector2Helpers.Infinity);
var combinedMinSize = tooltip.DesiredSize;
LayoutContainer.SetPosition(tooltip, new Vector2(screenPosition.X, screenPosition.Y - combinedMinSize.Y));
// If it overflows right bounds then just place left on the edge.
var right = MathF.Min(screenPosition.X + combinedMinSize.X, screenBounds.X);
var right = tooltip.Position.X + combinedMinSize.X;
var top = tooltip.Position.Y;
// However, better to clamp the end of the tooltip instead of the start.
var left = MathF.Max(0f, right - combinedMinSize.X);
if (right > screenBounds.X)
{
LayoutContainer.SetPosition(tooltip, new(screenPosition.X - combinedMinSize.X, tooltip.Position.Y));
}
var bottom = MathF.Min(screenPosition.Y, screenBounds.Y);
var top = MathF.Max(0f, bottom - combinedMinSize.Y);
if (top < 0f)
{
LayoutContainer.SetPosition(tooltip, new(tooltip.Position.X, 0f));
}
LayoutContainer.SetPosition(tooltip, new Vector2(left, top));
}
}
}

View File

@@ -126,6 +126,22 @@ namespace Robust.Client.ViewVariables
return new VVPropEditorString();
}
if (type == typeof(EntProtoId) ||
type == typeof(EntProtoId?))
{
return new VVPropEditorEntProtoId();
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ProtoId<>))
{
var editor =
(VVPropEditor)Activator.CreateInstance(
typeof(VVPropEditorProtoId<>).MakeGenericType(type.GenericTypeArguments[0]))!;
IoCManager.InjectDependencies(editor);
return editor;
}
if (typeof(IPrototype).IsAssignableFrom(type) || typeof(ViewVariablesBlobMembers.PrototypeReferenceToken).IsAssignableFrom(type))
{
return (VVPropEditor)Activator.CreateInstance(typeof(VVPropEditorIPrototype<>).MakeGenericType(type))!;

View File

@@ -0,0 +1,28 @@
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
namespace Robust.Client.ViewVariables.Editors;
internal sealed class VVPropEditorEntProtoId : VVPropEditor
{
protected override Control MakeUI(object? value)
{
var lineEdit = new LineEdit
{
Text = (EntProtoId) (value ?? ""),
Editable = !ReadOnly,
HorizontalExpand = true,
};
if (!ReadOnly)
{
lineEdit.OnTextEntered += e =>
{
ValueChanged((EntProtoId) e.Text);
};
}
return lineEdit;
}
}

View File

@@ -0,0 +1,38 @@
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
namespace Robust.Client.ViewVariables.Editors;
internal sealed class VVPropEditorProtoId<T> : VVPropEditor where T : class, IPrototype
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
protected override Control MakeUI(object? value)
{
var lineEdit = new LineEdit
{
Text = (ProtoId<T>) (value ?? ""),
Editable = !ReadOnly,
HorizontalExpand = true,
};
if (!ReadOnly)
{
lineEdit.OnTextEntered += e =>
{
var id = (ProtoId<T>)e.Text;
if (!_protoManager.HasIndex(id))
{
return;
}
ValueChanged(id);
};
}
return lineEdit;
}
}

View File

@@ -0,0 +1,46 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
namespace Robust.Roslyn.Shared;
#nullable enable
public static class AttributeHelper
{
public static bool HasAttribute(ISymbol symbol, string attributeMetadataName, [NotNullWhen(true)] out AttributeData? matchedAttribute)
{
foreach (var attribute in symbol.GetAttributes())
{
if (attribute.AttributeClass == null)
continue;
if (TypeSymbolHelper.ShittyTypeMatch(attribute.AttributeClass, attributeMetadataName))
{
matchedAttribute = attribute;
return true;
}
}
matchedAttribute = null;
return false;
}
public static bool GetNamedArgumentBool(AttributeData data, string name, bool defaultValue)
{
foreach (var kv in data.NamedArguments)
{
if (kv.Key != name)
continue;
if (kv.Value.Kind != TypedConstantKind.Primitive)
continue;
if (kv.Value.Value is not bool value)
continue;
return value;
}
return defaultValue;
}
}

View File

@@ -1,6 +1,6 @@
using Microsoft.CodeAnalysis;
namespace Robust.Analyzers;
namespace Robust.Roslyn.Shared;
public static class Diagnostics
{
@@ -24,6 +24,10 @@ public static class Diagnostics
public const string IdNestedDataDefinitionPartial = "RA0018";
public const string IdDataFieldWritable = "RA0019";
public const string IdDataFieldPropertyWritable = "RA0020";
public const string IdComponentPauseNotComponent = "RA0021";
public const string IdComponentPauseNoFields = "RA0022";
public const string IdComponentPauseNoParentAttribute = "RA0023";
public const string IdComponentPauseWrongTypeAttribute = "RA0024";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");

View File

@@ -0,0 +1,201 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// Taken from https://github.com/CommunityToolkit/dotnet/blob/ecd1711b740f4f88d2bb943ce292ae4fc90df1bc/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/EquatableArray%7BT%7D.cs
using System.Collections;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace Robust.Roslyn.Shared.Helpers;
#nullable enable
/// <summary>
/// Extensions for <see cref="EquatableArray{T}"/>.
/// </summary>
public static class EquatableArray
{
/// <summary>
/// Creates an <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.
/// </summary>
/// <typeparam name="T">The type of items in the input array.</typeparam>
/// <param name="array">The input <see cref="ImmutableArray{T}"/> instance.</param>
/// <returns>An <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.</returns>
public static EquatableArray<T> AsEquatableArray<T>(this ImmutableArray<T> array)
where T : IEquatable<T>
{
return new(array);
}
}
/// <summary>
/// An immutable, equatable array. This is equivalent to <see cref="ImmutableArray{T}"/> but with value equality support.
/// </summary>
/// <typeparam name="T">The type of values in the array.</typeparam>
public readonly struct EquatableArray<T> : IEquatable<EquatableArray<T>>, IEnumerable<T>
where T : IEquatable<T>
{
/// <summary>
/// The underlying <typeparamref name="T"/> array.
/// </summary>
private readonly T[]? array;
/// <summary>
/// Creates a new <see cref="EquatableArray{T}"/> instance.
/// </summary>
/// <param name="array">The input <see cref="ImmutableArray{T}"/> to wrap.</param>
public EquatableArray(ImmutableArray<T> array)
{
this.array = Unsafe.As<ImmutableArray<T>, T[]?>(ref array);
}
/// <summary>
/// Gets a reference to an item at a specified position within the array.
/// </summary>
/// <param name="index">The index of the item to retrieve a reference to.</param>
/// <returns>A reference to an item at a specified position within the array.</returns>
public ref readonly T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref AsImmutableArray().ItemRef(index);
}
/// <summary>
/// Gets a value indicating whether the current array is empty.
/// </summary>
public bool IsEmpty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => AsImmutableArray().IsEmpty;
}
/// <sinheritdoc/>
public bool Equals(EquatableArray<T> array)
{
return AsSpan().SequenceEqual(array.AsSpan());
}
/// <sinheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is EquatableArray<T> array && Equals(this, array);
}
/// <sinheritdoc/>
public override int GetHashCode()
{
if (this.array is not T[] array)
{
return 0;
}
HashCode hashCode = default;
foreach (T item in array)
{
hashCode.Add(item);
}
return hashCode.ToHashCode();
}
/// <summary>
/// Gets an <see cref="ImmutableArray{T}"/> instance from the current <see cref="EquatableArray{T}"/>.
/// </summary>
/// <returns>The <see cref="ImmutableArray{T}"/> from the current <see cref="EquatableArray{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ImmutableArray<T> AsImmutableArray()
{
return Unsafe.As<T[]?, ImmutableArray<T>>(ref Unsafe.AsRef(in this.array));
}
/// <summary>
/// Creates an <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.
/// </summary>
/// <param name="array">The input <see cref="ImmutableArray{T}"/> instance.</param>
/// <returns>An <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.</returns>
public static EquatableArray<T> FromImmutableArray(ImmutableArray<T> array)
{
return new(array);
}
/// <summary>
/// Returns a <see cref="ReadOnlySpan{T}"/> wrapping the current items.
/// </summary>
/// <returns>A <see cref="ReadOnlySpan{T}"/> wrapping the current items.</returns>
public ReadOnlySpan<T> AsSpan()
{
return AsImmutableArray().AsSpan();
}
/// <summary>
/// Copies the contents of this <see cref="EquatableArray{T}"/> instance to a mutable array.
/// </summary>
/// <returns>The newly instantiated array.</returns>
public T[] ToArray()
{
return AsImmutableArray().ToArray();
}
/// <summary>
/// Gets an <see cref="ImmutableArray{T}.Enumerator"/> value to traverse items in the current array.
/// </summary>
/// <returns>An <see cref="ImmutableArray{T}.Enumerator"/> value to traverse items in the current array.</returns>
public ImmutableArray<T>.Enumerator GetEnumerator()
{
return AsImmutableArray().GetEnumerator();
}
/// <sinheritdoc/>
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return ((IEnumerable<T>)AsImmutableArray()).GetEnumerator();
}
/// <sinheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)AsImmutableArray()).GetEnumerator();
}
/// <summary>
/// Implicitly converts an <see cref="ImmutableArray{T}"/> to <see cref="EquatableArray{T}"/>.
/// </summary>
/// <returns>An <see cref="EquatableArray{T}"/> instance from a given <see cref="ImmutableArray{T}"/>.</returns>
public static implicit operator EquatableArray<T>(ImmutableArray<T> array)
{
return FromImmutableArray(array);
}
/// <summary>
/// Implicitly converts an <see cref="EquatableArray{T}"/> to <see cref="ImmutableArray{T}"/>.
/// </summary>
/// <returns>An <see cref="ImmutableArray{T}"/> instance from a given <see cref="EquatableArray{T}"/>.</returns>
public static implicit operator ImmutableArray<T>(EquatableArray<T> array)
{
return array.AsImmutableArray();
}
/// <summary>
/// Checks whether two <see cref="EquatableArray{T}"/> values are the same.
/// </summary>
/// <param name="left">The first <see cref="EquatableArray{T}"/> value.</param>
/// <param name="right">The second <see cref="EquatableArray{T}"/> value.</param>
/// <returns>Whether <paramref name="left"/> and <paramref name="right"/> are equal.</returns>
public static bool operator ==(EquatableArray<T> left, EquatableArray<T> right)
{
return left.Equals(right);
}
/// <summary>
/// Checks whether two <see cref="EquatableArray{T}"/> values are not the same.
/// </summary>
/// <param name="left">The first <see cref="EquatableArray{T}"/> value.</param>
/// <param name="right">The second <see cref="EquatableArray{T}"/> value.</param>
/// <returns>Whether <paramref name="left"/> and <paramref name="right"/> are not equal.</returns>
public static bool operator !=(EquatableArray<T> left, EquatableArray<T> right)
{
return !left.Equals(right);
}
}

View File

@@ -0,0 +1,190 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// Taken from https://raw.githubusercontent.com/CommunityToolkit/dotnet/ecd1711b740f4f88d2bb943ce292ae4fc90df1bc/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/HashCode.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
#pragma warning disable CS0809
namespace System;
#nullable enable
/// <summary>
/// A polyfill type that mirrors some methods from <see cref="HashCode"/> on .NET 6.
/// </summary>
public struct HashCode
{
private const uint Prime1 = 2654435761U;
private const uint Prime2 = 2246822519U;
private const uint Prime3 = 3266489917U;
private const uint Prime4 = 668265263U;
private const uint Prime5 = 374761393U;
private static readonly uint seed = GenerateGlobalSeed();
private uint v1, v2, v3, v4;
private uint queue1, queue2, queue3;
private uint length;
/// <summary>
/// Initializes the default seed.
/// </summary>
/// <returns>A random seed.</returns>
private static unsafe uint GenerateGlobalSeed()
{
byte[] bytes = new byte[4];
using (RandomNumberGenerator generator = RandomNumberGenerator.Create())
{
generator.GetBytes(bytes);
}
return BitConverter.ToUInt32(bytes, 0);
}
/// <summary>
/// Adds a single value to the current hash.
/// </summary>
/// <typeparam name="T">The type of the value to add into the hash code.</typeparam>
/// <param name="value">The value to add into the hash code.</param>
public void Add<T>(T value)
{
Add(value?.GetHashCode() ?? 0);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4)
{
v1 = seed + Prime1 + Prime2;
v2 = seed + Prime2;
v3 = seed;
v4 = seed - Prime1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint Round(uint hash, uint input)
{
return RotateLeft(hash + input * Prime2, 13) * Prime1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint QueueRound(uint hash, uint queuedValue)
{
return RotateLeft(hash + queuedValue * Prime3, 17) * Prime4;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint MixState(uint v1, uint v2, uint v3, uint v4)
{
return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint MixEmptyState()
{
return seed + Prime5;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint MixFinal(uint hash)
{
hash ^= hash >> 15;
hash *= Prime2;
hash ^= hash >> 13;
hash *= Prime3;
hash ^= hash >> 16;
return hash;
}
private void Add(int value)
{
uint val = (uint)value;
uint previousLength = this.length++;
uint position = previousLength % 4;
if (position == 0)
{
this.queue1 = val;
}
else if (position == 1)
{
this.queue2 = val;
}
else if (position == 2)
{
this.queue3 = val;
}
else
{
if (previousLength == 3)
{
Initialize(out this.v1, out this.v2, out this.v3, out this.v4);
}
this.v1 = Round(this.v1, this.queue1);
this.v2 = Round(this.v2, this.queue2);
this.v3 = Round(this.v3, this.queue3);
this.v4 = Round(this.v4, val);
}
}
/// <summary>
/// Gets the resulting hashcode from the current instance.
/// </summary>
/// <returns>The resulting hashcode from the current instance.</returns>
public int ToHashCode()
{
uint length = this.length;
uint position = length % 4;
uint hash = length < 4 ? MixEmptyState() : MixState(this.v1, this.v2, this.v3, this.v4);
hash += length * 4;
if (position > 0)
{
hash = QueueRound(hash, this.queue1);
if (position > 1)
{
hash = QueueRound(hash, this.queue2);
if (position > 2)
{
hash = QueueRound(hash, this.queue3);
}
}
}
hash = MixFinal(hash);
return (int)hash;
}
/// <inheritdoc/>
[Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => throw new NotSupportedException();
/// <inheritdoc/>
[Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => throw new NotSupportedException();
/// <summary>
/// Rotates the specified value left by the specified number of bits.
/// Similar in behavior to the x86 instruction ROL.
/// </summary>
/// <param name="value">The value to rotate.</param>
/// <param name="offset">The number of bits to rotate by.
/// Any value outside the range [0..31] is treated as congruent mod 32.</param>
/// <returns>The rotated value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint RotateLeft(uint value, int offset)
{
return (value << offset) | (value >> (32 - offset));
}
}

View File

@@ -0,0 +1,121 @@
using System.Collections.Immutable;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Robust.Roslyn.Shared.Helpers;
namespace Robust.Roslyn.Shared;
#nullable enable
/// <summary>
/// All the information to make a partial type alternative for a type.
/// </summary>
public sealed record PartialTypeInfo(
string? Namespace,
string Name,
string DisplayName,
EquatableArray<string> TypeParameterNames,
bool IsValid,
Location SyntaxLocation,
Accessibility Accessibility,
TypeKind Kind,
bool IsRecord,
bool IsAbstract)
{
public static PartialTypeInfo FromSymbol(INamedTypeSymbol symbol, TypeDeclarationSyntax syntax)
{
var typeParameters = ImmutableArray<string>.Empty;
if (symbol.TypeParameters.Length > 0)
{
var builder = ImmutableArray.CreateBuilder<string>(symbol.TypeParameters.Length);
foreach (var typeParameter in symbol.TypeParameters)
{
builder.Add(typeParameter.Name);
}
typeParameters = builder.MoveToImmutable();
}
return new PartialTypeInfo(
symbol.ContainingNamespace.IsGlobalNamespace ? null : symbol.ContainingNamespace.ToDisplayString(),
symbol.Name,
symbol.ToDisplayString(),
typeParameters,
syntax.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword)),
syntax.Keyword.GetLocation(),
symbol.DeclaredAccessibility,
symbol.TypeKind,
symbol.IsRecord,
symbol.IsAbstract);
}
public bool CheckPartialDiagnostic(SourceProductionContext context, DiagnosticDescriptor diagnostic)
{
if (!IsValid)
{
context.ReportDiagnostic(Diagnostic.Create(diagnostic, SyntaxLocation, DisplayName));
return true;
}
return false;
}
public string GetGeneratedFileName()
{
var name = Namespace == null ? Name : $"{Namespace}.{Name}";
if (TypeParameterNames.AsImmutableArray().Length > 0)
name += $"`{TypeParameterNames.AsImmutableArray().Length}";
name += ".g.cs";
return name;
}
public void WriteHeader(StringBuilder builder)
{
if (Namespace != null)
builder.AppendLine($"namespace {Namespace};\n");
// TODO: Nested classes
var access = Accessibility switch
{
Accessibility.Private => "private",
Accessibility.ProtectedAndInternal => "private protected",
Accessibility.ProtectedOrInternal => "protected internal",
Accessibility.Protected => "protected",
Accessibility.Internal => "internal",
_ => "public"
};
string keyword;
if (Kind == TypeKind.Interface)
{
keyword = "interface";
}
else
{
if (IsRecord)
{
keyword = Kind == TypeKind.Struct ? "record struct" : "record";
}
else
{
keyword = Kind == TypeKind.Struct ? "struct" : "class";
}
}
builder.Append($"{access} {(IsAbstract ? "abstract " : "")}partial {keyword} {Name}");
if (TypeParameterNames.AsSpan().Length > 0)
{
builder.Append($"<{string.Join(", ", TypeParameterNames.AsImmutableArray())}>");
}
}
public void WriteFooter(StringBuilder builder)
{
// TODO: Nested classes
}
}

View File

@@ -0,0 +1,38 @@
<Project>
<!--
I wanted to make a Robust.Roslyn.Shared library project,
but doing that causes various random library load failures in practice.
Instead, you'll get this vomit. Enjoy.
-->
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>enable</ImplicitUsings>
<PolySharpIncludeGeneratedTypes>System.Index;System.Diagnostics.CodeAnalysis.NotNullWhenAttribute;System.Runtime.CompilerServices.IsExternalInit;System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute</PolySharpIncludeGeneratedTypes>
<NoWarn>RS2008</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<PackageReference Include="PolySharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Robust.Roslyn.Shared\**\*.cs">
<Link>Robust.Roslyn.Shared\%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>
<Compile Remove="..\Robust.Roslyn.Shared\obj\**\*.cs" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,28 @@
using Microsoft.CodeAnalysis;
namespace Robust.Roslyn.Shared;
#nullable enable
public static class TypeSymbolHelper
{
public static bool ShittyTypeMatch(INamedTypeSymbol type, string attributeMetadataName)
{
// Doing it like this only allocates when the type actually matches, which is good enough for me right now.
if (!attributeMetadataName.EndsWith(type.Name))
return false;
return type.ToDisplayString() == attributeMetadataName;
}
public static bool ImplementsInterface(INamedTypeSymbol type, string interfaceTypeName)
{
foreach (var interfaceType in type.AllInterfaces)
{
if (ShittyTypeMatch(interfaceType, interfaceTypeName))
return true;
}
return false;
}
}

View File

@@ -0,0 +1,252 @@
using System.Collections.Immutable;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Robust.Roslyn.Shared;
using Robust.Roslyn.Shared.Helpers;
namespace Robust.Serialization.Generator;
/// <summary>
/// Automatically generates implementations for handling timer unpausing.
/// </summary>
[Generator(LanguageNames.CSharp)]
public sealed class ComponentPauseGenerator : IIncrementalGenerator
{
private const string AutoGenerateComponentPauseAttributeName = "Robust.Shared.Analyzers.AutoGenerateComponentPauseAttribute";
private const string AutoPausedFieldAttributeName = "Robust.Shared.Analyzers.AutoPausedFieldAttribute";
private const string AutoNetworkFieldAttributeName = "Robust.Shared.Analyzers.AutoNetworkedFieldAttribute";
// ReSharper disable once InconsistentNaming
private const string IComponentTypeName = "Robust.Shared.GameObjects.IComponent";
private static readonly DiagnosticDescriptor NotComponentDiagnostic = new(
Diagnostics.IdComponentPauseNotComponent,
"Class must be an IComponent to use AutoGenerateComponentPause",
"Class '{0}' must implement IComponent to be used with [AutoGenerateComponentPause]",
"Usage",
DiagnosticSeverity.Error,
true);
private static readonly DiagnosticDescriptor NoFieldsDiagnostic = new(
Diagnostics.IdComponentPauseNoFields,
"AutoGenerateComponentPause has no fields",
"Class '{0}' has [AutoGenerateComponentPause] but has no fields or properties with [AutoPausedField]",
"Usage",
DiagnosticSeverity.Warning,
true);
private static readonly DiagnosticDescriptor NoParentAttributeDiagnostic = new(
Diagnostics.IdComponentPauseNoParentAttribute,
"AutoPausedField on type of field without AutoGenerateComponentPause",
"Field '{0}' has [AutoPausedField] but its containing type does not have [AutoGenerateComponentPause]",
"Usage",
DiagnosticSeverity.Error,
true);
private static readonly DiagnosticDescriptor WrongTypeAttributeDiagnostic = new(
Diagnostics.IdComponentPauseWrongTypeAttribute,
"AutoPausedField has wrong type",
"Field '{0}' has [AutoPausedField] but is not of type TimeSpan",
"Usage",
DiagnosticSeverity.Error,
true);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var componentInfos = context.SyntaxProvider.ForAttributeWithMetadataName(
AutoGenerateComponentPauseAttributeName,
(syntaxNode, _) => syntaxNode is TypeDeclarationSyntax,
(syntaxContext, _) =>
{
var symbol = (INamedTypeSymbol)syntaxContext.TargetSymbol;
var typeDeclarationSyntax = (TypeDeclarationSyntax) syntaxContext.TargetNode;
var partialTypeInfo = PartialTypeInfo.FromSymbol(
symbol,
typeDeclarationSyntax);
var dirty = AttributeHelper.GetNamedArgumentBool(syntaxContext.Attributes[0], "Dirty", false);
var fieldBuilder = ImmutableArray.CreateBuilder<FieldInfo>();
foreach (var member in symbol.GetMembers())
{
if (!AttributeHelper.HasAttribute(member, AutoPausedFieldAttributeName, out var _))
continue;
var type = member switch
{
IPropertySymbol property => property.Type,
IFieldSymbol field => field.Type,
_ => null
};
if (type is not INamedTypeSymbol namedType)
continue;
var invalid = false;
var nullable = false;
if (namedType.Name != "TimeSpan")
{
if (namedType is { Name: "Nullable", TypeArguments: [{Name: "TimeSpan"}] })
{
nullable = true;
}
else
{
invalid = true;
}
}
// If any pause field has [AutoNetworkedField], automatically mark it to dirty on unpause.
if (AttributeHelper.HasAttribute(member, AutoNetworkFieldAttributeName, out var _))
dirty = true;
fieldBuilder.Add(new FieldInfo(member.Name, nullable, invalid, member.Locations[0]));
}
return new ComponentInfo(
partialTypeInfo,
EquatableArray<FieldInfo>.FromImmutableArray(fieldBuilder.ToImmutable()),
dirty,
!TypeSymbolHelper.ImplementsInterface(symbol, IComponentTypeName),
typeDeclarationSyntax.Identifier.GetLocation());
});
context.RegisterImplementationSourceOutput(componentInfos, static (productionContext, info) =>
{
if (info.NotComponent)
{
productionContext.ReportDiagnostic(Diagnostic.Create(
NotComponentDiagnostic,
info.Location,
info.PartialTypeInfo.Name));
return;
}
// Component always have to be partial anyways due to the serialization generator.
// So I can't be arsed to define a diagnostic for this.
if (!info.PartialTypeInfo.IsValid)
return;
if (info.Fields.AsImmutableArray().Length == 0)
{
productionContext.ReportDiagnostic(Diagnostic.Create(
NoFieldsDiagnostic,
info.Location,
info.PartialTypeInfo.Name));
return;
}
var builder = new StringBuilder();
builder.AppendLine("""
// <auto-generated />
using Robust.Shared.GameObjects;
""");
info.PartialTypeInfo.WriteHeader(builder);
builder.AppendLine();
builder.AppendLine("{");
builder.AppendLine($$"""
[RobustAutoGenerated]
public sealed class {{info.PartialTypeInfo.Name}}_AutoPauseSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<{{info.PartialTypeInfo.Name}}, EntityUnpausedEvent>(OnEntityUnpaused);
}
private void OnEntityUnpaused(EntityUid uid, {{info.PartialTypeInfo.Name}} component, ref EntityUnpausedEvent args)
{
""");
var anyValidField = false;
foreach (var field in info.Fields)
{
if (field.Invalid)
{
productionContext.ReportDiagnostic(Diagnostic.Create(WrongTypeAttributeDiagnostic, field.Location));
continue;
}
if (field.Nullable)
{
builder.AppendLine($"""
if (component.{field.Name}.HasValue)
component.{field.Name} = component.{field.Name}.Value + args.PausedTime;
""");
}
else
{
builder.AppendLine($" component.{field.Name} += args.PausedTime;");
}
anyValidField = true;
}
if (!anyValidField)
return;
if (info.Dirty)
builder.AppendLine(" Dirty(uid, component);");
builder.AppendLine("""
}
}
""");
builder.AppendLine("}");
info.PartialTypeInfo.WriteFooter(builder);
productionContext.AddSource(info.PartialTypeInfo.GetGeneratedFileName(), builder.ToString());
});
// Code to report diagnostic for fields that have it but don't have the attribute on the parent.
var allFields = context.SyntaxProvider.ForAttributeWithMetadataName(
AutoPausedFieldAttributeName,
(syntaxNode, _) => syntaxNode is VariableDeclaratorSyntax or PropertyDeclarationSyntax,
(syntaxContext, _) =>
{
var errorTarget = syntaxContext.TargetNode is PropertyDeclarationSyntax prop
? prop.Identifier.GetLocation()
: syntaxContext.TargetNode.GetLocation();
return new AllFieldInfo(
syntaxContext.TargetSymbol.Name,
syntaxContext.TargetSymbol.ContainingType.ToDisplayString(),
errorTarget);
});
var allComponentsTogether = componentInfos.Collect();
var allFieldsTogether = allFields.Collect();
var componentFieldJoin = allFieldsTogether.Combine(allComponentsTogether);
context.RegisterImplementationSourceOutput(componentFieldJoin, (productionContext, info) =>
{
var componentsByName = new HashSet<string>(info.Right.Select(x => x.PartialTypeInfo.DisplayName));
foreach (var field in info.Left)
{
if (!componentsByName.Contains(field.ParentDisplayName))
{
productionContext.ReportDiagnostic(
Diagnostic.Create(NoParentAttributeDiagnostic, field.Location, field.Name));
}
}
});
}
public sealed record ComponentInfo(
PartialTypeInfo PartialTypeInfo,
EquatableArray<FieldInfo> Fields,
bool Dirty,
bool NotComponent,
Location Location);
public sealed record FieldInfo(string Name, bool Nullable, bool Invalid, Location Location);
public sealed record AllFieldInfo(string Name, string ParentDisplayName, Location Location);
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"Generators": {
"commandName": "DebugRoslynComponent",
"targetProject": "../../Content.Shared/Content.Shared.csproj"
}
}
}

View File

@@ -1,16 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>11</LangVersion>
<Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
</ItemGroup>
<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />
</Project>

View File

@@ -238,7 +238,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
}
public override void LoadStream<T>(AudioComponent component, T stream)
public override void LoadStream<T>(Entity<AudioComponent> entity, T stream)
{
// TODO: Yeah remove this...
}
}

View File

@@ -653,8 +653,8 @@ namespace Robust.Server
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
// shut down networking, kicking all players.
var shutdownReasonWithRedial = NetStructuredDisconnectMessages.Encode($"Server shutting down: {_shutdownReason}", true);
_network.Shutdown(shutdownReasonWithRedial);
var shutdownReasonWithRedial = new NetDisconnectMessage($"Server shutting down: {_shutdownReason}", true);
_network.Shutdown(shutdownReasonWithRedial.Encode());
// shutdown entities
_entityManager.Cleanup();

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Robust.Server.Player;
using Robust.Shared.Console;
@@ -283,25 +284,25 @@ namespace Robust.Server.Console
/// Get completions. Non-null results imply that the command was handled. If it is empty, it implies that
/// there are no completions for this command.
/// </summary>
private ValueTask<CompletionResult?> CalcCompletions(IConsoleShell shell, string[] args, string argStr)
private async ValueTask<CompletionResult?> CalcCompletions(IConsoleShell shell, string[] args, string argStr)
{
// Logger.Debug(string.Join(", ", args));
if (args.Length <= 1)
{
// Typing out command name, handle this ourselves.
return ValueTask.FromResult<CompletionResult?>(CompletionResult.FromOptions(
AvailableCommands.Values.Where(c => ShellCanExecute(shell, c.Command)).Select(c => new CompletionOption(c.Command, c.Description))));
return CompletionResult.FromOptions(
AvailableCommands.Values.Where(c => ShellCanExecute(shell, c.Command)).Select(c => new CompletionOption(c.Command, c.Description)));
}
var cmdName = args[0];
if (!RegisteredCommands.TryGetValue(cmdName, out var cmd))
return ValueTask.FromResult<CompletionResult?>(null);
return null;
if (!ShellCanExecute(shell, cmdName))
return ValueTask.FromResult<CompletionResult?>(CompletionResult.Empty);
return CompletionResult.Empty;
return ValueTask.FromResult<CompletionResult?>(cmd.GetCompletion(shell, args[1..]));
return await cmd.GetCompletionAsync(shell, args[1..], argStr, CancellationToken.None);
}
private sealed class SudoShell : IConsoleShell

View File

@@ -1,4 +1,5 @@
using System;
using Robust.Server.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables;
@@ -7,6 +8,7 @@ namespace Robust.Server.GameObjects
{
public sealed class VisibilitySystem : EntitySystem
{
[Dependency] private readonly PvsSystem _pvs = default!;
[Dependency] private readonly IViewVariablesManager _vvManager = default!;
private EntityQuery<TransformComponent> _xformQuery;
@@ -133,6 +135,7 @@ namespace Robust.Server.GameObjects
var xform = _xformQuery.GetComponent(uid);
meta.VisibilityMask = mask;
_pvs.SyncMetadata(meta);
foreach (var child in xform._children)
{

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Prometheus;
using Robust.Server.GameStates;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
@@ -42,7 +43,7 @@ namespace Robust.Server.GameObjects
#endif
private ISawmill _netEntSawmill = default!;
private EntityQuery<ActorComponent> _actorQuery;
private PvsSystem _pvs = default!;
public override void Initialize()
{
@@ -57,7 +58,7 @@ namespace Robust.Server.GameObjects
public override void Startup()
{
base.Startup();
_actorQuery = GetEntityQuery<ActorComponent>();
_pvs = System<PvsSystem>();
}
EntityUid IServerEntityManagerInternal.AllocEntity(EntityPrototype? prototype)
@@ -103,6 +104,40 @@ namespace Robust.Server.GameObjects
return entity;
}
/// <inheritdoc />
public override void RaiseSharedEvent<T>(T message, EntityUid? user = null)
{
if (user != null)
{
var filter = Filter.Broadcast().RemoveWhereAttachedEntity(e => e == user.Value);
foreach (var session in filter.Recipients)
{
EntityNetManager.SendSystemNetworkMessage(message, session.Channel);
}
}
else
{
EntityNetManager.SendSystemNetworkMessage(message);
}
}
/// <inheritdoc />
public override void RaiseSharedEvent<T>(T message, ICommonSession? user = null)
{
if (user != null)
{
var filter = Filter.Broadcast().RemovePlayer(user);
foreach (var session in filter.Recipients)
{
EntityNetManager.SendSystemNetworkMessage(message, session.Channel);
}
}
else
{
EntityNetManager.SendSystemNetworkMessage(message);
}
}
private void ClearTicks(EntityUid entity, EntityPrototype prototype)
{
foreach (var (netId, component) in GetNetComponents(entity))
@@ -116,6 +151,12 @@ namespace Robust.Server.GameObjects
}
}
internal override void SetLifeStage(MetaDataComponent meta, EntityLifeStage stage)
{
base.SetLifeStage(meta, stage);
_pvs.SyncMetadata(meta);
}
#region IEntityNetworkManager impl
public override IEntityNetworkManager EntityNetManager => this;

View File

@@ -1,46 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Robust.Shared.Maths;
namespace Robust.Server.GameStates;
public struct ChunkIndicesEnumerator
{
private Vector2i _bottomLeft;
private Vector2i _topRight;
private int _x;
private int _y;
public ChunkIndicesEnumerator(Vector2 viewPos, float range, float chunkSize)
{
var rangeVec = new Vector2(range, range);
_bottomLeft = ((viewPos - rangeVec) / chunkSize).Floored();
// Also floor this as we get the whole chunk anyway.
_topRight = ((viewPos + rangeVec) / chunkSize).Floored();
_x = _bottomLeft.X;
_y = _bottomLeft.Y;
}
public bool MoveNext([NotNullWhen(true)] out Vector2i? chunkIndices)
{
if (_y > _topRight.Y)
{
_x++;
_y = _bottomLeft.Y;
}
if (_x > _topRight.X)
{
chunkIndices = null;
return false;
}
chunkIndices = new Vector2i(_x, _y);
_y++;
return true;
}
}

View File

@@ -42,7 +42,7 @@ internal sealed class PvsChunk
/// <remarks>
/// This already includes <see cref="Map"/>, <see cref="Root"/>, and <see cref="Children"/>
/// </remarks>
public readonly List<Entity<MetaDataComponent>> Contents = new();
public readonly List<ChunkEntity> Contents = new();
/// <summary>
/// The unique location identifier for this chunk.
@@ -68,8 +68,8 @@ internal sealed class PvsChunk
// the same chunk can be repopulated more than once.
private List<HashSet<EntityUid>> _childSets = new();
private List<HashSet<EntityUid>> _nextChildSets = new();
private List<Entity<MetaDataComponent>> _lowPriorityChildren = new();
private List<Entity<MetaDataComponent>> _anchoredChildren = new();
private List<ChunkEntity> _lowPriorityChildren = new();
private List<ChunkEntity> _anchoredChildren = new();
/// <summary>
/// Effective "counts" of <see cref="Contents"/> that should be used to limit the number of entities in a chunk that
@@ -151,11 +151,11 @@ internal sealed class PvsChunk
childMeta.LastPvsLocation = Location;
if ((childMeta.Flags & MetaDataFlags.PvsPriority) == MetaDataFlags.PvsPriority)
Contents.Add((child, childMeta));
Contents.Add(new ChunkEntity(child, childMeta));
else if (childXform.Anchored)
_anchoredChildren.Add((child, childMeta));
_anchoredChildren.Add(new(child, childMeta));
else
_lowPriorityChildren.Add((child, childMeta));
_lowPriorityChildren.Add(new(child, childMeta));
var subCount = childXform._children.Count;
if (subCount == 0)
@@ -196,7 +196,7 @@ internal sealed class PvsChunk
}
childMeta.LastPvsLocation = Location;
Contents.Add((child, childMeta));
Contents.Add(new(child, childMeta));
var subCount = childXform._children.Count;
if (subCount == 0)
@@ -241,10 +241,10 @@ internal sealed class PvsChunk
set.Add(Map.Owner);
foreach (var child in Contents)
{
var parent = query.GetComponent(child).ParentUid;
var parent = query.GetComponent(child.Uid).ParentUid;
DebugTools.Assert(set.Contains(parent),
"A child's parent is not in the chunk, or is not listed first.");
DebugTools.Assert(set.Add(child), "Child appears more than once in the chunk.");
DebugTools.Assert(set.Add(child.Uid), "Child appears more than once in the chunk.");
}
}
@@ -274,4 +274,11 @@ internal sealed class PvsChunk
? $"map-{Root.Owner}-{Location.Indices}"
: $"grid-{Root.Owner}-{Location.Indices}";
}
public readonly struct ChunkEntity(EntityUid uid, MetaDataComponent meta)
{
public readonly EntityUid Uid = uid;
public readonly PvsIndex Ptr = meta.PvsData;
public readonly MetaDataComponent Meta = meta;
}
}

View File

@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -14,26 +15,24 @@ namespace Robust.Server.GameStates;
/// <summary>
/// Class for storing session specific PVS data.
/// </summary>
internal sealed class PvsSession(ICommonSession session)
internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion<PvsData> memoryRegion)
{
public readonly ICommonSession Session = session;
public readonly ResizableMemoryRegion<PvsData> DataMemory = memoryRegion;
public INetChannel Channel => Session.Channel;
/// <summary>
/// All <see cref="EntityUid"/>s that this session saw during the last <see cref="PvsSystem.DirtyBufferSize"/> ticks.
/// All entities that this session saw during the last <see cref="PvsSystem.DirtyBufferSize"/> ticks.
/// </summary>
public readonly OverflowDictionary<GameTick, List<PvsData>> PreviouslySent = new(PvsSystem.DirtyBufferSize);
/// <summary>
/// Dictionary containing data about all entities that this client has ever seen.
/// </summary>
public readonly Dictionary<NetEntity, PvsData> Entities = new();
public readonly OverflowDictionary<GameTick, List<PvsIndex>> PreviouslySent = new(PvsSystem.DirtyBufferSize);
/// <summary>
/// <see cref="PreviouslySent"/> overflow in case a player's last ack is more than
/// <see cref="PvsSystem.DirtyBufferSize"/> ticks behind the current tick.
/// </summary>
public (GameTick Tick, List<PvsData> SentEnts)? Overflow;
public (GameTick Tick, List<PvsIndex> SentEnts)? Overflow;
/// <summary>
/// The client's current visibility mask.
@@ -43,13 +42,13 @@ internal sealed class PvsSession(ICommonSession session)
/// <summary>
/// The list that is currently being prepared for sending.
/// </summary>
public List<PvsData>? ToSend;
public List<PvsIndex>? ToSend;
/// <summary>
/// The <see cref="ToSend"/> list from the previous tick. Also caches the current tick that the PVS leave message
/// should belong to, in case the processing is ever run asynchronously with normal system/game ticking.
/// </summary>
public (GameTick ToTick, List<PvsData> PreviouslySent)? LastSent;
public (GameTick ToTick, List<PvsIndex> PreviouslySent)? LastSent;
/// <summary>
/// Visible chunks, sorted by proximity to the clients's viewers;
@@ -126,10 +125,12 @@ internal sealed class PvsSession(ICommonSession session)
/// <summary>
/// Class for storing session-specific information about when an entity was last sent to a player.
/// </summary>
internal sealed class PvsData(NetEntity entity) : IEquatable<PvsData>
/// <remarks>
/// Size is padded to 16 bytes so
/// </remarks>
[StructLayout(LayoutKind.Sequential, Size = 16)]
internal struct PvsData
{
public readonly NetEntity NetEntity = entity;
/// <summary>
/// Tick at which this entity was last sent to a player.
/// </summary>
@@ -146,22 +147,55 @@ internal sealed class PvsData(NetEntity entity) : IEquatable<PvsData>
/// present in that state.
/// </summary>
public GameTick EntityLastAcked;
}
public bool Equals(PvsData? other)
{
DebugTools.Assert((NetEntity != other?.NetEntity) || ReferenceEquals(this, other));
return NetEntity == other?.NetEntity;
}
/// <summary>
/// Specialized struct with the same size as <see cref="PvsData"/> that is used to store metadata in the pinned PVsData array
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 16)]
internal struct PvsMetadata
{
/// <summary>
/// Tick at which this entity was last sent to a player.
/// </summary>
public NetEntity NetEntity;
public override int GetHashCode()
public GameTick LastModifiedTick;
public ushort VisMask;
public EntityLifeStage LifeStage;
#if DEBUG
// This struct is padded to a size of 16 so it's aligned to cache boundaries nicely.
// We have this extra space that isn't being used,
// so I'm opting to use them to make debugging the free list easier.
// "Marker" overlaps with the field used by the free list (which occupies the unused memory of PvsMetadata).
// So we set it to a bogus value and BAM! Errors are obvious!
private byte Pad0;
public uint Marker;
#endif
}
[StructLayout(LayoutKind.Sequential, Size = 16)]
internal struct PvsMetadataFreeLink
{
#if DEBUG
static unsafe PvsMetadataFreeLink()
{
return NetEntity.GetHashCode();
DebugTools.Assert(sizeof(PvsMetadataFreeLink) == sizeof(PvsMetadata));
}
#endif
public int Pad0;
public int Pad1;
public int Pad2;
// We offset the NextFree to be at the end of the struct.
// This is so that it overlaps with the debug Marker field of PvsMetadata instead of real data.
public PvsIndex NextFree;
}
/// <summary>
/// Struct for storing information about the current number of entities that are being sent to the player this tick.
/// Used to enforce pvs budgets.
/// </summary>
internal struct PvsBudget
{
public int NewLimit;

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -12,6 +14,7 @@ namespace Robust.Server.GameStates;
public sealed class PvsOverrideSystem : EntitySystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IConsoleHost _console = default!;
private readonly HashSet<EntityUid> _hasOverride = new();
@@ -28,8 +31,64 @@ public sealed class PvsOverrideSystem : EntitySystem
SubscribeLocalEvent<MapChangedEvent>(OnMapChanged);
SubscribeLocalEvent<GridInitializeEvent>(OnGridCreated);
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
// TODO console commands for adding/removing overrides?
_console.RegisterCommand(
"pvs_override_info",
Loc.GetString("cmd-pvs-override-info-desc"),
"pvs_override_info",
GetPvsInfo,
GetCompletion);
}
#region Console Commands
/// <summary>
/// Debug command for displaying PVS override information.
/// </summary>
private void GetPvsInfo(IConsoleShell shell, string argstr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error"));
return;
}
if (!NetEntity.TryParse(args[0], out var nuid) || !TryGetEntity(nuid, out var uid))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-uid"));
return;
}
if (!_hasOverride.Contains(uid.Value))
{
shell.WriteLine(Loc.GetString("cmd-pvs-override-info-empty", ("nuid", args[0])));
return;
}
if (GlobalOverride.Contains(uid.Value) || ForceSend.Contains(uid.Value))
shell.WriteLine(Loc.GetString("cmd-pvs-override-info-global", ("nuid", args[0])));
HashSet<ICommonSession> sessions = new();
sessions.UnionWith(SessionOverrides.Where(x => x.Value.Contains(uid.Value)).Select(x => x.Key));
sessions.UnionWith(SessionForceSend.Where(x => x.Value.Contains(uid.Value)).Select(x => x.Key));
if (sessions.Count == 0)
return;
var clients = string.Join(", ", sessions.Select(x => x.ToString()));
shell.WriteLine(Loc.GetString("cmd-pvs-override-info-clients", ("nuid", args[0]), ("clients", clients)));
}
private CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length != 1)
return CompletionResult.Empty;
return CompletionResult.FromHintOptions(CompletionHelper.NetEntities(args[0], EntityManager), "NetEntity");
}
#endregion
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs ev)
{
if (ev.NewStatus != SessionStatus.Disconnected)

View File

@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
using System.Threading;
using Prometheus;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Player;
using Robust.Shared.Threading;
@@ -105,7 +106,7 @@ internal sealed partial class PvsSystem
private void ProcessQueuedAck(PvsSession session)
{
var ackedTick = session.LastReceivedAck;
List<PvsData>? ackedEnts;
List<PvsIndex>? ackedEnts;
if (session.Overflow != null && session.Overflow.Value.Tick <= ackedTick)
{
@@ -125,12 +126,12 @@ internal sealed partial class PvsSystem
else if (!session.PreviouslySent.TryGetValue(ackedTick, out ackedEnts))
return;
foreach (var data in CollectionsMarshal.AsSpan(ackedEnts))
foreach (ref var intPtr in CollectionsMarshal.AsSpan(ackedEnts))
{
data.EntityLastAcked = ackedTick;
ref var data = ref session.DataMemory.GetRef(intPtr.Index);
DebugTools.AssertNotEqual(data.LastSeen, GameTick.Zero);
DebugTools.Assert(data.LastSeen >= ackedTick); // LastSent may equal ackedTick if the packet was sent reliably.
DebugTools.Assert(!session.Entities.TryGetValue(data.NetEntity, out var old)
|| ReferenceEquals(data, old));
data.EntityLastAcked = ackedTick;
}
// The client acked a tick. If they requested a full state, this ack happened some time after that, so we can safely set this to false

View File

@@ -8,6 +8,7 @@ using Prometheus;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Map.Enumerators;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Utility;

View File

@@ -0,0 +1,384 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
// This partial class handles the PvsData memory. This array stores information about when each entity was last sent to
// each player. This is somewhat faster than using a per-player Dictionary<EntityUid, PvsData>, though it can be less
// memory efficient.
internal sealed partial class PvsSystem
{
// This is used for asserts.
private HashSet<PvsIndex> _assignedEnts = new();
/// <summary>
/// Recently returned indexes from deleted entities. These get moved to <see cref="_pendingReturns"/> before
/// moving back into the free list.
/// </summary>
private List<PvsIndex> _incomingReturns = new();
/// <summary>
/// Recently returned pointers from deleted entities. These will get returned to the free list
/// after a minimum amount of time has passed, to ensure that processing late game-state ack messages doesn't
/// write data to deleted entities.
/// </summary>
private List<PvsIndex> _pendingReturns = new();
/// <summary>
/// Tick at which the <see cref="_pendingReturns"/> were last processed.
/// </summary>
private GameTick _lastReturn = GameTick.Zero;
/// <summary>
/// Memory region to store <see cref="PvsMetadata"/> instances and the free list.
/// </summary>
/// <remarks>
/// Unused elements form a linked list out of <see cref="PvsMetadataFreeLink"/> elements.
/// </remarks>
private ResizableMemoryRegion<PvsMetadata> _metadataMemory = default!;
/// <summary>
/// The head of the PVS data free list. This is the first element that will be used if a new one is needed.
/// </summary>
/// <remarks>
/// If the value is <see cref="PvsIndex.Invalid"/>,
/// there are no more free elements and the next allocation must expand the memory.
/// </remarks>
private PvsIndex _dataFreeListHead;
private WaitHandle? _deletionTask;
/// <summary>
/// Expand the size of <see cref="_metadataMemory"/> (and all session data stores) one iteration.
/// </summary>
/// <remarks>
/// This ensures that we have at least one free list slot.
/// </remarks>
private void ExpandEntityCapacity()
{
var initial = _metadataMemory.CurrentSize;
var entityGrowth = _configManager.GetCVar(CVars.NetPvsEntityGrowth);
var newSize = initial + (entityGrowth <= 0 ? initial : entityGrowth);
newSize = Math.Min(newSize, _metadataMemory.MaxSize);
if (newSize == initial)
throw new InvalidOperationException("Out of PVS entity capacity! Increase net.pvs_entity_max!");
Log.Debug($"Growing PvsData memory from {initial} -> {newSize} entities");
_metadataMemory.Expand(newSize);
foreach (var playerSession in PlayerData.Values)
{
playerSession.DataMemory.Expand(newSize);
}
var newSlots = _metadataMemory.GetSpan<PvsMetadataFreeLink>()[initial..];
InitializeFreeList(newSlots, initial, ref _dataFreeListHead);
}
/// <summary>
/// Initialize <see cref="_metadataMemory"/> and the free list.
/// </summary>
private void InitializePvsArray()
{
var initialCount = _configManager.GetCVar(CVars.NetPvsEntityInitial);
var maxCount = _configManager.GetCVar(CVars.NetPvsEntityMax);
if (initialCount <= 0 || maxCount <= 0)
throw new InvalidOperationException("net.pvs_entity_initial and net.pvs_entity_max must be positive");
_metadataMemory = new ResizableMemoryRegion<PvsMetadata>(maxCount, initialCount);
ResetDataMemory();
}
/// <summary>
/// Initialize a section of the free list.
/// </summary>
/// <param name="memory">The section of the free list to initialize.</param>
/// <param name="baseOffset">What offset in the total PVS data this section starts at.</param>
/// <param name="head">The current head storage of the free list to update.</param>
private static void InitializeFreeList(Span<PvsMetadataFreeLink> memory, int baseOffset, ref PvsIndex head)
{
for (var i = 0; i < memory.Length; i++)
{
memory[i].NextFree = new PvsIndex(baseOffset + i + 1);
}
memory[^1].NextFree = head;
head = new PvsIndex(baseOffset);
}
/// <summary>
/// Clear all PVS data. After this function is called,
/// <see cref="ResetDataMemory"/> must be called if the system isn't being shut down.
/// </summary>
private void ClearPvsData()
{
_leaveTask?.WaitOne();
_leaveTask = null;
_deletionTask?.WaitOne();
_deletionTask = null;
_incomingReturns.Clear();
_pendingReturns.Clear();
_deletionJob.ToClear.Clear();
_assignedEnts.Clear();
// Remove all pointers stored in any player's PVS send-histories. Required to avoid accidentally writing to
// invalid bits of memory while processing late game-state acks. This also forces all players to receive a full
// game state, in lieu of sending the required PVS leave messages.
foreach (var session in PlayerData.Values)
{
session.DataMemory.Clear();
ForceFullState(session);
}
_metadataMemory.Clear();
}
/// <summary>
/// Re-initialize the memory in <see cref="_metadataMemory"/> after it was fully cleared on reset.
/// </summary>
private void ResetDataMemory()
{
_dataFreeListHead = PvsIndex.Invalid;
InitializeFreeList(_metadataMemory.GetSpan<PvsMetadataFreeLink>(), 0, ref _dataFreeListHead);
}
/// <summary>
/// Shrink <see cref="_metadataMemory"/> (and all sessions) back down to initial entity size after clear.
/// </summary>
private void ShrinkDataMemory()
{
DebugTools.Assert(EntityManager.EntityCount == 0);
var initialCount = _configManager.GetCVar(CVars.NetPvsEntityInitial);
if (initialCount != _metadataMemory.CurrentSize)
{
Log.Debug($"Shrinking PVS data from {_metadataMemory.CurrentSize} -> {initialCount} entities");
_metadataMemory.Shrink(initialCount);
foreach (var player in PlayerData.Values)
{
player.DataMemory.Shrink(initialCount);
}
}
}
/// <summary>
/// This method shuffles the entity free list. This is used to avoid accidental / unrealistic cache locality
/// in benchmarks.
/// </summary>
internal void ShufflePointers(int seed)
{
throw new NotImplementedException();
/*List<IntPtr> ptrs = new(_pointerPool);
_pointerPool.Clear();
var rng = new Random(seed);
var n = ptrs.Count;
while (n > 0)
{
var k = rng.Next(n);
_pointerPool.Push(ptrs[k]);
ptrs[k] = ptrs[^1];
ptrs.RemoveAt(--n);
}*/
}
/// <summary>
/// Clear all of this sessions' PvsData for all entities. This effectively means that PVS will act as if the player
/// had never been sent information about any entity. Used when returning the player's index offset to the pool.
/// </summary>
private void ClearPlayerPvsData(PvsSession session)
{
session.DataMemory.Clear();
}
/// <summary>
/// Clear all of this entity' PvsData entries. This effectively means that PVS will act as if no player
/// had never been sent information about this entity. Used when returning the entity's index back to the free list.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ClearEntityPvsData(PvsIndex index)
{
foreach (var playerData in PlayerData.Values)
{
ref var entry = ref playerData.DataMemory.GetRef(index.Index);
entry = default;
}
}
/// <summary>
/// Get the NetEntity associated with a given <see cref="PvsIndex"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private NetEntity IndexToNetEntity(PvsIndex index)
{
DebugTools.Assert(_assignedEnts.Contains(index));
return _metadataMemory.GetRef(index.Index).NetEntity;
}
/// <summary>
/// Create a new <see cref="ResizableMemoryRegion{T}"/> suitable for assigning to a new <see cref="PvsSession"/>.
/// </summary>
private ResizableMemoryRegion<PvsData> CreateSessionDataMemory()
{
return new ResizableMemoryRegion<PvsData>(_metadataMemory.MaxSize, _metadataMemory.CurrentSize);
}
private static void FreeSessionDataMemory(PvsSession session)
{
session.DataMemory.Dispose();
}
private void OnEntityAdded(Entity<MetaDataComponent> entity)
{
DebugTools.Assert(entity.Comp.PvsData.Index == default);
AssignEntityPointer(entity.Comp);
}
/// <summary>
/// Retrieve a free entity index and assign it to an entity.
/// </summary>
private void AssignEntityPointer(MetaDataComponent meta)
{
if (_dataFreeListHead == PvsIndex.Invalid)
{
ExpandEntityCapacity();
DebugTools.Assert(_dataFreeListHead != PvsIndex.Invalid);
}
var index = _dataFreeListHead;
DebugTools.Assert(_assignedEnts.Add(index));
ref var metadata = ref _metadataMemory.GetRef(index.Index);
ref var freeLink = ref Unsafe.As<PvsMetadata, PvsMetadataFreeLink>(ref metadata);
_dataFreeListHead = freeLink.NextFree;
// TODO: re-introduce this assert.
// DebugTools.AssertEqual(((PvsMetadata*) ptr)->NetEntity, NetEntity.Invalid);
DebugTools.AssertNotEqual(meta.NetEntity, NetEntity.Invalid);
meta.PvsData = index;
metadata.NetEntity = meta.NetEntity;
metadata.LastModifiedTick = meta.LastModifiedTick;
metadata.VisMask = meta.VisibilityMask;
metadata.LifeStage = meta.EntityLifeStage;
#if DEBUG
metadata.Marker = uint.MaxValue;
#endif
}
/// <summary>
/// Return an entity's index in the data array back to the free list of available indices.
/// </summary>
private void OnEntityDeleted(Entity<MetaDataComponent> entity)
{
var ptr = entity.Comp.PvsData;
entity.Comp.PvsData = default;
if (ptr == default)
return;
_incomingReturns.Add(ptr);
}
/// <summary>
/// Immediately return all data indexes back to the pool after flushing all entities.
/// </summary>
private void AfterEntityFlush()
{
DebugTools.Assert(EntityManager.EntityCount == 0);
ClearPvsData();
ShrinkDataMemory();
ResetDataMemory();
}
/// <summary>
/// This update method periodically returns entity indices back to the pool, once we are sure no old
/// game state acks will use indices to that entity.
/// </summary>
private void ProcessDeletions()
{
var curTick = _gameTiming.CurTick;
if (curTick < _lastReturn + (uint)ForceAckThreshold + 1)
return;
if (curTick < _lastReturn)
throw new InvalidOperationException($"Time travel is not supported");
_leaveTask?.WaitOne();
_leaveTask = null;
_deletionTask?.WaitOne();
_deletionTask = null;
_lastReturn = curTick;
foreach (var index in CollectionsMarshal.AsSpan(_deletionJob.ToClear))
{
ReturnEntity(index);
}
_deletionJob.ToClear.Clear();
// Cycle lists.
(_deletionJob.ToClear, _pendingReturns, _incomingReturns) = (_pendingReturns, _incomingReturns, _deletionJob.ToClear);
if (_deletionJob.ToClear.Count == 0)
return;
#if DEBUG
foreach (var index in CollectionsMarshal.AsSpan(_deletionJob.ToClear))
{
DebugTools.Assert(_assignedEnts.Remove(index));
}
#endif
if (_deletionJob.ToClear.Count > 16)
{
_deletionTask = _parallelManager.Process(_deletionJob, _deletionJob.Count);
return;
}
foreach (var index in CollectionsMarshal.AsSpan(_deletionJob.ToClear))
{
ClearEntityPvsData(index);
}
}
private void ReturnEntity(PvsIndex index)
{
DebugTools.Assert(!_assignedEnts.Contains(index));
ref var freeLink = ref _metadataMemory.GetRef<PvsMetadataFreeLink>(index.Index);
freeLink.NextFree = _dataFreeListHead;
_dataFreeListHead = index;
}
private record struct PvsDeletionsJob(PvsSystem _pvs) : IParallelRobustJob
{
public int BatchSize => 8;
private PvsSystem _pvs = _pvs;
public List<PvsIndex> ToClear = new();
public int Count => ToClear.Count;
public void Execute(int index)
{
_pvs.ClearEntityPvsData(ToClear[index]);
}
}
}

View File

@@ -49,6 +49,12 @@ namespace Robust.Server.GameStates
private void OnEntityDirty(Entity<MetaDataComponent> uid)
{
if (uid.Comp.PvsData != default)
{
ref var meta = ref _metadataMemory.GetRef(uid.Comp.PvsData.Index);
meta.LastModifiedTick = uid.Comp.EntityLastModifiedTick;
}
if (!_addEntities[_currentIndex].Contains(uid))
_dirtyEntities[_currentIndex].Add(uid);
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Diagnostics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
@@ -25,11 +26,6 @@ internal sealed partial class PvsSystem
{
var meta = ev.Entity.Comp;
foreach (var sessionData in PlayerData.Values)
{
sessionData.Entities.Remove(meta.NetEntity);
}
_deletedEntities.Add(meta.NetEntity);
_deletedTick.Add(_gameTiming.CurTick);
RemoveEntityFromChunk(ev.Entity.Owner, meta);
@@ -111,4 +107,14 @@ internal sealed partial class PvsSystem
DebugTools.AssertNull(xform.MapUid);
AssertNullspace(xform.ParentUid);
}
internal void SyncMetadata(MetaDataComponent meta)
{
if (meta.PvsData == default)
return;
ref var ptr = ref _metadataMemory.GetRef(meta.PvsData.Index);
ptr.VisMask = meta.VisibilityMask;
ptr.LifeStage = meta.EntityLifeStage;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Prometheus;
@@ -47,12 +48,14 @@ internal sealed partial class PvsSystem
return;
var (toTick, lastSent) = session.LastSent.Value;
foreach (var data in CollectionsMarshal.AsSpan(lastSent))
foreach (var intPtr in CollectionsMarshal.AsSpan(lastSent))
{
ref var data = ref session.DataMemory.GetRef(intPtr.Index);
if (data.LastSeen == toTick)
continue;
session.LeftView.Add(data.NetEntity);
session.LeftView.Add(IndexToNetEntity(intPtr));
data.LastLeftView = toTick;
}

View File

@@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;
@@ -8,21 +10,23 @@ namespace Robust.Server.GameStates;
// range/chunk restrictions.
internal sealed partial class PvsSystem
{
private readonly List<Entity<MetaDataComponent>> _cachedForceOverride = new();
private readonly List<Entity<MetaDataComponent>> _cachedGlobalOverride = new();
private readonly List<PvsChunk.ChunkEntity> _cachedForceOverride = new();
private readonly List<PvsChunk.ChunkEntity> _cachedGlobalOverride = new();
private readonly HashSet<EntityUid> _forceOverrideSet = new();
private readonly HashSet<EntityUid> _globalOverrideSet = new();
private void AddAllOverrides(PvsSession session)
{
var mask = session.VisMask;
var fromTick = session.FromTick;
RaiseExpandEvent(session, fromTick);
foreach (var entity in _cachedGlobalOverride)
foreach (ref var ent in CollectionsMarshal.AsSpan(_cachedGlobalOverride))
{
if (!AddEntity(session, entity, fromTick))
break;
ref var meta = ref _metadataMemory.GetRef(ent.Ptr.Index);
if ((mask & meta.VisMask) == meta.VisMask)
AddEntity(session, ref ent, ref meta, fromTick);
}
if (!_pvsOverride.SessionOverrides.TryGetValue(session.Session, out var sessionOverrides))
@@ -42,10 +46,13 @@ internal sealed partial class PvsSystem
// Ignore PVS budgets
session.Budget = new() {NewLimit = int.MaxValue, EnterLimit = int.MaxValue};
var mask = session.VisMask;
var fromTick = session.FromTick;
foreach (var entity in _cachedForceOverride)
foreach (ref var ent in CollectionsMarshal.AsSpan(_cachedForceOverride))
{
AddEntity(session, entity, fromTick);
ref var meta = ref _metadataMemory.GetRef(ent.Ptr.Index);
if ((mask & meta.VisMask) == meta.VisMask)
AddEntity(session, ref ent, ref meta, fromTick);
}
foreach (var uid in session.Viewers)
@@ -156,7 +163,7 @@ internal sealed partial class PvsSystem
private bool CacheOverrideParents(
EntityUid uid,
List<Entity<MetaDataComponent>> list,
List<PvsChunk.ChunkEntity> list,
HashSet<EntityUid> set,
out TransformComponent xform)
{
@@ -175,11 +182,11 @@ internal sealed partial class PvsSystem
return false;
}
list.Add((uid, meta));
list.Add(new(uid, meta));
return true;
}
private void CacheOverrideChildren(TransformComponent xform, List<Entity<MetaDataComponent>> list, HashSet<EntityUid> set)
private void CacheOverrideChildren(TransformComponent xform, List<PvsChunk.ChunkEntity> list, HashSet<EntityUid> set)
{
foreach (var child in xform._children)
{
@@ -190,7 +197,7 @@ internal sealed partial class PvsSystem
}
if (set.Add(child))
list.Add((child, _metaQuery.GetComponent(child)));
list.Add(new(child, _metaQuery.GetComponent(child)));
CacheOverrideChildren(childXform, list, set);
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.GameObjects;
@@ -15,8 +16,8 @@ internal sealed partial class PvsSystem
/// </summary>
private const int MaxVisPoolSize = 1024;
private readonly ObjectPool<List<PvsData>> _entDataListPool
= new DefaultObjectPool<List<PvsData>>(new ListPolicy<PvsData>(), MaxVisPoolSize);
private readonly ObjectPool<List<PvsIndex>> _entDataListPool
= new DefaultObjectPool<List<PvsIndex>>(new ListPolicy<PvsIndex>(), MaxVisPoolSize);
private readonly ObjectPool<HashSet<EntityUid>> _uidSetPool
= new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>(), MaxVisPoolSize);

View File

@@ -72,7 +72,10 @@ internal sealed partial class PvsSystem
private PvsSession GetOrNewPvsSession(ICommonSession session)
{
if (!PlayerData.TryGetValue(session, out var pvsSession))
PlayerData[session] = pvsSession = new(session);
{
var memoryRegion = CreateSessionDataMemory();
PlayerData[session] = pvsSession = new(session, memoryRegion);
}
return pvsSession;
}
@@ -193,6 +196,5 @@ internal sealed partial class PvsSystem
session.PreviouslySent.Clear();
session.LastSent = null;
session.Entities.Clear();
}
}

View File

@@ -61,71 +61,91 @@ internal sealed partial class PvsSystem
for (var i = 0; i < limit; i++)
{
var ent = span[i];
if ((mask & ent.Comp.VisibilityMask) != ent.Comp.VisibilityMask)
ref var meta = ref _metadataMemory.GetRef(ent.Ptr.Index);
if ((mask & meta.VisMask) != meta.VisMask)
continue;
// TODO PVS improve this somehow
// Having entities "leave" pvs view just because the pvs entry budget was exceeded sucks.
// This probably requires changing client game state manager to support receiving entities with unknown parents.
// Probably needs to do something similar to pending net entity states, but for entity spawning.
if (!AddEntity(session, ent, fromTick))
if (!AddEntity(session, ref ent, ref meta, fromTick))
limit = directChildren;
}
}
/// <summary>
/// Attempt to add an entity to the to-send lists, while respecting pvs budgets.
/// </summary>
/// <returns>Returns false if the entity would exceed the client's PVS budget.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool AddEntity(PvsSession session, ref PvsChunk.ChunkEntity ent, ref PvsMetadata meta,
GameTick fromTick)
{
DebugTools.Assert(fromTick < _gameTiming.CurTick);
ref var data = ref session.DataMemory.GetRef(ent.Ptr.Index);
if (data.LastSeen == _gameTiming.CurTick)
return true;
if (meta.LifeStage >= EntityLifeStage.Terminating)
{
Log.Error($"Attempted to send deleted entity: {ToPrettyString(ent.Uid)}");
EntityManager.QueueDeleteEntity(ent.Uid);
return true;
}
var (entered,budgetExceeded) = IsEnteringPvsRange(ref data, fromTick, ref session.Budget);
if (budgetExceeded)
return false;
data.LastSeen = _gameTiming.CurTick;
session.ToSend!.Add(ent.Ptr);
if (session.RequestedFull)
{
var state = GetFullEntityState(session.Session, ent.Uid, ent.Meta);
session.States.Add(state);
return true;
}
if (entered)
{
var state = GetEntityState(session.Session, ent.Uid, data.EntityLastAcked, ent.Meta);
session.States.Add(state);
return true;
}
if (meta.LastModifiedTick <= fromTick)
return true;
var entState = GetEntityState(session.Session, ent.Uid, fromTick , ent.Meta);
if (!entState.Empty)
session.States.Add(entState);
return true;
}
/// <summary>
/// Attempt to add an entity to the to-send lists, while respecting pvs budgets.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool AddEntity(PvsSession session, Entity<MetaDataComponent> entity, GameTick fromTick)
{
var nuid = entity.Comp.NetEntity;
ref var data = ref CollectionsMarshal.GetValueRefOrAddDefault(session.Entities, nuid, out var exists);
if (!exists)
data = new(nuid);
DebugTools.Assert(fromTick < _gameTiming.CurTick);
ref var data = ref session.DataMemory.GetRef(entity.Comp.PvsData.Index);
if (entity.Comp.Deleted)
{
Log.Error($"Attempted to send deleted entity: {ToPrettyString(entity, entity)}");
session.Entities.Remove(entity.Comp.NetEntity);
return false;
}
DebugTools.AssertEqual(data!.NetEntity, entity.Comp.NetEntity);
if (data.LastSeen == _gameTiming.CurTick)
return true;
var (entered,budgetExceeded) = IsEnteringPvsRange(data, fromTick, ref session.Budget);
var (entered,budgetExceeded) = IsEnteringPvsRange(ref data, fromTick, ref session.Budget);
if (budgetExceeded)
return false;
if (!AddToSendList(session, data, entity, fromTick, entered))
return false;
DebugTools.AssertNotEqual(data.LastSeen, GameTick.Zero);
return true;
}
/// <summary>
/// This method adds an entity to the list of visible entities, updates the last-seen tick, and computes any
/// required game states.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool AddToSendList(PvsSession session, PvsData data, Entity<MetaDataComponent> entity, GameTick fromTick,
bool entered)
{
DebugTools.Assert(fromTick < _gameTiming.CurTick);
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (data == null)
{
Log.Error($"Encountered null EntityData.");
return false;
}
DebugTools.AssertNotEqual(data.LastSeen, _gameTiming.CurTick);
DebugTools.Assert(data.EntityLastAcked <= fromTick || fromTick == GameTick.Zero);
var (uid, meta) = entity;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
@@ -147,19 +167,18 @@ internal sealed partial class PvsSystem
}
data.LastSeen = _gameTiming.CurTick;
session.ToSend!.Add(data);
EntityState state;
session.ToSend!.Add(entity.Comp.PvsData);
if (session.RequestedFull)
{
state = GetFullEntityState(session.Session, uid, meta);
var state = GetFullEntityState(session.Session, uid, meta);
session.States.Add(state);
return true;
}
if (entered)
{
state = GetEntityState(session.Session, uid, data.EntityLastAcked, meta);
var state = GetEntityState(session.Session, uid, data.EntityLastAcked, meta);
session.States.Add(state);
return true;
}
@@ -167,10 +186,10 @@ internal sealed partial class PvsSystem
if (meta.EntityLastModifiedTick <= fromTick)
return true;
state = GetEntityState(session.Session, uid, fromTick , meta);
var entState = GetEntityState(session.Session, uid, fromTick , meta);
if (!state.Empty)
session.States.Add(state);
if (!entState.Empty)
session.States.Add(entState);
return true;
}
@@ -181,7 +200,7 @@ internal sealed partial class PvsSystem
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private (bool Entering, bool BudgetExceeded) IsEnteringPvsRange(
PvsData data,
ref PvsData data,
GameTick fromTick,
ref PvsBudget budget)
{

View File

@@ -3,6 +3,7 @@ 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;
@@ -15,6 +16,8 @@ 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;
@@ -75,6 +78,7 @@ internal sealed partial class PvsSystem : EntitySystem
private PvsAckJob _ackJob;
private PvsChunkJob _chunkJob;
private PvsLeaveJob _leaveJob;
private PvsDeletionsJob _deletionJob;
private EntityQuery<EyeComponent> _eyeQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
@@ -110,6 +114,10 @@ internal sealed partial class PvsSystem : EntitySystem
{
base.Initialize();
if (Marshal.SizeOf<PvsMetadata>() != Marshal.SizeOf<PvsData>())
throw new Exception($"Pvs struct sizes must match");
_deletionJob = new PvsDeletionsJob(this);
_leaveJob = new PvsLeaveJob(this);
_chunkJob = new PvsChunkJob(this);
_ackJob = new PvsAckJob(this);
@@ -125,6 +133,9 @@ internal sealed partial class PvsSystem : EntitySystem
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_transform.OnGlobalMoveEvent += OnEntityMove;
EntityManager.EntityAdded += OnEntityAdded;
EntityManager.EntityDeleted += OnEntityDeleted;
EntityManager.AfterEntityFlush += AfterEntityFlush;
Subs.CVar(_configManager, CVars.NetPVS, SetPvs, true);
Subs.CVar(_configManager, CVars.NetMaxUpdateRange, OnViewsizeChanged, true);
@@ -135,10 +146,10 @@ internal sealed partial class PvsSystem : EntitySystem
_serverGameStateManager.ClientAck += OnClientAck;
_serverGameStateManager.ClientRequestFull += OnClientRequestFull;
_parallelMgr.ParallelCountChanged += ResetParallelism;
InitializeDirty();
_parallelMgr.ParallelCountChanged += ResetParallelism;
InitializePvsArray();
}
public override void Shutdown()
@@ -147,15 +158,22 @@ internal sealed partial class PvsSystem : EntitySystem
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
_transform.OnGlobalMoveEvent -= OnEntityMove;
EntityManager.EntityAdded -= OnEntityAdded;
EntityManager.EntityDeleted -= OnEntityDeleted;
EntityManager.AfterEntityFlush -= AfterEntityFlush;
_parallelMgr.ParallelCountChanged -= ResetParallelism;
_serverGameStateManager.ClientAck -= OnClientAck;
_serverGameStateManager.ClientRequestFull -= OnClientRequestFull;
ClearPvsData();
ShutdownDirty();
_leaveTask?.WaitOne();
_leaveTask = null;
}
public override void Update(float frameTime)
{
ProcessDeletions();
}
/// <summary>
@@ -253,6 +271,7 @@ internal sealed partial class PvsSystem : EntitySystem
session.LastReceivedAck = _gameTiming.CurTick;
session.RequestedFull = true;
ClearSendHistory(session);
ClearPlayerPvsData(session);
}
private void OnViewsizeChanged(float value)
@@ -342,19 +361,20 @@ internal sealed partial class PvsSystem : EntitySystem
{
var toSend = pvsSession.ToSend;
var toSendSet = new HashSet<NetEntity>(toSend!.Count);
foreach (var data in toSend)
foreach (var intPtr in toSend)
{
toSendSet.Add(data.NetEntity);
toSendSet.Add(IndexToNetEntity(intPtr));
}
DebugTools.AssertEqual(toSend.Count, toSendSet.Count);
foreach (var data in CollectionsMarshal.AsSpan(toSend))
foreach (var intPtr in CollectionsMarshal.AsSpan(toSend))
{
ref var data = ref pvsSession.DataMemory.GetRef(intPtr.Index);
DebugTools.AssertEqual(data.LastSeen, _gameTiming.CurTick);
DebugTools.Assert(ReferenceEquals(data, pvsSession.Entities[data.NetEntity]));
// if an entity is visible, its parents should always be visible.
if (_xformQuery.GetComponent(GetEntity(data.NetEntity)).ParentUid is not {Valid: true} pUid)
if (_xformQuery.GetComponent(GetEntity(IndexToNetEntity(intPtr))).ParentUid is not {Valid: true} pUid)
continue;
DebugTools.Assert(toSendSet.Contains(GetNetEntity(pUid)),
@@ -362,11 +382,11 @@ internal sealed partial class PvsSystem : EntitySystem
}
pvsSession.PreviouslySent.TryGetValue(_gameTiming.CurTick - 1, out var lastSent);
foreach (var data in CollectionsMarshal.AsSpan(lastSent))
foreach (var intPtr in CollectionsMarshal.AsSpan(lastSent))
{
DebugTools.Assert(!pvsSession.Entities.TryGetValue(data.NetEntity, out var old) || ReferenceEquals(data, old));
ref var data = ref pvsSession.DataMemory.GetRef(intPtr.Index);
DebugTools.Assert(data.LastSeen != GameTick.Zero);
DebugTools.AssertEqual(toSendSet.Contains(data.NetEntity), data.LastSeen == _gameTiming.CurTick);
DebugTools.AssertEqual(toSendSet.Contains(IndexToNetEntity(intPtr)), data.LastSeen == _gameTiming.CurTick);
DebugTools.Assert(data.LastSeen == _gameTiming.CurTick
|| data.LastSeen == _gameTiming.CurTick - 1);
}
@@ -409,7 +429,10 @@ internal sealed partial class PvsSystem : EntitySystem
foreach (var session in _disconnected)
{
if (PlayerData.Remove(session, out var pvsSession))
{
ClearSendHistory(pvsSession);
FreeSessionDataMemory(pvsSession);
}
}
var ackJob = ProcessQueuedAcks();

View File

@@ -5,6 +5,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.Replays;
@@ -13,7 +14,7 @@ internal sealed class ReplayRecordingManager : SharedReplayRecordingManager, ISe
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
private PvsSystem _pvs = default!;
private PvsSession _pvsSession = new(default!) { DisableCulling = true };
private PvsSession _pvsSession = new(default!, new ResizableMemoryRegion<PvsData>(1)) { DisableCulling = true };
public override void Initialize()
{

View File

@@ -1,12 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Diagnostics;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using static Microsoft.CodeAnalysis.SymbolDisplayFormat;
using static Microsoft.CodeAnalysis.SymbolDisplayMiscellaneousOptions;
namespace Robust.Shared.CompNetworkGenerator
{
@@ -19,20 +17,26 @@ namespace Robust.Shared.CompNetworkGenerator
private const string GlobalEntityUidName = "global::Robust.Shared.GameObjects.EntityUid";
private const string GlobalNullableEntityUidName = "global::Robust.Shared.GameObjects.EntityUid?";
private const string GlobalNetEntityName = "global::Robust.Shared.GameObjects.NetEntity";
private const string GlobalNetEntityNullableName = "global::Robust.Shared.GameObjects.NetEntity?";
private const string GlobalEntityCoordinatesName = "global::Robust.Shared.Map.EntityCoordinates";
private const string GlobalNullableEntityCoordinatesName = "global::Robust.Shared.Map.EntityCoordinates?";
private const string GlobalEntityUidSetName = "global::System.Collections.Generic.HashSet<global::Robust.Shared.GameObjects.EntityUid>";
private const string GlobalNetEntityUidSetName = "global::System.Collections.Generic.HashSet<global::Robust.Shared.GameObjects.NetEntity>";
private const string GlobalNetEntityUidSetName = $"global::System.Collections.Generic.HashSet<{GlobalNetEntityName}>";
private const string GlobalEntityUidListName = "global::System.Collections.Generic.List<global::Robust.Shared.GameObjects.EntityUid>";
private const string GlobalNetEntityUidListName = "global::System.Collections.Generic.List<global::Robust.Shared.GameObjects.NetEntity>";
private const string GlobalNetEntityUidListName = $"global::System.Collections.Generic.List<{GlobalNetEntityName}>";
private const string GlobalDictionaryName = "global::System.Collections.Generic.Dictionary<TKey, TValue>";
private const string GlobalHashSetName = "global::System.Collections.Generic.HashSet<T>";
private const string GlobalListName = "global::System.Collections.Generic.List<T>";
private static string GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp, bool raiseAfterAutoHandle)
private static readonly SymbolDisplayFormat FullNullableFormat =
FullyQualifiedFormat.WithMiscellaneousOptions(IncludeNullableReferenceTypeModifier);
private static string? GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp, bool raiseAfterAutoHandle)
{
var nameSpace = classSymbol.ContainingNamespace.ToDisplayString();
var componentName = classSymbol.Name;
@@ -132,7 +136,7 @@ namespace Robust.Shared.CompNetworkGenerator
foreach (var (type, name) in fields)
{
var typeDisplayStr = type.ToDisplayString(FullyQualifiedFormat);
var typeDisplayStr = type.ToDisplayString(FullNullableFormat);
var nullable = type.NullableAnnotation == NullableAnnotation.Annotated;
var nullableAnnotation = nullable ? "?" : string.Empty;
@@ -181,6 +185,62 @@ namespace Robust.Shared.CompNetworkGenerator
break;
default:
if (type is INamedTypeSymbol { TypeArguments.Length: 2 } named &&
named.ConstructedFrom.ToDisplayString(FullyQualifiedFormat) == GlobalDictionaryName)
{
var key = named.TypeArguments[0].ToDisplayString(FullNullableFormat);
var keyNullable = key.EndsWith("?");
var value = named.TypeArguments[1].ToDisplayString(FullNullableFormat);
var valueNullable = value.EndsWith("?");
if (key is GlobalEntityUidName or GlobalNullableEntityUidName)
{
key = keyNullable ? GlobalNetEntityNullableName : GlobalNetEntityName;
var ensureGeneric = $"{componentName}, {value}";
if (value is GlobalEntityUidName or GlobalNullableEntityUidName)
{
value = valueNullable ? GlobalNetEntityNullableName : GlobalNetEntityName;
ensureGeneric = componentName;
}
stateFields.Append($@"
public Dictionary<{key}, {value}> {name} = default!;");
getStateInit.Append($@"
{name} = GetNetEntityDictionary(component.{name}),");
if (valueNullable && value is not GlobalNetEntityName and not GlobalNetEntityNullableName)
{
handleStateSetters.Append($@"
EnsureEntityDictionaryNullableValue<{componentName}, {value}>(state.{name}, uid, component.{name});");
}
else
{
handleStateSetters.Append($@"
EnsureEntityDictionary<{ensureGeneric}>(state.{name}, uid, component.{name});");
}
break;
}
if (value is GlobalEntityUidName or GlobalNullableEntityUidName)
{
value = valueNullable ? GlobalNetEntityNullableName : GlobalNetEntityName;
stateFields.Append($@"
public Dictionary<{key}, {value}> {name} = default!;");
getStateInit.Append($@"
{name} = GetNetEntityDictionary(component.{name}),");
handleStateSetters.Append($@"
EnsureEntityDictionary<{componentName}, {key}>(state.{name}, uid, component.{name});");
break;
}
}
stateFields.Append($@"
public {typeDisplayStr} {name} = default!;");
@@ -193,7 +253,7 @@ namespace Robust.Shared.CompNetworkGenerator
handleStateSetters.Append($@"
if (state.{name} == null)
component.{name} = null;
component.{name} = null!;
else
component.{name} = new(state.{name});");
}
@@ -219,6 +279,7 @@ namespace Robust.Shared.CompNetworkGenerator
}
return $@"// <auto-generated />
#nullable enable
using System;
using Robust.Shared.GameStates;
using Robust.Shared.GameObjects;
@@ -280,10 +341,10 @@ public partial class {componentName}
{
var attr = type.Attribute;
var raiseEv = false;
if (attr.ConstructorArguments.Length == 1 && attr.ConstructorArguments[0].Value != null)
if (attr.ConstructorArguments is [{Value: bool raise}])
{
// Get the afterautohandle bool, which is first constructor arg
raiseEv = (bool) attr.ConstructorArguments[0].Value;
raiseEv = raise;
}
var source = GenerateSource(context, type.Type, comp, raiseEv);
@@ -325,11 +386,11 @@ public partial class {componentName}
attr.AttributeClass != null &&
attr.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
if (typeSymbol == null)
continue;
if (relevantAttribute == null)
{
if (typeSymbol == null)
continue;
foreach (var mem in typeSymbol.GetMembers())
{
var attribute = mem.GetAttributes().FirstOrDefault(a =>

View File

@@ -1,13 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>9</LangVersion>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<Import Project="../Robust.Roslyn.Shared/Robust.Roslyn.Shared.props" />
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

@@ -186,10 +186,10 @@ namespace Robust.Shared.Maths
[Pure]
public readonly Box2 Intersect(in Box2 other)
{
var ourLeftBottom = new Vector2(Left, Bottom);
var ourRightTop = new Vector2(Right, Top);
var otherLeftBottom = new Vector2(other.Left, other.Bottom);
var otherRightTop = new Vector2(other.Right, other.Top);
var ourLeftBottom = BottomLeft;
var ourRightTop = TopRight;
var otherLeftBottom = other.BottomLeft;
var otherRightTop = other.TopRight;
var max = Vector2.Max(ourLeftBottom, otherLeftBottom);
var min = Vector2.Min(ourRightTop, otherRightTop);
@@ -219,10 +219,10 @@ namespace Robust.Shared.Maths
[Pure]
public readonly Box2 Union(in Box2 other)
{
var ourLeftBottom = new Vector2(Left, Bottom);
var otherLeftBottom = new Vector2(other.Left, other.Bottom);
var ourRightTop = new Vector2(Right, Top);
var otherRightTop = new Vector2(other.Right, other.Top);
var ourLeftBottom = BottomLeft;
var otherLeftBottom = other.BottomLeft;
var ourRightTop = TopRight;
var otherRightTop = other.TopRight;
var leftBottom = Vector2.Min(ourLeftBottom, otherLeftBottom);
var rightTop = Vector2.Max(ourRightTop, otherRightTop);

View File

@@ -3,6 +3,7 @@ using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using JetBrains.Annotations;
using Robust.Shared.Utility;
namespace Robust.Shared.Maths
@@ -57,7 +58,17 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// calculates the smallest AABB that will encompass the rotated box. The AABB is in local space.
/// Enlarges the box by the specified value.
/// </summary>
[Pure]
public readonly Box2Rotated Enlarged(float value)
{
var box = Box.Enlarged(value);
return new Box2Rotated(box, Rotation, Origin);
}
/// <summary>
/// Calculates the smallest AABB that will encompass the rotated box. The AABB is in local space.
/// </summary>
public readonly Box2 CalcBoundingBox()
{

View File

@@ -697,6 +697,32 @@ namespace Robust.Shared.Maths
#endregion
/// <summary>
/// Round up (ceiling) a value to a multiple of a known power of two.
/// </summary>
/// <param name="value">The value to round up.</param>
/// <param name="powerOfTwo">
/// The power of two to round up to a multiple of. The result is undefined if this is not a power of two.
/// </param>
/// <remarks>
/// The result is undefined if either value is negative.
/// </remarks>
/// <typeparam name="T">The type of integer to operate on.</typeparam>
/// <example>
/// <code>
/// MathHelper.CeilMultiplyPowerOfTwo(5, 4) // 8
/// MathHelper.CeilMultiplyPowerOfTwo(4, 4) // 4
/// MathHelper.CeilMultiplyPowerOfTwo(8, 4) // 8
/// </code>
/// </example>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T CeilMultipleOfPowerOfTwo<T>(T value, T powerOfTwo) where T : IBinaryInteger<T>
{
var mask = powerOfTwo - T.One;
var remainder = value & mask;
return remainder == T.Zero ? value : (value | mask) + T.One;
}
#endregion Public Members
}
}

View File

@@ -0,0 +1,29 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Analyzers;
/// <summary>
/// Indicate that a <see cref="Component"/> should automatically handle unpausing of timer fields.
/// </summary>
/// <remarks>
/// When this attribute is set on a <see cref="Component"/>, an <see cref="EntitySystem"/> will automatically be
/// generated that increments any fields tagged with <see cref="AutoPausedFieldAttribute"/> when the entity is unpaused
/// (<see cref="EntityUnpausedEvent"/>).
/// </remarks>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
[BaseTypeRequired(typeof(IComponent))]
public sealed class AutoGenerateComponentPauseAttribute : Attribute
{
public bool Dirty = false;
}
/// <summary>
/// Mark a field or property to automatically handle unpausing with <see cref="AutoGenerateComponentPauseAttribute"/>.
/// </summary>
/// <remarks>
/// The type of the field or prototype must be <see cref="TimeSpan"/> (potentially nullable).
/// </remarks>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class AutoPausedFieldAttribute : Attribute;

View File

@@ -266,7 +266,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
return sound == null ? null : PlayGlobal(GetSound(sound), recipient, sound.Params);
}
public abstract void LoadStream<T>(AudioComponent component, T stream);
public abstract void LoadStream<T>(Entity<AudioComponent> entity, T stream);
/// <summary>
/// Play an audio file globally, without position.

View File

@@ -183,6 +183,30 @@ namespace Robust.Shared
public static readonly CVarDef<bool> NetPVS =
CVarDef.Create("net.pvs", true, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Size increments for the automatic growth of Pvs' entity data storage. 0 will increase it by factors of 2
/// </summary>
public static readonly CVarDef<int> NetPvsEntityGrowth =
CVarDef.Create("net.pvs_entity_growth", 1 << 16, CVar.ARCHIVE | CVar.SERVERONLY);
/// <summary>
/// Initial size of PVS' entity data storage.
/// </summary>
public static readonly CVarDef<int> NetPvsEntityInitial =
CVarDef.Create("net.pvs_entity_initial", 1 << 16, CVar.ARCHIVE | CVar.SERVERONLY);
/// <summary>
/// Maximum ever size of PVS' entity data storage.
/// </summary>
/// <remarks>
/// <para>
/// Arbitrarily set to a default of 16 million entities.
/// Increasing this parameter does not increase real memory usage, only virtual.
/// </para>
/// </remarks>
public static readonly CVarDef<int> NetPvsEntityMax =
CVarDef.Create("net.pvs_entity_max", 1 << 24, CVar.ARCHIVE | CVar.SERVERONLY);
/// <summary>
/// If false, this will run more parts of PVS synchronously. This will generally slow it down, can be useful
/// for collecting tick timing metrics.
@@ -1031,6 +1055,12 @@ namespace Robust.Shared
* AUDIO
*/
/// <summary>
/// Default limit for concurrently playing an audio file.
/// </summary>
public static readonly CVarDef<int> AudioDefaultConcurrent =
CVarDef.Create("audio.default_concurrent", 16, CVar.CLIENTONLY | CVar.ARCHIVE);
public static readonly CVarDef<int> AudioAttenuation =
CVarDef.Create("audio.attenuation", (int) Attenuation.LinearDistanceClamped, CVar.REPLICATED | CVar.ARCHIVE);
@@ -1498,7 +1528,7 @@ namespace Robust.Shared
/// Maximum compressed size of a replay recording (in kilobytes) before recording automatically stops.
/// </summary>
public static readonly CVarDef<long> ReplayMaxCompressedSize = CVarDef.Create("replay.max_compressed_size",
1024L * 256, CVar.ARCHIVE);
1024L * 512, CVar.ARCHIVE);
/// <summary>
/// Maximum uncompressed size of a replay recording (in kilobytes) before recording automatically stops.

View File

@@ -6,7 +6,9 @@ using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Robust.Shared.Collections;
@@ -50,6 +52,41 @@ public struct ValueList<T> : IEnumerable<T>
Count = 0;
}
/// <summary>
/// Creates a list by copying the contents of the source list.
/// </summary>
public ValueList(List<T> list) : this(list, 0, list.Count)
{
}
/// <summary>
/// Creates a list by copying the contents of the source list.
/// </summary>
public ValueList(List<T> list, int start, int count)
{
_items = new T[count];
var liSpan = CollectionsMarshal.AsSpan(list)[start..(start + count)];
liSpan.CopyTo(_items);
Count = count;
}
/// <summary>
/// Creates a list by copying the contents of the source list.
/// </summary>
public ValueList(IReadOnlyCollection<T> list)
{
var count = list.Count;
_items = new T[count];
foreach (var entry in list)
{
var size = Count;
AddNoResize(entry, size);
}
}
/// <summary>
/// Create a list by copying the contents from another enumerable.
/// </summary>
@@ -157,12 +194,10 @@ public struct ValueList<T> : IEnumerable<T>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(T item)
{
var array = _items;
var size = Count;
if ((uint)size < (uint)Capacity)
{
Count = size + 1;
array![size] = item;
AddNoResize(item, size);
}
else
{
@@ -170,6 +205,14 @@ public struct ValueList<T> : IEnumerable<T>
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AddNoResize(T item, int size)
{
var array = _items;
Count = size + 1;
array![size] = item;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T AddRef()
{
@@ -522,4 +565,46 @@ public struct ValueList<T> : IEnumerable<T>
RemoveAt(Count - 1);
return old;
}
/// <summary>
/// Adds a range of values from the source list.
/// </summary>
public void AddRange(ValueList<T> list)
{
var liSpan = list.Span;
AddRange(liSpan);
}
/// <summary>
/// Adds a range of values from the source list.
/// </summary>
public void AddRange(List<T> list)
{
var liSpan = CollectionsMarshal.AsSpan(list);
AddRange(liSpan);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddRange(Span<T> span)
{
var spanCount = span.Length;
EnsureCapacity(Count + spanCount);
var target = new Span<T>(_items, Count, spanCount);
span.CopyTo(target);
Count += spanCount;
}
/// <summary>
/// Fills this with default data up to the specified count.
/// </summary>
public void EnsureLength(int newCount)
{
if (Count > newCount)
return;
EnsureCapacity(newCount);
var region = new Span<T>(_items, Count, (newCount - Count));
region.Clear();
Count = newCount;
}
}

View File

@@ -71,46 +71,6 @@ namespace Robust.Shared.Containers
[DataField("showEnts")]
public bool ShowContents { get; set; }
[Obsolete("Use container system method")]
public bool Insert(
EntityUid toinsert,
IEntityManager? entMan = null,
TransformComponent? transform = null,
TransformComponent? ownerTransform = null,
MetaDataComponent? meta = null,
PhysicsComponent? physics = null,
bool force = false)
{
IoCManager.Resolve(ref entMan);
return entMan.System<SharedContainerSystem>().Insert((toinsert, transform, meta, physics), this, ownerTransform, force);
}
/// <summary>
/// Whether the given entity can be inserted into this container.
/// </summary>
/// <param name="assumeEmpty">Whether to assume that the container is currently empty.</param>
protected internal virtual bool CanInsert(EntityUid toInsert, bool assumeEmpty, IEntityManager entMan) => true;
[Obsolete("Use container system method")]
public bool Remove(
EntityUid toRemove,
IEntityManager? entMan = null,
TransformComponent? xform = null,
MetaDataComponent? meta = null,
bool reparent = true,
bool force = false,
EntityCoordinates? destination = null,
Angle? localRotation = null
)
{
IoCManager.Resolve(ref entMan);
return entMan.System<SharedContainerSystem>().Remove((toRemove, xform, meta), this, reparent, force, destination, localRotation);
}
[Obsolete("Use container system method")]
public void ForceRemove(EntityUid toRemove, IEntityManager? entMan = null, MetaDataComponent? meta = null)
=> Remove(toRemove, entMan, meta: meta, reparent: false, force: true);
/// <summary>
/// Checks if the entity is contained in this container.
/// This is not recursive, so containers of children are not checked.
@@ -120,18 +80,10 @@ namespace Robust.Shared.Containers
public abstract bool Contains(EntityUid contained);
/// <summary>
/// Clears the container and marks it as deleted.
/// Whether the given entity can be inserted into this container.
/// </summary>
[Obsolete("use system method")]
public void Shutdown(IEntityManager? entMan = null, INetManager? _ = null)
{
IoCManager.Resolve(ref entMan);
entMan.System<SharedContainerSystem>().ShutdownContainer(this);
}
/// <inheritdoc />
[Access(typeof(SharedContainerSystem))]
protected internal abstract void InternalShutdown(IEntityManager entMan, SharedContainerSystem system, bool isClient);
/// <param name="assumeEmpty">Whether to assume that the container is currently empty.</param>
protected internal virtual bool CanInsert(EntityUid toInsert, bool assumeEmpty, IEntityManager entMan) => true;
/// <summary>
/// Implement to store the reference in whatever form you want
@@ -148,5 +100,14 @@ namespace Robust.Shared.Containers
/// <param name="entMan"></param>
[Access(typeof(SharedContainerSystem))]
protected internal abstract void InternalRemove(EntityUid toRemove, IEntityManager entMan);
/// <summary>
/// Implement to clear the container and mark it as deleted.
/// </summary>
/// <param name="entMan"></param>
/// <param name="system"></param>
/// <param name=isClient"></param>
[Access(typeof(SharedContainerSystem))]
protected internal abstract void InternalShutdown(IEntityManager entMan, SharedContainerSystem system, bool isClient);
}
}

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

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
@@ -199,6 +200,11 @@ namespace Robust.Shared.GameObjects
// (Creation can still be cleared though)
ClearCreationTick();
}
/// <summary>
/// Offset into internal PVS data.
/// </summary>
internal PvsIndex PvsData;
}
[Flags]
@@ -221,16 +227,32 @@ namespace Robust.Shared.GameObjects
/// </summary>
Detached = 1 << 2,
/// <summary>
/// Indicates this entity can never be handled by the client as PVS detached.
/// </summary>
Undetachable = 1 << 3,
/// <summary>
/// If true, then this entity is considered a "high priority" entity and will be sent to players from further
/// away. Useful for things like light sources and occluders. Only works if the entity is directly parented to
/// a grid or map.
/// </summary>
PvsPriority = 1 << 3,
PvsPriority = 1 << 4,
}
/// <summary>
/// Key struct for uniquely identifying a PVS chunk.
/// </summary>
internal readonly record struct PvsChunkLocation(EntityUid Uid, Vector2i Indices);
/// <summary>
/// An opaque index into the PVS data arrays on the server.
/// </summary>
internal readonly record struct PvsIndex(int Index)
{
/// <summary>
/// An invalid index. This is also used as a marker value in the free list.
/// </summary>
public static readonly PvsIndex Invalid = new PvsIndex(-1);
}
}

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
@@ -33,6 +32,7 @@ public partial class EntityManager
/// </summary>
internal void SetNetEntity(EntityUid uid, NetEntity netEntity, MetaDataComponent component)
{
DebugTools.Assert(component.NetEntity == NetEntity.Invalid || _netMan.IsClient);
DebugTools.Assert(!NetEntityLookup.ContainsKey(netEntity));
NetEntityLookup[netEntity] = (uid, component);
component.NetEntity = netEntity;
@@ -279,6 +279,66 @@ public partial class EntityManager
return entities;
}
public Dictionary<EntityUid, T> GetEntityDictionary<T>(Dictionary<NetEntity, T> netEntities)
{
var entities = new Dictionary<EntityUid, T>(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(GetEntity(pair.Key), pair.Value);
}
return entities;
}
public Dictionary<T, EntityUid> GetEntityDictionary<T>(Dictionary<T, NetEntity> netEntities) where T : notnull
{
var entities = new Dictionary<T, EntityUid>(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(pair.Key, GetEntity(pair.Value));
}
return entities;
}
public Dictionary<T, EntityUid?> GetEntityDictionary<T>(Dictionary<T, NetEntity?> netEntities) where T : notnull
{
var entities = new Dictionary<T, EntityUid?>(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(pair.Key, GetEntity(pair.Value));
}
return entities;
}
public Dictionary<EntityUid, EntityUid> GetEntityDictionary(Dictionary<NetEntity, NetEntity> netEntities)
{
var entities = new Dictionary<EntityUid, EntityUid>(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(GetEntity(pair.Key), GetEntity(pair.Value));
}
return entities;
}
public Dictionary<EntityUid, EntityUid?> GetEntityDictionary(Dictionary<NetEntity, NetEntity?> netEntities)
{
var entities = new Dictionary<EntityUid, EntityUid?>(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(GetEntity(pair.Key), GetEntity(pair.Value));
}
return entities;
}
public HashSet<EntityUid> EnsureEntitySet<T>(HashSet<NetEntity> netEntities, EntityUid callerEntity)
{
var entities = new HashSet<EntityUid>(netEntities.Count);
@@ -324,6 +384,72 @@ public partial class EntityManager
}
}
public void EnsureEntityDictionary<TComp, TValue>(Dictionary<NetEntity, TValue> netEntities, EntityUid callerEntity,
Dictionary<EntityUid, TValue> entities)
{
entities.Clear();
entities.EnsureCapacity(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(EnsureEntity<TComp>(pair.Key, callerEntity), pair.Value);
}
}
public void EnsureEntityDictionaryNullableValue<TComp, TValue>(Dictionary<NetEntity, TValue?> netEntities, EntityUid callerEntity,
Dictionary<EntityUid, TValue?> entities)
{
entities.Clear();
entities.EnsureCapacity(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(EnsureEntity<TComp>(pair.Key, callerEntity), pair.Value);
}
}
public void EnsureEntityDictionary<TComp, TKey>(Dictionary<TKey, NetEntity> netEntities, EntityUid callerEntity,
Dictionary<TKey, EntityUid> entities) where TKey : notnull
{
entities.Clear();
entities.EnsureCapacity(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(pair.Key, EnsureEntity<TComp>(pair.Value, callerEntity));
}
}
public void EnsureEntityDictionary<TComp, TKey>(Dictionary<TKey, NetEntity?> netEntities, EntityUid callerEntity,
Dictionary<TKey, EntityUid?> entities) where TKey : notnull
{
entities.Clear();
entities.EnsureCapacity(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(pair.Key, EnsureEntity<TComp>(pair.Value, callerEntity));
}
}
public void EnsureEntityDictionary<TComp>(Dictionary<NetEntity, NetEntity> netEntities, EntityUid callerEntity,
Dictionary<EntityUid, EntityUid> entities)
{
entities.Clear();
entities.EnsureCapacity(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(EnsureEntity<TComp>(pair.Key, callerEntity), EnsureEntity<TComp>(pair.Value, callerEntity));
}
}
public void EnsureEntityDictionary<TComp>(Dictionary<NetEntity, NetEntity?> netEntities, EntityUid callerEntity,
Dictionary<EntityUid, EntityUid?> entities)
{
entities.Clear();
entities.EnsureCapacity(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(EnsureEntity<TComp>(pair.Key, callerEntity), EnsureEntity<TComp>(pair.Value, callerEntity));
}
}
/// <inheritdoc />
public List<EntityUid> GetEntityList(ICollection<NetEntity> netEntities)
{
@@ -467,6 +593,71 @@ public partial class EntityManager
return netEntities;
}
/// <inheritdoc />
public Dictionary<NetEntity, T> GetNetEntityDictionary<T>(Dictionary<EntityUid, T> entities)
{
var netEntities = new Dictionary<NetEntity, T>(entities.Count);
foreach (var pair in entities)
{
netEntities.Add(GetNetEntity(pair.Key), pair.Value);
}
return netEntities;
}
/// <inheritdoc />
public Dictionary<T, NetEntity> GetNetEntityDictionary<T>(Dictionary<T, EntityUid> entities) where T : notnull
{
var netEntities = new Dictionary<T, NetEntity>(entities.Count);
foreach (var pair in entities)
{
netEntities.Add(pair.Key, GetNetEntity(pair.Value));
}
return netEntities;
}
/// <inheritdoc />
public Dictionary<T, NetEntity?> GetNetEntityDictionary<T>(Dictionary<T, EntityUid?> entities) where T : notnull
{
var netEntities = new Dictionary<T, NetEntity?>(entities.Count);
foreach (var pair in entities)
{
netEntities.Add(pair.Key, GetNetEntity(pair.Value));
}
return netEntities;
}
/// <inheritdoc />
public Dictionary<NetEntity, NetEntity> GetNetEntityDictionary(Dictionary<EntityUid, EntityUid> entities)
{
var netEntities = new Dictionary<NetEntity, NetEntity>(entities.Count);
foreach (var pair in entities)
{
netEntities.Add(GetNetEntity(pair.Key), GetNetEntity(pair.Value));
}
return netEntities;
}
/// <inheritdoc />
public Dictionary<NetEntity, NetEntity?> GetNetEntityDictionary(Dictionary<EntityUid, EntityUid?> entities)
{
var netEntities = new Dictionary<NetEntity, NetEntity?>(entities.Count);
foreach (var pair in entities)
{
netEntities.Add(GetNetEntity(pair.Key), GetNetEntity(pair.Value));
}
return netEntities;
}
/// <inheritdoc />
public HashSet<EntityCoordinates> GetEntitySet(HashSet<NetCoordinates> netEntities)
{

View File

@@ -906,6 +906,16 @@ namespace Robust.Shared.GameObjects
DebugTools.Assert("Why are you raising predictive events on the server?");
}
/// <summary>
/// Raises an event locally on client or networked on server.
/// </summary>
public abstract void RaiseSharedEvent<T>(T message, EntityUid? user = null) where T : EntityEventArgs;
/// <summary>
/// Raises an event locally on client or networked on server.
/// </summary>
public abstract void RaiseSharedEvent<T>(T message, ICommonSession? user = null) where T : EntityEventArgs;
/// <summary>
/// Factory for generating a new EntityUid for an entity currently being created.
/// </summary>

View File

@@ -1049,6 +1049,42 @@ public partial class EntitySystem
EntityManager.EnsureEntityList<T>(netEntities, callerEntity, entities);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void EnsureEntityDictionary<TComp, TValue>(Dictionary<NetEntity, TValue> netEntities, EntityUid callerEntity, Dictionary<EntityUid, TValue> entities)
{
EntityManager.EnsureEntityDictionary<TComp, TValue>(netEntities, callerEntity, entities);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void EnsureEntityDictionaryNullableValue<TComp, TValue>(Dictionary<NetEntity, TValue?> netEntities, EntityUid callerEntity, Dictionary<EntityUid, TValue?> entities)
{
EntityManager.EnsureEntityDictionaryNullableValue<TComp, TValue>(netEntities, callerEntity, entities);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void EnsureEntityDictionary<TComp, TKey>(Dictionary<TKey, NetEntity> netEntities, EntityUid callerEntity, Dictionary<TKey, EntityUid> entities) where TKey : notnull
{
EntityManager.EnsureEntityDictionary<TComp, TKey>(netEntities, callerEntity, entities);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void EnsureEntityDictionary<TComp, TKey>(Dictionary<TKey, NetEntity?> netEntities, EntityUid callerEntity, Dictionary<TKey, EntityUid?> entities) where TKey : notnull
{
EntityManager.EnsureEntityDictionary<TComp, TKey>(netEntities, callerEntity, entities);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void EnsureEntityDictionary<TComp>(Dictionary<NetEntity, NetEntity> netEntities, EntityUid callerEntity, Dictionary<EntityUid, EntityUid> entities)
{
EntityManager.EnsureEntityDictionary<TComp>(netEntities, callerEntity, entities);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void EnsureEntityDictionary<TComp>(Dictionary<NetEntity, NetEntity?> netEntities, EntityUid callerEntity, Dictionary<EntityUid, EntityUid?> entities)
{
EntityManager.EnsureEntityDictionary<TComp>(netEntities, callerEntity, entities);
}
/// <summary>
/// Returns the <see cref="EntityUid"/> of a <see cref="NetEntity"/>. Returns <see cref="EntityUid.Invalid"/> if it doesn't exist.
/// </summary>
@@ -1184,6 +1220,96 @@ public partial class EntitySystem
return EntityManager.GetEntityArray(netEntities);
}
/// <summary>
/// Returns the <see cref="NetEntity"/> versions of the supplied entities. Logs an error if the entities do not exist.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Dictionary<NetEntity, T> GetNetEntityDictionary<T>(Dictionary<EntityUid, T> uids)
{
return EntityManager.GetNetEntityDictionary(uids);
}
/// <summary>
/// Returns the <see cref="NetEntity"/> versions of the supplied entities. Logs an error if the entities do not exist.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Dictionary<T, NetEntity> GetNetEntityDictionary<T>(Dictionary<T, EntityUid> uids) where T : notnull
{
return EntityManager.GetNetEntityDictionary(uids);
}
/// <summary>
/// Returns the <see cref="NetEntity"/> versions of the supplied entities. Logs an error if the entities do not exist.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Dictionary<T, NetEntity?> GetNetEntityDictionary<T>(Dictionary<T, EntityUid?> uids) where T : notnull
{
return EntityManager.GetNetEntityDictionary(uids);
}
/// <summary>
/// Returns the <see cref="NetEntity"/> versions of the supplied entities. Logs an error if the entities do not exist.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Dictionary<NetEntity, NetEntity> GetNetEntityDictionary(Dictionary<EntityUid, EntityUid> uids)
{
return EntityManager.GetNetEntityDictionary(uids);
}
/// <summary>
/// Returns the <see cref="NetEntity"/> versions of the supplied entities. Logs an error if the entities do not exist.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Dictionary<NetEntity, NetEntity?> GetNetEntityDictionary(Dictionary<EntityUid, EntityUid?> uids)
{
return EntityManager.GetNetEntityDictionary(uids);
}
/// <summary>
/// Returns the <see cref="EntityUid"/> versions of the supplied <see cref="NetEntity"/>. Returns <see cref="EntityUid.Invalid"/> if it doesn't exist.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Dictionary<EntityUid, T> GetEntityDictionary<T>(Dictionary<NetEntity, T> uids)
{
return EntityManager.GetEntityDictionary(uids);
}
/// <summary>
/// Returns the <see cref="EntityUid"/> versions of the supplied <see cref="NetEntity"/>. Returns <see cref="EntityUid.Invalid"/> if it doesn't exist.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Dictionary<T, EntityUid> GetEntityDictionary<T>(Dictionary<T, NetEntity> uids) where T : notnull
{
return EntityManager.GetEntityDictionary(uids);
}
/// <summary>
/// Returns the <see cref="EntityUid"/> versions of the supplied <see cref="NetEntity"/>. Returns <see cref="EntityUid.Invalid"/> if it doesn't exist.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Dictionary<T, EntityUid?> GetEntityDictionary<T>(Dictionary<T, NetEntity?> uids) where T : notnull
{
return EntityManager.GetEntityDictionary(uids);
}
/// <summary>
/// Returns the <see cref="EntityUid"/> versions of the supplied <see cref="NetEntity"/>. Returns <see cref="EntityUid.Invalid"/> if it doesn't exist.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Dictionary<EntityUid, EntityUid> GetEntityDictionary(Dictionary<NetEntity, NetEntity> uids)
{
return EntityManager.GetEntityDictionary(uids);
}
/// <summary>
/// Returns the <see cref="EntityUid"/> versions of the supplied <see cref="NetEntity"/>. Returns <see cref="EntityUid.Invalid"/> if it doesn't exist.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Dictionary<EntityUid, EntityUid?> GetEntityDictionary(Dictionary<NetEntity, NetEntity?> uids)
{
return EntityManager.GetEntityDictionary(uids);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected NetCoordinates GetNetCoordinates(EntityCoordinates coordinates, MetaDataComponent? metadata = null)
{

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Map;
@@ -107,6 +106,16 @@ public partial interface IEntityManager
/// </summary>
EntityUid?[] GetEntityArray(NetEntity?[] netEntities);
/// <summary>
/// Dictionary version of <see cref="GetEntity"/>
/// </summary>
Dictionary<EntityUid, T> GetEntityDictionary<T>(Dictionary<NetEntity, T> netEntities);
/// <summary>
/// Dictionary version of <see cref="GetEntity"/>
/// </summary>
Dictionary<T, EntityUid> GetEntityDictionary<T>(Dictionary<T, NetEntity> netEntities) where T : notnull;
/// <summary>
/// HashSet version of <see cref="GetNetEntity"/>
/// </summary>
@@ -117,6 +126,11 @@ public partial interface IEntityManager
/// </summary>
public List<NetEntity> GetNetEntityList(List<EntityUid> entities);
/// <summary>
/// List version of <see cref="GetNetEntity"/>
/// </summary>
List<NetEntity> GetNetEntityList(IReadOnlyList<EntityUid> entities);
/// <summary>
/// List version of <see cref="GetNetEntity"/>
/// </summary>
@@ -128,15 +142,40 @@ public partial interface IEntityManager
public List<NetEntity?> GetNetEntityList(List<EntityUid?> entities);
/// <summary>
/// List version of <see cref="GetNetEntity"/>
/// Array version of <see cref="GetNetEntity"/>
/// </summary>
NetEntity[] GetNetEntityArray(EntityUid[] entities);
/// <summary>
/// List version of <see cref="GetNetEntity"/>
/// Array version of <see cref="GetNetEntity"/>
/// </summary>
NetEntity?[] GetNetEntityArray(EntityUid?[] entities);
/// <summary>
/// Dictionary version of <see cref="GetNetEntity"/>
/// </summary>
Dictionary<NetEntity, T> GetNetEntityDictionary<T>(Dictionary<EntityUid, T> entities);
/// <summary>
/// Dictionary version of <see cref="GetNetEntity"/>
/// </summary>
Dictionary<T, NetEntity> GetNetEntityDictionary<T>(Dictionary<T, EntityUid> entities) where T : notnull;
/// <summary>
/// Dictionary version of <see cref="GetNetEntity"/>
/// </summary>
Dictionary<T, NetEntity?> GetNetEntityDictionary<T>(Dictionary<T, EntityUid?> entities) where T : notnull;
/// <summary>
/// Dictionary version of <see cref="GetNetEntity"/>
/// </summary>
Dictionary<NetEntity, NetEntity> GetNetEntityDictionary(Dictionary<EntityUid, EntityUid> entities);
/// <summary>
/// Dictionary version of <see cref="GetNetEntity"/>
/// </summary>
Dictionary<NetEntity, NetEntity?> GetNetEntityDictionary(Dictionary<EntityUid, EntityUid?> entities);
/// <summary>
/// Returns the corresponding <see cref="NetCoordinates"/> for the specified local coordinates.
/// </summary>
@@ -178,6 +217,27 @@ public partial interface IEntityManager
public List<EntityUid> EnsureEntityList<T>(List<NetEntity> netEntities, EntityUid callerEntity);
void EnsureEntityList<T>(List<NetEntity> netEntities, EntityUid callerEntity, List<EntityUid> entities);
void EnsureEntityDictionary<TComp, TValue>(Dictionary<NetEntity, TValue> netEntities, EntityUid callerEntity,
Dictionary<EntityUid, TValue> entities);
void EnsureEntityDictionaryNullableValue<TComp, TValue>(Dictionary<NetEntity, TValue?> netEntities,
EntityUid callerEntity,
Dictionary<EntityUid, TValue?> entities);
void EnsureEntityDictionary<TComp, TKey>(Dictionary<TKey, NetEntity> netEntities, EntityUid callerEntity,
Dictionary<TKey, EntityUid> entities) where TKey : notnull;
void EnsureEntityDictionary<TComp, TKey>(Dictionary<TKey, NetEntity?> netEntities, EntityUid callerEntity,
Dictionary<TKey, EntityUid?> entities) where TKey : notnull;
void EnsureEntityDictionary<TComp>(Dictionary<NetEntity, NetEntity> netEntities, EntityUid callerEntity,
Dictionary<EntityUid, EntityUid> entities);
void EnsureEntityDictionary<TComp>(Dictionary<NetEntity, NetEntity?> netEntities, EntityUid callerEntity,
Dictionary<EntityUid, EntityUid?> entities);
public List<EntityCoordinates> GetEntityList(ICollection<NetCoordinates> netEntities);
public List<EntityCoordinates?> GetEntityList(List<NetCoordinates?> netEntities);

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@@ -744,9 +745,20 @@ public sealed partial class EntityLookupSystem
}
public HashSet<EntityUid> GetEntitiesInRange(EntityCoordinates coordinates, float range, LookupFlags flags = DefaultFlags)
{
var ents = new HashSet<EntityUid>();
GetEntitiesInRange(coordinates, range, ents, flags);
return ents;
}
public void GetEntitiesInRange(EntityCoordinates coordinates, float range, HashSet<EntityUid> entities, LookupFlags flags = DefaultFlags)
{
var mapPos = coordinates.ToMap(EntityManager, _transform);
return GetEntitiesInRange(mapPos, range, flags);
if (mapPos.MapId == MapId.Nullspace)
return;
GetEntitiesInRange(mapPos.MapId, mapPos.Position, range, entities, flags);
}
#endregion

View File

@@ -127,7 +127,31 @@ public abstract partial class SharedMapSystem
SubscribeLocalEvent<MapGridComponent, MoveEvent>(OnGridMove);
}
public void OnGridBoundsChange(EntityUid uid, MapGridComponent component)
/// <summary>
/// <see cref="GetGridPosition(Robust.Shared.GameObjects.Entity{Robust.Shared.Physics.Components.PhysicsComponent?},System.Numerics.Vector2,Robust.Shared.Maths.Angle)"/>
/// </summary>
public Vector2 GetGridPosition(Entity<PhysicsComponent?> grid, Vector2 worldPos, Angle worldRot)
{
if (!Resolve(grid.Owner, ref grid.Comp))
return Vector2.Zero;
return worldPos + worldRot.RotateVec(grid.Comp.LocalCenter);
}
/// <summary>
/// Gets the mapgrid's position considering its local physics center.
/// </summary>
public Vector2 GetGridPosition(Entity<PhysicsComponent?, TransformComponent?> grid)
{
if (!Resolve(grid.Owner, ref grid.Comp1, ref grid.Comp2))
return Vector2.Zero;
var (worldPos, worldRot) = _transform.GetWorldPositionRotation(grid.Comp2);
return GetGridPosition((grid.Owner, grid.Comp1), worldPos, worldRot);
}
private void OnGridBoundsChange(EntityUid uid, MapGridComponent component)
{
// Just MapLoader things.
if (component.MapProxy == DynamicTree.Proxy.Free) return;
@@ -193,7 +217,7 @@ public abstract partial class SharedMapSystem
if (xform.ParentUid != xform.MapUid && meta.EntityLifeStage < EntityLifeStage.Terminating && _netManager.IsServer)
{
Log.Error($"Grid {ToPrettyString(uid, meta)} it not parented to a map. y'all need jesus. {Environment.StackTrace}");
Log.Error($"Grid {ToPrettyString(uid, meta)} is not parented to {ToPrettyString(xform._parent)} which is not a map. y'all need jesus. {Environment.StackTrace}");
return;
}
@@ -974,19 +998,34 @@ public abstract partial class SharedMapSystem
internal ChunkEnumerator GetMapChunks(EntityUid uid, MapGridComponent grid, Box2 worldAABB)
{
var localAABB = _transform.GetInvWorldMatrix(uid).TransformBox(worldAABB);
return new ChunkEnumerator(grid.Chunks, localAABB, grid.ChunkSize);
return GetLocalMapChunks(uid, grid, localAABB);
}
internal ChunkEnumerator GetMapChunks(EntityUid uid, MapGridComponent grid, Box2Rotated worldArea)
{
var matrix = _transform.GetInvWorldMatrix(uid);
var localArea = matrix.TransformBox(worldArea);
return new ChunkEnumerator(grid.Chunks, localArea, grid.ChunkSize);
return GetLocalMapChunks(uid, grid, localArea);
}
internal ChunkEnumerator GetLocalMapChunks(EntityUid uid, MapGridComponent grid, Box2 localAABB)
{
return new ChunkEnumerator(grid.Chunks, localAABB, grid.ChunkSize);
Box2 compAABB;
// The entire area intersects.
if (_mapQuery.HasComponent(uid))
{
compAABB = localAABB;
}
else
{
compAABB = grid.LocalAABB.Intersect(localAABB);
}
if (compAABB.IsEmpty())
return ChunkEnumerator.Empty;
return new ChunkEnumerator(grid.Chunks, compAABB, grid.ChunkSize);
}
#endregion ChunkAccess
@@ -1348,8 +1387,36 @@ public abstract partial class SharedMapSystem
public EntityCoordinates GridTileToLocal(EntityUid uid, MapGridComponent grid, Vector2i gridTile)
{
return new(uid,
new Vector2(gridTile.X * grid.TileSize + (grid.TileSize / 2f), gridTile.Y * grid.TileSize + (grid.TileSize / 2f)));
var position = TileCenterToVector(uid, grid, gridTile);
return new(uid, position);
}
/// <summary>
/// Turns a gridtile origin into a Vector2, accounting for tile size.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector2 TileToVector(Entity<MapGridComponent> grid, Vector2i gridTile)
{
return new Vector2(gridTile.X * grid.Comp.TileSize, gridTile.Y * grid.Comp.TileSize);
}
/// <summary>
/// Turns a gridtile center into a Vector2, accounting for tile size.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector2 TileCenterToVector(EntityUid uid, MapGridComponent grid, Vector2i gridTile)
{
return TileCenterToVector((uid, grid), gridTile);
}
/// <summary>
/// Turns a gridtile center into a Vector2, accounting for tile size.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector2 TileCenterToVector(Entity<MapGridComponent> grid, Vector2i gridTile)
{
return new Vector2(gridTile.X * grid.Comp.TileSize, gridTile.Y * grid.Comp.TileSize) + grid.Comp.TileSizeHalfVector;
}
public Vector2 GridTileToWorldPos(EntityUid uid, MapGridComponent grid, Vector2i gridTile)

View File

@@ -616,7 +616,7 @@ public abstract partial class SharedTransformSystem
TransformComponent? newParent = null,
TransformComponent? oldParent = null)
{
SetCoordinates((uid, xform, MetaData(uid)), value, rotation, unanchor, newParent, oldParent);
SetCoordinates((uid, xform, _metaQuery.GetComponent(uid)), value, rotation, unanchor, newParent, oldParent);
}
#endregion

View File

@@ -1,5 +1,6 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using Robust.Shared.Map;
namespace Robust.Shared.GameObjects;
@@ -67,6 +68,16 @@ public abstract partial class SharedTransformSystem
return new MapCoordinates(worldPos, xform.MapID);
}
/// <summary>
/// Converts entity-local coordinates into map terms.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MapCoordinates ToMapCoordinates(NetCoordinates coordinates)
{
var eCoords = GetCoordinates(coordinates);
return ToMapCoordinates(eCoords);
}
/// <summary>
/// Creates EntityCoordinates given an entity and some MapCoordinates.
/// </summary>

View File

@@ -1,12 +1,19 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
namespace Robust.Shared.Map.Enumerators;
internal struct ChunkEnumerator
{
/// <summary>
/// An empty enumerator that will return nothing.
/// </summary>
public static ChunkEnumerator Empty => new(new Dictionary<Vector2i, MapChunk>(), Box2.Empty, 1);
private Dictionary<Vector2i, MapChunk> _chunks;
private Vector2i _chunkLB;
private Vector2i _chunkRT;

View File

@@ -0,0 +1,59 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Robust.Shared.Maths;
namespace Robust.Shared.Map.Enumerators;
/// <summary>
/// Generic iterator for chunk indices for the specified bounds with the specified chunk size.
/// </summary>
public struct ChunkIndicesEnumerator
{
private readonly Vector2i _chunkLB;
private readonly Vector2i _chunkRT;
private int _xIndex;
private int _yIndex;
public ChunkIndicesEnumerator(Vector2 viewPos, float range, float chunkSize)
{
var rangeVec = new Vector2(range, range);
_chunkLB = ((viewPos - rangeVec) / chunkSize).Floored();
// Also floor this as we get the whole chunk anyway.
_chunkRT = ((viewPos + rangeVec) / chunkSize).Floored();
_xIndex = _chunkLB.X;
_yIndex = _chunkLB.Y;
}
public ChunkIndicesEnumerator(Box2 localAABB, int chunkSize)
{
_chunkLB = (localAABB.BottomLeft / chunkSize).Floored();
_chunkRT = (localAABB.TopRight / chunkSize).Floored();
_xIndex = _chunkLB.X;
_yIndex = _chunkLB.Y;
}
public bool MoveNext([NotNullWhen(true)] out Vector2i? indices)
{
if (_yIndex > _chunkRT.Y)
{
_yIndex = _chunkLB.Y;
_xIndex++;
}
if (_xIndex > _chunkRT.X)
{
indices = null;
return false;
}
indices = new Vector2i(_xIndex, _yIndex);
_yIndex++;
return true;
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace Robust.Shared.Network
{
@@ -31,9 +30,10 @@ namespace Robust.Shared.Network
/// </summary>
public sealed class NetConnectingArgs : EventArgs
{
public bool IsDenied => DenyReason != null;
public bool IsDenied => DenyReasonData != null;
public string? DenyReason { get; private set; }
public string? DenyReason => DenyReasonData?.Text;
public NetDenyReason? DenyReasonData { get; private set; }
public NetUserData UserData { get; }
@@ -48,7 +48,12 @@ namespace Robust.Shared.Network
public void Deny(string reason)
{
DenyReason = reason;
Deny(new NetDenyReason(reason));
}
public void Deny(NetDenyReason reason)
{
DenyReasonData = reason;
}
/// <summary>
@@ -65,12 +70,29 @@ namespace Robust.Shared.Network
}
}
/// <summary>
/// Contains a reason for denying a client connection to the game server.
/// </summary>
/// <param name="Text">The textual reason, presented to the user.</param>
/// <param name="AdditionalProperties">
/// Additional JSON properties that will be included in the <see cref="NetDisconnectMessage"/>.
/// Valid value types are: string, int, float, bool.
/// </param>
/// <seealso cref="NetDisconnectMessage"/>
/// <seealso cref="NetConnectingArgs"/>
public record NetDenyReason(string Text, Dictionary<string, object> AdditionalProperties)
{
public NetDenyReason(string Text) : this(Text, new Dictionary<string, object>())
{
}
}
/// <summary>
/// Structured reason common interface.
/// </summary>
public interface INetStructuredReason
{
JsonObject StructuredReason { get; }
NetDisconnectMessage Message { get; }
string Reason { get; }
bool RedialFlag { get; }
}
@@ -80,33 +102,33 @@ namespace Robust.Shared.Network
/// </summary>
public sealed class NetConnectFailArgs : EventArgs, INetStructuredReason
{
public NetConnectFailArgs(string reason) : this(NetStructuredDisconnectMessages.Decode(reason))
public NetConnectFailArgs(string reason) : this(NetDisconnectMessage.Decode(reason))
{
}
public NetConnectFailArgs(JsonObject reason)
internal NetConnectFailArgs(NetDisconnectMessage reason)
{
StructuredReason = reason;
Message = reason;
}
public JsonObject StructuredReason { get; }
public string Reason => NetStructuredDisconnectMessages.ReasonOf(StructuredReason);
public bool RedialFlag => NetStructuredDisconnectMessages.RedialFlagOf(StructuredReason);
public NetDisconnectMessage Message { get; }
public string Reason => Message.Reason;
public bool RedialFlag => Message.RedialFlag;
}
public sealed class NetDisconnectedArgs : NetChannelArgs, INetStructuredReason
{
public NetDisconnectedArgs(INetChannel channel, string reason) : this(channel, NetStructuredDisconnectMessages.Decode(reason))
public NetDisconnectedArgs(INetChannel channel, string reason) : this(channel, NetDisconnectMessage.Decode(reason))
{
}
public NetDisconnectedArgs(INetChannel channel, JsonObject reason) : base(channel)
internal NetDisconnectedArgs(INetChannel channel, NetDisconnectMessage reason) : base(channel)
{
StructuredReason = reason;
Message = reason;
}
public JsonObject StructuredReason { get; }
public string Reason => NetStructuredDisconnectMessages.ReasonOf(StructuredReason);
public bool RedialFlag => NetStructuredDisconnectMessages.RedialFlagOf(StructuredReason);
public NetDisconnectMessage Message { get; }
public string Reason => Message.Reason;
public bool RedialFlag => Message.RedialFlag;
}
}

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