Compare commits

...

146 Commits

Author SHA1 Message Date
PJB3005
4caf72e165 Version: 252.0.3 2025-09-26 13:40:48 +02:00
PJB3005
74ec90ad11 Validate that content assemblies have a limited list of names.
Also, only read assemblies once from disk

(cherry picked from commit 443a8dfca65be7d60c4bd46181b4c749b4756114)
2025-09-26 13:40:47 +02:00
PJB3005
e2cf359c7a Version: 252.0.2 2025-09-19 09:17:32 +02:00
Skye
30b07b7060 Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:32 +02:00
PJB3005
977bb7385c Version: 252.0.1 2025-09-14 14:55:57 +02:00
PJB3005
8de8032904 Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

    This (and the other couple past commits) reported by Elelzedel.

commit 7654d38612
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 22:50:51 2025 +0200

    Move CEF cache out of data directory

    Don't want content messing with this...

commit cdcc255123
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:11:16 2025 +0200

    Make Robust.Client.WebView.Cef.Program internal.

commit 2f56a6a110
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:10:46 2025 +0200

    Update SpaceWizards.NFluidSynth to 0.2.2

commit 16fc48cef2
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:09:43 2025 +0200

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
2025-09-14 14:55:56 +02:00
metalgearsloth
f274de0f10 Version: 252.0.0 2025-04-12 01:43:29 +10:00
Milon
e128338f9d hi (#5811) 2025-04-12 01:29:41 +10:00
metalgearsloth
588c46273e Version: 251.0.0 2025-04-10 20:53:09 +10:00
Whatstone
919de8ce0e SharedPhysicsSystem.Island: set position after velocity (#5801) 2025-04-09 01:10:17 +10:00
Errant
af27d2d872 equatable FormattedMessage, take 2 (#5780)
* improved Equals and getHashCode

* less jank
2025-04-08 16:50:08 +02:00
Tayrtahn
45bb8740a0 Add ForbidLiteralAttribute and analyzer (#5808)
* Add ForbidLiteral attribute, analyzer, and test

* Removed unused code

* Switch order of methods. It's better this way.
2025-04-08 16:39:38 +02:00
Tobias Berger
4a24539629 Don't implement GetMassData twice (#5816)
The removed body didn't calculate mass for circles
2025-04-09 00:14:45 +10:00
PJB3005
7536c4ec68 Log late MsgEntity again
Yay :)
2025-04-07 15:50:40 +02:00
metalgearsloth
9268c8629d Refactor TileEdgeOverlay (#5295)
* Refactor TileEdgeOverlay

* weh

* Updates

* exweh

* Stupid weh noises

* I am le stupid

* Add logging about tile atlas build time

Seems fine, but wanted to check.

* Don't over-allocate edge tile region array

* Add DirectionExtensions.AllDirections

* Clean up iteration in ClydeTileDefinitionManager

* Don't stackalloc large chunk edge buffers, other code cleanup.

* More release notes

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-04-03 07:52:28 +02:00
PJB3005
0bc0cafe64 Show entity name in "physics shapeinfo" output 2025-04-03 02:59:09 +02:00
PJB3005
8891f3fa0a Make EntitySystem.Subscriptions.SubscribeLocalEvent not require EntityEventArgs
This means it can be used with struct events.
2025-04-03 02:58:20 +02:00
Tornado Tech
4f96c2d233 Added separate localization & clean up (#5227)
* Added separate localization & clean up

* Added new methods docs

* Added GetFoundCultures method

* Clean up code

* Removed some formating shit

* Do better CultureInfo comparison

* Oops

* Review

* Command fixes

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-04-02 16:41:02 +02:00
PJB3005
ab55d5b2f2 Revert "Add Loc property to LocalizedCommands"
This reverts commit 806c5b694b.
2025-04-02 05:23:43 +02:00
PJB3005
806c5b694b Add Loc property to LocalizedCommands
Avoids some ~300 usages of static Loc in SS14 and RT
2025-04-02 05:21:43 +02:00
metalgearsloth
6898053dbd Add autocomplete to tp command (#5795) 2025-04-02 03:48:16 +02:00
metalgearsloth
ae625ebad8 Inline manifold points (#5794)
* Inline manifold points

Max is 2 so no reason to use an array over a fixedarray.

* this
2025-04-02 00:04:51 +11:00
metalgearsloth
3c754a4f49 Don't disable contacting collisionwake ents (#5798)
Good for some content stuff I don't think it caused issues.
2025-04-01 15:04:05 +11:00
Tayrtahn
d84cb6327c Fix SharedTransformSystem methods erroring on failed Resolves (#5787)
* Don't error when GetGrid fails

* Fix other Resolves in SharedTransformSystem
2025-04-01 04:59:47 +11:00
Ciarán Walsh
4bfd92dbc5 Add button to jump to live chat when scrolled up (#5750)
* Add button to jump to live chat when scrolled up

* Expose scroll button class name as a public constant

* Add localisation string to engine

* Make enabling the OutputPanel scroll button opt-in

* Enable scroll button for the debug console

* De-duplicate visibility logic

* Update scroll button visibility when the enabling property is changed
2025-03-30 03:07:54 +02:00
metalgearsloth
c7d228c223 savemap / savegrid autocomplete (#5784)
How mappers coping without this.
2025-03-27 18:54:13 +11:00
Tayrtahn
f244c94905 Switch from checking comp.Owner == ent to GetComp(ent) == comp (#5776) 2025-03-27 15:21:05 +11:00
Tayrtahn
01cac6465b Cleanup warnings in EntityCoordinates_Tests (#5771)
* Fix warnings

* Use transform refs to simplify WithEntityId
2025-03-27 15:20:20 +11:00
metalgearsloth
5a5f238d9a Version: 250.0.0 2025-03-27 15:12:01 +11:00
Tayrtahn
089224cd44 Cleanup warnings in EntityLookup_Test (#5775)
* Replace MapManager.DeleteMap calls with MapSystem.DeleteMap

* Remove unused resolves
2025-03-27 15:07:05 +11:00
Tayrtahn
9f807f1ad2 Cleanup warnings in ClientGameStateManager (#5774)
* Fix unreachable code

* Remove unused variable
2025-03-27 15:06:37 +11:00
metalgearsloth
4be95ea375 Add OtherBody API to contacts (#5779)
* Add OtherBody API to contacts

Thought I had a pr for this.

* Update Robust.Shared/Physics/Dynamics/Contacts/Contact.cs

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2025-03-27 15:06:04 +11:00
Jerry
03010bf4be Fix DirectoryNotFoundException when saving map or grid on Unix systems (#5773)
* fix(maploader): Fix DirectoryNotFoundException

Someone forgor to create directory before saving map or grid, this caused
an exception on trying to save smth anywhere else than /

* update release notes
2025-03-27 13:56:26 +11:00
Tayrtahn
dacaa974d4 Replace MapManager.DeleteMap with SharedMapSystem.DeleteMap in misc tests (#5777)
* Replace MapManager.DeleteMap with SharedMapSystem.DeleteMap in various tests

* Poke tests

* I guess this is was technically a breaking change?
2025-03-27 13:50:56 +11:00
DrSmugleaf
9f73e0398a Make MappingDataNode.Equals take 2952 times less time to run when loading maps (#5781) 2025-03-27 12:52:29 +11:00
Tayrtahn
ccc383b1bf Cleanup: Remove redundant Prototype names (#5721)
* Cleanup: Remove redundant Prototype names

* Actually this one probably should stay

* Suppress warning

* Remove warning suppression on AudioMetadataPrototype

* But wait, there's more!
2025-03-26 01:39:10 +01:00
PJB3005
ceb59402a1 Make status and info APIs have CORS allow-origin: *
Allows it to be queried from browser JS. No harm in not allowing this.

Added helper function StatusExt.AddAllowOriginAny to make this easy to add.
2025-03-26 01:16:23 +01:00
Errant
5a6b29fcd2 equatable FormattedMessage (#5772) 2025-03-25 15:32:47 +01:00
PJB3005
6b87cd1e1c Deprecate HCY color space functions
These functions use the wrong color primaries (Rec 601 instead of 709) to calculate the luminance, so I'm just gonna throw them out.

Also, I don't actually trust anybody to know to do sRGB conversion before using them.

Discovered this as a result of reviewing #5360
2025-03-24 04:47:34 +01:00
Leon Friedrich
cf2d6a1dbf Make unshaded sprite layers not require shader/texture changes. (#5248)
* Make unshaded sprite layers not require shader/texture changes.

* Always sample texture

* revert some variable name changes

* Use color SIMD
2025-03-24 02:37:04 +01:00
chromiumboy
f8c838f425 Pass AnimationPlayerComponent in AnimationCompletionEvent (#5755)
* Added a field for the animation player component to the animation completion event

* Addressed review comment

* Updated
2025-03-24 02:22:46 +01:00
Tayrtahn
7405904041 Add entity description as tooltip on entity spawn panel (#5761) 2025-03-23 14:07:22 +01:00
metalgearsloth
2eeebab275 Add pure to some angle methods (#5763)
Saw some of these floating around in mover code.
2025-03-22 13:43:18 +01:00
PJB3005
2856bb3626 Shut up RS1038 warnings
We aren't going to fix these until #5610 is figured out, and these aren't even an indicator of an issue itself.

They indicate that we have code fixes in the same assembly as analyzers, meaning the analyzers COULD fail if we relied on some code fix libs - something we don't do.
2025-03-22 06:20:43 +01:00
PJB3005
be0189748b Fix serialization source gen with partial types
This fixes RMC compilation
2025-03-22 06:09:48 +01:00
Tayrtahn
4529a7569a Replace uses of ProtoId<EntityPrototype> with ProtoId<EntityCategoryPrototype> (#5762) 2025-03-21 04:08:12 +01:00
metalgearsloth
2b16e4db96 Version: 249.0.0 2025-03-21 00:52:45 +11:00
metalgearsloth
64f2245194 Add UpdateVisibilityMask method (#5745)
* Add UpdateVisibilityMask method

We tipped over to the point of systems stepping on each other's toes. Now we do the normal thing and just use the eventbus and it makes content a whole lot cleaner.

* Update resolve

* Update name in line with normal.

* Unserialize this

* weh
2025-03-21 00:47:40 +11:00
Milon
1029047e2f fix (#5752) 2025-03-20 22:30:59 +11:00
metalgearsloth
45dc9ad80e Inline polygon vertices (#5758)
* FastPoly

* Inline polygon vertices

No more pooling, pooling bad. No more arrays in engine, only span.

I made a slim version that's only the 4 verts so no padding on it compared to Polygon. Slightly more clamplicated code but entitylookup + mapmanager are both hotpaths and it's easy to do. Memory usage will likely go up for now but heap allocations should drop significantly due to removing the pooling.

* Unhide these

* Fixes

* More fixes

* More fixes

* Avoid potential bomb
2025-03-20 21:26:05 +11:00
metalgearsloth
54ad808eea Add GetWorldManifold overload (#5756)
* Add GetWorldManifold overload

* revert
2025-03-20 21:10:04 +11:00
metalgearsloth
37c75df6a2 Fix showvelocities (#5759)
* Fix showvelocities

Can't use StateRoot anymore so just pretend it's a window.

* Also file-scoped
2025-03-20 21:05:58 +11:00
slarticodefast
e93c1fae61 Add velocity and angular velocity debug overlays (#5693)
* add velocity and angular velocity debug overlays

* minor improvement

* add descriptions

* fix

* review

* Update RELEASE-NOTES.md

* Update RELEASE-NOTES.md

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-03-20 13:10:16 +11:00
metalgearsloth
cd0a35f542 Fix light Aabb query (#5744)
Forgot when this got dropped but we need it for grid-tree query as lights spill over grids.
2025-03-13 19:18:23 +11:00
PJB3005
80f2dc6dd3 Fix BoxContainer stretching causing controls to be made too small
In which I fix a bug by just deleting a ton of code and doing nothing else.

(also I added unit tests)
2025-03-13 01:01:18 +01:00
metalgearsloth
139b6f796c Version: 248.0.2 2025-03-12 20:26:50 +11:00
metalgearsloth
2ee7c35fd3 Don't throw on invalid MapUids for overlays (#5740) 2025-03-12 20:16:13 +11:00
metalgearsloth
56eae5ad08 Reduce EntityManager.IsDefault allocations (#5741)
* Reduce EntityManager.IsDefault allocations

We don't actually need the MappingDataNode so we can avoid allocating the entire class per entity.

* Adjust this order
2025-03-12 19:42:08 +11:00
DrSmugleaf
0662cae224 Version: 248.0.1 2025-03-11 19:38:08 -07:00
metalgearsloth
0e2b00edd0 Fix NaN gain audio (#5737) 2025-03-10 20:16:05 +11:00
Richard Van Tassel
d48f7ecb5b bumps ImageSharp version (#5733) 2025-03-08 15:19:23 +01:00
metalgearsloth
e064b7a4f9 Version: 248.0.0 2025-03-08 15:34:53 +11:00
metalgearsloth
654480862e Use Prototype for ITileDef (#5731)
Forgot to push this.
2025-03-08 15:20:25 +11:00
metalgearsloth
353c044b52 Hot reload resources (#5443)
* Fix ResPath CanonPath

Apparently this is supposed to standardise to / but this isn't always the case. Alternatively we could just assert for performance reasons I'm good with either. The comment as written says this should happen.

* Fixes

* change

* assert

* Fix bad respath input

* Buffer

* Merge conflicts

* review

* Fix
2025-03-08 15:16:31 +11:00
Whatstone
3dda8d9e93 Try/catch blocks around BUI open/dispose calls (#5730) 2025-03-08 15:16:11 +11:00
metalgearsloth
41ea10083d Fix ResPath CanonPath (#5452)
* Fix ResPath CanonPath

Apparently this is supposed to standardise to / but this isn't always the case. Alternatively we could just assert for performance reasons I'm good with either. The comment as written says this should happen.

* assert

* Fix bad respath input

* review
2025-03-08 15:02:46 +11:00
eoineoineoin
6290bb7af1 Adds method to Controls.ItemList which updates the item list without erasing contents (#5425)
* Add algorithm from ss14#30292 to ItemList, for use in other UIs

* Add overload for common case of comparing items by their text label
2025-03-08 14:43:43 +11:00
metalgearsloth
348ab70a8d Update B2DynamicTree (#5332)
* Update B2DynamicTree

* API updates

* weh

* forcing it

* Fix all of the bugs

* Rebuild

* A crumb of danger

* Fix merge conflicts
2025-03-08 14:15:47 +11:00
IProduceWidgets
47e11e988c Fix map netId completions (#5495)
* make map netId completions function.

* Update Robust.Shared/Console/CompletionHelper.cs

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-03-08 13:57:13 +11:00
Leon Friedrich
c459b55052 Add TryLoadGrid override (#5701)
* Add TryLoadGrid override

* fix cmd-addmap-help
2025-03-08 13:50:14 +11:00
Tayrtahn
f10e96a6d1 Clean up warnings in Stack_Test (#5705) 2025-03-08 13:49:52 +11:00
metalgearsloth
e8bac558c6 Use Entity<T> for TileChangedEvent (#5698)
Content is manually resolving this in a few spots so we can avoid that overhead.
2025-03-08 13:48:32 +11:00
metalgearsloth
ffa3bb7202 Fix savedpos caching on UI shutdown (#5729)
Don't need this as it gets handled already.
2025-03-08 13:28:29 +11:00
Dylan Craine
9bcdc95651 Fix deceptive "successfully saved" messages for mappers (#5714)
* Actually check if map save succeeded before displaying success message

It would be great to offer more clarity to the mapper about *why* the
save didn't succeed, but at least they won't be deceived into thinking
their work has been saved when it hasn't.

Portuguese localization text is via DuckDuckGo Translate, so I hope it's
reasonable.

* Actually check save success for saving grids

These messages need localization, too, but that seems out of scope for
my PR.

* Improve map save error message

Now it tells the mapper to go look at the server log.
Still translated via DuckDuckGo Translate.

* Normalize indentation and style
2025-03-08 13:27:59 +11:00
TemporalOroboros
543088ea1f Remove unused private members/local vars (#5722) 2025-03-05 15:50:54 +01:00
deltanedas
56daa63783 make map loading logs better (#5723)
Co-authored-by: deltanedas <@deltanedas:kde.org>
2025-03-05 15:49:20 +01:00
metalgearsloth
9fe9730d4a Add public API for physicshull (#5719) 2025-03-03 22:07:38 +11:00
SlamBamActionman
a1a7ea92d9 Add Regex.Count and StringBuilder.set_Chars to sandbox whitelist (#5717) 2025-03-01 22:42:19 +01:00
poklj
0dec6a425f Modify TryCopyComponents metadata TryGetComponent (#5710) 2025-02-27 11:25:28 +11:00
metalgearsloth
56ced913b7 Audio fixes (#5707) 2025-02-26 22:08:17 +11:00
PJB3005
76b46479b6 Version: 247.2.0 2025-02-23 01:44:44 +01:00
PJB3005
de9a8d286a Release notes 2025-02-23 01:43:58 +01:00
Milon
a1df0fb4af fix some issues with ClientDisconnect (#5625)
* fix

* actually fix for real this time

* just use HappyEyeballsHttp :godo:

* review
2025-02-23 01:33:58 +01:00
pathetic meowmeow
e6bc5a1057 Proxy scrollbar values and value targets in ScrollContainer (#5697) 2025-02-22 22:10:32 +01:00
beck-thompson
11b24579a2 Fix MultiRootInheritanceGraph not detecting circular inheritance (#5672)
* Fix

* This is better!

* Fixes
2025-02-22 22:08:31 +01:00
metalgearsloth
685d002bb7 Move VisibilitySystem to shared (#5694)
* Move VisibilitySystem to shared

* this

* Remove redundant qualifiers.

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-02-22 21:36:49 +01:00
Tobias Berger
2e0d18aeaf Fix wrong parameters for Regex.Escape in Sandbox whitelist (#5688) 2025-02-22 18:08:28 +01:00
Kyle Tyo
06dbff0429 believe that should be all of em. (#5691) 2025-02-22 18:01:07 +01:00
Southbridge
15958a9447 Fix issue regarding Tilemaps not saving when modified (#5696)
* One line C# fix

* fixed typo

* after spending way too long trying to figure out the problem, turns out all we needed was a simple one line change
2025-02-22 17:33:33 +01:00
pathetic meowmeow
fd5a4d9b8a Refactor audio system to send collection IDs over the network (#5540)
This is important groundwork for future features such as captioning,
as a caption and other data can be associated with the collection
prototype instead of passing extra data everywhere with the sound.
2025-02-22 17:29:47 +01:00
DrSmugleaf
6d958847cb Fix prototype hot reloading crashing when adding a component that an existing entity already has (#5695) 2025-02-22 16:30:05 +01:00
Milon
8a04a4f3a5 Add a method for copying components (#5654)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-02-21 09:46:13 +11:00
ElectroJr
7104a4f459 Version: 247.1.0 2025-02-20 16:26:04 +13:00
Leon Friedrich
f29949a32c Revert "Add ICloneable support to ComponentNetworkGenerator (#5656)" (#5687)
This reverts commit e14537074e.
2025-02-20 14:22:36 +11:00
Leon Friedrich
3bbbabf238 Update map format validator (#5686)
* Update map format validator

* string -> str
2025-02-20 12:11:12 +11:00
metalgearsloth
d95aca3d9e Fix DirtyFields proxy method (#5684) 2025-02-20 00:15:05 +11:00
Tayrtahn
e14537074e Add ICloneable support to ComponentNetworkGenerator (#5656) 2025-02-18 23:21:40 +11:00
DrSmugleaf
af2d01981f Add optional minimumDistance parameter to SharedJointSystem.CreateDistanceJoint (#5682) 2025-02-18 14:18:15 +11:00
Fildrance
7df23e047c feat: shaders now can accept array of Color as parameter (#5679)
* feat: shaders now can accept array of Color as parameter

* fix: Clyde.SetUniformDirect for Color[] doesn't mutate original array, removed invalid 'in' keyword on SetUniform

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
2025-02-16 14:13:04 +01:00
ElectroJr
5c7ab43049 Fix typo in RELEASE-NOTES.md 2025-02-17 00:15:27 +13:00
ElectroJr
8f75560ec4 Version: 247.0.0 2025-02-17 00:14:18 +13:00
Leon Friedrich
b323c8bd1e Add support for optional and params T[] toolshed command arguments. (#5573)
* Include argument name in completion suggestions

* Support optional args

* It (not so shrimply) works

* Add tests

* Add TestGenericPipeInference

* Fix tests

* Release notes

* Overzealous YAMLLinter

* Improve help signatures, fix map command

* Improve NoImplementationError

* Better type argument help signatures

* better pipe syntax

* fix NRE

* Add test

* a

* Fix silent toolshed failure

* Fix GetConcreteMethodInternal

* Improve vars command

* EntProtoId IAsType

* More GetConcreteMethodInternal fixes

* I hate this so much

* update tp command description

The command arguments call the the "other" entity the "target"

* Support localized argument hints/signatures
2025-02-16 21:55:05 +11:00
Leon Friedrich
faef44daaa Make PVS overrides respect vismasks (#5598)
* Make overrides respect vismasks

* Thread safety

* Release notes

* Use ExpandPvsEvent.Mask for other overrides

* check if already queued
2025-02-16 21:32:23 +11:00
Leon Friedrich
fbc706f37b Refactor map loading & saving (#5572)
* Refactor map loading & saving

* test fixes

* ISerializationManager tweaks

* Fix component composition

* Try fix entity deserialization component composition

* comments

* CL

* error preinit

* a

* cleanup

* error if version is too new

* Add AlwaysPushSerializationTest

* Add auto-inclusion test

* Better categorization

* Combine test components

* Save -> TrySave

Also better handling for saving multiple entities individually

* Create new partial class for map loading

* Add OrphanSerializationTest

* Include MapIds in BeforeSerializationEvent

* Addd LifetimeSerializationTest

* Add TestMixedLifetimeSerialization

* Add CategorizationTest

* explicitly serialize list of nullspace entities

* Add backwards compatibility test

* Version comments

also fixes wrong v4 format

* add MapMergeTest

* Add NetEntity support

* Optimize EntityDeserializer

Avoid unnecessary component deserialization

* fix assert & other bugs

* fucking containers strike again

* Fix deletion of pre-init entities

* fix release note merge conflict

* Update Robust.Shared/Map/MapManager.GridCollection.cs

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* VV

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-02-16 21:25:07 +11:00
metalgearsloth
9d1b15ab4b Version: 246.0.0 2025-02-16 19:32:42 +11:00
metalgearsloth
ea1cc5e446 Planet lighting pre-reqs (#5490)
* Add another lookup overload

* Fix RenderInRenderTarget

See the linked issue for what happens.

* Also this one

* stuff

* Fix stencilling

* fixes

* mix blend

* fix

* blur fixes

* Tile flag

* Minor tweak

* Fixes

* Render state fixes

* Fixes

* Fix stupidity

* More state render bug fixes

* MapUid on overlay draw

* Remove blur comment

* Fixes

* Fixes

* Remove

* Engine vibe
2025-02-16 19:29:32 +11:00
metalgearsloth
bcb5c2d35d Version: 245.1.0 2025-02-16 14:56:39 +11:00
metalgearsloth
c011eff80e Increase audio despawn buffer (#5665)
Apparently it can clip and the buffer is really just there so we despawn 'at some point' and rather than hunching over my debugger for potentially an hour this is easier and almost no impact.

I've also considered flagging some audio as "play the full thing" if someone misses the start of it but need to thonk on that one a bit in future.
2025-02-16 14:30:18 +11:00
Fildrance
e163c496c3 fix: fixed EntityPrototypeView not reacting on SetPrototype when EnteredTree already was called with _currentPrototype empty (#5649)
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
2025-02-16 14:29:59 +11:00
Tayrtahn
fec81bc2a1 Add more info to AnchorEntity debug assert (#5668) 2025-02-16 03:16:23 +01:00
Leon Friedrich
7016facb9a Tweak UserInterfaceComponent shutdown to prevent bugs (#5678) 2025-02-14 18:16:24 +11:00
Simon
0c41a041e3 Move ParseObject method into a public class for content to use (#5674) 2025-02-14 14:21:22 +11:00
ElectroJr
55571ef5b1 Version: 245.0.0 2025-02-14 16:11:50 +13:00
Leon Friedrich
afaef645b0 Fix MappingDataNode.TryAddCopy() (#5677) 2025-02-14 14:10:13 +11:00
Milon
d442d90d60 no more (#5676) 2025-02-13 01:41:42 -05:00
metalgearsloth
fea592e1d5 Version: 244.0.0 2025-02-12 13:31:03 +11:00
metalgearsloth
bb9517fd19 Check equatable on client-predicted BUI states (#5671) 2025-02-12 13:29:19 +11:00
metalgearsloth
a734bc50fa Avoid unnecessary UpdateState calls for BUIs (#5670) 2025-02-12 12:50:33 +11:00
metalgearsloth
9e9ac56c95 Bump physics speed cap (#5667) 2025-02-11 20:54:21 +11:00
metalgearsloth
6979a63b1e Add CreateWindowCenteredRight method (#5666) 2025-02-11 15:02:31 +11:00
metalgearsloth
ae7725aafe Add compreg methods to entitymanager (#5655) 2025-02-10 21:39:41 +11:00
metalgearsloth
1a7e490e4b Version: 243.0.1 2025-02-08 19:06:50 +11:00
metalgearsloth
51971d0994 Revert basewindow change (#5664) 2025-02-08 17:38:40 +11:00
Mohamed Dwidar
d2aa8ecb5a Fixing guidebook not resizable from left and right (#5618)
* Fixing guidebook not resizable from left and right

issue 34504 in space-wizards/space-station-14 needs this fix

* update fix guideBood not resizable

a safer and more error resistent solution to https://github.com/space-wizards/space-station-14/issues/34504

* Mask DragMode.Move

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2025-02-08 17:14:08 +11:00
ScarKy0
c4a5752c2a Shared portion of PvsOverrideSystem to allow for usage in shared (#5651) 2025-02-04 12:23:42 -05:00
metalgearsloth
6a336d236b Version: 243.0.0 2025-02-04 00:25:15 +11:00
metalgearsloth
fc55c8e0d3 Add saved BUI positions (#5650) 2025-02-04 00:19:22 +11:00
metalgearsloth
2719b9f0c8 Tweak grid state slightly (#5644) 2025-02-04 00:15:36 +11:00
metalgearsloth
bd69d51d36 Version: 242.0.1 2025-02-02 12:29:24 +11:00
metalgearsloth
1bf0687671 Fix poly pooling (#5645) 2025-02-01 15:19:34 +11:00
Leon Friedrich
bdef9e3401 Fix reloading prototypes with AlwaysPushInheritance (#5612) 2025-01-31 15:38:13 +11:00
metalgearsloth
43648201ce Drop debug allocs a lot (#5638)
IDK why the lifestage one in particular ballooned my dotmemory one up in particular but it did; I would've thought if it's boxing the other ones would've shown up. Doesn't matter for release just QOL to drop allocs by more than half.
2025-01-30 05:07:57 +01:00
slarticodefast
2b2d08ba47 don't normalize direction vector when calling atan2 (#5641) 2025-01-30 12:07:34 +11:00
metalgearsloth
d7f6a9ba43 Version: 242.0.0 2025-01-29 23:45:59 +11:00
metalgearsloth
15f81751f7 Reduce lookup allocs significantly (#5639) 2025-01-29 23:43:20 +11:00
metalgearsloth
033c52751a MergeImplicitData fix (#5640) 2025-01-29 23:36:14 +11:00
Leon Friedrich
51edceae4d Ensure parents are always initialized & started before children (#5595)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-01-29 23:16:14 +11:00
ShadowCommander
5d7720755a Fix keyboard not focusing on search bar of the tile spawn window on open (#5630) 2025-01-29 22:48:59 +11:00
Leon Friedrich
acc7bf7595 Add support for overlays to draw controls (#5223)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-01-29 22:48:11 +11:00
Fildrance
de55d1bc52 fix: EntityPrototypeView now only creates entities when the UI is open
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-01-28 00:25:52 +11:00
metalgearsloth
d5e6e91b58 Make collisionwake resolve false (#5637) 2025-01-28 00:05:29 +11:00
metalgearsloth
da2bfdaa10 Version: 241.0.0 2025-01-27 21:31:31 +11:00
Leon Friedrich
af6cac14d6 Add CollisionPredictionTest (#5493)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-01-27 21:23:23 +11:00
Leon Friedrich
3f37846731 Avoid unnecessary DirtyField() calls (#5620) 2025-01-27 21:21:53 +11:00
metalgearsloth
d9bf1d1afb BUI deferral tweaks (#5503) 2025-01-27 21:04:27 +11:00
metalgearsloth
b9b80192e7 Minor contact QOL (#5560) 2025-01-27 16:20:04 +11:00
369 changed files with 16936 additions and 5219 deletions

View File

@@ -55,9 +55,9 @@
<PackageVersion Include="Serilog" Version="4.2.0" />
<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.6" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.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,401 @@ END TEMPLATE-->
*None yet*
## 252.0.3
## 252.0.2
## 252.0.1
## 252.0.0
### Breaking changes
* BoundUserInterfaceMessageAttempt is raised directed against entities and no longer broadcast.
## 251.0.0
### Breaking changes
* Localization is now separate between client and server and is handled via cvar.
* Contacting entities no longer can be disabled for CollisionWake to avoid destroying the contacts unnecessarily.
### New features
* Added `DirectionExtensions.AllDirections`, which contains a list of all `Direction`s for easy enumeration.
* Add ForbidLiteralAttribute.
* Log late MsgEntity again.
* Show entity name in `physics shapeinfo` output.
* Make SubscribeLocalEvent not require EntityEventArgs.
* Add autocomplete to `tp` command.
* Add button to jump to live chat when scrolled up.
* Add autocomplete to `savemap` and `savegrid`.
### Bugfixes
* Fix velocity not re-applying correctly on re-parenting.
* Fix Equatable on FormattedMessage.
* Fix SharedTransformSystem methods logging errors on resolves.
### Other
* Significantly optimized tile edge rendering.
### Internal
* Remove duplicate GetMassData method.
* Inline manifold points for physics.
## 250.0.0
### Breaking changes
* The default shader now interprets negative color modulation as a flag that indicates that the light map should be ignored.
* This can be used to avoid having to change the light map texture, thus reducing draw batches.
* Sprite layers that are set to use the "unshaded" shader prototype now use this.
* Any fragment shaders that previously the `VtxModulate` colour modulation variable should instead use the new `MODULATE` variable, as the former may now contain negative values.
### New features
* Add OtherBody API to contacts.
* Make FormattedMessages Equatable.
* AnimationCompletionEvent now has the AnimationPlayerComponent.
* Add entity description as a tooltip on the entity spawn panel.
### Bugfixes
* Fix serialization source generator breaking if a class has two partial locations.
* Fix map saving throwing a `DirectoryNotFoundException` when given a path with a non-existent directory. Now it once again creates any missing directories.
* Fix map loading taking a significant time due to MappingDataNode.Equals calls being slow.
### Other
* Add Pure to some Angle methods.
### Internal
* Cleanup some warnings in classes.
## 249.0.0
### Breaking changes
* Layer is now read-only on VisibilityComponent and isn't serialized.
### New features
* Added a debug overlay for the linear and angular velocity of all entities on the screen. Use the `showvel` and `showangvel` commands to toggle it.
* Add a GetWorldManifold overload that doesn't require a span of points.
* Added a GetVisMaskEvent. Calling `RefreshVisibilityMask` will raise it and subscribers can update the vismask via the event rather than subscribers having to each manually try and handle the vismask directly.
### Bugfixes
* `BoxContainer` no longer causes stretching children to go below their minimum size.
* Fix lights on other grids getting clipped due to ignoring the light range cvar.
* Fix the `showvelocities` command.
* Fix the DirtyFields overload not being sandbox safe for content.
### Internal
* Polygon vertices are now inlined with FixedArray8 and a separate SlimPolygon using FixedArray4 for hot paths rather than using pooled arrays.
## 248.0.2
### Bugfixes
* Don't throw in overlay rendering if MapUid not found.
### Internal
* Reduce EntityManager.IsDefault allocations.
## 248.0.1
### Bugfixes
* Bump ImageSharp version.
* Fix instances of NaN gain for audio where a negative-infinity value is being used for volume.
## 248.0.0
### Breaking changes
* Use `Entity<MapGridComponent>` for TileChangedEvent instead of EntityUid.
* Audio files are no longer tempo perfect when being played if the offset is small. At some point in the future an AudioParams bool is likely to be added to enforce this.
* MoveProxy method args got changed in the B2DynamicTree update.
* ResPath will now assert in debug if you pass in an invalid path containing the non-standardized directory separator.
### New features
* Added a new `MapLoaderSystem.TryLoadGrid()` override that loads a grid onto a newly created map.
* Added a CVar for the endbuffer for audio. If an audio file will play below this length (for PVS reasons) it will be ignored.
* Added Regex.Count + StringBuilder.Chars setter to the sandbox.
* Added a public API for PhysicsHull.
* Made MapLoader log more helpful.
* Add TryLoadGrid override that also creates a map at the same time.
* Updated B2Dynamictree to the latest Box2D V3 version.
* Added SetItems to ItemList control to set items without removing the existing ones.
* Shaders, textures, and audio will now hot reload automatically to varying degrees. Also added IReloadManager to handle watching for file-system changes and relaying events.
* Wrap BUI disposes in a try-catch in case of exceptions.
### Bugfixes
* Fix some instances of invalid PlaybackPositions being set.
* Play audio from the start of a file if it's only just come into PVS range / had its state handled.
* Fix TryCopyComponents.
* Use shell.WriteError if TryLoad fails for mapping commands.
* Fix UI control position saving causing exceptions where the entity is cleaned-up alongside a state change.
* Fix Map NetId completions.
* Fix some ResPath calls using the wrong paths.
### Internal
* Remove some unused local variables and the associated warnings.
## 247.2.0
### New features
* Added functions for copying components to `IEntityManager` and `EntitySystem`.
* Sound played from sound collections is now sent as "collection ID + index" over the network instead of the final filename.
* This enables integration of future accessibility systems.
* Added a new `ResolvedSoundSpecifier` to represent played sounds. Methods that previously took a filename now take a `ResolvedSoundSpecifier`, with an implicit cast from string being interpreted as a raw filename.
* `VisibilitySystem` has been made accessible to shared as `SharedVisibilitySystem`.
* `ScrollContainer` now has properties exposing `Value` and `ValueTarget` on its internal scroll bars.
### Bugfixes
* Fix prototype hot reload crashing when adding a new component already exists on an entity.
* Fix maps failing to save in some cases related to tilemap IDs.
* Fix `Regex.Escape(string)` not being available in sandbox.
* Prototypes that parent themselves directly won't cause the game to hang on an infinite loop anymore.
* Fixed disconnecting during a connection attempt leaving the client stuck in a phantom state.
### Internal
* More warning cleanup.
## 247.1.0
### New features
* Added support for `Color[]` shader uniforms
* Added optional minimumDistance parameter to `SharedJointSystem.CreateDistanceJoint()`
### Bugfixes
* Fixed `EntitySystem.DirtyFields()` not actually marking fields as dirty.
### Other
* Updated the Yamale map file format validator to support v7 map/grid files.
## 247.0.0
### Breaking changes
* `ITileDefinitionManager.AssignAlias` and general tile alias functionality has been removed. `TileAliasPrototype` still exist, but are only used during entity deserialization.
* `IMapManager.AddUninitializedMap` has been removed. Use the map-init options on `CreateMap()` instead.
* Re-using a MapId will now log a warning. This may cause some integration tests to fail if they are configured to fail
when warnings are logged.
* The minimum supported map format / version has been increased from 2 to 3.
* The server-side `MapLoaderSystem` and associated classes & structs has been moved to `Robust.Shared`, and has been significantly modified.
* The `TryLoad` and `Save` methods have been replaced with grid, map, generic entity variants. I.e, `SaveGrid`, `SaveMap`, and `SaveEntities`.
* Most of the serialization logic and methods have been moved out of `MapLoaderSystem` and into new `EntitySerializer`
and `EntityDeserializer` classes, which also replace the old `MapSerializationContext`.
* The `MapLoadOptions` class has been split into `MapLoadOptions`, `SerializationOptions`, and `DeserializationOptions`
structs.
* The interaction between PVS overrides and visibility masks / layers have changed:
* Any forced entities (i.e., `PvsOverrideSystem.AddForceSend()`) now ignore visibility masks.
* Any global & session overrides (`PvsOverrideSystem.AddGlobalOverride()` & `PvsOverrideSystem.AddSessionOverride()`) now respect visibility masks.
* Entities added via the `ExpandPvsEvent` respect visibility masks.
* The mask used for any global/session overrides can be modified via `ExpandPvsEvent.Mask`.
* Toolshed Changes:
* The signature of Toolshed type parsers have changed. Instead of taking in an optional command argument name string, they now take in a `CommandArgument` struct.
* Toolshed commands can no longer contain a '|', as this symbol is now used for explicitly piping the output of one command to another. command pipes. The existing `|` and '|~' commands have been renamed to `bitor` and `bitnotor`.
* Semicolon terminated command blocks in toolshed commands no longer return anything. I.e., `i { i 2 ; }` is no longer a valid command, as the block has no return value.
### New features
* The current map format/version has increased from 6 to 7 and now contains more information to try support serialization of maps with null-space entities and full game saves.
* `IEntitySystemManager` now provides access to the system `IDependencyCollection`.
* Toolshed commands now support optional and `params T[]` arguments. optional / variable length commands can be terminated using ';' or '|'.
### Bugfixes
* Fixed entity deserialization for components with a data fields that have a AlwaysPushInheritance Attribute
* Audio entities attached to invisible / masked entities should no longer be able to temporarily make those entities visible to all players.
* The map-like Toolshed commands now work when a collection is piped in.
* Fixed a bug in toolshed that could cause it to preferentially use the incorrect command implementation.
* E.g., passing a concrete enumerable type would previously use the command implementation that takes in an unconstrained generic parameter `T` instead of a dedicated `IEnumeerable<T>` implementation.
### Other
* `MapChangedEvent` has been marked as obsolete, and should be replaced with `MapCreatedEvent` and `MapRemovedEvent.
* The default auto-completion hint for Toolshed commands have been changed and somewhat standardized. Most parsers should now generate a hint of the form:
* `<name (Type)>` for mandatory arguments
* `[name (Type)]` for optional arguments
* `[name (Type)]...` for variable length arguments (i.e., for `params T[]`)
## 246.0.0
### Breaking changes
* The fixes to renderer state may have inadvertantly broken some rendering code that relied upon the old behavior.
* TileRenderFlag has been removed and now it's just a byte flag on the tile for content usage.
### New features
* Add BeforeLighting overlay draw space for overlays that need to draw directly to lighting and want to do it immediately beforehand.
* Change BlurLights to BlurRenderTarget and make it public for content usage.
* Add ContentFlag to tiles for content-flag usage.
* Add a basic mix shader for doing canvas blends.
* Add GetClearColorEvent for content to override the clear color behavior.
### Bugfixes
* Fix pushing renderer state not restoring stencil status, blend status, queued shader instance scissor state.
## 245.1.0
### New features
* Add more info to the AnchorEntity debug message.
* Make ParseObject public where it will parse a supplied Type and string into the specified object.
### Bugfixes
* Fix EntityPrototypeView not always updating the entity correctly.
* Tweak BUI shutdown to potentially avoid skipping closing.
### Other
* Increase Audio entity despawn buffer to avoid clipping.
## 245.0.0
### Breaking changes
* `BoundUserInterface.Open()` now has the `MustCallBase` attribute
### Bugfixes
* Fixed an error in `MappingDataNode.TryAddCopy()`, which was causing yaml inheritance/deserialization bugs.
## 244.0.0
### Breaking changes
* Increase physics speedcap default from 35m/s to 400m/s in-line with box2d v3.
### New features
* Add EntityManager overloads for ComponentRegistration that's faster than the generic methods.
* Add CreateWindowCenteredRight for BUIs.
### Bugfixes
* Avoid calling UpdateState before opening a BUI.
## 243.0.1
### Bugfixes
* Fixed `BaseWindow` sometimes not properly updating the mouse cursor shape.
* Revert `BaseWindow` OnClose ordering due to prior reliance upon the ordering.
## 243.0.0
### Breaking changes
* RemoveChild is called after OnClose for BaseWindow.
### New features
* BUIs now have their positions saved when closed and re-used when opened when using the `CreateWindow<T>` helper or via manually registering it via RegisterControl.
### Other
* Ensure grid fixtures get updated in client state handling even if exceptions occur.
## 242.0.1
### Bugfixes
* Fixed prototype reloading/hotloading not properly handling data-fields with the `AlwaysPushInheritanceAttribute`
* Fix the pooled polygons using incorrect vertices for EntityLookup and MapManager.
### Internal
* Avoid normalizing angles constructed from vectors.
## 242.0.0
### Breaking changes
* The order in which the client initialises networked entities has changed. It will now always apply component states, initialise, and start an entity's parent before processing any children. This might break anything that was relying on the old behaviour where all component states were applied before any entities were initialised & started.
* `IClydeViewport` overlay rendering methods now take in an `IRenderHandle` instead of a world/screen handle.
* The `OverlayDrawArgs` struct now has an internal constructor.
### New features
* Controls can now be manually restyled via `Control.InvalidateStyleSheet()` and `Control.DoStyleUpdate()`
* Added `IUserInterfaceManager.RenderControl()` for manually drawing controls.
* `OverlayDrawArgs` struct now has an `IRenderHandle` field such that overlays can use the new `RenderControl()` methods.
* TileSpawnWindow will now take focus when opened.
### Bugfixes
* Fixed a client-side bug where `TransformComponent.GridUid` does not get set properly when an existing entity is attached to a new entity outside of the player's PVS range.
* EntityPrototypeView will only create entities when it's on the UI tree and not when the prototype is set.
* Make CollisionWake not log errors if it can't resolve.
### Other
* Replace IPhysShape API with generics on IMapManager and EntityLookupSystem.
### Internal
* Significantly reduce allocations for Box2 / Box2Rotated queries.
## 241.0.0
### Breaking changes
* Remove DeferredClose from BUIs.
### New features
* Added `EntityManager.DirtyFields()`, which allows components with delta states to simultaneously mark several fields as dirty at the same time.
* Add `CloserUserUIs<T>` to close keys of a specific key.
### Bugfixes
* Fixed `RaisePredictiveEvent()` not properly re-raising events during prediction for event handlers that did not take an `EntitySessionEventArgs` argument.
* BUI openings are now deferred to avoid having slight desync between deferred closes and opens occurring in the same tick.
## 240.1.2

View File

@@ -4,6 +4,12 @@
kind: canvas
light_mode: unshaded
# Simple mix blend
- type: shader
id: Mix
kind: canvas
blend_mode: Mix
- type: shader
id: shaded
kind: canvas

View File

@@ -11,6 +11,7 @@ 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-cultureinfo = "{$arg}" is not valid CultureInfo.
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}
@@ -156,6 +157,7 @@ cmd-savemap-not-exist = Target map does not exist.
cmd-savemap-init-warning = Attempted to save a post-init map without forcing the save.
cmd-savemap-attempt = Attempting to save map {$mapId} to {$path}.
cmd-savemap-success = Map successfully saved.
cmd-savemap-error = Could not save map! See server log for details.
cmd-hint-savemap-id = <MapID>
cmd-hint-savemap-path = <Path>
cmd-hint-savemap-force = [bool]
@@ -293,7 +295,7 @@ cmd-lsgrid-desc = Lists grids.
cmd-lsgrid-help = lsgrid
cmd-addmap-desc = Adds a new empty map to the round. If the mapID already exists, this command does nothing.
cmd-addmap-help = addmap <mapID> [initialize]
cmd-addmap-help = addmap <mapID> [pre-init]
cmd-rmmap-desc = Removes a map from the world. You cannot remove nullspace.
cmd-rmmap-help = rmmap <mapId>
@@ -427,11 +429,20 @@ cmd-entfo-help = Usage: entfo <entityuid>
The entity UID can be prefixed with 'c' to convert it to a client entity UID.
cmd-fuck-desc = Throws an exception
cmd-fuck-help = Throws an exception
cmd-fuck-help = Usage: fuck
cmd-showpos-desc = Enables debug drawing over all entity positions in the game.
cmd-showpos-desc = Show the position of all entities on the screen.
cmd-showpos-help = Usage: showpos
cmd-showrot-desc = Show the rotation of all entities on the screen.
cmd-showrot-help = Usage: showrot
cmd-showvel-desc = Show the local velocity of all entites on the screen.
cmd-showvel-help = Usage: showvel
cmd-showangvel-desc = Show the angular velocity of all entities on the screen.
cmd-showangvel-help = Usage: showangvel
cmd-sggcell-desc = Lists entities on a snap grid cell.
cmd-sggcell-help = Usage: sggcell <gridID> <vector2i>\nThat vector2i param is in the form x<int>,y<int>.
@@ -562,3 +573,8 @@ cmd-pvs-override-info-desc = Prints information about any PVS overrides associat
cmd-pvs-override-info-empty = Entity {$nuid} has no PVS overrides.
cmd-pvs-override-info-global = Entity {$nuid} has a global override.
cmd-pvs-override-info-clients = Entity {$nuid} has a session override for {$clients}.
cmd-localization_set_culture-desc = Set DefaultCulture for the client LocalizationManager
cmd-localization_set_culture-help = Usage: localization_set_culture <cultureName>
cmd-localization_set_culture-culture-name = <cultureName>
cmd-localization_set_culture-changed = Localization changed to { $code } ({ $nativeName } / { $englishName })

View File

@@ -14,6 +14,10 @@ tile-spawn-window-title = Place Tiles
console-line-edit-placeholder = Command Here
## OutputPanel
output-panel-scroll-down-button-text = Scroll Down
## Common Used
window-erase-button-text = Erase Mode

View File

@@ -42,8 +42,7 @@ command-description-as =
command-description-count =
Counts the amount of entries in it's input, returning an integer.
command-description-map =
Maps the input over the given block, with the provided expected return type.
This command may be modified to not need an explicit return type in the future.
Maps the input over the given block.
command-description-select =
Selects N objects or N% of objects from the input.
One can additionally invert this command with not to make it select everything except N objects instead.
@@ -149,7 +148,7 @@ command-description-max =
Returns the maximum of two values.
command-description-BitAndCommand =
Performs bitwise AND.
command-description-BitOrCommand =
command-description-bitor =
Performs bitwise OR.
command-description-BitXorCommand =
Performs bitwise XOR.
@@ -203,11 +202,11 @@ command-description-mappos =
command-description-pos =
Returns an entity's coordinates.
command-description-tp-coords =
Teleports the target to the given coordinates.
Teleports the given entities to the target coordinates.
command-description-tp-to =
Teleports the target to the given other entity.
Teleports the given entities to the target entity.
command-description-tp-into =
Teleports the target "into" the given other entity, attaching it at (0 0) relative to it.
Teleports the given entities "into" the target entity, attaching it at (0 0) relative to it.
command-description-comp-get =
Gets the given component from the given entity.
command-description-comp-add =
@@ -277,7 +276,7 @@ command-description-ModVecCommand =
Performs the modulus operation over the input with the given constant right-hand value.
command-description-BitAndNotCommand =
Performs bitwise AND-NOT over the input.
command-description-BitOrNotCommand =
command-description-bitornot =
Performs bitwise OR-NOT over the input.
command-description-BitXnorCommand =
Performs bitwise XNOR over the input.

View File

@@ -136,6 +136,7 @@ cmd-savemap-not-exist = O mapa de destino não existe.
cmd-savemap-init-warning = Tentativa de salvar um mapa pós-inicialização sem forçar o salvamento.
cmd-savemap-attempt = Tentando salvar o mapa {$mapId} em {$path}.
cmd-savemap-success = Mapa salvo com sucesso.
cmd-savemap-error = Não foi possível salvar o mapa! Consulte o log do servidor para obter detalhes.
cmd-hint-savemap-id = <MapID>
cmd-hint-savemap-path = <Path>
cmd-hint-savemap-force = [bool]

View File

@@ -0,0 +1,189 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ForbidLiteralAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
public sealed class ForbidLiteralAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<ForbidLiteralAnalyzer, DefaultVerifier>()
{
TestState =
{
Sources = { code },
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Analyzers.ForbidLiteralAttribute.cs"
);
test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
private const string TestTypeDefs = """
using System.Collections.Generic;
using Robust.Shared.Analyzers;
public sealed class TestClass
{
public static void OneParameterForbidden([ForbidLiteral] string value) { }
public static void TwoParametersFirstForbidden([ForbidLiteral] string first, string second) { }
public static void TwoParametersBothForbidden([ForbidLiteral] string first, [ForbidLiteral] string second) { }
public static void ListParameterForbidden([ForbidLiteral] List<string> values) { }
public static void ParamsListParameterForbidden([ForbidLiteral] params List<string> values) { }
}
public record struct StringWrapper(string value)
{
private readonly string _value = value;
public static implicit operator string(StringWrapper wrapper)
{
return wrapper._value;
}
}
""";
[Test]
public async Task TestOneParameter()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
private static readonly StringWrapper WrappedValue = new("biz");
public void Test()
{
TestClass.OneParameterForbidden(_constValue);
TestClass.OneParameterForbidden(StaticValue);
TestClass.OneParameterForbidden(WrappedValue);
TestClass.OneParameterForbidden("baz");
}
}
""";
await Verifier(code,
// /0/Test0.cs(12,41): error RA0033: The "value" parameter of OneParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(12, 41, 12, 46).WithArguments("value", "OneParameterForbidden")
);
}
[Test]
public async Task TestTwoParametersFirstForbidden()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
public void Test()
{
TestClass.TwoParametersFirstForbidden(_constValue, "whatever");
TestClass.TwoParametersFirstForbidden(_constValue, _constValue);
TestClass.TwoParametersFirstForbidden("foo", "whatever");
}
}
""";
await Verifier(code,
// /0/Test0.cs(9,47): error RA0033: The "first" parameter of TwoParametersFirstForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(9, 47, 9, 52).WithArguments("first", "TwoParametersFirstForbidden")
);
}
[Test]
public async Task TestTwoParametersBothForbidden()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
public void Test()
{
TestClass.TwoParametersBothForbidden(_constValue, _constValue);
TestClass.TwoParametersBothForbidden(_constValue, StaticValue);
TestClass.TwoParametersBothForbidden(_constValue, "whatever");
TestClass.TwoParametersBothForbidden("whatever", _constValue);
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,59): error RA0033: The "second" parameter of TwoParametersBothForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 59, 10, 69).WithArguments("second", "TwoParametersBothForbidden"),
// /0/Test0.cs(11,46): error RA0033: The "first" parameter of TwoParametersBothForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(11, 46, 11, 56).WithArguments("first", "TwoParametersBothForbidden")
);
}
[Test]
public async Task TestListParameter()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
private static readonly StringWrapper WrappedValue = new("biz");
public void Test()
{
TestClass.ListParameterForbidden([_constValue, StaticValue, WrappedValue]);
TestClass.ListParameterForbidden(["foo", _constValue, "bar"]);
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,43): warning RA0033: The "values" parameter of ListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 43, 10, 48).WithArguments("values", "ListParameterForbidden"),
// /0/Test0.cs(10,63): warning RA0033: The "values" parameter of ListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 63, 10, 68).WithArguments("values", "ListParameterForbidden")
);
}
[Test]
public async Task TestParamsListParameter()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
private static readonly StringWrapper WrappedValue = new("biz");
public void Test()
{
TestClass.ParamsListParameterForbidden(_constValue, StaticValue, WrappedValue);
TestClass.ParamsListParameterForbidden("foo", _constValue, "bar");
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,48): warning RA0033: The "values" parameter of ParamsListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 48, 10, 53).WithArguments("values", "ParamsListParameterForbidden"),
// /0/Test0.cs(10,68): warning RA0033: The "values" parameter of ParamsListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 68, 10, 73).WithArguments("values", "ParamsListParameterForbidden")
);
}
}

View File

@@ -13,6 +13,7 @@
<EmbeddedResource Include="..\Robust.Shared\Analyzers\MustCallBaseAttribute.cs" LogicalName="Robust.Shared.IoC.MustCallBaseAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferNonGenericVariantForAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferOtherTypeAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferOtherTypeAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ForbidLiteralAttribute.cs" LogicalName="Robust.Shared.Analyzers.ForbidLiteralAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\GameObjects\EventBusAttributes.cs" LogicalName="Robust.Shared.GameObjects.EventBusAttributes.cs" LinkBase="Implementations" />
</ItemGroup>

View File

@@ -0,0 +1,101 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ForbidLiteralAnalyzer : DiagnosticAnalyzer
{
private const string ForbidLiteralType = "Robust.Shared.Analyzers.ForbidLiteralAttribute";
public static DiagnosticDescriptor ForbidLiteralRule = new(
Diagnostics.IdForbidLiteral,
"Parameter forbids literal values",
"The {0} parameter of {1} forbids literal values",
"Usage",
DiagnosticSeverity.Warning,
true,
"Pass in a validated wrapper type like ProtoId, or a const or static value."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [ForbidLiteralRule];
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterOperationAction(AnalyzeOperation, OperationKind.Invocation);
}
private void AnalyzeOperation(OperationAnalysisContext context)
{
if (context.Operation is not IInvocationOperation invocationOperation)
return;
// Check each parameter of the method invocation
foreach (var argumentOperation in invocationOperation.Arguments)
{
// Check for our attribute on the parameter
if (!AttributeHelper.HasAttribute(argumentOperation.Parameter, ForbidLiteralType, out _))
continue;
// Handle parameters using the params keyword
if (argumentOperation.Syntax is InvocationExpressionSyntax subExpressionSyntax)
{
// Check each param value
foreach (var subArgument in subExpressionSyntax.ArgumentList.Arguments)
{
CheckArgumentSyntax(context, argumentOperation, subArgument);
}
continue;
}
// Not params, so just check the single parameter
if (argumentOperation.Syntax is not ArgumentSyntax argumentSyntax)
continue;
CheckArgumentSyntax(context, argumentOperation, argumentSyntax);
}
}
private void CheckArgumentSyntax(OperationAnalysisContext context, IArgumentOperation operation, ArgumentSyntax argumentSyntax)
{
// Handle collection types
if (argumentSyntax.Expression is CollectionExpressionSyntax collectionExpressionSyntax)
{
// Check each value of the collection
foreach (var elementSyntax in collectionExpressionSyntax.Elements)
{
if (elementSyntax is not ExpressionElementSyntax expressionSyntax)
continue;
// Check if a literal was passed in
if (expressionSyntax.Expression is not LiteralExpressionSyntax)
continue;
context.ReportDiagnostic(Diagnostic.Create(ForbidLiteralRule,
expressionSyntax.GetLocation(),
operation.Parameter.Name,
(context.Operation as IInvocationOperation).TargetMethod.Name
));
}
return;
}
// Not a collection, just a single value to check
// Check if it's a literal
if (argumentSyntax.Expression is not LiteralExpressionSyntax)
return;
context.ReportDiagnostic(Diagnostic.Create(ForbidLiteralRule,
argumentSyntax.GetLocation(),
operation.Parameter.Name,
(context.Operation as IInvocationOperation).TargetMethod.Name
));
}
}

View File

@@ -0,0 +1,96 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using JetBrains.Annotations;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.UnitTesting.Server;
namespace Robust.Benchmarks.EntityManager;
[Virtual]
public partial class HasComponentBenchmark
{
private static readonly Consumer Consumer = new();
private ISimulation _simulation = default!;
private IEntityManager _entityManager = default!;
private ComponentRegistration _compReg = default!;
private A _dummyA = new();
[UsedImplicitly]
[Params(1, 10, 100, 1000)]
public int N;
[GlobalSetup]
public void GlobalSetup()
{
_simulation = RobustServerSimulation
.NewSimulation()
.RegisterComponents(f => f.RegisterClass<A>())
.InitializeInstance();
_entityManager = _simulation.Resolve<IEntityManager>();
var map = _simulation.CreateMap().Uid;
var coords = new EntityCoordinates(map, default);
_compReg = _entityManager.ComponentFactory.GetRegistration(typeof(A));
for (var i = 0; i < N; i++)
{
var uid = _entityManager.SpawnEntity(null, coords);
_entityManager.AddComponent<A>(uid);
}
}
[Benchmark]
public void HasComponentGeneric()
{
for (var i = 2; i <= N+1; i++)
{
var uid = new EntityUid(i);
var result = _entityManager.HasComponent<A>(uid);
Consumer.Consume(result);
}
}
[Benchmark]
public void HasComponentCompReg()
{
for (var i = 2; i <= N+1; i++)
{
var uid = new EntityUid(i);
var result = _entityManager.HasComponent(uid, _compReg);
Consumer.Consume(result);
}
}
[Benchmark]
public void HasComponentType()
{
for (var i = 2; i <= N+1; i++)
{
var uid = new EntityUid(i);
var result = _entityManager.HasComponent(uid, typeof(A));
Consumer.Consume(result);
}
}
[Benchmark]
public void HasComponentGetType()
{
for (var i = 2; i <= N+1; i++)
{
var uid = new EntityUid(i);
var type = _dummyA.GetType();
var result = _entityManager.HasComponent(uid, type);
Consumer.Consume(result);
}
}
[ComponentProtoName("A")]
public sealed partial class A : Component
{
}
}

View File

@@ -26,10 +26,10 @@ public class PhysicsTumblerBenchmark
var entManager = _sim.Resolve<IEntityManager>();
var physics = entManager.System<SharedPhysicsSystem>();
var fixtures = entManager.System<FixtureSystem>();
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
var mapUid = entManager.System<SharedMapSystem>().CreateMap(out var mapId);
SetupTumbler(entManager, mapId);
for (var i = 0; i < 800; i++)
for (var i = 0; i < 300; i++)
{
entManager.TickUpdate(0.016f, false);
var boxUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 10f, mapId));
@@ -42,6 +42,9 @@ public class PhysicsTumblerBenchmark
physics.WakeBody(boxUid, body: box);
physics.SetSleepingAllowed(boxUid, box, false);
}
if (entManager.TryGetComponent(mapUid, out BroadphaseComponent? mapBroadphase))
entManager.System<SharedBroadphaseSystem>().RebuildBottomUp(mapBroadphase);
}
[Benchmark]
@@ -49,7 +52,7 @@ public class PhysicsTumblerBenchmark
{
var entManager = _sim.Resolve<IEntityManager>();
for (var i = 0; i < 5000; i++)
for (var i = 0; i < 1000; i++)
{
entManager.TickUpdate(0.016f, false);
}

View File

@@ -6,7 +6,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
public static class Program
internal static class Program
{
// This was supposed to be the main entry for the subprocess program... It doesn't work.
public static int Main(string[] args)

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Reflection;
using System.Text;
using Robust.Client.Console;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
@@ -24,6 +25,7 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameControllerInternal _gameController = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -61,7 +63,10 @@ namespace Robust.Client.WebView.Cef
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
var settings = new CefSettings()
{

View File

@@ -40,11 +40,7 @@ namespace Robust.Client.Animations
var keyFrame = KeyFrames[keyFrameIndex];
var audioParams = keyFrame.AudioParamsFunc.Invoke();
var audio = new SoundPathSpecifier(keyFrame.Resource)
{
Params = audioParams
};
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(audio, Filter.Local(), entity, true);
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(keyFrame.Specifier, Filter.Local(), entity, true, audioParams);
}
return (keyFrameIndex, playingTime);
@@ -55,7 +51,7 @@ namespace Robust.Client.Animations
/// <summary>
/// The RSI state to play when this keyframe gets triggered.
/// </summary>
public readonly string Resource;
public readonly ResolvedSoundSpecifier Specifier;
/// <summary>
/// A function that returns the audio parameter to be used.
@@ -69,9 +65,9 @@ namespace Robust.Client.Animations
/// </summary>
public readonly float KeyTime;
public KeyFrame(string resource, float keyTime, Func<AudioParams>? audioParams = null)
public KeyFrame(ResolvedSoundSpecifier specifier, float keyTime, Func<AudioParams>? audioParams = null)
{
Resource = resource;
Specifier = specifier;
KeyTime = keyTime;
AudioParamsFunc = audioParams ?? (() => AudioParams.Default);
}

View File

@@ -84,6 +84,19 @@ internal partial class AudioManager
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
}
void IAudioInternal.Remove(AudioStream stream)
{
if (stream.ClydeHandle == null)
return;
if (!_audioSampleBuffers.Remove(stream.BufferId))
{
return;
}
AL.DeleteBuffer(stream.BufferId);
}
/// <inheritdoc/>
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
{
@@ -120,9 +133,9 @@ internal partial class AudioManager
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
return new AudioStream(handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
return new AudioStream(this, buffer, handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
}
/// <inheritdoc/>
@@ -179,9 +192,9 @@ internal partial class AudioManager
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
return new AudioStream(handle, length, wav.NumChannels, name);
return new AudioStream(this, buffer, handle, length, wav.NumChannels, name);
}
/// <inheritdoc/>
@@ -210,8 +223,8 @@ internal partial class AudioManager
var handle = new ClydeHandle(_audioSampleBuffers.Count);
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
return new AudioStream(handle, length, channels, name);
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
return new AudioStream(this, buffer, handle, length, channels, name);
}
public void SetMasterGain(float newGain)
@@ -293,7 +306,7 @@ internal partial class AudioManager
// ReSharper disable once PossibleInvalidOperationException
// TODO: This really shouldn't be indexing based on the ClydeHandle...
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value].BufferHandle);
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[stream.BufferId].BufferHandle);
var audioSource = new AudioSource(this, source, stream);
_audioSources.Add(source, new WeakReference<BaseAudioSource>(audioSource));
@@ -370,5 +383,12 @@ internal partial class AudioManager
}
_bufferedAudioSources.Clear();
foreach (var buffer in _audioSampleBuffers.Values)
{
DeleteAudioBufferOnMainThread(buffer.BufferHandle);
}
_audioSampleBuffers.Clear();
}
}

View File

@@ -5,6 +5,7 @@ using System.Threading;
using OpenTK.Audio.OpenAL;
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Sources;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
@@ -17,13 +18,15 @@ internal sealed partial class AudioManager : IAudioInternal
{
[Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
[Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
[Shared.IoC.Dependency] private readonly IReloadManager _reload = default!;
[Shared.IoC.Dependency] private readonly IResourceCache _cache = default!;
private Thread? _gameThread;
private ALDevice _openALDevice;
private ALContext _openALContext;
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
private readonly Dictionary<int, LoadedAudioSample> _audioSampleBuffers = new();
private readonly Dictionary<int, WeakReference<BaseAudioSource>> _audioSources =
new();
@@ -116,6 +119,22 @@ internal sealed partial class AudioManager : IAudioInternal
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterGain, true);
_reload.Register("/Audio", "*.ogg");
_reload.Register("/Audio", "*.wav");
_reload.OnChanged += OnReload;
}
private void OnReload(ResPath args)
{
if (args.Extension != "ogg" &&
args.Extension != "wav")
{
return;
}
_cache.ReloadResource<AudioResource>(args);
}
internal bool IsMainThread()
@@ -140,6 +159,11 @@ internal sealed partial class AudioManager : IAudioInternal
}
}
internal void LogError(string message)
{
OpenALSawmill.Error(message);
}
/// <summary>
/// Like _checkAlError but allows custom data to be passed in as relevant.
/// </summary>

View File

@@ -6,8 +6,15 @@ namespace Robust.Client.Audio;
/// <summary>
/// Has the metadata for a particular audio stream as well as the relevant internal handle to it.
/// </summary>
public sealed class AudioStream
public sealed class AudioStream : IDisposable
{
private IAudioInternal _audio;
/// <summary>
/// Buffer ID for this audio in AL.
/// </summary>
internal int BufferId { get; }
public TimeSpan Length { get; }
internal IClydeHandle? ClydeHandle { get; }
public string? Name { get; }
@@ -15,8 +22,10 @@ public sealed class AudioStream
public string? Artist { get; }
public int ChannelCount { get; }
internal AudioStream(IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
internal AudioStream(IAudioInternal internalAudio, int bufferId, IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
{
_audio = internalAudio;
BufferId = bufferId;
ClydeHandle = handle;
Length = length;
ChannelCount = channelCount;
@@ -24,4 +33,9 @@ public sealed class AudioStream
Title = title;
Artist = artist;
}
public void Dispose()
{
_audio.Remove(this);
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using OpenTK.Audio.OpenAL;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
@@ -56,6 +57,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
private EntityQuery<PhysicsComponent> _physicsQuery;
private float _maxRayLength;
private float _zOffset;
private float _audioEndBuffer;
public override float ZOffset
{
@@ -79,8 +82,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
}
private float _zOffset;
/// <inheritdoc />
public override void Initialize()
{
@@ -108,20 +109,31 @@ public sealed partial class AudioSystem : SharedAudioSystem
SubscribeNetworkEvent<PlayAudioEntityMessage>(OnEntityAudio);
SubscribeNetworkEvent<PlayAudioPositionalMessage>(OnEntityCoordinates);
Subs.CVar(CfgManager, CVars.AudioEndBuffer, OnAudioBuffer, true);
Subs.CVar(CfgManager, CVars.AudioAttenuation, OnAudioAttenuation, true);
Subs.CVar(CfgManager, CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
Subs.CVar(CfgManager, CVars.AudioTickRate, OnAudioTickRate, true);
InitializeLimit();
}
private void OnAudioBuffer(float value)
{
_audioEndBuffer = value;
}
private void OnAudioTickRate(int obj)
{
_audioFrameTime = 1f / obj;
_audioFrameTimeRemaining = MathF.Min(_audioFrameTimeRemaining, _audioFrameTime);
}
private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAutoHandleStateEvent args)
private void OnAudioState(Entity<AudioComponent> entity, ref AfterAutoHandleStateEvent args)
{
var component = entity.Comp;
if (component.LifeStage < ComponentLifeStage.Initialized)
return;
ApplyAudioParams(component.Params, component);
component.Source.Global = component.Global;
@@ -145,21 +157,29 @@ public sealed partial class AudioSystem : SharedAudioSystem
case AudioState.Stopped:
component.StopPlaying();
component.PlaybackPosition = 0f;
break;
return;
}
// If playback position changed then update it.
if (!string.IsNullOrEmpty(component.FileName))
{
var position = (float) ((component.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
var currentPosition = component.Source.PlaybackPosition;
var diff = Math.Abs(position - currentPosition);
var position = (float) ((entity.Comp.PauseTime ?? Timing.CurTime) - entity.Comp.AudioStart).TotalSeconds;
var currentPosition = entity.Comp.Source.PlaybackPosition;
var diff = Math.Abs(position - currentPosition);
if (diff > 0.1f)
// Don't try to set the audio too far ahead.
if (!string.IsNullOrEmpty(entity.Comp.FileName))
{
if (position > GetAudioLengthImpl(entity.Comp.FileName).TotalSeconds - _audioEndBuffer)
{
component.PlaybackPosition = position;
entity.Comp.StopPlaying();
return;
}
}
// If the difference is minor then we'll just keep playing it.
if (diff > 0.1f)
{
entity.Comp.PlaybackPosition = position;
}
}
/// <summary>
@@ -207,6 +227,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
private void SetupSource(Entity<AudioComponent> entity, AudioResource audioResource, TimeSpan? length = null)
{
var component = entity.Comp;
length ??= GetAudioLength(component.FileName);
// If audio came into range then start playback at the correct position.
var offset = ((entity.Comp.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
if (TryAudioLimit(component.FileName))
{
@@ -230,10 +254,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
// Don't play until first frame so occlusion etc. are correct.
component.Gain = 0f;
length ??= GetAudioLength(component.FileName);
// If audio came into range then start playback at the correct position.
var offset = (Timing.CurTime - component.AudioStart).TotalSeconds % length.Value.TotalSeconds;
// If the offset < buffer than just play it from the start.
if (offset < AudioDespawnBuffer)
{
offset = 0;
}
// Not enough audio to play
else if (offset > length.Value.TotalSeconds - _audioEndBuffer)
{
component.StopPlaying();
return;
}
if (offset > 0)
{
@@ -415,6 +446,16 @@ public sealed partial class AudioSystem : SharedAudioSystem
return occlusion;
}
private bool TryGetAudio(ResolvedSoundSpecifier specifier, [NotNullWhen(true)] out AudioResource? audio)
{
var filename = GetAudioPath(specifier);
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
return true;
Log.Error($"Server tried to play audio file {filename} which does not exist.");
return false;
}
private bool TryGetAudio(string filename, [NotNullWhen(true)] out AudioResource? audio)
{
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
@@ -433,15 +474,15 @@ public sealed partial class AudioSystem : SharedAudioSystem
return false;
}
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityCoordinates coordinates,
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
return PlayStatic(filename, Filter.Local(), coordinates, true, audioParams);
return PlayStatic(specifier, Filter.Local(), coordinates, true, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, Filter.Local(), uid, true, audioParams);
return PlayEntity(specifier, Filter.Local(), uid, true, audioParams);
}
/// <inheritdoc />
@@ -477,21 +518,21 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, AudioParams? audioParams = null, bool recordReplay = true)
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
{
FileName = filename,
Specifier = specifier,
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayGlobal(audio, audioParams) : default;
return TryGetAudio(specifier, out var audio) ? PlayGlobal(audio, specifier, audioParams) : default;
}
/// <summary>
@@ -499,9 +540,9 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="audioParams"></param>
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, AudioParams? audioParams = null)
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
{
var (entity, component) = CreateAndStartPlayingStream(audioParams, stream);
var (entity, component) = CreateAndStartPlayingStream(audioParams, specifier, stream);
component.Global = true;
component.Source.Global = true;
DirtyField(entity, component, nameof(AudioComponent.Global));
@@ -513,22 +554,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
private (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
private (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
{
FileName = filename,
Specifier = specifier,
NetEntity = GetNetEntity(entity),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayEntity(audio, entity, audioParams) : default;
return TryGetAudio(specifier, out var audio) ? PlayEntity(audio, entity, specifier, audioParams) : default;
}
/// <summary>
@@ -537,7 +578,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="stream">The audio stream to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="audioParams"></param>
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, AudioParams? audioParams = null)
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(entity))
{
@@ -545,7 +586,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
return playing;
@@ -557,22 +598,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
private (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
{
FileName = filename,
Specifier = specifier,
Coordinates = GetNetCoordinates(coordinates),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayStatic(audio, coordinates, audioParams) : default;
return TryGetAudio(specifier, out var audio) ? PlayStatic(audio, coordinates, specifier, audioParams) : default;
}
/// <summary>
@@ -581,7 +622,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="stream">The audio stream to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams"></param>
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, AudioParams? audioParams = null)
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
@@ -589,33 +630,33 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
_xformSys.SetCoordinates(playing.Entity, coordinates);
return playing;
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
return PlayGlobal(specifier, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
{
return PlayEntity(filename, entity, audioParams);
return PlayEntity(specifier, entity, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
return PlayStatic(specifier, coordinates, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
return PlayGlobal(specifier, audioParams);
}
public override void LoadStream<T>(Entity<AudioComponent> entity, T stream)
@@ -629,39 +670,39 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, EntityUid recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
return PlayGlobal(specifier, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
return PlayEntity(specifier, uid, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
return PlayEntity(specifier, uid, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
return PlayStatic(specifier, coordinates, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
return PlayStatic(specifier, coordinates, audioParams);
}
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, AudioStream stream)
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, ResolvedSoundSpecifier? specifier, AudioStream stream)
{
var audioP = audioParams ?? AudioParams.Default;
var entity = SetupAudio(null, audioP, initialize: false, length: stream.Length);
var entity = SetupAudio(specifier, audioP, initialize: false, length: stream.Length);
LoadStream(entity, stream);
EntityManager.InitializeAndStartEntity(entity);
var comp = entity.Comp;
@@ -694,17 +735,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
private void OnEntityCoordinates(PlayAudioPositionalMessage ev)
{
PlayStatic(ev.FileName, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
PlayStatic(ev.Specifier, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
}
private void OnEntityAudio(PlayAudioEntityMessage ev)
{
PlayEntity(ev.FileName, GetEntity(ev.NetEntity), ev.AudioParams, false);
PlayEntity(ev.Specifier, GetEntity(ev.NetEntity), ev.AudioParams, false);
}
private void OnGlobalAudio(PlayAudioGlobalMessage ev)
{
PlayGlobal(ev.FileName, ev.AudioParams, false);
PlayGlobal(ev.Specifier, ev.AudioParams, false);
}
protected override TimeSpan GetAudioLengthImpl(string filename)

View File

@@ -13,6 +13,8 @@ namespace Robust.Client.Audio;
/// </summary>
internal sealed class HeadlessAudioManager : IAudioInternal
{
private int _audioBuffer;
/// <inheritdoc />
public void InitializePostWindowing()
{
@@ -65,6 +67,11 @@ internal sealed class HeadlessAudioManager : IAudioInternal
{
}
/// <inheritdoc />
public void Remove(AudioStream stream)
{
}
/// <inheritdoc />
public void StopAllAudio()
{
@@ -101,11 +108,11 @@ internal sealed class HeadlessAudioManager : IAudioInternal
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
{
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
return new AudioStream(null, length, channels, name);
return new AudioStream(this, _audioBuffer++, null, length, channels, name);
}
private static AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
private AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
{
return new AudioStream(null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
return new AudioStream(this, _audioBuffer++, null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
}
}

View File

@@ -44,6 +44,8 @@ internal interface IAudioInternal : IAudioManager
void SetAttenuation(Attenuation attenuation);
void Remove(AudioStream stream);
/// <summary>
/// Stops all audio from playing.
/// </summary>

View File

@@ -1,6 +1,5 @@
using System;
using System.IO;
using Robust.Client.Audio.Sources;
using Robust.Shared.Audio.Sources;
namespace Robust.Client.Audio;
@@ -11,7 +10,7 @@ namespace Robust.Client.Audio;
public interface IAudioManager
{
IAudioSource? CreateAudioSource(AudioStream stream);
AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
AudioStream LoadAudioWav(Stream stream, string? name = null);

View File

@@ -13,7 +13,7 @@ internal sealed class AudioSource : BaseAudioSource
/// <summary>
/// Underlying stream to the audio.
/// </summary>
private readonly AudioStream _sourceStream;
internal readonly AudioStream SourceStream;
#if DEBUG
private bool _didPositionWarning;
@@ -21,7 +21,7 @@ internal sealed class AudioSource : BaseAudioSource
public AudioSource(AudioManager master, int sourceHandle, AudioStream sourceStream) : base(master, sourceHandle)
{
_sourceStream = sourceStream;
SourceStream = sourceStream;
}
/// <inheritdoc />
@@ -47,13 +47,13 @@ internal sealed class AudioSource : BaseAudioSource
#if DEBUG
// OpenAL doesn't seem to want to play stereo positionally.
// Log a warning if people try to.
if (_sourceStream.ChannelCount > 1 && !_didPositionWarning)
if (SourceStream.ChannelCount > 1 && !_didPositionWarning)
{
_didPositionWarning = true;
Master.OpenALSawmill.Warning("Attempting to set position on audio source with multiple audio channels! Stream: '{0}'. Make sure the audio is MONO, not stereo.",
_sourceStream.Name);
SourceStream.Name);
// warning isn't enough, people just ignore it :(
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{_sourceStream.Name}'. Make sure the audio is MONO, not stereo.");
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{SourceStream.Name}'. Make sure the audio is MONO, not stereo.");
}
#endif

View File

@@ -208,6 +208,12 @@ public abstract class BaseAudioSource : IAudioSource
}
set
{
if (float.IsNaN(value))
{
Master.LogError($"Tried to set NaN gain, setting audio source to 0f: {Environment.StackTrace}");
value = 0f;
}
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)

View File

@@ -115,10 +115,6 @@ namespace Robust.Client
/// <inheritdoc />
public void DisconnectFromServer(string reason)
{
DebugTools.Assert(RunLevel > ClientRunLevel.Initialize);
DebugTools.Assert(_net.IsConnected);
// run level changed in OnNetDisconnect()
// are both of these *really* needed?
_net.ClientDisconnect(reason);
}

View File

@@ -46,6 +46,7 @@ using Robust.Shared.Replays;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Upload;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client
@@ -102,6 +103,7 @@ namespace Robust.Client
deps.Register<ProfViewManager>();
deps.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
deps.Register<NetworkResourceManager>();
deps.Register<IReloadManager, ReloadManager>();
switch (mode)
{

View File

@@ -173,29 +173,51 @@ namespace Robust.Client.Console.Commands
}
}
internal sealed class ShowPositionsCommand : LocalizedCommands
internal sealed class ShowPositionsCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showpos";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mgr = _entitySystems.GetEntitySystem<DebugDrawingSystem>();
mgr.DebugPositions = !mgr.DebugPositions;
_debugDrawing.DebugPositions = !_debugDrawing.DebugPositions;
}
}
internal sealed class ShowRotationsCommand : LocalizedCommands
internal sealed class ShowRotationsCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showrot";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mgr = _entitySystems.GetEntitySystem<DebugDrawingSystem>();
mgr.DebugRotations = !mgr.DebugRotations;
_debugDrawing.DebugRotations = !_debugDrawing.DebugRotations;
}
}
internal sealed class ShowVelocitiesCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showvel";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_debugDrawing.DebugVelocities = !_debugDrawing.DebugVelocities;
}
}
internal sealed class ShowAngularVelocitiesCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showangvel";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_debugDrawing.DebugAngularVelocities = !_debugDrawing.DebugAngularVelocities;
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.Console;
using Robust.Shared.Localization;
namespace Robust.Client.Console.Commands;
[UsedImplicitly]
internal sealed class LocalizationSetCulture : LocalizedCommands
{
private const string Name = "localization_set_culture";
private const int ArgumentCount = 1;
public override string Command => Name;
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != ArgumentCount)
{
shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error"));
return;
}
CultureInfo culture;
try
{
culture = CultureInfo.GetCultureInfo(args[0], predefinedOnly: false);
}
catch (CultureNotFoundException)
{
shell.WriteError(Loc.GetString("cmd-parse-failure-cultureinfo", ("arg", args[0])));
return;
}
LocalizationManager.SetCulture(culture);
shell.WriteLine(LocalizationManager.GetString("cmd-localization_set_culture-changed",
("code", culture.Name),
("nativeName", culture.NativeName),
("englishName", culture.EnglishName)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 => CompletionResult.FromHintOptions(GetCultureNames(),
LocalizationManager.GetString("cmd-localization_set_culture-culture-name")),
_ => CompletionResult.Empty
};
}
private static HashSet<string> GetCultureNames()
{
var cultureInfos = CultureInfo.GetCultures(CultureTypes.AllCultures)
.Where(x => !string.IsNullOrEmpty(x.Name))
.ToArray();
var allNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
allNames.UnionWith(cultureInfos.Select(x => x.TwoLetterISOLanguageName));
allNames.UnionWith(cultureInfos.Select(x => x.Name));
return allNames;
}
}

View File

@@ -5,15 +5,15 @@ using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
public sealed class VelocitiesCommand : LocalizedCommands
public sealed class ShowPlayerVelocityCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
public override string Command => "showvelocities";
public override string Command => "showplayervelocity";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_entitySystems.GetEntitySystem<VelocityDebugSystem>().Enabled ^= true;
_entitySystems.GetEntitySystem<ShowPlayerVelocityDebugSystem>().Enabled ^= true;
}
}
}

View File

@@ -1,140 +1,221 @@
using System.Numerics;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using System.Numerics;
namespace Robust.Client.Debugging
namespace Robust.Client.Debugging;
/// <summary>
/// A collection of visual debug overlays for the client game.
/// </summary>
public sealed class DebugDrawingSystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private bool _debugPositions;
private bool _debugRotations;
private bool _debugVelocities;
private bool _debugAngularVelocities;
/// <summary>
/// A collection of visual debug overlays for the client game.
/// Toggles the visual overlay of the local origin for each entity on screen.
/// </summary>
public sealed class DebugDrawingSystem : EntitySystem
public bool DebugPositions
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly TransformSystem _transform = default!;
private bool _debugPositions;
private bool _debugRotations;
/// <summary>
/// Toggles the visual overlay of the local origin for each entity on screen.
/// </summary>
public bool DebugPositions
get => _debugPositions;
set
{
get => _debugPositions;
set
if (value == DebugPositions)
{
if (value == DebugPositions)
{
return;
}
return;
}
_debugPositions = value;
_debugPositions = value;
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
{
_overlayManager.AddOverlay(new EntityPositionOverlay(_lookup, EntityManager, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
}
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
{
_overlayManager.AddOverlay(new EntityPositionOverlay(_lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
}
}
}
/// <summary>
/// Toggles the visual overlay of the local rotation.
/// </summary>
public bool DebugRotations
/// <summary>
/// Toggles the visual overlay of the rotation for each entity on screen.
/// </summary>
public bool DebugRotations
{
get => _debugRotations;
set
{
get => _debugRotations;
set
if (value == DebugRotations)
{
if (value == DebugRotations)
{
return;
}
return;
}
_debugRotations = value;
_debugRotations = value;
if (value && !_overlayManager.HasOverlay<EntityRotationOverlay>())
{
_overlayManager.AddOverlay(new EntityRotationOverlay(_lookup, EntityManager));
}
else
{
_overlayManager.RemoveOverlay<EntityRotationOverlay>();
}
if (value && !_overlayManager.HasOverlay<EntityRotationOverlay>())
{
_overlayManager.AddOverlay(new EntityRotationOverlay(_lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityRotationOverlay>();
}
}
}
private sealed class EntityPositionOverlay : Overlay
/// <summary>
/// Toggles the visual overlay of the local velocity for each entity on screen.
/// </summary>
public bool DebugVelocities
{
get => _debugVelocities;
set
{
private readonly EntityLookupSystem _lookup;
private readonly IEntityManager _entityManager;
private readonly SharedTransformSystem _transform;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityPositionOverlay(EntityLookupSystem lookup, IEntityManager entityManager, SharedTransformSystem transform)
if (value == DebugVelocities)
{
_lookup = lookup;
_entityManager = entityManager;
_transform = transform;
return;
}
protected internal override void Draw(in OverlayDrawArgs args)
_debugVelocities = value;
if (value && !_overlayManager.HasOverlay<EntityVelocityOverlay>())
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
foreach (var entity in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = _transform.GetWorldPositionRotation(entity);
var xLine = worldRotation.RotateVec(Vector2.UnitX);
var yLine = worldRotation.RotateVec(Vector2.UnitY);
worldHandle.DrawLine(center, center + xLine * stubLength, Color.Red);
worldHandle.DrawLine(center, center + yLine * stubLength, Color.Green);
}
_overlayManager.AddOverlay(new EntityVelocityOverlay(EntityManager, _lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityVelocityOverlay>();
}
}
}
private sealed class EntityRotationOverlay : Overlay
/// <summary>
/// Toggles the visual overlay of the angular velocity for each entity on screen.
/// </summary>
public bool DebugAngularVelocities
{
get => _debugAngularVelocities;
set
{
private readonly EntityLookupSystem _lookup;
private readonly IEntityManager _entityManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityRotationOverlay(EntityLookupSystem lookup, IEntityManager entityManager)
if (value == DebugAngularVelocities)
{
_lookup = lookup;
_entityManager = entityManager;
return;
}
protected internal override void Draw(in OverlayDrawArgs args)
_debugAngularVelocities = value;
if (value && !_overlayManager.HasOverlay<EntityAngularVelocityOverlay>())
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
_overlayManager.AddOverlay(new EntityAngularVelocityOverlay(EntityManager, _lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityAngularVelocityOverlay>();
}
}
}
private sealed class EntityPositionOverlay(EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
foreach (var entity in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = xformQuery.GetComponent(entity).GetWorldPositionRotation();
protected internal override void Draw(in OverlayDrawArgs args)
{
const float stubLength = 0.25f;
var drawLine = worldRotation.RotateVec(-Vector2.UnitY);
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
worldHandle.DrawLine(center, center + drawLine * stubLength, Color.Red);
}
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = _transform.GetWorldPositionRotation(uid);
var xLine = worldRotation.RotateVec(Vector2.UnitX);
var yLine = worldRotation.RotateVec(Vector2.UnitY);
worldHandle.DrawLine(center, center + xLine * stubLength, Color.Red);
worldHandle.DrawLine(center, center + yLine * stubLength, Color.Green);
}
}
}
private sealed class EntityRotationOverlay(EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
protected internal override void Draw(in OverlayDrawArgs args)
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = _transform.GetWorldPositionRotation(uid);
var drawLine = worldRotation.RotateVec(-Vector2.UnitY);
worldHandle.DrawLine(center, center + drawLine * stubLength, Color.Red);
}
}
}
private sealed class EntityVelocityOverlay(IEntityManager _entityManager, EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
protected internal override void Draw(in OverlayDrawArgs args)
{
const float multiplier = 0.2f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
if(!physicsQuery.TryGetComponent(uid, out var physicsComp))
continue;
var center = _transform.GetWorldPosition(uid);
var localVelocity = physicsComp.LinearVelocity;
if (localVelocity != Vector2.Zero)
worldHandle.DrawLine(center, center + localVelocity * multiplier, Color.Yellow);
}
}
}
private sealed class EntityAngularVelocityOverlay(IEntityManager _entityManager, EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
protected internal override void Draw(in OverlayDrawArgs args)
{
const float multiplier = (float)(0.2 / (2 * System.Math.PI));
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
if(!physicsQuery.TryGetComponent(uid, out var physicsComp))
continue;
var center = _transform.GetWorldPosition(uid);
var angularVelocity = physicsComp.AngularVelocity;
if (angularVelocity != 0.0f)
worldHandle.DrawCircle(center, angularVelocity * multiplier, angularVelocity > 0 ? Color.Magenta : Color.Blue, false);
}
}
}
}

View File

@@ -413,8 +413,9 @@ namespace Robust.Client.Debugging
}
var body = bodyEnt.Comp;
var meta = _entityManager.GetComponent<MetaDataComponent>(bodyEnt);
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner}");
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner} ({meta.EntityName})");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
row++;

View File

@@ -93,6 +93,7 @@ namespace Robust.Client
[Dependency] private readonly IReplayPlaybackManager _replayPlayback = default!;
[Dependency] private readonly IReplayRecordingManagerInternal _replayRecording = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IReloadManager _reload = default!;
private IWebViewManagerHook? _webViewHook;
@@ -185,6 +186,7 @@ namespace Robust.Client
// before prototype load.
ProgramShared.FinishCheckBadFileExtensions(checkBadExtensions);
_reload.Initialize();
_reflectionManager.Initialize();
_prototypeManager.Initialize();
_prototypeManager.LoadDefaultPrototypes();
@@ -382,7 +384,7 @@ namespace Robust.Client
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)

View File

@@ -101,11 +101,33 @@ namespace Robust.Client.GameObjects
/// <inheritdoc />
public override void Dirty<T>(Entity<T> ent, MetaDataComponent? meta = null)
{
// Client only dirties during prediction
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.Dirty(ent, meta);
}
public override void DirtyField<T>(EntityUid uid, T comp, string fieldName, MetaDataComponent? metadata = null)
{
// TODO Prediction
// does the client actually need to dirty the field?
// I.e., can't it just dirty the whole component to trigger a reset?
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.DirtyField(uid, comp, fieldName, metadata);
}
public override void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params string[] fields)
{
// TODO Prediction
// does the client actually need to dirty the field?
// I.e., can't it just dirty the whole component to trigger a reset?
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.DirtyFields(uid, comp, meta, fields);
}
/// <inheritdoc />
public override void Dirty<T1, T2>(Entity<T1, T2> ent, MetaDataComponent? meta = null)
{

View File

@@ -30,6 +30,7 @@ using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
using static Robust.Shared.Serialization.TypeSerializers.Implementations.SpriteSpecifierSerializer;
using Direction = Robust.Shared.Maths.Direction;
using Vector4 = Robust.Shared.Maths.Vector4;
using SysVec4 = System.Numerics.Vector4;
namespace Robust.Client.GameObjects
{
@@ -753,12 +754,20 @@ namespace Robust.Client.GameObjects
if (layerDatum.Shader == string.Empty)
{
layer.ShaderPrototype = null;
layer.UnShaded = false;
layer.Shader = null;
}
else if (layerDatum.Shader == SpriteSystem.UnshadedId.Id)
{
layer.ShaderPrototype = SpriteSystem.UnshadedId;
layer.UnShaded = true;
layer.Shader = null;
}
else if (prototypes.TryIndex<ShaderPrototype>(layerDatum.Shader, out var prototype))
{
layer.ShaderPrototype = layerDatum.Shader;
layer.Shader = prototype.Instance();
layer.UnShaded = false;
}
else
{
@@ -835,11 +844,28 @@ namespace Robust.Client.GameObjects
if (!TryGetLayer(layer, out var theLayer, true))
return;
if (shader == null)
{
theLayer.UnShaded = false;
theLayer.Shader = null;
theLayer.ShaderPrototype = null;
return;
}
if (prototype == SpriteSystem.UnshadedId.Id)
{
theLayer.UnShaded = true;
theLayer.ShaderPrototype = SpriteSystem.UnshadedId;
theLayer.Shader = null;
return;
}
theLayer.UnShaded = false;
theLayer.Shader = shader;
theLayer.ShaderPrototype = prototype;
}
public void LayerSetShader(object layerKey, ShaderInstance shader, string? prototype = null)
public void LayerSetShader(object layerKey, ShaderInstance? shader, string? prototype = null)
{
if (!LayerMapTryGet(layerKey, out var layer, true))
return;
@@ -1493,10 +1519,18 @@ namespace Robust.Client.GameObjects
{
[ViewVariables] private readonly SpriteComponent _parent;
[ViewVariables] public string? ShaderPrototype;
[ViewVariables] public ProtoId<ShaderPrototype>? ShaderPrototype;
[ViewVariables] public ShaderInstance? Shader;
[ViewVariables] public Texture? Texture;
/// <summary>
/// If true, then this layer is drawn without lighting applied.
/// Unshaded layers are given special treatment and don't just use the unshaded-shader to avoid having to
/// unnecessarily swap out the light texture. This helps the number of batches that need to be sent to the
/// GPU while drawing sprites.
/// </summary>
[ViewVariables] internal bool UnShaded;
private RSI? _rsi;
[ViewVariables] public RSI? RSI
{
@@ -1663,6 +1697,7 @@ namespace Robust.Client.GameObjects
if (toClone.Shader != null)
{
Shader = toClone.Shader.Mutable ? toClone.Shader.Duplicate() : toClone.Shader;
UnShaded = toClone.UnShaded;
ShaderPrototype = toClone.ShaderPrototype;
}
Texture = toClone.Texture;
@@ -2078,6 +2113,20 @@ namespace Robust.Client.GameObjects
drawingHandle.UseShader(Shader);
var layerColor = _parent.color * Color;
DebugTools.Assert(layerColor is {R: >= 0, G: >= 0, B: >= 0, A: >= 0}, "Negative colour modulation");
if (UnShaded)
{
DebugTools.AssertNull(Shader);
// Negative modulation values are used to disable light shading in the default shader.
// Specifically we set colour = -1 - colour
// This ensures that non-negative values become negative & is trivially invertible.
// Alternatively we could just clamp the colour to [0,1] and subtract a constant.
layerColor = new(new SysVec4(-1) - layerColor.RGBA);
}
var textureSize = texture.Size / (float)EyeManager.PixelsPerMeter;
var quad = Box2.FromDimensions(textureSize/-2, textureSize);

View File

@@ -76,7 +76,7 @@ namespace Robust.Client.GameObjects
foreach (var key in remie)
{
component.PlayingAnimations.Remove(key);
var completedEvent = new AnimationCompletedEvent {Uid = uid, Key = key, Finished = true};
var completedEvent = new AnimationCompletedEvent(uid, component, key, true);
EntityManager.EventBus.RaiseLocalEvent(uid, completedEvent, true);
}
@@ -187,7 +187,7 @@ namespace Robust.Client.GameObjects
return;
}
var completedEvent = new AnimationCompletedEvent {Uid = entity.Owner, Key = key, Finished = false};
var completedEvent = new AnimationCompletedEvent(entity.Owner, entity.Comp, key, false);
EntityManager.EventBus.RaiseLocalEvent(entity.Owner, completedEvent, true);
}
@@ -202,13 +202,33 @@ namespace Robust.Client.GameObjects
/// </summary>
public sealed class AnimationCompletedEvent : EntityEventArgs
{
/// <summary>
/// The entity associated with the event.
/// </summary>
public EntityUid Uid { get; init; }
/// <summary>
/// The animation player component associated with the entity this event was raised on.
/// </summary>
public AnimationPlayerComponent AnimationPlayer { get; init; }
/// <summary>
/// The key associated with the animation that was completed.
/// </summary>
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.
/// If false, it was removed prematurely via <see cref="AnimationPlayerSystem.Stop(EntityUid,AnimationPlayerComponent,string)"/> or similar overloads.
/// </summary>
public bool Finished { get; init; }
public AnimationCompletedEvent(EntityUid uid, AnimationPlayerComponent animationPlayer, string key, bool finished = true)
{
Uid = uid;
AnimationPlayer = animationPlayer;
Key = key;
Finished = finished;
}
}
}

View File

@@ -9,30 +9,14 @@ namespace Robust.Client.GameObjects;
public sealed class MapSystem : SharedMapSystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
protected override MapId GetNextMapId()
{
// Client-side map entities use negative map Ids to avoid conflict with server-side maps.
var id = new MapId(--LastMapId);
while (MapManager.MapExists(id))
while (MapExists(id) || UsedIds.Contains(id))
{
id = new MapId(--LastMapId);
}
return id;
}
public override void Initialize()
{
base.Initialize();
_overlayManager.AddOverlay(new TileEdgeOverlay(EntityManager, _resource, _tileDefinitionManager));
}
public override void Shutdown()
{
base.Shutdown();
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
}
}

View File

@@ -0,0 +1,66 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics.Components;
namespace Robust.Client.GameObjects;
public sealed class ShowPlayerVelocityDebugSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
internal bool Enabled
{
get => _label.Parent != null;
set
{
if (value)
{
_uiManager.WindowRoot.AddChild(_label);
}
else
{
_label.Orphan();
}
}
}
private Label _label = default!;
public override void Initialize()
{
base.Initialize();
_label = new Label();
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
if (!Enabled)
{
_label.Visible = false;
return;
}
var player = _playerManager.LocalEntity;
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
{
_label.Visible = false;
return;
}
var screenPos = _eyeManager.WorldToScreen(_transform.GetWorldPosition(Transform(player.Value)));
LayoutContainer.SetPosition(_label, screenPos + new Vector2(0, 50));
_label.Visible = true;
_label.Text = $"Speed: {body.LinearVelocity.Length():0.00}\nLinear: {body.LinearVelocity.X:0.00}, {body.LinearVelocity.Y:0.00}\nAngular: {body.AngularVelocity}";
}
}

View File

@@ -37,6 +37,7 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly SharedTransformSystem _xforms = default!;
public static readonly ProtoId<ShaderPrototype> UnshadedId = "unshaded";
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
/// <summary>

View File

@@ -1,10 +1,18 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.UserInterface;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
private Dictionary<EntityUid, Dictionary<Enum, Vector2>> _savedPositions = new();
private Dictionary<BoundUserInterface, Control> _registeredControls = new();
public override void Initialize()
{
base.Initialize();
@@ -17,6 +25,53 @@ public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
ProtoManager.PrototypesReloaded -= OnProtoReload;
}
/// <inheritdoc />
public override void OpenUi(Entity<UserInterfaceComponent?> entity, Enum key, bool predicted = false)
{
var player = Player.LocalEntity;
if (player == null)
return;
OpenUi(entity, key, player.Value, predicted);
}
protected override void SavePosition(BoundUserInterface bui)
{
if (!_registeredControls.Remove(bui, out var control))
return;
var keyed = _savedPositions[bui.Owner];
keyed[bui.UiKey] = control.Position;
}
/// <summary>
/// Registers a control so it will later have its position stored by <see cref="SavePosition"/> when the BUI is closed.
/// </summary>
public void RegisterControl(BoundUserInterface bui, Control control)
{
DebugTools.Assert(!_registeredControls.ContainsKey(bui));
_registeredControls[bui] = control;
_savedPositions.GetOrNew(bui.Owner);
}
public override bool TryGetPosition(Entity<UserInterfaceComponent?> entity, Enum key, out Vector2 position)
{
position = default;
if (!_savedPositions.TryGetValue(entity.Owner, out var keyed))
{
return false;
}
if (!keyed.TryGetValue(key, out position))
{
return false;
}
return true;
}
private void OnProtoReload(PrototypesReloadedEventArgs obj)
{
var player = Player.LocalEntity;

View File

@@ -1,54 +0,0 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
namespace Robust.Client.GameObjects
{
public sealed class VelocityDebugSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly TransformSystem _transform = default!;
internal bool Enabled { get; set; }
private Label _label = default!;
public override void Initialize()
{
base.Initialize();
_label = new Label();
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(_label);
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
if (!Enabled)
{
_label.Visible = false;
return;
}
var player = _playerManager.LocalEntity;
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
{
_label.Visible = false;
return;
}
var screenPos = _eyeManager.WorldToScreen(_transform.GetWorldPosition(Transform(player.Value)));
LayoutContainer.SetPosition(_label, screenPos + new Vector2(0, 50));
_label.Visible = true;
_label.Text = $"Speed: {body.LinearVelocity.Length():0.00}\nLinear: {body.LinearVelocity.X:0.00}, {body.LinearVelocity.Y:0.00}\nAngular: {body.AngularVelocity}";
}
}
}

View File

@@ -0,0 +1,8 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects;
public sealed class VisibilitySystem : SharedVisibilitySystem
{
}

View File

@@ -23,7 +23,6 @@ using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Profiling;
@@ -42,13 +41,13 @@ namespace Robust.Client.GameStates
private uint _nextInputCmdSeq = 1;
private readonly Queue<FullInputCmdMessage> _pendingInputs = new();
private readonly Queue<(uint sequence, GameTick sourceTick, EntityEventArgs msg, object sessionMsg)>
private readonly Queue<(uint sequence, GameTick sourceTick, object msg, object sessionMsg)>
_pendingSystemMessages
= new();
// Game state dictionaries that get used every tick.
private readonly Dictionary<EntityUid, (NetEntity NetEntity, MetaDataComponent Meta, bool EnteringPvs, GameTick LastApplied, EntityState? curState, EntityState? nextState)> _toApply = new();
private readonly Dictionary<NetEntity, EntityState> _toCreate = new();
private readonly Dictionary<EntityUid, StateData> _toApply = new();
private StateData[] _toApplySorted = default!;
private readonly Dictionary<ushort, (IComponent Component, IComponentState? curState, IComponentState? nextState)> _compStateWork = new();
private readonly Dictionary<EntityUid, HashSet<Type>> _pendingReapplyNetStates = new();
private readonly HashSet<NetEntity> _stateEnts = new();
@@ -56,15 +55,29 @@ namespace Robust.Client.GameStates
private readonly List<IComponent> _toRemove = new();
private readonly Dictionary<NetEntity, Dictionary<ushort, IComponentState?>> _outputData = new();
private readonly List<(EntityUid, TransformComponent)> _queuedBroadphaseUpdates = new();
private readonly HashSet<EntityUid> _sorted = new();
private readonly List<NetEntity> _created = new();
private readonly List<NetEntity> _detached = new();
private readonly record struct StateData(
EntityUid Uid,
NetEntity NetEntity,
MetaDataComponent Meta,
bool Created,
bool EnteringPvs,
GameTick LastApplied,
EntityState? CurState,
EntityState? NextState,
HashSet<Type>? PendingReapply);
private readonly ObjectPool<Dictionary<ushort, IComponentState?>> _compDataPool =
new DefaultObjectPool<Dictionary<ushort, IComponentState?>>(new DictPolicy<ushort, IComponentState?>(), 256);
private uint _metaCompNetId;
private uint _xformCompNetId;
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
[Dependency] private readonly IComponentFactory _compFactory = default!;
[Dependency] private readonly IClientEntityManagerInternal _entities = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IClientNetManager _network = default!;
[Dependency] private readonly IBaseClient _client = default!;
@@ -72,7 +85,7 @@ namespace Robust.Client.GameStates
[Dependency] private readonly INetConfigurationManager _config = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly ClientEntityManager _entityManager = default!;
[Dependency] private readonly ClientEntityManager _entities = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly ProfManager _prof = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
@@ -126,7 +139,6 @@ namespace Robust.Client.GameStates
private bool _resettingPredictedEntities;
private readonly List<EntityUid> _brokenEnts = new();
private readonly List<(EntityUid, NetEntity)> _toStart = new();
/// <inheritdoc />
public void Initialize()
@@ -172,6 +184,12 @@ namespace Robust.Client.GameStates
throw new InvalidOperationException("MetaDataComponent does not have a NetId.");
_metaCompNetId = metaId.Value;
var xformId = _compFactory.GetRegistration(typeof(TransformComponent)).NetID;
if (!xformId.HasValue)
throw new InvalidOperationException("TransformComponent does not have a NetId.");
_xformCompNetId = xformId.Value;
}
private void OnComponentAdded(AddedComponentEventArgs args)
@@ -183,11 +201,11 @@ namespace Robust.Client.GameStates
if (comp.NetID == null)
return;
if (_entityManager.IsClientSide(args.BaseArgs.Owner))
if (_entities.IsClientSide(args.BaseArgs.Owner))
return;
_sawmill.Error($"""
Added component {comp.Name} to entity {_entityManager.ToPrettyString(args.BaseArgs.Owner)} while resetting predicted entities.
Added component {comp.Name} to entity {_entities.ToPrettyString(args.BaseArgs.Owner)} while resetting predicted entities.
Stack trace:
{Environment.StackTrace}
""");
@@ -366,7 +384,6 @@ namespace Robust.Client.GameStates
_processor.UpdateFullRep(curState);
}
IEnumerable<NetEntity> createdEntities;
using (_prof.Group("ApplyGameState"))
{
if (_timing.LastProcessedTick < targetProcessedTick && nextState != null)
@@ -385,7 +402,7 @@ namespace Robust.Client.GameStates
try
{
#endif
createdEntities = ApplyGameState(curState, nextState);
ApplyGameState(curState, nextState);
#if EXCEPTION_TOLERANCE
}
catch (MissingMetadataException e)
@@ -399,7 +416,7 @@ namespace Robust.Client.GameStates
using (_prof.Group("MergeImplicitData"))
{
GenerateImplicitStates(createdEntities);
MergeImplicitData();
}
if (_lastProcessedInput < curState.LastProcessedInput)
@@ -456,7 +473,7 @@ namespace Robust.Client.GameStates
using (_prof.Group("Tick"))
{
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: !IsPredictionEnabled);
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: !IsPredictionEnabled, histogram: null);
}
}
@@ -504,9 +521,7 @@ namespace Robust.Client.GameStates
while (hasPendingMessage && pendingMessagesEnumerator.Current.sourceTick <= _timing.CurTick)
{
var msg = pendingMessagesEnumerator.Current.msg;
_entities.EventBus.RaiseEvent(EventSource.Local, msg);
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.msg);
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.sessionMsg);
hasPendingMessage = pendingMessagesEnumerator.MoveNext();
}
@@ -545,7 +560,7 @@ namespace Robust.Client.GameStates
PredictionNeedsResetting = false;
var countReset = 0;
var system = _entitySystemManager.GetEntitySystem<ClientDirtySystem>();
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
var metaQuery = _entities.GetEntityQuery<MetaDataComponent>();
RemQueue<IComponent> toRemove = new();
foreach (var entity in system.DirtyEntities)
@@ -632,7 +647,7 @@ namespace Robust.Client.GameStates
if (!last.TryGetValue(netId, out var state))
continue;
var comp = _entityManager.AddComponent(entity, netId, meta);
var comp = _entities.AddComponent(entity, netId, meta);
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was removed: {comp.GetType()}");
@@ -652,7 +667,7 @@ namespace Robust.Client.GameStates
meta.EntityLastModifiedTick = _timing.LastRealTick;
}
_entityManager.System<PhysicsSystem>().ResetContacts();
_entities.System<PhysicsSystem>().ResetContacts();
// TODO maybe reset more of physics?
// E.g., warm impulses for warm starting?
@@ -671,21 +686,22 @@ namespace Robust.Client.GameStates
/// initial server state for any newly created entity. It does this by simply using the standard <see
/// cref="IEntityManager.GetComponentState"/>.
/// </remarks>
public void GenerateImplicitStates(IEnumerable<NetEntity> createdEntities)
public void MergeImplicitData()
{
var bus = _entityManager.EventBus;
var bus = _entities.EventBus;
foreach (var netEntity in createdEntities)
foreach (var netEntity in _created)
{
#if EXCEPTION_TOLERANCE
if (!_entityManager.TryGetEntityData(netEntity, out _, out var meta))
if (!_entities.TryGetEntityData(netEntity, out _, out var meta))
{
_sawmill.Error($"Encountered deleted entity while merging implicit data! NetEntity: {netEntity}");
continue;
}
#if !EXCEPTION_TOLERANCE
throw new KeyNotFoundException();
#else
var (_, meta) = _entityManager.GetEntityData(netEntity);
continue;
#endif
}
var compData = _compDataPool.Get();
_outputData.Add(netEntity, compData);
@@ -694,12 +710,13 @@ namespace Robust.Client.GameStates
{
DebugTools.Assert(component.NetSyncEnabled);
var state = _entityManager.GetComponentState(bus, component, null, GameTick.Zero);
var state = _entities.GetComponentState(bus, component, null, GameTick.Zero);
DebugTools.Assert(state is not IComponentDeltaState);
compData.Add(netId, state);
}
}
_created.Clear();
_processor.MergeImplicitData(_outputData);
foreach (var data in _outputData.Values)
@@ -735,10 +752,9 @@ namespace Robust.Client.GameStates
_config.TickProcessMessages();
}
(IEnumerable<NetEntity> Created, List<NetEntity> Detached) output;
using (_prof.Group("Entity"))
{
output = ApplyEntityStates(curState, nextState);
ApplyEntityStates(curState, nextState);
}
using (_prof.Group("Player"))
@@ -748,13 +764,13 @@ namespace Robust.Client.GameStates
using (_prof.Group("Callback"))
{
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState, output.Detached));
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState, _detached));
}
return output.Created;
return _created;
}
private (IEnumerable<NetEntity> Created, List<NetEntity> Detached) ApplyEntityStates(GameState curState, GameState? nextState)
private void ApplyEntityStates(GameState curState, GameState? nextState)
{
var metas = _entities.GetEntityQuery<MetaDataComponent>();
var xforms = _entities.GetEntityQuery<TransformComponent>();
@@ -762,90 +778,74 @@ namespace Robust.Client.GameStates
var enteringPvs = 0;
_toApply.Clear();
_toCreate.Clear();
_created.Clear();
_pendingReapplyNetStates.Clear();
var curSpan = curState.EntityStates.Span;
// Create new entities
// This is done BEFORE state application to ensure any new parents exist before existing children have their states applied, otherwise, we may have issues with entity transforms!
{
using var _ = _prof.Group("Create uninitialized entities");
var count = 0;
using (_prof.Group("Create uninitialized entities"))
{
var created = 0;
foreach (var es in curSpan)
{
if (_entityManager.TryGetEntity(es.NetEntity, out var nUid))
if (_entities.TryGetEntity(es.NetEntity, out var nUid))
{
DebugTools.Assert(_entityManager.EntityExists(nUid));
DebugTools.Assert(_entities.EntityExists(nUid));
continue;
}
count++;
var metaState = (MetaDataComponentState?)es.ComponentChanges.Value?.FirstOrDefault(c => c.NetID == _metaCompNetId).State;
if (metaState == null)
throw new MissingMetadataException(es.NetEntity);
var uid = _entities.CreateEntity(metaState.PrototypeId, out var newMeta);
_toCreate.Add(es.NetEntity, es);
_toApply.Add(uid, (es.NetEntity, newMeta, false, GameTick.Zero, es, null));
// Client creates a client-side net entity for the newly created entity.
// We need to clear this mapping before assigning the real net id.
// TODO NetEntity Jank: prevent the client from creating this in the first place.
_entityManager.ClearNetEntity(newMeta.NetEntity);
_entityManager.SetNetEntity(uid, es.NetEntity, newMeta);
newMeta.LastStateApplied = curState.ToSequence;
// Check if there's any component states awaiting this entity.
if (_entityManager.PendingNetEntityStates.Remove(es.NetEntity, out var value))
{
foreach (var (type, owner) in value)
{
var pending = _pendingReapplyNetStates.GetOrNew(owner);
pending.Add(type);
}
}
created++;
CreateNewEntity(es, curState.ToSequence);
}
_prof.WriteValue("Count", ProfData.Int32(count));
_prof.WriteValue("Count", ProfData.Int32(created));
}
// Add entity entities that aren't new to _toCreate.
// In the process, we also check if these entities are re-entering PVS range.
foreach (var es in curSpan)
{
if (_toCreate.ContainsKey(es.NetEntity))
if (!_entities.TryGetEntityData(es.NetEntity, out var uid, out var meta))
continue;
if (!_entityManager.TryGetEntityData(es.NetEntity, out var uid, out var meta))
continue;
bool isEnteringPvs = (meta.Flags & MetaDataFlags.Detached) != 0;
var isEnteringPvs = (meta.Flags & MetaDataFlags.Detached) != 0;
if (isEnteringPvs)
{
// _toApply already contains newly created entities, but these should never be "entering PVS"
DebugTools.Assert(!_toApply.ContainsKey(uid.Value));
meta.Flags &= ~MetaDataFlags.Detached;
enteringPvs++;
}
else if (meta.LastStateApplied >= es.EntityLastModified && meta.LastStateApplied != GameTick.Zero)
{
// _toApply already contains newly created entities, but for those this set should have no effect
DebugTools.Assert(!_toApply.ContainsKey(uid.Value) || meta.LastStateApplied == curState.ToSequence);
meta.LastStateApplied = curState.ToSequence;
continue;
}
_toApply.Add(uid.Value, (es.NetEntity, meta, isEnteringPvs, meta.LastStateApplied, es, null));
// Any newly created entities already added to _toApply should've already been caught by the previous continue
DebugTools.Assert(!_toApply.ContainsKey(uid.Value));
_toApply.Add(uid.Value, new(uid.Value, es.NetEntity, meta, false, isEnteringPvs, meta.LastStateApplied, es, null, null));
meta.LastStateApplied = curState.ToSequence;
}
// Detach entities to null space
var containerSys = _entitySystemManager.GetEntitySystem<ContainerSystem>();
var lookupSys = _entitySystemManager.GetEntitySystem<EntityLookupSystem>();
var detached = ProcessPvsDeparture(curState.ToSequence, metas, xforms, xformSys, containerSys, lookupSys);
ProcessPvsDeparture(curState.ToSequence, metas, xforms, xformSys, containerSys, lookupSys);
// Check next state (AFTER having created new entities introduced in curstate)
if (nextState != null)
{
foreach (var es in nextState.EntityStates.Span)
{
if (!_entityManager.TryGetEntityData(es.NetEntity, out var uid, out var meta))
if (!_entities.TryGetEntityData(es.NetEntity, out var uid, out var meta))
continue;
// Does the next state actually have any future information about this entity that could be used for interpolation?
@@ -854,15 +854,14 @@ namespace Robust.Client.GameStates
ref var state = ref CollectionsMarshal.GetValueRefOrAddDefault(_toApply, uid.Value, out var exists);
if (exists)
state = (es.NetEntity, meta, state.EnteringPvs, state.LastApplied, state.curState, es);
else
state = (es.NetEntity, meta, false, GameTick.Zero, null, es);
state = exists
? state with {NextState = es}
: new(uid.Value, es.NetEntity, meta, false, false, GameTick.Zero, null, es, null);
}
}
// Check pending states and see if we need to force any entities to re-run component states.
foreach (var uid in _pendingReapplyNetStates.Keys)
foreach (var (uid, pending) in _pendingReapplyNetStates)
{
// Original entity referencing the NetEntity may have been deleted.
if (!metas.TryGetComponent(uid, out var meta))
@@ -879,51 +878,30 @@ namespace Robust.Client.GameStates
DebugTools.Assert(!curState.EntityDeletions.Value.Contains(meta.NetEntity));
// State already being re-applied so don't bulldoze it.
ref var state = ref CollectionsMarshal.GetValueRefOrAddDefault(_toApply, uid, out var exists);
if (exists)
continue;
state = (meta.NetEntity, meta, false, GameTick.Zero, null, null);
state = exists
? state with {PendingReapply = pending}
: new(uid, meta.NetEntity, meta, false, false, GameTick.Zero, null, null, pending);
}
_queuedBroadphaseUpdates.Clear();
using (_prof.Group("Sort States"))
{
SortStates(_toApply);
}
// Apply entity states.
using (_prof.Group("Apply States"))
{
foreach (var (entity, data) in _toApply)
var span = _toApplySorted.AsSpan(0, _toApply.Count);
foreach (ref var data in span)
{
#if EXCEPTION_TOLERANCE
try
{
#endif
HandleEntityState(entity, data.NetEntity, data.Meta, _entities.EventBus, data.curState,
data.nextState, data.LastApplied, curState.ToSequence, data.EnteringPvs);
#if EXCEPTION_TOLERANCE
}
catch (Exception e)
{
_sawmill.Error($"Caught exception while applying entity state. Entity: {_entities.ToPrettyString(entity)}. Exception: {e}");
_entityManager.DeleteEntity(entity);
RequestFullState();
continue;
}
#endif
if (!data.EnteringPvs)
continue;
// Now that things like collision data, fixtures, and positions have been updated, we queue a
// broadphase update. However, if this entity is parented to some other entity also re-entering PVS,
// we only need to update it's parent (as it recursively updates children anyways).
var xform = xforms.GetComponent(entity);
DebugTools.Assert(xform.Broadphase == BroadphaseData.Invalid);
xform.Broadphase = null;
if (!_toApply.TryGetValue(xform.ParentUid, out var parent) || !parent.EnteringPvs)
_queuedBroadphaseUpdates.Add((entity, xform));
ApplyEntState(data, curState.ToSequence);
}
Array.Clear(_toApplySorted, 0, _toApply.Count);
_prof.WriteValue("Count", ProfData.Int32(_toApply.Count));
}
@@ -958,14 +936,168 @@ namespace Robust.Client.GameStates
}
}
// Initialize and start the newly created entities.
if (_toCreate.Count > 0)
InitializeAndStart(_toCreate, metas, xforms);
// Delete any entities that failed to properly initialize/start
foreach (var entity in _brokenEnts)
{
_entities.DeleteEntity(entity);
}
_brokenEnts.Clear();
_prof.WriteValue("State Size", ProfData.Int32(curSpan.Length));
_prof.WriteValue("Entered PVS", ProfData.Int32(enteringPvs));
}
return (_toCreate.Keys, detached);
private void ApplyEntState(in StateData data, GameTick toTick)
{
try
{
HandleEntityState(data, _entities.EventBus, toTick);
}
catch (Exception e)
{
_sawmill.Error($"Caught exception while applying entity state. Entity: {_entities.ToPrettyString(data.Uid)}. Exception: {e}");
_brokenEnts.Add(data.Uid);
RequestFullState();
#if !EXCEPTION_TOLERANCE
throw;
#else
return;
#endif
}
if (data.Created)
{
try
{
_entities.InitializeEntity(data.Uid, data.Meta);
_entities.StartEntity(data.Uid);
}
catch (Exception e)
{
_sawmill.Error(
$"Caught exception while initializing or starting entity: {_entities.ToPrettyString(data.Uid)}. Exception: {e}");
_brokenEnts.Add(data.Uid);
RequestFullState();
#if !EXCEPTION_TOLERANCE
throw;
#else
return;
#endif
}
}
if (!data.EnteringPvs)
return;
// Now that things like collision data, fixtures, and positions have been updated, we queue a
// broadphase update. However, if this entity is parented to some other entity also re-entering PVS,
// we only need to update it's parent (as it recursively updates children anyways).
var xform = _entities.TransformQuery.Comp(data.Uid);
DebugTools.Assert(xform.Broadphase == BroadphaseData.Invalid);
xform.Broadphase = null;
if (!_toApply.TryGetValue(xform.ParentUid, out var parent) || !parent.EnteringPvs)
_queuedBroadphaseUpdates.Add((data.Uid, xform));
}
private void CreateNewEntity(EntityState state, GameTick toTick)
{
// TODO GAME STATE
// store MetaData & Transform information separately.
var metaState =
(MetaDataComponentState?) state.ComponentChanges.Value?.FirstOrDefault(c => c.NetID == _metaCompNetId)
.State;
if (metaState == null)
throw new MissingMetadataException(state.NetEntity);
var uid = _entities.CreateEntity(metaState.PrototypeId, out var newMeta);
_toApply.Add(uid, new(uid, state.NetEntity, newMeta, true, false, GameTick.Zero, state, null, null));
_created.Add(state.NetEntity);
// Client creates a client-side net entity for the newly created entity.
// We need to clear this mapping before assigning the real net id.
// TODO NetEntity Jank: prevent the client from creating this in the first place.
_entities.ClearNetEntity(newMeta.NetEntity);
_entities.SetNetEntity(uid, state.NetEntity, newMeta);
newMeta.LastStateApplied = toTick;
// Check if there's any component states awaiting this entity.
if (!_entities.PendingNetEntityStates.Remove(state.NetEntity, out var value))
return;
foreach (var (type, owner) in value)
{
var pending = _pendingReapplyNetStates.GetOrNew(owner);
pending.Add(type);
}
}
/// <summary>
/// Sort states to ensure that we always apply states, initialize, and start parent entities before any of their
/// children.
/// </summary>
private void SortStates(Dictionary<EntityUid, StateData> toApply)
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (_toApplySorted == null || _toApplySorted.Length < toApply.Count)
Array.Resize(ref _toApplySorted, toApply.Count);
_sorted.Clear();
var i = 0;
foreach (var (ent, data) in toApply)
{
AddToSorted(ent, data, ref i);
}
DebugTools.AssertEqual(i, toApply.Count);
}
private void AddToSorted(EntityUid ent, in StateData data, ref int i)
{
if (!_sorted.Add(ent))
return;
EnsureParentsSorted(ent, data, ref i);
_toApplySorted[i++] = data;
}
private void EnsureParentsSorted(EntityUid ent, in StateData data, ref int i)
{
var parent = GetStateParent(ent, data);
while (parent != EntityUid.Invalid)
{
if (_toApply.TryGetValue(parent, out var parentData))
{
AddToSorted(parent, parentData, ref i);
// The above method will handle the rest of the transform hierarchy, so we can just return early.
return;
}
parent = _entities.TransformQuery.GetComponent(parent).ParentUid;
}
}
/// <summary>
/// Get the entity's parent in the game state that is being applies. I.e., if the state contains a new
/// transform state, get the parent from that. Otherwise, return the entity's current parent.
/// </summary>
private EntityUid GetStateParent(EntityUid uid, in StateData data)
{
// TODO GAME STATE
// store MetaData & Transform information separately.
if (data.CurState != null
&& data.CurState.ComponentChanges.Value
.TryFirstOrNull(c => c.NetID == _xformCompNetId, out var found))
{
var state = (TransformComponentState) found.Value.State!;
return _entities.GetEntity(state.ParentID);
}
return _entities.TransformQuery.GetComponent(uid).ParentUid;
}
/// <inheritdoc />
@@ -1000,7 +1132,7 @@ namespace Robust.Client.GameStates
_toDelete.Clear();
// Client side entities won't need the transform, but that should always be a tiny minority of entities
var metaQuery = _entityManager.AllEntityQueryEnumerator<MetaDataComponent, TransformComponent>();
var metaQuery = _entities.AllEntityQueryEnumerator<MetaDataComponent, TransformComponent>();
while (metaQuery.MoveNext(out var ent, out var metadata, out var xform))
{
@@ -1067,9 +1199,9 @@ namespace Robust.Client.GameStates
foreach (var netEntity in delSpan)
{
// Don't worry about this for later.
_entityManager.PendingNetEntityStates.Remove(netEntity);
_entities.PendingNetEntityStates.Remove(netEntity);
if (!_entityManager.TryGetEntity(netEntity, out var id))
if (!_entities.TryGetEntity(netEntity, out var id))
continue;
if (!xforms.TryGetComponent(id, out var xform))
@@ -1099,9 +1231,10 @@ namespace Robust.Client.GameStates
var containerSys = _entitySystemManager.GetEntitySystem<ContainerSystem>();
var lookupSys = _entitySystemManager.GetEntitySystem<EntityLookupSystem>();
Detach(GameTick.MaxValue, null, entities, metas, xforms, xformSys, containerSys, lookupSys);
_detached.Clear();
}
private List<NetEntity> ProcessPvsDeparture(
private void ProcessPvsDeparture(
GameTick toTick,
EntityQuery<MetaDataComponent> metas,
EntityQuery<TransformComponent> xforms,
@@ -1110,25 +1243,23 @@ namespace Robust.Client.GameStates
EntityLookupSystem lookupSys)
{
var toDetach = _processor.GetEntitiesToDetach(toTick, _pvsDetachBudget);
var detached = new List<NetEntity>();
if (toDetach.Count == 0)
return detached;
return;
// TODO optimize
// If an entity is leaving PVS, so are all of its children. If we can preserve the hierarchy we can avoid
// things like container insertion and ejection.
using var _ = _prof.Group("Leave PVS");
detached.EnsureCapacity(toDetach.Count);
_detached.Clear();
foreach (var (tick, ents) in toDetach)
{
Detach(tick, toTick, ents, metas, xforms, xformSys, containerSys, lookupSys, detached);
Detach(tick, toTick, ents, metas, xforms, xformSys, containerSys, lookupSys);
}
_prof.WriteValue("Count", ProfData.Int32(detached.Count));
return detached;
_prof.WriteValue("Count", ProfData.Int32(_detached.Count));
}
private void Detach(GameTick maxTick,
@@ -1138,12 +1269,11 @@ namespace Robust.Client.GameStates
EntityQuery<TransformComponent> xforms,
SharedTransformSystem xformSys,
ContainerSystem containerSys,
EntityLookupSystem lookupSys,
List<NetEntity>? detached = null)
EntityLookupSystem lookupSys)
{
foreach (var netEntity in entities)
{
if (!_entityManager.TryGetEntityData(netEntity, out var ent, out var meta))
if (!_entities.TryGetEntityData(netEntity, out var ent, out var meta))
continue;
if (meta.LastStateApplied > maxTick)
@@ -1184,159 +1314,75 @@ namespace Robust.Client.GameStates
containerSys.AddExpectedEntity(netEntity, container);
}
detached?.Add(netEntity);
_detached.Add(netEntity);
}
}
private void InitializeAndStart(
Dictionary<NetEntity, EntityState> toCreate,
EntityQuery<MetaDataComponent> metas,
EntityQuery<TransformComponent> xforms)
{
_toStart.Clear();
using (_prof.Group("Initialize Entity"))
{
EntityUid entity = default;
foreach (var netEntity in toCreate.Keys)
{
(entity, var meta) = _entityManager.GetEntityData(netEntity);
InitializeRecursive(entity, meta, metas, xforms);
}
}
using (_prof.Group("Start Entity"))
{
foreach (var (entity, netEntity) in _toStart)
{
try
{
_entities.StartEntity(entity);
}
catch (Exception e)
{
_sawmill.Error($"Server entity threw in Start: nent={netEntity}, ent={_entityManager.ToPrettyString(entity)}");
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
_toCreate.Remove(netEntity);
_brokenEnts.Add(entity);
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
}
}
foreach (var entity in _brokenEnts)
{
_entityManager.DeleteEntity(entity);
}
_brokenEnts.Clear();
}
private void InitializeRecursive(
EntityUid entity,
MetaDataComponent meta,
EntityQuery<MetaDataComponent> metas,
EntityQuery<TransformComponent> xforms)
{
var xform = xforms.GetComponent(entity);
if (xform.ParentUid is {Valid: true} parent)
{
var parentMeta = metas.GetComponent(parent);
if (parentMeta.EntityLifeStage < EntityLifeStage.Initialized)
InitializeRecursive(parent, parentMeta, metas, xforms);
}
if (meta.EntityLifeStage >= EntityLifeStage.Initialized)
{
// Was probably already initialized because one of its children appeared earlier in the list.
DebugTools.AssertEqual(_toStart.Count(x => x.Item1 == entity), 1);
return;
}
try
{
_entities.InitializeEntity(entity, meta);
_toStart.Add((entity, meta.NetEntity));
}
catch (Exception e)
{
_sawmill.Error($"Server entity threw in Init: nent={meta.NetEntity}, ent={_entities.ToPrettyString(entity)}");
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
_toCreate.Remove(meta.NetEntity);
_brokenEnts.Add(entity);
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
}
private void HandleEntityState(EntityUid uid, NetEntity netEntity, MetaDataComponent meta, IEventBus bus, EntityState? curState,
EntityState? nextState, GameTick lastApplied, GameTick toTick, bool enteringPvs)
private void HandleEntityState(in StateData data, IEventBus bus, GameTick toTick)
{
_compStateWork.Clear();
// First remove any deleted components
if (curState?.NetComponents != null)
if (data.CurState?.NetComponents is {} netComps)
{
_toRemove.Clear();
foreach (var (id, comp) in meta.NetComponents)
foreach (var (id, comp) in data.Meta.NetComponents)
{
DebugTools.Assert(comp.NetSyncEnabled);
if (!curState.NetComponents.Contains(id))
if (!netComps.Contains(id))
_toRemove.Add(comp);
}
foreach (var comp in _toRemove)
{
_entities.RemoveComponent(uid, comp, meta);
_entities.RemoveComponent(data.Uid, comp, data.Meta);
}
}
if (enteringPvs)
if (data.EnteringPvs)
{
// last-server state has already been updated with new information from curState
// --> simply reset to the most recent server state.
//
// as to why we need to reset: because in the process of detaching to null-space, we will have dirtied
// the entity. most notably, all entities will have been ejected from their containers.
foreach (var (id, state) in _processor.GetLastServerStates(netEntity))
foreach (var (id, state) in _processor.GetLastServerStates(data.NetEntity))
{
if (!meta.NetComponents.TryGetValue(id, out var comp))
if (!data.Meta.NetComponents.TryGetValue(id, out var comp))
{
comp = _compFactory.GetComponent(id);
_entityManager.AddComponent(uid, comp, true, metadata: meta);
_entities.AddComponent(data.Uid, comp, true, metadata: data.Meta);
}
_compStateWork[id] = (comp, state, null);
}
}
else if (curState != null)
else if (data.CurState != null)
{
foreach (var compChange in curState.ComponentChanges.Span)
foreach (var compChange in data.CurState.ComponentChanges.Span)
{
if (!meta.NetComponents.TryGetValue(compChange.NetID, out var comp))
if (!data.Meta.NetComponents.TryGetValue(compChange.NetID, out var comp))
{
comp = _compFactory.GetComponent(compChange.NetID);
_entityManager.AddComponent(uid, comp, true, metadata:meta);
_entities.AddComponent(data.Uid, comp, true, metadata: data.Meta);
}
else if (compChange.LastModifiedTick <= lastApplied && lastApplied != GameTick.Zero)
else if (compChange.LastModifiedTick <= data.LastApplied && data.LastApplied != GameTick.Zero)
continue;
_compStateWork[compChange.NetID] = (comp, compChange.State, null);
}
}
if (nextState != null)
if (data.NextState != null)
{
foreach (var compState in nextState.ComponentChanges.Span)
foreach (var compState in data.NextState.ComponentChanges.Span)
{
if (compState.LastModifiedTick != toTick + 1)
continue;
if (!meta.NetComponents.TryGetValue(compState.NetID, out var comp))
if (!data.Meta.NetComponents.TryGetValue(compState.NetID, out var comp))
{
// The component can be null here due to interp, because the NEXT state will have a new
// component, but the component does not yet exist.
@@ -1354,9 +1400,10 @@ namespace Robust.Client.GameStates
}
// If we have a NetEntity we reference come in then apply their state.
if (_pendingReapplyNetStates.TryGetValue(uid, out var reapplyTypes))
DebugTools.Assert(_pendingReapplyNetStates.ContainsKey(data.Uid) == (data.PendingReapply != null));
if (data.PendingReapply is {} reapplyTypes)
{
var lastState = _processor.GetLastServerStates(netEntity);
var lastState = _processor.GetLastServerStates(data.NetEntity);
foreach (var type in reapplyTypes)
{
@@ -1366,7 +1413,7 @@ namespace Robust.Client.GameStates
if (netId == null)
continue;
if (!meta.NetComponents.TryGetValue(netId.Value, out var comp) ||
if (!data.Meta.NetComponents.TryGetValue(netId.Value, out var comp) ||
!lastState.TryGetValue(netId.Value, out var lastCompState))
{
continue;
@@ -1388,7 +1435,7 @@ namespace Robust.Client.GameStates
continue;
var handleState = new ComponentHandleState(cur, next);
bus.RaiseComponentEvent(uid, comp, ref handleState);
bus.RaiseComponentEvent(data.Uid, comp, ref handleState);
}
}
@@ -1510,7 +1557,7 @@ namespace Robust.Client.GameStates
{
using var _ = _timing.StartStateApplicationArea();
var query = _entityManager.AllEntityQueryEnumerator<MetaDataComponent>();
var query = _entities.AllEntityQueryEnumerator<MetaDataComponent>();
while (query.MoveNext(out var uid, out var meta))
{
@@ -1536,14 +1583,14 @@ namespace Robust.Client.GameStates
if (!meta.NetComponents.TryGetValue(id, out var comp))
{
comp = _compFactory.GetComponent(id);
_entityManager.AddComponent(uid, comp, true, meta);
_entities.AddComponent(uid, comp, true, meta);
}
if (state == null)
continue;
var handleState = new ComponentHandleState(state, null);
_entityManager.EventBus.RaiseComponentEvent(uid, comp, ref handleState);
_entities.EventBus.RaiseComponentEvent(uid, comp, ref handleState);
}
// ensure we don't have any extra components

View File

@@ -82,11 +82,7 @@ namespace Robust.Client.GameStates
/// </summary>
IEnumerable<NetEntity> ApplyGameState(GameState curState, GameState? nextState);
/// <summary>
/// Generates implicit component states for newly created entities.
/// This should always be called after running <see cref="ApplyGameState(GameState, GameState)"/>.
/// </summary>
void GenerateImplicitStates(IEnumerable<NetEntity> states);
void MergeImplicitData();
/// <summary>
/// Resets any entities that have changed while predicting future ticks.

View File

@@ -0,0 +1,5 @@
using Robust.Shared.GameStates;
namespace Robust.Client.GameStates;
public sealed partial class PvsOverrideSystem : SharedPvsOverrideSystem;

View File

@@ -21,7 +21,7 @@ namespace Robust.Client.Graphics.Clyde
private const string UniModelMatrix = "modelMatrix";
private const string UniTexturePixelSize = "TEXTURE_PIXEL_SIZE";
private const string UniMainTexture = "TEXTURE";
private const string UniLightTexture = "lightMap";
private const string UniLightTexture = "lightMap"; // TODO CLYDE consistent shader variable naming
private const string UniProjViewMatrices = "projectionViewMatrices";
private const string UniUniformConstants = "uniformConstants";

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
@@ -22,6 +23,9 @@ namespace Robust.Client.Graphics.Clyde
/// </summary>
private HashSet<Type> _erroredGridOverlays = new();
private Vertex2D[]? _chunkMeshBuilderVertexBuffer;
private ushort[]? _chunkMeshBuilderIndexBuffer;
private int _verticesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * 4;
private int _indicesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * GetQuadBatchIndexCount();
@@ -63,29 +67,78 @@ namespace Robust.Client.Graphics.Clyde
gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1));
}
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid);
gridProgram.SetUniform(UniIModelMatrix, _transformSystem.GetWorldMatrix(transform));
gridProgram.SetUniform(UniIModelMatrix, _transformSystem.GetWorldMatrix(mapGrid));
var enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
// Handle base texture updates.
while (enumerator.MoveNext(out var chunk))
{
DebugTools.Assert(chunk.FilledTiles > 0);
if (!data.TryGetValue(chunk.Indices, out MapChunkData? datum))
data[chunk.Indices] = datum = _initChunkBuffers(mapGrid, chunk);
var datum = EnsureChunkInitialized(data, chunk, mapGrid);
if (datum.Dirty)
_updateChunkMesh(mapGrid, chunk, datum);
DebugTools.Assert(datum.TileCount > 0);
if (datum.TileCount == 0)
if (!datum.Dirty)
continue;
BindVertexArray(datum.VAO);
CheckGlError();
_updateChunkMesh(mapGrid, chunk, datum);
// Dirty edge tiles for next step.
datum.EdgeDirty = true;
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
var neighbor = chunk.Indices + new Vector2i(x, y);
if (!mapGrid.Comp.Chunks.TryGetValue(neighbor, out var neighborChunk))
continue;
var neighborDatum = EnsureChunkInitialized(data, neighborChunk, mapGrid);
neighborDatum.EdgeDirty = true;
}
}
}
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
// Handle edge sprites.
while (enumerator.MoveNext(out var chunk))
{
var datum = data[chunk.Indices];
if (!datum.EdgeDirty)
continue;
_updateChunkEdges(mapGrid, chunk, datum);
}
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
// Draw chunks
while (enumerator.MoveNext(out var chunk))
{
var datum = data[chunk.Indices];
DebugTools.Assert(datum.TileCount > 0);
if (datum.TileCount > 0)
{
BindVertexArray(datum.VAO);
CheckGlError();
_debugStats.LastGLDrawCalls += 1;
GL.DrawElements(GetQuadGLPrimitiveType(), datum.TileCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
CheckGlError();
}
if (datum.EdgeCount > 0)
{
BindVertexArray(datum.EdgeVAO);
CheckGlError();
_debugStats.LastGLDrawCalls += 1;
GL.DrawElements(GetQuadGLPrimitiveType(), datum.EdgeCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
CheckGlError();
}
_debugStats.LastGLDrawCalls += 1;
GL.DrawElements(GetQuadGLPrimitiveType(), datum.TileCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
CheckGlError();
}
requiresFlush = false;
@@ -117,6 +170,17 @@ namespace Robust.Client.Graphics.Clyde
CullEmptyChunks();
}
private MapChunkData EnsureChunkInitialized(Dictionary<Vector2i, MapChunkData> data, MapChunk chunk, Entity<MapGridComponent> mapGrid)
{
if (!data.TryGetValue(chunk.Indices, out var datum))
{
data[chunk.Indices] = datum = new MapChunkData();
_initChunkBuffers(mapGrid, chunk, datum);
}
return datum;
}
private void CullEmptyChunks()
{
foreach (var (grid, chunks) in _mapChunkData)
@@ -138,66 +202,141 @@ namespace Robust.Client.Graphics.Clyde
private void _updateChunkMesh(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
Span<ushort> indexBuffer = stackalloc ushort[_indicesPerChunk(chunk)];
Span<Vertex2D> vertexBuffer = stackalloc Vertex2D[_verticesPerChunk(chunk)];
Span<ushort> indexBuffer = EnsureSize(ref _chunkMeshBuilderIndexBuffer, _indicesPerChunk(chunk));
Span<Vertex2D> vertexBuffer = EnsureSize(ref _chunkMeshBuilderVertexBuffer, _verticesPerChunk(chunk));
var i = 0;
var cSz = grid.Comp.ChunkSize;
var cScaled = chunk.Indices * cSz;
for (ushort x = 0; x < cSz; x++)
var chunkSize = grid.Comp.ChunkSize;
var chunkOriginScaled = chunk.Indices * chunkSize;
for (ushort x = 0; x < chunkSize; x++)
{
for (ushort y = 0; y < cSz; y++)
for (ushort y = 0; y < chunkSize; y++)
{
var gridX = x + chunkOriginScaled.X;
var gridY = y + chunkOriginScaled.Y;
var tile = chunk.GetTile(x, y);
if (tile.IsEmpty)
continue;
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile);
Box2 region;
if (regionMaybe == null || regionMaybe.Length <= tile.Variant)
// Tile render
if (x != chunkSize && y != chunkSize)
{
region = _tileDefinitionManager.ErrorTileRegion;
}
else
{
region = regionMaybe[tile.Variant];
}
// ReSharper disable once IntVariableOverflowInUncheckedContext
if (tile.IsEmpty)
continue;
var gx = x + cScaled.X;
var gy = y + cScaled.Y;
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile);
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(gx, gy, region.Left, region.Bottom, Color.White);
vertexBuffer[vIdx + 1] = new Vertex2D(gx + 1, gy, region.Right, region.Bottom, Color.White);
vertexBuffer[vIdx + 2] = new Vertex2D(gx + 1, gy + 1, region.Right, region.Top, Color.White);
vertexBuffer[vIdx + 3] = new Vertex2D(gx, gy + 1, region.Left, region.Top, Color.White);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort)(i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
i += 1;
Box2 region;
if (regionMaybe == null || regionMaybe.Length <= tile.Variant)
{
region = _tileDefinitionManager.ErrorTileRegion;
}
else
{
region = regionMaybe[tile.Variant];
}
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
i += 1;
}
}
}
var indexSlice = indexBuffer[..(i * GetQuadBatchIndexCount())];
var vertSlice = vertexBuffer[..(i * 4)];
GL.BindVertexArray(datum.VAO);
CheckGlError();
datum.EBO.Use();
datum.VBO.Use();
datum.EBO.Reallocate(indexBuffer[..(i * GetQuadBatchIndexCount())]);
datum.VBO.Reallocate(vertexBuffer[..(i * 4)]);
datum.Dirty = false;
datum.EBO.Reallocate(indexSlice);
datum.VBO.Reallocate(vertSlice);
datum.TileCount = i;
datum.Dirty = false;
}
private unsafe MapChunkData _initChunkBuffers(Entity<MapGridComponent> grid, MapChunk chunk)
private void _updateChunkEdges(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
// Need a buffer that can potentially store all neighbor tiles
Span<ushort> indexBuffer = EnsureSize(ref _chunkMeshBuilderIndexBuffer, _indicesPerChunk(chunk) * 8);
Span<Vertex2D> vertexBuffer = EnsureSize(ref _chunkMeshBuilderVertexBuffer, _verticesPerChunk(chunk) * 8);
var i = 0;
var chunkSize = grid.Comp.ChunkSize;
var chunkOriginScaled = chunk.Indices * chunkSize;
var maps = _entityManager.System<SharedMapSystem>();
for (ushort x = 0; x < chunkSize; x++)
{
for (ushort y = 0; y < chunkSize; y++)
{
var gridX = x + chunkOriginScaled.X;
var gridY = y + chunkOriginScaled.Y;
var tile = chunk.GetTile(x, y);
var tileDef = _tileDefinitionManager[tile.TypeId];
// Edge render
for (var nx = -1; nx <= 1; nx++)
{
for (var ny = -1; ny <= 1; ny++)
{
if (nx == 0 && ny == 0)
continue;
var neighborIndices = new Vector2i(gridX + nx, gridY + ny);
if (!maps.TryGetTile(grid.Comp, neighborIndices, out var neighborTile))
continue;
var neighborDef = _tileDefinitionManager[neighborTile.TypeId];
// If it's the same tile then no edge to be drawn.
if (tile.TypeId == neighborTile.TypeId || neighborDef.EdgeSprites.Count == 0)
continue;
// If neighbor is a lower priority then us then don't draw on our tile.
if (neighborDef.EdgeSpritePriority < tileDef.EdgeSpritePriority)
continue;
var direction = new Vector2i(nx, ny).AsDirection().GetOpposite();
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(neighborTile.TypeId, direction);
if (regionMaybe == null)
continue;
var region = regionMaybe[0];
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
i += 1;
}
}
}
}
// We don't save the edge buffers back because we might need to re-use it if a neighbor chunk updates.
var indexSlice = indexBuffer[..(i * GetQuadBatchIndexCount())];
var vertSlice = vertexBuffer[..(i * 4)];
GL.BindVertexArray(datum.EdgeVAO);
CheckGlError();
datum.EdgeEBO.Use();
datum.EdgeVBO.Use();
datum.EdgeEBO.Reallocate(indexSlice);
datum.EdgeVBO.Reallocate(vertSlice);
datum.EdgeCount = i;
datum.EdgeDirty = false;
}
private unsafe void _initChunkBuffers(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
var vboSize = _verticesPerChunk(chunk) * sizeof(Vertex2D);
var eboSize = _indicesPerChunk(chunk) * sizeof(ushort);
// Base VAO
var vao = GenVertexArray();
BindVertexArray(vao);
CheckGlError();
var vboSize = _verticesPerChunk(chunk) * sizeof(Vertex2D);
var eboSize = _indicesPerChunk(chunk) * sizeof(ushort);
var vbo = new GLBuffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw,
vboSize, $"Grid {grid.Owner} chunk {chunk.Indices} VBO");
var ebo = new GLBuffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw,
@@ -212,12 +351,30 @@ namespace Robust.Client.Graphics.Clyde
vbo.Use();
ebo.Use();
var datum = new MapChunkData(vao, vbo, ebo)
{
Dirty = true
};
datum.EBO = ebo;
datum.VBO = vbo;
datum.VAO = vao;
return datum;
// EdgeVAO
var edgeVao = GenVertexArray();
BindVertexArray(edgeVao);
CheckGlError();
var edgeVbo = new GLBuffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw,
vboSize * 8, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeVBO");
var edgeEbo = new GLBuffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw,
eboSize * 8, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeEBO");
ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, vao, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeVAO");
SetupVAOLayout();
CheckGlError();
edgeVbo.Use();
edgeEbo.Use();
datum.EdgeEBO = edgeEbo;
datum.EdgeVBO = edgeVbo;
datum.EdgeVAO = edgeVao;
}
private void DeleteChunk(MapChunkData data)
@@ -254,19 +411,49 @@ namespace Robust.Client.Graphics.Clyde
_mapChunkData.Remove(gridId);
}
private static T[] EnsureSize<T>(ref T[]? field, int size)
{
if (field == null || field.Length < size)
field = new T[size];
return field;
}
private void WriteTileToBuffers(
int i,
int gridX,
int gridY,
Span<Vertex2D> vertexBuffer,
Span<ushort> indexBuffer,
Box2 region)
{
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(gridX, gridY, region.Left, region.Bottom, Color.White);
vertexBuffer[vIdx + 1] = new Vertex2D(gridX + 1, gridY, region.Right, region.Bottom, Color.White);
vertexBuffer[vIdx + 2] = new Vertex2D(gridX + 1, gridY + 1, region.Right, region.Top, Color.White);
vertexBuffer[vIdx + 3] = new Vertex2D(gridX, gridY + 1, region.Left, region.Top, Color.White);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort)(i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
}
private sealed class MapChunkData
{
public bool Dirty;
public readonly uint VAO;
public readonly GLBuffer VBO;
public readonly GLBuffer EBO;
public bool EdgeDirty = true;
public bool Dirty = true;
public uint VAO;
public GLBuffer VBO = default!;
public GLBuffer EBO = default!;
public int TileCount;
public MapChunkData(uint vao, GLBuffer vbo, GLBuffer ebo)
public uint EdgeVAO;
public GLBuffer EdgeVBO = default!;
public GLBuffer EdgeEBO = default!;
public int EdgeCount;
public MapChunkData()
{
VAO = vao;
VBO = vbo;
EBO = ebo;
}
}
}

View File

@@ -125,7 +125,8 @@ namespace Robust.Client.Graphics.Clyde
{
DebugTools.Assert(space != OverlaySpace.ScreenSpaceBelowWorld && space != OverlaySpace.ScreenSpace);
var args = new OverlayDrawArgs(space, null, vp, _renderHandle.DrawingHandleWorld, new UIBox2i((0, 0), vp.Size), vp.Eye!.Position.MapId, worldBox, worldBounds);
var mapId = vp.Eye!.Position.MapId;
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), _mapSystem.GetMapOrInvalid(mapId), mapId, worldBox, worldBounds);
if (!overlay.BeforeDraw(args))
return;
@@ -165,7 +166,7 @@ namespace Robust.Client.Graphics.Clyde
private void RenderOverlaysDirect(
Viewport vp,
IViewportControl vpControl,
DrawingHandleBase handle,
IRenderHandle handle,
OverlaySpace space,
in UIBox2i bounds)
{
@@ -175,8 +176,9 @@ namespace Robust.Client.Graphics.Clyde
var worldBounds = CalcWorldBounds(vp);
var worldAABB = worldBounds.CalcBoundingBox();
var mapId = vp.Eye!.Position.MapId;
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, vp.Eye!.Position.MapId, worldAABB, worldBounds);
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, _mapSystem.GetMapOrInvalid(mapId), mapId, worldAABB, worldBounds);
foreach (var overlay in list)
{
@@ -421,12 +423,19 @@ namespace Robust.Client.Graphics.Clyde
var oldTransform = _currentMatrixModel;
var oldScissor = _currentScissorState;
var oldMatrixProj = _currentMatrixProj;
var oldMatrixView = _currentMatrixView;
var oldBoundTarget = _currentBoundRenderTarget;
var oldRenderTarget = _currentRenderTarget;
var oldShader = _queuedShaderInstance;
var oldCaps = _glCaps;
// Need to get state before flushing render queue in case they modify the original state.
var state = PushRenderStateFull();
// Have to flush the render queue so that all commands finish rendering to the previous framebuffer.
FlushRenderQueue();
var state = PushRenderStateFull();
{
BindRenderTargetFull(RtToLoaded(rt));
if (clearColor is not null)
@@ -448,8 +457,16 @@ namespace Robust.Client.Graphics.Clyde
PopRenderStateFull(state);
_updateUniformConstants(_currentRenderTarget.Size);
SetScissorFull(oldScissor);
_currentMatrixModel = oldTransform;
DebugTools.Assert(oldCaps.Equals(_glCaps));
DebugTools.Assert(_currentMatrixModel.Equals(oldTransform));
DebugTools.Assert(_currentScissorState.Equals(oldScissor));
DebugTools.Assert(_currentMatrixProj.Equals(oldMatrixProj));
DebugTools.Assert(oldMatrixView.Equals(_currentMatrixView));
DebugTools.Assert(oldRenderTarget.Equals(_currentRenderTarget));
DebugTools.Assert(oldBoundTarget.Equals(_currentBoundRenderTarget));
DebugTools.Assert(oldShader.Equals(_queuedShaderInstance));
}
private void RenderViewport(Viewport viewport)

View File

@@ -1,21 +1,19 @@
using System;
using System.Collections.Generic;
using System.Buffers;
using System.Diagnostics.Contracts;
using System.Numerics;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
using Robust.Shared.Physics;
using Robust.Client.ComponentTrees;
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using static Robust.Shared.GameObjects.OccluderComponent;
using Robust.Shared.Utility;
@@ -100,9 +98,11 @@ namespace Robust.Client.Graphics.Clyde
private LightCapacityComparer _lightCap = new();
private ShadowCapacityComparer _shadowCap = new ShadowCapacityComparer();
private float _maxLightRadius;
private unsafe void InitLighting()
{
_cfg.OnValueChanged(CVars.MaxLightRadius, val => { _maxLightRadius = val;}, true);
// Other...
LoadLightingShaders();
@@ -279,8 +279,7 @@ namespace Robust.Client.Graphics.Clyde
{
const float arbitraryDistanceMax = 1234;
GL.Disable(EnableCap.Blend);
CheckGlError();
IsBlending = false;
GL.Enable(EnableCap.DepthTest);
CheckGlError();
@@ -329,8 +328,7 @@ namespace Robust.Client.Graphics.Clyde
GL.Disable(EnableCap.DepthTest);
CheckGlError();
GL.Enable(EnableCap.Blend);
CheckGlError();
IsBlending = true;
}
private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 worldAABB, IEye eye)
@@ -394,21 +392,43 @@ namespace Robust.Client.Graphics.Clyde
FinalizeDepthDraw();
}
GL.Enable(EnableCap.StencilTest);
_isStencilling = true;
IsStencilling = true;
var (lightW, lightH) = GetLightMapSize(viewport.Size);
GL.Viewport(0, 0, lightW, lightH);
CheckGlError();
BindRenderTargetImmediate(RtToLoaded(viewport.LightRenderTarget));
DebugTools.Assert(_currentBoundRenderTarget.TextureHandle.Equals(viewport.LightRenderTarget.Texture.TextureId));
CheckGlError();
GLClearColor(_entityManager.GetComponentOrNull<MapLightComponent>(mapUid)?.AmbientLightColor ?? MapLightComponent.DefaultColor);
var clearEv = new GetClearColorEvent();
_entityManager.EventBus.RaiseEvent(EventSource.Local, ref clearEv);
var clearColor = clearEv.Color ?? GetClearColor(mapUid);
GLClearColor(clearColor);
GL.ClearStencil(0xFF);
GL.StencilMask(0xFF);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
CheckGlError();
var oldTarget = _currentRenderTarget;
var oldProj = _currentMatrixProj;
var oldShader = _queuedShaderInstance;
var oldModel = _currentMatrixModel;
var oldScissor = _currentScissorState;
var state = PushRenderStateFull();
RenderOverlays(viewport, OverlaySpace.BeforeLighting, worldAABB, worldBounds);
PopRenderStateFull(state);
DebugTools.Assert(oldScissor.Equals(_currentScissorState));
DebugTools.Assert(oldModel.Equals(_currentMatrixModel));
DebugTools.Assert(oldShader.Equals(_queuedShaderInstance));
DebugTools.Assert(oldProj.Equals(_currentMatrixProj));
DebugTools.Assert(oldTarget.Equals(_currentRenderTarget));
DebugTools.Assert(_currentBoundRenderTarget.TextureHandle.Equals(viewport.LightRenderTarget.Texture.TextureId));
ApplyLightingFovToBuffer(viewport, eye);
var lightShader = _loadedShaders[_enableSoftShadows ? _lightSoftShaderHandle : _lightHardShaderHandle]
@@ -509,13 +529,12 @@ namespace Robust.Client.Graphics.Clyde
}
ResetBlendFunc();
GL.Disable(EnableCap.StencilTest);
_isStencilling = false;
IsStencilling = false;
CheckGlError();
if (_cfg.GetCVar(CVars.LightBlur))
BlurLights(viewport, eye);
BlurRenderTarget(viewport, viewport.LightRenderTarget, viewport.LightBlurTarget, eye, 14f);
using (_prof.Group("BlurOntoWalls"))
{
@@ -531,9 +550,8 @@ namespace Robust.Client.Graphics.Clyde
GL.Viewport(0, 0, viewport.Size.X, viewport.Size.Y);
CheckGlError();
Array.Clear(_lightsToRenderList, 0, count);
_lightingReady = true;
Array.Clear(_lightsToRenderList, 0, count);
}
private static bool LightQuery(ref (
@@ -601,8 +619,9 @@ namespace Robust.Client.Graphics.Clyde
// Use worldbounds for this one as we only care if the light intersects our actual bounds
var xforms = _entityManager.GetEntityQuery<TransformComponent>();
var state = (this, count: 0, shadowCastingCount: 0, xforms, worldAABB);
var lightAabb = worldAABB.Enlarged(_maxLightRadius);
foreach (var (uid, comp) in _lightTreeSystem.GetIntersectingTrees(map, worldAABB))
foreach (var (uid, comp) in _lightTreeSystem.GetIntersectingTrees(map, lightAabb))
{
var bounds = _transformSystem.GetInvWorldMatrix(uid, xforms).TransformBox(worldBounds);
comp.Tree.QueryAabb(ref state, LightQuery, bounds);
@@ -643,21 +662,33 @@ namespace Robust.Client.Graphics.Clyde
return (state.count, expandedBounds);
}
private void BlurLights(Viewport viewport, IEye eye)
/// <inheritdoc/>
[Pure]
public Color GetClearColor(EntityUid mapUid)
{
using var _ = DebugGroup(nameof(BlurLights));
return _entityManager.GetComponentOrNull<MapLightComponent>(mapUid)?.AmbientLightColor ??
MapLightComponent.DefaultColor;
}
GL.Disable(EnableCap.Blend);
CheckGlError();
/// <inheritdoc/>
public void BlurRenderTarget(IClydeViewport viewport, IRenderTarget target, IRenderTarget blurBuffer, IEye eye, float multiplier)
{
if (target is not RenderTexture rTexture || blurBuffer is not RenderTexture blurTexture)
return;
using var _ = DebugGroup(nameof(BlurRenderTarget));
var state = PushRenderStateFull();
IsBlending = false;
CalcScreenMatrices(viewport.Size, out var proj, out var view);
SetProjViewBuffer(proj, view);
var shader = _loadedShaders[_lightBlurShaderHandle].Program;
shader.Use();
SetupGlobalUniformsImmediate(shader, viewport.LightRenderTarget.Texture);
SetupGlobalUniformsImmediate(shader, rTexture.Texture);
var size = viewport.LightRenderTarget.Size;
var size = target.Size;
shader.SetUniformMaybe("size", (Vector2)size);
shader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
@@ -667,14 +698,13 @@ namespace Robust.Client.Graphics.Clyde
// Initially we're pulling from the light render target.
// So we set it out of the loop so
// _wallBleedIntermediateRenderTarget2 gets bound at the end of the loop body.
SetTexture(TextureUnit.Texture0, viewport.LightRenderTarget.Texture);
SetTexture(TextureUnit.Texture0, rTexture.Texture);
// Have to scale the blurring radius based on viewport size and camera zoom.
const float refCameraHeight = 14;
var facBase = _cfg.GetCVar(CVars.LightBlurFactor);
var cameraSize = eye.Zoom.Y * viewport.Size.Y * (1 / viewport.RenderScale.Y) / EyeManager.PixelsPerMeter;
// 7e-3f is just a magic factor that makes it look ok.
var factor = facBase * (refCameraHeight / cameraSize);
var factor = facBase * (multiplier / cameraSize);
// Multi-iteration gaussian blur.
for (var i = 3; i > 0; i--)
@@ -683,35 +713,31 @@ namespace Robust.Client.Graphics.Clyde
// Set factor.
shader.SetUniformMaybe("radius", scale);
BindRenderTargetFull(viewport.LightBlurTarget);
BindRenderTargetImmediate(RtToLoaded(blurBuffer));
// Blur horizontally to _wallBleedIntermediateRenderTarget1.
shader.SetUniformMaybe("direction", Vector2.UnitX);
_drawQuad(Vector2.Zero, viewport.Size, Matrix3x2.Identity, shader);
SetTexture(TextureUnit.Texture0, viewport.LightBlurTarget.Texture);
SetTexture(TextureUnit.Texture0, blurTexture.Texture);
BindRenderTargetFull(viewport.LightRenderTarget);
BindRenderTargetImmediate(RtToLoaded(rTexture));
// Blur vertically to _wallBleedIntermediateRenderTarget2.
shader.SetUniformMaybe("direction", Vector2.UnitY);
_drawQuad(Vector2.Zero, viewport.Size, Matrix3x2.Identity, shader);
SetTexture(TextureUnit.Texture0, viewport.LightRenderTarget.Texture);
SetTexture(TextureUnit.Texture0, rTexture.Texture);
}
GL.Enable(EnableCap.Blend);
CheckGlError();
// We didn't trample over the old _currentMatrices so just roll it back.
SetProjViewBuffer(_currentMatrixProj, _currentMatrixView);
PopRenderStateFull(state);
}
private void BlurOntoWalls(Viewport viewport, IEye eye)
{
using var _ = DebugGroup(nameof(BlurOntoWalls));
GL.Disable(EnableCap.Blend);
CheckGlError();
IsBlending = false;
CalcScreenMatrices(viewport.Size, out var proj, out var view);
SetProjViewBuffer(proj, view);
@@ -761,8 +787,7 @@ namespace Robust.Client.Graphics.Clyde
SetTexture(TextureUnit.Texture0, viewport.WallBleedIntermediateRenderTarget2.Texture);
}
GL.Enable(EnableCap.Blend);
CheckGlError();
IsBlending = true;
// We didn't trample over the old _currentMatrices so just roll it back.
SetProjViewBuffer(_currentMatrixProj, _currentMatrixView);
}
@@ -775,8 +800,7 @@ namespace Robust.Client.Graphics.Clyde
GL.Viewport(0, 0, viewport.LightRenderTarget.Size.X, viewport.LightRenderTarget.Size.Y);
CheckGlError();
GL.Disable(EnableCap.Blend);
CheckGlError();
IsBlending = false;
var shader = _loadedShaders[_mergeWallLayerShaderHandle].Program;
shader.Use();
@@ -796,8 +820,7 @@ namespace Robust.Client.Graphics.Clyde
IntPtr.Zero);
CheckGlError();
GL.Enable(EnableCap.Blend);
CheckGlError();
IsBlending = true;
}
private void ApplyFovToBuffer(Viewport viewport, IEye eye)
@@ -827,8 +850,7 @@ namespace Robust.Client.Graphics.Clyde
FovSetTransformAndBlit(viewport, eye.Position.Position, fovShader);
GL.StencilMask(0x00);
GL.Disable(EnableCap.StencilTest);
_isStencilling = false;
IsStencilling = false;
}
private void ApplyLightingFovToBuffer(Viewport viewport, IEye eye)
@@ -1135,22 +1157,20 @@ namespace Robust.Client.Graphics.Clyde
var lightMapSize = GetLightMapSize(viewport.Size);
var lightMapSizeQuart = GetLightMapSize(viewport.Size, true);
var lightMapColorFormat = _hasGLFloatFramebuffers
? RenderTargetColorFormat.R11FG11FB10F
: RenderTargetColorFormat.Rgba8;
var lightMapSampleParameters = new TextureSampleParameters { Filter = true };
viewport.LightRenderTarget?.Dispose();
viewport.WallMaskRenderTarget?.Dispose();
viewport.WallBleedIntermediateRenderTarget1?.Dispose();
viewport.WallBleedIntermediateRenderTarget2?.Dispose();
var lightMapColorFormat = _hasGLFloatFramebuffers
? RenderTargetColorFormat.R11FG11FB10F
: RenderTargetColorFormat.Rgba8;
var lightMapSampleParameters = new TextureSampleParameters { Filter = true };
viewport.WallMaskRenderTarget = CreateRenderTarget(viewport.Size, RenderTargetColorFormat.R8,
name: $"{viewport.Name}-{nameof(viewport.WallMaskRenderTarget)}");
viewport.LightRenderTarget = CreateRenderTarget(lightMapSize,
new RenderTargetFormatParameters(lightMapColorFormat, hasDepthStencil: true),
lightMapSampleParameters,
viewport.LightRenderTarget = (RenderTexture) CreateLightRenderTarget(lightMapSize,
$"{viewport.Name}-{nameof(viewport.LightRenderTarget)}");
viewport.LightBlurTarget = CreateRenderTarget(lightMapSize,
@@ -1158,11 +1178,13 @@ namespace Robust.Client.Graphics.Clyde
lightMapSampleParameters,
$"{viewport.Name}-{nameof(viewport.LightBlurTarget)}");
viewport.WallBleedIntermediateRenderTarget1 = CreateRenderTarget(lightMapSizeQuart, lightMapColorFormat,
viewport.WallBleedIntermediateRenderTarget1 = CreateRenderTarget(lightMapSizeQuart,
new RenderTargetFormatParameters(lightMapColorFormat),
lightMapSampleParameters,
$"{viewport.Name}-{nameof(viewport.WallBleedIntermediateRenderTarget1)}");
viewport.WallBleedIntermediateRenderTarget2 = CreateRenderTarget(lightMapSizeQuart, lightMapColorFormat,
viewport.WallBleedIntermediateRenderTarget2 = CreateRenderTarget(lightMapSizeQuart,
new RenderTargetFormatParameters(lightMapColorFormat),
lightMapSampleParameters,
$"{viewport.Name}-{nameof(viewport.WallBleedIntermediateRenderTarget2)}");
}

View File

@@ -30,6 +30,20 @@ namespace Robust.Client.Graphics.Clyde
// It, like _mainWindowRenderTarget, is initialized in Clyde's constructor
private LoadedRenderTarget _currentBoundRenderTarget;
public IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true)
{
var lightMapColorFormat = _hasGLFloatFramebuffers
? RTCF.R11FG11FB10F
: RTCF.Rgba8;
var lightMapSampleParameters = new TextureSampleParameters { Filter = true };
return CreateRenderTarget(size,
new RenderTargetFormatParameters(lightMapColorFormat, hasDepthStencil: depthStencil),
lightMapSampleParameters,
name: name);
}
IRenderTexture IClyde.CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
TextureSampleParameters? sampleParameters, string? name)
{
@@ -204,7 +218,8 @@ namespace Robust.Client.Graphics.Clyde
Size = size,
TextureHandle = textureObject.TextureId,
MemoryPressure = pressure,
ColorFormat = format.ColorFormat
ColorFormat = format.ColorFormat,
SampleParameters = sampleParameters,
};
//GC.AddMemoryPressure(pressure);
@@ -251,9 +266,15 @@ namespace Robust.Client.Graphics.Clyde
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private LoadedRenderTarget RtToLoaded(RenderTargetBase rt)
private LoadedRenderTarget RtToLoaded(IRenderTarget rt)
{
return _renderTargets[rt.Handle];
switch (rt)
{
case RenderTargetBase based:
return _renderTargets[based.Handle];
default:
throw new NotImplementedException();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -302,6 +323,8 @@ namespace Robust.Client.Graphics.Clyde
// Renderbuffer handle
public GLHandle DepthStencilHandle;
public long MemoryPressure;
public TextureSampleParameters? SampleParameters;
}
private abstract class RenderTargetBase : IRenderTarget

View File

@@ -90,9 +90,61 @@ namespace Robust.Client.Graphics.Clyde
// (queue) and (misc), current state of the scissor test. Null if disabled.
private UIBox2i? _currentScissorState;
// Some simple flags that basically just tracks the current state of glEnable(GL_STENCIL/GL_SCISSOR_TEST)
private bool _isScissoring;
private bool _isStencilling;
/// <summary>
/// Tracks enabled GL capabilities for renderer state.
/// </summary>
private GLCaps _glCaps = GLCaps.None;
private bool IsStencilling
{
get => (_glCaps & GLCaps.Stencilling) == GLCaps.Stencilling;
set
{
if (value == IsStencilling)
return;
if (value)
{
_glCaps |= GLCaps.Stencilling;
GL.Enable(EnableCap.StencilTest);
}
else
{
_glCaps &= ~GLCaps.Stencilling;
GL.Disable(EnableCap.StencilTest);
}
CheckGlError();
}
}
private bool IsBlending
{
get => (_glCaps & GLCaps.Blending) == GLCaps.Blending;
set
{
if (value == IsBlending)
return;
if (value)
{
_glCaps |= GLCaps.Blending;
GL.Enable(EnableCap.Blend);
}
else
{
_glCaps &= ~GLCaps.Blending;
GL.Disable(EnableCap.Blend);
}
CheckGlError();
}
}
private bool IsScissoring
{
get => _currentScissorState != null;
}
private readonly RefList<RenderCommand> _queuedRenderCommands = new RefList<RenderCommand>();
@@ -364,16 +416,17 @@ namespace Robust.Client.Graphics.Clyde
private void SetScissorImmediate(bool enable, in UIBox2i box)
{
var oldIsScissoring = _isScissoring;
_isScissoring = enable;
if (_isScissoring)
if (enable)
{
if (!oldIsScissoring)
{
GL.Enable(EnableCap.ScissorTest);
CheckGlError();
}
GL.Enable(EnableCap.ScissorTest);
}
else
{
GL.Disable(EnableCap.ScissorTest);
}
if (enable)
{
// Don't forget to flip it, these coordinates have bottom left as origin.
// TODO: Broken when rendering to non-screen render targets.
@@ -387,11 +440,6 @@ namespace Robust.Client.Graphics.Clyde
}
CheckGlError();
}
else if (oldIsScissoring)
{
GL.Disable(EnableCap.ScissorTest);
CheckGlError();
}
}
// NOTE: sRGB IS IN LINEAR IF FRAMEBUFFER_SRGB IS ACTIVE.
@@ -420,17 +468,11 @@ namespace Robust.Client.Graphics.Clyde
var program = shader.Program;
program.Use();
IsStencilling = instance.Stencil.Enabled;
// Handle stencil parameters.
if (instance.Stencil.Enabled)
{
if (!_isStencilling)
{
GL.Enable(EnableCap.StencilTest);
CheckGlError();
_isStencilling = true;
}
GL.StencilMask(instance.Stencil.WriteMask);
CheckGlError();
GL.StencilFunc(ToGLStencilFunc(instance.Stencil.Func), instance.Stencil.Ref, instance.Stencil.ReadMask);
@@ -438,12 +480,6 @@ namespace Robust.Client.Graphics.Clyde
GL.StencilOp(TKStencilOp.Keep, TKStencilOp.Keep, ToGLStencilOp(instance.Stencil.Op));
CheckGlError();
}
else if (_isStencilling)
{
GL.Disable(EnableCap.StencilTest);
CheckGlError();
_isStencilling = false;
}
if (instance.Parameters.Count == 0)
return (program, instance);
@@ -487,6 +523,9 @@ namespace Robust.Client.Graphics.Clyde
case Color color:
program.SetUniform(name, color);
break;
case Color[] colorArr:
program.SetUniform(name, colorArr);
break;
case int i:
program.SetUniform(name, i);
break;
@@ -859,17 +898,34 @@ namespace Robust.Client.Graphics.Clyde
private FullStoredRendererState PushRenderStateFull()
{
return new FullStoredRendererState(_currentMatrixProj, _currentMatrixView, _currentRenderTarget);
return new FullStoredRendererState(
_currentMatrixProj,
_currentMatrixView,
_currentBoundRenderTarget,
_currentRenderTarget,
_queuedShaderInstance,
_currentScissorState,
_glCaps);
}
private void PopRenderStateFull(in FullStoredRendererState state)
{
SetProjViewFull(state.ProjMatrix, state.ViewMatrix);
BindRenderTargetFull(state.RenderTarget);
BindRenderTargetImmediate(state.BoundRenderTarget);
var (width, height) = state.RenderTarget.Size;
_queuedShaderInstance = state.QueuedShaderInstance;
_currentRenderTarget = state.RenderTarget;
var (width, height) = state.BoundRenderTarget.Size;
GL.Viewport(0, 0, width, height);
CheckGlError();
IsStencilling = (state.GLCaps & GLCaps.Stencilling) == GLCaps.Stencilling;
IsBlending = (state.GLCaps & GLCaps.Blending) == GLCaps.Blending;
SetScissorFull(state.ScissorState);
GL.ClearStencil(0xFF);
GL.StencilMask(0xFF);
GL.Clear(ClearBufferMask.StencilBufferBit);
}
private void SetViewportImmediate(Box2i box)
@@ -1061,15 +1117,44 @@ namespace Robust.Client.Graphics.Clyde
{
public readonly Matrix3x2 ProjMatrix;
public readonly Matrix3x2 ViewMatrix;
public readonly LoadedRenderTarget BoundRenderTarget;
public readonly LoadedRenderTarget RenderTarget;
public readonly ClydeShaderInstance QueuedShaderInstance;
public FullStoredRendererState(in Matrix3x2 projMatrix, in Matrix3x2 viewMatrix,
LoadedRenderTarget renderTarget)
public readonly UIBox2i? ScissorState;
public readonly GLCaps GLCaps;
public FullStoredRendererState(
in Matrix3x2 projMatrix,
in Matrix3x2 viewMatrix,
LoadedRenderTarget boundRenderTarget,
LoadedRenderTarget renderTarget,
ClydeShaderInstance queuedShaderInstance,
UIBox2i? scissorState,
GLCaps glcaps
)
{
ProjMatrix = projMatrix;
ViewMatrix = viewMatrix;
BoundRenderTarget = boundRenderTarget;
RenderTarget = renderTarget;
QueuedShaderInstance = queuedShaderInstance;
ScissorState = scissorState;
GLCaps = glcaps;
}
}
[Flags]
private enum GLCaps : ushort
{
// If you add flags here make sure to update PopRenderState!
None = 0,
Blending = 1 << 0,
Stencilling = 1 << 2,
}
}
}

View File

@@ -485,6 +485,13 @@ namespace Robust.Client.Graphics.Clyde
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, Color[] value)
{
var data = Parent._shaderInstances[Handle];
data.ParametersDirty = true;
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, int value)
{
var data = Parent._shaderInstances[Handle];

View File

@@ -171,7 +171,7 @@ namespace Robust.Client.Graphics.Clyde
}
public void RenderScreenOverlaysBelow(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds)
{
@@ -179,7 +179,7 @@ namespace Robust.Client.Graphics.Clyde
}
public void RenderScreenOverlaysAbove(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds)
{

View File

@@ -10,6 +10,7 @@ using Robust.Client.Input;
using Robust.Client.Map;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
@@ -21,7 +22,9 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
namespace Robust.Client.Graphics.Clyde
@@ -47,6 +50,8 @@ namespace Robust.Client.Graphics.Clyde
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly ClientEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IReloadManager _reloads = default!;
private GLUniformBuffer<ProjViewMatrices> ProjViewUBO = default!;
private GLUniformBuffer<UniformConstants> UniformConstantsUBO = default!;
@@ -98,6 +103,16 @@ namespace Robust.Client.Graphics.Clyde
_sawmillOgl = _logManager.GetSawmill("clyde.ogl");
_sawmillWin = _logManager.GetSawmill("clyde.win");
_reloads.Register("/Shaders", "*.swsl");
_reloads.Register("/Textures/Shaders", "*.swsl");
_reloads.Register("/Textures", "*.jpg");
_reloads.Register("/Textures", "*.jpeg");
_reloads.Register("/Textures", "*.png");
_reloads.Register("/Textures", "*.webp");
_reloads.OnChanged += OnChange;
_proto.PrototypesReloaded += OnProtoReload;
_cfg.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
_cfg.OnValueChanged(CVars.DisplayVSync, VSyncChanged, true);
_cfg.OnValueChanged(CVars.DisplayWindowMode, WindowModeChanged, true);
@@ -121,6 +136,38 @@ namespace Robust.Client.Graphics.Clyde
return InitWindowing();
}
private void OnProtoReload(PrototypesReloadedEventArgs obj)
{
if (!obj.WasModified<ShaderPrototype>())
return;
foreach (var shader in obj.ByType[typeof(ShaderPrototype)].Modified.Keys)
{
_resourceCache.ReloadResource<ShaderSourceResource>(shader);
}
}
private void OnChange(ResPath obj)
{
if ((obj.TryRelativeTo(new ResPath("/Shaders"), out _) || obj.TryRelativeTo(new ResPath("/Textures/Shaders"), out _)) && obj.Extension == "swsl")
{
_resourceCache.ReloadResource<ShaderSourceResource>(obj);
}
if (obj.TryRelativeTo(new ResPath("/Textures"), out _) && !obj.TryRelativeTo(new ResPath("/Textures/Tiles"), out _))
{
if (obj.Extension == "jpg" || obj.Extension == "jpeg" || obj.Extension == "webp")
{
_resourceCache.ReloadResource<TextureResource>(obj);
}
if (obj.Extension == "png")
{
_resourceCache.ReloadResource<TextureResource>(obj);
}
}
}
public bool InitializePostWindowing()
{
_gameThread = Thread.CurrentThread;
@@ -245,7 +292,7 @@ namespace Robust.Client.Graphics.Clyde
overrideVersion != null,
_windowing!.GetDescription());
GL.Enable(EnableCap.Blend);
IsBlending = true;
if (_hasGLSrgb && !_isGLES)
{
GL.Enable(EnableCap.FramebufferSrgb);

View File

@@ -9,6 +9,7 @@ using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -189,6 +190,22 @@ namespace Robust.Client.Graphics.Clyde
return new DummyTexture(size);
}
/// <inheritdoc />
public Color GetClearColor(EntityUid mapUid)
{
return Color.Transparent;
}
public void BlurRenderTarget(IClydeViewport viewport, IRenderTarget target, IRenderTarget blurBuffer, IEye eye, float multiplier)
{
// NOOP
}
public IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true)
{
return CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.R8, hasDepthStencil: depthStencil), null, name: name);
}
public IRenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
TextureSampleParameters? sampleParameters = null, string? name = null)
{
@@ -352,6 +369,10 @@ namespace Robust.Client.Graphics.Clyde
{
}
private protected override void SetParameterImpl(string name, Color[] value)
{
}
private protected override void SetParameterImpl(string name, int value)
{
}
@@ -497,7 +518,7 @@ namespace Robust.Client.Graphics.Clyde
}
public void RenderScreenOverlaysBelow(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds)
{
@@ -505,7 +526,7 @@ namespace Robust.Client.Graphics.Clyde
}
public void RenderScreenOverlaysAbove(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds)
{

View File

@@ -334,7 +334,7 @@ namespace Robust.Client.Graphics.Clyde
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, in Color color, bool convertToLinear=true)
private void SetUniformDirect(int slot, in Color color, bool convertToLinear = true)
{
var converted = color;
if (convertToLinear)
@@ -349,6 +349,39 @@ namespace Robust.Client.Graphics.Clyde
}
}
public void SetUniform(string uniformName, Color[] colors)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, colors);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, Color[] colors, bool convertToLinear = true)
{
scoped Span<Color> colorsToPass;
if (convertToLinear)
{
colorsToPass = stackalloc Color[colors.Length];
for (int i = 0; i < colors.Length; i++)
{
colorsToPass[i] = Color.FromSrgb(colors[i]);
}
}
else
{
colorsToPass = colors;
}
unsafe
{
fixed (Color* ptr = &colorsToPass[0])
{
GL.Uniform4(slot, colorsToPass.Length, (float*)ptr);
_clyde.CheckGlError();
}
}
}
public void SetUniform(string uniformName, in Vector3 vector)
{
var uniformId = GetUniform(uniformName);

View File

@@ -1,8 +1,22 @@
// UV coordinates in texture-space. I.e., (0,0) is the corner of the texture currently being used to draw.
// When drawing a sprite from a texture atlas, (0,0) is the corner of the atlas, not the specific sprite being drawn.
varying highp vec2 UV;
// UV coordinates in quad-space. I.e., when drawing a sprite from a texture atlas (0,0) is the corner of the sprite
// currently being drawn.
varying highp vec2 UV2;
// TBH I'm not sure what this is for. I think it is scree UV coordiantes, i.e., FRAGCOORD.xy * SCREEN_PIXEL_SIZE ?
// TODO CLYDE Is this still needed?
varying highp vec2 Pos;
// Vertex colour modulation. Note that negative values imply that the LIGHTMAP should be ignored. This is used to avoid
// having to set the texture to a white/blank texture for sprites that have no light shading applied.
varying highp vec4 VtxModulate;
// The current light map. Unless disabled, this is automatically sampled to create the LIGHT vector, which is then used
// to modulate the output colour.
// TODO CLYDE consistent shader variable naming
uniform sampler2D lightMap;
// [SHADER_HEADER_CODE]
@@ -11,11 +25,37 @@ void main()
{
highp vec4 FRAGCOORD = gl_FragCoord;
// The output colour. This should get set by the shader code block.
// This will get modified by the LIGHT and MODULATE vectors.
lowp vec4 COLOR;
lowp vec3 lightSample = texture2D(lightMap, Pos).rgb;
// The light colour, usually sampled from the LIGHTMAP
lowp vec4 LIGHT;
// Colour modulation vector.
highp vec4 MODULATE;
// Sample the texture outside of the branch / with uniform control flow.
LIGHT = texture2D(lightMap, Pos);
if (VtxModulate.x < 0.0)
{
// Negative VtxModulate implies unshaded/no lighting.
MODULATE = -1.0 - VtxModulate;
LIGHT = vec4(1.0);
}
else
{
MODULATE = VtxModulate;
}
// TODO CLYDE consistent shader variable naming
// Requires breaking changes.
lowp vec3 lightSample = LIGHT.xyz;
// [SHADER_CODE]
gl_FragColor = zAdjustResult(COLOR * VtxModulate * vec4(lightSample, 1.0));
LIGHT.xyz = lightSample;
gl_FragColor = zAdjustResult(COLOR * MODULATE * LIGHT);
}

View File

@@ -18,6 +18,7 @@ uniform mat3 modelMatrix;
// Allows us to do texture atlassing with texture coordinates 0->1
// Input texture coordinates get mapped to this range.
uniform vec4 modifyUV;
// TODO CLYDE Is this still needed?
// [SHADER_HEADER_CODE]
@@ -39,5 +40,15 @@ void main()
Pos = (VERTEX + 1.0) / 2.0;
UV = mix(modifyUV.xy, modifyUV.zw, tCoord);
UV2 = tCoord2;
VtxModulate = zFromSrgb(modulate);
// Negative modulation is being used as a hacky way to squeeze in lighting data.
// I.e., negative modulation implies we ignore the lighting.
if (modulate.x < 0.0)
{
VtxModulate = -1.0 - zFromSrgb(-1.0 - modulate);
}
else
{
VtxModulate = zFromSrgb(modulate);
}
}

View File

@@ -1,6 +1,7 @@
varying highp vec2 UV;
varying highp vec2 UV2;
// TODO CLYDE consistent shader variable naming
uniform sampler2D lightMap;
// [SHADER_HEADER_CODE]

View File

@@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Numerics;
using System.Threading.Tasks;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics
{
@@ -71,6 +74,24 @@ namespace Robust.Client.Graphics
in TextureLoadParameters? loadParams = null)
where T : unmanaged, IPixel<T>;
/// <summary>
/// Gets the clear color for the specified map viewport.
/// </summary>
[Pure]
Color GetClearColor(EntityUid mapUid);
/// <summary>
/// Applies a blur to the specified render target. Requires a separate buffer with similar properties to draw intermediate steps into.
/// </summary>
/// <param name="viewport">The viewport being used for drawing.</param>
/// <param name="target">The blur target.</param>
/// <param name="blurBuffer">The separate buffer to draw into.</param>
/// <param name="eye">The eye being drawn with.</param>
/// <param name="multiplier">Scale of how much blur to blur by.</param>
void BlurRenderTarget(IClydeViewport viewport, IRenderTarget target, IRenderTarget blurBuffer, IEye eye, float multiplier);
IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true);
IRenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
TextureSampleParameters? sampleParameters = null, string? name = null);

View File

@@ -18,6 +18,7 @@ namespace Robust.Client.Graphics
/// </summary>
IRenderTexture RenderTarget { get; }
IRenderTexture LightRenderTarget { get; }
IEye? Eye { get; set; }
Vector2i Size { get; }
@@ -66,7 +67,7 @@ namespace Robust.Client.Graphics
/// Not relative to the current transform of <see cref="handle"/>.
/// </param>
public void RenderScreenOverlaysBelow(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds);
@@ -80,7 +81,7 @@ namespace Robust.Client.Graphics
/// Not relative to the current transform of <see cref="handle"/>.
/// </param>
public void RenderScreenOverlaysAbove(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds);
}

View File

@@ -1,4 +1,6 @@
using System;
using System.Numerics;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using SixLabors.ImageSharp.PixelFormats;
@@ -15,5 +17,42 @@ namespace Robust.Client.Graphics
Vector2i Size { get; }
void CopyPixelsToMemory<T>(CopyPixelsDelegate<T> callback, UIBox2i? subRegion = null) where T : unmanaged, IPixel<T>;
public Vector2 LocalToWorld(IEye eye, Vector2 point, Vector2 scale)
{
var newPoint = point;
// (inlined version of UiProjMatrix^-1)
newPoint -= Size / 2f;
newPoint *= new Vector2(1, -1) / EyeManager.PixelsPerMeter;
// view matrix
eye.GetViewMatrixInv(out var viewMatrixInv, scale);
newPoint = Vector2.Transform(newPoint, viewMatrixInv);
return newPoint;
}
public Vector2 WorldToLocal(Vector2 point, IEye eye, Vector2 scale)
{
var newPoint = point;
eye.GetViewMatrix(out var viewMatrix, scale);
newPoint = Vector2.Transform(newPoint, viewMatrix);
// (inlined version of UiProjMatrix)
newPoint *= new Vector2(1, -1) * EyeManager.PixelsPerMeter;
newPoint += Size / 2f;
return newPoint;
}
public Matrix3x2 GetWorldToLocalMatrix(IEye eye, Vector2 scale)
{
eye.GetViewMatrix(out var viewMatrix, scale * new Vector2(EyeManager.PixelsPerMeter, -EyeManager.PixelsPerMeter));
viewMatrix.M31 += Size.X / 2f;
viewMatrix.M32 += Size.Y / 2f;
return viewMatrix;
}
}
}

View File

@@ -0,0 +1,13 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics;
/// <summary>
/// Raised by the engine if content wishes to override the default clear color.
/// </summary>
[ByRefEvent]
public record struct GetClearColorEvent
{
public Color? Color;
}

View File

@@ -9,6 +9,5 @@ namespace Robust.Client.Graphics
public bool DrawHardFov { get; set; } = true;
public bool DrawLighting { get; set; } = true;
public bool LockConsoleAccess { get; set; } = false;
public Color AmbientLightColor { get; set; } = Color.FromSrgb(Color.Black);
}
}

View File

@@ -1,6 +1,7 @@
using JetBrains.Annotations;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -39,6 +40,8 @@ namespace Robust.Client.Graphics
/// </summary>
public readonly UIBox2i ViewportBounds;
public readonly EntityUid MapUid;
/// <summary>
/// <see cref="MapId"/> of the viewport's eye.
/// </summary>
@@ -54,24 +57,32 @@ namespace Robust.Client.Graphics
/// </summary>
public readonly Box2Rotated WorldBounds;
public readonly IRenderHandle RenderHandle;
public DrawingHandleScreen ScreenHandle => (DrawingHandleScreen) DrawingHandle;
public DrawingHandleWorld WorldHandle => (DrawingHandleWorld) DrawingHandle;
public OverlayDrawArgs(
internal OverlayDrawArgs(
OverlaySpace space,
IViewportControl? viewportControl,
IClydeViewport viewport,
DrawingHandleBase drawingHandle,
IRenderHandle renderHandle,
in UIBox2i viewportBounds,
in EntityUid mapUid,
in MapId mapId,
in Box2 worldAabb,
in Box2Rotated worldBounds)
{
DrawingHandle = space is OverlaySpace.ScreenSpace or OverlaySpace.ScreenSpaceBelowWorld
? renderHandle.DrawingHandleScreen
: renderHandle.DrawingHandleWorld;
Space = space;
ViewportControl = viewportControl;
Viewport = viewport;
DrawingHandle = drawingHandle;
RenderHandle = renderHandle;
ViewportBounds = viewportBounds;
MapUid = mapUid;
MapId = mapId;
WorldAABB = worldAabb;
WorldBounds = worldBounds;

View File

@@ -221,11 +221,12 @@ namespace Robust.Client.Graphics
public static bool TypeSupportsArrays(this ShaderDataType type)
{
// TODO: add support for int, and vec3/4 arrays
// TODO: add support for int, and vec3 arrays
return
(type == ShaderDataType.Float) ||
(type == ShaderDataType.Vec2) ||
(type == ShaderDataType.Bool);
(type == ShaderDataType.Bool) ||
(type == ShaderDataType.Vec4);
}
[SuppressMessage("ReSharper", "StringLiteralTypo")]

View File

@@ -113,6 +113,13 @@ namespace Robust.Client.Graphics
SetParameterImpl(name, value);
}
public void SetParameter(string name, Color[] value)
{
EnsureAlive();
EnsureMutable();
SetParameterImpl(name, value);
}
public void SetParameter(string name, Vector4 value)
{
EnsureAlive();
@@ -223,6 +230,7 @@ namespace Robust.Client.Graphics
private protected abstract void SetParameterImpl(string name, Vector3 value);
private protected abstract void SetParameterImpl(string name, Vector4 value);
private protected abstract void SetParameterImpl(string name, Color value);
private protected abstract void SetParameterImpl(string name, Color[] value);
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);

View File

@@ -16,7 +16,7 @@ using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.Graphics
{
[Prototype("shader")]
[Prototype]
public sealed partial class ShaderPrototype : IPrototype, ISerializationHooks
{
[ViewVariables]

View File

@@ -1,28 +1,28 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Map;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Toolshed;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace Robust.Client.Map
{
internal sealed class ClydeTileDefinitionManager : TileDefinitionManager, IClydeTileDefinitionManager, IPostInjectInit
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IReloadManager _reload = default!;
[Dependency] private readonly IResourceManager _manager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
@@ -32,7 +32,7 @@ namespace Robust.Client.Map
public Texture TileTextureAtlas => _tileTextureAtlas ?? Texture.Transparent;
private readonly Dictionary<int, Box2[]> _tileRegions = new();
private FrozenDictionary<(int Id, Direction Direction), Box2[]> _tileRegions = FrozenDictionary<(int Id, Direction Direction), Box2[]>.Empty;
public Box2 ErrorTileRegion { get; private set; }
@@ -45,7 +45,14 @@ namespace Robust.Client.Map
/// <inheritdoc />
public Box2[]? TileAtlasRegion(int tileType)
{
if (_tileRegions.TryGetValue(tileType, out var region))
return TileAtlasRegion(tileType, Direction.Invalid);
}
/// <inheritdoc />
public Box2[]? TileAtlasRegion(int tileType, Direction direction)
{
// ReSharper disable once CanSimplifyDictionaryTryGetValueWithGetValueOrDefault
if (_tileRegions.TryGetValue((tileType, direction), out var region))
{
return region;
}
@@ -57,12 +64,37 @@ namespace Robust.Client.Map
{
base.Initialize();
_protoManager.PrototypesReloaded += OnProtoReload;
_reload.Register("/Textures/Tiles", "*.png");
_reload.OnChanged += OnReload;
_genTextureAtlas();
}
private void OnProtoReload(PrototypesReloadedEventArgs obj)
{
if (!obj.WasModified<ITileDefinition>())
return;
_genTextureAtlas();
}
private void OnReload(ResPath obj)
{
if (obj.Extension != "png")
return;
if (!obj.TryRelativeTo(new ResPath("/Textures/Tiles"), out _))
return;
_genTextureAtlas();
}
internal void _genTextureAtlas()
{
_tileRegions.Clear();
var sw = RStopwatch.StartNew();
var tileRegs = new Dictionary<(int Id, Direction Direction), Box2[]>();
_tileTextureAtlas = null;
var defList = TileDefs.Where(t => t.Sprite != null).ToList();
@@ -73,7 +105,7 @@ namespace Robust.Client.Map
const int tileSize = EyeManager.PixelsPerMeter;
var tileCount = defList.Select(x => (int)x.Variants).Sum() + 1;
var tileCount = defList.Select(x => x.Variants + x.EdgeSprites.Count).Sum() + 1;
var dimensionX = (int) Math.Ceiling(Math.Sqrt(tileCount));
var dimensionY = (int) Math.Ceiling((float) tileCount / dimensionX);
@@ -81,11 +113,11 @@ namespace Robust.Client.Map
var imgWidth = dimensionX * tileSize;
var imgHeight = dimensionY * tileSize;
var sheet = new Image<Rgba32>(imgWidth, imgHeight);
var w = (float) sheet.Width;
var h = (float) sheet.Height;
// Add in the missing tile texture sprite as tile texture 0.
{
var w = (float) sheet.Width;
var h = (float) sheet.Height;
ErrorTileRegion = Box2.FromDimensions(
0, (h - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h);
@@ -133,25 +165,98 @@ namespace Robust.Client.Map
var box = new UIBox2i(0, 0, tileSize, tileSize).Translated(new Vector2i(j * tileSize, 0));
image.Blit(box, sheet, point);
var w = (float) sheet.Width;
var h = (float) sheet.Height;
regionList[j] = Box2.FromDimensions(
point.X / w, (h - point.Y - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h);
column++;
if (column >= dimensionX)
{
column = 0;
row++;
}
BumpColumn(ref row, ref column, dimensionX);
}
_tileRegions.Add(def.TileId, regionList);
tileRegs.Add((def.TileId, Direction.Invalid), regionList);
// Edges
if (def.EdgeSprites.Count <= 0)
continue;
foreach (var direction in DirectionExtensions.AllDirections)
{
if (!def.EdgeSprites.TryGetValue(direction, out var edge))
continue;
using (var stream = _manager.ContentFileRead(edge))
{
image = Image.Load<Rgba32>(stream);
}
if (image.Width != tileSize || image.Height != tileSize)
{
throw new NotSupportedException(
$"Unable to load {path}, due to being unable to use tile textures with a dimension other than {tileSize}x{tileSize}.");
}
Angle angle = Angle.Zero;
switch (direction)
{
// Corner sprites
case Direction.SouthEast:
break;
case Direction.NorthEast:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.NorthWest:
angle = new Angle(MathF.PI);
break;
case Direction.SouthWest:
angle = new Angle(MathF.PI * 1.5f);
break;
// Edge sprites
case Direction.South:
break;
case Direction.East:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.North:
angle = new Angle(MathF.PI);
break;
case Direction.West:
angle = new Angle(MathF.PI * 1.5f);
break;
}
if (angle != Angle.Zero)
{
image.Mutate(o => o.Rotate((float)-angle.Degrees));
}
var point = new Vector2i(column * tileSize, row * tileSize);
var box = new UIBox2i(0, 0, tileSize, tileSize);
image.Blit(box, sheet, point);
// If you ever need edge variants then you could just bump this.
var edgeList = new Box2[1];
edgeList[0] = Box2.FromDimensions(
point.X / w, (h - point.Y - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h);
tileRegs.Add((def.TileId, direction), edgeList);
BumpColumn(ref row, ref column, dimensionX);
}
}
_tileRegions = tileRegs.ToFrozenDictionary();
_tileTextureAtlas = Texture.LoadFromImage(sheet, "Tile Atlas");
_sawmill.Debug($"Tile atlas took {sw.Elapsed} to build");
}
private void BumpColumn(ref int row, ref int column, int dimensionX)
{
column++;
if (column >= dimensionX)
{
column = 0;
row++;
}
}
void IPostInjectInit.PostInject()

View File

@@ -29,5 +29,12 @@ namespace Robust.Client.Map
/// </summary>
/// <returns>If null, do not draw the tile at all.</returns>
Box2[]? TileAtlasRegion(int tileType);
/// <summary>
/// Gets the region inside the texture atlas to use to draw a tile type.
/// Also handles edge sprites.
/// </summary>
/// <returns>If null, do not draw the tile at all.</returns>
public Box2[]? TileAtlasRegion(int tileType, Direction direction);
}
}

View File

@@ -1,125 +0,0 @@
using System;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Map;
/// <summary>
/// Draws border sprites for tiles that support them.
/// </summary>
public sealed class TileEdgeOverlay : GridOverlay
{
private readonly IEntityManager _entManager;
private readonly IResourceCache _resource;
private readonly ITileDefinitionManager _tileDefManager;
public TileEdgeOverlay(IEntityManager entManager, IResourceCache resource, ITileDefinitionManager tileDefManager)
{
_entManager = entManager;
_resource = resource;
_tileDefManager = tileDefManager;
ZIndex = -1;
}
protected internal override void Draw(in OverlayDrawArgs args)
{
if (args.MapId == MapId.Nullspace)
return;
var mapSystem = _entManager.System<SharedMapSystem>();
var xformSystem = _entManager.System<SharedTransformSystem>();
var tileSize = Grid.Comp.TileSize;
var tileDimensions = new Vector2(tileSize, tileSize);
var (_, _, worldMatrix, invMatrix) = xformSystem.GetWorldPositionRotationMatrixWithInv(Grid.Owner);
args.WorldHandle.SetTransform(worldMatrix);
var bounds = args.WorldBounds;
bounds = new Box2Rotated(bounds.Box.Enlarged(1), bounds.Rotation, bounds.Origin);
var localAABB = invMatrix.TransformBox(bounds);
var enumerator = mapSystem.GetLocalTilesEnumerator(Grid.Owner, Grid, localAABB, false);
while (enumerator.MoveNext(out var tileRef))
{
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
if (tileDef.EdgeSprites.Count == 0)
continue;
// Get what tiles border us to determine what sprites we need to draw.
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x == 0 && y == 0)
continue;
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
var neighborTile = mapSystem.GetTileRef(Grid.Owner, Grid, neighborIndices);
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
// If it's the same tile then no edge to be drawn.
if (tileRef.Tile.TypeId == neighborTile.Tile.TypeId)
continue;
// Don't draw if the the neighbor tile edges should draw over us (or if we have the same priority)
if (neighborDef.EdgeSprites.Count != 0 && neighborDef.EdgeSpritePriority >= tileDef.EdgeSpritePriority)
continue;
var direction = new Vector2i(x, y).AsDirection();
// No edge tile
if (!tileDef.EdgeSprites.TryGetValue(direction, out var edgePath))
continue;
var texture = _resource.GetResource<TextureResource>(edgePath);
var box = Box2.FromDimensions(neighborIndices, tileDimensions);
var angle = Angle.Zero;
// If we ever need one for both cardinals and corners then update this.
switch (direction)
{
// Corner sprites
case Direction.SouthEast:
break;
case Direction.NorthEast:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.NorthWest:
angle = new Angle(MathF.PI);
break;
case Direction.SouthWest:
angle = new Angle(MathF.PI * 1.5f);
break;
// Edge sprites
case Direction.South:
break;
case Direction.East:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.North:
angle = new Angle(MathF.PI);
break;
case Direction.West:
angle = new Angle(MathF.PI * 1.5f);
break;
}
if (angle == Angle.Zero)
args.WorldHandle.DrawTextureRect(texture.Texture, box);
else
args.WorldHandle.DrawTextureRect(texture.Texture, new Box2Rotated(box, angle, box.Center));
RequiresFlush = true;
}
}
}
args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
}

View File

@@ -93,7 +93,7 @@ public sealed partial class PhysicsSystem
var maps = new HashSet<EntityUid>();
var enumerator = AllEntityQuery<PredictedPhysicsComponent, PhysicsComponent, TransformComponent>();
while (enumerator.MoveNext(out var _, out var physics, out var xform))
while (enumerator.MoveNext(out _, out var physics, out var xform))
{
DebugTools.Assert(physics.Predict);

View File

@@ -144,7 +144,7 @@ namespace Robust.Client.Placement
if (mousePos.MapId == MapId.Nullspace)
yield break;
var (_, (x, y)) = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
var (_, (x, y)) = transformSys.ToCoordinates(pManager.StartPoint.EntityId, mousePos) - pManager.StartPoint;
float iterations;
Vector2 distance;
if (Math.Abs(x) > Math.Abs(y))
@@ -176,7 +176,7 @@ namespace Robust.Client.Placement
if (mousePos.MapId == MapId.Nullspace)
yield break;
var placementdiff = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
var placementdiff = transformSys.ToCoordinates(pManager.StartPoint.EntityId, mousePos) - pManager.StartPoint;
var xSign = Math.Sign(placementdiff.X);
var ySign = Math.Sign(placementdiff.Y);
@@ -264,13 +264,15 @@ namespace Robust.Client.Placement
protected EntityCoordinates ScreenToCursorGrid(ScreenCoordinates coords)
{
var mapCoords = pManager.EyeManager.PixelToMap(coords.Position);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid))
{
return EntityCoordinates.FromMap(pManager.MapManager, mapCoords);
return transformSys.ToCoordinates(mapCoords);
}
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
return EntityCoordinates.FromMap(gridUid, mapCoords, transformSys, pManager.EntityManager);
return transformSys.ToCoordinates(gridUid, mapCoords);
}
}
}

View File

@@ -1,35 +1,23 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using Robust.Client.Graphics;
using Robust.Client.Timing;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Client.Utility;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
namespace Robust.Client.Prototypes
{
public sealed class ClientPrototypeManager : PrototypeManager
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IClientGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameControllerInternal _controller = default!;
private readonly List<FileSystemWatcher> _watchers = new();
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
private CancellationTokenSource _reloadToken = new();
private readonly HashSet<ResPath> _reloadQueue = new();
[Dependency] private readonly IReloadManager _reload = default!;
public override void Initialize()
{
@@ -37,9 +25,8 @@ namespace Robust.Client.Prototypes
_netManager.RegisterNetMessage<MsgReloadPrototypes>(accept: NetMessageAccept.Server);
_clyde.OnWindowFocused += WindowFocusedChanged;
WatchResources();
_reload.Register("/Prototypes", "*.yml");
_reload.OnChanged += ReloadPrototypeQueue;
}
public override void LoadDefaultPrototypes(Dictionary<Type, HashSet<string>>? changed = null)
@@ -49,99 +36,27 @@ namespace Robust.Client.Prototypes
ResolveResults();
}
private void WindowFocusedChanged(WindowFocusedEventArgs args)
private void ReloadPrototypeQueue(ResPath file)
{
#if TOOLS
if (args.Focused && _reloadQueue.Count > 0)
{
Timer.Spawn(_reloadDelay, ReloadPrototypeQueue, _reloadToken.Token);
}
else
{
_reloadToken.Cancel();
_reloadToken = new CancellationTokenSource();
}
#endif
}
if (file.Extension != "yml")
return;
private void ReloadPrototypeQueue()
{
#if TOOLS
var sw = Stopwatch.StartNew();
var msg = new MsgReloadPrototypes();
msg.Paths = _reloadQueue.ToArray();
var msg = new MsgReloadPrototypes
{
Paths = [file]
};
_netManager.ClientSendMessage(msg);
// Reloading prototypes modifies entities. This currently causes some state management debug asserts to
// fail. To avoid this, we set `IGameTiming.ApplyingState` to true, even though this isn't really applying a
// server state.
using var _ = _timing.StartStateApplicationArea();
ReloadPrototypes(_reloadQueue);
_reloadQueue.Clear();
ReloadPrototypes([file]);
Logger.Info($"Reloaded prototypes in {sw.ElapsedMilliseconds} ms");
#endif
}
private void WatchResources()
{
if (!_cfg.GetCVar(CVars.ResPrototypeReloadWatch))
return;
#if TOOLS
foreach (var path in Resources.GetContentRoots().Select(r => r.ToString())
.Where(r => Directory.Exists(r + "/Prototypes")).Select(p => p + "/Prototypes"))
{
var watcher = new FileSystemWatcher(path, "*.yml")
{
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.LastWrite
};
watcher.Changed += (_, args) =>
{
switch (args.ChangeType)
{
case WatcherChangeTypes.Renamed:
case WatcherChangeTypes.Deleted:
return;
case WatcherChangeTypes.Created:
// case WatcherChangeTypes.Deleted:
case WatcherChangeTypes.Changed:
case WatcherChangeTypes.All:
break;
default:
throw new ArgumentOutOfRangeException();
}
TaskManager.RunOnMainThread(() =>
{
var file = new ResPath(args.FullPath);
foreach (var root in Resources.GetContentRoots())
{
if (!file.TryRelativeTo(root, out var relative))
{
continue;
}
_reloadQueue.Add(relative.Value);
}
});
};
try
{
watcher.EnableRaisingEvents = true;
_watchers.Add(watcher);
}
catch (IOException ex)
{
Logger.Error($"Watching resources in path {path} threw an exception:\n{ex}");
}
}
#endif
}
}

View File

@@ -352,6 +352,9 @@ public sealed partial class ReplayLoadManager
// prototype changes when jumping around in time. This also requires reworking how the initial
// implicit state data is generated, because we can't simply cache it anymore.
// Also, does reloading prototypes in release mode modify existing entities?
// Yes, yes it does. See PrototypeReloadSystem.UpdateEntity()
// Its just not supported ATM.
// TBH it'd be easier if overriding existing prototypes in release mode was just forbidden.
var msg = $"Overwriting an existing prototype! Kind: {kind.Name}. Ids: {string.Join(", ", ids)}";
if (_confMan.GetCVar(CVars.ReplayIgnoreErrors))
@@ -361,7 +364,6 @@ public sealed partial class ReplayLoadManager
}
}
_protoMan.ResolveResults();
_protoMan.ReloadPrototypes(changed);
_locMan.ReloadLocalizations();
}

View File

@@ -44,8 +44,8 @@ internal sealed partial class ReplayPlaybackManager
_gameState.UpdateFullRep(state, cloneDelta: true);
var next = Replay.NextState;
BeforeApplyState?.Invoke((state, next));
var created = _gameState.ApplyGameState(state, next);
_gameState.GenerateImplicitStates(created);
_gameState.ApplyGameState(state, next);
_gameState.MergeImplicitData();
DebugTools.Assert(Replay.LastApplied >= state.FromSequence);
DebugTools.Assert(Replay.LastApplied + 1 <= state.ToSequence);
Replay.LastApplied = state.ToSequence;

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Threading;
using Robust.Client.Audio;
using Robust.Shared.Audio;
using Robust.Shared.ContentPack;
@@ -26,22 +27,26 @@ public sealed class AudioResource : BaseResource
throw new FileNotFoundException("Content file does not exist for audio sample.");
}
using (var fileStream = cache.ContentFileRead(path))
using var fileStream = cache.ContentFileRead(path);
var audioManager = dependencies.Resolve<IAudioInternal>();
if (path.Extension == "ogg")
{
var audioManager = dependencies.Resolve<IAudioInternal>();
if (path.Extension == "ogg")
{
AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
}
else if (path.Extension == "wav")
{
AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
}
else
{
throw new NotSupportedException("Unable to load audio files outside of ogg Vorbis or PCM wav");
}
AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
}
else if (path.Extension == "wav")
{
AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
}
else
{
throw new NotSupportedException("Unable to load audio files outside of ogg Vorbis or PCM wav");
}
}
public override void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default)
{
dependencies.Resolve<IAudioInternal>().Remove(AudioStream);
Load(dependencies, path);
}
public AudioResource(AudioStream stream) : base()

View File

@@ -1,4 +1,5 @@
using System;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects;
@@ -6,14 +7,63 @@ namespace Robust.Client.UserInterface;
public static class BoundUserInterfaceExt
{
private static T GetWindow<T>(BoundUserInterface bui) where T : BaseWindow, new()
{
var window = bui.CreateDisposableControl<T>();
window.OnClose += bui.Close;
var system = bui.EntMan.System<UserInterfaceSystem>();
system.RegisterControl(bui, window);
return window;
}
/// <summary>
/// Helper method to create a window and also handle closing the BUI when it's closed.
/// </summary>
public static T CreateWindow<T>(this BoundUserInterface bui) where T : BaseWindow, new()
{
var window = bui.CreateDisposableControl<T>();
window.OpenCentered();
window.OnClose += bui.Close;
var window = GetWindow<T>(bui);
if (bui.EntMan.System<UserInterfaceSystem>().TryGetPosition(bui.Owner, bui.UiKey, out var position))
{
window.Open(position);
}
else
{
window.OpenCentered();
}
return window;
}
public static T CreateWindowCenteredLeft<T>(this BoundUserInterface bui) where T : BaseWindow, new()
{
var window = GetWindow<T>(bui);
if (bui.EntMan.System<UserInterfaceSystem>().TryGetPosition(bui.Owner, bui.UiKey, out var position))
{
window.Open(position);
}
else
{
window.OpenCenteredLeft();
}
return window;
}
public static T CreateWindowCenteredRight<T>(this BoundUserInterface bui) where T : BaseWindow, new()
{
var window = GetWindow<T>(bui);
if (bui.EntMan.System<UserInterfaceSystem>().TryGetPosition(bui.Owner, bui.UiKey, out var position))
{
window.Open(position);
}
else
{
window.OpenCenteredRight();
}
return window;
}

View File

@@ -128,7 +128,7 @@ namespace Robust.Client.UserInterface
UserInterfaceManagerInternal.QueueStyleUpdate(this);
}
internal void StyleSheetUpdate()
public void InvalidateStyleSheet()
{
_stylesheetUpdateNeeded = true;
@@ -137,7 +137,7 @@ namespace Robust.Client.UserInterface
internal void StylesheetUpdateRecursive()
{
StyleSheetUpdate();
InvalidateStyleSheet();
foreach (var child in Children)
{
@@ -149,7 +149,7 @@ namespace Robust.Client.UserInterface
}
}
internal void DoStyleUpdate()
public void DoStyleUpdate()
{
_styleProperties.Clear();

View File

@@ -549,7 +549,7 @@ namespace Robust.Client.UserInterface
{
}
internal virtual void DrawInternal(IRenderHandle renderHandle)
protected internal virtual void Draw(IRenderHandle renderHandle)
{
Draw(renderHandle.DrawingHandleScreen);
}

View File

@@ -87,7 +87,6 @@ public sealed class TileSpawningUIController : UIController
return;
_window = UIManager.CreateWindow<TileSpawnWindow>();
LayoutContainer.SetAnchorPreset(_window,LayoutContainer.LayoutPreset.CenterLeft);
_window.SearchBar.GrabKeyboardFocus();
_window.ClearButton.OnPressed += OnTileClearPressed;
_window.SearchBar.OnTextChanged += OnTileSearchChanged;
_window.TileList.OnItemSelected += OnTileItemSelected;

View File

@@ -71,21 +71,11 @@ namespace Robust.Client.UserInterface.Controls
}
// First, we measure non-stretching children.
var stretching = new List<Control>();
float totalStretchRatio = 0;
foreach (var child in Children)
{
if (!child.Visible)
continue;
var stretch = Vertical ? child.VerticalExpand : child.HorizontalExpand;
if (stretch)
{
totalStretchRatio += child.SizeFlagsStretchRatio;
stretching.Add(child);
continue;
}
child.Measure(availableSize);
if (Vertical)
@@ -102,35 +92,6 @@ namespace Robust.Client.UserInterface.Controls
}
}
if (stretching.Count == 0)
return desiredSize;
// Then we measure stretching children
foreach (var child in stretching)
{
var size = availableSize;
if (Vertical)
{
size.Y *= child.SizeFlagsStretchRatio / totalStretchRatio;
child.Measure(size);
desiredSize.Y += child.DesiredSize.Y;
desiredSize.X = Math.Max(desiredSize.X, child.DesiredSize.X);
}
else
{
size.X *= child.SizeFlagsStretchRatio / totalStretchRatio;
child.Measure(size);
desiredSize.X += child.DesiredSize.X;
desiredSize.Y = Math.Max(desiredSize.Y, child.DesiredSize.Y);
}
// TODO Maybe make BoxContainer.MeasureOverride more rigorous.
// This should check if size < desired size. If it is, treat child as non-stretching (see the code in
// ArrangeOverride). This requires remeasuring all stretching controls + the control that just became
// non-stretching. But the re-measured controls might then become smaller (e.g. rich text wrapping),
// leading to a recursion problem.
}
return desiredSize;
}

View File

@@ -9,6 +9,7 @@ public class EntityPrototypeView : SpriteView
{
private string? _currentPrototype;
private EntityUid? _ourEntity;
private bool _isShowing;
public EntityPrototypeView()
{
@@ -22,8 +23,6 @@ public class EntityPrototypeView : SpriteView
public void SetPrototype(EntProtoId? entProto)
{
SpriteSystem ??= EntMan.System<SpriteSystem>();
if (entProto == _currentPrototype
&& EntMan.TryGetComponent(Entity?.Owner, out MetaDataComponent? meta)
&& meta.EntityPrototype?.ID == _currentPrototype)
@@ -32,14 +31,10 @@ public class EntityPrototypeView : SpriteView
}
_currentPrototype = entProto;
SetEntity(null);
EntMan.DeleteEntity(_ourEntity);
if (_currentPrototype != null)
if (_ourEntity != null || _isShowing)
{
_ourEntity = EntMan.Spawn(_currentPrototype);
SpriteSystem.ForceUpdate(_ourEntity.Value);
SetEntity(_ourEntity);
UpdateEntity();
}
}
@@ -48,12 +43,38 @@ public class EntityPrototypeView : SpriteView
base.EnteredTree();
if (_currentPrototype != null)
SetPrototype(_currentPrototype);
{
UpdateEntity();
}
_isShowing = true;
}
protected override void ExitedTree()
{
base.ExitedTree();
EntMan.TryQueueDeleteEntity(_ourEntity);
_ourEntity = null;
_isShowing = false;
}
private void UpdateEntity()
{
SetEntity(null);
EntMan.DeleteEntity(_ourEntity);
if (_currentPrototype != null)
{
SpriteSystem ??= EntMan.System<SpriteSystem>();
_ourEntity = EntMan.Spawn(_currentPrototype);
SpriteSystem.ForceUpdate(_ourEntity.Value);
SetEntity(_ourEntity);
}
else
{
_ourEntity = null;
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
@@ -176,6 +176,71 @@ namespace Robust.Client.UserInterface.Controls
_scrollBar.MoveToEnd();
}
/// <summary>
/// Replace the current list of items with the items in newItems.
/// newItems should be in the order which they should appear in the list,
/// and items are considered equal if the Item text is equal in each item.
///
/// Provided the existing items have not been re-ordered relative to each
/// other, any items which already exist in the list are not destroyed,
/// which maintains consistency of scrollbars, selected items, etc.
/// </summary>
/// <param name="newItems">The list of items to update this list to</param>
public void SetItems(List<Item> newItems)
{
SetItems(newItems, (a,b) => string.Compare(a.Text, b.Text));
}
/// <inheritdoc />
/// <summary>
/// This variant allows for a custom equality operator to compare items, when
/// comparing the Item text is not desired.
/// </summary>
/// <param name="itemCmp">Comparison function to compare existing to new items.</param>
public void SetItems(List<Item> newItems, Comparison<Item> itemCmp)
{
// Walk through the existing items in this list and in newItems
// in parallel to synchronize our items with those in newItems.
int i = this.Count - 1;
int j = newItems.Count - 1;
while(i >= 0 && j >= 0)
{
var cmpResult = itemCmp(this[i], newItems[j]);
if (cmpResult == 0)
{
// This item exists in both our list and `newItems`. Nothing to do.
i--;
j--;
}
else if (cmpResult > 0)
{
// Item exists in our list, but not in `newItems`. Remove it.
RemoveAt(i);
i--;
}
else if (cmpResult < 0)
{
// A new entry which doesn't exist in our list. Insert it.
Insert(i + 1, newItems[j]);
j--;
}
}
// Any remaining items in our list don't exist in `newItems` so remove them
while (i >= 0)
{
RemoveAt(i);
i--;
}
// And finally, any remaining items in `newItems` don't exist in our list. Create them.
while (j >= 0)
{
Insert(0, newItems[j]);
j--;
}
}
// Without this attribute, this would compile into a property called "Item", causing problems with the Item class.
[System.Runtime.CompilerServices.IndexerName("IndexItem")]
public Item this[int index]

View File

@@ -21,7 +21,6 @@ namespace Robust.Client.UserInterface.Controls
[Virtual]
public class LineEdit : Control
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;

View File

@@ -4,6 +4,7 @@ using Robust.Client.Graphics;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Collections;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -15,10 +16,23 @@ namespace Robust.Client.UserInterface.Controls
[Virtual]
public class OutputPanel : Control
{
public const string StyleClassOutputPanelScrollDownButton = "outputPanelScrollDownButton";
[Dependency] private readonly MarkupTagManager _tagManager = default!;
public const string StylePropertyStyleBox = "stylebox";
public bool ShowScrollDownButton
{
get => _showScrollDownButton;
set
{
_showScrollDownButton = value;
_updateScrollButtonVisibility();
}
}
private bool _showScrollDownButton;
private readonly RingBufferList<RichTextEntry> _entries = new();
private bool _isAtBottom = true;
@@ -26,6 +40,7 @@ namespace Robust.Client.UserInterface.Controls
private bool _firstLine = true;
private StyleBox? _styleBoxOverride;
private VScrollBar _scrollBar;
private Button _scrollDownButton;
public bool ScrollFollowing { get; set; } = true;
@@ -43,7 +58,25 @@ namespace Robust.Client.UserInterface.Controls
HorizontalAlignment = HAlignment.Right
};
AddChild(_scrollBar);
_scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
AddChild(_scrollDownButton = new Button()
{
Name = "scrollLiveBtn",
StyleClasses = { StyleClassOutputPanelScrollDownButton },
VerticalAlignment = VAlignment.Bottom,
HorizontalAlignment = HAlignment.Center,
Text = String.Format("⬇ {0} ⬇", Loc.GetString("output-panel-scroll-down-button-text")),
MaxWidth = 300,
Visible = false,
});
_scrollDownButton.OnPressed += _ => ScrollToBottom();
_scrollBar.OnValueChanged += _ =>
{
_isAtBottom = _scrollBar.IsAtEnd;
_updateScrollButtonVisibility();
};
}
public int EntryCount => _entries.Count;
@@ -184,6 +217,7 @@ namespace Robust.Client.UserInterface.Controls
var styleBoxSize = _getStyleBox()?.MinimumSize.Y ?? 0;
_scrollBar.Page = UIScale * (Height - styleBoxSize);
_updateScrollButtonVisibility();
_invalidateEntries();
}
@@ -284,5 +318,10 @@ namespace Robust.Client.UserInterface.Controls
_invalidOnVisible = false;
}
}
private void _updateScrollButtonVisibility()
{
_scrollDownButton.Visible = ShowScrollDownButton && !_isAtBottom;
}
}
}

View File

@@ -28,6 +28,30 @@ namespace Robust.Client.UserInterface.Controls
public int ScrollSpeedX { get; set; } = 50;
public int ScrollSpeedY { get; set; } = 50;
public float VScroll
{
get => _vScrollBar.Value;
set => _vScrollBar.Value = value;
}
public float VScrollTarget
{
get => _vScrollBar.ValueTarget;
set => _vScrollBar.ValueTarget = value;
}
public float HScroll
{
get => _hScrollBar.Value;
set => _hScrollBar.Value = value;
}
public float HScrollTarget
{
get => _hScrollBar.ValueTarget;
set => _hScrollBar.ValueTarget = value;
}
private bool _reserveScrollbarSpace;
public bool ReserveScrollbarSpace
{

View File

@@ -224,7 +224,7 @@ namespace Robust.Client.UserInterface.Controls
_spriteSize = new Vector2(longestRotatedSide, longestRotatedSide);
}
internal override void DrawInternal(IRenderHandle renderHandle)
protected internal override void Draw(IRenderHandle renderHandle)
{
if (!ResolveEntity(out var uid, out var sprite, out var xform))
return;

View File

@@ -38,7 +38,6 @@ namespace Robust.Client.UserInterface.Controls;
public sealed class TextEdit : Control
{
[Dependency] private readonly IClipboardManager _clipboard = null!;
[Dependency] private readonly IClyde _clyde = null!;
// @formatter:off
public const string StylePropertyCursorColor = "cursor-color";

View File

@@ -5,6 +5,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
@@ -102,6 +103,8 @@ namespace Robust.Client.UserInterface.CustomControls
{
var cursor = CursorShape.Arrow;
var previewDragMode = GetDragModeFor(args.RelativePosition);
previewDragMode &= ~DragMode.Move;
switch (previewDragMode)
{
case DragMode.Top:
@@ -116,9 +119,6 @@ namespace Robust.Client.UserInterface.CustomControls
case DragMode.Bottom | DragMode.Left:
case DragMode.Top | DragMode.Right:
cursor = CursorShape.Crosshair;
break;
case DragMode.Bottom | DragMode.Right:
case DragMode.Top | DragMode.Left:
cursor = CursorShape.Crosshair;
@@ -160,15 +160,6 @@ namespace Robust.Client.UserInterface.CustomControls
var rect = new UIBox2(left, top, right, bottom);
LayoutContainer.SetPosition(this, rect.TopLeft);
SetSize = rect.Size;
/*
var timing = IoCManager.Resolve<IGameTiming>();
var l = GetValue<float>(LayoutContainer.MarginLeftProperty);
var t = GetValue<float>(LayoutContainer.MarginTopProperty);
Logger.Debug($"{timing.CurFrame}: {rect.TopLeft}/({l}, {t}), {rect.Size}/{SetSize}");
*/
}
}
@@ -229,6 +220,16 @@ namespace Robust.Client.UserInterface.CustomControls
OnOpen?.Invoke();
}
/// <summary>
/// Opens the window and places it at the specified position.
/// </summary>
public void Open(Vector2 position)
{
Measure(Vector2Helpers.Infinity);
Open();
LayoutContainer.SetPosition(this, position);
}
public void OpenCentered() => OpenCenteredAt(new Vector2(0.5f, 0.5f));
public void OpenToLeft() => OpenCenteredAt(new Vector2(0, 0.5f));

View File

@@ -1,7 +1,7 @@
<Control xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics">
<BoxContainer Orientation="Vertical">
<OutputPanel Name="Output" VerticalExpand="True" StyleClasses="monospace">
<OutputPanel Name="Output" VerticalExpand="True" StyleClasses="monospace" ShowScrollDownButton="True">
<OutputPanel.StyleBoxOverride>
<gfx:StyleBoxFlat BackgroundColor="#25252add"
ContentMarginLeftOverride="3" ContentMarginRightOverride="3"

View File

@@ -59,6 +59,7 @@ namespace Robust.Client.UserInterface.CustomControls
}
button.EntityLabel.Text = entityLabelText;
button.ActualButton.ToolTip = prototype.Description;
if (prototype == SelectedPrototype)
{

View File

@@ -10,4 +10,11 @@ public sealed partial class TileSpawnWindow : DefaultWindow
{
RobustXamlLoader.Load(this);
}
protected override void Opened()
{
base.Opened();
SearchBar.GrabKeyboardFocus();
}
}

View File

@@ -65,13 +65,13 @@ namespace Robust.Client.UserInterface.CustomControls
// -- Handlers: Out --
protected internal override void Draw(DrawingHandleScreen handle)
protected internal override void Draw(IRenderHandle handle)
{
base.Draw(handle);
if (Viewport == null)
{
handle.DrawRect(UIBox2.FromDimensions(new Vector2(0, 0), Size * UIScale), Color.Red);
handle.DrawingHandleScreen.DrawRect(UIBox2.FromDimensions(new Vector2(0, 0), Size * UIScale), Color.Red);
}
else
{
@@ -82,7 +82,7 @@ namespace Robust.Client.UserInterface.CustomControls
Viewport.RenderScreenOverlaysBelow(handle, this, viewportBounds);
Viewport.Render();
handle.DrawTextureRect(Viewport.RenderTarget.Texture,
handle.DrawingHandleScreen.DrawTextureRect(Viewport.RenderTarget.Texture,
UIBox2.FromDimensions(new Vector2(0, 0), (Vector2i) (Viewport.Size / _viewportResolution)));
Viewport.RenderScreenOverlaysAbove(handle, this, viewportBounds);

View File

@@ -7,6 +7,7 @@ using Robust.Client.UserInterface.CustomControls;
using Robust.Shared;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface
{
@@ -146,6 +147,16 @@ namespace Robust.Client.UserInterface
/// but not necessarily a new or existing control is rearranged.
/// </summary>
void UpdateHovered();
/// <summary>
/// Render a control and all of its children.
/// </summary>
void RenderControl(in Control.ControlRenderArguments args, Control control);
/// <summary>
/// Render a control and all of its children.
/// </summary>
void RenderControl(IRenderHandle handle, Control control, Vector2i position);
}
public readonly struct PostDrawUIRootEventArgs

View File

@@ -4,7 +4,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
[Prototype("font")]
[Prototype]
public sealed partial class FontPrototype : IPrototype
{
[IdDataField]

View File

@@ -313,7 +313,10 @@ internal partial class UserInterfaceManager
private void _clearTooltip()
{
if (!_showingTooltip) return;
_resetTooltipTimer();
if (!_showingTooltip)
return;
if (_suppliedTooltip != null)
{
@@ -322,7 +325,6 @@ internal partial class UserInterfaceManager
}
CurrentlyHovered?.PerformHideTooltip();
_resetTooltipTimer();
_showingTooltip = false;
}

View File

@@ -37,7 +37,7 @@ internal sealed partial class UserInterfaceManager
_roots.Add(newRoot);
_windowsToRoot.Add(window.Id, newRoot);
newRoot.StyleSheetUpdate();
newRoot.InvalidateStyleSheet();
newRoot.InvalidateMeasure();
QueueMeasureUpdate(newRoot);
QueueArrangeUpdate(newRoot);

View File

@@ -335,6 +335,30 @@ namespace Robust.Client.UserInterface
}
}
public void RenderControl(in Control.ControlRenderArguments args, Control control)
{
var _ = 0;
RenderControl(args.Handle,
ref _,
control,
args.Position,
args.Modulate,
args.ScissorBox,
args.CoordinateTransform);
}
public void RenderControl(IRenderHandle handle, Control control, Vector2i position)
{
var _ = 0;
RenderControl(handle,
ref _,
control,
position,
Color.White,
null,
Matrix3x2.Identity);
}
public void RenderControl(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
UIBox2i? scissorBox, Matrix3x2 coordinateTransform)
{
@@ -393,7 +417,7 @@ namespace Robust.Client.UserInterface
// Handle modulation with care.
var oldMod = handle.Modulate;
handle.Modulate = modulate * control.ActualModulateSelf;
control.DrawInternal(renderHandle);
control.Draw(renderHandle);
handle.Modulate = oldMod;
handle.UseShader(null);
}
@@ -409,10 +433,11 @@ namespace Robust.Client.UserInterface
control.PreRenderChildren(ref args);
foreach (var child in control.Children)
for (var index = 0; index < control.ChildCount; index++)
{
var child = control.GetChild(index);
var pos = position + (Vector2i)Vector2.Transform(child.PixelPosition, coordinateTransform);
control.RenderChildOverride(ref args, child.GetPositionInParent(), pos);
control.RenderChildOverride(ref args, index, pos);
}
control.PostRenderChildren(ref args);

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Robust.Client.Graphics;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
namespace Robust.Client.Utility;
internal sealed class ReloadManager : IReloadManager
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILogManager _logMan = default!;
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly ITaskManager _tasks = default!;
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
private CancellationTokenSource _reloadToken = new();
private readonly HashSet<ResPath> _reloadQueue = new();
private List<FileSystemWatcher> _watchers = new();
public event Action<ResPath>? OnChanged;
private ISawmill _sawmill = default!;
public void Initialize()
{
_sawmill = _logMan.GetSawmill("reload");
_clyde.OnWindowFocused += WindowFocusedChanged;
}
private void WindowFocusedChanged(WindowFocusedEventArgs args)
{
#if TOOLS
if (args.Focused && _reloadQueue.Count > 0)
{
Timer.Spawn(_reloadDelay, ReloadFiles, _reloadToken.Token);
}
else
{
_reloadToken.Cancel();
_reloadToken = new CancellationTokenSource();
}
#endif
}
private void ReloadFiles()
{
foreach (var file in _reloadQueue)
{
var rootedFile = file.ToRootedPath();
if (!_res.ContentFileExists(rootedFile))
continue;
_sawmill.Info($"Reloading {rootedFile}");
OnChanged?.Invoke(rootedFile);
}
_reloadQueue.Clear();
}
public void Register(string directory, string filter)
{
if (!_cfg.GetCVar(CVars.ResPrototypeReloadWatch))
return;
#if TOOLS
foreach (var root in _res.GetContentRoots())
{
var path = root + directory;
if (!Directory.Exists(path))
{
continue;
}
var watcher = new FileSystemWatcher(path, filter)
{
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.LastWrite
};
_watchers.Add(watcher);
watcher.Changed += OnWatch;
try
{
watcher.EnableRaisingEvents = true;
}
catch (IOException ex)
{
Logger.Error($"Watching resources in path {path} threw an exception:\n{ex}");
}
}
void OnWatch(object sender, FileSystemEventArgs args)
{
switch (args.ChangeType)
{
case WatcherChangeTypes.Renamed:
case WatcherChangeTypes.Deleted:
return;
case WatcherChangeTypes.Created:
// case WatcherChangeTypes.Deleted:
case WatcherChangeTypes.Changed:
case WatcherChangeTypes.All:
break;
default:
throw new ArgumentOutOfRangeException();
}
_tasks.RunOnMainThread(() =>
{
var fullPath = args.FullPath.Replace(Path.DirectorySeparatorChar, '/');
var file = new ResPath(fullPath);
foreach (var rootIter in _res.GetContentRoots())
{
if (!file.TryRelativeTo(rootIter, out var relative))
{
continue;
}
_reloadQueue.Add(relative.Value);
}
});
}
#endif
}
}

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