Compare commits

...

81 Commits

Author SHA1 Message Date
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
metalgearsloth
9e0fc7017c Version: 209.0.1 2024-02-02 01:02:42 +11:00
metalgearsloth
76317b7ab3 Fix bad import (#4882)
fork why are you so SLOW.
2024-02-02 01:01:21 +11:00
metalgearsloth
d5f4d4bf2f Version: 209.0.0 2024-02-02 00:40:18 +11:00
metalgearsloth
156d1a6b14 Add SharedMapSystem helper for tile methods (#4845)
* Add SharedMapSystem helper for tile methods

* weh

* release

* Resolves

* Move some of these

* note

* Remove obsolete

* note
2024-02-02 00:39:20 +11:00
metalgearsloth
3fa456fd44 Fix relay refreshes where the entity gets contained to itself (#4865)
If the parent is somewhere below the transform hierarchy should still be okay, this just fixes where something getting pulled gets picked up as pulling doesn't seem to be handling this anymore.
2024-02-01 20:33:33 +11:00
LordCarve
bf9bb46154 Added ReferenceEquals() tests for RobustIntegrationTest (#4839)
* Added a test that checks that RobustIntegrationTest Client and Server do not end up with same sub-ComponentState reference objects.

* Un-ignore and adjust the test.

* review

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2024-02-01 20:22:52 +11:00
metalgearsloth
fb3da0b53c Add entitylookup methods for parent / map (#4870)
* Add entitylookup methods for parent / map

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

* weh

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-31 17:21:41 +01:00
Pieter-Jan Briers
5c635c09b4 Replay int overflows, fixed again (#4878)
* Reapply "Fix replay int overflow issues." (#4802)

This reverts commit 32049e34f2.

* IConfigurationManager.LoadDefaultsFromTomlStream now does required type conversions.

This fixes scenarios like loading of `long` CVars.
2024-01-31 17:17:28 +01:00
Pieter-Jan Briers
fad539212d Cache TotalTicks in MIDI renderer
This is the "total length" of the MIDI file, but it's not cached by FluidSynth and expensive to calculate.

This property was used in the render code (for local files only) and the instrument menu. Caching it drastically improves performance.
2024-01-31 00:29:03 +01:00
Pieter-Jan Briers
7275302639 Stop multithreading FluidSynth with synth.cpu-cores.
It makes no sense for our use case, and it caused FluidSynth to allocate a different thread pool *per* mixer. And every one of those threads have *high* priority. That's like *really* bad.

Furthermore, it was based on ParallelProcessCount which is currently bugged, and because of that we were always allocating  256 (!!!) real OS threads for a MIDI synthesizer. CHRIST. (fix for ParallelProcessCount is separate)

I assume this is responsible for a ton of people's MIDI lag, it just murdering their PC's CPU scheduler.

The ability to multithread FluidSynth still exists as a CVar but it'll default to 1 and I don't think it makes sense to ever change it.

Also there was code to dynamically change the parameter, as far as I could test this just always crashed the process so out it goes.
2024-01-31 00:27:30 +01:00
metalgearsloth
5057ff97a3 Add MaxDimension property to Box2 (#4871)
* Add MaxDimension property to Box2

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

* a

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-30 13:28:05 +01:00
metalgearsloth
754d5a1fbb Add GetLocalPosition to controls (#4872)
* 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.

* weh

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-30 13:27:28 +01:00
Kara
7cee5b67a7 More easings (#4876)
* More easings

* yeah
2024-01-30 13:24:16 +01:00
Kara
21729e7e48 Smarter EntitySystem sawmill name conversion (#4875) 2024-01-29 22:52:39 +01:00
metalgearsloth
394d1e6cc2 Add global rectangles for controls (#4873)
* 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.

* weh

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-01-29 11:08:02 +01:00
metalgearsloth
f73d7f7285 Add dotted line drawing to screen handle (#4874)
Probably needs anti-aliasing but idk an easy way to do it.
2024-01-29 11:07:15 +01:00
DrSmugleaf
e505cfffd8 Fix TileEdgeOverlay flickering with tiles that are barely out of view (#4868) 2024-01-29 14:13:19 +11:00
collinlunn
d0fe3591ef Fixes parsing of ShaderBlendMode in ShaderPrototype (#4867)
* Fixes parsing of ShaderBlendMode in ShaderPrototype

* Changes ShaderPrototype parser to use case insensitive overload
2024-01-28 12:20:06 -08:00
James Simonson
1868f32457 Exposed "Bottom Margin" to itemlist (#4862)
* Exposed "Bottom Margin" to itemlist

* Changed from ItemBottomMargin to ItemSeparation

* Update ItemList.cs
2024-01-27 17:26:37 +01:00
127 changed files with 3931 additions and 489 deletions

View File

@@ -45,17 +45,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.3" />
<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,192 @@ END TEMPLATE-->
*None yet*
## 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
* Fix missed import from 209.0.0.
## 209.0.0
### Breaking changes
* `replay.max_compressed_size` and `replay.max_uncompressed_size` CVars are now `long`.
* Remove obsolete CoordinatesExtension for ToEntityCoordinates from GridUid / Vector2i.
### New features
* Add GetEntitiesOnMap / GetChildEntities to EntityLookupSystem to return components on the specified map and components with the specified parent respectively.
* 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.
* `IConfigurationManager.LoadDefaultsFromTomlStream` properly does type conversions. This fixes scenarios like loading of `long` CVars.
* Add helper methods for TileRef / Vector2i to SharedMapSystem for ToCenterCoordinates (tile center EntityCoordinates) and ToCoordinates (tile origin to EntityCoordinates).
* Copy some of the coordinates extensions to SharedTransformSystem.
### Bugfixes
* Fixed integer overflows in replay max size calculation.
* Explicitly capped `replay.replay_tick_batchSize` internally to avoid high values causing allocation failures.
### Other
* Important MIDI performance improvements.
## 208.0.0
### Breaking changes

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.

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

@@ -106,8 +106,8 @@ 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);
}
private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAutoHandleStateEvent args)
@@ -133,13 +133,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();

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";
@@ -192,7 +192,12 @@ internal sealed partial class MidiManager : IMidiManager
_settings["synth.midi-bank-select"].StringValue = "gm";
//_settings["synth.verbose"].IntValue = 1; // Useful for debugging.
_parallel.AddAndInvokeParallelCountChanged(UpdateParallelCount);
var midiParallel = _cfgMan.GetCVar(CVars.MidiParallelism);
_settings["synth.polyphony"].IntValue = Math.Clamp(1024 + (int)(Math.Log2(midiParallel) * 2048), 1, 65535);
_settings["synth.cpu-cores"].IntValue = Math.Clamp(midiParallel, 1, 256);
_midiSawmill.Debug($"Synth Cores: {_settings["synth.cpu-cores"].IntValue}");
_midiSawmill.Debug($"Synth Polyphony: {_settings["synth.polyphony"].IntValue}");
}
catch (Exception e)
{
@@ -201,7 +206,10 @@ internal sealed partial class MidiManager : IMidiManager
return;
}
_midiThread = new Thread(ThreadUpdate);
_midiThread = new Thread(ThreadUpdate)
{
Name = "RobustToolbox MIDI Thread"
};
_midiThread.Start();
_updateJob = new MidiUpdateJob()
@@ -219,18 +227,6 @@ internal sealed partial class MidiManager : IMidiManager
FluidsynthInitialized = true;
}
private void UpdateParallelCount()
{
if (_settings == null)
return;
_settings["synth.polyphony"].IntValue = Math.Clamp(1024 + (int)(Math.Log2(_parallel.ParallelProcessCount) * 2048), 1, 65535);
_settings["synth.cpu-cores"].IntValue = Math.Clamp(_parallel.ParallelProcessCount, 1, 256);
_midiSawmill.Debug($"Synth Cores: {_settings["synth.cpu-cores"].IntValue}");
_midiSawmill.Debug($"Synth Polyphony: {_settings["synth.polyphony"].IntValue}");
}
private void LoggerDelegate(NFluidsynth.Logger.LogLevel level, string message, IntPtr data)
{
var rLevel = level switch

View File

@@ -38,6 +38,7 @@ internal sealed class MidiRenderer : IMidiRenderer
private readonly Synth _synth;
private readonly Sequencer _sequencer;
private NFluidsynth.Player? _player;
private int _playerTotalTicks;
private MidiDriver? _driver;
private byte _midiProgram = 1;
private byte _midiBank = 1;
@@ -144,7 +145,21 @@ internal sealed class MidiRenderer : IMidiRenderer
public bool DisableProgramChangeEvent { get; set; } = true;
[ViewVariables(VVAccess.ReadWrite)]
public int PlayerTotalTick => _player?.GetTotalTicks ?? 0;
public int PlayerTotalTick
{
get
{
// GetTotalTicks is really expensive (has to iterate the entire file, not cached).
// Slight problem with caching it ourselves: the value only becomes available when the player loads the MIDI file.
// And that only happens after playback really starts, with the timer and synth and all that stuff.
// So we cache it "as soon as it's available", i.e. not 0.
// We don't care about playlists and such, so it shouldn't change anymore after.
if (_playerTotalTicks != 0)
return _playerTotalTicks;
return _playerTotalTicks = _player?.GetTotalTicks ?? 0;
}
}
[ViewVariables(VVAccess.ReadWrite)]
public int PlayerTick
@@ -339,6 +354,7 @@ internal sealed class MidiRenderer : IMidiRenderer
return false;
}
_playerTotalTicks = 0;
_player?.Dispose();
_player = new NFluidsynth.Player(_synth);
_player.SetPlaybackCallback(MidiPlayerEventHandler);
@@ -377,6 +393,7 @@ internal sealed class MidiRenderer : IMidiRenderer
_player?.Join();
_player?.Dispose();
_player = null;
_playerTotalTicks = 0;
}
StopAllNotes();

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

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

@@ -221,7 +221,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)));

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

@@ -2,6 +2,7 @@ using System;
using System.Numerics;
using System.Text;
using Robust.Client.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
@@ -14,6 +15,79 @@ namespace Robust.Client.Graphics
{
}
/// <summary>
/// Simialr to DrawLine but has dashes interspersed.
/// </summary>
/// <param name="offset">Offset from the start of the line.</param>
/// <param name="dashSize">How long a dash is.</param>
/// <param name="gapSize">How long the gap between dashes is.</param>
public void DrawDottedLine(Vector2 from, Vector2 to, Color color, float offset = 0f, float dashSize = 8f, float gapSize = 2f)
{
var lineVector = to - from;
// No drawing for you.
if (lineVector.LengthSquared() < 10f * float.Epsilon)
return;
var lineAndGap = gapSize + dashSize;
var lines = new ValueList<Vector2>();
// Minimum distance.
if (lineVector.Length() < lineAndGap)
{
lines.Add(from);
lines.Add(to);
}
else
{
var maxLength = lineVector.Length();
var normalizedLine = lineVector.Normalized();
var dashVector = normalizedLine * dashSize;
var gapVector = normalizedLine * gapSize;
var position = from;
offset %= (dashSize + gapSize);
var length = offset;
var dashLength = dashSize;
// If offset is less than gap size then start with a gap
// otherwise start with a partial line
if (offset > 0f)
{
if (offset < gapSize)
{
position += normalizedLine * offset;
length += offset;
}
else
{
dashLength = (offset - gapSize);
}
}
while (length < maxLength)
{
lines.Add(position);
position += normalizedLine * dashLength;
var lengthFromStart = (position - from).Length();
// if over length then cap the thing.
if (lengthFromStart > maxLength)
{
position = to;
}
lines.Add(position);
dashLength = dashVector.Length();
position += gapVector;
length = (position - from).Length();
}
}
DrawPrimitives(DrawPrimitiveTopology.LineList, lines.Span, color);
}
public abstract void DrawRect(UIBox2 rect, Color color, bool filled = true);
public abstract void DrawTextureRectRegion(Texture texture, UIBox2 rect, UIBox2? subRegion = null, Color? modulate = null);

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

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.ResourceManagement;
@@ -69,7 +69,7 @@ namespace Robust.Client.Graphics
ShaderBlendMode? blend = null;
if (_rawBlendMode != null)
{
if (!Enum.TryParse<ShaderBlendMode>(_rawBlendMode.ToUpper(), out var parsed))
if (!Enum.TryParse<ShaderBlendMode>(_rawBlendMode, true, out var parsed))
Logger.Error($"invalid mode: {_rawBlendMode}");
else
blend = parsed;

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

@@ -37,7 +37,9 @@ public sealed class TileEdgeOverlay : GridOverlay
var tileDimensions = new Vector2(tileSize, tileSize);
var (_, _, worldMatrix, invMatrix) = xformSystem.GetWorldPositionRotationMatrixWithInv(Grid.Owner);
args.WorldHandle.SetTransform(worldMatrix);
var localAABB = invMatrix.TransformBox(args.WorldBounds);
var bounds = args.WorldBounds;
bounds = new Box2Rotated(bounds.Box.Enlarged(1), bounds.Rotation, bounds.Origin);
var localAABB = invMatrix.TransformBox(bounds);
var enumerator = mapSystem.GetLocalTilesEnumerator(Grid.Owner, Grid, localAABB, false);

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

@@ -226,6 +226,10 @@ namespace Robust.Client.UserInterface
/// <seealso cref="Rect"/>
public UIBox2i PixelRect => UIBox2i.FromDimensions(PixelPosition, PixelSize);
public UIBox2 GlobalRect => UIBox2.FromDimensions(GlobalPosition, _size);
public UIBox2i GlobalPixelRect => UIBox2i.FromDimensions(GlobalPixelPosition, PixelSize);
/// <summary>
/// Horizontal alignment mode.
/// This determines how the control should be laid out horizontally
@@ -464,6 +468,14 @@ namespace Robust.Client.UserInterface
}
}
/// <summary>
/// Gets the screen coordinates position relative to the control.
/// </summary>
public Vector2 GetLocalPosition(ScreenCoordinates coordinates)
{
return coordinates.Position - GlobalPixelPosition;
}
/// <summary>
/// Notify the layout system that this control's <see cref="Measure"/> result may have changed
/// and must be recalculated.

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

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
@@ -9,6 +9,9 @@ using Robust.Shared.Input;
using Robust.Shared.Maths;
using Timer = Robust.Shared.Timing.Timer;
/// <summary>
/// Represents a scrollable list of items in a user interface.
/// </summary>
namespace Robust.Client.UserInterface.Controls
{
[Virtual]
@@ -29,6 +32,10 @@ namespace Robust.Client.UserInterface.Controls
public const string StylePropertySelectedItemBackground = "selected-item-background";
public const string StylePropertyDisabledItemBackground = "disabled-item-background";
/// <summary>
/// Gets or sets the ItemSeparation of individual list items
/// </summary>
public int ItemSeparation { get; set; } = 0; // Default value is 0px
public int Count => _itemList.Count;
public bool IsReadOnly => false;
@@ -68,8 +75,10 @@ namespace Robust.Client.UserInterface.Controls
itemHeight += ActualItemBackground.MinimumSize.Y * UIScale;
_totalContentHeight += (int)Math.Ceiling(itemHeight);
_totalContentHeight += ItemSeparation;
}
//Remove unneeded ItemSeparation on last item.
_totalContentHeight -= ItemSeparation;
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
_updateScrollbarVisibility();
}
@@ -390,6 +399,9 @@ namespace Robust.Client.UserInterface.Controls
}
offset += itemHeight;
// Add a ItemSeparation at the bottom of each item.
offset += ItemSeparation;
}
}
@@ -399,7 +411,7 @@ namespace Robust.Client.UserInterface.Controls
var color = ActualFontColor;
var offsetY = (int) (box.Height - font.GetHeight(UIScale)) / 2;
var baseLine = new Vector2i(0, offsetY + font.GetAscent(UIScale)) + box.TopLeft;
var baseLine = new Vector2i(5, offsetY + font.GetAscent(UIScale)) + box.TopLeft;
foreach (var rune in text.EnumerateRunes())
{

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

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

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

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

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

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

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

@@ -126,11 +126,12 @@ internal sealed partial class PvsSystem : EntitySystem
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_transform.OnGlobalMoveEvent += OnEntityMove;
_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;
@@ -138,7 +139,6 @@ internal sealed partial class PvsSystem : EntitySystem
InitializeDirty();
_parallelMgr.ParallelCountChanged += ResetParallelism;
_configManager.OnValueChanged(CVars.NetPvsCompressLevel, ResetParallelism, true);
}
public override void Shutdown()
@@ -148,10 +148,6 @@ internal sealed partial class PvsSystem : EntitySystem
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
_transform.OnGlobalMoveEvent -= OnEntityMove;
_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;

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>

View File

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

View File

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

View File

@@ -75,6 +75,15 @@ namespace Robust.Shared.Maths
get => new(Width, Height);
}
/// <summary>
/// Returns the highest of width or height.
/// </summary>
public readonly float MaxDimension
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => MathF.Max(Height, Width);
}
public readonly Vector2 Center
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -177,13 +186,13 @@ namespace Robust.Shared.Maths
[Pure]
public readonly Box2 Intersect(in Box2 other)
{
var ourLeftBottom = new System.Numerics.Vector2(Left, Bottom);
var ourRightTop = new System.Numerics.Vector2(Right, Top);
var otherLeftBottom = new System.Numerics.Vector2(other.Left, other.Bottom);
var otherRightTop = new System.Numerics.Vector2(other.Right, other.Top);
var ourLeftBottom = BottomLeft;
var ourRightTop = TopRight;
var otherLeftBottom = other.BottomLeft;
var otherRightTop = other.TopRight;
var max = System.Numerics.Vector2.Max(ourLeftBottom, otherLeftBottom);
var min = System.Numerics.Vector2.Min(ourRightTop, otherRightTop);
var max = Vector2.Max(ourLeftBottom, otherLeftBottom);
var min = Vector2.Min(ourRightTop, otherRightTop);
if (max.X <= min.X && max.Y <= min.Y)
return new Box2(max.X, max.Y, min.X, min.Y);
@@ -210,13 +219,13 @@ namespace Robust.Shared.Maths
[Pure]
public readonly Box2 Union(in Box2 other)
{
var ourLeftBottom = new System.Numerics.Vector2(Left, Bottom);
var otherLeftBottom = new System.Numerics.Vector2(other.Left, other.Bottom);
var ourRightTop = new System.Numerics.Vector2(Right, Top);
var otherRightTop = new System.Numerics.Vector2(other.Right, other.Top);
var ourLeftBottom = BottomLeft;
var otherLeftBottom = other.BottomLeft;
var ourRightTop = TopRight;
var otherRightTop = other.TopRight;
var leftBottom = System.Numerics.Vector2.Min(ourLeftBottom, otherLeftBottom);
var rightTop = System.Numerics.Vector2.Max(ourRightTop, otherRightTop);
var leftBottom = Vector2.Min(ourLeftBottom, otherLeftBottom);
var rightTop = Vector2.Max(ourRightTop, otherRightTop);
if (leftBottom.X <= rightTop.X && leftBottom.Y <= rightTop.Y)
return new Box2(leftBottom.X, leftBottom.Y, rightTop.X, rightTop.Y);
@@ -396,11 +405,11 @@ namespace Robust.Shared.Maths
[Pure]
public static Box2 Union(in Vector2 a, in Vector2 b)
{
var vecA = new System.Numerics.Vector2(a.X, a.Y);
var vecB = new System.Numerics.Vector2(b.X, b.Y);
var vecA = new Vector2(a.X, a.Y);
var vecB = new Vector2(b.X, b.Y);
var min = System.Numerics.Vector2.Min(vecA, vecB);
var max = System.Numerics.Vector2.Max(vecA, vecB);
var min = Vector2.Min(vecA, vecB);
var max = Vector2.Max(vecA, vecB);
return new Box2(min.X, min.Y, max.X, max.Y);
}
@@ -412,12 +421,12 @@ namespace Robust.Shared.Maths
[Pure]
public readonly Box2 ExtendToContain(Vector2 vec)
{
var leftBottom = new System.Numerics.Vector2(Left, Bottom);
var rightTop = new System.Numerics.Vector2(Right, Top);
var vector = new System.Numerics.Vector2(vec.X, vec.Y);
var leftBottom = new Vector2(Left, Bottom);
var rightTop = new Vector2(Right, Top);
var vector = new Vector2(vec.X, vec.Y);
var min = System.Numerics.Vector2.Min(vector, leftBottom);
var max = System.Numerics.Vector2.Max(vector, rightTop);
var min = Vector2.Min(vector, leftBottom);
var max = Vector2.Max(vector, rightTop);
return new Box2(min.X, min.Y, max.X, max.Y);
}

View File

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

View File

@@ -1,13 +1,229 @@
using System;
using JetBrains.Annotations;
namespace Robust.Shared.Maths;
// Reference: https://easings.net/
internal static class Easings
/// <summary>
/// A static class for computing easings for animations.
/// The parameter "p" is the absolute progress of the animation between 0 and 1.
/// </summary>
[PublicAPI]
public static class Easings
{
#region Trig
public static float InSine(float p)
{
return 1.0f - MathF.Cos(p * MathF.PI / 2.0f);
}
public static float OutSine(float p)
{
return MathF.Sin(p * MathF.PI / 2);
}
public static float InOutSine(float p)
{
return -(MathF.Cos(MathF.PI * p) - 1.0f) / 2.0f;
}
#endregion
#region Polynomial
public static float InQuad(float p)
{
return p * p;
}
public static float OutQuad(float p)
{
return 1 - (1 - p) * (1 - p);
}
public static float InOutQuad(float p)
{
return p < 0.5 ? 2 * p * p : 1 - MathF.Pow(-2 * p + 2, 2) / 2;
}
public static float InCubic(float p)
{
return p * p * p;
}
public static float OutCubic(float p)
{
return 1 - MathF.Pow(1 - p, 3);
}
public static float InOutCubic(float p)
{
return p < 0.5 ? 4 * p * p * p : 1 - MathF.Pow(-2 * p + 2, 3) / 2;
}
public static float InQuart(float p)
{
return p * p * p * p;
}
public static float OutQuart(float p)
{
return 1 - MathF.Pow(1 - p, 4);
}
public static float InOutQuart(float p)
{
return p < 0.5 ? 8 * p * p * p * p : 1 - MathF.Pow(-2 * p + 2, 4) / 2;
}
public static float InQuint(float p)
{
return p * p * p * p * p;
}
public static float OutQuint(float p)
{
return 1 - MathF.Pow(1 - p, 5);
}
public static float InOutQuint(float p)
{
return p < 0.5f ? (16 * p * p * p * p * p) : 1 - MathF.Pow(-2 * p + 2, 5) / 2;
return p < 0.5f ? 16 * p * p * p * p * p : 1 - MathF.Pow(-2 * p + 2, 5) / 2;
}
#endregion
#region Other
public static float InExpo(float p)
{
return p == 0 ? 0 : MathF.Pow(2, 10 * p - 10);
}
public static float OutExpo(float p)
{
return Math.Abs(p - 1) < 0.0001f ? 1 : 1 - MathF.Pow(2, -10 * p);
}
public static float InOutExpo(float p)
{
return p == 0.0f
? 0
: Math.Abs(p - 1) < 0.0001f
? 1
: p < 0.5f
? MathF.Pow(2, 20 * p - 10) / 2
: (2 - MathF.Pow(2, -20 * p + 10)) / 2;
}
public static float InCirc(float p)
{
return 1 - MathF.Sqrt(1 - MathF.Pow(p, 2));
}
public static float OutCirc(float p)
{
return MathF.Sqrt(1 - MathF.Pow(p - 1, 2));
}
public static float InOutCirc(float p)
{
return p < 0.5
? (1 - MathF.Sqrt(1 - MathF.Pow(2 * p, 2))) / 2
: (MathF.Sqrt(1 - MathF.Pow(-2 * p + 2, 2)) + 1) / 2;
}
public static float InBack(float p)
{
var c1 = 1.70158f;
var c3 = c1 + 1;
return c3 * p * p * p - c1 * p * p;
}
public static float OutBack(float p)
{
const float c1 = 1.70158f;
const float c3 = c1 + 1;
return 1 + c3 * MathF.Pow(p - 1, 3) + c1 * MathF.Pow(p - 1, 2);
}
public static float InOutBack(float p)
{
const float c1 = 1.70158f;
const float c2 = c1 * 1.525f;
return p < 0.5
? MathF.Pow(2 * p, 2) * ((c2 + 1) * 2 * p - c2) / 2
: (MathF.Pow(2 * p - 2, 2) * ((c2 + 1) * (p * 2 - 2) + c2) + 2) / 2;
}
/// <remarks>
/// elastic in, not "inelastic"
/// </remarks>
public static float InElastic(float p)
{
const float c4 = 2 * MathF.PI / 3;
return p == 0
? 0
: Math.Abs(p - 1) < 0.0001f
? 1
: -MathF.Pow(2, 10 * p - 10) * MathF.Sin((p * 10 - 10.75f) * c4);
}
public static float OutElastic(float p)
{
const float c4 = 2.0f * MathF.PI / 3.0f;
return p == 0
? 0
: Math.Abs(p - 1) < 0.0001f
? 1
: MathF.Pow(2, -10 * p) * MathF.Sin((p * 10.0f - 0.75f) * c4) + 1.0f;
}
public static float InOutElastic(float p)
{
const float c5 = 2.0f * MathF.PI / 4.5f;
return p == 0
? 0
: Math.Abs(p - 1) < 0.0001f
? 1
: p < 0.5
? -(MathF.Pow(2, 20 * p - 10) * MathF.Sin((20.0f * p - 11.125f) * c5)) / 2.0f
: MathF.Pow(2, -20.0f * p + 10.0f) * MathF.Sin((20.0f * p - 11.125f) * c5) / 2.0f + 1.0f;
}
public static float InBounce(float p)
{
return 1 - OutBounce(1 - p);
}
public static float OutBounce(float p)
{
const float n1 = 7.5625f;
const float d1 = 2.75f;
if (p < 1 / d1) return n1 * p * p;
if (p < 2 / d1) return n1 * (p -= 1.5f / d1) * p + 0.75f;
if (p < 2.5 / d1) return n1 * (p -= 2.25f / d1) * p + 0.9375f;
return n1 * (p -= 2.625f / d1) * p + 0.984375f;
}
public static float InOutBounce(float p)
{
return p < 0.5
? (1 - OutBounce(1 - 2 * p)) / 2
: (1 + OutBounce(2 * p - 1)) / 2;
}
#endregion
}

View File

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

View File

@@ -50,17 +50,11 @@ public abstract partial class SharedAudioSystem : EntitySystem
base.Initialize();
InitializeEffect();
ZOffset = CfgManager.GetCVar(CVars.AudioZOffset);
CfgManager.OnValueChanged(CVars.AudioZOffset, SetZOffset);
Subs.CVar(CfgManager, CVars.AudioZOffset, SetZOffset);
SubscribeLocalEvent<AudioComponent, ComponentGetStateAttemptEvent>(OnAudioGetStateAttempt);
SubscribeLocalEvent<AudioComponent, EntityUnpausedEvent>(OnAudioUnpaused);
}
public override void Shutdown()
{
base.Shutdown();
CfgManager.UnsubValueChanged(CVars.AudioZOffset, SetZOffset);
}
protected void SetZOffset(float value)
{
ZOffset = value;

View File

@@ -1239,7 +1239,7 @@ namespace Robust.Shared
/// Enable Discord rich presence integration.
/// </summary>
public static readonly CVarDef<bool> DiscordEnabled =
CVarDef.Create("discord.enabled", true, CVar.CLIENTONLY);
CVarDef.Create("discord.enabled", true, CVar.CLIENTONLY | CVar.ARCHIVE);
public static readonly CVarDef<string> DiscordRichPresenceMainIconId =
CVarDef.Create("discord.rich_main_icon_id", "devstation", CVar.SERVER | CVar.REPLICATED);
@@ -1323,6 +1323,16 @@ namespace Robust.Shared
public static readonly CVarDef<float> MidiVolume =
CVarDef.Create("midi.volume", 0.50f, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// Controls amount of CPU cores and (by extension) polyphony for Fluidsynth.
/// </summary>
/// <remarks>
/// You probably don't want to set this to be multithreaded, the way Fluidsynth's multithreading works is
/// probably worse-than-nothing for Robust's usage.
/// </remarks>
public static readonly CVarDef<int> MidiParallelism =
CVarDef.Create("midi.parallelism", 1, CVar.CLIENTONLY | CVar.ARCHIVE);
/*
* HUB
* CVars related to public master server hub
@@ -1487,14 +1497,14 @@ namespace Robust.Shared
/// <summary>
/// Maximum compressed size of a replay recording (in kilobytes) before recording automatically stops.
/// </summary>
public static readonly CVarDef<int> ReplayMaxCompressedSize = CVarDef.Create("replay.max_compressed_size",
1024 * 256, CVar.ARCHIVE);
public static readonly CVarDef<long> ReplayMaxCompressedSize = CVarDef.Create("replay.max_compressed_size",
1024L * 512, CVar.ARCHIVE);
/// <summary>
/// Maximum uncompressed size of a replay recording (in kilobytes) before recording automatically stops.
/// </summary>
public static readonly CVarDef<int> ReplayMaxUncompressedSize = CVarDef.Create("replay.max_uncompressed_size",
1024 * 1024, CVar.ARCHIVE);
public static readonly CVarDef<long> ReplayMaxUncompressedSize = CVarDef.Create("replay.max_uncompressed_size",
1024L * 1024, CVar.ARCHIVE);
/// <summary>
/// Uncompressed size of individual files created by the replay (in kilobytes), where each file contains data

View File

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

View File

@@ -113,10 +113,46 @@ namespace Robust.Shared.Configuration
{
var loaded = new HashSet<string>();
foreach (var (cvar, value) in ParseCVarValuesFromToml(stream))
var callbackEvents = new ValueList<ValueChangedInvoke>();
// Ensure callbacks are raised OUTSIDE the write lock.
using (Lock.WriteGuard())
{
loaded.Add(cvar);
OverrideDefault(cvar, value);
foreach (var (cVarName, value) in ParseCVarValuesFromToml(stream))
{
if (!_configVars.TryGetValue(cVarName, out var cVar) || !cVar.Registered)
{
_sawmill.Error($"Trying to set unregistered variable '{cVarName}'");
continue;
}
var convertedValue = value;
if (!cVar.DefaultValue.GetType().IsEnum && cVar.DefaultValue.GetType() != value.GetType())
{
try
{
convertedValue = ConvertToCVarType(value, cVar.DefaultValue.GetType());
}
catch
{
_sawmill.Error($"Override TOML parsed cvar does not match registered cvar type. Name: {cVarName}. Code Type: {cVar.DefaultValue.GetType()}. Toml type: {value.GetType()}");
continue;
}
}
cVar.DefaultValue = value;
if (cVar.OverrideValue == null && cVar.Value == null)
{
if (SetupInvokeValueChanged(cVar, convertedValue) is { } invoke)
callbackEvents.Add(invoke);
}
}
}
foreach (var callback in callbackEvents)
{
InvokeValueChanged(callback);
}
return loaded;
@@ -303,8 +339,7 @@ namespace Robust.Shared.Configuration
{
try
{
// try convert thing like int to float.
cVar.Value = Convert.ChangeType(cVar.Value, type);
cVar.Value = ConvertToCVarType(cVar.Value, type);
}
catch
{
@@ -711,6 +746,26 @@ namespace Robust.Shared.Configuration
}
}
/// <summary>
/// Try to convert a compatible value to the actual registration type of a CVar.
/// </summary>
/// <remarks>
/// When CVars are parsed from TOML, their in-code type is not known.
/// This function does the necessary conversions from e.g. int to long.
/// </remarks>
/// <param name="value">
/// The value to convert.
/// This must be a simple type like strings or integers.
/// </param>
/// <param name="cVar">
/// The registration type of the CVar.
/// </param>
/// <returns></returns>
private static object ConvertToCVarType(object value, Type cVar)
{
return Convert.ChangeType(value, cVar);
}
/// <summary>
/// Holds the data for a single configuration variable.
/// </summary>

View File

@@ -73,6 +73,9 @@ WhitelistedNamespaces:
# * The API is not *relevant* to content. e.g. System.Type.IsAnsiClass.
# * I am lazy these API lists are huge dude.
Types:
Avalonia.Metadata:
XmlnsDefinitionAttribute:
All: True
NetSerializer:
NetListAsArray`1:
Fields:
@@ -626,6 +629,7 @@ Types:
- "System.Text.Encoding get_UTF7()"
- "System.Text.Encoding get_UTF8()"
- "System.Text.Encoding get_UTF32()"
- "System.Text.Encoding GetEncoding(string)"
NormalizationForm: { } # Enum
Rune: { All: True }
StringBuilder:
@@ -716,6 +720,9 @@ Types:
ChunkEnumerator: { All: True }
AppendInterpolatedStringHandler: { All: True }
StringRuneEnumerator: { All: True }
System.Text.Unicode:
UnicodeRange: { All: True }
UnicodeRanges: { All: True }
System.Threading.Tasks:
Task: { All: True }
Task`1: { All: True }

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
@@ -279,6 +278,66 @@ public partial class EntityManager
return entities;
}
public Dictionary<EntityUid, T> GetEntityDictionary<T>(Dictionary<NetEntity, T> netEntities)
{
var entities = new Dictionary<EntityUid, T>(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(GetEntity(pair.Key), pair.Value);
}
return entities;
}
public Dictionary<T, EntityUid> GetEntityDictionary<T>(Dictionary<T, NetEntity> netEntities) where T : notnull
{
var entities = new Dictionary<T, EntityUid>(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(pair.Key, GetEntity(pair.Value));
}
return entities;
}
public Dictionary<T, EntityUid?> GetEntityDictionary<T>(Dictionary<T, NetEntity?> netEntities) where T : notnull
{
var entities = new Dictionary<T, EntityUid?>(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(pair.Key, GetEntity(pair.Value));
}
return entities;
}
public Dictionary<EntityUid, EntityUid> GetEntityDictionary(Dictionary<NetEntity, NetEntity> netEntities)
{
var entities = new Dictionary<EntityUid, EntityUid>(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(GetEntity(pair.Key), GetEntity(pair.Value));
}
return entities;
}
public Dictionary<EntityUid, EntityUid?> GetEntityDictionary(Dictionary<NetEntity, NetEntity?> netEntities)
{
var entities = new Dictionary<EntityUid, EntityUid?>(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(GetEntity(pair.Key), GetEntity(pair.Value));
}
return entities;
}
public HashSet<EntityUid> EnsureEntitySet<T>(HashSet<NetEntity> netEntities, EntityUid callerEntity)
{
var entities = new HashSet<EntityUid>(netEntities.Count);
@@ -324,6 +383,72 @@ public partial class EntityManager
}
}
public void EnsureEntityDictionary<TComp, TValue>(Dictionary<NetEntity, TValue> netEntities, EntityUid callerEntity,
Dictionary<EntityUid, TValue> entities)
{
entities.Clear();
entities.EnsureCapacity(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(EnsureEntity<TComp>(pair.Key, callerEntity), pair.Value);
}
}
public void EnsureEntityDictionaryNullableValue<TComp, TValue>(Dictionary<NetEntity, TValue?> netEntities, EntityUid callerEntity,
Dictionary<EntityUid, TValue?> entities)
{
entities.Clear();
entities.EnsureCapacity(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(EnsureEntity<TComp>(pair.Key, callerEntity), pair.Value);
}
}
public void EnsureEntityDictionary<TComp, TKey>(Dictionary<TKey, NetEntity> netEntities, EntityUid callerEntity,
Dictionary<TKey, EntityUid> entities) where TKey : notnull
{
entities.Clear();
entities.EnsureCapacity(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(pair.Key, EnsureEntity<TComp>(pair.Value, callerEntity));
}
}
public void EnsureEntityDictionary<TComp, TKey>(Dictionary<TKey, NetEntity?> netEntities, EntityUid callerEntity,
Dictionary<TKey, EntityUid?> entities) where TKey : notnull
{
entities.Clear();
entities.EnsureCapacity(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(pair.Key, EnsureEntity<TComp>(pair.Value, callerEntity));
}
}
public void EnsureEntityDictionary<TComp>(Dictionary<NetEntity, NetEntity> netEntities, EntityUid callerEntity,
Dictionary<EntityUid, EntityUid> entities)
{
entities.Clear();
entities.EnsureCapacity(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(EnsureEntity<TComp>(pair.Key, callerEntity), EnsureEntity<TComp>(pair.Value, callerEntity));
}
}
public void EnsureEntityDictionary<TComp>(Dictionary<NetEntity, NetEntity?> netEntities, EntityUid callerEntity,
Dictionary<EntityUid, EntityUid?> entities)
{
entities.Clear();
entities.EnsureCapacity(netEntities.Count);
foreach (var pair in netEntities)
{
entities.Add(EnsureEntity<TComp>(pair.Key, callerEntity), EnsureEntity<TComp>(pair.Value, callerEntity));
}
}
/// <inheritdoc />
public List<EntityUid> GetEntityList(ICollection<NetEntity> netEntities)
{
@@ -467,6 +592,71 @@ public partial class EntityManager
return netEntities;
}
/// <inheritdoc />
public Dictionary<NetEntity, T> GetNetEntityDictionary<T>(Dictionary<EntityUid, T> entities)
{
var netEntities = new Dictionary<NetEntity, T>(entities.Count);
foreach (var pair in entities)
{
netEntities.Add(GetNetEntity(pair.Key), pair.Value);
}
return netEntities;
}
/// <inheritdoc />
public Dictionary<T, NetEntity> GetNetEntityDictionary<T>(Dictionary<T, EntityUid> entities) where T : notnull
{
var netEntities = new Dictionary<T, NetEntity>(entities.Count);
foreach (var pair in entities)
{
netEntities.Add(pair.Key, GetNetEntity(pair.Value));
}
return netEntities;
}
/// <inheritdoc />
public Dictionary<T, NetEntity?> GetNetEntityDictionary<T>(Dictionary<T, EntityUid?> entities) where T : notnull
{
var netEntities = new Dictionary<T, NetEntity?>(entities.Count);
foreach (var pair in entities)
{
netEntities.Add(pair.Key, GetNetEntity(pair.Value));
}
return netEntities;
}
/// <inheritdoc />
public Dictionary<NetEntity, NetEntity> GetNetEntityDictionary(Dictionary<EntityUid, EntityUid> entities)
{
var netEntities = new Dictionary<NetEntity, NetEntity>(entities.Count);
foreach (var pair in entities)
{
netEntities.Add(GetNetEntity(pair.Key), GetNetEntity(pair.Value));
}
return netEntities;
}
/// <inheritdoc />
public Dictionary<NetEntity, NetEntity?> GetNetEntityDictionary(Dictionary<EntityUid, EntityUid?> entities)
{
var netEntities = new Dictionary<NetEntity, NetEntity?>(entities.Count);
foreach (var pair in entities)
{
netEntities.Add(GetNetEntity(pair.Key), GetNetEntity(pair.Value));
}
return netEntities;
}
/// <inheritdoc />
public HashSet<EntityCoordinates> GetEntitySet(HashSet<NetCoordinates> netEntities)
{

View File

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

View File

@@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.Collections;
namespace Robust.Shared.GameObjects
{
public abstract partial class EntitySystem
{
private List<SubBase>? _subscriptions;
private ValueList<SubBase> _subscriptions;
/// <summary>
/// A handle to allow subscription on this entity system's behalf.
@@ -84,7 +84,6 @@ namespace Robust.Shared.GameObjects
{
EntityManager.EventBus.SubscribeEvent(src, this, handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubBroadcast<T>(src));
}
@@ -96,7 +95,6 @@ namespace Robust.Shared.GameObjects
{
EntityManager.EventBus.SubscribeEvent(src, this, handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubBroadcast<T>(src));
}
@@ -108,7 +106,6 @@ namespace Robust.Shared.GameObjects
{
EntityManager.EventBus.SubscribeSessionEvent(src, this, handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubBroadcast<EntitySessionMessage<T>>(src));
}
@@ -122,7 +119,6 @@ namespace Robust.Shared.GameObjects
{
EntityManager.EventBus.SubscribeLocalEvent(handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubLocal<TComp, TEvent>());
}
@@ -134,7 +130,6 @@ namespace Robust.Shared.GameObjects
{
EntityManager.EventBus.SubscribeLocalEvent(handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubLocal<TComp, TEvent>());
}
@@ -146,21 +141,17 @@ namespace Robust.Shared.GameObjects
{
EntityManager.EventBus.SubscribeLocalEvent(handler, GetType(), before, after);
_subscriptions ??= new();
_subscriptions.Add(new SubLocal<TComp, TEvent>());
}
private void ShutdownSubscriptions()
{
if (_subscriptions == null)
return;
foreach (var sub in _subscriptions)
{
sub.Unsubscribe(this, EntityManager.EventBus);
}
_subscriptions = null;
_subscriptions = default;
}
/// <summary>
@@ -232,6 +223,19 @@ namespace Robust.Shared.GameObjects
{
System.SubscribeLocalEvent(handler, before, after);
}
/// <summary>
/// Register an action to be ran when this entity system is shut down.
/// </summary>
/// <remarks>
/// This can be used by extension methods for <see cref="Subscriptions"/>
/// to unsubscribe from from external sources such as CVars.
/// </remarks>
/// <param name="action">An action to be ran when the entity system is shut down.</param>
public void RegisterUnsubscription(Action action)
{
System._subscriptions.Add(new SubAction(action));
}
}
private abstract class SubBase
@@ -261,5 +265,13 @@ namespace Robust.Shared.GameObjects
bus.UnsubscribeLocalEvent<TComp, TBase>();
}
}
private sealed class SubAction(Action action) : SubBase
{
public override void Unsubscribe(EntitySystem sys, IEventBus bus)
{
action();
}
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.IoC;
@@ -41,8 +42,16 @@ namespace Robust.Shared.GameObjects
name = name.Substring(0, name.Length - "System".Length);
// Convert CamelCase to snake_case
name = string.Concat(name.Select(x => char.IsUpper(x) ? $"_{char.ToLower(x)}" : x.ToString()));
name = name.Trim('_');
// Ignore if all uppercase, assume acronym (e.g. NPC or HTN)
if (name.All(char.IsUpper))
{
name = name.ToLower(CultureInfo.InvariantCulture);
}
else
{
name = string.Concat(name.Select(x => char.IsUpper(x) ? $"_{char.ToLower(x)}" : x.ToString()));
name = name.Trim('_');
}
return $"system.{name}";
}

View File

@@ -0,0 +1,130 @@
using System;
using Robust.Shared.Configuration;
namespace Robust.Shared.GameObjects;
/// <summary>
/// Extra subscription helpers for <see cref="EntitySystem"/> that are not part of the core entity system behavior.
/// </summary>
public static class EntitySystemSubscriptionExt
{
/// <summary>
/// Listen for an event for if the config value changes.
/// </summary>
/// <remarks>
/// This is an O(n) operation.
/// </remarks>
/// <param name="subs">
/// The entity system subscriptions.
/// Call this with <see cref="EntitySystem.Subscriptions"/>.
/// </param>
/// <param name="cfg">The configuration manager.</param>
/// <param name="name">The name of the CVar to listen for.</param>
/// <param name="onValueChanged">The delegate to run when the value was changed.</param>
/// <param name="invokeImmediately">
/// Whether to run the callback immediately inw this method. Can help reduce boilerplate
/// </param>
/// <typeparam name="T">The type of value contained in this CVar.</typeparam>
public static void CVar<T>(
this EntitySystem.Subscriptions subs,
IConfigurationManager cfg,
string name,
Action<T> onValueChanged,
bool invokeImmediately = false)
where T : notnull
{
cfg.OnValueChanged(name, onValueChanged, invokeImmediately);
subs.RegisterUnsubscription(() => cfg.UnsubValueChanged(name, onValueChanged));
}
/// <summary>
/// Listen for an event for if the config value changes.
/// </summary>
/// <remarks>
/// This is an O(n) operation.
/// </remarks>
/// <param name="subs">
/// The entity system subscriptions.
/// Call this with <see cref="EntitySystem.Subscriptions"/>.
/// </param>
/// <param name="cfg">The configuration manager.</param>
/// <param name="cVar">The CVar to listen for.</param>
/// <param name="onValueChanged">The delegate to run when the value was changed.</param>
/// <param name="invokeImmediately">
/// Whether to run the callback immediately in this method. Can help reduce boilerplate
/// </param>
/// <typeparam name="T">The type of value contained in this CVar.</typeparam>
public static void CVar<T>(
this EntitySystem.Subscriptions subs,
IConfigurationManager cfg,
CVarDef<T> cVar,
Action<T> onValueChanged,
bool invokeImmediately = false)
where T : notnull
{
cfg.OnValueChanged(cVar, onValueChanged, invokeImmediately);
subs.RegisterUnsubscription(() => cfg.UnsubValueChanged(cVar, onValueChanged));
}
/// <summary>
/// Listen for an event for if the config value changes.
/// </summary>
/// <remarks>
/// This is an O(n) operation.
/// </remarks>
/// <param name="subs">
/// The entity system subscriptions.
/// Call this with <see cref="EntitySystem.Subscriptions"/>.
/// </param>
/// <param name="cfg">The configuration manager.</param>
/// <param name="name">The name of the CVar to listen for.</param>
/// <param name="onValueChanged">The delegate to run when the value was changed.</param>
/// <param name="invokeImmediately">
/// Whether to run the callback immediately in this method. Can help reduce boilerplate
/// </param>
/// <typeparam name="T">The type of value contained in this CVar.</typeparam>
public static void CVar<T>(
this EntitySystem.Subscriptions subs,
IConfigurationManager cfg,
string name,
CVarChanged<T> onValueChanged,
bool invokeImmediately = false)
where T : notnull
{
cfg.OnValueChanged(name, onValueChanged, invokeImmediately);
subs.RegisterUnsubscription(() => cfg.UnsubValueChanged(name, onValueChanged));
}
/// <summary>
/// Listen for an event for if the config value changes.
/// </summary>
/// <remarks>
/// This is an O(n) operation.
/// </remarks>
/// <param name="subs">
/// The entity system subscriptions.
/// Call this with <see cref="EntitySystem.Subscriptions"/>.
/// </param>
/// <param name="cfg">The configuration manager.</param>
/// <param name="cVar">The CVar to listen for.</param>
/// <param name="onValueChanged">The delegate to run when the value was changed.</param>
/// <param name="invokeImmediately">
/// Whether to run the callback immediately in this method. Can help reduce boilerplate
/// </param>
/// <typeparam name="T">The type of value contained in this CVar.</typeparam>
public static void CVar<T>(
this EntitySystem.Subscriptions subs,
IConfigurationManager cfg,
CVarDef<T> cVar,
CVarChanged<T> onValueChanged,
bool invokeImmediately = false)
where T : notnull
{
cfg.OnValueChanged(cVar, onValueChanged, invokeImmediately);
subs.RegisterUnsubscription(() => cfg.UnsubValueChanged(cVar, onValueChanged));
}
}

View File

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

View File

@@ -703,16 +703,75 @@ public sealed partial class EntityLookupSystem
GetEntitiesIntersecting(mapId, shape, transform, entities, flags);
}
/// <summary>
/// Gets entities with the specified component with the specified map.
/// </summary>
public void GetEntitiesOnMap<TComp1>(MapId mapId, HashSet<Entity<TComp1>> entities) where TComp1 : IComponent
{
var query = AllEntityQuery<TComp1, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var xform))
{
if (xform.MapID != mapId)
continue;
entities.Add((uid, comp));
}
}
/// <summary>
/// Gets entities with the specified component with the specified parent.
/// </summary>
public void GetEntitiesOnMap<TComp1, TComp2>(MapId mapId, HashSet<Entity<TComp1, TComp2>> entities)
where TComp1 : IComponent
where TComp2 : IComponent
{
var query = AllEntityQuery<TComp1, TComp2, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var comp2, out var xform))
{
if (xform.MapID != mapId)
continue;
entities.Add((uid, comp, comp2));
}
}
#endregion
private readonly record struct GridQueryCompState<T>(
HashSet<Entity<IComponent>> Intersecting,
IPhysShape Shape,
Transform Transform,
EntityLookupSystem Lookup,
LookupFlags Flags,
EntityQuery<T> Query
) where T : IComponent;
/// <summary>
/// Gets entities with the specified component with the specified parent.
/// </summary>
public void GetChildEntities<TComp1>(EntityUid parentUid, HashSet<Entity<TComp1>> entities) where TComp1 : IComponent
{
var query = AllEntityQuery<TComp1, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var xform))
{
if (xform.ParentUid != parentUid)
continue;
entities.Add((uid, comp));
}
}
/// <summary>
/// Gets entities with the specified component with the specified parent.
/// </summary>
public void GetChildEntities<TComp1, TComp2>(EntityUid parentUid, HashSet<Entity<TComp1, TComp2>> entities)
where TComp1 : IComponent
where TComp2 : IComponent
{
var query = AllEntityQuery<TComp1, TComp2, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var comp2, out var xform))
{
if (xform.ParentUid != parentUid)
continue;
entities.Add((uid, comp, comp2));
}
}
private readonly record struct GridQueryState<T>(
HashSet<Entity<T>> Intersecting,

View File

@@ -36,8 +36,8 @@ namespace Robust.Shared.GameObjects
base.Initialize();
UpdatesBefore.Add(typeof(SharedBroadphaseSystem));
_cfg.OnValueChanged(CVars.GenerateGridFixtures, SetEnabled, true);
_cfg.OnValueChanged(CVars.GridFixtureEnlargement, SetEnlargement, true);
Subs.CVar(_cfg, CVars.GenerateGridFixtures, SetEnabled, true);
Subs.CVar(_cfg, CVars.GridFixtureEnlargement, SetEnlargement, true);
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
SubscribeLocalEvent<RegenerateGridBoundsEvent>(OnGridBoundsRegenerate);
@@ -58,14 +58,6 @@ namespace Robust.Shared.GameObjects
_map.RegenerateCollision(ev.EntityUid, grid, _map.GetMapChunks(ev.EntityUid, grid).Values.ToHashSet());
}
public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(CVars.GenerateGridFixtures, SetEnabled);
_cfg.UnsubValueChanged(CVars.GridFixtureEnlargement, SetEnlargement);
}
private void SetEnabled(bool value) => _enabled = value;
private void SetEnlargement(float value) => _fixtureEnlargement = value;

View File

@@ -0,0 +1,83 @@
using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
namespace Robust.Shared.GameObjects;
public abstract partial class SharedMapSystem
{
/// <summary>
/// If the supplied coordinates intersects a grid will align with the tile center, otherwise returns the coordinates.
/// </summary>
/// <param name="coordinates"></param>
/// <returns></returns>
[Pure]
public EntityCoordinates AlignToGrid(EntityCoordinates coordinates)
{
// Check if the parent is already a grid.
if (_gridQuery.TryGetComponent(coordinates.EntityId, out var gridComponent))
{
var tile = CoordinatesToTile(coordinates.EntityId, gridComponent, coordinates);
return ToCenterCoordinates(coordinates.EntityId, tile, gridComponent);
}
// Check if mappos intersects a grid.
var mapPos = coordinates.ToMap(EntityManager, _transform);
if (_mapInternal.TryFindGridAt(mapPos, out var gridUid, out gridComponent))
{
var tile = CoordinatesToTile(gridUid, gridComponent, coordinates);
return ToCenterCoordinates(gridUid, tile, gridComponent);
}
// No grid so just return it.
return coordinates;
}
/// <summary>
/// Converts a tileRef to EntityCoordinates.
/// </summary>
[Pure]
public EntityCoordinates ToCoordinates(TileRef tileRef, MapGridComponent? gridComponent = null)
{
return ToCoordinates(tileRef.GridUid, tileRef.GridIndices, gridComponent);
}
/// <summary>
/// Converts a tileRef to EntityCoordinates.
/// </summary>
[Pure]
public EntityCoordinates ToCoordinates(EntityUid gridUid, Vector2i tile, MapGridComponent? gridComponent = null)
{
if (!_gridQuery.Resolve(gridUid, ref gridComponent))
{
return EntityCoordinates.Invalid;
}
return new EntityCoordinates(gridUid, tile * gridComponent.TileSize);
}
/// <summary>
/// Converts a tileRef to EntityCoordinates for the center of the tile.
/// </summary>
[Pure]
public EntityCoordinates ToCenterCoordinates(TileRef tileRef, MapGridComponent? gridComponent = null)
{
return ToCenterCoordinates(tileRef.GridUid, tileRef.GridIndices, gridComponent);
}
/// <summary>
/// Converts a tileRef to EntityCoordinates for the center of the tile.
/// </summary>
[Pure]
public EntityCoordinates ToCenterCoordinates(EntityUid gridUid, Vector2i tile, MapGridComponent? gridComponent = null)
{
if (!_gridQuery.Resolve(gridUid, ref gridComponent))
{
return EntityCoordinates.Invalid;
}
return new EntityCoordinates(gridUid, tile * gridComponent.TileSize + gridComponent.TileSizeHalfVector);
}
}

View File

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

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