Compare commits

...

100 Commits

Author SHA1 Message Date
Pieter-Jan Briers
c7f7030b89 Version: 227.0.2 2024-08-11 19:54:40 +02:00
Pieter-Jan Briers
54218fb40b Use absolute path for explorer.exe
frick me

(cherry picked from commit 0284eb0430)
2024-08-11 19:54:40 +02:00
Pieter-Jan Briers
7b2c1701f3 Version: 227.0.1 2024-08-11 17:54:18 +02:00
Pieter-Jan Briers
0e3930d7d7 Security updates (#5353)
* Fix security bug in WritableDirProvider.OpenOsWindow()

Reported by @NarryG and @nyeogmi

* Sandbox updates

* Update ImageSharp again

(cherry picked from commit 7d778248ee)
(cherry picked from commit f66cda74e95619ddba2221bda644bf4394619805)
2024-08-11 17:54:18 +02:00
metalgearsloth
a9aea7027f Version: 227.0.0 2024-07-01 15:54:45 +10:00
metalgearsloth
2a49c2d9b8 Add loop support for SpriteSystem.GetFrame (#5265)
For Ftl I just want it played once.
2024-07-01 15:50:49 +10:00
metalgearsloth
a0c069f1ea Add LocalTilesIntersecting for circles (#5262)
* Add LocalTilesIntersecting for circles

* Update Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
2024-06-29 14:57:06 +10:00
Pieter-Jan Briers
2c6fb95e53 Add EntityManager dependency to base LocalizedEntityCommands 2024-06-28 17:00:14 +02:00
Nemanja
afe337644e Make spin box controls disable buttons that can't be pressed (#5221)
* spin box changes

* make SpinboxButton private

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2024-06-28 17:53:10 +10:00
Plykiya
b8924f3ddf Removes obsolete visibility system functions (#5209)
* Removes obsolete visibility system functions

* guh, forgot to add the test

---------

Co-authored-by: plykiya <plykiya@protonmail.com>
2024-06-28 17:30:02 +10:00
Pieter-Jan Briers
08970e745b Entity console commands system. (#5267)
* Entity console commands system.

This adds a new base type, LocalizedEntityCommands, which is able to import entity systems as dependencies. This is done by only registering these while the entity system is active.

Handling registration separately like this required a bit of changes around ConsoleHost to make it more suitable for this purpose:

You can now directly register command instances, and also have a system to suppress `UpdateAvailableCommands` on the client so there's no bad O(N*M) behavior.

* Convert TeleportCommands.cs to new entity commands.

Removes some obsoletion warnings without pain from having to manually import transform system.

* Fix RobustServerSimulation dependency issue.

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2024-06-28 17:29:24 +10:00
Leon Friedrich
0ba4a66787 Always process networked events via the priority queue (#5205) 2024-06-28 17:02:14 +10:00
Pieter-Jan Briers
75b3431ee6 New "must call base" analyzer. (#5266)
* New "must call base" analyzer.

This enforces that you actually call base when overriding stuff. This is intended for base methods like entity system's, where server/client systems overriding shared ones SHOULD call Initialize() and such.

* Add MustCallBase to entity system methods
2024-06-28 14:44:49 +10:00
geraeumig
c0ef976588 Make PvsSystem consider offset and zoom from EyeComponent (#5228)
* Make PvsSystem consider offset and zoom from EyeComponent

* Just use PvsScale float

* float.IsFinite

---------

Co-authored-by: geraeumig <alfenos@proton.me>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2024-06-28 14:24:22 +10:00
Pieter-Jan Briers
fe5cdf9e3c Fix loading of replays if string package is compressed in zip.
This happened when I had to re-compress a recovered replay from a server crash, and then loaded it up in a dev environment.
2024-06-27 16:07:49 +02:00
Morb
450349188b Dispose memory stream after deserialization exception (#4840)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-06-26 23:16:56 +02:00
metalgearsloth
897ad998d9 Add option for left or right-aligned checkboxes (#4739)
* Add option for left or right-aligned checkboxes

I think right-aligned is better but this is less of a breaking change.

* Cleanup
2024-06-26 23:11:17 +02:00
metalgearsloth
635ae3c353 Remove IGameTiming from TransformComponent (#5273) 2024-06-26 17:10:34 +02:00
Pieter-Jan Briers
a4ea5a4620 Add AnimationCompletedEvent "Finished" boolean.
Content has multiple cases where AnimationCompletedEvent is used to loop an animation. #5238 broke some of these by making this event raised even when manually removed.

Luckily most cases in content tie the animation looping to the presence of a component, so the component getting removed means there's nothing to refresh the loop. LightBehavior is not as fortunate however, causing bugs like https://github.com/space-wizards/space-station-14/issues/29144

This boolean allows looping code to properly distinguish the event, so it won't try to restart an animation after removing it directly.
2024-06-25 15:35:53 +02:00
Pieter-Jan Briers
90e87526d0 Quote tab completions containing spaces. 2024-06-24 16:05:00 +02:00
metalgearsloth
cd6576ddf9 Mark EntityCoordinates.Offset as pure (#5264)
Doesn't do anything just being called and sometimes I forget.
2024-06-24 10:53:41 +02:00
Leon Friedrich
e2cf4ee3db SIMD Colour multiplication (#5251)
* color simd

* removed wrong one

* A

* Use Unsafe.BitCast

* Color4 -> Color

* remove constructor

* remove `in`
2024-06-22 16:42:40 +02:00
metalgearsloth
860c9af2bf Version: 226.3.0 2024-06-22 14:10:43 +10:00
Pieter-Jan Briers
87bb29408a Try to report method source of sandboxing issues. 2024-06-21 00:31:47 +02:00
Pieter-Jan Briers
738cfbe992 Add non-generic IList and ICollection to sandbox.
Used by collection expressions in some cases.
2024-06-21 00:31:47 +02:00
wixoa
90edc02259 Add style property overrides to ContainerButton and TabContainer (#5222)
* Add style box override properties to ContainerButton and TabContainer

* Add background panel to TabContainer, and add text color overrides

* Undo background panel
You can achieve the same by instead putting the TabContainer in a PanelContainer

* Add BackgroundColor property to StyleBoxTexture

* Remove BackgroundColor from StyleBoxTexture
2024-06-20 20:50:51 +02:00
metalgearsloth
da5416a2da Version: 226.2.0 2024-06-20 17:28:11 +10:00
metalgearsloth
021845d956 Add some System.Random methods (#5177)
* Add some System.Random methods

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

* Defer updating invisible OutputPanels on UIScale change

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

* Avoid redundant UI Scale updates on window resize.

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

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

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

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

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

Just allocate a buncha dictionaries what could possibly go wrong.

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

There's some truly repulsive stuff here. Ugh.

* Cap debug console content size.

It's a CVar.

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

---------

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

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

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

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

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

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

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

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

* Fix ComponentNetworkGenerator

* a

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

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

* Try prevent event bus linked list loops

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

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

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

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

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

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

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

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

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

* Actually just remove it

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

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

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

- Avoids >1GB of gas tile allocations.

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

This reverts commit 1a478944a6.

* Fix delta state merge issues

---------

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

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

* Fix merge error

* Fix it again, but for real this time

---------

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

* pee jay bee rahvew

* Fix EnteredTree() not respawning the entity

---------

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

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

* Change `HandleInputCommand` argument type

* Localize console errors

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

* rename to SetUi, add if missing

---------

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

* Add tests and verify desired behavior has been reached.

* Remove duplicate messages.

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

* Restart the test pipeline.

* Restart the test pipeline.

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

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

* Fix text breakage.

* Remove obsolete // TODO LINGUINI

* line wrapping conventions

---------

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

* A

* I love test IoC

* I love test init logic
2024-06-03 00:52:27 +10:00
Leon Friedrich
d8f2b917b4 Fix buis not closing properly on entity deletion (#5198) 2024-06-02 22:12:39 +10:00
Leon Friedrich
6dd6b79db6 Fix PVS exception (#5196) 2024-06-02 21:58:48 +10:00
ElectroJr
ff4548f108 Version: 224.0.0 2024-06-02 16:24:12 +12:00
ElectroJr
8f41405b31 Update release notes 2024-06-02 16:23:06 +12:00
Leon Friedrich
f285c62674 Rework entity prototype categories (#5061)
* Improve entity categories

* A
2024-06-02 14:10:11 +10:00
eoineoineoin
56c30edf04 Replace Matrix3 with System.Numerics.Matrix3x2 (#5078)
* Delete Matrix3. Replace with System.Numerics.Matrix3x2

* Feedback

* release notes
2024-06-02 14:08:47 +10:00
Nemanja
783d529ec4 more placement manager fixes (#5186)
* more placement manager fixes

* Update Robust.Shared/GameObjects/EntityManager.cs

Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>

---------

Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
2024-06-01 10:13:52 -07:00
Pieter-Jan Briers
895bfb8ec0 Add Array.Clear(Array) to sandbox
API added in .NET 6, overload that took explicit array bounds was already in sandbox.
2024-06-01 14:17:28 +02:00
Leon Friedrich
6ef67cf513 Add stack trace to some error logs and remove some audio error logs (#5188)
* Add stack trace to some error logs

* Remove terminating entity audio error logs
2024-05-31 13:16:47 -07:00
Leon Friedrich
15a2f6702c Add try-catch to EnsureClientBui() (#5189)
* Add `try-catch` to `EnsureClientBui()`

* Better comment

* Uneccesary !

* a
2024-05-31 18:01:29 +10:00
Leon Friedrich
c5c2c2022a Ignore invalid entityuids in bui system update method. (#5187) 2024-05-31 10:47:55 +10:00
Pieter-Jan Briers
3c378640dd Add cvar_subs command
Dumb little thing, just wanted to test something.
2024-05-31 02:00:53 +02:00
metalgearsloth
0a149fa91c Expand PVS BUIs (#5179)
Mainly happens to aghosts who go out of range. Shouldn't be a huge perf impact as the User component is added / removed as BUIs get opened.
2024-05-31 09:51:11 +10:00
metalgearsloth
fe8d1d9422 Fix BUI state getting stuck (#5180) 2024-05-30 11:02:44 +10:00
metalgearsloth
c89c529ba4 Version: 223.3.0 2024-05-29 17:35:10 +10:00
metalgearsloth
dd56de70b7 Fix grid-based audio (#5087)
* Fix grid-based audio

- Fixes parenting issues.
- Add SetGridAudio as an easy way to set it up and apply the override too.

* No more global

* Rejig it all

* mergew

* review

* Minor optimisation

* Revert "Minor optimisation"

This reverts commit d0cdac7690.
2024-05-29 16:14:37 +10:00
Nemanja
710408c613 Fix placement manager rotation jank (#5176) 2024-05-29 12:32:14 +10:00
metalgearsloth
2461cd94dd Better parallel exception logging (#5175) 2024-05-29 12:31:46 +10:00
235 changed files with 6241 additions and 3159 deletions

View File

@@ -15,10 +15,10 @@
<PackageVersion Include="ILReader.Core" Version="1.0.0.4" />
<PackageVersion Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageVersion Include="JetBrains.Profiler.Api" Version="1.4.0" />
<PackageVersion Include="Linguini.Bundle" Version="0.1.3" />
<PackageVersion Include="Linguini.Bundle" Version="0.8.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.1"/>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.1"/>
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.8.0" />
@@ -43,7 +43,7 @@
<PackageVersion Include="NUnit.Analyzers" Version="3.10.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Nett" Version="0.15.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageVersion Include="OpenTK.OpenAL" Version="4.7.7" />
<PackageVersion Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" />
<PackageVersion Include="Pidgin" Version="3.2.2" />
@@ -55,8 +55,8 @@
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.3" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />

View File

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

View File

@@ -54,6 +54,213 @@ END TEMPLATE-->
*None yet*
## 227.0.2
## 227.0.1
## 227.0.0
### Breaking changes
* Add a `loop` arg to SpriteSystem.GetFrame in case you don't want to get a looping animation.
* Remove obsolete VisibileSystem methods.
### New features
* Added `LocalizedEntityCommands`, which are console commands that have the ability to take entity system dependencies.
* Added `BeginRegistrationRegion` to `IConsoleHost` to allow efficient bulk-registration of console commands.
* Added `IConsoleHost.RegisterCommand` overload that takes an `IConsoleCommand`.
* Added a `Finished` boolean to `AnimationCompletedEvent` which allows distinguishing if an animation was removed prematurely or completed naturally.
* Add GetLocalTilesIntersecting for MapSystem.
* Add an analyzer for methods that should call the base implementation and use it for EntitySystems.
### Bugfixes
* Fix loading replays if string package is compressed inside a zip.
### Other
* Tab completions containing spaces are now properly quoted, so the command will actually work properly once entered.
* Mark EntityCoordinates.Offset as Pure so it shows as warnings if the variable is unused.
* Networked events will always be processed in order even if late.
## 226.3.0
### New features
* `System.Collections.IList` and `System.Collections.ICollection` are now sandbox safe, this fixes some collection expression cases.
* The sandboxing system will now report the methods responsible for references to illegal items.
## 226.2.0
### New features
* `Control.VisibilityChanged()` virtual function.
* Add some System.Random methods for NextFloat and NextPolarVector2.
### Bugfixes
* Fixes ContainerSystem failing client-side debug asserts when an entity gets unanchored & inserted into a container on the same tick.
* Remove potential race condition on server startup from invoking ThreadPool.SetMinThreads.
### Other
* Increase default value of res.rsi_atlas_size.
* Fix internal networking logic.
* Updates of `OutputPanel` contents caused by change in UI scale are now deferred until visible. Especially important to avoid updates from debug console.
* Debug console is now limited to only keep `con.max_entries` entries.
* Non-existent resources are cached by `IResourceCache.TryGetResource`. This avoids the game constantly trying to re-load non-existent resources in common patterns such as UI theme texture fallbacks.
* Default IPv4 MTU has been lowered to 700.
* Update Robust.LoaderApi.
### Internal
* Split out PVS serialization from compression and sending game states.
* Turn broadphase contacts into an IParallelRobustJob and remove unnecessary GetMapEntityIds for every contact.
## 226.1.0
### New features
* Add some GetLocalEntitiesIntersecting methods for `Entity<T>`.
### Other
* Fix internal networking logic
## 226.0.0
### Breaking changes
* `IEventBus.RaiseComponentEvent` now requires an EntityUid argument.
* The `AddedComponentEventArgs` and `RemovedComponentEventArgs` constructors are now internal
### New features
* Allow RequestScreenTexture to be set in overlays.
### Bugfixes
* Fix AnimationCompletedEvent not always going out.
## 225.0.0
### Breaking changes
* `NetEntity.Parse` and `TryParse` will now fail to parse empty strings.
* Try to prevent EventBus looping. This also caps the amount of directed component subscriptions for a particular component to 256.
### New features
* `IPrototypeManager.TryIndex` will now default to logging errors if passed an invalid prototype id struct (i,e., `EntProtoId` or `ProtoId<T>`). There is a new optional bool argument to disable logging errors.
* `Eye` now allows its `Position` to be set directly. Please only do this with the `FixedEye` child type constructed manually.
* Engine now respects the hub's `can_skip_build` parameter on info query, fixing an issue where the first hub advertisement fails due to ACZ taking too long.
* Add GetSession & TryGetSession to ActorSystem.
* Raise an event when an entity's name is changed.
### Bugfixes
* The `ent` toolshed command now takes `NetEntity` values, fixing parsing in practical uses.
* Fix ComponentFactory test mocks.
* Fix LookupFlags missing from a couple of EntityLookupSystem methods.
### Other
* Improved engine's Happy Eyeballs implementation, should result in more usage of IPv6 for HTTP APIs when available.
* Remove CompIdx locks to improve performance inside Pvs at higher player counts.
* Avoid a read lock in GetEntityQuery to also improve performance.
* Mark `EntityManager.System<T>` as Pure.
## 224.1.1
### Bugfixes
* Fixed UserInterfaceSystem sometimes throwing a key-not-found exception when trying to close UIs.
## 224.1.0
### New features
* `ServerIntegrationInstance` has new methods for adding dummy player sessions for tests that require multiple players.
* Linguini has been updated to v0.8.1. Errors will now be logged when a duplicate localization key is found.
* Added `UserInterfaceSystem.SetUi()` for modifying the `InterfaceData` associated with some BUI.
* Added the `EntityPrototypeView` control for spawning & rendering an entity prototype.
### Bugfixes
* Fix `UserInterfaceSystem` spamming client side errors when entities with UIs open are deleted while outside of PVS range.
* Fix Toolshed's EnumTypeParse not working enum values with upercase characters.
* Fixed `incmd` command not working due to an invalid cast.
### Other
* There have been various performance improvements to replay loading & playback.
### Internal
* Added `DummySession` and `DummyChannel` classes for use in integration tests and benchmarks to fool the server into thinking that there are multiple players connected.
* Added `ICommonSessionInternal` and updated `CommonSession` so that the internal setters now go through that interface.
## 224.0.1
### Bugfixes
* Fixes PVS throwing exceptions when invalid entities are passed to `ExpandPvsEvent`. Now it just logs an error.
* Fixes BUIs not properly closing, resulting in invalid entities in `UserInterfaceUserComponent.OpenInterfaces`
* Fixes an unknown/invalid prototype exception sometimes being thrown when running ``IPrototypeManager.ResolveResults()`
## 224.0.0
### Breaking changes
* `Matrix3` has been replaced with `System.Numerics.Matrix3x2`. Various Matrix related methods have been turned into extension methods in the `Matrix3Helpers` class.
* Engine `EntityCategory` prototype IDs have been changed to use CamelCase. I.e., `hideSpawnMenu` -> `HideSpawnMenu`
* Prototypes can now be implicitly cast `ProtoId<T>` or `EntProtoId` ID structs. The new implicit cast might cause previous function calls to be ambiguous.
### New features
* `Array.Clear(Array)` is now available in the sandbox.
* BUIs now use `ExpandPvsEvent`. I.e., if a player has a UI open, then the entity associated with that UI will always get sent to the player by the PVS system.
* Added `cvar_subs` command for listing all subscribers to cvar changes
* Entity categories have been reworked
* Each category now has a `HideSpawnMenu` field. The old `HideSpawnMenu` category is now just a normal category with that field set to true.
* Reworked category inheritance. Inheritance can now be disabled per category using a `Inheritable` field.
* Entity prototypes can now be automatically added to categories based on the components that they have, either by specifying components when defining the category in yml, or by adding the EntityCategoryAttribute to the component class.
### Bugfixes
* Fixed client-side BUI error log spam if an unknown entity has a UI open.
* Fixed placement manager spawning entities with incorrect rotations.
### Other
* Added a try-catch block to BUI constructors, to avoid clients getting stuck in error loops while applying states.
* Attempting to play sounds on terminating entities no longer logs an error.
## 223.3.0
### New features
* Better exception logging for IRobustJob.
* Add SetGridAudio helper for SharedAudioSystem.
### Bugfixes
* Fix placement manager not setting entity rotation correctly.
* Fix grid-based audio not playing correctly.
## 223.2.0
### New features

View File

@@ -2,4 +2,7 @@
id: Audio
name: Audio
description: Audio entity used by engine
save: false
save: false
components:
- type: Transform
gridTraversal: false

View File

@@ -1,7 +1,7 @@
- type: entity
id: debugRotation
abstract: true
categories: [ debug ]
categories: [ Debug ]
components:
- type: Sprite
netsync: false

View File

@@ -1,17 +1,20 @@
# debug related entities
- type: entityCategory
id: debug
id: Debug
name: entity-category-name-debug
description: entity-category-desc-debug
suffix: entity-category-suffix-debug
# entities that spawn other entities
- type: entityCategory
id: spawner
id: Spawner
name: entity-category-name-spawner
description: entity-category-desc-spawner
# entities that should be hidden from the spawn menu
# simple category that just exists to hide prototypes in spawn menus
- type: entityCategory
id: hideSpawnMenu
id: HideSpawnMenu
name: entity-category-name-hide
description: entity-category-desc-hide
hideSpawnMenu: true
inheritable: false

View File

@@ -9,6 +9,7 @@ cmd-parse-failure-float = {$arg} is not a valid float.
cmd-parse-failure-bool = {$arg} is not a valid bool.
cmd-parse-failure-uid = {$arg} is not a valid entity UID.
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
cmd-parse-failure-enum = {$arg} is not a {$enum} Enum.
cmd-parse-failure-grid = {$arg} is not a valid grid.
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
cmd-parse-failure-session = There is no session with username: {$username}
@@ -43,6 +44,13 @@ cmd-cvar-compl-list = List available CVars
cmd-cvar-arg-name = <name | ?>
cmd-cvar-value-hidden = <value hidden>
## 'cvar_subs' command
cmd-cvar_subs-desc = Lists the OnValueChanged subscriptions for a CVar.
cmd-cvar_subs-help = Usage: cvar_subs <name>
cmd-cvar_subs-invalid-args = Must provide exactly one argument.
cmd-cvar_subs-arg-name = <name>
## 'list' command
cmd-list-desc = Lists available commands, with optional search filter
cmd-list-help = Usage: list [filter]
@@ -245,9 +253,6 @@ cmd-bind-arg-command = <InputCommand>
cmd-net-draw-interp-desc = Toggles the debug drawing of the network interpolation.
cmd-net-draw-interp-help = Usage: net_draw_interp
cmd-net-draw-interp-desc = Toggles the debug drawing of the network interpolation.
cmd-net-draw-interp-help = Usage: net_draw_interp
cmd-net-watch-ent-desc = Dumps all network updates for an EntityId to the console.
cmd-net-watch-ent-help = Usage: net_watchent <0|EntityUid>
@@ -299,16 +304,9 @@ cmd-savegrid-help = savegrid <gridID> <Path>
cmd-testbed-desc = Loads a physics testbed on the specified map.
cmd-testbed-help = testbed <mapid> <test>
cmd-saveconfig-desc = Saves the client configuration to the config file.
cmd-saveconfig-help = saveconfig
## 'flushcookies' command
# Note: the flushcookies command is from Robust.Client.WebView, it's not in the main engine code.
cmd-flushcookies-desc = Flush CEF cookie storage to disk
cmd-flushcookies-help = This ensure cookies are properly saved to disk in the event of unclean shutdowns.
Note that the actual operation is asynchronous.
## 'addcomp' command
cmd-addcomp-desc = Adds a component to an entity.
cmd-addcomp-help = addcomp <uid> <componentName>
@@ -384,9 +382,9 @@ cmd-tp-desc = Teleports a player to any location in the round.
cmd-tp-help = tp <x> <y> [<mapID>]
cmd-tpto-desc = Teleports the current player or the specified players/entities to the location of the first player/entity.
cmd-tpto-help = tpto <username|uid> [username|uid]...
cmd-tpto-destination-hint = destination (uid or username)
cmd-tpto-victim-hint = entity to teleport (uid or username)
cmd-tpto-help = tpto <username|uid> [username|NetEntity]...
cmd-tpto-destination-hint = destination (NetEntity or username)
cmd-tpto-victim-hint = entity to teleport (NetEntity or username)
cmd-tpto-parse-error = Cant resolve entity or player: {$str}
cmd-listplayers-desc = Lists all players currently connected.
@@ -446,9 +444,6 @@ cmd-showanchored-help = Usage: showanchored
cmd-dmetamem-desc = Dumps a type's members in a format suitable for the sandbox configuration file.
cmd-dmetamem-help = Usage: dmetamem <type>
cmd-dmetamem-desc = Displays chunk bounds for the purposes of rendering.
cmd-dmetamem-help = Usage: showchunkbb <type>
cmd-launchauth-desc = Load authentication tokens from launcher data to aid in testing of live servers.
cmd-launchauth-help = Usage: launchauth <account name>
@@ -515,9 +510,6 @@ cmd-profsnap-help = Usage: profsnap
cmd-devwindow-desc = Dev Window
cmd-devwindow-help = Usage: devwindow
cmd-devwindow-desc = Open file
cmd-devwindow-help = Usage: testopenfile
cmd-scene-desc = Immediately changes the UI scene/state.
cmd-scene-help = Usage: scene <className>
@@ -528,14 +520,11 @@ cmd-hwid-desc = Returns the current HWID (HardWare ID).
cmd-hwid-help = Usage: hwid
cmd-vvread-desc = Retrieve a path's value using VV (View Variables).
cmd-vvread-desc = Usage: vvread <path>
cmd-vvread-help = Usage: vvread <path>
cmd-vvwrite-desc = Modify a path's value using VV (View Variables).
cmd-vvwrite-help = Usage: vvwrite <path>
cmd-vv-desc = Opens View Variables (VV).
cmd-vv-help = Usage: vv <path|entity ID|guihover>
cmd-vvinvoke-desc = Invoke/Call a path with arguments using VV.
cmd-vvinvoke-help = Usage: vvinvoke <path> [arguments...]

View File

@@ -1,8 +1,9 @@
entity-category-name-debug = Debug
entity-category-desc-debug = Entity prototypes intended for debugging & testing.
entity-category-suffix-debug = Debug
entity-category-name-spawner = Spawner
entity-category-desc-spawner = Entity prototypes that spawn other entities.
entity-category-name-hide = Hidden
entity-category-desc-hide = Entity prototypes that should be hidden from the spawn menu
entity-category-desc-hide = Entity prototypes that should be hidden from entity spawn menus

View File

@@ -0,0 +1,92 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.MustCallBaseAnalyzer>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
public sealed class MustCallBaseAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<MustCallBaseAnalyzer, NUnitVerifier>()
{
TestState =
{
Sources = { code }
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.IoC.MustCallBaseAttribute.cs"
);
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task Test()
{
const string code = """
using Robust.Shared.Analyzers;
public class Foo
{
[MustCallBase]
public virtual void Function()
{
}
[MustCallBase(true)]
public virtual void Function2()
{
}
}
public class Bar : Foo
{
public override void Function()
{
}
public override void Function2()
{
}
}
public class Baz : Foo
{
public override void Function()
{
base.Function();
}
}
public class Bal : Bar
{
public override void Function2()
{
}
}
""";
await Verifier(code,
// /0/Test0.cs(20,26): warning RA0028: Overriders of this function must always call the base function
VerifyCS.Diagnostic().WithSpan(20, 26, 20, 34),
// /0/Test0.cs(41,26): warning RA0028: Overriders of this function must always call the base function
VerifyCS.Diagnostic().WithSpan(41, 26, 41, 35));
}
}

View File

@@ -10,6 +10,7 @@
<ItemGroup>
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" LogicalName="Robust.Shared.Analyzers.AccessAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\AccessPermissions.cs" LogicalName="Robust.Shared.Analyzers.AccessPermissions.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\MustCallBaseAttribute.cs" LogicalName="Robust.Shared.IoC.MustCallBaseAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
</ItemGroup>

View File

@@ -0,0 +1,111 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
#nullable enable
/// <summary>
/// Enforces <c>MustCallBaseAttribute</c>.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MustCallBaseAnalyzer : DiagnosticAnalyzer
{
private const string Attribute = "Robust.Shared.Analyzers.MustCallBaseAttribute";
private static readonly DiagnosticDescriptor Rule = new(
Diagnostics.IdMustCallBase,
"No base call in overriden function",
"Overriders of this function must always call the base function",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method);
}
private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
if (context.Symbol is not IMethodSymbol { IsOverride: true } method)
return;
var attrSymbol = context.Compilation.GetTypeByMetadataName(Attribute);
if (attrSymbol == null)
return;
if (DoesMethodOverriderHaveAttribute(method, attrSymbol) is not { } data)
return;
if (data is { onlyOverrides: true, depth: < 2 })
return;
var syntax = (MethodDeclarationSyntax) method.DeclaringSyntaxReferences[0].GetSyntax();
if (HasBaseCall(syntax))
return;
var diag = Diagnostic.Create(Rule, syntax.Identifier.GetLocation());
context.ReportDiagnostic(diag);
}
private static (int depth, bool onlyOverrides)? DoesMethodOverriderHaveAttribute(
IMethodSymbol method,
INamedTypeSymbol attributeSymbol)
{
var depth = 0;
while (method.OverriddenMethod != null)
{
depth += 1;
method = method.OverriddenMethod;
if (GetAttribute(method, attributeSymbol) is not { } attribute)
continue;
var onlyOverrides = attribute.ConstructorArguments is [{Kind: TypedConstantKind.Primitive, Value: true}];
return (depth, onlyOverrides);
}
return null;
}
private static bool HasBaseCall(MethodDeclarationSyntax syntax)
{
return syntax.Accept(new BaseCallLocator());
}
private static AttributeData? GetAttribute(ISymbol namedTypeSymbol, INamedTypeSymbol attrSymbol)
{
return namedTypeSymbol.GetAttributes()
.SingleOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol));
}
private sealed class BaseCallLocator : CSharpSyntaxVisitor<bool>
{
public override bool VisitBaseExpression(BaseExpressionSyntax node)
{
return true;
}
public override bool DefaultVisit(SyntaxNode node)
{
foreach (var childNode in node.ChildNodes())
{
if (childNode is not CSharpSyntaxNode cSharpSyntax)
continue;
if (cSharpSyntax.Accept(this))
return true;
}
return false;
}
}
}

View File

@@ -26,7 +26,8 @@ public sealed class DefaultSQLConfig : IConfig
public IEnumerable<IExporter> GetExporters()
{
yield return SQLExporter.Default;
//yield return SQLExporter.Default;
yield break;
}
public IEnumerable<IColumnProvider> GetColumnProviders() => DefaultConfig.Instance.GetColumnProviders();

View File

@@ -15,11 +15,10 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Npgsql;
using Npgsql.Internal;
using Npgsql.Internal.TypeHandlers;
using Npgsql.Internal.TypeHandling;
namespace Robust.Benchmarks.Exporters;
/*
public sealed class SQLExporter : IExporter
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions
@@ -98,7 +97,9 @@ public sealed class SQLExporter : IExporter
public string Name => "sql";
}
*/
/*
// https://github.com/npgsql/efcore.pg/issues/1107#issuecomment-945126627
class JsonOverrideTypeHandlerResolverFactory : TypeHandlerResolverFactory
{
@@ -138,6 +139,7 @@ class JsonOverrideTypeHandlerResolverFactory : TypeHandlerResolverFactory
=> null; // Let the built-in resolver do this
}
}
*/
public sealed class DesignTimeContextFactoryPostgres : IDesignTimeDbContextFactory<BenchmarkContext>
{

View File

@@ -41,7 +41,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
[Dependency] private readonly IParallelManager _parMan = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
[Dependency] private readonly IAudioInternal _audio = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly SharedMapSystem _maps = default!;
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
@@ -216,11 +216,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
}
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
{
_metadata.SetFlag(entity.Owner, MetaDataFlags.Undetachable, true);
}
// Need to set all initial data for first frame.
ApplyAudioParams(component.Params, component);
component.Source.Global = component.Global;
@@ -336,55 +331,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
Vector2 worldPos;
var gridUid = xform.ParentUid;
component.Volume = component.Params.Volume;
Vector2 delta;
// Handle grid audio differently by using nearest-edge instead of entity centre.
// Handle grid audio differently by using grid position.
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
{
// It's our grid so max volume.
if (_listenerGrid == gridUid)
{
component.Volume = component.Params.Volume;
component.Occlusion = 0f;
component.Position = listener.Position;
return;
}
// TODO: Need a grid-optimised version because this is gonna be expensive.
// Just to avoid clipping on and off grid or nearestPoint changing we'll
// always set the sound to listener's pos, we'll just manually do gain ourselves.
if (_physics.TryGetNearest(gridUid, listener, out _, out var gridDistance))
{
// Out of range
if (gridDistance > component.MaxDistance)
{
component.Gain = 0f;
return;
}
var paramsGain = VolumeToGain(component.Params.Volume);
// Thought I'd never have to manually calculate gain again but this is the least
// unpleasant audio I could get at the moment.
component.Gain = paramsGain * _audio.GetAttenuationGain(
gridDistance,
component.Params.RolloffFactor,
component.Params.ReferenceDistance,
component.Params.MaxDistance);
component.Position = listener.Position;
return;
}
// Can't get nearest point so don't play anymore.
component.Gain = 0f;
return;
var parentUid = xform.ParentUid;
worldPos = _maps.GetGridPosition(parentUid);
}
else
{
worldPos = _xformSys.GetWorldPosition(entity);
}
worldPos = _xformSys.GetWorldPosition(entity);
component.Volume = component.Params.Volume;
// Max distance check
var delta = worldPos - listener.Position;
delta = worldPos - listener.Position;
var distance = delta.Length();
// Out of range so just clip it for us.
@@ -403,8 +365,15 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
// Update audio occlusion
var occlusion = GetOcclusion(listener, delta, distance, entity);
component.Occlusion = occlusion;
if ((component.Flags & AudioFlags.NoOcclusion) == AudioFlags.NoOcclusion)
{
component.Occlusion = 0f;
}
else
{
var occlusion = GetOcclusion(listener, delta, distance, entity);
component.Occlusion = occlusion;
}
// Update audio positions.
component.Position = worldPos;
@@ -671,10 +640,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, AudioStream stream)
{
var audioP = audioParams ?? AudioParams.Default;
var entity = EntityManager.CreateEntityUninitialized("Audio", MapCoordinates.Nullspace);
var comp = SetupAudio(entity, null, audioP, stream.Length);
LoadStream((entity, comp), stream);
var entity = SetupAudio(null, audioP, initialize: false, length: stream.Length);
LoadStream(entity, stream);
EntityManager.InitializeAndStartEntity(entity);
var comp = entity.Comp;
var source = comp.Source;
// TODO clamp the offset inside of SetPlaybackPosition() itself.

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.Console.Commands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var type = Type.GetType(args[0]);
var type = GetType(args[0]);
if (type == null)
{
@@ -25,6 +25,17 @@ namespace Robust.Client.Console.Commands
shell.WriteLine(sig);
}
}
private Type? GetType(string name)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.GetType(name) is { } type)
return type;
}
return null;
}
}
#endif
}

View File

@@ -368,7 +368,7 @@ namespace Robust.Client.Debugging
}
worldHandle.UseShader(null);
worldHandle.SetTransform(Matrix3.Identity);
worldHandle.SetTransform(Matrix3x2.Identity);
}
private void DrawScreen(DrawingHandleScreen screenHandle, OverlayDrawArgs args)
@@ -443,7 +443,7 @@ namespace Robust.Client.Debugging
}
screenHandle.UseShader(null);
screenHandle.SetTransform(Matrix3.Identity);
screenHandle.SetTransform(Matrix3x2.Identity);
}
protected internal override void Draw(in OverlayDrawArgs args)
@@ -520,11 +520,11 @@ namespace Robust.Client.Debugging
var matrix1 = xform1.WorldMatrix;
var matrix2 = xform2.WorldMatrix;
var xf1 = new Vector2(matrix1.R0C2, matrix1.R1C2);
var xf2 = new Vector2(matrix2.R0C2, matrix2.R1C2);
var xf1 = new Vector2(matrix1.M31, matrix1.M32);
var xf2 = new Vector2(matrix2.M31, matrix2.M32);
var p1 = matrix1.Transform(joint.LocalAnchorA);
var p2 = matrix2.Transform(joint.LocalAnchorB);
var p1 = Vector2.Transform(joint.LocalAnchorA, matrix1);
var p2 = Vector2.Transform(joint.LocalAnchorB, matrix2);
var xfa = new Transform(xf1, xform1.WorldRotation);
var xfb = new Transform(xf2, xform2.WorldRotation);

View File

@@ -150,7 +150,7 @@ namespace Robust.Client.GameObjects
[DataField("color")]
private Color color = Color.White;
public Matrix3 LocalMatrix = Matrix3.Identity;
public Matrix3x2 LocalMatrix = Matrix3x2.Identity;
[Animatable]
[ViewVariables(VVAccess.ReadWrite)]
@@ -389,10 +389,10 @@ namespace Robust.Client.GameObjects
internal void UpdateLocalMatrix()
{
LocalMatrix = Matrix3.CreateTransform(in offset, in rotation, in scale);
LocalMatrix = Matrix3Helpers.CreateTransform(in offset, in rotation, in scale);
}
public Matrix3 GetLocalMatrix()
public Matrix3x2 GetLocalMatrix()
{
return LocalMatrix;
}
@@ -1304,22 +1304,21 @@ namespace Robust.Client.GameObjects
// worldRotation + eyeRotation should be the angle of the entity on-screen. If no-rot is enabled this is just set to zero.
// However, at some point later the eye-matrix is applied separately, so we subtract -eye rotation for now:
var entityMatrix = Matrix3.CreateTransform(worldPosition, NoRotation ? -eyeRotation : worldRotation - cardinal);
var entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, NoRotation ? -eyeRotation : worldRotation - cardinal);
Matrix3.Multiply(in LocalMatrix, in entityMatrix, out var transformSprite);
var transformSprite = Matrix3x2.Multiply(LocalMatrix, entityMatrix);
if (GranularLayersRendering)
{
//Default rendering
entityMatrix = Matrix3.CreateTransform(worldPosition, worldRotation);
Matrix3.Multiply(in LocalMatrix, in entityMatrix, out var transformDefault);
entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, worldRotation);
var transformDefault = Matrix3x2.Multiply(LocalMatrix, entityMatrix);
//Snap to cardinals
entityMatrix = Matrix3.CreateTransform(worldPosition, worldRotation - angle.GetCardinalDir().ToAngle());
Matrix3.Multiply(in LocalMatrix, in entityMatrix, out var transformSnap);
entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, worldRotation - angle.GetCardinalDir().ToAngle());
var transformSnap = Matrix3x2.Multiply(LocalMatrix, entityMatrix);
//No rotation
entityMatrix = Matrix3.CreateTransform(worldPosition, -eyeRotation);
Matrix3.Multiply(in LocalMatrix, in entityMatrix, out var transformNoRot);
entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, -eyeRotation);
var transformNoRot = Matrix3x2.Multiply(LocalMatrix, entityMatrix);
foreach (var layer in Layers) {
switch (layer.RenderingStrategy)
@@ -1380,7 +1379,7 @@ namespace Robust.Client.GameObjects
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
var ev = new QueueSpriteTreeUpdateEvent(entities.GetComponent<TransformComponent>(Owner));
entities.EventBus.RaiseComponentEvent(this, ref ev);
entities.EventBus.RaiseComponentEvent(Owner, this, ref ev);
}
private void QueueUpdateIsInert()
@@ -1390,7 +1389,7 @@ namespace Robust.Client.GameObjects
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
var ev = new SpriteUpdateInertEvent();
entities.EventBus.RaiseComponentEvent(this, ref ev);
entities.EventBus.RaiseComponentEvent(Owner, this, ref ev);
}
[Obsolete("Use SpriteSystem instead.")]
@@ -1546,7 +1545,7 @@ namespace Robust.Client.GameObjects
private RSI.State? _actualState;
[ViewVariables] public RSI.State? ActualState => _actualState;
public Matrix3 LocalMatrix = Matrix3.Identity;
public Matrix3x2 LocalMatrix = Matrix3x2.Identity;
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 Scale
@@ -1692,7 +1691,7 @@ namespace Robust.Client.GameObjects
internal void UpdateLocalMatrix()
{
LocalMatrix = Matrix3.CreateTransform(in _offset, in _rotation, in _scale);
LocalMatrix = Matrix3Helpers.CreateTransform(in _offset, in _rotation, in _scale);
}
RSI? ISpriteLayer.Rsi { get => RSI; set => SetRsi(value); }
@@ -1964,27 +1963,27 @@ namespace Robust.Client.GameObjects
/// Given the apparent rotation of an entity on screen (world + eye rotation), get layer's matrix for drawing &
/// relevant RSI direction.
/// </summary>
public void GetLayerDrawMatrix(RsiDirection dir, out Matrix3 layerDrawMatrix)
public void GetLayerDrawMatrix(RsiDirection dir, out Matrix3x2 layerDrawMatrix)
{
if (_parent.NoRotation || dir == RsiDirection.South)
layerDrawMatrix = LocalMatrix;
else
{
Matrix3.Multiply(in _rsiDirectionMatrices[(int)dir], in LocalMatrix, out layerDrawMatrix);
layerDrawMatrix = Matrix3x2.Multiply(_rsiDirectionMatrices[(int)dir], LocalMatrix);
}
}
private static Matrix3[] _rsiDirectionMatrices = new Matrix3[]
private static Matrix3x2[] _rsiDirectionMatrices = new Matrix3x2[]
{
// array order chosen such that this array can be indexed by casing an RSI direction to an int
Matrix3.Identity, // should probably just avoid matrix multiplication altogether if the direction is south.
Matrix3.CreateRotation(-Direction.North.ToAngle()),
Matrix3.CreateRotation(-Direction.East.ToAngle()),
Matrix3.CreateRotation(-Direction.West.ToAngle()),
Matrix3.CreateRotation(-Direction.SouthEast.ToAngle()),
Matrix3.CreateRotation(-Direction.SouthWest.ToAngle()),
Matrix3.CreateRotation(-Direction.NorthEast.ToAngle()),
Matrix3.CreateRotation(-Direction.NorthWest.ToAngle())
Matrix3x2.Identity, // should probably just avoid matrix multiplication altogether if the direction is south.
Matrix3Helpers.CreateRotation(-Direction.North.ToAngle()),
Matrix3Helpers.CreateRotation(-Direction.East.ToAngle()),
Matrix3Helpers.CreateRotation(-Direction.West.ToAngle()),
Matrix3Helpers.CreateRotation(-Direction.SouthEast.ToAngle()),
Matrix3Helpers.CreateRotation(-Direction.SouthWest.ToAngle()),
Matrix3Helpers.CreateRotation(-Direction.NorthEast.ToAngle()),
Matrix3Helpers.CreateRotation(-Direction.NorthWest.ToAngle())
};
/// <summary>
@@ -2018,7 +2017,7 @@ namespace Robust.Client.GameObjects
/// <summary>
/// Render a layer. This assumes that the input angle is between 0 and 2pi.
/// </summary>
internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3 spriteMatrix, Angle angle, Direction? overrideDirection)
internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3x2 spriteMatrix, Angle angle, Direction? overrideDirection)
{
if (!Visible || Blank)
return;
@@ -2040,7 +2039,7 @@ namespace Robust.Client.GameObjects
if (CopyToShaderParameters == null)
{
// Set the drawing transform for this layer
Matrix3.Multiply(in layerMatrix, in spriteMatrix, out var transformMatrix);
var transformMatrix = Matrix3x2.Multiply(layerMatrix, spriteMatrix);
drawingHandle.SetTransform(in transformMatrix);
RenderTexture(drawingHandle, texture);

View File

@@ -11,6 +11,7 @@ namespace Robust.Client.GameObjects
{
private readonly List<Entity<AnimationPlayerComponent>> _activeAnimations = new();
private EntityQuery<AnimationPlayerComponent> _playerQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
[Dependency] private readonly IComponentFactory _compFact = default!;
@@ -18,6 +19,7 @@ namespace Robust.Client.GameObjects
public override void Initialize()
{
base.Initialize();
_playerQuery = GetEntityQuery<AnimationPlayerComponent>();
_metaQuery = GetEntityQuery<MetaDataComponent>();
}
@@ -74,7 +76,8 @@ namespace Robust.Client.GameObjects
foreach (var key in remie)
{
component.PlayingAnimations.Remove(key);
EntityManager.EventBus.RaiseLocalEvent(uid, new AnimationCompletedEvent {Uid = uid, Key = key}, true);
var completedEvent = new AnimationCompletedEvent {Uid = uid, Key = key, Finished = true};
EntityManager.EventBus.RaiseLocalEvent(uid, completedEvent, true);
}
return false;
@@ -171,31 +174,42 @@ namespace Robust.Client.GameObjects
return component.PlayingAnimations.ContainsKey(key);
}
[Obsolete]
public void Stop(AnimationPlayerComponent component, string key)
{
component.PlayingAnimations.Remove(key);
Stop((component.Owner, component), key);
}
public void Stop(EntityUid uid, string key)
public void Stop(Entity<AnimationPlayerComponent?> entity, string key)
{
if (!TryComp<AnimationPlayerComponent>(uid, out var player))
if (!_playerQuery.Resolve(entity.Owner, ref entity.Comp, false) ||
!entity.Comp.PlayingAnimations.Remove(key))
{
return;
}
player.PlayingAnimations.Remove(key);
var completedEvent = new AnimationCompletedEvent {Uid = entity.Owner, Key = key, Finished = false};
EntityManager.EventBus.RaiseLocalEvent(entity.Owner, completedEvent, true);
}
public void Stop(EntityUid uid, AnimationPlayerComponent? component, string key)
{
if (!Resolve(uid, ref component, false))
return;
component.PlayingAnimations.Remove(key);
Stop((uid, component), key);
}
}
/// <summary>
/// Raised whenever an animation stops, either due to running its course or being stopped manually.
/// </summary>
public sealed class AnimationCompletedEvent : EntityEventArgs
{
public EntityUid Uid { get; init; }
public string Key { get; init; } = string.Empty;
/// <summary>
/// If true, the animation finished by getting to its natural end.
/// If false, it was removed prematurely via <see cref="AnimationPlayerSystem.Stop(Robust.Client.GameObjects.AnimationPlayerComponent,string)"/> or similar overloads.
/// </summary>
public bool Finished { get; init; }
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.Containers;
@@ -118,7 +119,7 @@ public sealed class EntityLookupOverlay : Overlay
//DebugTools.Assert(!ent.IsInContainer(_entityManager));
var (entPos, entRot) = _transform.GetWorldPositionRotation(ent);
var lookupPos = invMatrix.Transform(entPos);
var lookupPos = Vector2.Transform(entPos, invMatrix);
var lookupRot = entRot - rotation;
var aabb = _lookup.GetAABB(ent, lookupPos, lookupRot, xform, _xformQuery);
@@ -127,6 +128,6 @@ public sealed class EntityLookupOverlay : Overlay
}
});
worldHandle.SetTransform(Matrix3.Identity);
worldHandle.SetTransform(Matrix3x2.Identity);
}
}

View File

@@ -109,7 +109,7 @@ namespace Robust.Client.GameObjects
}
}
worldHandle.SetTransform(Matrix3.Identity);
worldHandle.SetTransform(Matrix3x2.Identity);
}
}
}

View File

@@ -20,6 +20,7 @@ namespace Robust.Client.GameObjects
/// </summary>
public sealed class InputSystem : SharedInputSystem, IPostInjectInit
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientGameStateManager _stateManager = default!;
@@ -82,18 +83,35 @@ namespace Robust.Client.GameObjects
}
}
// send it off to the server
var clientMsg = (ClientFullInputCmdMessage)message;
var fullMsg = new FullInputCmdMessage(
clientMsg.Tick,
clientMsg.SubTick,
(int)clientMsg.InputSequence,
clientMsg.InputFunctionId,
clientMsg.State,
GetNetCoordinates(clientMsg.Coordinates),
clientMsg.ScreenCoordinates)
var clientMsg = message switch
{
Uid = GetNetEntity(clientMsg.Uid)
ClientFullInputCmdMessage clientInput => clientInput,
FullInputCmdMessage fullInput => new ClientFullInputCmdMessage(
fullInput.Tick,
fullInput.SubTick,
fullInput.InputFunctionId,
GetCoordinates(fullInput.Coordinates),
fullInput.ScreenCoordinates,
fullInput.State,
GetEntity(fullInput.Uid)),
_ => throw new ArgumentOutOfRangeException()
};
var fullMsg = message switch
{
FullInputCmdMessage fullInput => fullInput,
ClientFullInputCmdMessage client => new FullInputCmdMessage(
client.Tick,
client.SubTick,
client.InputFunctionId,
clientMsg.State,
GetNetCoordinates(client.Coordinates),
clientMsg.ScreenCoordinates,
GetNetEntity(clientMsg.Uid)
),
_ => throw new ArgumentOutOfRangeException()
};
DispatchInputCommand(clientMsg, fullMsg);
@@ -131,7 +149,7 @@ namespace Robust.Client.GameObjects
_conHost.RegisterCommand("incmd",
"Inserts an input command into the simulation",
"incmd <KeyFunction> <d|u KeyState> <wxPos> <wyPos>",
"incmd <KeyFunction> <KeyState> [wxPos] [wyPos]",
GenerateInputCommand);
}
@@ -147,17 +165,47 @@ namespace Robust.Client.GameObjects
if (_playerManager.LocalEntity is not { } pent)
return;
BoundKeyFunction keyFunction = new BoundKeyFunction(args[0]);
BoundKeyState state = args[1] == "u" ? BoundKeyState.Up: BoundKeyState.Down;
if (args.Length is not (2 or 4))
{
shell.WriteLine(Loc.GetString($"cmd-invalid-arg-number-error"));
return;
}
var pxform = Transform(pent);
var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3]));
var coords = EntityCoordinates.FromMap(pent, new MapCoordinates(wPos, pxform.MapID), _transform, EntityManager);
var keyFunction = new BoundKeyFunction(args[0]);
if (!Enum.TryParse<BoundKeyState>(args[1], out var state))
{
shell.WriteLine(Loc.GetString("cmd-parse-failure-enum", ("arg", args[1]), ("enum", nameof(BoundKeyState))));
return;
}
var wOffset = Vector2.Zero;
if (args.Length == 4)
{
if (!float.TryParse(args[2], out var wX))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[2])));
return;
}
if (!float.TryParse(args[3], out var wY))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[3])));
return;
}
wOffset = new Vector2(wX, wY);
}
var coords = EntityCoordinates.FromMap(pent, _transform.GetMapCoordinates(pent).Offset(wOffset), _transform, EntityManager);
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);
var message = new FullInputCmdMessage(_timing.CurTick, _timing.TickFraction, funcId, state,
GetNetCoordinates(coords), new ScreenCoordinates(0, 0, default), NetEntity.Invalid);
var message = new ClientFullInputCmdMessage(_timing.CurTick,
_timing.TickFraction,
funcId,
coords,
new ScreenCoordinates(0, 0, default),
state,
EntityUid.Invalid);
HandleInputCommand(_playerManager.LocalSession, keyFunction, message);
}

View File

@@ -184,7 +184,8 @@ namespace Robust.Client.GameObjects
/// <summary>
/// Gets the specified frame for this sprite at the specified time.
/// </summary>
public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime)
/// <param name="loop">Should we clamp on the last frame and not loop</param>
public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime, bool loop = true)
{
Texture? sprite = null;
@@ -196,19 +197,29 @@ namespace Robust.Client.GameObjects
var frames = state!.GetFrames(RsiDirection.South);
var delays = state.GetDelays();
var totalDelay = delays.Sum();
var time = curTime.TotalSeconds % totalDelay;
var delaySum = 0f;
for (var i = 0; i < delays.Length; i++)
// No looping
if (!loop && curTime.TotalSeconds >= totalDelay)
{
var delay = delays[i];
delaySum += delay;
sprite = frames[^1];
}
// Loopable
else
{
var time = curTime.TotalSeconds % totalDelay;
var delaySum = 0f;
if (time > delaySum)
continue;
for (var i = 0; i < delays.Length; i++)
{
var delay = delays[i];
delaySum += delay;
sprite = frames[i];
break;
if (time > delaySum)
continue;
sprite = frames[i];
break;
}
}
sprite ??= Frame0(spriteSpec);

View File

@@ -603,7 +603,7 @@ namespace Robust.Client.GameStates
if (compState != null)
{
var handleState = new ComponentHandleState(compState, null);
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
_entities.EventBus.RaiseComponentEvent(entity, comp, ref handleState);
}
comp.LastModifiedTick = _timing.LastRealTick;
@@ -640,7 +640,7 @@ namespace Robust.Client.GameStates
if (state != null)
{
var stateEv = new ComponentHandleState(state, null);
_entities.EventBus.RaiseComponentEvent(comp, ref stateEv);
_entities.EventBus.RaiseComponentEvent(entity, comp, ref stateEv);
}
comp.ClearCreationTick(); // don't undo the re-adding.
@@ -1361,7 +1361,7 @@ namespace Robust.Client.GameStates
continue;
var handleState = new ComponentHandleState(cur, next);
bus.RaiseComponentEvent(comp, ref handleState);
bus.RaiseComponentEvent(uid, comp, ref handleState);
}
}
@@ -1516,7 +1516,7 @@ namespace Robust.Client.GameStates
continue;
var handleState = new ComponentHandleState(state, null);
_entityManager.EventBus.RaiseComponentEvent(comp, ref handleState);
_entityManager.EventBus.RaiseComponentEvent(uid, comp, ref handleState);
}
// ensure we don't have any extra components

View File

@@ -101,18 +101,16 @@ namespace Robust.Client.Graphics
}
/// <inheritdoc />
public void GetScreenProjectionMatrix(out Matrix3 projMatrix)
public void GetScreenProjectionMatrix(out Matrix3x2 projMatrix)
{
Matrix3 result = default;
Matrix3x2 result = default;
result.R0C0 = PixelsPerMeter;
result.R1C1 = -PixelsPerMeter;
result.M11 = PixelsPerMeter;
result.M22 = -PixelsPerMeter;
var screenSize = _displayManager.ScreenSize;
result.R0C2 = screenSize.X / 2f;
result.R1C2 = screenSize.Y / 2f;
result.R2C2 = 1;
result.M31 = screenSize.X / 2f;
result.M32 = screenSize.Y / 2f;
/* column major
Sx 0 Tx

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Numerics;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Graphics;
@@ -44,7 +44,7 @@ namespace Robust.Client.Graphics
/// to UI screen space.
/// </summary>
/// <param name="projMatrix"></param>
void GetScreenProjectionMatrix(out Matrix3 projMatrix);
void GetScreenProjectionMatrix(out Matrix3x2 projMatrix);
/// <summary>
/// Projects a point from world space to UI screen space using the main viewport.

View File

@@ -367,7 +367,7 @@ namespace Robust.Client.Graphics.Clyde
_renderHandle.UseShader(entry.Sprite.PostShader);
CalcScreenMatrices(viewport.Size, out var proj, out var view);
_renderHandle.SetProjView(proj, view);
_renderHandle.SetModelTransform(Matrix3.Identity);
_renderHandle.SetModelTransform(Matrix3x2.Identity);
var rounded = roundedPos - entityPostRenderTarget.Size / 2;
@@ -530,7 +530,7 @@ namespace Robust.Client.Graphics.Clyde
// Because the math is wrong.
// So there are distortions from incorrect projection.
_renderHandle.UseShader(_fovDebugShaderInstance);
_renderHandle.DrawingHandleScreen.SetTransform(Matrix3.Identity);
_renderHandle.DrawingHandleScreen.SetTransform(Matrix3x2.Identity);
var pos = UIBox2.FromDimensions(viewport.Size / 2 - new Vector2(200, 200), new Vector2(400, 400));
_renderHandle.DrawingHandleScreen.DrawTextureRect(FovTexture, pos);
}
@@ -538,7 +538,7 @@ namespace Robust.Client.Graphics.Clyde
if (DebugLayers == ClydeDebugLayers.Light)
{
_renderHandle.UseShader(null);
_renderHandle.DrawingHandleScreen.SetTransform(Matrix3.Identity);
_renderHandle.DrawingHandleScreen.SetTransform(Matrix3x2.Identity);
_renderHandle.DrawingHandleScreen.DrawTextureRect(
viewport.WallBleedIntermediateRenderTarget2.Texture,
UIBox2.FromDimensions(Vector2.Zero, viewport.Size), new Color(1, 1, 1, 0.5f));

View File

@@ -98,28 +98,34 @@ namespace Robust.Client.Graphics.Clyde
[FieldOffset(20 * sizeof(float))] public Vector3 ViewMatrixC2;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ProjViewMatrices(in Matrix3 projMatrix, in Matrix3 viewMatrix)
public ProjViewMatrices(in Matrix3x2 projMatrix, in Matrix3x2 viewMatrix)
{
ProjMatrixC0 = new Vector3(projMatrix.R0C0, projMatrix.R1C0, projMatrix.R2C0);
ProjMatrixC1 = new Vector3(projMatrix.R0C1, projMatrix.R1C1, projMatrix.R2C1);
ProjMatrixC2 = new Vector3(projMatrix.R0C2, projMatrix.R1C2, projMatrix.R2C2);
// We put the rows of the input matrix into the columns of our GPU matrices
// this transpose is required, as in C#, we premultiply vectors with matrices
// (vM) while GL postmultiplies vectors with matrices (Mv); however, since
// the Matrix3x2 data is stored row-major, and GL uses column-major, the
// memory layout is the same (or would be, if Matrix3x2 didn't have an
// implicit column)
ProjMatrixC0 = new Vector3(projMatrix.M11, projMatrix.M12, 0);
ProjMatrixC1 = new Vector3(projMatrix.M21, projMatrix.M22, 0);
ProjMatrixC2 = new Vector3(projMatrix.M31, projMatrix.M32, 1);
ViewMatrixC0 = new Vector3(viewMatrix.R0C0, viewMatrix.R1C0, viewMatrix.R2C0);
ViewMatrixC1 = new Vector3(viewMatrix.R0C1, viewMatrix.R1C1, viewMatrix.R2C1);
ViewMatrixC2 = new Vector3(viewMatrix.R0C2, viewMatrix.R1C2, viewMatrix.R2C2);
ViewMatrixC0 = new Vector3(viewMatrix.M11, viewMatrix.M12, 0);
ViewMatrixC1 = new Vector3(viewMatrix.M21, viewMatrix.M22, 0);
ViewMatrixC2 = new Vector3(viewMatrix.M31, viewMatrix.M32, 1);
}
public void Apply(Clyde clyde, GLShaderProgram program)
{
program.SetUniformMaybe("projectionMatrix", new Matrix3(
ProjMatrixC0.X, ProjMatrixC1.X, ProjMatrixC2.X,
ProjMatrixC0.Y, ProjMatrixC1.Y, ProjMatrixC2.Y,
ProjMatrixC0.Z, ProjMatrixC1.Z, ProjMatrixC2.Z
program.SetUniformMaybe("projectionMatrix", new Matrix3x2(
ProjMatrixC0.X, ProjMatrixC0.Y, // Implicit 0
ProjMatrixC1.X, ProjMatrixC1.Y, // Implicit 0
ProjMatrixC2.X, ProjMatrixC2.Y // Implicit 1
));
program.SetUniformMaybe("viewMatrix", new Matrix3(
ViewMatrixC0.X, ViewMatrixC1.X, ViewMatrixC2.X,
ViewMatrixC0.Y, ViewMatrixC1.Y, ViewMatrixC2.Y,
ViewMatrixC0.Z, ViewMatrixC1.Z, ViewMatrixC2.Z
program.SetUniformMaybe("viewMatrix", new Matrix3x2(
ViewMatrixC0.X, ViewMatrixC0.Y, // Implicit 0
ViewMatrixC1.X, ViewMatrixC1.Y, // Implicit 0
ViewMatrixC2.X, ViewMatrixC2.Y // Implicit 1
));
}
}

View File

@@ -492,18 +492,18 @@ namespace Robust.Client.Graphics.Clyde
var offset = new Vector2(component.Radius, component.Radius);
Matrix3 matrix;
Matrix3x2 matrix;
if (mask == null)
{
matrix = Matrix3.Identity;
matrix = Matrix3x2.Identity;
}
else
{
// Only apply rotation if a mask is said, because else it doesn't matter.
matrix = Matrix3.CreateRotation(rotation);
matrix = Matrix3Helpers.CreateRotation(rotation);
}
(matrix.R0C2, matrix.R1C2) = lightPos;
(matrix.M31, matrix.M32) = lightPos;
_drawQuad(-offset, offset, matrix, lightShader);
}
@@ -692,7 +692,7 @@ namespace Robust.Client.Graphics.Clyde
// Blur horizontally to _wallBleedIntermediateRenderTarget1.
shader.SetUniformMaybe("direction", Vector2.UnitX);
_drawQuad(Vector2.Zero, viewport.Size, Matrix3.Identity, shader);
_drawQuad(Vector2.Zero, viewport.Size, Matrix3x2.Identity, shader);
SetTexture(TextureUnit.Texture0, viewport.LightBlurTarget.Texture);
@@ -700,7 +700,7 @@ namespace Robust.Client.Graphics.Clyde
// Blur vertically to _wallBleedIntermediateRenderTarget2.
shader.SetUniformMaybe("direction", Vector2.UnitY);
_drawQuad(Vector2.Zero, viewport.Size, Matrix3.Identity, shader);
_drawQuad(Vector2.Zero, viewport.Size, Matrix3x2.Identity, shader);
SetTexture(TextureUnit.Texture0, viewport.LightRenderTarget.Texture);
}
@@ -754,14 +754,14 @@ namespace Robust.Client.Graphics.Clyde
// Blur horizontally to _wallBleedIntermediateRenderTarget1.
shader.SetUniformMaybe("direction", Vector2.UnitX);
_drawQuad(Vector2.Zero, viewport.Size, Matrix3.Identity, shader);
_drawQuad(Vector2.Zero, viewport.Size, Matrix3x2.Identity, shader);
SetTexture(TextureUnit.Texture0, viewport.WallBleedIntermediateRenderTarget1.Texture);
BindRenderTargetFull(viewport.WallBleedIntermediateRenderTarget2);
// Blur vertically to _wallBleedIntermediateRenderTarget2.
shader.SetUniformMaybe("direction", Vector2.UnitY);
_drawQuad(Vector2.Zero, viewport.Size, Matrix3.Identity, shader);
_drawQuad(Vector2.Zero, viewport.Size, Matrix3x2.Identity, shader);
SetTexture(TextureUnit.Texture0, viewport.WallBleedIntermediateRenderTarget2.Texture);
}
@@ -909,13 +909,13 @@ namespace Robust.Client.Graphics.Clyde
// Second modification is that output must be fov-centred (difference-space)
uZero -= fovCentre;
var clipToDiff = new Matrix3(in uX, in uY, in uZero);
var clipToDiff = new Matrix3x2(uX.X, uX.Y, uY.X, uY.Y, uZero.X, uZero.Y);
fovShader.SetUniformMaybe("clipToDiff", clipToDiff);
_drawQuad(Vector2.Zero, Vector2.One, Matrix3.Identity, fovShader);
_drawQuad(Vector2.Zero, Vector2.One, Matrix3x2.Identity, fovShader);
}
private void UpdateOcclusionGeometry(MapId map, Box2 expandedBounds, Matrix3 eyeTransform)
private void UpdateOcclusionGeometry(MapId map, Box2 expandedBounds, Matrix3x2 eyeTransform)
{
using var _ = _prof.Group("UpdateOcclusionGeometry");
using var _p = DebugGroup(nameof(UpdateOcclusionGeometry));
@@ -968,9 +968,9 @@ namespace Robust.Client.Graphics.Clyde
var worldTransform = xformSystem.GetWorldMatrix(transform, xforms);
var box = occluder.BoundingBox;
var tl = worldTransform.Transform(box.TopLeft);
var tr = worldTransform.Transform(box.TopRight);
var br = worldTransform.Transform(box.BottomRight);
var tl = Vector2.Transform(box.TopLeft, worldTransform);
var tr = Vector2.Transform(box.TopRight, worldTransform);
var br = Vector2.Transform(box.BottomRight, worldTransform);
var bl = tl + br - tr;
// Faces.
@@ -1010,9 +1010,9 @@ namespace Robust.Client.Graphics.Clyde
//
// Calculate delta positions from camera.
var dTl = eyeTransform.Transform(tl);
var dTr = eyeTransform.Transform(tr);
var dBl = eyeTransform.Transform(bl);
var dTl = Vector2.Transform(tl, eyeTransform);
var dTr = Vector2.Transform(tr, eyeTransform);
var dBl = Vector2.Transform(bl, eyeTransform);
var dBr = dBl + dTr - dTl;
// Get which neighbors are occluding.

View File

@@ -33,17 +33,17 @@ namespace Robust.Client.Graphics.Clyde
DrawingHandleWorld = new DrawingHandleWorldImpl(white, this);
}
public void SetModelTransform(in Matrix3 matrix)
public void SetModelTransform(in Matrix3x2 matrix)
{
_clyde.DrawSetModelTransform(matrix);
}
public Matrix3 GetModelTransform()
public Matrix3x2 GetModelTransform()
{
return _clyde.DrawGetModelTransform();
}
public void SetProjView(in Matrix3 proj, in Matrix3 view)
public void SetProjView(in Matrix3x2 proj, in Matrix3x2 view)
{
_clyde.DrawSetProjViewTransform(proj, view);
}
@@ -177,9 +177,9 @@ namespace Robust.Client.Graphics.Clyde
var oldModel = _clyde._currentMatrixModel;
var newModel = oldModel;
position += new Vector2(oldModel.R0C2, oldModel.R1C2);
newModel.R0C2 = 0;
newModel.R1C2 = 0;
position += new Vector2(oldModel.M31, oldModel.M32);
newModel.M31 = 0;
newModel.M32 = 0;
SetModelTransform(newModel);
// Switch rendering to pseudo-world space.
@@ -194,7 +194,7 @@ namespace Robust.Client.Graphics.Clyde
// Maaaaybe this is meant to have a minus sign.
var rot = -(float) eyeRot.Theta;
var view = Matrix3.CreateTransform(ofsX, ofsY, rot, scale.X, scale.Y);
var view = Matrix3Helpers.CreateTransform(ofsX, ofsY, rot, scale.X, scale.Y);
SetProjView(proj, view);
}
@@ -297,12 +297,12 @@ namespace Robust.Client.Graphics.Clyde
_renderHandle = renderHandle;
}
public override void SetTransform(in Matrix3 matrix)
public override void SetTransform(in Matrix3x2 matrix)
{
_renderHandle.SetModelTransform(matrix);
}
public override Matrix3 GetTransform()
public override Matrix3x2 GetTransform()
{
return _renderHandle.GetModelTransform();
}
@@ -402,12 +402,12 @@ namespace Robust.Client.Graphics.Clyde
_renderHandle = renderHandle;
}
public override void SetTransform(in Matrix3 matrix)
public override void SetTransform(in Matrix3x2 matrix)
{
_renderHandle.SetModelTransform(matrix);
}
public override Matrix3 GetTransform()
public override Matrix3x2 GetTransform()
{
return _renderHandle.GetModelTransform();
}

View File

@@ -62,7 +62,7 @@ namespace Robust.Client.Graphics.Clyde
// This matrix is applied to most normal geometry coming in.
// Some is applied while the batch is being created (e.g. simple texture draw calls).
// For DrawPrimitives OTOH the model matrix is passed along with the render command so is applied in the shader.
private Matrix3 _currentMatrixModel = Matrix3.Identity;
private Matrix3x2 _currentMatrixModel = Matrix3x2.Identity;
// Buffers and data for the batching system. Written into during (queue) and processed during (submit).
private readonly Vertex2D[] BatchVertexData = new Vertex2D[MaxBatchQuads * 4];
@@ -84,8 +84,8 @@ namespace Robust.Client.Graphics.Clyde
// Current projection & view matrices that are being used ot render.
// This gets updated to keep track during (queue) and (misc), but not during (submit).
private Matrix3 _currentMatrixProj;
private Matrix3 _currentMatrixView;
private Matrix3x2 _currentMatrixProj;
private Matrix3x2 _currentMatrixView;
// (queue) and (misc), current state of the scissor test. Null if disabled.
private UIBox2i? _currentScissorState;
@@ -110,25 +110,25 @@ namespace Robust.Client.Graphics.Clyde
UniformConstantsUBO.Reallocate(constants);
}
private void CalcScreenMatrices(in Vector2i screenSize, out Matrix3 proj, out Matrix3 view)
private void CalcScreenMatrices(in Vector2i screenSize, out Matrix3x2 proj, out Matrix3x2 view)
{
proj = Matrix3.Identity;
proj.R0C0 = 2f / screenSize.X;
proj.R1C1 = -2f / screenSize.Y;
proj.R0C2 = -1;
proj.R1C2 = 1;
proj = Matrix3x2.Identity;
proj.M11 = 2f / screenSize.X;
proj.M22 = -2f / screenSize.Y;
proj.M31 = -1;
proj.M32 = 1;
if (_currentRenderTarget.FlipY)
{
proj.R1C1 *= -1;
proj.R1C2 *= -1;
proj.M22 *= -1;
proj.M32 *= -1;
}
view = Matrix3.Identity;
view = Matrix3x2.Identity;
}
private void CalcWorldMatrices(in Vector2i screenSize, in Vector2 renderScale, IEye eye,
out Matrix3 proj, out Matrix3 view)
out Matrix3x2 proj, out Matrix3x2 view)
{
eye.GetViewMatrix(out view, renderScale);
@@ -136,20 +136,20 @@ namespace Robust.Client.Graphics.Clyde
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CalcWorldProjMatrix(in Vector2i screenSize, out Matrix3 proj)
private void CalcWorldProjMatrix(in Vector2i screenSize, out Matrix3x2 proj)
{
proj = Matrix3.Identity;
proj.R0C0 = EyeManager.PixelsPerMeter * 2f / screenSize.X;
proj.R1C1 = EyeManager.PixelsPerMeter * 2f / screenSize.Y;
proj = Matrix3x2.Identity;
proj.M11 = EyeManager.PixelsPerMeter * 2f / screenSize.X;
proj.M22 = EyeManager.PixelsPerMeter * 2f / screenSize.Y;
if (_currentRenderTarget.FlipY)
{
proj.R1C1 *= -1;
proj.R1C2 *= -1;
proj.M22 *= -1;
proj.M32 *= -1;
}
}
private void SetProjViewBuffer(in Matrix3 proj, in Matrix3 view)
private void SetProjViewBuffer(in Matrix3x2 proj, in Matrix3x2 view)
{
// TODO: Fix perf here.
// This immediately causes a glBufferData() call every time this is changed.
@@ -160,7 +160,7 @@ namespace Robust.Client.Graphics.Clyde
ProjViewUBO.Reallocate(combined);
}
private void SetProjViewFull(in Matrix3 proj, in Matrix3 view)
private void SetProjViewFull(in Matrix3x2 proj, in Matrix3x2 view)
{
_currentMatrixProj = proj;
_currentMatrixView = view;
@@ -285,21 +285,21 @@ namespace Robust.Client.Graphics.Clyde
};
}
private void _drawQuad(Vector2 a, Vector2 b, in Matrix3 modelMatrix, GLShaderProgram program)
private void _drawQuad(Vector2 a, Vector2 b, in Matrix3x2 modelMatrix, GLShaderProgram program)
{
DrawQuadWithVao(QuadVAO, a, b, modelMatrix, program);
}
private void DrawQuadWithVao(GLHandle vao, Vector2 a, Vector2 b, in Matrix3 modelMatrix,
private void DrawQuadWithVao(GLHandle vao, Vector2 a, Vector2 b, in Matrix3x2 modelMatrix,
GLShaderProgram program)
{
BindVertexArray(vao.Handle);
CheckGlError();
var rectTransform = Matrix3.Identity;
(rectTransform.R0C0, rectTransform.R1C1) = b - a;
(rectTransform.R0C2, rectTransform.R1C2) = a;
rectTransform.Multiply(modelMatrix);
var rectTransform = Matrix3x2.Identity;
(rectTransform.M11, rectTransform.M22) = b - a;
(rectTransform.M31, rectTransform.M32) = a;
rectTransform = rectTransform * modelMatrix;
program.SetUniformMaybe(UniIModelMatrix, rectTransform);
_debugStats.LastGLDrawCalls += 1;
@@ -315,7 +315,7 @@ namespace Robust.Client.Graphics.Clyde
FlushBatchQueue();
// Reset renderer state.
_currentMatrixModel = Matrix3.Identity;
_currentMatrixModel = Matrix3x2.Identity;
_queuedShaderInstance = _defaultShader;
SetScissorFull(null);
}
@@ -496,7 +496,7 @@ namespace Robust.Client.Graphics.Clyde
case bool b:
program.SetUniform(name, b ? 1 : 0);
break;
case Matrix3 matrix3:
case Matrix3x2 matrix3:
program.SetUniform(name, matrix3);
break;
case Matrix4 matrix4:
@@ -532,17 +532,17 @@ namespace Robust.Client.Graphics.Clyde
return ref command;
}
private void DrawSetModelTransform(in Matrix3 matrix)
private void DrawSetModelTransform(in Matrix3x2 matrix)
{
_currentMatrixModel = matrix;
}
private Matrix3 DrawGetModelTransform()
private Matrix3x2 DrawGetModelTransform()
{
return _currentMatrixModel;
}
private void DrawSetProjViewTransform(in Matrix3 proj, in Matrix3 view)
private void DrawSetProjViewTransform(in Matrix3x2 proj, in Matrix3x2 view)
{
BreakBatch();
@@ -571,9 +571,9 @@ namespace Robust.Client.Graphics.Clyde
EnsureBatchSpaceAvailable(4, GetQuadBatchIndexCount());
EnsureBatchState(texture, true, GetQuadBatchPrimitiveType(), _queuedShader);
bl = _currentMatrixModel.Transform(bl);
br = _currentMatrixModel.Transform(br);
tr = _currentMatrixModel.Transform(tr);
bl = Vector2.Transform(bl, _currentMatrixModel);
br = Vector2.Transform(br, _currentMatrixModel);
tr = Vector2.Transform(tr, _currentMatrixModel);
tl = tr + bl - br;
// TODO: split batch if necessary.
@@ -676,8 +676,8 @@ namespace Robust.Client.Graphics.Clyde
EnsureBatchSpaceAvailable(2, 0);
EnsureBatchState(_stockTextureWhite.TextureId, false, BatchPrimitiveType.LineList, _queuedShader);
a = _currentMatrixModel.Transform(a);
b = _currentMatrixModel.Transform(b);
a = Vector2.Transform(a, _currentMatrixModel);
b = Vector2.Transform(b, _currentMatrixModel);
// TODO: split batch if necessary.
var vIdx = BatchVertexIndex;
@@ -807,7 +807,7 @@ namespace Robust.Client.Graphics.Clyde
command.DrawBatch.ShaderInstance = metaData.ShaderInstance;
command.DrawBatch.Count = currentIndex - metaData.StartIndex;
command.DrawBatch.ModelMatrix = Matrix3.Identity;
command.DrawBatch.ModelMatrix = Matrix3x2.Identity;
_debugStats.LastBatches += 1;
}
@@ -882,7 +882,7 @@ namespace Robust.Client.Graphics.Clyde
_queuedRenderCommands.Clear();
_currentViewport = null;
_lightingReady = false;
_currentMatrixModel = Matrix3.Identity;
_currentMatrixModel = Matrix3x2.Identity;
SetScissorFull(null);
BindRenderTargetFull(_mainWindow!.RenderTarget);
_batchMetaData = null;
@@ -961,13 +961,13 @@ namespace Robust.Client.Graphics.Clyde
public BatchPrimitiveType PrimitiveType;
// TODO: this makes the render commands so much more large please remove.
public Matrix3 ModelMatrix;
public Matrix3x2 ModelMatrix;
}
private struct RenderCommandProjViewMatrix
{
public Matrix3 ProjMatrix;
public Matrix3 ViewMatrix;
public Matrix3x2 ProjMatrix;
public Matrix3x2 ViewMatrix;
}
private struct RenderCommandScissor
@@ -1056,11 +1056,11 @@ namespace Robust.Client.Graphics.Clyde
private readonly struct FullStoredRendererState
{
public readonly Matrix3 ProjMatrix;
public readonly Matrix3 ViewMatrix;
public readonly Matrix3x2 ProjMatrix;
public readonly Matrix3x2 ViewMatrix;
public readonly LoadedRenderTarget RenderTarget;
public FullStoredRendererState(in Matrix3 projMatrix, in Matrix3 viewMatrix,
public FullStoredRendererState(in Matrix3x2 projMatrix, in Matrix3x2 viewMatrix,
LoadedRenderTarget renderTarget)
{
ProjMatrix = projMatrix;

View File

@@ -506,7 +506,7 @@ namespace Robust.Client.Graphics.Clyde
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, in Matrix3 value)
private protected override void SetParameterImpl(string name, in Matrix3x2 value)
{
var data = Parent._shaderInstances[Handle];
data.ParametersDirty = true;

View File

@@ -52,7 +52,7 @@ namespace Robust.Client.Graphics.Clyde
// view matrix
vp.Eye.GetViewMatrixInv(out var viewMatrixInv, vp.RenderScale);
point = viewMatrixInv * point;
point = Vector2.Transform(point, viewMatrixInv);
return point;
}
@@ -136,7 +136,7 @@ namespace Robust.Client.Graphics.Clyde
// view matrix
Eye.GetViewMatrixInv(out var viewMatrixInv, RenderScale);
newPoint = viewMatrixInv * newPoint;
newPoint = Vector2.Transform(newPoint, viewMatrixInv);
return new MapCoordinates(newPoint, Eye.Position.MapId);
}
@@ -150,7 +150,7 @@ namespace Robust.Client.Graphics.Clyde
var newPoint = point;
eye.GetViewMatrix(out var viewMatrix, RenderScale);
newPoint = viewMatrix * newPoint;
newPoint = Vector2.Transform(newPoint, viewMatrix);
// (inlined version of UiProjMatrix)
newPoint *= new Vector2(1, -1) * EyeManager.PixelsPerMeter;
@@ -159,14 +159,14 @@ namespace Robust.Client.Graphics.Clyde
return newPoint;
}
public Matrix3 GetWorldToLocalMatrix()
public Matrix3x2 GetWorldToLocalMatrix()
{
if (Eye == null)
return Matrix3.Identity;
return Matrix3x2.Identity;
Eye.GetViewMatrix(out var viewMatrix, RenderScale * new Vector2(EyeManager.PixelsPerMeter, -EyeManager.PixelsPerMeter));
viewMatrix.R0C2 += Size.X / 2f;
viewMatrix.R1C2 += Size.Y / 2f;
viewMatrix.M31 += Size.X / 2f;
viewMatrix.M32 += Size.Y / 2f;
return viewMatrix;
}

View File

@@ -361,7 +361,7 @@ namespace Robust.Client.Graphics.Clyde
{
}
private protected override void SetParameterImpl(string name, in Matrix3 value)
private protected override void SetParameterImpl(string name, in Matrix3x2 value)
{
}
@@ -482,7 +482,7 @@ namespace Robust.Client.Graphics.Clyde
return default;
}
public Matrix3 GetWorldToLocalMatrix() => default;
public Matrix3x2 GetWorldToLocalMatrix() => default;
public Vector2 WorldToLocal(Vector2 point)
{

View File

@@ -247,28 +247,33 @@ namespace Robust.Client.Graphics.Clyde
GL.Uniform1(uniformId, singles.Length, singles);
}
public void SetUniform(string uniformName, in Matrix3 matrix)
public void SetUniform(string uniformName, in Matrix3x2 matrix)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, matrix);
}
public void SetUniform(int uniformName, in Matrix3 matrix)
public void SetUniform(int uniformName, in Matrix3x2 matrix)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, matrix);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe void SetUniformDirect(int slot, in Matrix3 value, bool transpose=true)
private unsafe void SetUniformDirect(int slot, in Matrix3x2 value)
{
Matrix3 tmpTranspose = value;
if (transpose)
{
// transposition not supported on GLES2, & no access to _hasGLES
tmpTranspose.Transpose();
}
GL.UniformMatrix3(slot, 1, false, (float*) &tmpTranspose);
// We put the rows of the input matrix into the columns of our GPU matrices
// this transpose is required, as in C#, we premultiply vectors with matrices
// (vM) while GL postmultiplies vectors with matrices (Mv); however, since
// the Matrix3x2 data is stored row-major, and GL uses column-major, the
// memory layout is the same (or would be, if Matrix3x2 didn't have an
// implicit column)
var buf = stackalloc float[9]{
value.M11, value.M12, 0,
value.M21, value.M22, 0,
value.M31, value.M32, 1
};
GL.UniformMatrix3(slot, 1, false, (float*)buf);
_clyde.CheckGlError();
}
@@ -474,7 +479,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
public void SetUniformMaybe(string uniformName, in Matrix3 value)
public void SetUniformMaybe(string uniformName, in Matrix3x2 value)
{
if (TryGetUniform(uniformName, out var slot))
{
@@ -490,7 +495,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
public void SetUniformMaybe(int uniformName, in Matrix3 value)
public void SetUniformMaybe(int uniformName, in Matrix3x2 value)
{
if (TryGetUniform(uniformName, out var slot))
{

View File

@@ -44,19 +44,19 @@ namespace Robust.Client.Graphics
{
CheckDisposed();
var matrix = Matrix3.CreateTransform(in position, in rotation, in scale);
var matrix = Matrix3Helpers.CreateTransform(in position, in rotation, in scale);
SetTransform(in matrix);
}
public void SetTransform(in Vector2 position, in Angle rotation)
{
var matrix = Matrix3.CreateTransform(in position, in rotation);
var matrix = Matrix3Helpers.CreateTransform(in position, in rotation);
SetTransform(in matrix);
}
public abstract void SetTransform(in Matrix3 matrix);
public abstract void SetTransform(in Matrix3x2 matrix);
public abstract Matrix3 GetTransform();
public abstract Matrix3x2 GetTransform();
public abstract void UseShader(ShaderInstance? shader);

View File

@@ -49,7 +49,7 @@ namespace Robust.Client.Graphics
/// <summary>
/// Matrix equivalent of <see cref="LocalToWorld(Vector2)"/>.
/// </summary>
Matrix3 GetWorldToLocalMatrix();
Matrix3x2 GetWorldToLocalMatrix();
/// <summary>
/// Converts a point in world-space to the viewport's screen coordinates.

View File

@@ -22,7 +22,7 @@ namespace Robust.Client.Graphics
/// If set to true, <see cref="ScreenTexture"/> will be set to the current frame (at the moment before the overlay is rendered). This can be costly to performance, but
/// some shaders will require it as a passed in uniform to operate.
/// </summary>
public virtual bool RequestScreenTexture => false;
public virtual bool RequestScreenTexture { get; set; } = false;
/// <summary>
/// If <see cref="RequestScreenTexture"> is true, then this will be set to the texture corresponding to the current frame. If false, it will always be null.

View File

@@ -148,7 +148,7 @@ namespace Robust.Client.Graphics
SetParameterImpl(name, value);
}
public void SetParameter(string name, in Matrix3 value)
public void SetParameter(string name, in Matrix3x2 value)
{
EnsureAlive();
EnsureMutable();
@@ -219,7 +219,7 @@ namespace Robust.Client.Graphics
private protected abstract void SetParameterImpl(string name, int value);
private protected abstract void SetParameterImpl(string name, Vector2i value);
private protected abstract void SetParameterImpl(string name, bool value);
private protected abstract void SetParameterImpl(string name, in Matrix3 value);
private protected abstract void SetParameterImpl(string name, in Matrix3x2 value);
private protected abstract void SetParameterImpl(string name, in Matrix4 value);
private protected abstract void SetParameterImpl(string name, Texture value);
private protected abstract void SetStencilImpl(StencilParameters value);

View File

@@ -176,7 +176,7 @@ namespace Robust.Client.Graphics
return node.AsVector4();
}
case ShaderDataType.Mat3:
return node.AsMatrix3();
return node.AsMatrix3x2();
case ShaderDataType.Mat4:
return node.AsMatrix4();
default:
@@ -219,7 +219,7 @@ namespace Robust.Client.Graphics
case bool i:
instance.SetParameter(key, i);
break;
case Matrix3 i:
case Matrix3x2 i:
instance.SetParameter(key, i);
break;
case Matrix4 i:

View File

@@ -120,6 +120,6 @@ public sealed class TileEdgeOverlay : GridOverlay
}
}
args.WorldHandle.SetTransform(Matrix3.Identity);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
}

View File

@@ -139,7 +139,7 @@ namespace Robust.Client.Physics
return true;
}, true);
worldHandle.SetTransform(Matrix3.Identity);
worldHandle.SetTransform(Matrix3x2.Identity);
}
}
}

View File

@@ -2,6 +2,7 @@ using System.Buffers;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;

View File

@@ -261,8 +261,8 @@ namespace Robust.Client.Player
{
// This is a new userid, so we create a new session.
DebugTools.Assert(state.UserId != LocalPlayer?.UserId);
var newSession = (CommonSession) CreateAndAddSession(state.UserId, state.Name);
newSession.Ping = state.Ping;
var newSession = (ICommonSessionInternal)CreateAndAddSession(state.UserId, state.Name);
newSession.SetPing(state.Ping);
SetStatus(newSession, state.Status);
SetAttachedEntity(newSession, controlled, out _, true);
dirty = true;
@@ -279,9 +279,9 @@ namespace Robust.Client.Player
}
dirty = true;
var local = (CommonSession) session;
local.Name = state.Name;
local.Ping = state.Ping;
var local = (ICommonSessionInternal)session;
local.SetName(state.Name);
local.SetPing(state.Ping);
SetStatus(local, state.Status);
SetAttachedEntity(local, controlled, out _, true);
}

View File

@@ -20,6 +20,31 @@ namespace Robust.Client.Replays.Loading;
// so that when jumping to tick 1001 the client only has to apply states for tick 1000 and 1001, instead of 0, 1, 2, ...
public sealed partial class ReplayLoadManager
{
// Scratch data used by UpdateEntityStates.
// Avoids copying changes for every change to an entity between checkpoints, instead copies once per checkpoint on
// first change. We can also use this to avoid building a dictionary of ComponentChange inside the inner loop.
private class UpdateScratchData
{
public Dictionary<ushort, ComponentChange> Changes;
public EntityState lastChange;
public HashSet<ushort>? netComps;
public UpdateScratchData(EntityState oldEntState)
{
Changes = oldEntState.ComponentChanges.Value.ToDictionary(x => x.NetID);
lastChange = oldEntState;
netComps = oldEntState.NetComponents;
}
public EntityState BakeChanges()
{
return new EntityState(lastChange.NetEntity,
Changes.Values.ToList(),
lastChange.EntityLastModified,
netComps);
}
}
public async Task<(CheckpointState[], TimeSpan[])> GenerateCheckpointsAsync(
ReplayMessage? initMessages,
HashSet<string> initialCvars,
@@ -138,6 +163,7 @@ public sealed partial class ReplayLoadManager
var stats_due_spawned = 0;
var stats_due_state = 0;
var modifiedEntities = new Dictionary<NetEntity, UpdateScratchData>();
for (var i = 1; i < states.Count; i++)
{
if (i % 10 == 0)
@@ -148,10 +174,10 @@ public sealed partial class ReplayLoadManager
DebugTools.Assert(curState.FromSequence <= lastState.ToSequence);
UpdatePlayerStates(curState.PlayerStates.Span, playerStates);
UpdateEntityStates(curState.EntityStates.Span, entStates, ref spawnedTracker, ref stateTracker, detached);
UpdateEntityStates(curState.EntityStates.Span, entStates, modifiedEntities, ref spawnedTracker, ref stateTracker, detached);
UpdateMessages(messages[i], uploadedFiles, prototypes, cvars, detachQueue, ref timeBase);
ProcessQueue(curState.ToSequence, detachQueue, detached, entStates);
UpdateDeletions(curState.EntityDeletions, entStates, detached);
UpdateDeletions(curState.EntityDeletions, entStates, detached, modifiedEntities);
serverTime[i] = GetTime(curState.ToSequence) - initialTime;
ticksSinceLastCheckpoint++;
@@ -182,6 +208,8 @@ public sealed partial class ReplayLoadManager
ticksSinceLastCheckpoint = 0;
spawnedTracker = 0;
stateTracker = 0;
ApplyModifiedEntities(entStates, modifiedEntities);
var newState = new GameState(GameTick.Zero,
curState.ToSequence,
default,
@@ -339,16 +367,18 @@ public sealed partial class ReplayLoadManager
}
private void UpdateDeletions(NetListAsArray<NetEntity> entityDeletions,
Dictionary<NetEntity, EntityState> entStates, HashSet<NetEntity> detached)
Dictionary<NetEntity, EntityState> entStates, HashSet<NetEntity> detached, Dictionary<NetEntity, UpdateScratchData> modifiedEntities)
{
foreach (var ent in entityDeletions.Span)
{
entStates.Remove(ent);
detached.Remove(ent);
modifiedEntities.Remove(ent);
}
}
private void UpdateEntityStates(ReadOnlySpan<EntityState> span, Dictionary<NetEntity, EntityState> entStates,
Dictionary<NetEntity, UpdateScratchData> modified,
ref int spawnedTracker, ref int stateTracker, HashSet<NetEntity> detached)
{
foreach (var entState in span)
@@ -369,9 +399,22 @@ public sealed partial class ReplayLoadManager
continue;
}
// Get scratch versions (with write access) for entities modified since last checkpoint
UpdateScratchData? scratch;
if (!modified.TryGetValue(entState.NetEntity, out scratch))
{
scratch = new UpdateScratchData(oldEntState);
modified[entState.NetEntity] = scratch;
}
stateTracker++;
DebugTools.Assert(oldEntState.NetEntity == entState.NetEntity);
entStates[entState.NetEntity] = MergeStates(entState, oldEntState.ComponentChanges.Value, oldEntState.NetComponents);
// Note this does not change entStates, that change occurs later in ApplyModifiedEntities (to avoid early copies)
UpdateScratch(entState, scratch.Changes);
if (entState.NetComponents != null)
scratch.netComps = entState.NetComponents;
scratch.lastChange = entState;
#if DEBUG
foreach (var state in entStates[entState.NetEntity].ComponentChanges.Span)
@@ -382,6 +425,53 @@ public sealed partial class ReplayLoadManager
}
}
private void ApplyModifiedEntities(Dictionary<NetEntity, EntityState> entStates, Dictionary<NetEntity, UpdateScratchData> modifiedEntities)
{
foreach (var modified in modifiedEntities)
{
entStates[modified.Key] = modified.Value.BakeChanges();
}
modifiedEntities.Clear();
}
private void UpdateScratch(
EntityState newState,
Dictionary<ushort, ComponentChange> oldState)
{
// remove any deleted components
if (newState.NetComponents != null)
{
foreach (var change in oldState.Values)
{
if (!newState.NetComponents.Contains(change.NetID))
oldState.Remove(change.NetID);
}
}
foreach (var newCompState in newState.ComponentChanges.Value)
{
if (!oldState.TryGetValue(newCompState.NetID, out var existing))
{
// This is a new component
// I'm not 100% sure about this, but I think delta states should always be full states here?
DebugTools.Assert(newCompState.State is not IComponentDeltaState newDelta);
oldState[newCompState.NetID] = newCompState;
continue;
}
// Modify or replace existing component
if (newCompState.State is not IComponentDeltaState delta)
{
oldState[newCompState.NetID] = newCompState;
continue;
}
DebugTools.Assert(existing.State != null && existing.State is not IComponentDeltaState);
oldState[newCompState.NetID] = new ComponentChange(existing.NetID, delta.CreateNewFullState(existing.State!), newCompState.LastModifiedTick);
}
}
private EntityState MergeStates(
EntityState newState,
IReadOnlyCollection<ComponentChange> oldState,
@@ -420,7 +510,7 @@ public sealed partial class ReplayLoadManager
foreach (var compChange in newCompStates.Values)
{
// I'm not 100% sure about this, but I think delta states should always be full states here?
DebugTools.Assert(compChange.State is not IComponentDeltaState delta);
DebugTools.Assert(compChange.State is not IComponentDeltaState);
combined.Add(compChange);
}

View File

@@ -162,9 +162,7 @@ public sealed partial class ReplayLoadManager
}
using var stringFile = fileReader.Open(FileStrings);
var stringData = new byte[stringFile.Length];
stringFile.ReadExactly(stringData);
_serializer.SetStringSerializerPackage(stringHash, stringData);
_serializer.SetStringSerializerPackage(stringHash, stringFile.CopyToArray());
using var cvarsFile = fileReader.Open(FileCvars);
// Note, this does not invoke the received-initial-cvars event. But at least currently, that doesn't matter

View File

@@ -17,7 +17,7 @@ internal sealed partial class ReplayPlaybackManager
/// </summary>
/// <param name="index">The target tick/index. The actual checkpoint will have an index less than or equal to this.</param>
/// <param name="flushEntities">Whether to delete all entities</param>
public void ResetToNearestCheckpoint(int index, bool flushEntities)
public void ResetToNearestCheckpoint(int index, bool flushEntities, CheckpointState? checkpoint = null)
{
if (Replay == null)
throw new Exception("Not currently playing a replay");
@@ -25,7 +25,8 @@ internal sealed partial class ReplayPlaybackManager
if (flushEntities)
_entMan.FlushEntities();
var checkpoint = GetLastCheckpoint(Replay, index);
// Look up the desired checkpoint, unless our caller kindly provided one to us.
checkpoint ??= GetLastCheckpoint(Replay, index);
_sawmill.Info($"Resetting to checkpoint. From {Replay.CurrentIndex} to {checkpoint.Index}");
var st = new Stopwatch();

View File

@@ -1,6 +1,7 @@
using System;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.Replays.Playback;
@@ -27,14 +28,19 @@ internal sealed partial class ReplayPlaybackManager
return;
}
Playing &= !pausePlayback;
value = Math.Clamp(value, 0, Replay.States.Count - 1);
if (value == Replay.CurrentIndex)
{
ScrubbingTarget = null;
return;
}
BeforeSetTick?.Invoke();
// Begin timing replay processing so we can abort when we run out of time (_replayMaxScrubTime)
var st = RStopwatch.StartNew();
bool skipEffectEvents = value > Replay.CurrentIndex + _visualEventThreshold;
if (value < Replay.CurrentIndex)
{
@@ -45,9 +51,12 @@ internal sealed partial class ReplayPlaybackManager
{
// If we are skipping many ticks into the future, we try to skip directly to a checkpoint instead of
// applying every tick.
var nextCheckpoint = GetNextCheckpoint(Replay, Replay.CurrentIndex);
if (nextCheckpoint.Index < value && nextCheckpoint.Index > Replay.CurrentIndex)
ResetToNearestCheckpoint(value, false);
var nextCheckpoint = GetLastCheckpoint(Replay, value);
// Sanity-Check that the checkpoint is actually BEFORE the desired position.
// Also check that it gets us closer to goal position than we already are.
if (nextCheckpoint.Index <= value && nextCheckpoint.Index > Replay.CurrentIndex)
ResetToNearestCheckpoint(value, false, nextCheckpoint);
}
_entMan.EntitySysManager.GetEntitySystem<ClientDirtySystem>().Reset();
@@ -75,6 +84,23 @@ internal sealed partial class ReplayPlaybackManager
DebugTools.Assert(Replay.LastApplied >= state.FromSequence);
DebugTools.Assert(Replay.LastApplied + 1 <= state.ToSequence);
Replay.LastApplied = state.ToSequence;
if (st.Elapsed.TotalMilliseconds > _replayMaxScrubTime)
{
// Out of time to advance replay this tick
// Note: We check at end of loop so we always advance at least 1 tick.
break;
}
}
// Use ScrubbingTarget to force a later invocation to continue moving towards the target tick
if (Replay.CurrentIndex < value)
{
ScrubbingTarget = value;
}
else
{
ScrubbingTarget = null;
}
AfterSetTick?.Invoke();

View File

@@ -52,6 +52,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
public ReplayData? Replay { get; private set; }
public NetUserId? Recorder => Replay?.Recorder;
private int _checkpointMinInterval;
private int _replayMaxScrubTime;
private int _visualEventThreshold;
public uint? AutoPauseCountdown { get; set; }
public int? ScrubbingTarget { get; set; }
@@ -94,6 +95,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
_sawmill = _logMan.GetSawmill("replay");
_metaId = _factory.GetRegistration(typeof(MetaDataComponent)).NetID!.Value;
_confMan.OnValueChanged(CVars.CheckpointMinInterval, (value) => _checkpointMinInterval = value, true);
_confMan.OnValueChanged(CVars.ReplayMaxScrubTime, (value) => _replayMaxScrubTime = value, true);
_confMan.OnValueChanged(CVars.ReplaySkipThreshold, (value) => _visualEventThreshold = value, true);
_client.RunLevelChanged += OnRunLevelChanged;
}

View File

@@ -47,7 +47,7 @@ namespace Robust.Client.ResourceManagement
{
sawmill.Debug("Preloading textures...");
var sw = Stopwatch.StartNew();
var resList = GetTypeDict<TextureResource>();
var resList = GetTypeData<TextureResource>().Resources;
var texList = _manager.ContentFindFiles("/Textures/")
// Skip PNG files inside RSIs.
@@ -119,7 +119,7 @@ namespace Robust.Client.ResourceManagement
private void PreloadRsis(ISawmill sawmill)
{
var sw = Stopwatch.StartNew();
var resList = GetTypeDict<RSIResource>();
var resList = GetTypeData<RSIResource>().Resources;
var rsiList = _manager.ContentFindFiles("/Textures/")
.Where(p => p.ToString().EndsWith(".rsi/meta.json"))

View File

@@ -17,9 +17,7 @@ namespace Robust.Client.ResourceManagement;
/// </summary>
internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
{
private readonly Dictionary<Type, Dictionary<ResPath, BaseResource>> _cachedResources =
new();
private readonly Dictionary<Type, TypeData> _cachedResources = new();
private readonly Dictionary<Type, BaseResource> _fallbacks = new();
public T GetResource<T>(string path, bool useFallback = true) where T : BaseResource, new()
@@ -29,8 +27,8 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
public T GetResource<T>(ResPath path, bool useFallback = true) where T : BaseResource, new()
{
var cache = GetTypeDict<T>();
if (cache.TryGetValue(path, out var cached))
var cache = GetTypeData<T>();
if (cache.Resources.TryGetValue(path, out var cached))
{
return (T) cached;
}
@@ -40,7 +38,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
{
var dependencies = IoCManager.Instance!;
resource.Load(dependencies, path);
cache[path] = resource;
cache.Resources[path] = resource;
return resource;
}
catch (Exception e)
@@ -67,24 +65,31 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
public bool TryGetResource<T>(ResPath path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
{
var cache = GetTypeDict<T>();
if (cache.TryGetValue(path, out var cached))
var cache = GetTypeData<T>();
if (cache.Resources.TryGetValue(path, out var cached))
{
resource = (T) cached;
return true;
}
if (cache.NonExistent.Contains(path))
{
resource = null;
return false;
}
var _resource = new T();
try
{
var dependencies = IoCManager.Instance!;
_resource.Load(dependencies, path);
resource = _resource;
cache[path] = resource;
cache.Resources[path] = resource;
return true;
}
catch (FileNotFoundException)
{
cache.NonExistent.Add(path);
resource = null;
return false;
}
@@ -109,9 +114,9 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
public void ReloadResource<T>(ResPath path) where T : BaseResource, new()
{
var cache = GetTypeDict<T>();
var cache = GetTypeData<T>();
if (!cache.TryGetValue(path, out var res))
if (!cache.Resources.TryGetValue(path, out var res))
{
return;
}
@@ -145,7 +150,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
public void CacheResource<T>(ResPath path, T resource) where T : BaseResource, new()
{
GetTypeDict<T>()[path] = resource;
GetTypeData<T>().Resources[path] = resource;
}
public T GetFallback<T>() where T : BaseResource, new()
@@ -168,7 +173,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
public IEnumerable<KeyValuePair<ResPath, T>> GetAllResources<T>() where T : BaseResource, new()
{
return GetTypeDict<T>().Select(p => new KeyValuePair<ResPath, T>(p.Key, (T) p.Value));
return GetTypeData<T>().Resources.Select(p => new KeyValuePair<ResPath, T>(p.Key, (T) p.Value));
}
public event Action<TextureLoadedEventArgs>? OnRawTextureLoaded;
@@ -193,7 +198,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
if (disposing)
{
foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Values))
foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Resources.Values))
{
res.Dispose();
}
@@ -210,15 +215,9 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
#endregion IDisposable Members
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected Dictionary<ResPath, BaseResource> GetTypeDict<T>()
private TypeData GetTypeData<T>()
{
if (!_cachedResources.TryGetValue(typeof(T), out var ret))
{
ret = new Dictionary<ResPath, BaseResource>();
_cachedResources.Add(typeof(T), ret);
}
return ret;
return _cachedResources.GetOrNew(typeof(T));
}
public void TextureLoaded(TextureLoadedEventArgs eventArgs)
@@ -230,4 +229,13 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
{
OnRsiLoaded?.Invoke(eventArgs);
}
private sealed class TypeData
{
public readonly Dictionary<ResPath, BaseResource> Resources = new();
// List of resources which DON'T exist.
// Needed to avoid innocuous TryGet calls repeatedly trying to re-load non-existent resources from disk.
public readonly HashSet<ResPath> NonExistent = new();
}
}

View File

@@ -212,9 +212,18 @@ namespace Robust.Client.UserInterface
}
}
/// <summary>
/// Called when this control's visibility in the control tree changed.
/// </summary>
protected virtual void VisibilityChanged(bool newVisible)
{
}
private void _propagateVisibilityChanged(bool newVisible)
{
VisibilityChanged(newVisible);
OnVisibilityChanged?.Invoke(this);
if (!VisibleInTree)
{
UserInterfaceManagerInternal.ControlHidden(this);
@@ -567,7 +576,7 @@ namespace Robust.Client.UserInterface
public Vector2i Position;
public Color Modulate;
public UIBox2i? ScissorBox;
public ref Matrix3 CoordinateTransform;
public ref Matrix3x2 CoordinateTransform;
}
protected void RenderControl(ref ControlRenderArguments args, int childIndex, Vector2i position)

View File

@@ -15,6 +15,36 @@ namespace Robust.Client.UserInterface.Controls
public Label Label { get; }
public TextureRect TextureRect { get; }
/// <summary>
/// Should the checkbox be to the left or the right of the label.
/// </summary>
public bool LeftAlign
{
get => _leftAlign;
set
{
if (_leftAlign == value)
return;
_leftAlign = value;
if (value)
{
Label.HorizontalExpand = false;
TextureRect.SetPositionFirst();
Label.SetPositionInParent(1);
}
else
{
Label.HorizontalExpand = true;
Label.SetPositionFirst();
TextureRect.SetPositionInParent(1);
}
}
}
private bool _leftAlign = true;
public CheckBox()
{
ToggleMode = true;
@@ -31,10 +61,21 @@ namespace Robust.Client.UserInterface.Controls
StyleClasses = { StyleClassCheckBox },
VerticalAlignment = VAlignment.Center,
};
hBox.AddChild(TextureRect);
Label = new Label();
hBox.AddChild(Label);
if (LeftAlign)
{
Label.HorizontalExpand = false;
hBox.AddChild(TextureRect);
hBox.AddChild(Label);
}
else
{
Label.HorizontalExpand = true;
hBox.AddChild(Label);
hBox.AddChild(TextureRect);
}
}
protected override void DrawModeChanged()

View File

@@ -1,3 +1,4 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.IoC;
@@ -20,19 +21,19 @@ public sealed class ColorSelectorStyleBox : StyleBoxTexture
/// <summary>
/// Base background colour.
/// </summary>
public Vector4 BaseColor;
public Robust.Shared.Maths.Vector4 BaseColor;
/// <summary>
/// Colour to add to the background colour along the X-axis.
/// I.e., from left to right the background colour will vary from (BaseColour) to (BaseColour + XAxis)
/// </summary>
public Vector4 XAxis;
public Robust.Shared.Maths.Vector4 XAxis;
/// <summary>
/// Colour to add to the background colour along the y-axis.
/// I.e., from left to right the background colour will vary from (BaseColour) to (BaseColour + XAxis)
/// </summary>
public Vector4 YAxis;
public Robust.Shared.Maths.Vector4 YAxis;
/// <summary>
/// If true, then <see cref="BaseColor"/>, <see cref="XAxis"/>, and <see cref="YAxis"/> will be interpreted as HSVa
@@ -53,7 +54,7 @@ public sealed class ColorSelectorStyleBox : StyleBoxTexture
var old = handle.GetShader();
handle.UseShader(_shader);
var globalPixelPos = handle.GetTransform().Transform(default);
var globalPixelPos = Vector2.Transform(default, handle.GetTransform());
_shader.SetParameter("size", box.Size);
_shader.SetParameter("offset", globalPixelPos);
_shader.SetParameter("xAxis", XAxis);
@@ -92,14 +93,14 @@ public sealed class ColorSelectorStyleBox : StyleBoxTexture
{
var colorData = Hsv
? Color.ToHsv(color)
: new Vector4(color.R, color.G, color.B, color.A);
: new Robust.Shared.Maths.Vector4(color.R, color.G, color.B, color.A);
SetBaseColor(colorData);
}
/// <summary>
/// Helper method that sets the base color by taking in some color and removing the components that are controlled by the x and y axes.
/// </summary>
public void SetBaseColor(Vector4 colorData)
public void SetBaseColor(Robust.Shared.Maths.Vector4 colorData)
{
BaseColor = colorData - colorData * XAxis - colorData * YAxis;
}

View File

@@ -15,6 +15,8 @@ namespace Robust.Client.UserInterface.Controls
public const string StylePseudoClassHover = "hover";
public const string StylePseudoClassDisabled = "disabled";
public StyleBox? StyleBoxOverride { get; set; }
public ContainerButton()
{
DrawModeChanged();
@@ -24,6 +26,11 @@ namespace Robust.Client.UserInterface.Controls
{
get
{
if (StyleBoxOverride != null)
{
return StyleBoxOverride;
}
if (TryGetStyleProperty<StyleBox>(StylePropertyStyleBox, out var box))
{
return box;

View File

@@ -0,0 +1,64 @@
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Robust.Client.UserInterface.Controls;
[Virtual]
public class EntityPrototypeView : SpriteView
{
private string? _currentPrototype;
private EntityUid? _ourEntity;
public EntityPrototypeView()
{
}
public EntityPrototypeView(EntProtoId? entProto, IEntityManager entMan) : base(entMan)
{
SetPrototype(entProto);
}
public void SetPrototype(EntProtoId? entProto)
{
SpriteSystem ??= EntMan.System<SpriteSystem>();
if (entProto == _currentPrototype
&& EntMan.TryGetComponent(Entity?.Owner, out MetaDataComponent? meta)
&& meta.EntityPrototype?.ID == _currentPrototype)
{
return;
}
_currentPrototype = entProto;
SetEntity(null);
if (_ourEntity != null)
{
EntMan.DeleteEntity(_ourEntity);
}
if (_currentPrototype != null)
{
_ourEntity = EntMan.Spawn(_currentPrototype);
SpriteSystem.ForceUpdate(_ourEntity.Value);
SetEntity(_ourEntity);
}
}
protected override void EnteredTree()
{
base.EnteredTree();
if (_currentPrototype != null)
SetPrototype(_currentPrototype);
}
protected override void ExitedTree()
{
base.ExitedTree();
if (!EntMan.Deleted(_ourEntity))
EntMan.QueueDeleteEntity(_ourEntity);
}
}

View File

@@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Collections;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -20,7 +19,7 @@ namespace Robust.Client.UserInterface.Controls
public const string StylePropertyStyleBox = "stylebox";
private readonly List<RichTextEntry> _entries = new();
private readonly RingBufferList<RichTextEntry> _entries = new();
private bool _isAtBottom = true;
private int _totalContentHeight;
@@ -30,6 +29,8 @@ namespace Robust.Client.UserInterface.Controls
public bool ScrollFollowing { get; set; } = true;
private bool _invalidOnVisible;
public OutputPanel()
{
IoCManager.InjectDependencies(this);
@@ -45,6 +46,8 @@ namespace Robust.Client.UserInterface.Controls
_scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
}
public int EntryCount => _entries.Count;
public StyleBox? StyleBoxOverride
{
get => _styleBoxOverride;
@@ -91,7 +94,7 @@ namespace Robust.Client.UserInterface.Controls
{
var entry = new RichTextEntry(message, this, _tagManager, null);
entry.Update(_getFont(), _getContentBox().Width, UIScale);
entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale);
_entries.Add(entry);
var font = _getFont();
@@ -134,7 +137,7 @@ namespace Robust.Client.UserInterface.Controls
// So when a new color tag gets hit this stack gets the previous color pushed on.
var context = new MarkupDrawingContext(2);
foreach (ref var entry in CollectionsMarshal.AsSpan(_entries))
foreach (ref var entry in _entries)
{
if (entryOffset + entry.Height < 0)
{
@@ -147,7 +150,7 @@ namespace Robust.Client.UserInterface.Controls
break;
}
entry.Draw(handle, font, contentBox, entryOffset, context, UIScale);
entry.Draw(_tagManager, handle, font, contentBox, entryOffset, context, UIScale);
entryOffset += entry.Height + font.GetLineSeparation(UIScale);
}
@@ -185,9 +188,9 @@ namespace Robust.Client.UserInterface.Controls
_totalContentHeight = 0;
var font = _getFont();
var sizeX = _getContentBox().Width;
foreach (ref var entry in CollectionsMarshal.AsSpan(_entries))
foreach (ref var entry in _entries)
{
entry.Update(font, sizeX, UIScale);
entry.Update(_tagManager, font, sizeX, UIScale);
_totalContentHeight += entry.Height + font.GetLineSeparation(UIScale);
}
@@ -239,7 +242,13 @@ namespace Robust.Client.UserInterface.Controls
protected internal override void UIScaleChanged()
{
_invalidateEntries();
// If this control isn't visible, don't invalidate entries immediately.
// This saves invalidating the debug console if it's hidden,
// which is a huge boon as auto-scaling changes UI scale a lot in that scenario.
if (!VisibleInTree)
_invalidOnVisible = true;
else
_invalidateEntries();
base.UIScaleChanged();
}
@@ -257,5 +266,14 @@ namespace Robust.Client.UserInterface.Controls
// existing ones were valid when the UI scale was set.
_invalidateEntries();
}
protected override void VisibilityChanged(bool newVisible)
{
if (newVisible && _invalidOnVisible)
{
_invalidateEntries();
_invalidOnVisible = false;
}
}
}
}

View File

@@ -68,7 +68,7 @@ namespace Robust.Client.UserInterface.Controls
}
var font = _getFont();
_entry.Update(font, availableSize.X * UIScale, UIScale, LineHeightScale);
_entry.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
return new Vector2(_entry.Width / UIScale, _entry.Height / UIScale);
}
@@ -82,7 +82,7 @@ namespace Robust.Client.UserInterface.Controls
return;
}
_entry.Draw(handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
_entry.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
}
[Pure]

View File

@@ -1,4 +1,3 @@
using Robust.Shared.Maths;
using System;
using System.Collections.Generic;
using System.Numerics;
@@ -15,9 +14,10 @@ namespace Robust.Client.UserInterface.Controls
public const string RightButtonStyle = "spinbox-right";
public const string MiddleButtonStyle = "spinbox-middle";
public LineEdit LineEditControl { get; }
private List<Button> _leftButtons = new();
private List<Button> _rightButtons = new();
private List<SpinBoxButton> _leftButtons = new();
private List<SpinBoxButton> _rightButtons = new();
private int _stepSize = 1;
private bool _buttonsDisabled;
/// <summary>
/// Determines whether the SpinBox value gets changed by the input text.
@@ -30,12 +30,7 @@ namespace Robust.Client.UserInterface.Controls
get => _value;
set
{
if (IsValid != null && !IsValid(value))
{
return;
}
_value = value;
LineEditControl.Text = value.ToString();
OverrideValue(value);
ValueChanged?.Invoke(new ValueChangedEventArgs(value));
}
}
@@ -52,6 +47,7 @@ namespace Robust.Client.UserInterface.Controls
return;
}
_value = value;
UpdateButtonCanPress();
LineEditControl.Text = value.ToString();
}
@@ -87,6 +83,7 @@ namespace Robust.Client.UserInterface.Controls
ClearButtons();
AddLeftButton(-1, "-");
AddRightButton(1, "+");
UpdateButtonCanPress();
}
/// <summary>
@@ -94,8 +91,8 @@ namespace Robust.Client.UserInterface.Controls
/// </summary>
public void AddRightButton(int num, string text)
{
var button = new Button { Text = text };
button.OnPressed += (args) => Value += num;
var button = new SpinBoxButton(num) { Text = text };
button.OnPressed += _ => Value += num;
AddChild(button);
button.AddStyleClass(RightButtonStyle);
if (_rightButtons.Count > 0)
@@ -111,8 +108,8 @@ namespace Robust.Client.UserInterface.Controls
/// </summary>
public void AddLeftButton(int num, string text)
{
var button = new Button { Text = text };
button.OnPressed += (args) => Value += num;
var button = new SpinBoxButton(num) { Text = text };
button.OnPressed += _ => Value += num;
AddChild(button);
button.SetPositionInParent(_leftButtons.Count);
button.AddStyleClass(_leftButtons.Count == 0 ? LeftButtonStyle : MiddleButtonStyle);
@@ -162,6 +159,24 @@ namespace Robust.Client.UserInterface.Controls
{
rightButton.Disabled = disabled;
}
_buttonsDisabled = disabled;
}
private void UpdateButtonCanPress()
{
if (IsValid == null)
return;
foreach (var button in _leftButtons)
{
button.Disabled = !IsValid(_value + button.Value) || _buttonsDisabled;
}
foreach (var button in _rightButtons)
{
button.Disabled = !IsValid(_value + button.Value) || _buttonsDisabled;
}
}
/// <summary>
@@ -195,6 +210,16 @@ namespace Robust.Client.UserInterface.Controls
else if (args.Delta.Y < 0)
Value -= _stepSize;
}
private sealed class SpinBoxButton : Button
{
public readonly int Value;
public SpinBoxButton(int value)
{
Value = value;
}
}
}
public sealed class ValueChangedEventArgs : EventArgs

View File

@@ -14,9 +14,9 @@ namespace Robust.Client.UserInterface.Controls
[Virtual]
public class SpriteView : Control
{
private SpriteSystem? _sprite;
protected SpriteSystem? SpriteSystem;
private SharedTransformSystem? _transform;
private readonly IEntityManager _entMan;
protected readonly IEntityManager EntMan;
[ViewVariables]
public SpriteComponent? Sprite => Entity?.Comp1;
@@ -120,20 +120,26 @@ namespace Robust.Client.UserInterface.Controls
public SpriteView()
{
IoCManager.Resolve(ref _entMan);
IoCManager.Resolve(ref EntMan);
RectClipContent = true;
}
public SpriteView(IEntityManager entMan)
{
EntMan = entMan;
RectClipContent = true;
}
public SpriteView(EntityUid? uid, IEntityManager entMan)
{
_entMan = entMan;
EntMan = entMan;
RectClipContent = true;
SetEntity(uid);
}
public SpriteView(NetEntity uid, IEntityManager entMan)
{
_entMan = entMan;
EntMan = entMan;
RectClipContent = true;
SetEntity(uid);
}
@@ -154,8 +160,8 @@ namespace Robust.Client.UserInterface.Controls
if (Entity?.Owner == uid)
return;
if (!_entMan.TryGetComponent(uid, out SpriteComponent? sprite)
|| !_entMan.TryGetComponent(uid, out TransformComponent? xform))
if (!EntMan.TryGetComponent(uid, out SpriteComponent? sprite)
|| !EntMan.TryGetComponent(uid, out TransformComponent? xform))
{
Entity = null;
NetEnt = null;
@@ -163,7 +169,7 @@ namespace Robust.Client.UserInterface.Controls
}
Entity = new(uid.Value, sprite, xform);
NetEnt = _entMan.GetNetEntity(uid);
NetEnt = EntMan.GetNetEntity(uid);
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
@@ -223,11 +229,11 @@ namespace Robust.Client.UserInterface.Controls
if (!ResolveEntity(out var uid, out var sprite, out var xform))
return;
_sprite ??= _entMan.System<SpriteSystem>();
_transform ??= _entMan.System<TransformSystem>();
SpriteSystem ??= EntMan.System<SpriteSystem>();
_transform ??= EntMan.System<TransformSystem>();
// Ensure the sprite is animated despite possible not being visible in any viewport.
_sprite.ForceUpdate(uid);
SpriteSystem.ForceUpdate(uid);
var stretchVec = Stretch switch
{
@@ -258,13 +264,13 @@ namespace Robust.Client.UserInterface.Controls
[NotNullWhen(true)] out SpriteComponent? sprite,
[NotNullWhen(true)] out TransformComponent? xform)
{
if (NetEnt != null && Entity == null && _entMan.TryGetEntity(NetEnt, out var ent))
if (NetEnt != null && Entity == null && EntMan.TryGetEntity(NetEnt, out var ent))
SetEntity(ent);
if (Entity != null)
{
(uid, sprite, xform) = Entity.Value;
return !_entMan.Deleted(uid);
return !EntMan.Deleted(uid);
}
sprite = null;

View File

@@ -65,6 +65,10 @@ namespace Robust.Client.UserInterface.Controls
}
}
public StyleBox? PanelStyleBoxOverride { get; set; }
public Color? TabFontColorOverride { get; set; }
public Color? TabFontColorInactiveOverride { get; set; }
public event Action<int>? OnTabChanged;
public TabContainer()
@@ -361,6 +365,9 @@ namespace Robust.Client.UserInterface.Controls
[System.Diagnostics.Contracts.Pure]
private Color _getTabFontColorActive()
{
if (TabFontColorOverride != null)
return TabFontColorOverride.Value;
if (TryGetStyleProperty(stylePropertyTabFontColor, out Color color))
{
return color;
@@ -371,6 +378,9 @@ namespace Robust.Client.UserInterface.Controls
[System.Diagnostics.Contracts.Pure]
private Color _getTabFontColorInactive()
{
if (TabFontColorInactiveOverride != null)
return TabFontColorInactiveOverride.Value;
if (TryGetStyleProperty(StylePropertyTabFontColorInactive, out Color color))
{
return color;
@@ -381,6 +391,9 @@ namespace Robust.Client.UserInterface.Controls
[System.Diagnostics.Contracts.Pure]
private StyleBox? _getPanel()
{
if (PanelStyleBoxOverride != null)
return PanelStyleBoxOverride;
TryGetStyleProperty<StyleBox>(StylePropertyPanelStyleBox, out var box);
return box;
}

View File

@@ -13,6 +13,12 @@ namespace Robust.Client.UserInterface.Controls
}
public override float UIScale => UIScaleSet;
internal float UIScaleSet { get; set; }
/// <summary>
/// Set after the window is resized, to batch up UI scale updates on window resizes.
/// </summary>
internal bool UIScaleUpdateNeeded { get; set; }
public override IClydeWindow Window { get; }
/// <summary>

View File

@@ -277,8 +277,21 @@ public sealed partial class DebugConsole
CommandBar.CursorPosition = lastRange.end;
CommandBar.SelectionStart = lastRange.start;
var insertValue = CommandParsing.Escape(completion);
// If the replacement contains a space, we must quote it to treat it as a single argument.
var mustQuote = insertValue.Contains(' ');
if ((completionFlags & CompletionOptionFlags.PartialCompletion) == 0)
{
if (mustQuote)
insertValue = $"\"{insertValue}\"";
insertValue += " ";
}
else if (mustQuote)
{
// If it's a partial completion, only quote the start.
insertValue = '"' + insertValue;
}
CommandBar.InsertAtCursor(insertValue);

View File

@@ -7,6 +7,7 @@ using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Input;
@@ -51,6 +52,8 @@ namespace Robust.Client.UserInterface.CustomControls
private readonly ConcurrentQueue<FormattedMessage> _messageQueue = new();
private readonly ISawmill _logger;
private int _maxEntries;
public DebugConsole()
{
RobustXamlLoader.Load(this);
@@ -78,6 +81,7 @@ namespace Robust.Client.UserInterface.CustomControls
_consoleHost.AddString += OnAddString;
_consoleHost.AddFormatted += OnAddFormatted;
_consoleHost.ClearText += OnClearText;
_cfg.OnValueChanged(CVars.ConMaxEntries, MaxEntriesChanged, true);
UserInterfaceManager.ModalRoot.AddChild(_compPopup);
}
@@ -89,10 +93,17 @@ namespace Robust.Client.UserInterface.CustomControls
_consoleHost.AddString -= OnAddString;
_consoleHost.AddFormatted -= OnAddFormatted;
_consoleHost.ClearText -= OnClearText;
_cfg.UnsubValueChanged(CVars.ConMaxEntries, MaxEntriesChanged);
UserInterfaceManager.ModalRoot.RemoveChild(_compPopup);
}
private void MaxEntriesChanged(int value)
{
_maxEntries = value;
TrimExtraOutputEntries();
}
private void OnClearText(object? _, EventArgs args)
{
Clear();
@@ -165,6 +176,15 @@ namespace Robust.Client.UserInterface.CustomControls
private void _addFormattedLineInternal(FormattedMessage message)
{
Output.AddMessage(message);
TrimExtraOutputEntries();
}
private void TrimExtraOutputEntries()
{
while (Output.EntryCount > _maxEntries)
{
Output.RemoveEntry(0);
}
}
private void _flushQueue()

View File

@@ -43,11 +43,11 @@ namespace Robust.Client.UserInterface.CustomControls
/// <remarks>
/// This is generally just be a combination of <see cref="IClydeViewport.GetWorldToLocalMatrix"/> and <see cref="GetLocalToScreenMatrix"/>
/// </remarks>
Matrix3 GetWorldToScreenMatrix();
Matrix3x2 GetWorldToScreenMatrix();
/// <summary>
/// Returns a matrix that can be used to transform from view-port local to screen coordinates.
/// </summary>
Matrix3 GetLocalToScreenMatrix();
Matrix3x2 GetLocalToScreenMatrix();
}
}

View File

@@ -155,17 +155,17 @@ namespace Robust.Client.UserInterface.CustomControls
return WorldToLocalPixel(point) + GlobalPixelPosition;
}
public Matrix3 GetWorldToScreenMatrix()
public Matrix3x2 GetWorldToScreenMatrix()
{
if (Viewport == null)
return Matrix3.Identity;
return Matrix3x2.Identity;
return Viewport.GetWorldToLocalMatrix() * GetLocalToScreenMatrix();
}
public Matrix3 GetLocalToScreenMatrix()
public Matrix3x2 GetLocalToScreenMatrix()
{
return Matrix3.CreateTransform(GlobalPixelPosition, 0, Vector2.One / _viewportResolution);
return Matrix3Helpers.CreateTransform(GlobalPixelPosition, 0, Vector2.One / _viewportResolution);
}
}
}

View File

@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Input;
@@ -63,7 +63,7 @@ namespace Robust.Client.UserInterface
Color GetMainClearColor();
void RenderControl(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
UIBox2i? scissorBox, Matrix3 coordinateTransform);
UIBox2i? scissorBox, Matrix3x2 coordinateTransform);
}
}

View File

@@ -17,7 +17,6 @@ namespace Robust.Client.UserInterface
internal struct RichTextEntry
{
private readonly Color _defaultColor;
private readonly MarkupTagManager _tagManager;
private readonly Type[]? _tagsAllowed;
public readonly FormattedMessage Message;
@@ -37,7 +36,7 @@ namespace Robust.Client.UserInterface
/// </summary>
public ValueList<int> LineBreaks;
private readonly Dictionary<int, Control> _tagControls = new();
private readonly Dictionary<int, Control>? _tagControls;
public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
@@ -46,23 +45,26 @@ namespace Robust.Client.UserInterface
Width = 0;
LineBreaks = default;
_defaultColor = defaultColor ?? new(200, 200, 200);
_tagManager = tagManager;
_tagsAllowed = tagsAllowed;
Dictionary<int, Control>? tagControls = null;
var nodeIndex = -1;
foreach (var node in Message.Nodes)
foreach (var node in Message)
{
nodeIndex++;
if (node.Name == null)
continue;
if (!_tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
continue;
parent.Children.Add(control);
_tagControls.Add(nodeIndex, control);
tagControls ??= new Dictionary<int, Control>();
tagControls.Add(nodeIndex, control);
}
_tagControls = tagControls;
}
/// <summary>
@@ -72,7 +74,7 @@ namespace Robust.Client.UserInterface
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
/// <param name="uiScale"></param>
/// <param name="lineHeightScale"></param>
public void Update(Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
public void Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
{
// This method is gonna suck due to complexity.
// Bear with me here.
@@ -91,10 +93,10 @@ namespace Robust.Client.UserInterface
// Nodes can change the markup drawing context and return additional text.
// It's also possible for nodes to return inline controls. They get treated as one large rune.
var nodeIndex = -1;
foreach (var node in Message.Nodes)
foreach (var node in Message)
{
nodeIndex++;
var text = ProcessNode(node, context);
var text = ProcessNode(tagManager, node, context);
if (!context.Font.TryPeek(out var font))
font = defaultFont;
@@ -113,7 +115,7 @@ namespace Robust.Client.UserInterface
return;
}
if (!_tagControls.TryGetValue(nodeIndex, out var control))
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
continue;
if (ProcessRune(ref this, new Rune(' '), out breakLine))
@@ -166,6 +168,7 @@ namespace Robust.Client.UserInterface
}
public readonly void Draw(
MarkupTagManager tagManager,
DrawingHandleScreen handle,
Font defaultFont,
UIBox2 drawBox,
@@ -184,10 +187,10 @@ namespace Robust.Client.UserInterface
var controlYAdvance = 0f;
var nodeIndex = -1;
foreach (var node in Message.Nodes)
foreach (var node in Message)
{
nodeIndex++;
var text = ProcessNode(node, context);
var text = ProcessNode(tagManager, node, context);
if (!context.Color.TryPeek(out var color) || !context.Font.TryPeek(out var font))
{
color = _defaultColor;
@@ -210,7 +213,7 @@ namespace Robust.Client.UserInterface
globalBreakCounter += 1;
}
if (!_tagControls.TryGetValue(nodeIndex, out var control))
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
continue;
var invertedScale = 1f / uiScale;
@@ -223,24 +226,22 @@ namespace Robust.Client.UserInterface
}
}
private readonly string ProcessNode(MarkupNode node, MarkupDrawingContext context)
private readonly string ProcessNode(MarkupTagManager tagManager, MarkupNode node, MarkupDrawingContext context)
{
// If a nodes name is null it's a text node.
if (node.Name == null)
return node.Value.StringValue ?? "";
//Skip the node if there is no markup tag for it.
if (!_tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
return "";
if (!node.Closing)
{
context.Tags.Add(tag);
tag.PushDrawContext(node, context);
return tag.TextBefore(node);
}
context.Tags.Remove(tag);
tag.PopDrawContext(node, context);
return tag.TextAfter(node);
}

View File

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

View File

@@ -123,7 +123,12 @@ internal partial class UserInterfaceManager
private void UpdateUIScale(WindowRoot root)
{
root.UIScaleSet = CalculateAutoScale(root);
var newScale = CalculateAutoScale(root);
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (newScale == root.UIScaleSet)
return;
root.UIScaleSet = newScale;
_propagateUIScaleChanged(root);
root.InvalidateMeasure();
}
@@ -142,7 +147,21 @@ internal partial class UserInterfaceManager
{
if (!_windowsToRoot.TryGetValue(windowResizedEventArgs.Window.Id, out var root))
return;
UpdateUIScale(root);
root.UIScaleUpdateNeeded = true;
root.InvalidateMeasure();
}
private void CheckRootUIScaleUpdate(WindowRoot root)
{
if (!root.UIScaleUpdateNeeded)
return;
using (_prof.Group("UIScaleUpdate"))
{
UpdateUIScale(root);
}
root.UIScaleUpdateNeeded = false;
}
}

View File

@@ -216,6 +216,8 @@ namespace Robust.Client.UserInterface
{
foreach (var root in _roots)
{
CheckRootUIScaleUpdate(root);
using (_prof.Group("Root"))
{
var totalUpdated = root.DoFrameUpdateRecursive(args);
@@ -330,7 +332,7 @@ namespace Robust.Client.UserInterface
}
public void RenderControl(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
UIBox2i? scissorBox, Matrix3 coordinateTransform)
UIBox2i? scissorBox, Matrix3x2 coordinateTransform)
{
if (!control.Visible)
{
@@ -378,8 +380,7 @@ namespace Robust.Client.UserInterface
var handle = renderHandle.DrawingHandleScreen;
var oldXform = handle.GetTransform();
var xform = oldXform;
xform.Multiply(Matrix3.CreateTransform(position, Angle.Zero, Vector2.One));
var xform = oldXform * Matrix3Helpers.CreateTransform(position, Angle.Zero, Vector2.One);
handle.SetTransform(xform);
modulate *= control.Modulate;
@@ -406,7 +407,7 @@ namespace Robust.Client.UserInterface
foreach (var child in control.Children)
{
var pos = position + (Vector2i) coordinateTransform.Transform(child.PixelPosition);
var pos = position + (Vector2i)Vector2.Transform(child.PixelPosition, coordinateTransform);
control.RenderChildOverride(ref args, child.GetPositionInParent(), pos);
}

View File

@@ -4,22 +4,6 @@ namespace Robust.Client.Utility
{
internal static class OpenTKConversions
{
public static OpenToolkit.Mathematics.Matrix3 ConvertOpenTK(this Matrix3 matrix)
{
return new()
{
M11 = matrix.R0C0,
M12 = matrix.R0C1,
M13 = matrix.R0C2,
M21 = matrix.R1C0,
M22 = matrix.R1C1,
M23 = matrix.R1C2,
M31 = matrix.R2C0,
M32 = matrix.R2C1,
M33 = matrix.R2C2
};
}
public static OpenToolkit.Mathematics.Color4 ConvertOpenTK(this Color color)
{
return new(color.R, color.G, color.B, color.A);

View File

@@ -31,6 +31,7 @@ public static class Diagnostics
public const string IdDependencyFieldAssigned = "RA0025";
public const string IdUncachedRegex = "RA0026";
public const string IdDataFieldRedundantTag = "RA0027";
public const string IdMustCallBase = "RA0028";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");

View File

@@ -42,6 +42,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
component.Source = new DummyAudioSource();
}
public override void SetGridAudio(Entity<AudioComponent>? entity)
{
if (entity == null)
return;
base.SetGridAudio(entity);
// Need to global override so everyone can hear it.
_pvs.AddGlobalOverride(entity.Value.Owner);
}
public override void SetMapAudio(Entity<AudioComponent>? audio)
{
if (audio == null)
@@ -81,11 +92,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
var entity = Spawn("Audio", MapCoordinates.Nullspace);
var audio = SetupAudio(entity, filename, audioParams);
AddAudioFilter(entity, audio, playerFilter);
audio.Global = true;
return (entity, audio);
var entity = SetupAudio(filename, audioParams);
AddAudioFilter(entity, entity.Comp, playerFilter);
entity.Comp.Global = true;
return (entity, entity.Comp);
}
/// <inheritdoc />
@@ -95,16 +105,14 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
if (TerminatingOrDeleted(uid))
{
Log.Error($"Tried to play audio on a terminating / deleted entity {ToPrettyString(uid)}. Trace: {Environment.StackTrace}");
return null;
}
var entity = Spawn("Audio", new EntityCoordinates(uid, Vector2.Zero));
var audio = SetupAudio(entity, filename, audioParams);
AddAudioFilter(entity, audio, playerFilter);
var entity = SetupAudio(filename, audioParams);
// Move it after setting it up
XformSystem.SetCoordinates(entity, new EntityCoordinates(uid, Vector2.Zero));
AddAudioFilter(entity, entity.Comp, playerFilter);
return (entity, audio);
return (entity, entity.Comp);
}
/// <inheritdoc />
@@ -114,15 +122,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
if (TerminatingOrDeleted(uid))
{
Log.Error($"Tried to play audio on a terminating / deleted entity {ToPrettyString(uid)}. Trace: {Environment.StackTrace}");
return null;
}
var entity = Spawn("Audio", new EntityCoordinates(uid, Vector2.Zero));
var audio = SetupAudio(entity, filename, audioParams);
var entity = SetupAudio(filename, audioParams);
XformSystem.SetCoordinates(entity, new EntityCoordinates(uid, Vector2.Zero));
return (entity, audio);
return (entity, entity.Comp);
}
/// <inheritdoc />
@@ -140,11 +145,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (!coordinates.IsValid(EntityManager))
return null;
var entity = Spawn("Audio", coordinates);
var audio = SetupAudio(entity, filename, audioParams);
AddAudioFilter(entity, audio, playerFilter);
var entity = SetupAudio(filename, audioParams);
XformSystem.SetCoordinates(entity, coordinates);
AddAudioFilter(entity, entity.Comp, playerFilter);
return (entity, audio);
return (entity, entity.Comp);
}
/// <inheritdoc />
@@ -163,10 +168,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (!coordinates.IsValid(EntityManager))
return null;
var entity = Spawn("Audio", coordinates);
var audio = SetupAudio(entity, filename, audioParams);
// TODO: Transform TryFindGridAt mess + optimisation required.
var entity = SetupAudio(filename, audioParams);
XformSystem.SetCoordinates(entity, coordinates);
return (entity, audio);
return (entity, entity.Comp);
}
/// <inheritdoc />

View File

@@ -844,7 +844,7 @@ public sealed class MapLoaderSystem : EntitySystem
if (xformQuery.TryGetComponent(rootEntity, out var xform) && IsRoot(xform, mapQuery) && !HasComp<MapComponent>(rootEntity))
{
_transform.SetLocalPosition(xform, data.Options.TransformMatrix.Transform(xform.LocalPosition));
_transform.SetLocalPosition(xform, Vector2.Transform(xform.LocalPosition, data.Options.TransformMatrix));
xform.LocalRotation += data.Options.Rotation;
}
}
@@ -856,7 +856,7 @@ public sealed class MapLoaderSystem : EntitySystem
if (xformQuery.TryGetComponent(entity, out var xform) && IsRoot(xform, mapQuery))
{
// Don't want to trigger events
xform._localPosition = data.Options.TransformMatrix.Transform(xform.LocalPosition);
xform._localPosition = Vector2.Transform(xform.LocalPosition, data.Options.TransformMatrix);
if (!xform.NoLocalRotation)
xform._localRotation += data.Options.Rotation;

View File

@@ -1,15 +1,10 @@
using Robust.Server.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
namespace Robust.Server.GameObjects;
public sealed class ServerMetaDataSystem : MetaDataSystem
{
[Dependency] private readonly PvsSystem _pvsSystem = default!;
private EntityQuery<MetaDataComponent> _mQuery;
public override void Initialize()
{
base.Initialize();
@@ -18,7 +13,6 @@ public sealed class ServerMetaDataSystem : MetaDataSystem
EntityManager.ComponentAdded += OnComponentAdded;
EntityManager.ComponentRemoved += OnComponentRemoved;
_mQuery = GetEntityQuery<MetaDataComponent>();
}
public override void Shutdown()

View File

@@ -1,9 +1,32 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Robust.Server.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.Utility;
namespace Robust.Server.GameObjects;
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UserInterfaceUserComponent, ExpandPvsEvent>(OnBuiUserExpand);
}
private void OnBuiUserExpand(Entity<UserInterfaceUserComponent> ent, ref ExpandPvsEvent args)
{
var buis = ent.Comp.OpenInterfaces.Keys;
if (buis.Count == 0)
return;
args.Entities ??= new List<EntityUid>(buis.Count);
foreach (var ui in buis)
{
DebugTools.Assert(ent.Comp.OpenInterfaces[ui].Count > 0);
DebugTools.Assert(HasComp<UserInterfaceComponent>(ui));
args.Entities.Add(ui);
}
}
}

View File

@@ -40,12 +40,6 @@ namespace Robust.Server.GameObjects
EntityManager.EntityInitialized -= OnEntityInit;
}
[Obsolete("Use Entity<T> variant")]
public void AddLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true)
{
AddLayer((uid, component), (ushort)layer, refresh);
}
public void AddLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp<VisibilityComponent>(ent.Owner);
@@ -59,13 +53,6 @@ namespace Robust.Server.GameObjects
RefreshVisibility(ent);
}
[Obsolete("Use Entity<T> variant")]
public void RemoveLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true)
{
RemoveLayer((uid, component), (ushort)layer, refresh);
}
public void RemoveLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
if (!_visibilityQuery.Resolve(ent.Owner, ref ent.Comp, false))
@@ -80,12 +67,6 @@ namespace Robust.Server.GameObjects
RefreshVisibility(ent);
}
[Obsolete("Use Entity<T> variant")]
public void SetLayer(EntityUid uid, VisibilityComponent component, int layer, bool refresh = true)
{
SetLayer((uid, component), (ushort)layer, refresh);
}
public void SetLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp<VisibilityComponent>(ent.Owner);

View File

@@ -229,21 +229,6 @@ namespace Robust.Server.GameObjects
private void HandleEntityNetworkMessage(MsgEntity message)
{
var msgT = message.SourceTick;
var cT = _gameTiming.CurTick;
if (msgT <= cT)
{
if (msgT < cT && _logLateMsgs)
{
_netEntSawmill.Warning("Got late MsgEntity! Diff: {0}, msgT: {2}, cT: {3}, player: {1}",
(int) msgT.Value - (int) cT.Value, message.MsgChannel.UserName, msgT, cT);
}
DispatchEntityNetworkMessage(message);
return;
}
_queue.Add(message);
}

View File

@@ -62,7 +62,7 @@ internal sealed class PvsChunk
/// <summary>
/// The <see cref="Root"/>'s inverse world matrix.
/// </summary>
public Matrix3 InvWorldMatrix { get; set; }
public Matrix3x2 InvWorldMatrix { get; set; }
// These are only used while populating the chunk. They aren't local variables because the chunks are pooled, so
// the same chunk can be repopulated more than once.

View File

@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -115,6 +117,16 @@ internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion<P
/// </summary>
public GameState? State;
/// <summary>
/// The serialized <see cref="State"/> object.
/// </summary>
public MemoryStream? StateStream;
/// <summary>
/// Whether we should force reliable sending of the <see cref="MsgState"/>.
/// </summary>
public bool ForceSendReliably { get; set; }
/// <summary>
/// Clears all stored game state data. This should only be used after the game state has been serialized.
/// </summary>

View File

@@ -72,7 +72,7 @@ internal sealed partial class PvsSystem
var xform = Transform(chunk.Root);
DebugTools.AssertEqual(chunk.Map.Owner, xform.MapUid);
chunk.InvWorldMatrix = xform.InvLocalMatrix;
var worldPos = xform.LocalMatrix.Transform(chunk.Centre);
var worldPos = Vector2.Transform(chunk.Centre, xform.LocalMatrix);
chunk.Position = new(worldPos, xform.MapID);
chunk.UpdateQueued = false;
}
@@ -137,7 +137,7 @@ internal sealed partial class PvsSystem
foreach (var (grid, _) in _grids)
{
var localPos = _transform.GetInvWorldMatrix(grid).Transform(viewPos);
var localPos = Vector2.Transform(viewPos, _transform.GetInvWorldMatrix(grid));
var gridChunkEnumerator = new ChunkIndicesEnumerator(localPos, range, ChunkSize);
while (gridChunkEnumerator.MoveNext(out var gridChunkIndices))
{

View File

@@ -75,15 +75,15 @@ namespace Robust.Server.GameStates
return true;
}
private void CleanupDirty(ICommonSession[] sessions)
private void CleanupDirty()
{
using var _ = Histogram.WithLabels("Clean Dirty").NewTimer();
if (!CullingEnabled)
{
_seenAllEnts.Clear();
foreach (var player in sessions)
foreach (var player in _sessions)
{
_seenAllEnts.Add(player);
_seenAllEnts.Add(player.Session);
}
}

View File

@@ -17,13 +17,12 @@ internal sealed partial class PvsSystem
{
private WaitHandle? _leaveTask;
private void ProcessLeavePvs(ICommonSession[] sessions)
private void ProcessLeavePvs()
{
if (!CullingEnabled || sessions.Length == 0)
if (!CullingEnabled || _sessions.Length == 0)
return;
DebugTools.AssertNull(_leaveTask);
_leaveJob.Setup(sessions);
if (_async)
{
@@ -76,29 +75,19 @@ internal sealed partial class PvsSystem
{
public int BatchSize => 2;
private PvsSystem _pvs = _pvs;
public int Count => _sessions.Length;
private PvsSession[] _sessions;
public int Count => _pvs._sessions.Length;
public void Execute(int index)
{
try
{
_pvs.ProcessLeavePvs(_sessions[index]);
_pvs.ProcessLeavePvs(_pvs._sessions[index]);
}
catch (Exception e)
{
_pvs.Log.Log(LogLevel.Error, e, $"Caught exception while processing pvs-leave messages.");
}
}
public void Setup(ICommonSession[] sessions)
{
// Copy references to PvsSession, in case players disconnect while the job is running.
Array.Resize(ref _sessions, sessions.Length);
for (var i = 0; i < sessions.Length; i++)
{
_sessions[i] = _pvs.PlayerData[sessions[i]];
}
}
}
}

View File

@@ -100,7 +100,13 @@ internal sealed partial class PvsSystem
/// </summary>
private bool RecursivelyAddOverride(PvsSession session, EntityUid uid, GameTick fromTick, bool addChildren)
{
var xform = _xformQuery.GetComponent(uid);
if (!_xformQuery.TryGetComponent(uid, out var xform))
{
// Can happen if systems add deleted entities to PVS move event.
Log.Error($"Attempted to add non-existent entity {uid} to PVS override for session {session.Session}");
return false;
}
var parent = xform.ParentUid;
// First we process all parents. This is because while this entity may already have been added

View File

@@ -0,0 +1,85 @@
using System;
using System.Threading.Tasks;
using Prometheus;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
internal sealed partial class PvsSystem
{
/// <summary>
/// Compress and send game states to connected clients.
/// </summary>
private void SendStates()
{
// TODO PVS make this async
// AFAICT ForEachAsync doesn't support using a threadlocal PvsThreadResources.
// Though if it is getting pooled, does it really matter?
// If this does get run async, then ProcessDisconnections() has to ensure that the job has finished before modifying
// the sessions array
using var _ = Histogram.WithLabels("Send States").NewTimer();
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
Parallel.ForEach(_sessions, opts, _threadResourcesPool.Get, SendSessionState, _threadResourcesPool.Return);
}
private PvsThreadResources SendSessionState(PvsSession data, ParallelLoopState state, PvsThreadResources resource)
{
try
{
SendSessionState(data, resource.CompressionContext);
}
catch (Exception e)
{
Log.Log(LogLevel.Error, e, $"Caught exception while sending mail for {data.Session}.");
}
return resource;
}
private void SendSessionState(PvsSession data, ZStdCompressionContext ctx)
{
DebugTools.AssertEqual(data.State, null);
// PVS benchmarks use dummy sessions.
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (data.Session.Channel is not DummyChannel)
{
DebugTools.AssertNotEqual(data.StateStream, null);
var msg = new MsgState
{
StateStream = data.StateStream,
ForceSendReliably = data.ForceSendReliably,
CompressionContext = ctx
};
_netMan.ServerSendMessage(msg, data.Session.Channel);
if (msg.ShouldSendReliably())
{
data.RequestedFull = false;
data.LastReceivedAck = _gameTiming.CurTick;
lock (PendingAcks)
{
PendingAcks.Add(data.Session);
}
}
}
else
{
// Always "ack" dummy sessions.
data.LastReceivedAck = _gameTiming.CurTick;
data.RequestedFull = false;
lock (PendingAcks)
{
PendingAcks.Add(data.Session);
}
}
data.StateStream?.Dispose();
data.StateStream = null;
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Threading.Tasks;
using Prometheus;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Player;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
internal sealed partial class PvsSystem
{
[Dependency] private readonly IRobustSerializer _serializer = default!;
/// <summary>
/// Get and serialize <see cref="GameState"/> objects for each player. Compressing & sending the states is done later.
/// </summary>
private void SerializeStates()
{
using var _ = Histogram.WithLabels("Serialize States").NewTimer();
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
_oldestAck = GameTick.MaxValue.Value;
Parallel.For(-1, _sessions.Length, opts, SerializeState);
}
/// <summary>
/// Get and serialize a <see cref="GameState"/> for a single session (or the current replay).
/// </summary>
private void SerializeState(int i)
{
try
{
var guid = i >= 0 ? _sessions[i].Session.UserId.UserId : default;
ServerGameStateManager.PvsEventSource.Log.WorkStart(_gameTiming.CurTick.Value, i, guid);
if (i >= 0)
SerializeSessionState(_sessions[i]);
else
_replay.Update();
ServerGameStateManager.PvsEventSource.Log.WorkStop(_gameTiming.CurTick.Value, i, guid);
}
catch (Exception e) // Catch EVERY exception
{
var source = i >= 0 ? _sessions[i].Session.ToString() : "replays";
Log.Log(LogLevel.Error, e, $"Caught exception while serializing game state for {source}.");
}
}
/// <summary>
/// Get and serialize a <see cref="GameState"/> for a single session.
/// </summary>
private void SerializeSessionState(PvsSession data)
{
ComputeSessionState(data);
InterlockedHelper.Min(ref _oldestAck, data.FromTick.Value);
DebugTools.AssertEqual(data.StateStream, null);
// PVS benchmarks use dummy sessions.
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (data.Session.Channel is not DummyChannel)
{
data.StateStream = RobustMemoryManager.GetMemoryStream();
_serializer.SerializeDirect(data.StateStream, data.State);
}
data.ClearState();
}
}

View File

@@ -7,7 +7,6 @@ using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -26,49 +25,6 @@ internal sealed partial class PvsSystem
private List<ICommonSession> _disconnected = new();
private void SendStateUpdate(ICommonSession session, PvsThreadResources resources)
{
var data = GetOrNewPvsSession(session);
ComputeSessionState(data);
InterlockedHelper.Min(ref _oldestAck, data.FromTick.Value);
// actually send the state
var msg = new MsgState
{
State = data.State,
CompressionContext = resources.CompressionContext
};
// PVS benchmarks use dummy sessions.
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (session.Channel != null)
{
_netMan.ServerSendMessage(msg, session.Channel);
if (msg.ShouldSendReliably())
{
data.RequestedFull = false;
data.LastReceivedAck = _gameTiming.CurTick;
lock (PendingAcks)
{
PendingAcks.Add(session);
}
}
}
else
{
// Always "ack" dummy sessions.
data.LastReceivedAck = _gameTiming.CurTick;
data.RequestedFull = false;
lock (PendingAcks)
{
PendingAcks.Add(session);
}
}
data.ClearState();
}
private PvsSession GetOrNewPvsSession(ICommonSession session)
{
if (!PlayerData.TryGetValue(session, out var pvsSession))
@@ -103,7 +59,7 @@ internal sealed partial class PvsSystem
session.PlayerStates,
_deletedEntities);
session.State.ForceSendReliably = session.RequestedFull
session.ForceSendReliably = session.RequestedFull
|| _gameTiming.CurTick > session.LastReceivedAck + (uint) ForceAckThreshold;
}
@@ -124,14 +80,17 @@ internal sealed partial class PvsSystem
// Update visibility masks & viewer positions
// TODO PVS do this before sending state.
// I,e, we already enumerate over all eyes when computing visible chunks.
Span<MapCoordinates> positions = stackalloc MapCoordinates[session.Viewers.Length];
Span<(MapCoordinates pos, float scale)> positions = stackalloc (MapCoordinates, float)[session.Viewers.Length];
int i = 0;
foreach (var viewer in session.Viewers)
{
if (viewer.Comp2 != null)
session.VisMask |= viewer.Comp2.VisibilityMask;
positions[i++] = _transform.GetMapCoordinates(viewer.Owner, viewer.Comp1);
var mapCoordinates = _transform.GetMapCoordinates(viewer.Owner, viewer.Comp1);
mapCoordinates = mapCoordinates.Offset(viewer.Comp2?.Offset ?? Vector2.Zero);
var scale = MathF.Max((viewer.Comp2?.PvsScale ?? 1), 0.1f);
positions[i++] = (mapCoordinates, scale);
}
if (!CullingEnabled || session.DisableCulling)
@@ -156,16 +115,17 @@ internal sealed partial class PvsSystem
DebugTools.Assert(!chunk.UpdateQueued);
DebugTools.Assert(!chunk.Dirty);
foreach (var pos in positions)
foreach (var (pos, scale) in positions)
{
if (pos.MapId != chunk.Position.MapId)
continue;
dist = Math.Min(dist, (pos.Position - chunk.Position.Position).LengthSquared());
var relative = chunk.InvWorldMatrix.Transform(pos.Position) - chunk.Centre;
var relative = Vector2.Transform(pos.Position, chunk.InvWorldMatrix) - chunk.Centre;
relative = Vector2.Abs(relative);
chebDist = Math.Min(chebDist, Math.Max(relative.X, relative.Y));
chebDist = Math.Min(chebDist, Math.Max(relative.X, relative.Y) / scale);
}
distances.Add(dist);

View File

@@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.ObjectPool;
using Prometheus;
using Robust.Server.Configuration;
@@ -16,9 +14,6 @@ using Robust.Server.Replays;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
@@ -99,6 +94,10 @@ internal sealed partial class PvsSystem : EntitySystem
/// </summary>
private readonly List<GameTick> _deletedTick = new();
/// <summary>
/// The sessions that are currently being processed. Note that this is in general used by parallel & async tasks.
/// Hence player disconnection processing is deferred and only run via <see cref="ProcessDisconnections"/>.
/// </summary>
private PvsSession[] _sessions = default!;
private bool _async;
@@ -183,52 +182,25 @@ internal sealed partial class PvsSystem : EntitySystem
/// </summary>
internal void SendGameStates(ICommonSession[] players)
{
// Wait for pending jobs and process disconnected players
ProcessDisconnections();
// Ensure each session has a PvsSession entry before starting any parallel jobs.
CacheSessionData(players);
// Get visible chunks, and update any dirty chunks.
BeforeSendState();
BeforeSerializeStates();
// Construct & send the game state to each player.
SendStates(players);
// Construct & serialize the game state for each player (and for the replay).
SerializeStates();
// Compress & send the states.
SendStates();
// Cull deletion history
AfterSendState(players);
AfterSerializeStates();
ProcessLeavePvs(players);
}
private void SendStates(ICommonSession[] players)
{
using var _ = Histogram.WithLabels("Send States").NewTimer();
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
_oldestAck = GameTick.MaxValue.Value;
// Replays process game states in parallel with players
Parallel.For(-1, players.Length, opts, _threadResourcesPool.Get, SendPlayer, _threadResourcesPool.Return);
PvsThreadResources SendPlayer(int i, ParallelLoopState state, PvsThreadResources resource)
{
try
{
var guid = i >= 0 ? players[i].UserId.UserId : default;
ServerGameStateManager.PvsEventSource.Log.WorkStart(_gameTiming.CurTick.Value, i, guid);
if (i >= 0)
SendStateUpdate(players[i], resource);
else
_replay.Update();
ServerGameStateManager.PvsEventSource.Log.WorkStop(_gameTiming.CurTick.Value, i, guid);
}
catch (Exception e) // Catch EVERY exception
{
var source = i >= 0 ? players[i].ToString() : "replays";
Log.Log(LogLevel.Error, e, $"Caught exception while generating mail for {source}.");
}
return resource;
}
ProcessLeavePvs();
}
private void ResetParallelism(int _) => ResetParallelism();
@@ -390,8 +362,19 @@ internal sealed partial class PvsSystem : EntitySystem
private (Vector2 worldPos, float range, EntityUid? map) CalcViewBounds(Entity<TransformComponent, EyeComponent?> eye)
{
var size = Math.Max(eye.Comp2?.PvsSize ?? _priorityViewSize, 1);
return (_transform.GetWorldPosition(eye.Comp1), size / 2f, eye.Comp1.MapUid);
var size = _priorityViewSize;
var worldPos = _transform.GetWorldPosition(eye.Comp1);
if (eye.Comp2 is not null)
{
// not using EyeComponent.Eye.Position, because it's updated only on the client's side
worldPos += eye.Comp2.Offset;
size *= eye.Comp2.PvsScale;
}
size = Math.Max(size, 1);
return (worldPos, size / 2f, eye.Comp1.MapUid);
}
private void CullDeletionHistoryUntil(GameTick tick)
@@ -414,23 +397,11 @@ internal sealed partial class PvsSystem : EntitySystem
}
}
private void BeforeSendState()
private void BeforeSerializeStates()
{
DebugTools.Assert(_chunks.Values.All(x => Exists(x.Map) && Exists(x.Root)));
DebugTools.Assert(_chunkSets.Keys.All(Exists));
_leaveTask?.WaitOne();
_leaveTask = null;
foreach (var session in _disconnected)
{
if (PlayerData.Remove(session, out var pvsSession))
{
ClearSendHistory(pvsSession);
FreeSessionDataMemory(pvsSession);
}
}
var ackJob = ProcessQueuedAcks();
// Figure out what chunks players can see and cache some chunk data.
@@ -443,6 +414,21 @@ internal sealed partial class PvsSystem : EntitySystem
ackJob?.WaitOne();
}
internal void ProcessDisconnections()
{
_leaveTask?.WaitOne();
_leaveTask = null;
foreach (var session in _disconnected)
{
if (PlayerData.Remove(session, out var pvsSession))
{
ClearSendHistory(pvsSession);
FreeSessionDataMemory(pvsSession);
}
}
}
internal void CacheSessionData(ICommonSession[] players)
{
Array.Resize(ref _sessions, players.Length);
@@ -452,9 +438,9 @@ internal sealed partial class PvsSystem : EntitySystem
}
}
private void AfterSendState(ICommonSession[] players)
private void AfterSerializeStates()
{
CleanupDirty(players);
CleanupDirty();
if (_oldestAck == GameTick.MaxValue.Value)
{

View File

@@ -1,4 +1,4 @@
using System.Numerics;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Shared.Maths;
@@ -21,7 +21,7 @@ namespace Robust.Server.Maps
get => _offset;
set
{
TransformMatrix = Matrix3.CreateTransform(value, Rotation);
TransformMatrix = Matrix3Helpers.CreateTransform(value, Rotation);
_offset = value;
}
}
@@ -37,14 +37,14 @@ namespace Robust.Server.Maps
get => _rotation;
set
{
TransformMatrix = Matrix3.CreateTransform(Offset, value);
TransformMatrix = Matrix3Helpers.CreateTransform(Offset, value);
_rotation = value;
}
}
private Angle _rotation = Angle.Zero;
public Matrix3 TransformMatrix { get; set; } = Matrix3.Identity;
public Matrix3x2 TransformMatrix { get; set; } = Matrix3x2.Identity;
/// <summary>
/// If there is a map entity serialized should we also load it.

View File

@@ -32,7 +32,7 @@ public sealed partial class GridFixtureSystem
TransformComponent? xformA = null,
TransformComponent? xformB = null)
{
var matrix = Matrix3.CreateTransform(offset, rotation);
var matrix = Matrix3Helpers.CreateTransform(offset, rotation);
Merge(gridAUid, gridBUid, matrix, gridA, gridB, xformA, xformB);
}
@@ -45,7 +45,7 @@ public sealed partial class GridFixtureSystem
public void Merge(
EntityUid gridAUid,
EntityUid gridBUid,
Matrix3 matrix,
Matrix3x2 matrix,
MapGridComponent? gridA = null,
MapGridComponent? gridB = null,
TransformComponent? xformA = null,
@@ -63,7 +63,7 @@ public sealed partial class GridFixtureSystem
while (enumerator.MoveNext(out var tileRef))
{
var offsetTile = matrix.Transform(new Vector2(tileRef.Value.GridIndices.X, tileRef.Value.GridIndices.Y) + gridA.TileSizeHalfVector);
var offsetTile = Vector2.Transform(new Vector2(tileRef.Value.GridIndices.X, tileRef.Value.GridIndices.Y) + gridA.TileSizeHalfVector, matrix);
tiles.Add((offsetTile.Floored(), tileRef.Value.Tile));
}
@@ -87,7 +87,7 @@ public sealed partial class GridFixtureSystem
if (snapgrid == null || snapgrid.Count == 0)
continue;
var offsetTile = matrix.Transform(new Vector2(tileRef.Value.GridIndices.X, tileRef.Value.GridIndices.Y) + gridA.TileSizeHalfVector);
var offsetTile = Vector2.Transform(new Vector2(tileRef.Value.GridIndices.X, tileRef.Value.GridIndices.Y) + gridA.TileSizeHalfVector, matrix);
var tileIndex = offsetTile.Floored();
for (var j = snapgrid.Count - 1; j >= 0; j--)
@@ -124,7 +124,7 @@ public sealed partial class GridFixtureSystem
if (entXform.ParentUid != gridBUid ||
!bounds.Contains(entXform.LocalPosition)) continue;
var newPos = matrix.Transform(entXform.LocalPosition);
var newPos = Vector2.Transform(entXform.LocalPosition, matrix);
_xformSystem.SetCoordinates(ent, entXform, new EntityCoordinates(gridAUid, newPos), entXform.LocalRotation + rotationDiff, oldParent: xformB, newParent: xformA);
}

View File

@@ -172,16 +172,10 @@ namespace Robust.Server.Placement
}
}
var created = _entityManager.SpawnEntity(entityTemplateName, coordinates);
var created = _entityManager.SpawnAttachedTo(entityTemplateName, coordinates, rotation: dirRcv.ToAngle());
var placementCreateEvent = new PlacementEntityEvent(created, coordinates, PlacementEventAction.Create, msg.MsgChannel.UserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementCreateEvent);
// Some entities immediately delete themselves
if (_entityManager.EntityExists(created))
{
_entityManager.GetComponent<TransformComponent>(created).LocalRotation = dirRcv.ToAngle();
}
}
else
{

View File

@@ -90,18 +90,18 @@ namespace Robust.Server.Player
_cfg.SyncConnectingClient(args.Channel);
}
private void EndSession(object? sender, NetChannelArgs args)
{
EndSession(args.Channel.UserId);
}
/// <summary>
/// Ends a clients session, and disconnects them.
/// </summary>
private void EndSession(object? sender, NetChannelArgs args)
internal void EndSession(NetUserId user)
{
if (!TryGetSessionByChannel(args.Channel, out var session))
{
if (!TryGetSessionById(user, out var session))
return;
}
// make sure nothing got messed up during the life of the session
DebugTools.Assert(session.Channel == args.Channel);
SetStatus(session, SessionStatus.Disconnected);
SetAttachedEntity(session, null, out _, true);
@@ -143,7 +143,6 @@ namespace Robust.Server.Player
list.Add(info);
}
netMsg.Plyrs = list;
netMsg.PlyCount = (byte)list.Count;
channel.SendMessage(netMsg);
}
@@ -159,5 +158,32 @@ namespace Robust.Server.Player
session = actor.PlayerSession;
return true;
}
internal ICommonSession AddDummySession(NetUserId user, string name)
{
#if FULL_RELEASE
// Lets not make it completely trivial to fake player counts.
throw new NotSupportedException();
#endif
Lock.EnterWriteLock();
DummySession session;
try
{
UserIdMap[name] = user;
if (!PlayerData.TryGetValue(user, out var data))
PlayerData[user] = data = new(user, name);
session = new DummySession(user, name, data);
InternalSessions.Add(user, session);
}
finally
{
Lock.ExitWriteLock();
}
UpdateState(session);
return session;
}
}
}

View File

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

View File

@@ -116,7 +116,7 @@ internal sealed class HubManager
if (!response.IsSuccessStatusCode)
{
var errorText = await response.Content.ReadAsStringAsync();
_sawmill.Error("Error status while advertising server: [{StatusCode}] {ErrorText}, to {HubUrl}",
_sawmill.Error("Error status while advertising server: [{StatusCode}] {ErrorText}, from {HubUrl}",
response.StatusCode,
errorText,
hubUrl);

View File

@@ -312,14 +312,14 @@ namespace Robust.Server.ServerStatus
}
// Only call this if the download URL is not available!
private async Task<AczManifestInfo?> PrepareAcz()
private async Task<AczManifestInfo?> PrepareAcz(bool optional = false)
{
// Take the ACZ lock asynchronously
await _aczLock.WaitAsync();
try
{
// Setting this now ensures that it won't fail repeatedly on exceptions/etc.
if (_aczPrepareAttempted)
if (_aczPrepareAttempted || optional)
return _aczPrepared;
_aczPrepareAttempted = true;

View File

@@ -80,8 +80,7 @@ namespace Robust.Server.ServerStatus
if (string.IsNullOrEmpty(downloadUrl))
{
var query = HttpUtility.ParseQueryString(context.Url.Query);
var optional = query.Keys;
buildInfo = await PrepareACZBuildInfo();
buildInfo = await PrepareACZBuildInfo(optional: query.Get("can_skip_build") == "1");
}
else
{
@@ -129,9 +128,9 @@ namespace Robust.Server.ServerStatus
};
}
private async Task<JsonObject?> PrepareACZBuildInfo()
private async Task<JsonObject?> PrepareACZBuildInfo(bool optional)
{
var acm = await PrepareAcz();
var acm = await PrepareAcz(optional);
if (acm == null) return null;
// Fork ID is an interesting case, we don't want to cause too many redownloads but we also don't want to pollute disk.

View File

@@ -275,7 +275,7 @@ namespace Robust.Shared.CompNetworkGenerator
{
eventRaise = @"
var ev = new AfterAutoHandleStateEvent(args.Current);
EntityManager.EventBus.RaiseComponentEvent(component, ref ev);";
EntityManager.EventBus.RaiseComponentEvent(uid, component, ref ev);";
}
return $@"// <auto-generated />

View File

@@ -33,7 +33,7 @@ namespace Robust.Shared.Maths
public readonly Vector2 BottomLeft => Origin + Rotation.RotateVec(Box.BottomLeft - Origin);
public readonly Vector2 Center => Origin + Rotation.RotateVec((Box.BottomLeft + Box.TopRight)/2 - Origin);
public Matrix3 Transform => Matrix3.CreateTransform(Origin - Rotation.RotateVec(Origin), Rotation);
public Matrix3x2 Transform => Matrix3Helpers.CreateTransform(Origin - Rotation.RotateVec(Origin), Rotation);
public Box2Rotated(Vector2 bottomLeft, Vector2 topRight)
: this(new Box2(bottomLeft, topRight))

View File

@@ -26,6 +26,7 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -35,11 +36,6 @@ using Robust.Shared.Utility;
using SysVector3 = System.Numerics.Vector3;
using SysVector4 = System.Numerics.Vector4;
#if NETCOREAPP
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
#endif
namespace Robust.Shared.Maths
{
/// <summary>
@@ -50,37 +46,43 @@ namespace Robust.Shared.Maths
public struct Color : IEquatable<Color>, ISpanFormattable
{
/// <summary>
/// The red component of this Color4 structure.
/// The red component of this Color structure.
/// </summary>
public float R;
/// <summary>
/// The green component of this Color4 structure.
/// The green component of this Color structure.
/// </summary>
public float G;
/// <summary>
/// The blue component of this Color4 structure.
/// The blue component of this Color structure.
/// </summary>
public float B;
/// <summary>
/// The alpha component of this Color4 structure.
/// The alpha component of this Color structure.
/// </summary>
public float A;
/// <summary>
/// Vector representation, for easy SIMD operations.
/// </summary>
// ReSharper disable once InconsistentNaming
public readonly SysVector4 RGBA => Unsafe.BitCast<Color, SysVector4>(this);
public readonly byte RByte => (byte) (R * byte.MaxValue);
public readonly byte GByte => (byte) (G * byte.MaxValue);
public readonly byte BByte => (byte) (B * byte.MaxValue);
public readonly byte AByte => (byte) (A * byte.MaxValue);
/// <summary>
/// Constructs a new Color4 structure from the specified components.
/// Constructs a new <see cref="Color"/> structure from the specified components.
/// </summary>
/// <param name="r">The red component of the new Color4 structure.</param>
/// <param name="g">The green component of the new Color4 structure.</param>
/// <param name="b">The blue component of the new Color4 structure.</param>
/// <param name="a">The alpha component of the new Color4 structure.</param>
/// <param name="r">The red component of the new Color structure.</param>
/// <param name="g">The green component of the new Color structure.</param>
/// <param name="b">The blue component of the new Color structure.</param>
/// <param name="a">The alpha component of the new Color structure.</param>
public Color(float r, float g, float b, float a = 1)
{
R = r;
@@ -90,14 +92,23 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Constructs a new Color4 structure from the specified components.
/// Constructs a new Color structure from the components in a <see cref="SysVector4"/>.
/// </summary>
/// <param name="r">The red component of the new Color4 structure.</param>
/// <param name="g">The green component of the new Color4 structure.</param>
/// <param name="b">The blue component of the new Color4 structure.</param>
/// <param name="a">The alpha component of the new Color4 structure.</param>
public Color(in SysVector4 vec)
{
this = Unsafe.BitCast<SysVector4, Color>(vec);
}
/// <summary>
/// Constructs a new Color structure from the specified components.
/// </summary>
/// <param name="r">The red component of the new Color structure.</param>
/// <param name="g">The green component of the new Color structure.</param>
/// <param name="b">The blue component of the new Color structure.</param>
/// <param name="a">The alpha component of the new Color structure.</param>
public Color(byte r, byte g, byte b, byte a = 255)
{
Unsafe.SkipInit(out this);
R = r / (float) byte.MaxValue;
G = g / (float) byte.MaxValue;
B = b / (float) byte.MaxValue;
@@ -124,7 +135,7 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Compares the specified Color4 structures for equality.
/// Compares the specified Color structures for equality.
/// </summary>
/// <param name="left">The left-hand side of the comparison.</param>
/// <param name="right">The right-hand side of the comparison.</param>
@@ -135,7 +146,7 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Compares the specified Color4 structures for inequality.
/// Compares the specified Color structures for inequality.
/// </summary>
/// <param name="left">The left-hand side of the comparison.</param>
/// <param name="right">The right-hand side of the comparison.</param>
@@ -146,10 +157,10 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Converts the specified System.Drawing.Color to a Color4 structure.
/// Converts the specified System.Drawing.Color to a Color structure.
/// </summary>
/// <param name="color">The System.Drawing.Color to convert.</param>
/// <returns>A new Color4 structure containing the converted components.</returns>
/// <returns>A new Color structure containing the converted components.</returns>
public static implicit operator Color(System.Drawing.Color color)
{
return new(color.R, color.G, color.B, color.A);
@@ -181,9 +192,9 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Converts the specified Color4 to a System.Drawing.Color structure.
/// Converts the specified Color to a System.Drawing.Color structure.
/// </summary>
/// <param name="color">The Color4 to convert.</param>
/// <param name="color">The Color to convert.</param>
/// <returns>A new System.Drawing.Color structure containing the converted components.</returns>
public static explicit operator System.Drawing.Color(Color color)
{
@@ -210,11 +221,11 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Compares whether this Color4 structure is equal to the specified object.
/// Compares whether this Color structure is equal to the specified object.
/// </summary>
/// <param name="obj">An object to compare to.</param>
/// <returns>True obj is a Color4 structure with the same components as this Color4; false otherwise.</returns>
public override readonly bool Equals(object? obj)
/// <returns>True obj is a Color structure with the same components as this Color; false otherwise.</returns>
public readonly override bool Equals(object? obj)
{
if (!(obj is Color))
return false;
@@ -223,19 +234,19 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Calculates the hash code for this Color4 structure.
/// Calculates the hash code for this Color structure.
/// </summary>
/// <returns>A System.Int32 containing the hash code of this Color4 structure.</returns>
public override readonly int GetHashCode()
/// <returns>A System.Int32 containing the hash code of this Color structure.</returns>
public readonly override int GetHashCode()
{
return ToArgb();
}
/// <summary>
/// Creates a System.String that describes this Color4 structure.
/// Creates a System.String that describes this Color structure.
/// </summary>
/// <returns>A System.String that describes this Color4 structure.</returns>
public override readonly string ToString()
/// <returns>A System.String that describes this Color structure.</returns>
public readonly override string ToString()
{
return $"{{(R, G, B, A) = ({R}, {G}, {B}, {A})}}";
}
@@ -309,7 +320,6 @@ namespace Robust.Shared.Maths
public static Color FromSrgb(Color srgb)
{
float r, g, b;
#if NETCOREAPP
if (srgb.R <= 0.04045f)
r = srgb.R / 12.92f;
else
@@ -324,22 +334,6 @@ namespace Robust.Shared.Maths
b = srgb.B / 12.92f;
else
b = MathF.Pow((srgb.B + 0.055f) / (1.0f + 0.055f), 2.4f);
#else
if (srgb.R <= 0.04045f)
r = srgb.R / 12.92f;
else
r = (float) Math.Pow((srgb.R + 0.055f) / (1.0f + 0.055f), 2.4f);
if (srgb.G <= 0.04045f)
g = srgb.G / 12.92f;
else
g = (float) Math.Pow((srgb.G + 0.055f) / (1.0f + 0.055f), 2.4f);
if (srgb.B <= 0.04045f)
b = srgb.B / 12.92f;
else
b = (float) Math.Pow((srgb.B + 0.055f) / (1.0f + 0.055f), 2.4f);
#endif
return new Color(r, g, b, srgb.A);
}
@@ -355,7 +349,6 @@ namespace Robust.Shared.Maths
{
float r, g, b;
#if NETCOREAPP
if (rgb.R <= 0.0031308)
r = 12.92f * rgb.R;
else
@@ -370,22 +363,6 @@ namespace Robust.Shared.Maths
b = 12.92f * rgb.B;
else
b = (1.0f + 0.055f) * MathF.Pow(rgb.B, 1.0f / 2.4f) - 0.055f;
#else
if (rgb.R <= 0.0031308)
r = 12.92f * rgb.R;
else
r = (1.0f + 0.055f) * (float) Math.Pow(rgb.R, 1.0f / 2.4f) - 0.055f;
if (rgb.G <= 0.0031308)
g = 12.92f * rgb.G;
else
g = (1.0f + 0.055f) * (float) Math.Pow(rgb.G, 1.0f / 2.4f) - 0.055f;
if (rgb.B <= 0.0031308)
b = 12.92f * rgb.B;
else
b = (1.0f + 0.055f) * (float) Math.Pow(rgb.B, 1.0f / 2.4f) - 0.055f;
#endif
return new Color(r, g, b, rgb.A);
}
@@ -471,6 +448,7 @@ namespace Robust.Shared.Maths
/// Each has a range of 0.0 to 1.0.
/// </returns>
/// <param name="rgb">Color value to convert.</param>
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
public static Vector4 ToHsl(Color rgb)
{
var max = MathF.Max(rgb.R, MathF.Max(rgb.G, rgb.B));
@@ -582,6 +560,7 @@ namespace Robust.Shared.Maths
/// Each has a range of 0.0 to 1.0.
/// </returns>
/// <param name="rgb">Color value to convert.</param>
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
public static Vector4 ToHsv(Color rgb)
{
var max = MathF.Max(rgb.R, MathF.Max(rgb.G, rgb.B));
@@ -770,6 +749,7 @@ namespace Robust.Shared.Maths
/// Each has a range of 0.0 to 1.0.
/// </returns>
/// <param name="rgb">Color value to convert.</param>
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
public static Vector4 ToHcy(Color rgb)
{
var max = MathF.Max(rgb.R, MathF.Max(rgb.G, rgb.B));
@@ -828,23 +808,10 @@ namespace Robust.Shared.Maths
/// with 0.5 being 50% of both colors, 0.25 being 25% of <paramref name="β" /> and 75%
/// <paramref name="α" />.
/// </param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color InterpolateBetween(Color α, Color β, float λ)
{
if (Sse.IsSupported && Fma.IsSupported)
{
var vecA = Unsafe.As<Color, Vector128<float>>(ref α);
var vecB = Unsafe.As<Color, Vector128<float>>(ref β);
vecB = Fma.MultiplyAdd(Sse.Subtract(vecB, vecA), Vector128.Create(λ), vecA);
return Unsafe.As<Vector128<float>, Color>(ref vecB);
}
ref var svA = ref Unsafe.As<Color, SysVector4>(ref α);
ref var svB = ref Unsafe.As<Color, SysVector4>(ref β);
var res = SysVector4.Lerp(svA, svB, λ);
return Unsafe.As<SysVector4, Color>(ref res);
return new(SysVector4.Lerp(α.RGBA, β.RGBA, λ));
}
public static Color? TryFromHex(ReadOnlySpan<char> hexColor)
@@ -1000,13 +967,8 @@ namespace Robust.Shared.Maths
/// <summary>
/// Component wise multiplication of two colors.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static Color operator *(Color a, Color b)
{
return new(a.R * b.R, a.G * b.G, a.B * b.B, a.A * b.A);
}
public static Color operator *(in Color a, in Color b)
=> new(a.RGBA * b.RGBA);
public readonly string ToHex()
{
@@ -1030,17 +992,16 @@ namespace Robust.Shared.Maths
}
/// <summary>
/// Compares whether this Color4 structure is equal to the specified Color4.
/// Compares whether this Color structure is equal to the specified Color.
/// </summary>
/// <param name="other">The Color4 structure to compare to.</param>
/// <returns>True if both Color4 structures contain the same components; false otherwise.</returns>
/// <param name="other">The Color structure to compare to.</param>
/// <returns>True if both Color structures contain the same components; false otherwise.</returns>
public readonly bool Equals(Color other)
{
return
MathHelper.CloseToPercent(R, other.R) &&
MathHelper.CloseToPercent(G, other.G) &&
MathHelper.CloseToPercent(B, other.B) &&
MathHelper.CloseToPercent(A, other.A);
// TODO COLOR why is this approximate
// This method literally doesn't do what its docstring says it does.
// If people wanted approximate equality, they can check that manually.
return MathHelper.CloseToPercent(this, other);
}
[PublicAPI]
@@ -1942,7 +1903,7 @@ namespace Robust.Shared.Maths
public readonly string? Name()
{
return DefaultColorsInverted.TryGetValue(this, out var name) ? name : null;
return DefaultColorsInverted.GetValueOrDefault(this);
}
public static bool TryParse(string input, out Color color)

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