Compare commits

...

88 Commits

Author SHA1 Message Date
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
Leon Friedrich
f8cb1729a3 Version: 210.1.0 2024-02-16 11:21:40 +13:00
Leon Friedrich
fd9d5c8aa8 Make ButtonGroup setter behaviour consistent with comment 2024-02-16 11:19:14 +13:00
Pieter-Jan Briers
4677296934 Add IsNoneSetAllowed mode to ButtonGroup.
This allows a button group to have no button pressed by default, which is the behavior of most radio buttons.
2024-02-15 01:16:33 +01:00
Pieter-Jan Briers
708f5dd376 Un-hardcode C:\Windows in MidiManager
Keeping an eye out for our bros who put Windows on a drive that isn't C:
2024-02-14 14:32:05 +01:00
Pieter-Jan Briers
4a06acda32 NetUserId implement ISelfSerialize
For @VasilisThePikachu
2024-02-14 01:08:38 +01:00
Pieter-Jan Briers
e7beb2032b Version: 210.0.3
This version changes nothing but I need it because I pushed the previous
version after amending it which means the tag is the wrong commit SORRY.
2024-02-13 16:11:47 +01:00
Pieter-Jan Briers
c7bd75f800 Version: 210.0.2 2024-02-13 16:06:28 +01:00
Pieter-Jan Briers
b4165e8661 ALSO revert changes to TextureRect from #4841
Stretch modes are broken or something, SS14 lobby art looks wrong. Can't be arsed to debug it myself.
2024-02-13 16:05:46 +01:00
Pieter-Jan Briers
ad339b5bfd Version: 210.0.1 2024-02-13 15:30:20 +01:00
Pieter-Jan Briers
e1197af8ce Revert changes to TextureButton from #4841
Breaks SS14 stylesheets due to not responding to style properties anymore.

At least one of those seems to be unfixable (ModulateSelf usage) which makes me think we should just deprecate ModulateSelf instead. However I'm not fixing that here.
2024-02-13 15:29:46 +01:00
metalgearsloth
102cadf3a6 Version: 210.0.0 2024-02-13 18:26:01 +11:00
Hannah Giovanna Dawson
e7723b61bc Add UnicodeRange to sandbox (#4894)
* Add GetEncoding to sandbox (#4892)
Need this struct allowlisted to for nice unicode sanitization.

* Add UnicodeRanges too

* Changelog

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-02-11 17:22:11 +01:00
Moony
a9d17337a3 RT Patches for UI improvements (#4841)
* fix up buttons

* wah

* ough

* huhwuhuahsdhsfdj

* loud incorrect buzzer

* wawa

* Allow XmlnsDefinition

* wawa

* Release notes.

* Expose keybind loading.

* address reviews and other things

---------

Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-02-11 17:18:39 +01:00
Pieter-Jan Briers
74622bac83 Add DateTimeSerializer 2024-02-11 16:40:52 +01:00
Pieter-Jan Briers
a3047b1687 More warning fixes 2024-02-11 15:51:07 +01:00
Pieter-Jan Briers
3a55118143 Replace CVar OnValueChanged in systems with Subs.CVar 2024-02-11 13:29:27 +01:00
Pieter-Jan Briers
3c5fbc648a Add Subs.CVar helper for subscribing to CVar changes from entity systems 2024-02-11 13:29:27 +01:00
Pieter-Jan Briers
f9c39bce0b Use ValueList for EntitySystem subscriptions list.
That's a hundred something lists just gone and I don't have to ??=
2024-02-11 13:29:27 +01:00
Hannah Giovanna Dawson
0e8c803c0f Add GetEncoding to sandbox (#4892) 2024-02-10 17:59:13 +01:00
c4llv07e
6bb7b88c69 Save discord rich presense to the user config (#4884)
Signed-off-by: c4llv07e <kseandi@gmail.com>
2024-02-02 00:36:59 +01:00
155 changed files with 5204 additions and 792 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 -->
<!-- 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,231 @@ END TEMPLATE-->
*None yet*
## 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
* `NetUserId` implements `ISelfSerialize` so can be used in data fields.
* `ButtonGroup.IsNoneSetAllowed` to allow a button group to have no buttons pressed by default.
## 210.0.3
## 210.0.2
### Bugfixes
* Revert changes to `TextureRect` too.
## 210.0.1
### Bugfixes
* Revert changes to `TextureButton` that broke style property handling.
## 210.0.0
### New features
* Controls can now hook before, after, and during rendering of their children.
* IRenderHandle is now a public API, with the caveat that it's properties and methods are unstable.
* ButtonGroup now exposes what buttons it contains, alongside which is currently pressed.
* OptionButton has additional styleclasses, and has a hook for modifying it's internal buttons.
* PanelContainer.GetStyleBox() is now protected rather than private.
* TextureButton now uses a TextureRect instead of custom drawing code.
* TextureRect has additional style properties exposed.
* A new property, TextureSizeTarget, was added, which allows specifying a size in virtual pixels that the control should attempt to draw at.
* Stretch mode is now a style property.
* Scale is now a style property.
* Avalonia.Metadata.XmlnsDefinitionAttribute is now permitted by the sandbox.
* Add MaxDimension property to Box2 to return the higher of the Width or Height.
* Add GetLocalPosition to convert ScreenCoordinates to coordinates relative to the control. Ignores window.
* Add GlobalRect and GlobalPixelRect for controls to get their UIBox2i in screen terms.
* Add dotted line drawing to DrawingHandleScreen.
* You can use `Subs.CVar()` from an entity systems to subscribe to CVar changes. This is more convenient than `IConfigurationManager.OnValueChanged` as it automatically unsubscribes on system shutdown.
* There is now a built-in type serializer for `DateTime`, so you can put `DateTime`s in your data fields.
* `System.Text.Unicode.UnicodeRange` and `UnicodeRanges` are now available in the sandbox.
### Bugfixes
* UI drawing now properly accounts for a control's draw routine potentially mangling the current matrix.
* UI roots now properly update when the global stylesheet is changed. They previously only did so if they had a dedicated stylesheet (which is the one case where they would be unaffected by a global sheet update.
## 209.0.1
### Bugfixes

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

@@ -39,7 +39,7 @@ public sealed class AudioOverlay : Overlay
protected internal override void Draw(in OverlayDrawArgs args)
{
var localPlayer = _playerManager.LocalPlayer?.ControlledEntity;
var localPlayer = _playerManager.LocalEntity;
if (args.ViewportControl == null || localPlayer == null)
return;

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;
@@ -106,8 +108,9 @@ public sealed partial class AudioSystem : SharedAudioSystem
SubscribeNetworkEvent<PlayAudioEntityMessage>(OnEntityAudio);
SubscribeNetworkEvent<PlayAudioPositionalMessage>(OnEntityCoordinates);
CfgManager.OnValueChanged(CVars.AudioAttenuation, OnAudioAttenuation, true);
CfgManager.OnValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
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)
@@ -133,13 +136,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
_audio.SetMasterGain(value);
}
public override void Shutdown()
{
CfgManager.UnsubValueChanged(CVars.AudioAttenuation, OnAudioAttenuation);
CfgManager.UnsubValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged);
base.Shutdown();
}
private void OnAudioPaused(EntityUid uid, AudioComponent component, ref EntityPausedEvent args)
{
component.Pause();
@@ -170,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;
@@ -209,6 +216,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
// Breaks with prediction?
component.Source.Dispose();
RemoveAudioLimit(component.FileName);
}
private void OnAudioAttenuation(int obj)
@@ -583,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;
}
}
@@ -628,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

@@ -114,7 +114,7 @@ internal sealed partial class MidiManager : IMidiManager
"/usr/share/sounds/sf2/TimGM6mb.sf2",
};
private const string WindowsSoundfont = @"C:\WINDOWS\system32\drivers\gm.dls";
private static readonly string WindowsSoundfont = $@"{Environment.GetEnvironmentVariable("SystemRoot")}\system32\drivers\gm.dls";
private const string OsxSoundfont =
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";

View File

@@ -229,7 +229,7 @@ namespace Robust.Client
// Don't invoke PlayerLeaveServer if PlayerJoinedServer & GameStartedSetup hasn't been called yet.
if (RunLevel > ClientRunLevel.Connecting)
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalPlayer?.Session));
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalSession));
LastDisconnectReason = args.Reason;
GameStoppedReset();

View File

@@ -188,7 +188,7 @@ namespace Robust.Client.Console
}
args.RemoveAt(0);
var shell = new ConsoleShell(this, session ?? _player.LocalPlayer?.Session, session == null);
var shell = new ConsoleShell(this, session ?? _player.LocalSession, session == null);
var cmdArgs = args.ToArray();
AnyCommandExecuted?.Invoke(shell, commandName, command, cmdArgs);
@@ -200,8 +200,7 @@ namespace Robust.Client.Console
// When not connected to a server, you can run all local commands.
// When connected to a server, you can only run commands according to the con group controller.
return _player.LocalPlayer == null
|| _player.LocalPlayer.Session.Status <= SessionStatus.Connecting
return _player.LocalSession is not { Status: > SessionStatus.Connecting }
|| _conGroup.CanCommand(cmdName);
}

View File

@@ -16,8 +16,7 @@ namespace Robust.Client.Console.Commands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var controlled = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
if (controlled == EntityUid.Invalid)
if (_playerManager.LocalEntity is not { } controlled)
{
shell.WriteLine("You don't have an attached entity.");
return;

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

@@ -420,7 +420,7 @@ namespace Robust.Client.Debugging
if (mapPos.MapId != args.MapId)
return;
var player = _playerManager.LocalPlayer?.ControlledEntity;
var player = _playerManager.LocalEntity;
if (!_entityManager.TryGetComponent<TransformComponent>(player, out var playerXform) ||
playerXform.MapID != args.MapId)

View File

@@ -42,6 +42,8 @@ public sealed partial class ClientEntityManager
var pending = PendingNetEntityStates.GetOrNew(nEntity);
pending.Add((typeof(T), callerEntity));
return entity.Item1;
}

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;
@@ -221,7 +240,7 @@ namespace Robust.Client.GameObjects
public void DispatchReceivedNetworkMsg(EntityEventArgs msg)
{
var sessionType = typeof(EntitySessionMessage<>).MakeGenericType(msg.GetType());
var sessionMsg = Activator.CreateInstance(sessionType, new EntitySessionEventArgs(_playerManager.LocalPlayer!.Session), msg)!;
var sessionMsg = Activator.CreateInstance(sessionType, new EntitySessionEventArgs(_playerManager.LocalSession!), msg)!;
ReceivedSystemMessage?.Invoke(this, msg);
ReceivedSystemMessage?.Invoke(this, sessionMsg);
}

View File

@@ -105,12 +105,10 @@ namespace Robust.Client.GameObjects
/// <param name="inputCmd">Input command to handle as predicted.</param>
public void PredictInputCommand(IFullInputCmdMessage inputCmd)
{
DebugTools.AssertNotNull(_playerManager.LocalPlayer);
var keyFunc = _inputManager.NetworkBindMap.KeyFunctionName(inputCmd.InputFunctionId);
Predicted = true;
var session = _playerManager.LocalPlayer!.Session;
var session = _playerManager.LocalSession;
foreach (var handler in BindRegistry.GetHandlers(keyFunc))
{
if (handler.HandleCmdMessage(EntityManager, session, inputCmd))
@@ -145,27 +143,22 @@ namespace Robust.Client.GameObjects
private void GenerateInputCommand(IConsoleShell shell, string argstr, string[] args)
{
var localPlayer = _playerManager.LocalPlayer;
if(localPlayer is null)
return;
var pent = localPlayer.ControlledEntity;
if(pent is null)
if (_playerManager.LocalEntity is not { } pent)
return;
BoundKeyFunction keyFunction = new BoundKeyFunction(args[0]);
BoundKeyState state = args[1] == "u" ? BoundKeyState.Up: BoundKeyState.Down;
var pxform = Transform(pent.Value);
var pxform = Transform(pent);
var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3]));
var coords = EntityCoordinates.FromMap(EntityManager, pent.Value, new MapCoordinates(wPos, pxform.MapID));
var coords = EntityCoordinates.FromMap(EntityManager, pent, new MapCoordinates(wPos, pxform.MapID));
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);
var message = new FullInputCmdMessage(_timing.CurTick, _timing.TickFraction, funcId, state,
GetNetCoordinates(coords), new ScreenCoordinates(0, 0, default), NetEntity.Invalid);
HandleInputCommand(localPlayer.Session, keyFunction, message);
HandleInputCommand(_playerManager.LocalSession, keyFunction, message);
}
private void OnAttachedEntityChanged(LocalPlayerAttachedEvent message)
@@ -208,11 +201,8 @@ namespace Robust.Client.GameObjects
/// </summary>
public void SetEntityContextActive()
{
var controlled = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
if (controlled == EntityUid.Invalid)
{
if (_playerManager.LocalEntity is not { } controlled)
return;
}
SetEntityContextActive(_inputManager, controlled);
}

View File

@@ -62,7 +62,7 @@ namespace Robust.Client.GameObjects
SubscribeLocalEvent<SpriteComponent, SpriteUpdateInertEvent>(QueueUpdateInert);
SubscribeLocalEvent<SpriteComponent, ComponentInit>(OnInit);
_cfg.OnValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
_sawmill = _logManager.GetSawmill("sprite");
}
@@ -72,12 +72,6 @@ namespace Robust.Client.GameObjects
QueueUpdateInert(uid, component);
}
public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged);
}
private void OnBiasChanged(double value)
{
SpriteComponent.DirectionBias = value;

View File

@@ -29,10 +29,7 @@ namespace Robust.Client.GameObjects
var uiKey = ev.UiKey;
var message = ev.Message;
// This should probably not happen at this point, but better make extra sure!
if (_playerManager.LocalPlayer != null)
message.Session = _playerManager.LocalPlayer.Session;
message.Session = _playerManager.LocalSession!;
message.Entity = GetNetEntity(uid);
message.UiKey = uiKey;
@@ -75,8 +72,7 @@ namespace Robust.Client.GameObjects
boundInterface.Open();
uiComp.OpenInterfaces[uiKey] = boundInterface;
var playerSession = _playerManager.LocalPlayer?.Session;
if (playerSession != null)
if (_playerManager.LocalSession is { } playerSession)
{
uiComp.Interfaces[uiKey]._subscribedSessions.Add(playerSession);
RaiseLocalEvent(uid, new BoundUIOpenedEvent(uiKey, uid, playerSession), true);

View File

@@ -35,7 +35,7 @@ namespace Robust.Client.GameObjects
return;
}
var player = _playerManager.LocalPlayer?.ControlledEntity;
var player = _playerManager.LocalEntity;
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
{

View File

@@ -232,9 +232,9 @@ namespace Robust.Client.GameStates
return default;
}
DebugTools.AssertNotNull(_players.LocalPlayer);
DebugTools.Assert(_players.LocalSession != null);
var evArgs = new EntitySessionEventArgs(_players.LocalPlayer!.Session);
var evArgs = new EntitySessionEventArgs(_players.LocalSession);
_pendingSystemMessages.Enqueue((_nextInputCmdSeq, _timing.CurTick, message,
new EntitySessionMessage<T>(evArgs, message)));
@@ -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

@@ -313,7 +313,7 @@ namespace Robust.Client.GameStates
if (args.Length == 0)
{
entity = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
entity = _playerManager.LocalEntity ?? EntityUid.Invalid;
}
else if (!NetEntity.TryParse(args[0], out var netEntity) || !_entManager.TryGetEntity(netEntity, out entity))
{

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

@@ -6,7 +6,10 @@ using Robust.Shared.Maths;
namespace Robust.Client.Graphics
{
internal interface IRenderHandle
/// <remarks>
/// Unstable API. Likely to break hard during renderer rewrite if you rely on it.
/// </remarks>
public interface IRenderHandle
{
DrawingHandleScreen DrawingHandleScreen { get; }
DrawingHandleWorld DrawingHandleWorld { get; }

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

@@ -126,7 +126,7 @@ namespace Robust.Client.Input
{
try
{
LoadKeyFile(path, true);
LoadKeyFile(path, false, true);
}
catch (Exception e)
{
@@ -136,7 +136,7 @@ namespace Robust.Client.Input
if (_resourceMan.ContentFileExists(path))
{
LoadKeyFile(path, false);
LoadKeyFile(path, true);
}
}
@@ -496,7 +496,13 @@ namespace Robust.Client.Input
return true;
}
private void LoadKeyFile(ResPath file, bool userData)
/// <summary>
/// Loads a keybind file, configuring keybinds.
/// </summary>
/// <param name="file">File to load from the content package</param>
/// <param name="defaultRegistration">Whether or not this is a "default" keybind set. If it is, then it won't override the current configuration, only the defaults.</param>
/// <param name="userData">Whether or not to load from the user data directory instead of the content package.</param>
public void LoadKeyFile(ResPath file, bool defaultRegistration, bool userData = false)
{
TextReader reader;
if (userData)
@@ -517,16 +523,19 @@ 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 (!userData)
if (defaultRegistration)
{
_defaultRegistrations.Add(reg);
@@ -538,19 +547,24 @@ namespace Robust.Client.Input
}
}
RegisterBinding(reg, markModified: userData);
RegisterBinding(reg, markModified: !defaultRegistration, invalid);
}
}
if (userData && mapping.TryGet("leaveEmpty", out var node))
if (!defaultRegistration && mapping.TryGet("leaveEmpty", out var node))
{
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);
}
}
}
@@ -578,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);
@@ -609,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.
@@ -624,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

@@ -495,7 +495,7 @@ namespace Robust.Client.Placement
{
// Try to get current map.
var map = MapId.Nullspace;
if (EntityManager.TryGetComponent(PlayerManager.LocalPlayer?.ControlledEntity, out TransformComponent? xform))
if (EntityManager.TryGetComponent(PlayerManager.LocalEntity, out TransformComponent? xform))
{
map = xform.MapID;
}
@@ -512,7 +512,7 @@ namespace Robust.Client.Placement
private bool CurrentEraserMouseCoordinates(out EntityCoordinates coordinates)
{
var ent = PlayerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
var ent = PlayerManager.LocalEntity ?? EntityUid.Invalid;
if (ent == EntityUid.Invalid)
{
coordinates = new EntityCoordinates();
@@ -640,7 +640,7 @@ namespace Robust.Client.Placement
if (CurrentPermission is not {Range: > 0} ||
!CurrentMode.RangeRequired ||
PlayerManager.LocalPlayer?.ControlledEntity is not {Valid: true} controlled)
PlayerManager.LocalEntity is not {Valid: true} controlled)
return;
var worldPos = EntityManager.GetComponent<TransformComponent>(controlled).WorldPosition;

View File

@@ -216,7 +216,7 @@ namespace Robust.Client.Placement
{
if (!RangeRequired)
return true;
var controlled = pManager.PlayerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
var controlled = pManager.PlayerManager.LocalEntity ?? EntityUid.Invalid;
if (controlled == EntityUid.Invalid)
{
return false;

View File

@@ -10,14 +10,13 @@ namespace Robust.Client.Player
public override Filter FromEntities(Filter filter, params EntityUid[] entities)
{
if (_playerManager.LocalPlayer is not { } localPlayer
|| localPlayer.Session.AttachedEntity is not {Valid: true} attachedUid)
if (_playerManager.LocalEntity is not {Valid: true} attachedUid)
return filter;
foreach (var uid in entities)
{
if (uid == attachedUid)
filter.AddPlayer(localPlayer.Session);
filter.AddPlayer(_playerManager.LocalSession!);
}
return filter;

View File

@@ -36,12 +36,12 @@ internal sealed class ReplayRecordingManager : SharedReplayRecordingManager
private void OnRecordingStarted(MappingDataNode metadata, List<object> messages)
{
if (_player.LocalPlayer == null)
if (_player.LocalSession == null)
return;
// Add information about the user doing the recording. This is used to set the default replay observer position
// when playing back the replay.
var guid = _player.LocalPlayer.UserId.UserId.ToString();
var guid = _player.LocalUser.ToString();
metadata[ReplayConstants.MetaKeyRecordedBy] = new ValueDataNode(guid);
}

View File

@@ -545,6 +545,36 @@ namespace Robust.Client.UserInterface
Draw(renderHandle.DrawingHandleScreen);
}
protected internal virtual void PreRenderChildren(ref ControlRenderArguments args)
{
}
protected internal virtual void PostRenderChildren(ref ControlRenderArguments args)
{
}
protected internal virtual void RenderChildOverride(ref ControlRenderArguments args, int childIndex, Vector2i position)
{
RenderControl(ref args, childIndex, position);
}
public ref struct ControlRenderArguments
{
public IRenderHandle Handle;
public ref int Total;
public Vector2i Position;
public Color Modulate;
public UIBox2i? ScissorBox;
public ref Matrix3 CoordinateTransform;
}
protected void RenderControl(ref ControlRenderArguments args, int childIndex, Vector2i position)
{
UserInterfaceManagerInternal.RenderControl(args.Handle, ref args.Total, GetChild(childIndex), position, args.Modulate, args.ScissorBox, args.CoordinateTransform);
}
public void UpdateDraw()
{
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.Audio;
using Robust.Client.ResourceManagement;
using Robust.Shared.ContentPack;
@@ -37,8 +38,10 @@ namespace Robust.Client.UserInterface.Controls
get => _group;
set
{
if (value?.InternalButtons.Contains(this) ?? false)
return; // No work to do.
// Remove from old group.
_group?.Buttons.Remove(this);
_group?.InternalButtons.Remove(this);
_group = value;
@@ -47,11 +50,21 @@ namespace Robust.Client.UserInterface.Controls
return;
}
value.Buttons.Add(this);
value.InternalButtons.Add(this);
ToggleMode = true;
// Set us to pressed if we're the first button.
Pressed = value.Buttons.Count == 0;
if (value.IsNoneSetAllowed)
{
// Still UNPRESS if there's another pressed button, but don't PRESS it otherwise.
if (value.Pressed != this)
_pressed = false;
}
else
{
// Set us to pressed if we're the first button. Doesn't go through the setter to avoid setting off our own error check.
_pressed = value.InternalButtons.Count == 1;
}
DrawModeChanged();
}
}
@@ -95,7 +108,7 @@ namespace Robust.Client.UserInterface.Controls
return;
}
if (!value && Group != null)
if (!value && Group is { IsNoneSetAllowed: false })
{
throw new InvalidOperationException("Cannot directly unset a grouped button. Set another button in the group instead.");
}
@@ -326,7 +339,7 @@ namespace Robust.Client.UserInterface.Controls
return;
}
foreach (var button in _group.Buttons)
foreach (var button in _group.InternalButtons)
{
if (button != this && button.Pressed)
{
@@ -440,6 +453,29 @@ namespace Robust.Client.UserInterface.Controls
/// </remarks>
public sealed class ButtonGroup
{
internal readonly List<BaseButton> Buttons = new();
/// <summary>
/// Whether it is legal for this button group to have no selected button.
/// </summary>
/// <remarks>
/// If true, it's legal for no button in the group to be active.
/// This is then the initial state of a new group of buttons (no button is automatically selected),
/// and it becomes legal to manually clear the active button through code.
/// The user cannot manually unselect the active button regardless, only by selecting a difference button.
/// </remarks>
public bool IsNoneSetAllowed { get; }
/// <summary>
/// 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 = true)
{
IsNoneSetAllowed = isNoneSetAllowed;
}
internal readonly List<BaseButton> InternalButtons = new();
public IReadOnlyList<BaseButton> Buttons => InternalButtons;
public BaseButton? Pressed => InternalButtons.FirstOrDefault(x => x.Pressed);
}
}

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

@@ -12,6 +12,7 @@ namespace Robust.Client.UserInterface.Controls
public class OptionButton : ContainerButton
{
public const string StyleClassOptionButton = "optionButton";
public const string StyleClassPopup = "optionButtonPopup";
public const string StyleClassOptionTriangle = "optionTriangle";
public readonly ScrollContainer OptionsScroll;
@@ -74,7 +75,8 @@ namespace Robust.Client.UserInterface.Controls
_popup = new Popup()
{
Children = { OptionsScroll }
Children = { new PanelContainer(), OptionsScroll },
StyleClasses = { StyleClassPopup }
};
_popup.OnPopupHide += OnPopupHide;
@@ -99,6 +101,11 @@ namespace Robust.Client.UserInterface.Controls
AddItem(label, id);
}
public virtual void ButtonOverride(Button button)
{
}
public void AddItem(string label, int? id = null)
{
if (id == null)
@@ -132,6 +139,8 @@ namespace Robust.Client.UserInterface.Controls
{
Select(0);
}
ButtonOverride(button);
}
private void TogglePopup(bool show)
@@ -139,6 +148,8 @@ namespace Robust.Client.UserInterface.Controls
if (show)
{
var globalPos = GlobalPosition;
globalPos.Y += Size.Y + 1; // Place it below us, with a safety margin.
globalPos.Y -= Margin.SumVertical;
OptionsScroll.Measure(Window?.Size ?? Vector2Helpers.Infinity);
var (minX, minY) = OptionsScroll.DesiredSize;
var box = UIBox2.FromDimensions(globalPos, new Vector2(Math.Max(minX, Width), minY));

View File

@@ -15,13 +15,13 @@ namespace Robust.Client.UserInterface.Controls
{
base.Draw(handle);
var style = _getStyleBox();
var style = GetStyleBox();
style?.Draw(handle, PixelSizeBox, UIScale);
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var styleSize = _getStyleBox()?.MinimumSize ?? Vector2.Zero;
var styleSize = GetStyleBox()?.MinimumSize ?? Vector2.Zero;
var measureSize = Vector2.Max(availableSize - styleSize, Vector2.Zero);
var childSize = Vector2.Zero;
foreach (var child in Children)
@@ -36,7 +36,7 @@ namespace Robust.Client.UserInterface.Controls
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var ourSize = UIBox2.FromDimensions(Vector2.Zero, finalSize);
var contentBox = _getStyleBox()?.GetContentBox(ourSize, 1) ?? ourSize;
var contentBox = GetStyleBox()?.GetContentBox(ourSize, 1) ?? ourSize;
foreach (var child in Children)
{
@@ -47,7 +47,7 @@ namespace Robust.Client.UserInterface.Controls
}
[System.Diagnostics.Contracts.Pure]
private StyleBox? _getStyleBox()
protected StyleBox? GetStyleBox()
{
if (PanelOverride != null)
{

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

@@ -61,6 +61,9 @@ namespace Robust.Client.UserInterface
Vector2? CalcRelativeMousePositionFor(Control control, ScreenCoordinates mousePos);
Color GetMainClearColor();
void RenderControl(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
UIBox2i? scissorBox, Matrix3 coordinateTransform);
}
}

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

@@ -132,9 +132,10 @@ internal sealed partial class UserInterfaceManager
try
{
var total = 0;
_render(renderHandle, ref total, root, Vector2i.Zero, Color.White, null);
var drawingHandle = renderHandle.DrawingHandleScreen;
drawingHandle.SetTransform(Vector2.Zero, Angle.Zero, Vector2.One);
drawingHandle.SetTransform(Matrix3.Identity);
RenderControl(renderHandle, ref total, root, Vector2i.Zero, Color.White, null, Matrix3.Identity);
drawingHandle.SetTransform(Matrix3.Identity);
OnPostDrawUIRoot?.Invoke(new PostDrawUIRootEventArgs(root, drawingHandle));
_prof.WriteValue("Controls rendered", ProfData.Int32(total));

View File

@@ -75,7 +75,7 @@ namespace Robust.Client.UserInterface
foreach (var root in _roots)
{
if (root.Stylesheet != null)
if (root.Stylesheet == null)
{
root.StylesheetUpdateRecursive();
}
@@ -329,8 +329,8 @@ namespace Robust.Client.UserInterface
}
}
private void _render(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
UIBox2i? scissorBox)
public void RenderControl(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
UIBox2i? scissorBox, Matrix3 coordinateTransform)
{
if (!control.Visible)
{
@@ -377,7 +377,10 @@ namespace Robust.Client.UserInterface
total += 1;
var handle = renderHandle.DrawingHandleScreen;
handle.SetTransform(position, Angle.Zero, Vector2.One);
var oldXform = handle.GetTransform();
var xform = oldXform;
xform.Multiply(Matrix3.CreateTransform(position, Angle.Zero, Vector2.One));
handle.SetTransform(xform);
modulate *= control.Modulate;
if (_rendering || control.AlwaysRender)
@@ -389,16 +392,32 @@ namespace Robust.Client.UserInterface
handle.Modulate = oldMod;
handle.UseShader(null);
}
handle.SetTransform(oldXform);
var args = new Control.ControlRenderArguments()
{
Handle = renderHandle,
Total = ref total,
Modulate = modulate,
ScissorBox = scissorRegion,
CoordinateTransform = ref coordinateTransform
};
control.PreRenderChildren(ref args);
foreach (var child in control.Children)
{
_render(renderHandle, ref total, child, position + child.PixelPosition, modulate, scissorRegion);
var pos = position + (Vector2i) coordinateTransform.Transform(child.PixelPosition);
control.RenderChildOverride(ref args, child.GetPositionInParent(), pos);
}
control.PostRenderChildren(ref args);
if (clip)
{
renderHandle.SetScissor(scissorBox);
}
handle.SetTransform(oldXform);
}
public Color GetMainClearColor() => RootControl.ActualBgColor;

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

@@ -28,7 +28,7 @@ namespace Robust.Server.GameObjects
base.Initialize();
SubscribeLocalEvent<MapGridComponent, EmptyGridEvent>(HandleGridEmpty);
_cfg.OnValueChanged(CVars.GameDeleteEmptyGrids, SetGridDeletion, true);
Subs.CVar(_cfg, CVars.GameDeleteEmptyGrids, SetGridDeletion, true);
}
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
@@ -64,13 +64,6 @@ namespace Robust.Server.GameObjects
return !(grid.GetAllTiles().Any());
}
public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(CVars.GameDeleteEmptyGrids, SetGridDeletion);
}
private void HandleGridEmpty(EntityUid uid, MapGridComponent component, EmptyGridEvent args)
{
if (!_deleteEmptyGrids || TerminatingOrDeleted(uid) || HasComp<MapComponent>(uid))

View File

@@ -20,7 +20,8 @@ namespace Robust.Server.GameObjects
{
base.Initialize();
LoadMetricCVar();
_configurationManager.OnValueChanged(CVars.MetricsEnabled, _ => LoadMetricCVar());
Subs.CVar(_configurationManager, CVars.MetricsEnabled, _ => LoadMetricCVar());
}
private void LoadMetricCVar()

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,20 +133,23 @@ internal sealed partial class PvsSystem : EntitySystem
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_transform.OnGlobalMoveEvent += OnEntityMove;
EntityManager.EntityAdded += OnEntityAdded;
EntityManager.EntityDeleted += OnEntityDeleted;
EntityManager.AfterEntityFlush += AfterEntityFlush;
_configManager.OnValueChanged(CVars.NetPVS, SetPvs, true);
_configManager.OnValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged, true);
_configManager.OnValueChanged(CVars.NetLowLodRange, OnLodChanged, true);
_configManager.OnValueChanged(CVars.NetForceAckThreshold, OnForceAckChanged, true);
_configManager.OnValueChanged(CVars.NetPvsAsync, OnAsyncChanged, true);
Subs.CVar(_configManager, CVars.NetPVS, SetPvs, true);
Subs.CVar(_configManager, CVars.NetMaxUpdateRange, OnViewsizeChanged, true);
Subs.CVar(_configManager, CVars.NetLowLodRange, OnLodChanged, true);
Subs.CVar(_configManager, CVars.NetForceAckThreshold, OnForceAckChanged, true);
Subs.CVar(_configManager, CVars.NetPvsAsync, OnAsyncChanged, true);
Subs.CVar(_configManager, CVars.NetPvsCompressLevel, ResetParallelism, true);
_serverGameStateManager.ClientAck += OnClientAck;
_serverGameStateManager.ClientRequestFull += OnClientRequestFull;
_parallelMgr.ParallelCountChanged += ResetParallelism;
InitializeDirty();
_parallelMgr.ParallelCountChanged += ResetParallelism;
_configManager.OnValueChanged(CVars.NetPvsCompressLevel, ResetParallelism, true);
InitializePvsArray();
}
public override void Shutdown()
@@ -147,19 +158,22 @@ internal sealed partial class PvsSystem : EntitySystem
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
_transform.OnGlobalMoveEvent -= OnEntityMove;
EntityManager.EntityAdded -= OnEntityAdded;
EntityManager.EntityDeleted -= OnEntityDeleted;
EntityManager.AfterEntityFlush -= AfterEntityFlush;
_configManager.UnsubValueChanged(CVars.NetPVS, SetPvs);
_configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged);
_configManager.UnsubValueChanged(CVars.NetForceAckThreshold, OnForceAckChanged);
_configManager.UnsubValueChanged(CVars.NetPvsCompressLevel, ResetParallelism);
_parallelMgr.ParallelCountChanged -= ResetParallelism;
_serverGameStateManager.ClientAck -= OnClientAck;
_serverGameStateManager.ClientRequestFull -= OnClientRequestFull;
ClearPvsData();
ShutdownDirty();
_leaveTask?.WaitOne();
_leaveTask = null;
}
public override void Update(float frameTime)
{
ProcessDeletions();
}
/// <summary>
@@ -257,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)
@@ -346,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)),
@@ -366,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);
}
@@ -413,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

@@ -64,7 +64,7 @@ namespace Robust.Server.Physics
SubscribeNetworkEvent<RequestGridNodesMessage>(OnDebugRequest);
SubscribeNetworkEvent<StopGridNodesMessage>(OnDebugStopRequest);
_cfg.OnValueChanged(CVars.GridSplitting, SetSplitAllowed, true);
Subs.CVar(_cfg, CVars.GridSplitting, SetSplitAllowed, true);
}
private void SetSplitAllowed(bool value) => SplitAllowed = value;
@@ -73,7 +73,6 @@ namespace Robust.Server.Physics
{
base.Shutdown();
_subscribedSessions.Clear();
_cfg.UnsubValueChanged(CVars.GridSplitting, SetSplitAllowed);
}
/// <summary>

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