Compare commits

..

168 Commits

Author SHA1 Message Date
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
DrSmugleaf
e03aec47ef Fix component network generator not working for dicts with entityuid values but not keys 2025-01-21 19:09:22 -08:00
DrSmugleaf
ee906af16e Version: 240.1.2 2025-01-21 18:43:28 -08:00
wixoa
e205ae3627 Use the color arg when drawing a font character in world space (#5626) 2025-01-21 22:39:07 +01:00
Jerry
d818c5aa0c fix StatusHost RespondJsonAsync method (#5622) 2025-01-21 16:58:37 +01:00
ElectroJr
aa8fe8ac92 Version: 240.1.1 2025-01-20 14:16:39 +13:00
Leon Friedrich
4ba6687b9d Fix OverlayManager.RemoveOverlay (#5623) 2025-01-20 12:14:58 +11:00
ElectroJr
8f2817aa4e Version: 240.1.0 2025-01-20 13:04:19 +13:00
Leon Friedrich
89c7839fe2 Fix exception in DestroyContacts (#5619) 2025-01-19 16:25:58 +11:00
DrSmugleaf
9799132001 Make GamePrototypeLoadManager send a single message with all uploaded prototypes in a single message (#5616) 2025-01-19 11:49:27 +11:00
Milon
34ffa56c57 add AsNullable() extension method to Entity<T> (#5617)
* just works first try

* use a constructor instead
2025-01-18 20:52:18 +01:00
ElectroJr
f8410a4674 Version: 240.0.1 2025-01-18 16:54:03 +13:00
Leon Friedrich
43b991c690 Fix SharedBroadphaseSystem.GetBroadphases (#5615) 2025-01-18 14:37:54 +11:00
PJB3005
c463fc5e78 Make source generators emit EditorBrowsable(Never)
This hides the generated types from intellisense if you have the relevant option in Rider enabled.

Good because honestly these just bloat IntelliSense, and if you want to show them there's an IDE setting for it.
2025-01-18 01:23:35 +01:00
PJB3005
e21b3e069a Version: 240.0.0 2025-01-17 17:57:31 +01:00
PJB3005
c8f94ab40d Update release notes 2025-01-17 17:57:14 +01:00
Tornado Tech
2a882b5555 Moved Overlay sorting to OverlayManager (#5614)
* Moved Overlay sorting to OverlayManager

* Changed Add to AddRange
2025-01-17 17:47:13 +01:00
Leon Friedrich
32d8a1cba9 Misc grid state changes (#5597) 2025-01-17 17:07:09 +01:00
Leon Friedrich
5d84be9c78 Make ComponentRegistry not implement ISerializationContext (#5613) 2025-01-17 15:01:25 +01:00
PJB3005
eaaa70437a Reduce DynamicMethod use in Serv3
Data definitions created individual read/write methods for every single field that can be serialized. This was extremely inefficient and likely caused lots of overhead.

These methods are necessary because, in some cases, we can't directly use expression trees to write fields. But... we can still do it most of the time! So now for most data fields we can avoid a DynamicMethod entirely.
2025-01-16 15:10:35 +01:00
Leon Friedrich
448e8b0c2c Validate static EntProtoId<T> fields (#5593) 2025-01-16 10:36:47 +01:00
Leon Friedrich
42948d8f8e Fix autocompletion hint for toolshed strings (#5584)
* Fix autocompletion hint for toolshed strings

* Split functionality

* Remove combined flag
2025-01-16 10:30:42 +01:00
c4llv07e
039468f4b6 Add function to check if localization culture was already loaded. (#5603) 2025-01-16 10:28:15 +01:00
Leon Friedrich
6a3f88b1c6 Fix replay playback bugs (#5604)
* Sort replay entities

* Fix nameof(TState)

* I forgot to generate implicit states

* release notes
2025-01-16 10:26:53 +01:00
PJB3005
a314c5f797 Tickrate is now a ushort
Fixes #5592

This allows net.tickrate to be set to a max of 65535 instead of 255.

I didn't raise it fully to a uint because there are many places it's cast to an int, so uint would cause various compiler errors and compat issues I don't wanna deal with.
2025-01-16 01:13:50 +01:00
PJB3005
bcc4cd77cf Version: 239.0.1 2025-01-15 20:16:05 +01:00
PJB3005
941cb4c1d6 Release notes 2025-01-15 20:15:21 +01:00
Myra
7b58760331 Downgrade VorbisPizza back to 1.3.0 (#5607)
Somewhere between 1.3.0 and 1.4.0 (I tested all versions) exists a bug that for some reason messes with the audio on some devices.

A proper fix would require us trying to make contact with the developer of VorbisPizza, find the buggy commit and waiting them to update their repo. This repo has been inactive for 8 months. I doubt we can get support unless we fork it and find the bug ourselves.

Closes & fixes #5605
2025-01-15 17:18:23 +01:00
PJB3005
f0306b593a Optimize Robust.Serialization.Generator
Slightly less bad use of incremental generators.

Remove format of generated code, it was taking most of the CPU time.

This seems to significantly cut compile time cost of this generator. Nice. I'm seeing 20 -> 5 seconds on my system.
2025-01-15 02:41:57 +01:00
PJB3005
5e1935c310 Optimize ByRefEventAnalyzer
According to a log, like 20 seconds of the build is spent in this analyzer. It now takes ~200ms. Hooray!
2025-01-13 05:52:31 +01:00
PJB3005
67c44a5fc5 Add unit test for ByRefEventAnalyzer 2025-01-13 05:44:35 +01:00
PJB3005
3ea0a0244b Fix net.packet RECV logging 2025-01-10 18:09:27 +01:00
PJB3005
325a39ee4b Wow way to expose that my lazy ass didn't actually try running package_webview.py on this build. 2025-01-09 04:22:25 +01:00
PJB3005
e4190f4f29 Version: 239.0.0 2025-01-09 01:45:49 +01:00
PJB3005
4d163ed818 Update release notes 2025-01-09 01:44:16 +01:00
ike709
09c6a816e0 Bump cefglue (#5585)
* Bump cefglue

* another bump

* Update build-test.yml

* Update to latest CEF natives and fix code to be compatible.

* Switch CEF CreateBrowserWindow to Alloy style

you will NOT open Chrome

* Update cefglue, again

---------

Co-authored-by: ike709 <ike709@github.com>
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-01-08 02:32:24 +01:00
Pieter-Jan Briers
dfc4894c8b Dependencies update & cleanup (#5590)
* Dependencies update & cleanup

Fixes security vuln warnings etc

* Remove ILReader dependency

RIP in piss FastAccessors.
2025-01-08 02:19:27 +01:00
PJB3005
f2b096f145 Fix SDL3 backend
.NET 9 update changed library import generator behavior and it broke my funny shit.
2025-01-08 01:39:56 +01:00
PJB3005
1984e97d2f Show "null" in loglevel command completions
This was already supported, but the completions didn't list it.
2025-01-07 21:33:37 +01:00
PJB3005
57291b88c0 Log received/sent net messages at verbose level
This has always been in the code but commented out for debugging. Now it's actually logging to Verbose level. Also the log level is "net.packet" now instead of just "net". Also it logs received size too.

This makes use of the new IsLogLevelEnabled to avoid the perf overhead when not being used.
2025-01-07 21:11:56 +01:00
PJB3005
f40dd51648 Add ISawmill.IsLogLevelEnabled
So you can avoid logging things unless somebody's actually trying to debug something.
2025-01-07 20:54:14 +01:00
Leon Friedrich
d08fdd3a18 Fix component delta state auto-generation (#5589)
* Fix component delta state auto-generation

* re-use ApplyToFullState

* release notes
2025-01-07 16:54:59 +01:00
Leon Friedrich
80dbf02af4 Log errors when encountering entity data fields (#5578)
* Log errors when encountering entity data fields

* Cleanup & comments

* Remove unnecessary GetGenericTypeDefinition
2025-01-07 16:54:25 +01:00
IProduceWidgets
a2983a5ee0 safe dictionary assignment (#5587) 2025-01-06 21:02:02 +01:00
PJB3005
7810cd0c2e Disable CETCompat in Robust.Client.WebView
Guess what doesn't work with CEF (not that it works in the first place for us)
2025-01-06 03:15:36 +01:00
PJB3005
6c8b863731 Add string split with ROS<char> to sandbox 2025-01-06 03:08:28 +01:00
ike709
c2ca7c7811 .NET 9 (#5552)
Co-authored-by: ike709 <ike709@github.com>
2025-01-06 01:16:01 +01:00
PJB3005
347d240fae Add API for creating mock configuration managers in unit tests. 2025-01-05 23:27:59 +01:00
Pieter-Jan Briers
87a5745519 SDL3 (#5583)
* Start converting SDL2 backend to SDL3.

Game starts, but a lot of stuff is broken. Oh well.

* Fix text input

SDL3 changed the API somewhat, for the better. Changes all over UI/Clyde/SDL3 layer.

* Fix mouse buttons being broken

* Remove records from SDL3 WSI

The fact that this shaved 2-3% off Robust.Client.dll is mindboggling. Records are so bad.

* Set Windows/X11 native window properties

* Fix window resize events getting wrong size

oops

* Remove "using static" from SDL3 WSI

Seriously seems to hurt IDE performance, oh well.

* Apparently I never called CheckThreadApartment().

* Add STAThreadAttribute to sandbox

Necessary for content start

* Set window title on creation properly.

* Load window icons

* Fix GLFW NoTitleBar style handling

Yeah this PR is supposed to be about SDL3, so what?

* Implement more window creation settings in SDL3

Mostly the ones that need a lot of platform-specific stuff to work.

* Make fullscreen work properly in SDL3.

* File dialogs with SDL3

Removes need for swnfd.

* Fix some TODOs

* Fix WebView build
2025-01-03 18:42:57 +01:00
ElectroJr
e47ba0faea Version: 238.0.1 2024-12-28 18:51:38 +13:00
Southbridge
fb705702fb PlacementManager CurrentEraserMouseCoordinates fix (#5576) 2024-12-28 16:48:26 +11:00
sleepyyapril
a2aec44ebb Fix Build Error with Auto-networked EntityUid Dictionaries (#5569) 2024-12-22 21:12:44 +11:00
ElectroJr
5e97db435c Version: 238.0.0 2024-12-21 19:51:56 +13:00
Leon Friedrich
9af119f57a Toolshed Rejig (#5455)
* Toolshed Rejig

* shorten hint string

* Try fix conflicts. Ill make with work later

* bodge

* Fix ProtoIdTypeParser assert

* comment

* AllEntities

* Remove more linq from WhereCommand

* better help strings

* Add ContainsCommand

* loc strings

* Add contains command description

* Add $self variable

* Errors for writing to readonly variables

* A
2024-12-21 17:49:11 +11:00
MLGTASTICa
6247be2c84 Changes SharedGridTraversalSystem accesibility from internal to public (#5551)
* Make this public.

* Add warnings.
2024-12-21 17:11:18 +11:00
metalgearsloth
acb1d37b99 Version: 237.4.0 2024-12-21 15:53:21 +11:00
metalgearsloth
82c94fc8b0 transform traversals (#5564) 2024-12-21 15:49:43 +11:00
metalgearsloth
9837c33de7 Add sourcegenned field deltas (#5155)
* Remove full "delta" states

* Update MapGridComponentState

* abstract ComponentState

* Release notes

* Fix tests

* Fix nullable errors

* A

* Sourcegen component deltas

* Audio deltas + methids

* Also eye

* Optimise out the dictionary

* Minor fixes

* Physics deltas

* Also this

* Fix field deltas

* remove old release notes

* Make IComponentDelta implement IComponent

* add sourcegen launch settings

* make silent error loud

* Review

* UI deltas

* Slimmer

* Sourcegen bandaid

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2024-12-21 15:48:33 +11:00
MilenVolf
ac30ad1820 Replace all usages of obsolete MapGridComponent methods (#5547)
* Replace obsolete MapGridComponent methods

Some other obsolete methods were also replaced in edited files

* Whitespace and better readability

* Value instead of cast

* Fix ISawmill not initialized and incorrect log
2024-12-18 18:46:53 +01:00
metalgearsloth
9c30fdf5fd Version: 237.3.0 2024-12-16 16:33:24 +11:00
metalgearsloth
0b7e8c2560 Add shapecasts + raycasts (#5440)
* Add shapecasts + raycasts

Actual raycasts. Need this for AI LIDAR experiment.

* cassette

* more cudin

* Mostly ported

* more work

* More ports

* the big house

* rays

* builds

* Janky not working raycasts

* Fix GJK

* Test fixes

* Shapecast + fixes

* free

* tests

* More fixes

* Minor changes

* Not these

* Release notes
2024-12-16 16:07:24 +11:00
IProduceWidgets
7982aa236c TryFindComponentOnEntityContainerOrParent no fail please (#5322)
* TryFindComponentsOnEntityContainerOrParent no fail please

* remove my comments because Im dumb

* out makes sense to me!

* Revert "out makes sense to me!" because PJB no want breaky

This reverts commit 54f4a6d50c.
2024-12-14 17:16:43 +01:00
PJB3005
0559339143 Add AssetPassFilterDrop
Planning to use this to drop .svg files from SS14's resources folder. Tiny opt.
2024-12-14 17:07:57 +01:00
Zachary Higgs
89fcd1dd2b Add InterfaceData constructor (#5559)
* Add InterfaceData constructor

Add InterfaceData constructor to allow for dynamic UI assignment via
SharedUiSystem::setUi

* empty commit because of heisentest
2024-12-14 16:19:05 +01:00
metalgearsloth
649378e59a BUI helpers (#5558)
* BUI helpers

- Some virtual methods for BUI to make it slightly easier. Haven't though of a good way to do it via sourcegen yet.
- TryGetUiState which is occasionally useful.

* Also this one
2024-12-13 01:33:17 +01:00
SpaceManiac
0c7ace16d1 Fix most non-obsolete warnings (#5555) 2024-12-13 01:25:00 +01:00
metalgearsloth
27f7f5ee36 Add Pure attribute to some entmanager methods (#5557) 2024-12-12 18:24:20 +01:00
Pieter-Jan Briers
fe0fcbd851 Add RSI key to disable meta-atlas (#5544)
This allows huge textures (e.g. Ratvar) to be removed from the meta-atlas. This saves a significant chunk of VRAM from the meta atlas in SS14 (~126 MB) which might help a bit with low-VRAM systems.
2024-12-08 23:49:12 +01:00
Pieter-Jan Briers
aca7847933 Add CVars for privacy policy information (#5545)
* Add CVars for privacy policy information

Engine side of https://github.com/space-wizards/SS14.Launcher/issues/194

* Improve/fix cvar desc
2024-12-08 23:48:50 +01:00
Leon Friedrich
1621d25a92 Fix UserInterfaceSystem debug assert (#5546) 2024-11-30 13:09:51 +01:00
Amy
b7e0a9bc03 Make font drawing more generic (#5533)
* make richtextentry more generic

* font

* oops
2024-11-29 11:28:27 +01:00
metalgearsloth
9909416006 Fix grid container layout (#5543)
This decrements index and cooks the layout if controls are invisible.
2024-11-29 10:54:34 +01:00
Leon Friedrich
c3e487b61c Fix IPrototypeManager.TryGetKindFrom() (#5542) 2024-11-29 01:48:02 +01:00
Nikolai Korolev
89ad8b6c9f Upgrade GitHub actions in workflows (#5536)
* Upgrade github workflows to node20

* Fix incorrect version for actions/checkout in test-content.yml
2024-11-28 19:52:14 +01:00
Nikolai Korolev
efbc9ef2bf Fix codeql-analysis breaking in every fork repo that enables GitHub Actions (#5537) 2024-11-28 19:50:39 +01:00
Pieter-Jan Briers
ce240773e8 ValueList<T> extensions (#5534)
Stack-like functions. Just some code I had lying around and never committed.

Add ROS overload for AddRange
2024-11-28 19:49:51 +01:00
SpaceManiac
8563466011 Fix wrong filename used when log.enabled is set (#5541) 2024-11-28 19:46:53 +01:00
Nikolai Korolev
af4d53fb54 No need for disabling RA0003 warning in FastNoise (#5535) 2024-11-25 00:37:05 +01:00
Pieter-Jan Briers
3086fc446c Sandbox error reference locator now works with generic method calls
This means resolving the MethodSpec table entry for it.
2024-11-22 18:06:33 +01:00
Nikolai Korolev
5f3a54376d Fix warnings for using async without any await (#5532)
* Fix warnings for using async without any await

* Fix async without await warning in EntityTypeParser

* Fix async without await in EnumTypeParser

* Update SessionTypeParser.cs

* Update EntityTypeParser.cs

* Update BoolTypeParser.cs

* Update EntityTypeParser.cs

* Update EnumTypeParser.cs

* Update SessionTypeParser.cs

* Fix compilation and formatting
2024-11-22 02:18:35 +01:00
Nikolai Korolev
9bb7af364e Fix warning for using non-generic variant of TryComp for MetaDataComponent and TransformComponent RA0030 (#5531)
* Fix warning for using non-generic variant of TryComp for MetaDataComponent RA0030 (Use non-generic variant)

* Use non-generic variant of TryComp for TransformComponent
2024-11-22 01:30:55 +01:00
530 changed files with 34389 additions and 18963 deletions

View File

@@ -5,30 +5,30 @@ on:
- cron: "0 0 * * 0"
jobs:
docfx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
with:
submodules: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 8.0.x
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
- name: Install dependencies
run: dotnet restore
- name: Install dependencies
run: dotnet restore
- name: Build Project
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- name: Build Project
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- name: Build DocFX
uses: nikeee/docfx-action@v1.0.0
with:
args: Robust.Docfx/docfx.json
- name: Build DocFX
uses: nikeee/docfx-action@v1.0.0
with:
args: Robust.Docfx/docfx.json
- name: Publish Docfx Documentation on GitHub Pages
uses: maxheld83/ghpages@master
env:
BUILD_DIR: Robust.Docfx/_robust-site
GH_PAT: ${{ secrets.GH_PAT }}
- name: Publish Docfx Documentation on GitHub Pages
uses: maxheld83/ghpages@master
env:
BUILD_DIR: Robust.Docfx/_robust-site
GH_PAT: ${{ secrets.GH_PAT }}

View File

@@ -2,33 +2,32 @@ name: Build & Test
on:
push:
branches: [ master ]
branches: [master]
pull_request:
branches: [ master ]
branches: [master]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest ] # , macos-latest] - temporarily disabled due to libfreetype.dll errors.
os: [ubuntu-latest, windows-latest] # , macos-latest] - temporarily disabled due to libfreetype.dll errors.
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3.6.0
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- name: Robust.UnitTesting
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -- NUnit.ConsoleOut=0
- name: Robust.Analyzers.Tests
run: dotnet test --no-build Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj -- NUnit.ConsoleOut=0
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- name: Robust.UnitTesting
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -- NUnit.ConsoleOut=0
- name: Robust.Analyzers.Tests
run: dotnet test --no-build Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj -- NUnit.ConsoleOut=0

View File

@@ -11,14 +11,8 @@
#
name: "CodeQL"
#on:
# push:
# branches: [ master ]
# pull_request:
# # The branches below must be a subset of the branches above
# branches: [ master ]
# schedule:
# - cron: '30 18 * * 6'
on:
workflow_dispatch
jobs:
analyze:
@@ -28,50 +22,50 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'csharp' ]
language: ["csharp"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v3.6.0
with:
submodules: true
- name: Checkout repository
uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 7.0.x
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 7.0.x
- name: Build
run: dotnet build
- name: Build
run: dotnet build
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -3,51 +3,50 @@
on:
push:
tags:
- 'v*'
- "v*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Parse version
id: parse_version
shell: pwsh
run: |
$ver = [regex]::Match($env:GITHUB_REF, "refs/tags/v?(.+)").Groups[1].Value
echo ("::set-output name=version::{0}" -f $ver)
- name: Parse version
id: parse_version
shell: pwsh
run: |
$ver = [regex]::Match($env:GITHUB_REF, "refs/tags/v?(.+)").Groups[1].Value
echo ("::set-output name=version::{0}" -f $ver)
- uses: actions/checkout@v3.6.0
with:
submodules: true
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 8.0.x
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
- name: Package client
run: Tools/package_client_build.py -p windows mac linux
- name: Package client
run: Tools/package_client_build.py -p windows mac linux
- name: Shuffle files around
run: |
mkdir "release/${{ steps.parse_version.outputs.version }}"
mv release/*.zip "release/${{ steps.parse_version.outputs.version }}"
- name: Shuffle files around
run: |
mkdir "release/${{ steps.parse_version.outputs.version }}"
mv release/*.zip "release/${{ steps.parse_version.outputs.version }}"
- name: Upload files to Suns
uses: appleboy/scp-action@master
with:
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
source: "release/${{ steps.parse_version.outputs.version }}"
target: "/var/lib/robust-builds/builds/"
strip_components: 1
- name: Update manifest JSON
uses: appleboy/ssh-action@master
with:
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }}
- name: Upload files to Suns
uses: appleboy/scp-action@master
with:
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
source: "release/${{ steps.parse_version.outputs.version }}"
target: "/var/lib/robust-builds/builds/"
strip_components: 1
- name: Update manifest JSON
uses: appleboy/ssh-action@master
with:
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }}

View File

@@ -2,40 +2,39 @@ name: Test content master against engine
on:
push:
branches: [ master ]
branches: [master]
pull_request:
branches: [ master ]
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out content
uses: actions/checkout@v3.6.0
with:
repository: space-wizards/space-station-14
submodules: recursive
- name: Check out content
uses: actions/checkout@v4.2.2
with:
repository: space-wizards/space-station-14
submodules: recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 8.0.x
- name: Disable submodule autoupdate
run: touch BuildChecker/DISABLE_SUBMODULE_AUTOUPDATE
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
- name: Disable submodule autoupdate
run: touch BuildChecker/DISABLE_SUBMODULE_AUTOUPDATE
- name: Check out engine version
run: |
cd RobustToolbox
git fetch origin ${{ github.sha }}
git checkout FETCH_HEAD
git submodule update --init --recursive
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Tools --no-restore
- name: Content.Tests
run: dotnet test --no-build Content.Tests/Content.Tests.csproj -v n
- name: Content.IntegrationTests
run: COMPlus_gcServer=1 dotnet test --no-build Content.IntegrationTests/Content.IntegrationTests.csproj -v n
- name: Check out engine version
run: |
cd RobustToolbox
git fetch origin ${{ github.sha }}
git checkout FETCH_HEAD
git submodule update --init --recursive
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Tools --no-restore
- name: Content.Tests
run: dotnet test --no-build Content.Tests/Content.Tests.csproj -v n
- name: Content.IntegrationTests
run: COMPlus_gcServer=1 dotnet test --no-build Content.IntegrationTests/Content.IntegrationTests.csproj -v n

View File

@@ -10,66 +10,69 @@
<ManagePackageVersionsCentrally />
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageVersion Include="ILReader.Core" Version="1.0.0.4" />
<PackageVersion Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageVersion Include="JetBrains.Profiler.Api" Version="1.4.0" />
<PackageVersion Include="JetBrains.Annotations" Version="2024.3.0" />
<PackageVersion Include="JetBrains.Profiler.Api" Version="1.4.8" />
<PackageVersion Include="Linguini.Bundle" Version="0.8.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.NUnit" Version="1.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeCoverage" Version="17.8.0" />
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="8.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.12.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.12.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.12.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.12.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.12.0" />
<PackageVersion Include="Microsoft.CodeCoverage" Version="17.12.0" />
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="9.0.0" />
<PackageVersion Include="Microsoft.DotNet.RemoteExecutor" Version="8.0.0-beta.24059.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="8.0.0" />
<PackageVersion Include="Microsoft.ILVerification" Version="8.0.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageVersion Include="Microsoft.NET.ILLink.Tasks" Version="8.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="9.0.0" />
<PackageVersion Include="Microsoft.ILVerification" Version="9.0.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageVersion Include="Microsoft.NET.ILLink.Tasks" Version="9.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageVersion Include="Moq" Version="4.20.70" />
<PackageVersion Include="NUnit" Version="4.0.1" />
<PackageVersion Include="NUnit.Analyzers" Version="3.10.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="NUnit" Version="4.3.2" />
<PackageVersion Include="NUnit.Analyzers" Version="4.5.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageVersion Include="Nett" Version="0.15.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
<PackageVersion Include="OpenTK.OpenAL" Version="4.7.7" />
<PackageVersion Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" />
<PackageVersion Include="Pidgin" Version="3.2.2" />
<PackageVersion Include="Pidgin" Version="3.3.0" />
<PackageVersion Include="Robust.Natives" Version="0.1.1" />
<PackageVersion Include="Robust.Natives.Cef" Version="120.1.9" />
<PackageVersion Include="Robust.Natives.Cef" Version="131.3.5" />
<PackageVersion Include="Robust.Shared.AuthLib" Version="0.1.2" />
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.7" />
<PackageVersion Include="SQLitePCLRaw.provider.sqlite3" Version="2.1.7" />
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
<PackageVersion Include="SQLitePCLRaw.provider.sqlite3" Version="2.1.10" />
<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.5" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="System.Numerics.Vectors" Version="4.5.0" />
<PackageVersion Include="System.Memory" Version="4.5.5" />
<PackageVersion Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.22621.5" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />
<PackageVersion Include="TerraFX.Interop.Xlib" Version="6.4.0" />
<PackageVersion Include="VorbisPizza" Version="1.3.0" />
<PackageVersion Include="YamlDotNet" Version="13.7.1" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="PolySharp" Version="1.14.1" />
<PackageVersion Include="PolySharp" Version="1.15.0" />
<!-- Transitive deps that we need to pin versions for to avoid NuGet warnings. -->
<PackageVersion Include="System.Formats.Asn1" Version="9.0.0" />
<PackageVersion Include="System.Reflection.Metadata" Version="9.0.0" />
<PackageVersion Include="System.Text.Json" Version="9.0.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.12.0" />
</ItemGroup>
</Project>

View File

@@ -12,7 +12,7 @@
<SkipRobustAnalyzer>true</SkipRobustAnalyzer>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
<LangVersion>13.0</LangVersion>
</PropertyGroup>
<ItemGroup>

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

@@ -1,8 +1,8 @@
<Project>
<!-- Engine-specific properties. Content should not use this file. -->
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12</LangVersion>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>13</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>

View File

@@ -3,7 +3,7 @@
<!-- Import this at the end of any project files in Robust and Content. -->
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

View File

@@ -1,4 +1,4 @@
# Release notes for RobustToolbox.
# Release notes for RobustToolbox.
<!--
NOTE: automatically updated sometimes by version.py.
@@ -54,10 +54,426 @@ END TEMPLATE-->
*None yet*
## 237.2.2
## 248.0.1
## 237.2.1
## 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
## 240.1.1
### Bugfixes
* Fixed one of the `IOverlayManager.RemoveOverlay` overrides not fully removing the overlay.
## 240.1.0
### New features
* Added an `AsNullable` extension method for converting an `Entity<T>` into an `Entity<T?>`
### Bugfixes
* Fixed an exception in `PhysicsSystem.DestroyContacts()` that could result in entities getting stuck with broken physics.
### Other
* `GamePrototypeLoadManager` will now send all uploaded prototypes to connecting players in a single `GamePrototypeLoadMessage`, as opposed to one message per upload.
## 240.0.1
### Bugfixes
* Fixed `SharedBroadphaseSystem.GetBroadphases()` not returning the map itself, which was causing physics to not work properly off-grid.
## 240.0.0
### Breaking changes
* `ComponentRegistry` no longer implements `ISerializationContext`
* Tickrate values are now `ushort`, allowing them to go up to 65535.
### New features
* Console completion options now have new flags for preventing suggestions from being escaped or quoted.
* Added `ILocalizationManager.HasCulture()`.
* Static `EntProtoId<T>` fields are now validated to exist.
### Bugfixes
* Fixed a state handling bug in replays, which was causing exceptions to be thrown when applying delta states.
### Other
* Reduced amount of `DynamicMethod`s used by serialization system. This should improve performance somewhat.
### Internal
* Avoided sorting overlays every render frame.
* Various clean up to grid fixture code/adding asserts.
## 239.0.1
### Bugfixes
* Fix logging of received packets with `net.packet` logging level.
* Downgrade `VorbisPizza` to fix audio playback for systems without AVX2 support.
### Other
* Improved performance of some Roslyn analyzers and source generators, which should significantly improve compile times and IDE performance.
## 239.0.0
### Breaking changes
* Robust now uses **.NET 9**.
* `ISerializationManager` will now log errors if it encounters `Entity<T>` data-fields.
* To be clear, this has never been supported and is not really a breaking change, but this will likely require manual intervention to prevent tests from failing.
* `IClyde.TextInputSetRect`, `TextInputStart` and `TextInputStop` have been moved to be on `IClydeWindow`.
* Updated various NuGet dependencies and removed some other ones, of note:
* `FastAccessors`, which is a transitive dep we never used, is now gone. It might have snuck into some `using` statement thanks to your IDE, and those will now fail to compile. Remove them.
* NUnit `Is.EqualTo(default)` seems to have ambiguous overload resolution in some cases now, this can be fixed by using an explicit `default(type)` syntax.
* This also fixed various false-positive warnings reported by NuGet.
### New features
* Added `MockInterfaces.MakeConfigurationManager` for creating functional configuration managers for unit test mocking.
* Added `ISawmill.IsLogLevelEnabled()` to avoid doing expensive verbose logging operations when not necessary.
* ``string[] Split(System.ReadOnlySpan`1<char>)`` is now available in sandbox.
### Bugfixes
* Fixed auto-generated component delta-states not raising `AfterAutoHandleStateEvent`
* Fixed auto-generated component delta-states improperly implementing `IComponentDeltaState` methods. May have caused bugs in replays.
* Fixed `Robust.Client.WebView` on the launcher via a new release.
* Fixed an exception that could occur when saving a map that had tiles migrated by alias.
### Other
* The `loglevel` command now properly shows the "`null`" log level that resets the level to inheriting from parent. This was already supported by it, but the completions didn't list it.
### Internal
* Experimental SDL2 windowing backend has been replaced with SDL3. SDL3 backend is also more feature-complete, though it is still not in use.
* Updated CEF used by Robust.Client.WebView to 131.3.5.
## 238.0.1
### Bugfixes
* Fixed source generation for auto-networked EntityUid Dictionaries missing a semicolon
* Fixed PlacementManager using the wrong coordinates when deleting entities in an area.
## 238.0.0
### Breaking changes
* Some toolshed command syntax/parsing has changed slightly, and several toolshed related classes and interfaces have changed significantly, including ToolshedManager, type parsers, invocation contexts, and parser contexts. For more detail see the the description of PR #5455
## 237.4.0
### New features
* Implement automatic field-level delta states via AutoGenerateComponentState via opt-in.
### Bugfixes
* Remove redundant TransformComponentState bool.
## 237.3.0
### New features
* Added stack-like functions to `ValueList<T>` and added an `AddRange(ReadOnlySpan<T>)` overload.
* Added new `AssetPassFilterDrop`.
* Added a new RayCastSystem with the latest Box2D raycast + shapecasts implemented.
### Bugfixes
* Fixed `IPrototypeManager.TryGetKindFrom()` not working for prototypes with automatically inferred kind names.
### Other
* Sandbox error reference locator now works with generic method calls.
## 237.2.0

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

@@ -156,6 +156,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 +294,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>

View File

@@ -1,4 +1,8 @@
command-description-tpto =
command-help-usage =
Usage:
command-help-invertible =
The behaviour of this command can be inverted using the "not" prefix.
command-description-tpto =
Teleport the given entities to some target entity.
command-description-player-list =
Returns a list of all player sessions.
@@ -19,7 +23,7 @@ command-description-buildinfo =
command-description-cmd-list =
Returns a list of all commands, for this side.
command-description-explain =
Explains the given expression, providing command descriptions and signatures.
Explains the given expression, providing command descriptions and signatures. This only works for valid expressions, it can't explain commands that it fails to parse.
command-description-search =
Searches through the input for the provided value.
command-description-stopwatch =
@@ -38,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.
@@ -53,10 +56,8 @@ command-description-entities =
Returns all entities on the server.
command-description-paused =
Filters the input entities by whether or not they are paused.
This command can be inverted with not.
command-description-with =
Filters the input entities by whether or not they have the given component.
This command can be inverted with not.
command-description-fuck =
Throws an exception.
command-description-ecscomp-listty =
@@ -95,6 +96,8 @@ command-description-vars =
Provides a list of all variables set in this session.
command-description-any =
Returns true if there's any values in the input, otherwise false.
command-description-contains =
Returns whether the input enumerable contains the specified value.
command-description-ArrowCommand =
Assigns the input to a variable.
command-description-isempty =
@@ -119,6 +122,8 @@ command-description-splat =
"Splats" a block, value, or variable, creating N copies of it in a list.
command-description-val =
Casts the given value, block, or variable to the given type. This is mostly a workaround for current limitations of variables.
command-description-var =
Returns the contents of the given variable. This will attempt to automatically infer a variables type. Compound commands that modify a variable may need to use the 'val' command instead.
command-description-actor-controlled =
Filters entities by whether or not they're actively controlled.
command-description-actor-session =
@@ -143,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.
@@ -197,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 =
@@ -271,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

@@ -1,12 +1,8 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using Robust.Analyzers;
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.AccessAnalyzer>;
using static Microsoft.CodeAnalysis.Testing.DiagnosticResult;
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.AccessAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
@@ -16,7 +12,7 @@ public sealed class AccessAnalyzer_Test
{
public Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<AccessAnalyzer, NUnitVerifier>()
var test = new CSharpAnalyzerTest<AccessAnalyzer, DefaultVerifier>()
{
TestState =
{

View File

@@ -0,0 +1,114 @@
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.ByRefEventAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture, TestOf(typeof(ByRefEventAnalyzer))]
public sealed class ByRefEventAnalyzerTest
{
private const string EventBusDef = """
namespace Robust.Shared.GameObjects;
public readonly struct EntityUid;
public sealed class EntitySystem
{
public void RaiseLocalEvent<TEvent>(EntityUid uid, ref TEvent args, bool broadcast = false)
where TEvent : notnull { }
public void RaiseLocalEvent<TEvent>(EntityUid uid, TEvent args, bool broadcast = false)
where TEvent : notnull { }
}
public sealed class EntityEventBus
{
public void RaiseLocalEvent<TEvent>(EntityUid uid, ref TEvent args, bool broadcast = false)
where TEvent : notnull { }
public void RaiseLocalEvent<TEvent>(EntityUid uid, TEvent args, bool broadcast = false)
where TEvent : notnull { }
}
""";
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<ByRefEventAnalyzer, DefaultVerifier>()
{
TestState =
{
Sources = { code }
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.GameObjects.EventBusAttributes.cs"
);
test.TestState.Sources.Add(("EntityEventBus.cs", EventBusDef));
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task TestSuccess()
{
const string code = """
using Robust.Shared.GameObjects;
[ByRefEvent]
public readonly struct RefEvent;
public readonly struct ValueEvent;
public static class Foo
{
public static void Bar(EntityEventBus bus)
{
bus.RaiseLocalEvent(default(EntityUid), new ValueEvent());
var refEv = new RefEvent();
bus.RaiseLocalEvent(default(EntityUid), ref refEv);
}
}
""";
await Verifier(code);
}
[Test]
public async Task TestWrong()
{
const string code = """
using Robust.Shared.GameObjects;
[ByRefEvent]
public readonly struct RefEvent;
public readonly struct ValueEvent;
public static class Foo
{
public static void Bar(EntityEventBus bus)
{
bus.RaiseLocalEvent(default(EntityUid), new RefEvent());
var valueEv = new ValueEvent();
bus.RaiseLocalEvent(default(EntityUid), ref valueEv);
}
}
""";
await Verifier(
code,
// /0/Test0.cs(11,49): error RA0015: Tried to raise a by-ref event 'RefEvent' by value
VerifyCS.Diagnostic(ByRefEventAnalyzer.ByRefEventRaisedByValueRule).WithSpan(11, 49, 11, 63).WithArguments("RefEvent"),
// /0/Test0.cs(13,49): error RA0016: Tried to raise a value event 'ValueEvent' by-ref
VerifyCS.Diagnostic(ByRefEventAnalyzer.ByValueEventRaisedByRefRule).WithSpan(13, 49, 13, 60).WithArguments("ValueEvent")
);
}
}

View File

@@ -66,6 +66,7 @@ public sealed class ComponentPauseGeneratorTest
public partial class FooComponent
{
[RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()
@@ -106,6 +107,7 @@ public sealed class ComponentPauseGeneratorTest
public partial class FooComponent
{
[RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()
@@ -147,6 +149,7 @@ public sealed class ComponentPauseGeneratorTest
public partial class FooComponent
{
[RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()
@@ -188,6 +191,7 @@ public sealed class ComponentPauseGeneratorTest
public partial class FooComponent
{
[RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()

View File

@@ -1,10 +1,9 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.DataDefinitionAnalyzer>;
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.DataDefinitionAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
@@ -14,7 +13,7 @@ public sealed class DataDefinitionAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<DataDefinitionAnalyzer, NUnitVerifier>()
var test = new CSharpAnalyzerTest<DataDefinitionAnalyzer, DefaultVerifier>()
{
TestState =
{

View File

@@ -1,10 +1,9 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.DependencyAssignAnalyzer>;
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.DependencyAssignAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
@@ -14,7 +13,7 @@ public sealed class DependencyAssignAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<DependencyAssignAnalyzer, NUnitVerifier>()
var test = new CSharpAnalyzerTest<DependencyAssignAnalyzer, DefaultVerifier>()
{
TestState =
{

View File

@@ -1,10 +1,9 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.DuplicateDependencyAnalyzer>;
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.DuplicateDependencyAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
@@ -15,7 +14,7 @@ public sealed class DuplicateDependencyAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<DuplicateDependencyAnalyzer, NUnitVerifier>()
var test = new CSharpAnalyzerTest<DuplicateDependencyAnalyzer, DefaultVerifier>()
{
TestState =
{

View File

@@ -1,10 +1,9 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.MustCallBaseAnalyzer>;
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.MustCallBaseAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
@@ -14,7 +13,7 @@ public sealed class MustCallBaseAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<MustCallBaseAnalyzer, NUnitVerifier>()
var test = new CSharpAnalyzerTest<MustCallBaseAnalyzer, DefaultVerifier>()
{
TestState =
{

View File

@@ -1,10 +1,9 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.NoUncachedRegexAnalyzer>;
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.NoUncachedRegexAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
@@ -14,7 +13,7 @@ public sealed class NoUncachedRegexAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<NoUncachedRegexAnalyzer, NUnitVerifier>()
var test = new CSharpAnalyzerTest<NoUncachedRegexAnalyzer, DefaultVerifier>()
{
TestState =
{

View File

@@ -1,10 +1,9 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.PreferNonGenericVariantForAnalyzer>;
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PreferNonGenericVariantForAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
@@ -14,7 +13,7 @@ public sealed class PreferNonGenericVariantForTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<PreferNonGenericVariantForAnalyzer, NUnitVerifier>()
var test = new CSharpAnalyzerTest<PreferNonGenericVariantForAnalyzer, DefaultVerifier>()
{
TestState =
{

View File

@@ -1,10 +1,9 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.PreferOtherTypeAnalyzer>;
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PreferOtherTypeAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
@@ -14,7 +13,7 @@ public sealed class PreferOtherTypeAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<PreferOtherTypeAnalyzer, NUnitVerifier>()
var test = new CSharpAnalyzerTest<PreferOtherTypeAnalyzer, DefaultVerifier>()
{
TestState =
{

View File

@@ -1,10 +1,9 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.PreferOtherTypeAnalyzer>;
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PreferOtherTypeAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
@@ -12,7 +11,7 @@ public sealed class PreferOtherTypeFixerTest
{
private static Task Verifier(string code, string fixedCode, params DiagnosticResult[] expected)
{
var test = new CSharpCodeFixTest<PreferOtherTypeAnalyzer, PreferOtherTypeFixer, NUnitVerifier>()
var test = new CSharpCodeFixTest<PreferOtherTypeAnalyzer, PreferOtherTypeFixer, DefaultVerifier>()
{
TestState =
{

View File

@@ -14,6 +14,7 @@
<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\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>
<PropertyGroup>
@@ -27,13 +28,17 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzer.Testing"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.NUnit"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing"/>
<PackageReference Include="NUnit"/>
<PackageReference Include="NUnit3TestAdapter"/>
<PackageReference Include="NUnit.Analyzers"/>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<!-- Needed to fix transitive dependency versions -->
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="System.Formats.Asn1" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
@@ -24,7 +24,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
"Make sure that methods subscribing to a ref event have the ref keyword for the event argument."
);
private static readonly DiagnosticDescriptor ByRefEventRaisedByValueRule = new(
public static readonly DiagnosticDescriptor ByRefEventRaisedByValueRule = new(
Diagnostics.IdByRefEventRaisedByValue,
"By-ref event raised by value",
"Tried to raise a by-ref event '{0}' by value",
@@ -34,7 +34,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
"Make sure to use the ref keyword when raising ref events."
);
private static readonly DiagnosticDescriptor ByValueEventRaisedByRefRule = new(
public static readonly DiagnosticDescriptor ByValueEventRaisedByRefRule = new(
Diagnostics.IdValueEventRaisedByRef,
"Value event raised by-ref",
"Tried to raise a value event '{0}' by-ref",
@@ -54,32 +54,44 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterOperationAction(CheckEventRaise, OperationKind.Invocation);
context.RegisterCompilationStartAction(compilationContext =>
{
var raiseMethods = compilationContext.Compilation
.GetTypeByMetadataName("Robust.Shared.GameObjects.EntitySystem")?
.GetMembers()
.Where(m => m.Name.Contains("RaiseLocalEvent") && m.Kind == SymbolKind.Method)
.Cast<IMethodSymbol>();
var busRaiseMethods = compilationContext.Compilation
.GetTypeByMetadataName("Robust.Shared.GameObjects.EntityEventBus")?
.GetMembers()
.Where(m => m.Name.Contains("RaiseLocalEvent") && m.Kind == SymbolKind.Method)
.Cast<IMethodSymbol>();
if (raiseMethods == null)
return;
if (busRaiseMethods != null)
raiseMethods = raiseMethods.Concat(busRaiseMethods);
var raiseMethodsArray = raiseMethods.ToArray();
compilationContext.RegisterOperationAction(
ctx => CheckEventRaise(ctx, raiseMethodsArray),
OperationKind.Invocation);
});
}
private void CheckEventRaise(OperationAnalysisContext context)
private static void CheckEventRaise(
OperationAnalysisContext context,
IReadOnlyCollection<IMethodSymbol> raiseMethods)
{
if (context.Operation is not IInvocationOperation operation)
return;
var raiseMethods = context.Compilation
.GetTypeByMetadataName("Robust.Shared.GameObjects.EntitySystem")?
.GetMembers()
.Where(m => m.Name.Contains("RaiseLocalEvent") && m.Kind == SymbolKind.Method)
.Cast<IMethodSymbol>();
var busRaiseMethods = context.Compilation
.GetTypeByMetadataName("Robust.Shared.GameObjects.EntityEventBus")?
.GetMembers()
.Where(m => m.Name.Contains("RaiseLocalEvent") && m.Kind == SymbolKind.Method)
.Cast<IMethodSymbol>();
if (raiseMethods == null)
if (!operation.TargetMethod.Name.Contains("RaiseLocalEvent"))
return;
if (busRaiseMethods != null)
raiseMethods = raiseMethods.Concat(busRaiseMethods);
if (!raiseMethods.Any(m => m.Equals(operation.TargetMethod.OriginalDefinition, Default)))
{
// If you try to do this normally by concatenating like busRaiseMethods above

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

@@ -19,6 +19,10 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
<!-- Needed to pin transitive dependency versions. -->
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" />
<PackageReference Include="System.Formats.Asn1" />
</ItemGroup>
<Import Project="..\MSBuild\Robust.Properties.targets" />

View File

@@ -15,6 +15,9 @@ using XamlX.Transform;
using XamlX.Transform.Transformers;
using XamlX.TypeSystem;
// Yes dude I know this source generator isn't incremental, I'll fix it eventually.
#pragma warning disable RS1035
namespace Robust.Client.NameGenerator
{
/// <summary>

View File

@@ -64,6 +64,8 @@ internal abstract class BaseRobustCefClient : CefClient
string title,
string defaultFilePath,
string[] acceptFilters,
string[] acceptExtensions,
string[] acceptDescriptions,
CefFileDialogCallback callback)
{
callback.Cancel();

View File

@@ -6,7 +6,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
internal static class Program
public 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

@@ -44,6 +44,8 @@ namespace Robust.Client.WebView.Cef
//commandLine.AppendSwitch("--disable-gpu-compositing");
//commandLine.AppendSwitch("--in-process-gpu");
commandLine.AppendSwitch("--off-screen-rendering-enabled");
commandLine.AppendSwitch("disable-threaded-scrolling", "1");
commandLine.AppendSwitch("disable-features", "TouchpadAndWheelScrollLatching,AsyncWheelEvents");

View File

@@ -23,6 +23,7 @@ namespace Robust.Client.WebView.Cef
var info = CefWindowInfo.Create();
info.Bounds = new CefRectangle(0, 0, createParams.Width, createParams.Height);
info.SetAsPopup(mainHWnd, "ss14cef");
info.RuntimeStyle = CefRuntimeStyle.Alloy;
var impl = new WebViewWindowImpl(this);

View File

@@ -484,27 +484,27 @@ namespace Robust.Client.WebView.Cef
public void FocusEntered()
{
if (_textInputActive)
_clyde.TextInputStart();
Owner.Root?.Window?.TextInputStart();
}
public void FocusExited()
{
if (_textInputActive)
_clyde.TextInputStop();
Owner.Root?.Window?.TextInputStop();
}
public void TextInputStart()
{
_textInputActive = true;
if (Owner.HasKeyboardFocus())
_clyde.TextInputStart();
Owner.Root?.Window?.TextInputStart();
}
public void TextInputStop()
{
_textInputActive = false;
if (Owner.HasKeyboardFocus())
_clyde.TextInputStop();
Owner.Root?.Window?.TextInputStop();
}
private sealed class LiveData
@@ -587,8 +587,11 @@ namespace Robust.Client.WebView.Cef
}
}
protected override void OnAcceleratedPaint(CefBrowser browser, CefPaintElementType type,
CefRectangle[] dirtyRects, IntPtr sharedHandle)
protected override void OnAcceleratedPaint(
CefBrowser browser,
CefPaintElementType type,
CefRectangle[] dirtyRects,
in CefAcceleratedPaintInfo info)
{
// Unused, but we're forced to implement it so.. NOOP.
}

View File

@@ -5,7 +5,6 @@ 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;
@@ -25,7 +24,6 @@ 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!;
@@ -63,10 +61,7 @@ namespace Robust.Client.WebView.Cef
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
var settings = new CefSettings()
{

View File

@@ -5,6 +5,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<OutputType>WinExe</OutputType>
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
<CETCompat>false</CETCompat>
</PropertyGroup>
<ItemGroup>

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,12 +540,12 @@ 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;
Dirty(entity, component);
DirtyField(entity, component, nameof(AudioComponent.Global));
return (entity, component);
}
@@ -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

@@ -88,10 +88,10 @@ namespace Robust.Client
{
if (GameInfo != null)
{
GameInfo.TickRate = (byte) tickrate;
GameInfo.TickRate = (ushort) tickrate;
}
_timing.SetTickRateAt((byte) tickrate, info.TickChanged);
_timing.SetTickRateAt((ushort) tickrate, info.TickChanged);
_logger.Info($"Tickrate changed to: {tickrate} on tick {_timing.CurTick}");
}
@@ -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);
}
@@ -395,6 +391,6 @@ namespace Robust.Client
/// </summary>
public int ServerMaxPlayers { get; set; }
public byte TickRate { get; internal set; }
public uint TickRate { get; internal set; }
}
}

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

@@ -23,6 +23,7 @@ namespace Robust.Client
private Thread? _gameThread;
private ISawmill _logger = default!;
[STAThread]
public static void Main(string[] args)
{
Start(args, new GameControllerOptions());

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, hideUserDataDir: true);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
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 ReadOnlySpan<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

@@ -26,6 +26,8 @@ namespace Robust.Client.GameObjects
protected override void OnAppearanceGetState(EntityUid uid, AppearanceComponent component, ref ComponentGetState args)
{
// TODO Game State
// Force the client to serialize & de-serialize implicitly generated component states.
var clone = CloneAppearanceData(component.AppearanceData);
args.State = new AppearanceComponentState(clone);
}

View File

@@ -5,7 +5,6 @@ using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Utility;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -241,7 +240,7 @@ namespace Robust.Client.GameObjects
#if DEBUG
var uid = GetEntity(netEntity);
if (TryComp<MetaDataComponent>(uid, out var meta))
if (TryComp(uid, out MetaDataComponent? meta))
{
DebugTools.Assert((meta.Flags & ( MetaDataFlags.Detached | MetaDataFlags.InContainer) ) == MetaDataFlags.Detached,
$"Adding entity {ToPrettyString(uid)} to list of expected entities for container {container.ID} in {ToPrettyString(container.Owner)}, despite it already being in a container.");

View File

@@ -17,7 +17,7 @@ public sealed class MapSystem : SharedMapSystem
{
// 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);
}

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

@@ -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}
""");
@@ -385,7 +403,7 @@ namespace Robust.Client.GameStates
try
{
#endif
createdEntities = ApplyGameState(curState, nextState);
ApplyGameState(curState, nextState);
#if EXCEPTION_TOLERANCE
}
catch (MissingMetadataException e)
@@ -399,7 +417,7 @@ namespace Robust.Client.GameStates
using (_prof.Group("MergeImplicitData"))
{
MergeImplicitData(createdEntities);
MergeImplicitData();
}
if (_lastProcessedInput < curState.LastProcessedInput)
@@ -456,7 +474,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 +522,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 +561,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 +648,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 +668,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 +687,21 @@ 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>
private void MergeImplicitData(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}");
#if !EXCEPTION_TOLERANCE
throw new KeyNotFoundException();
#endif
continue;
}
#else
var (_, meta) = _entityManager.GetEntityData(netEntity);
#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,166 @@ 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;
#endif
return;
}
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;
#endif
return;
}
}
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 +1130,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 +1197,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 +1229,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 +1241,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 +1267,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 +1312,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 +1398,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 +1411,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 +1433,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 +1555,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 +1581,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,6 +82,8 @@ namespace Robust.Client.GameStates
/// </summary>
IEnumerable<NetEntity> ApplyGameState(GameState curState, GameState? nextState);
void MergeImplicitData();
/// <summary>
/// Resets any entities that have changed while predicting future ticks.
/// </summary>

View File

@@ -26,6 +26,7 @@ namespace Robust.Client.GameStates
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly IConsoleHost _host = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
private const int HistorySize = 60 * 5; // number of ticks to keep in history.
@@ -78,7 +79,7 @@ namespace Robust.Client.GameStates
string? entStateString = null;
string? entDelString = null;
var conShell = IoCManager.Resolve<IConsoleHost>().LocalShell;
var conShell = _host.LocalShell;
var entStates = args.AppliedState.EntityStates;
if (entStates.HasContents)

View File

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

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), _mapManager.GetMapEntityIdOrThrow(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, _mapManager.GetMapEntityIdOrThrow(mapId), mapId, worldAABB, worldBounds);
foreach (var overlay in list)
{
@@ -215,8 +217,6 @@ namespace Robust.Client.Graphics.Clyde
}
}
_overlays.Sort(OverlayComparer.Instance);
return _overlays;
}
@@ -423,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)
@@ -450,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)
@@ -574,17 +589,5 @@ namespace Robust.Client.Graphics.Clyde
return new Box2Rotated(aabb, rotation, aabb.Center);
}
private sealed class OverlayComparer : IComparer<Overlay>
{
public static readonly OverlayComparer Instance = new();
public int Compare(Overlay? x, Overlay? y)
{
var zX = x?.ZIndex ?? 0;
var zY = y?.ZIndex ?? 0;
return zX.CompareTo(zY);
}
}
}
}

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;
@@ -279,8 +277,7 @@ namespace Robust.Client.Graphics.Clyde
{
const float arbitraryDistanceMax = 1234;
GL.Disable(EnableCap.Blend);
CheckGlError();
IsBlending = false;
GL.Enable(EnableCap.DepthTest);
CheckGlError();
@@ -329,8 +326,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 +390,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 +527,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 +548,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 (
@@ -643,21 +659,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 +695,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 +710,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 +784,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 +797,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 +817,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 +847,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 +1154,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 +1175,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

@@ -362,6 +362,11 @@ namespace Robust.Client.Graphics.Clyde
rect.BottomLeft, rect.BottomRight, color, subRegion);
}
public override void DrawTexture(Texture texture, Vector2 position, Color? modulate = null)
{
base.DrawTexture(texture, position, modulate);
}
/// <summary>
/// Draws an entity.
/// </summary>

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

@@ -109,8 +109,8 @@ namespace Robust.Client.Graphics.Clyde
case "glfw":
winImpl = new GlfwWindowingImpl(this, _deps);
break;
case "sdl2":
winImpl = new Sdl2WindowingImpl(this, _deps);
case "sdl3":
winImpl = new Sdl3WindowingImpl(this, _deps);
break;
default:
_logManager.GetSawmill("clyde.win").Log(
@@ -467,26 +467,7 @@ namespace Robust.Client.Graphics.Clyde
_windowing!.RunOnWindowThread(a);
}
public void TextInputSetRect(UIBox2i rect)
{
DebugTools.AssertNotNull(_windowing);
_windowing!.TextInputSetRect(rect);
}
public void TextInputStart()
{
DebugTools.AssertNotNull(_windowing);
_windowing!.TextInputStart();
}
public void TextInputStop()
{
DebugTools.AssertNotNull(_windowing);
_windowing!.TextInputStop();
}
public IFileDialogManager? FileDialogImpl => _windowing as IFileDialogManager;
private abstract class WindowReg
{
@@ -590,6 +571,27 @@ namespace Robust.Client.Graphics.Clyde
remove => Reg.Resized -= value;
}
public void TextInputSetRect(UIBox2i rect, int cursor)
{
DebugTools.AssertNotNull(_clyde._windowing);
_clyde._windowing!.TextInputSetRect(Reg, rect, cursor);
}
public void TextInputStart()
{
DebugTools.AssertNotNull(_clyde._windowing);
_clyde._windowing!.TextInputStart(Reg);
}
public void TextInputStop()
{
DebugTools.AssertNotNull(_clyde._windowing);
_clyde._windowing!.TextInputStop(Reg);
}
public nint? WindowsHWnd => _clyde._windowing!.WindowGetWin32Window(Reg);
}

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

@@ -7,7 +7,9 @@ using JetBrains.Annotations;
using Robust.Client.Audio;
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;
@@ -188,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)
{
@@ -284,6 +302,8 @@ namespace Robust.Client.Graphics.Clyde
action();
}
public IFileDialogManager? FileDialogImpl => null;
private sealed class DummyCursor : ICursor
{
public void Dispose()
@@ -349,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)
{
}
@@ -494,7 +518,7 @@ namespace Robust.Client.Graphics.Clyde
}
public void RenderScreenOverlaysBelow(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds)
{
@@ -502,7 +526,7 @@ namespace Robust.Client.Graphics.Clyde
}
public void RenderScreenOverlaysAbove(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds)
{
@@ -531,6 +555,21 @@ namespace Robust.Client.Graphics.Clyde
public event Action<WindowDestroyedEventArgs>? Destroyed;
public event Action<WindowResizedEventArgs>? Resized { add { } remove { } }
public void TextInputSetRect(UIBox2i rect, int cursor)
{
// Nop.
}
public void TextInputStart()
{
// Nop.
}
public void TextInputStop()
{
// Nop.
}
public void MaximizeOnMonitor(IClydeMonitor monitor)
{
}

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

@@ -96,7 +96,8 @@ namespace Robust.Client.Graphics.Clyde
private void ProcessEventChar(EventChar ev)
{
if (!_textInputActive)
var windowReg = FindWindow(ev.Window);
if (windowReg is not { TextInputActive: true })
return;
_clyde.SendText(new TextEnteredEventArgs(new Rune(ev.CodePoint).ToString()));

View File

@@ -468,6 +468,8 @@ namespace Robust.Client.Graphics.Clyde
GLFW.WindowHint(WindowHintInt.AlphaBits, 8);
GLFW.WindowHint(WindowHintInt.StencilBits, 8);
GLFW.WindowHint(WindowHintBool.Decorated, (parameters.Styles & OSWindowStyles.NoTitleBar) == 0);
var window = GLFW.CreateWindow(
parameters.Width, parameters.Height,
parameters.Title,
@@ -485,23 +487,12 @@ namespace Robust.Client.Graphics.Clyde
GLFW.MaximizeWindow(window);
}
if ((parameters.Styles & OSWindowStyles.NoTitleBar) != 0)
{
GLFW.WindowHint(WindowHintBool.Decorated, false);
}
if ((parameters.Styles & OSWindowStyles.NoTitleOptions) != 0)
{
if (OperatingSystem.IsWindows())
{
var hWnd = (HWND) GLFW.GetWin32Window(window);
DebugTools.Assert(hWnd != HWND.NULL);
Windows.SetWindowLongPtrW(
hWnd,
GWL.GWL_STYLE,
// Cast to long here to work around a bug in rider with nint bitwise operators.
(nint)((long)Windows.GetWindowLongPtrW(hWnd, GWL.GWL_STYLE) & ~WS.WS_SYSMENU));
WsiShared.SetWindowStyleNoTitleOptionsWindows(hWnd);
}
else if (OperatingSystem.IsLinux())
{
@@ -509,23 +500,7 @@ namespace Robust.Client.Graphics.Clyde
{
var x11Window = (X11Window)GLFW.GetX11Window(window);
var x11Display = (Display*) GLFW.GetX11Display(window);
DebugTools.Assert(x11Window != X11Window.NULL);
// https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm46181547486832
var newPropValString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE_DIALOG");
var newPropVal = Xlib.XInternAtom(x11Display, (sbyte*)newPropValString, Xlib.False);
DebugTools.Assert(newPropVal != Atom.NULL);
var propNameString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE");
#pragma warning disable CA1806
// [display] [window] [property] [type] [format (8, 16,32)] [mode] [data] [element count]
Xlib.XChangeProperty(x11Display, x11Window,
Xlib.XInternAtom(x11Display, (sbyte*)propNameString, Xlib.False), // should never be null; part of spec
Xlib.XA_ATOM, 32, Xlib.PropModeReplace,
(byte*)&newPropVal, 1);
#pragma warning restore CA1806
Marshal.FreeCoTaskMem(newPropValString);
Marshal.FreeCoTaskMem(propNameString);
WsiShared.SetWindowStyleNoTitleOptionsX11(x11Display, x11Window);
}
catch (EntryPointNotFoundException)
{
@@ -643,16 +618,16 @@ namespace Robust.Client.Graphics.Clyde
return reg;
}
private WindowReg? FindWindow(nint window) => FindWindow((Window*) window);
private GlfwWindowReg? FindWindow(nint window) => FindWindow((Window*) window);
private WindowReg? FindWindow(Window* window)
private GlfwWindowReg? FindWindow(Window* window)
{
foreach (var windowReg in _clyde._windows)
{
var glfwReg = (GlfwWindowReg) windowReg;
if (glfwReg.GlfwWindow == window)
{
return windowReg;
return glfwReg;
}
}
@@ -739,23 +714,23 @@ namespace Robust.Client.Graphics.Clyde
return (void*) GLFW.GetProcAddress(procName);
}
public void TextInputSetRect(UIBox2i rect)
public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor)
{
// Not supported on GLFW.
}
public void TextInputStart()
public void TextInputStart(WindowReg reg)
{
// Not properly supported on GLFW.
_textInputActive = true;
((GlfwWindowReg)reg).TextInputActive = true;
}
public void TextInputStop()
public void TextInputStop(WindowReg reg)
{
// Not properly supported on GLFW.
_textInputActive = false;
((GlfwWindowReg)reg).TextInputActive = false;
}
private void CheckWindowDisposed(WindowReg reg)
@@ -770,6 +745,10 @@ namespace Robust.Client.Graphics.Clyde
// Kept around to avoid it being GCd.
public CursorImpl? Cursor;
// While GLFW does not provide proper IME APIs, we can at least emulate SDL3's StartTextInput() system.
// This will ensure some level of consistency between the backends.
public bool TextInputActive;
}
}
}

View File

@@ -25,10 +25,6 @@ namespace Robust.Client.Graphics.Clyde
private bool _glfwInitialized;
private bool _win32Experience;
// While GLFW does not provide proper IME APIs, we can at least emulate SDL2's StartTextInput() system.
// This will ensure some level of consistency between the backends.
private bool _textInputActive;
public GlfwWindowingImpl(Clyde clyde, IDependencyCollection deps)
{
_clyde = clyde;

View File

@@ -66,9 +66,9 @@ namespace Robust.Client.Graphics.Clyde
void RunOnWindowThread(Action a);
// IME
void TextInputSetRect(UIBox2i rect);
void TextInputStart();
void TextInputStop();
void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor);
void TextInputStart(WindowReg reg);
void TextInputStop(WindowReg reg);
string GetDescription();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
/* SDL3-CS - C# Bindings for SDL3
*
* Copyright (c) 2024 Colin Jackson
*
* This software is provided 'as-is', without any express or implied warranty.
* In no event will the authors be held liable for any damages arising from
* the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software in a
* product, an acknowledgment in the product documentation would be
* appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source distribution.
*
* Colin "cryy22" Jackson <c@cryy22.art>
*
*/

View File

@@ -0,0 +1,56 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SDL3;
public static partial class SDL
{
// Extensions to SDL3-CS that aren't part of the main library.
[LibraryImport(nativeLibName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static unsafe partial void SDL_SetLogOutputFunction(delegate* unmanaged[Cdecl] <void*, int, SDL_LogPriority, byte*, void> callback, void* userdata);
[LibraryImport(nativeLibName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static unsafe partial SDLBool SDL_AddEventWatch(delegate* unmanaged[Cdecl] <void*, SDL_Event*, byte> filter, void* userdata);
[LibraryImport(nativeLibName)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static unsafe partial void SDL_RemoveEventWatch(delegate* unmanaged[Cdecl] <void*, SDL_Event*, byte> filter, void* userdata);
[LibraryImport(nativeLibName, StringMarshalling = StringMarshalling.Utf8)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static unsafe partial void SDL_ShowFileDialogWithProperties(int type, delegate* unmanaged[Cdecl]<void*, byte**, int, void> callback, void* userdata, uint properties);
[LibraryImport(nativeLibName, EntryPoint = "SDL_WaitEvent")]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static partial SDLBool SDL_WaitEventRef(ref SDL_Event @event);
public const byte SDL_BUTTON_LEFT = 1;
public const byte SDL_BUTTON_MIDDLE = 2;
public const byte SDL_BUTTON_RIGHT = 3;
public const byte SDL_BUTTON_X1 = 4;
public const byte SDL_BUTTON_X2 = 5;
public const int SDL_GL_CONTEXT_PROFILE_CORE = 0x0001;
public const int SDL_GL_CONTEXT_PROFILE_COMPATIBILITY = 0x0002;
public const int SDL_GL_CONTEXT_PROFILE_ES = 0x0004;
public const int SDL_GL_CONTEXT_DEBUG_FLAG = 0x0001;
public const int SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG = 0x0002;
public const int SDL_GL_CONTEXT_ROBUST_ACCESS_FLAG = 0x0004;
public const int SDL_GL_CONTEXT_RESET_ISOLATION_FLAG = 0x0008;
public const int SDL_FILEDIALOG_OPENFILE = 0;
public const int SDL_FILEDIALOG_SAVEFILE = 1;
public const int SDL_FILEDIALOG_OPENFOLDER = 2;
public const string SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER = "SDL.filedialog.nfilters";
public const string SDL_PROP_FILE_DIALOG_FILTERS_POINTER = "SDL.filedialog.filters";
public static int SDL_VERSIONNUM_MAJOR(int version) => version / 1000000;
public static int SDL_VERSIONNUM_MINOR(int version) => version / 1000 % 1000;
public static int SDL_VERSIONNUM_MICRO(int version) => version % 1000;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,224 +0,0 @@
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Robust.Client.Input;
using Robust.Shared;
using static SDL2.SDL;
using static SDL2.SDL.SDL_Scancode;
using Key = Robust.Client.Input.Keyboard.Key;
using Button = Robust.Client.Input.Mouse.Button;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl
{
// Indices are values of SDL_Scancode
private static readonly Key[] KeyMap;
private static readonly FrozenDictionary<Key, SDL_Scancode> KeyMapReverse;
private static readonly Button[] MouseButtonMap;
// TODO: to avoid having to ask the windowing thread, key names are cached.
private readonly Dictionary<Key, string> _printableKeyNameMap = new();
private void ReloadKeyMap()
{
// This may be ran concurrently from the windowing thread.
lock (_printableKeyNameMap)
{
_printableKeyNameMap.Clear();
// List of mappable keys from SDL2's source appears to be:
// entries in SDL_default_keymap that aren't an SDLK_ enum reference.
// (the actual logic is more nuanced, but it appears to match the above)
// Comes out to these two ranges:
for (var k = SDL_SCANCODE_A; k <= SDL_SCANCODE_0; k++)
{
CacheKey(k);
}
for (var k = SDL_SCANCODE_MINUS; k <= SDL_SCANCODE_SLASH; k++)
{
CacheKey(k);
}
void CacheKey(SDL_Scancode scancode)
{
var rKey = ConvertSdl2Scancode(scancode);
if (rKey == Key.Unknown)
return;
var name = SDL_GetKeyName(SDL_GetKeyFromScancode(scancode));
if (!string.IsNullOrEmpty(name))
_printableKeyNameMap.Add(rKey, name);
}
}
}
public string? KeyGetName(Key key)
{
lock (_printableKeyNameMap)
{
if (_printableKeyNameMap.TryGetValue(key, out var name))
return name;
return null;
}
}
internal static Key ConvertSdl2Scancode(SDL_Scancode scancode)
{
return KeyMap[(int) scancode];
}
public static Button ConvertSdl2Button(int button)
{
return MouseButtonMap[button];
}
static Sdl2WindowingImpl()
{
MouseButtonMap = new Button[6];
MouseButtonMap[SDL_BUTTON_LEFT] = Button.Left;
MouseButtonMap[SDL_BUTTON_RIGHT] = Button.Right;
MouseButtonMap[SDL_BUTTON_MIDDLE] = Button.Middle;
MouseButtonMap[SDL_BUTTON_X1] = Button.Button4;
MouseButtonMap[SDL_BUTTON_X2] = Button.Button5;
KeyMap = new Key[(int) SDL_NUM_SCANCODES];
MapKey(SDL_SCANCODE_A, Key.A);
MapKey(SDL_SCANCODE_B, Key.B);
MapKey(SDL_SCANCODE_C, Key.C);
MapKey(SDL_SCANCODE_D, Key.D);
MapKey(SDL_SCANCODE_E, Key.E);
MapKey(SDL_SCANCODE_F, Key.F);
MapKey(SDL_SCANCODE_G, Key.G);
MapKey(SDL_SCANCODE_H, Key.H);
MapKey(SDL_SCANCODE_I, Key.I);
MapKey(SDL_SCANCODE_J, Key.J);
MapKey(SDL_SCANCODE_K, Key.K);
MapKey(SDL_SCANCODE_L, Key.L);
MapKey(SDL_SCANCODE_M, Key.M);
MapKey(SDL_SCANCODE_N, Key.N);
MapKey(SDL_SCANCODE_O, Key.O);
MapKey(SDL_SCANCODE_P, Key.P);
MapKey(SDL_SCANCODE_Q, Key.Q);
MapKey(SDL_SCANCODE_R, Key.R);
MapKey(SDL_SCANCODE_S, Key.S);
MapKey(SDL_SCANCODE_T, Key.T);
MapKey(SDL_SCANCODE_U, Key.U);
MapKey(SDL_SCANCODE_V, Key.V);
MapKey(SDL_SCANCODE_W, Key.W);
MapKey(SDL_SCANCODE_X, Key.X);
MapKey(SDL_SCANCODE_Y, Key.Y);
MapKey(SDL_SCANCODE_Z, Key.Z);
MapKey(SDL_SCANCODE_0, Key.Num0);
MapKey(SDL_SCANCODE_1, Key.Num1);
MapKey(SDL_SCANCODE_2, Key.Num2);
MapKey(SDL_SCANCODE_3, Key.Num3);
MapKey(SDL_SCANCODE_4, Key.Num4);
MapKey(SDL_SCANCODE_5, Key.Num5);
MapKey(SDL_SCANCODE_6, Key.Num6);
MapKey(SDL_SCANCODE_7, Key.Num7);
MapKey(SDL_SCANCODE_8, Key.Num8);
MapKey(SDL_SCANCODE_9, Key.Num9);
MapKey(SDL_SCANCODE_KP_0, Key.NumpadNum0);
MapKey(SDL_SCANCODE_KP_1, Key.NumpadNum1);
MapKey(SDL_SCANCODE_KP_2, Key.NumpadNum2);
MapKey(SDL_SCANCODE_KP_3, Key.NumpadNum3);
MapKey(SDL_SCANCODE_KP_4, Key.NumpadNum4);
MapKey(SDL_SCANCODE_KP_5, Key.NumpadNum5);
MapKey(SDL_SCANCODE_KP_6, Key.NumpadNum6);
MapKey(SDL_SCANCODE_KP_7, Key.NumpadNum7);
MapKey(SDL_SCANCODE_KP_8, Key.NumpadNum8);
MapKey(SDL_SCANCODE_KP_9, Key.NumpadNum9);
MapKey(SDL_SCANCODE_ESCAPE, Key.Escape);
MapKey(SDL_SCANCODE_LCTRL, Key.Control);
MapKey(SDL_SCANCODE_RCTRL, Key.Control);
MapKey(SDL_SCANCODE_RSHIFT, Key.Shift);
MapKey(SDL_SCANCODE_LSHIFT, Key.Shift);
MapKey(SDL_SCANCODE_LALT, Key.Alt);
MapKey(SDL_SCANCODE_RALT, Key.Alt);
MapKey(SDL_SCANCODE_LGUI, Key.LSystem);
MapKey(SDL_SCANCODE_RGUI, Key.RSystem);
MapKey(SDL_SCANCODE_MENU, Key.Menu);
MapKey(SDL_SCANCODE_LEFTBRACKET, Key.LBracket);
MapKey(SDL_SCANCODE_RIGHTBRACKET, Key.RBracket);
MapKey(SDL_SCANCODE_SEMICOLON, Key.SemiColon);
MapKey(SDL_SCANCODE_COMMA, Key.Comma);
MapKey(SDL_SCANCODE_PERIOD, Key.Period);
MapKey(SDL_SCANCODE_APOSTROPHE, Key.Apostrophe);
MapKey(SDL_SCANCODE_SLASH, Key.Slash);
MapKey(SDL_SCANCODE_BACKSLASH, Key.BackSlash);
MapKey(SDL_SCANCODE_GRAVE, Key.Tilde);
MapKey(SDL_SCANCODE_EQUALS, Key.Equal);
MapKey(SDL_SCANCODE_SPACE, Key.Space);
MapKey(SDL_SCANCODE_RETURN, Key.Return);
MapKey(SDL_SCANCODE_KP_ENTER, Key.NumpadEnter);
MapKey(SDL_SCANCODE_BACKSPACE, Key.BackSpace);
MapKey(SDL_SCANCODE_TAB, Key.Tab);
MapKey(SDL_SCANCODE_PAGEUP, Key.PageUp);
MapKey(SDL_SCANCODE_PAGEDOWN, Key.PageDown);
MapKey(SDL_SCANCODE_END, Key.End);
MapKey(SDL_SCANCODE_HOME, Key.Home);
MapKey(SDL_SCANCODE_INSERT, Key.Insert);
MapKey(SDL_SCANCODE_DELETE, Key.Delete);
MapKey(SDL_SCANCODE_MINUS, Key.Minus);
MapKey(SDL_SCANCODE_KP_PLUS, Key.NumpadAdd);
MapKey(SDL_SCANCODE_KP_MINUS, Key.NumpadSubtract);
MapKey(SDL_SCANCODE_KP_DIVIDE, Key.NumpadDivide);
MapKey(SDL_SCANCODE_KP_MULTIPLY, Key.NumpadMultiply);
MapKey(SDL_SCANCODE_KP_DECIMAL, Key.NumpadDecimal);
MapKey(SDL_SCANCODE_LEFT, Key.Left);
MapKey(SDL_SCANCODE_RIGHT, Key.Right);
MapKey(SDL_SCANCODE_UP, Key.Up);
MapKey(SDL_SCANCODE_DOWN, Key.Down);
MapKey(SDL_SCANCODE_F1, Key.F1);
MapKey(SDL_SCANCODE_F2, Key.F2);
MapKey(SDL_SCANCODE_F3, Key.F3);
MapKey(SDL_SCANCODE_F4, Key.F4);
MapKey(SDL_SCANCODE_F5, Key.F5);
MapKey(SDL_SCANCODE_F6, Key.F6);
MapKey(SDL_SCANCODE_F7, Key.F7);
MapKey(SDL_SCANCODE_F8, Key.F8);
MapKey(SDL_SCANCODE_F9, Key.F9);
MapKey(SDL_SCANCODE_F10, Key.F10);
MapKey(SDL_SCANCODE_F11, Key.F11);
MapKey(SDL_SCANCODE_F12, Key.F12);
MapKey(SDL_SCANCODE_F13, Key.F13);
MapKey(SDL_SCANCODE_F14, Key.F14);
MapKey(SDL_SCANCODE_F15, Key.F15);
MapKey(SDL_SCANCODE_F16, Key.F16);
MapKey(SDL_SCANCODE_F17, Key.F17);
MapKey(SDL_SCANCODE_F18, Key.F18);
MapKey(SDL_SCANCODE_F19, Key.F19);
MapKey(SDL_SCANCODE_F20, Key.F20);
MapKey(SDL_SCANCODE_F21, Key.F21);
MapKey(SDL_SCANCODE_F22, Key.F22);
MapKey(SDL_SCANCODE_F23, Key.F23);
MapKey(SDL_SCANCODE_F24, Key.F24);
MapKey(SDL_SCANCODE_PAUSE, Key.Pause);
var keyMapReverse = new Dictionary<Key, SDL_Scancode>();
for (var code = 0; code < KeyMap.Length; code++)
{
var key = KeyMap[code];
if (key != Key.Unknown)
keyMapReverse[key] = (SDL_Scancode) code;
}
KeyMapReverse = keyMapReverse.ToFrozenDictionary();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void MapKey(SDL_Scancode code, Key key)
{
KeyMap[(int)code] = key;
}
}
}
}

View File

@@ -1,133 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using SDL2;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl
{
// NOTE: SDL2 calls them "displays". GLFW calls them monitors. GLFW's is the one I'm going with.
// Can't use ClydeHandle because it's not thread safe to allocate.
private int _nextMonitorId = 1;
private readonly Dictionary<int, WinThreadMonitorReg> _winThreadMonitors = new();
private readonly Dictionary<int, Sdl2MonitorReg> _monitors = new();
private void InitMonitors()
{
var numDisplays = SDL.SDL_GetNumVideoDisplays();
for (var i = 0; i < numDisplays; i++)
{
// SDL.SDL_GetDisplayDPI(i, out var ddpi, out var hdpi, out var vdpi);
// _sawmill.Info($"[{i}] {ddpi} {hdpi} {vdpi}");
WinThreadSetupMonitor(i);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void WinThreadSetupMonitor(int displayIdx)
{
var id = _nextMonitorId++;
var name = SDL.SDL_GetDisplayName(displayIdx);
var modeCount = SDL.SDL_GetNumDisplayModes(displayIdx);
SDL.SDL_GetCurrentDisplayMode(displayIdx, out var curMode);
var modes = new VideoMode[modeCount];
for (var i = 0; i < modes.Length; i++)
{
SDL.SDL_GetDisplayMode(displayIdx, i, out var mode);
modes[i] = ConvertVideoMode(mode);
}
_winThreadMonitors.Add(id, new WinThreadMonitorReg { Id = id, DisplayIdx = displayIdx });
SendEvent(new EventMonitorSetup(id, name, ConvertVideoMode(curMode), modes));
if (displayIdx == 0)
_clyde._primaryMonitorId = id;
}
private static VideoMode ConvertVideoMode(in SDL.SDL_DisplayMode mode)
{
return new()
{
Width = (ushort)mode.w,
Height = (ushort)mode.h,
RefreshRate = (ushort)mode.refresh_rate,
// TODO: set bits count based on format (I'm lazy)
RedBits = 8,
GreenBits = 8,
BlueBits = 8,
};
}
private void ProcessSetupMonitor(EventMonitorSetup ev)
{
var impl = new MonitorHandle(
ev.Id,
ev.Name,
(ev.CurrentMode.Width, ev.CurrentMode.Height),
ev.CurrentMode.RefreshRate,
ev.AllModes);
_clyde._monitorHandles.Add(ev.Id, impl);
_monitors[ev.Id] = new Sdl2MonitorReg
{
Id = ev.Id,
Handle = impl
};
}
private void WinThreadDestroyMonitor(int displayIdx)
{
var monitorId = 0;
foreach (var (id, monitorReg) in _winThreadMonitors)
{
if (monitorReg.DisplayIdx == displayIdx)
{
monitorId = id;
break;
}
}
if (monitorId == 0)
return;
// So SDL2 doesn't have a very nice indexing system for monitors like GLFW does.
// This means that, when a monitor is disconnected, all monitors *after* it get shifted down one slot.
// Now, this happens *after* the event is fired, to make matters worse.
// So we're basically trying to match unspecified SDL2 internals here. Great.
_winThreadMonitors.Remove(monitorId);
foreach (var (_, reg) in _winThreadMonitors)
{
if (reg.DisplayIdx > displayIdx)
reg.DisplayIdx -= 1;
}
SendEvent(new EventMonitorDestroy(monitorId));
}
private void ProcessEventDestroyMonitor(EventMonitorDestroy ev)
{
_monitors.Remove(ev.Id);
_clyde._monitorHandles.Remove(ev.Id);
}
private sealed class Sdl2MonitorReg : MonitorReg
{
public int Id;
}
private sealed class WinThreadMonitorReg
{
public int Id;
public int DisplayIdx;
}
}
}

View File

@@ -1,316 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Robust.Shared.Maths;
using TerraFX.Interop.Windows;
using static SDL2.SDL;
using static SDL2.SDL.SDL_EventType;
using static SDL2.SDL.SDL_SYSWM_TYPE;
using static SDL2.SDL.SDL_WindowEventID;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl
{
[UnmanagedCallersOnly(CallConvs = new []{typeof(CallConvCdecl)})]
private static unsafe int EventWatch(void* userdata, SDL_Event* sdlevent)
{
var obj = (Sdl2WindowingImpl) GCHandle.FromIntPtr((IntPtr)userdata).Target!;
ref readonly var ev = ref *sdlevent;
obj.ProcessSdl2Event(in ev);
return 0;
}
private void ProcessSdl2Event(in SDL_Event ev)
{
switch (ev.type)
{
case SDL_WINDOWEVENT:
ProcessSdl2EventWindow(in ev.window);
break;
case SDL_KEYDOWN:
case SDL_KEYUP:
ProcessSdl2KeyEvent(in ev.key);
break;
case SDL_TEXTINPUT:
ProcessSdl2EventTextInput(in ev.text);
break;
case SDL_TEXTEDITING:
ProcessSdl2EventTextEditing(in ev.edit);
break;
case SDL_KEYMAPCHANGED:
ProcessSdl2EventKeyMapChanged();
break;
case SDL_TEXTEDITING_EXT:
ProcessSdl2EventTextEditingExt(in ev.editExt);
break;
case SDL_MOUSEMOTION:
ProcessSdl2EventMouseMotion(in ev.motion);
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
ProcessSdl2EventMouseButton(in ev.button);
break;
case SDL_MOUSEWHEEL:
ProcessSdl2EventMouseWheel(in ev.wheel);
break;
case SDL_DISPLAYEVENT:
ProcessSdl2EventDisplay(in ev.display);
break;
case SDL_SYSWMEVENT:
ProcessSdl2EventSysWM(in ev.syswm);
break;
case SDL_QUIT:
ProcessSdl2EventQuit();
break;
}
}
private void ProcessSdl2EventQuit()
{
SendEvent(new EventQuit());
}
private void ProcessSdl2EventDisplay(in SDL_DisplayEvent evDisplay)
{
switch (evDisplay.displayEvent)
{
case SDL_DisplayEventID.SDL_DISPLAYEVENT_CONNECTED:
WinThreadSetupMonitor((int) evDisplay.display);
break;
case SDL_DisplayEventID.SDL_DISPLAYEVENT_DISCONNECTED:
WinThreadDestroyMonitor((int) evDisplay.display);
break;
}
}
private void ProcessSdl2EventMouseWheel(in SDL_MouseWheelEvent ev)
{
SendEvent(new EventWheel(ev.windowID, ev.preciseX, ev.preciseY));
}
private void ProcessSdl2EventMouseButton(in SDL_MouseButtonEvent ev)
{
SendEvent(new EventMouseButton(ev.windowID, ev.type, ev.button));
}
private void ProcessSdl2EventMouseMotion(in SDL_MouseMotionEvent ev)
{
// _sawmill.Info($"{evMotion.x}, {evMotion.y}, {evMotion.xrel}, {evMotion.yrel}");
SendEvent(new EventMouseMotion(ev.windowID, ev.x, ev.y, ev.xrel, ev.yrel));
}
private unsafe void ProcessSdl2EventTextInput(in SDL_TextInputEvent ev)
{
fixed (byte* text = ev.text)
{
var str = Marshal.PtrToStringUTF8((IntPtr)text) ?? "";
// _logManager.GetSawmill("ime").Debug($"Input: {str}");
SendEvent(new EventText(ev.windowID, str));
}
}
private unsafe void ProcessSdl2EventTextEditing(in SDL_TextEditingEvent ev)
{
fixed (byte* text = ev.text)
{
SendTextEditing(ev.windowID, text, ev.start, ev.length);
}
}
private unsafe void ProcessSdl2EventTextEditingExt(in SDL_TextEditingExtEvent ev)
{
SendTextEditing(ev.windowID, (byte*) ev.text, ev.start, ev.length);
SDL_free(ev.text);
}
private unsafe void SendTextEditing(uint window, byte* text, int start, int length)
{
var str = Marshal.PtrToStringUTF8((nint) text) ?? "";
// _logManager.GetSawmill("ime").Debug($"Editing: '{str}', start: {start}, len: {length}");
SendEvent(new EventTextEditing(window, str, start, length));
}
private void ProcessSdl2EventKeyMapChanged()
{
ReloadKeyMap();
SendEvent(new EventKeyMapChanged());
}
private void ProcessSdl2KeyEvent(in SDL_KeyboardEvent ev)
{
SendEvent(new EventKey(
ev.windowID,
ev.keysym.scancode,
ev.type,
ev.repeat != 0,
ev.keysym.mod));
}
private void ProcessSdl2EventWindow(in SDL_WindowEvent ev)
{
var window = SDL_GetWindowFromID(ev.windowID);
switch (ev.windowEvent)
{
case SDL_WINDOWEVENT_SIZE_CHANGED:
var width = ev.data1;
var height = ev.data2;
SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
var (xScale, yScale) = GetWindowScale(window);
_sawmill.Debug($"{width}x{height}, {fbW}x{fbH}, {xScale}x{yScale}");
SendEvent(new EventWindowSize(ev.windowID, width, height, fbW, fbH, xScale, yScale));
break;
default:
SendEvent(new EventWindow(ev.windowID, ev.windowEvent));
break;
}
}
// ReSharper disable once InconsistentNaming
private unsafe void ProcessSdl2EventSysWM(in SDL_SysWMEvent ev)
{
ref readonly var sysWmMessage = ref *(SDL_SysWMmsg*)ev.msg;
if (sysWmMessage.subsystem != SDL_SYSWM_WINDOWS)
return;
ref readonly var winMessage = ref *(SDL_SysWMmsgWin32*)ev.msg;
if (winMessage.msg is WM.WM_KEYDOWN or WM.WM_KEYUP)
{
TryWin32VirtualVKey(in winMessage);
}
}
private void TryWin32VirtualVKey(in SDL_SysWMmsgWin32 msg)
{
// Workaround for https://github.com/ocornut/imgui/issues/2977
// This is gonna bite me in the ass if SDL2 ever fixes this upstream, isn't it...
// (I spent disproportionate amounts of effort on this).
// Code for V key.
if ((int)msg.wParam is not (0x56 or VK.VK_CONTROL))
return;
var scanCode = (msg.lParam >> 16) & 0xFF;
if (scanCode != 0)
return;
SendEvent(new EventWindowsFakeV(msg.hwnd, msg.msg, msg.wParam));
}
private abstract record EventBase;
private record EventWindowCreate(
Sdl2WindowCreateResult Result,
TaskCompletionSource<Sdl2WindowCreateResult> Tcs
) : EventBase;
private record EventKey(
uint WindowId,
SDL_Scancode Scancode,
SDL_EventType Type,
bool Repeat,
SDL_Keymod Mods
) : EventBase;
private record EventMouseMotion(
uint WindowId,
int X, int Y,
int XRel, int YRel
) : EventBase;
private record EventMouseButton(
uint WindowId,
SDL_EventType Type,
byte Button
) : EventBase;
private record EventText(
uint WindowId,
string Text
) : EventBase;
private record EventTextEditing(
uint WindowId,
string Text,
int Start,
int Length
) : EventBase;
private record EventWindowSize(
uint WindowId,
int Width,
int Height,
int FramebufferWidth,
int FramebufferHeight,
float XScale,
float YScale
) : EventBase;
private record EventWheel(
uint WindowId,
float XOffset,
float YOffset
) : EventBase;
// SDL_WindowEvents that don't have special handling like size.
private record EventWindow(
uint WindowId,
SDL_WindowEventID EventId
) : EventBase;
private record EventMonitorSetup
(
int Id,
string Name,
VideoMode CurrentMode,
VideoMode[] AllModes
) : EventBase;
private record EventMonitorDestroy
(
int Id
) : EventBase;
private record EventWindowsFakeV(HWND Window,
uint Message, WPARAM WParam) : EventBase;
private record EventKeyMapChanged : EventBase;
private record EventQuit : EventBase;
[StructLayout(LayoutKind.Sequential)]
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "IdentifierTypo")]
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
private struct SDL_SysWMmsg
{
public SDL_version version;
public SDL_SYSWM_TYPE subsystem;
}
[StructLayout(LayoutKind.Sequential)]
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "IdentifierTypo")]
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Local")]
private struct SDL_SysWMmsgWin32
{
public SDL_version version;
public SDL_SYSWM_TYPE subsystem;
public HWND hwnd;
public uint msg;
public WPARAM wParam;
public LPARAM lParam;
}
}
}

View File

@@ -1,586 +0,0 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static SDL2.SDL;
using static SDL2.SDL.SDL_bool;
using static SDL2.SDL.SDL_FlashOperation;
using static SDL2.SDL.SDL_GLattr;
using static SDL2.SDL.SDL_GLcontext;
using static SDL2.SDL.SDL_GLprofile;
using static SDL2.SDL.SDL_SYSWM_TYPE;
using static SDL2.SDL.SDL_WindowFlags;
using BOOL = TerraFX.Interop.Windows.BOOL;
using HWND = TerraFX.Interop.Windows.HWND;
using GWLP = TerraFX.Interop.Windows.GWLP;
using Windows = TerraFX.Interop.Windows.Windows;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl
{
private int _nextWindowId = 1;
public (WindowReg?, string? error) WindowCreate(
GLContextSpec? spec,
WindowCreateParameters parameters,
WindowReg? share,
WindowReg? owner)
{
nint shareWindow = 0;
nint shareContext = 0;
if (share is Sdl2WindowReg shareReg)
{
shareWindow = shareReg.Sdl2Window;
shareContext = shareReg.GlContext;
}
nint ownerPtr = 0;
if (owner is Sdl2WindowReg ownerReg)
ownerPtr = ownerReg.Sdl2Window;
var task = SharedWindowCreate(spec, parameters, shareWindow, shareContext, ownerPtr);
// Block the main thread (to avoid stuff like texture uploads being problematic).
WaitWindowCreate(task);
#pragma warning disable RA0004
// Block above ensured task is done, this is safe.
var (reg, error) = task.Result;
#pragma warning restore RA0004
if (reg != null)
{
reg.Owner = reg.Handle;
}
return (reg, error);
}
private void WaitWindowCreate(Task<Sdl2WindowCreateResult> windowTask)
{
while (!windowTask.IsCompleted)
{
// Keep processing events until the window task gives either an error or success.
WaitEvents();
ProcessEvents(single: true);
}
}
private Task<Sdl2WindowCreateResult> SharedWindowCreate(
GLContextSpec? glSpec,
WindowCreateParameters parameters,
nint shareWindow,
nint shareContext,
nint owner)
{
//
// IF YOU'RE WONDERING WHY THIS IS TASK-BASED:
// I originally wanted this to be async so we could avoid blocking the main thread
// while the OS takes its stupid 100~ms just to initialize a fucking GL context.
// This doesn't *work* because
// we have to release the GL context while the shared context is being created.
// (at least on WGL, I didn't test other platforms and I don't care to.)
// Not worth it to avoid a main thread blockage by allowing Clyde to temporarily release the GL context,
// because rendering would be locked up *anyways*.
//
// Basically what I'm saying is that everything about OpenGL is a fucking mistake
// and I should get on either Veldrid or Vulkan some time.
// Probably Veldrid tbh.
//
// Yes we ping-pong this TCS through the window thread and back, deal with it.
var tcs = new TaskCompletionSource<Sdl2WindowCreateResult>();
SendCmd(new CmdWinCreate(glSpec, parameters, shareWindow, shareContext, owner, tcs));
return tcs.Task;
}
private static void FinishWindowCreate(EventWindowCreate ev)
{
var (res, tcs) = ev;
tcs.TrySetResult(res);
}
private void WinThreadWinCreate(CmdWinCreate cmd)
{
var (glSpec, parameters, shareWindow, shareContext, owner, tcs) = cmd;
var (window, context) = CreateSdl2WindowForRenderer(glSpec, parameters, shareWindow, shareContext, owner);
if (window == 0)
{
var err = SDL_GetError();
SendEvent(new EventWindowCreate(new Sdl2WindowCreateResult(null, err), tcs));
return;
}
// We can't invoke the TCS directly from the windowing thread because:
// * it'd hit the synchronization context,
// which would make (blocking) main window init more annoying.
// * it'd not be synchronized to other incoming window events correctly which might be icky.
// So we send the TCS back to the game thread
// which processes events in the correct order and has better control of stuff during init.
var reg = WinThreadSetupWindow(window, context);
SendEvent(new EventWindowCreate(new Sdl2WindowCreateResult(reg, null), tcs));
}
private static void WinThreadWinDestroy(CmdWinDestroy cmd)
{
if (OperatingSystem.IsWindows() && cmd.HadOwner)
{
// On Windows, closing the child window causes the owner to be minimized, apparently.
// Clear owner on close to avoid this.
SDL_SysWMinfo wmInfo = default;
SDL_VERSION(out wmInfo.version);
if (SDL_GetWindowWMInfo(cmd.Window, ref wmInfo) == SDL_TRUE && wmInfo.subsystem == SDL_SYSWM_WINDOWS)
{
var hWnd = (HWND)wmInfo.info.win.window;
DebugTools.Assert(hWnd != HWND.NULL);
Windows.SetWindowLongPtrW(
hWnd,
GWLP.GWLP_HWNDPARENT,
0);
}
}
SDL_DestroyWindow(cmd.Window);
}
private (nint window, nint context) CreateSdl2WindowForRenderer(
GLContextSpec? spec,
WindowCreateParameters parameters,
nint shareWindow,
nint shareContext,
nint ownerWindow)
{
var windowFlags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
if (spec is { } s)
{
windowFlags |= SDL_WINDOW_OPENGL;
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, s.Profile == GLContextProfile.Es ? 0 : 1);
SDL_GLcontext ctxFlags = 0;
#if DEBUG
ctxFlags |= SDL_GL_CONTEXT_DEBUG_FLAG;
#endif
if (s.Profile == GLContextProfile.Core)
ctxFlags |= SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG;
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, (int)ctxFlags);
if (shareContext != 0)
{
SDL_GL_MakeCurrent(shareWindow, shareContext);
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
}
else
{
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
}
var profile = s.Profile switch
{
GLContextProfile.Compatibility => SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
GLContextProfile.Core => SDL_GL_CONTEXT_PROFILE_CORE,
GLContextProfile.Es => SDL_GL_CONTEXT_PROFILE_ES,
_ => SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
};
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, profile);
SDL_SetHint("SDL_OPENGL_ES_DRIVER", s.CreationApi == GLContextCreationApi.Egl ? "1" : "0");
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, s.Major);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, s.Minor);
if (s.CreationApi == GLContextCreationApi.Egl)
WsiShared.EnsureEglAvailable();
}
if (OperatingSystem.IsMacOS())
{
windowFlags |= SDL_WINDOW_ALLOW_HIGHDPI;
}
if (parameters.Fullscreen)
{
windowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
}
nint window = SDL_CreateWindow(
"",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
parameters.Width, parameters.Height,
windowFlags);
if (window == 0)
return default;
nint glContext = SDL_GL_CreateContext(window);
if (glContext == 0)
{
SDL_DestroyWindow(window);
return default;
}
// TODO: Monitors, window maximize.
// TODO: a bunch of win32 calls for funny window properties I still haven't ported to other platforms.
// Make sure window thread doesn't keep hold of the GL context.
SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
if (OperatingSystem.IsWindows())
{
SDL_SysWMinfo info = default;
SDL_VERSION(out info.version);
if (SDL_GetWindowWMInfo(window, ref info) == SDL_TRUE && info.subsystem == SDL_SYSWM_WINDOWS)
WsiShared.WindowsSharedWindowCreate((HWND) info.info.win.window, _cfg);
}
if (parameters.Visible)
SDL_ShowWindow(window);
return (window, glContext);
}
private unsafe Sdl2WindowReg WinThreadSetupWindow(nint window, nint context)
{
var reg = new Sdl2WindowReg
{
Sdl2Window = window,
GlContext = context,
WindowId = SDL_GetWindowID(window),
Id = new WindowId(_nextWindowId++)
};
var handle = new WindowHandle(_clyde, reg);
reg.Handle = handle;
SDL_VERSION(out reg.SysWMinfo.version);
var res = SDL_GetWindowWMInfo(window, ref reg.SysWMinfo);
if (res == SDL_FALSE)
_sawmill.Error("Failed to get window WM info: {error}", SDL_GetError());
// LoadWindowIcon(window);
SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
reg.FramebufferSize = (fbW, fbH);
reg.WindowScale = GetWindowScale(window);
SDL_GetWindowSize(window, out var w, out var h);
reg.PrevWindowSize = reg.WindowSize = (w, h);
SDL_GetWindowPosition(window, out var x, out var y);
reg.PrevWindowPos = (x, y);
reg.PixelRatio = reg.FramebufferSize / (Vector2) reg.WindowSize;
return reg;
}
public void WindowDestroy(WindowReg window)
{
var reg = (Sdl2WindowReg) window;
SendCmd(new CmdWinDestroy(reg.Sdl2Window, window.Owner != null));
}
public void UpdateMainWindowMode()
{
if (_clyde._mainWindow == null)
return;
var win = (Sdl2WindowReg) _clyde._mainWindow;
SendCmd(new CmdWinWinSetMode(win.Sdl2Window, _clyde._windowMode));
}
private static void WinThreadWinSetMode(CmdWinWinSetMode cmd)
{
var flags = cmd.Mode switch
{
WindowMode.Fullscreen => (uint) SDL_WINDOW_FULLSCREEN_DESKTOP,
_ => 0u
};
SDL_SetWindowFullscreen(cmd.Window, flags);
}
public void WindowSetTitle(WindowReg window, string title)
{
SendCmd(new CmdWinSetTitle(WinPtr(window), title));
}
private static void WinThreadWinSetTitle(CmdWinSetTitle cmd)
{
SDL_SetWindowTitle(cmd.Window, cmd.Title);
}
public void WindowSetMonitor(WindowReg window, IClydeMonitor monitor)
{
// API isn't really used and kinda wack, don't feel like figuring it out for SDL2 yet.
_sawmill.Warning("WindowSetMonitor not implemented on SDL2");
}
public void WindowSetSize(WindowReg window, Vector2i size)
{
SendCmd(new CmdWinSetSize(WinPtr(window), size.X, size.Y));
}
public void WindowSetVisible(WindowReg window, bool visible)
{
window.IsVisible = visible;
SendCmd(new CmdWinSetVisible(WinPtr(window), visible));
}
private static void WinThreadWinSetSize(CmdWinSetSize cmd)
{
SDL_SetWindowSize(cmd.Window, cmd.W, cmd.H);
}
private static void WinThreadWinSetVisible(CmdWinSetVisible cmd)
{
if (cmd.Visible)
SDL_ShowWindow(cmd.Window);
else
SDL_HideWindow(cmd.Window);
}
public void WindowRequestAttention(WindowReg window)
{
SendCmd(new CmdWinRequestAttention(WinPtr(window)));
}
private void WinThreadWinRequestAttention(CmdWinRequestAttention cmd)
{
var res = SDL_FlashWindow(cmd.Window, SDL_FLASH_UNTIL_FOCUSED);
if (res < 0)
_sawmill.Error("Failed to flash window: {error}", SDL_GetError());
}
public unsafe void WindowSwapBuffers(WindowReg window)
{
var reg = (Sdl2WindowReg)window;
var windowPtr = WinPtr(reg);
// On Windows, SwapBuffers does not correctly sync to the DWM compositor.
// This means OpenGL vsync is effectively broken by default on Windows.
// We manually sync via DwmFlush(). GLFW does this automatically, SDL2 does not.
//
// Windows DwmFlush logic partly taken from:
// https://github.com/love2d/love/blob/5175b0d1b599ea4c7b929f6b4282dd379fa116b8/src/modules/window/sdl/Window.cpp#L1018
// https://github.com/glfw/glfw/blob/d3ede7b6847b66cf30b067214b2b4b126d4c729b/src/wgl_context.c#L321-L340
// See also: https://github.com/libsdl-org/SDL/issues/5797
var dwmFlush = false;
var swapInterval = 0;
if (OperatingSystem.IsWindows() && !reg.Fullscreen && reg.SwapInterval > 0)
{
BOOL compositing;
// 6.2 is Windows 8
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
if (OperatingSystem.IsWindowsVersionAtLeast(6, 2)
|| Windows.SUCCEEDED(Windows.DwmIsCompositionEnabled(&compositing)) && compositing)
{
var curCtx = SDL_GL_GetCurrentContext();
var curWin = SDL_GL_GetCurrentWindow();
if (curCtx != reg.GlContext || curWin != reg.Sdl2Window)
throw new InvalidOperationException("Window context must be current!");
SDL_GL_SetSwapInterval(0);
dwmFlush = true;
swapInterval = reg.SwapInterval;
}
}
SDL_GL_SwapWindow(windowPtr);
if (dwmFlush)
{
var i = swapInterval;
while (i-- > 0)
{
Windows.DwmFlush();
}
SDL_GL_SetSwapInterval(swapInterval);
}
}
public uint? WindowGetX11Id(WindowReg window)
{
CheckWindowDisposed(window);
var reg = (Sdl2WindowReg) window;
if (reg.SysWMinfo.subsystem != SDL_SYSWM_X11)
return null;
return (uint?) reg.SysWMinfo.info.x11.window;
}
public nint? WindowGetX11Display(WindowReg window)
{
CheckWindowDisposed(window);
var reg = (Sdl2WindowReg) window;
if (reg.SysWMinfo.subsystem != SDL_SYSWM_X11)
return null;
return reg.SysWMinfo.info.x11.display;
}
public nint? WindowGetWin32Window(WindowReg window)
{
CheckWindowDisposed(window);
var reg = (Sdl2WindowReg) window;
if (reg.SysWMinfo.subsystem != SDL_SYSWM_WINDOWS)
return null;
return reg.SysWMinfo.info.win.window;
}
public void RunOnWindowThread(Action a)
{
SendCmd(new CmdRunAction(a));
}
public void TextInputSetRect(UIBox2i rect)
{
SendCmd(new CmdTextInputSetRect(new SDL_Rect
{
x = rect.Left,
y = rect.Top,
w = rect.Width,
h = rect.Height
}));
}
private static void WinThreadSetTextInputRect(CmdTextInputSetRect cmdTextInput)
{
var rect = cmdTextInput.Rect;
SDL_SetTextInputRect(ref rect);
}
public void TextInputStart()
{
SendCmd(CmdTextInputStart.Instance);
}
private static void WinThreadStartTextInput()
{
SDL_StartTextInput();
}
public void TextInputStop()
{
SendCmd(CmdTextInputStop.Instance);
}
private static void WinThreadStopTextInput()
{
SDL_StopTextInput();
}
public void ClipboardSetText(WindowReg mainWindow, string text)
{
SendCmd(new CmdSetClipboard(text));
}
private void WinThreadSetClipboard(CmdSetClipboard cmd)
{
var res = SDL_SetClipboardText(cmd.Text);
if (res < 0)
_sawmill.Error("Failed to set clipboard text: {error}", SDL_GetError());
}
public Task<string> ClipboardGetText(WindowReg mainWindow)
{
var tcs = new TaskCompletionSource<string>();
SendCmd(new CmdGetClipboard(tcs));
return tcs.Task;
}
private static void WinThreadGetClipboard(CmdGetClipboard cmd)
{
cmd.Tcs.TrySetResult(SDL_GetClipboardText());
}
private static Vector2 GetWindowScale(nint window)
{
// Get scale by diving size in pixels with size in points.
SDL_GetWindowSizeInPixels(window, out var pixW, out var pixH);
SDL_GetWindowSize(window, out var pointW, out var pointH);
// Avoiding degenerate cases, not sure if these can actually happen.
if (pixW == 0 || pixH == 0 || pointW == 0 || pointH == 0)
return new Vector2(1, 1);
var scaleH = pixW / (float) pointW;
var scaleV = pixH / (float) pointH;
// Round to 5% increments to avoid rounding errors causing constantly different scales.
scaleH = MathF.Round(scaleH * 20) / 20;
scaleV = MathF.Round(scaleV * 20) / 20;
return new Vector2(scaleH, scaleV);
}
private static void CheckWindowDisposed(WindowReg reg)
{
if (reg.IsDisposed)
throw new ObjectDisposedException("Window disposed");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nint WinPtr(WindowReg reg) => ((Sdl2WindowReg)reg).Sdl2Window;
private WindowReg? FindWindow(uint windowId)
{
foreach (var windowReg in _clyde._windows)
{
var glfwReg = (Sdl2WindowReg) windowReg;
if (glfwReg.WindowId == windowId)
return windowReg;
}
return null;
}
private sealed class Sdl2WindowReg : WindowReg
{
public nint Sdl2Window;
public uint WindowId;
public nint GlContext;
public SDL_SysWMinfo SysWMinfo;
#pragma warning disable CS0649
public bool Fullscreen;
#pragma warning restore CS0649
public int SwapInterval;
// Kept around to avoid it being GCd.
public CursorImpl? Cursor;
}
}
}

View File

@@ -1,190 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using static SDL2.SDL;
using static SDL2.SDL.SDL_LogCategory;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl : IWindowingImpl
{
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private readonly Clyde _clyde;
private GCHandle _selfGCHandle;
private readonly ISawmill _sawmill;
private readonly ISawmill _sawmillSdl2;
public Sdl2WindowingImpl(Clyde clyde, IDependencyCollection deps)
{
_clyde = clyde;
deps.InjectDependencies(this, true);
_sawmill = _logManager.GetSawmill("clyde.win");
_sawmillSdl2 = _logManager.GetSawmill("clyde.win.sdl2");
}
public bool Init()
{
InitChannels();
if (!InitSdl2())
return false;
return true;
}
private unsafe bool InitSdl2()
{
_selfGCHandle = GCHandle.Alloc(this, GCHandleType.Normal);
SDL_LogSetAllPriority(SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE);
SDL_LogSetOutputFunction(&LogOutputFunction, (void*) GCHandle.ToIntPtr(_selfGCHandle));
SDL_SetHint("SDL_WINDOWS_DPI_SCALING", "1");
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, "1");
SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
var res = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
if (res < 0)
{
_sawmill.Fatal("Failed to initialize SDL2: {error}", SDL_GetError());
return false;
}
SDL_GetVersion(out var version);
var videoDriver = SDL_GetCurrentVideoDriver();
_sawmill.Debug(
"SDL2 initialized, version: {major}.{minor}.{patch}, video driver: {videoDriver}", version.major, version.minor, version.patch, videoDriver);
_sdlEventWakeup = SDL_RegisterEvents(1);
SDL_EventState(SDL_EventType.SDL_SYSWMEVENT, SDL_ENABLE);
InitCursors();
InitMonitors();
ReloadKeyMap();
SDL_AddEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
// SDL defaults to having text input enabled, so we have to manually turn it off in init for consistency.
// If we don't, text input will remain enabled *until* the user first leaves a LineEdit/TextEdit.
SDL_StopTextInput();
return true;
}
public unsafe void Shutdown()
{
if (_selfGCHandle != default)
{
SDL_DelEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
_selfGCHandle.Free();
}
SDL_LogSetOutputFunction(null, null);
if (SDL_WasInit(0) != 0)
{
_sawmill.Debug("Terminating SDL2");
SDL_Quit();
}
}
public void FlushDispose()
{
// Not currently used
}
public void GLMakeContextCurrent(WindowReg? reg)
{
int res;
if (reg is Sdl2WindowReg sdlReg)
res = SDL_GL_MakeCurrent(sdlReg.Sdl2Window, sdlReg.GlContext);
else
res = SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
if (res < 0)
_sawmill.Error("SDL_GL_MakeCurrent failed: {error}", SDL_GetError());
}
public void GLSwapInterval(WindowReg reg, int interval)
{
((Sdl2WindowReg)reg).SwapInterval = interval;
SDL_GL_SetSwapInterval(interval);
}
public unsafe void* GLGetProcAddress(string procName)
{
return (void*) SDL_GL_GetProcAddress(procName);
}
public string GetDescription()
{
SDL_GetVersion(out var version);
_sawmill.Debug(
"SDL2 initialized, version: {major}.{minor}.{patch}", version.major, version.minor, version.patch);
var videoDriver = SDL_GetCurrentVideoDriver();
return $"SDL2 {version.major}.{version.minor}.{version.patch} ({videoDriver})";
}
[UnmanagedCallersOnly(CallConvs = new []{typeof(CallConvCdecl)})]
private static unsafe void LogOutputFunction(
void* userdata,
int category,
SDL_LogPriority priority,
byte* message)
{
var obj = (Sdl2WindowingImpl) GCHandle.FromIntPtr((IntPtr)userdata).Target!;
var level = priority switch
{
SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE => LogLevel.Verbose,
SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG => LogLevel.Debug,
SDL_LogPriority.SDL_LOG_PRIORITY_INFO => LogLevel.Info,
SDL_LogPriority.SDL_LOG_PRIORITY_WARN => LogLevel.Warning,
SDL_LogPriority.SDL_LOG_PRIORITY_ERROR => LogLevel.Error,
SDL_LogPriority.SDL_LOG_PRIORITY_CRITICAL => LogLevel.Fatal,
_ => LogLevel.Error
};
var msg = Marshal.PtrToStringUTF8((IntPtr) message) ?? "";
if (msg == "That operation is not supported")
{
obj._sawmillSdl2.Info(Environment.StackTrace);
}
var categoryName = SdlLogCategoryName(category);
obj._sawmillSdl2.Log(level, $"[{categoryName}] {msg}");
}
private static string SdlLogCategoryName(int category)
{
return (SDL_LogCategory) category switch {
// @formatter:off
SDL_LOG_CATEGORY_APPLICATION => "application",
SDL_LOG_CATEGORY_ERROR => "error",
SDL_LOG_CATEGORY_ASSERT => "assert",
SDL_LOG_CATEGORY_SYSTEM => "system",
SDL_LOG_CATEGORY_AUDIO => "audio",
SDL_LOG_CATEGORY_VIDEO => "video",
SDL_LOG_CATEGORY_RENDER => "render",
SDL_LOG_CATEGORY_INPUT => "input",
SDL_LOG_CATEGORY_TEST => "test",
_ => "unknown"
// @formatter:on
};
}
}
}

View File

@@ -3,16 +3,15 @@ using System.Collections.Generic;
using Robust.Client.Utility;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SDL3;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using static SDL2.SDL;
using static SDL2.SDL.SDL_SystemCursor;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl : IWindowingImpl
private sealed partial class Sdl3WindowingImpl
{
private readonly Dictionary<ClydeHandle, WinThreadCursorReg> _winThreadCursors = new();
private readonly CursorImpl[] _standardCursors = new CursorImpl[(int)StandardCursorShape.CountCursors];
@@ -28,31 +27,32 @@ internal partial class Clyde
image.GetPixelSpan().CopyTo(cloneImg.GetPixelSpan());
var id = _clyde.AllocRid();
SendCmd(new CmdCursorCreate(cloneImg, hotSpot, id));
SendCmd(new CmdCursorCreate { Bytes = cloneImg, Hotspot = hotSpot, Cursor = id });
return new CursorImpl(this, id, false);
}
private unsafe void WinThreadCursorCreate(CmdCursorCreate cmd)
{
var (img, (hotX, hotY), id) = cmd;
using var img = cmd.Bytes;
fixed (Rgba32* pixPtr = img.GetPixelSpan())
{
var surface = SDL_CreateRGBSurfaceWithFormatFrom(
var surface = SDL.SDL_CreateSurfaceFrom(
img.Width,
img.Height,
SDL.SDL_PixelFormat.SDL_PIXELFORMAT_ABGR8888,
(IntPtr)pixPtr,
img.Width, img.Height, 0,
sizeof(Rgba32) * img.Width,
SDL_PIXELFORMAT_RGBA8888);
sizeof(Rgba32) * img.Width);
var cursor = SDL_CreateColorCursor(surface, hotX, hotY);
var cursor = SDL.SDL_CreateColorCursor(surface, cmd.Hotspot.X, cmd.Hotspot.Y);
if (cursor == 0)
throw new InvalidOperationException("SDL_CreateColorCursor failed");
_winThreadCursors.Add(id, new WinThreadCursorReg { Ptr = cursor });
_winThreadCursors.Add(cmd.Cursor, new WinThreadCursorReg { Ptr = cursor });
SDL_FreeSurface(surface);
SDL.SDL_DestroySurface(surface);
}
img.Dispose();
}
public void CursorSet(WindowReg window, ICursor? cursor)
@@ -62,7 +62,7 @@ internal partial class Clyde
// SDL_SetCursor(NULL) does redraw, not reset.
cursor ??= CursorGetStandard(StandardCursorShape.Arrow);
var reg = (Sdl2WindowReg)window;
var reg = (Sdl3WindowReg)window;
if (reg.Cursor == cursor)
return;
@@ -74,7 +74,7 @@ internal partial class Clyde
throw new ObjectDisposedException(nameof(cursor));
reg.Cursor = impl;
SendCmd(new CmdWinCursorSet(reg.Sdl2Window, impl.Id));
SendCmd(new CmdWinCursorSet { Window = reg.Sdl3Window, Cursor = impl.Id });
}
private void WinThreadWinCursorSet(CmdWinCursorSet cmd)
@@ -83,22 +83,22 @@ internal partial class Clyde
var ptr = _winThreadCursors[cmd.Cursor].Ptr;
// TODO: multi-window??
SDL_SetCursor(ptr);
SDL.SDL_SetCursor(ptr);
}
private void InitCursors()
{
Add(StandardCursorShape.Arrow, SDL_SYSTEM_CURSOR_ARROW);
Add(StandardCursorShape.IBeam, SDL_SYSTEM_CURSOR_IBEAM);
Add(StandardCursorShape.Crosshair, SDL_SYSTEM_CURSOR_CROSSHAIR);
Add(StandardCursorShape.Hand, SDL_SYSTEM_CURSOR_HAND);
Add(StandardCursorShape.HResize, SDL_SYSTEM_CURSOR_SIZEWE);
Add(StandardCursorShape.VResize, SDL_SYSTEM_CURSOR_SIZENS);
Add(StandardCursorShape.Arrow, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_DEFAULT);
Add(StandardCursorShape.IBeam, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_TEXT);
Add(StandardCursorShape.Crosshair, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_CROSSHAIR);
Add(StandardCursorShape.Hand, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_POINTER);
Add(StandardCursorShape.HResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_EW_RESIZE);
Add(StandardCursorShape.VResize, SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NS_RESIZE);
void Add(StandardCursorShape shape, SDL_SystemCursor sysCursor)
void Add(StandardCursorShape shape, SDL.SDL_SystemCursor sysCursor)
{
var id = _clyde.AllocRid();
var cursor = SDL_CreateSystemCursor(sysCursor);
var cursor = SDL.SDL_CreateSystemCursor(sysCursor);
var impl = new CursorImpl(this, id, true);
@@ -107,13 +107,21 @@ internal partial class Clyde
}
}
private void WinThreadCursorDestroy(CmdCursorDestroy cmd)
{
if (!_winThreadCursors.TryGetValue(cmd.Cursor, out var cursor))
return;
SDL.SDL_DestroyCursor(cursor.Ptr);
}
private sealed class CursorImpl : ICursor
{
private readonly bool _standard;
public Sdl2WindowingImpl Owner { get; }
public Sdl3WindowingImpl Owner { get; }
public ClydeHandle Id { get; private set; }
public CursorImpl(Sdl2WindowingImpl clyde, ClydeHandle id, bool standard)
public CursorImpl(Sdl3WindowingImpl clyde, ClydeHandle id, bool standard)
{
_standard = standard;
Owner = clyde;
@@ -127,7 +135,7 @@ internal partial class Clyde
private void DisposeImpl()
{
Owner.SendCmd(new CmdCursorDestroy(Id));
Owner.SendCmd(new CmdCursorDestroy { Cursor = Id });
Id = default;
}
@@ -147,9 +155,5 @@ internal partial class Clyde
{
public nint Ptr;
}
private void WinThreadCursorDestroy(CmdCursorDestroy cmd)
{
}
}
}

View File

@@ -2,18 +2,16 @@
using System.Numerics;
using Robust.Client.Input;
using Robust.Shared.Map;
using TerraFX.Interop.Windows;
using static SDL2.SDL;
using static SDL2.SDL.SDL_EventType;
using static SDL2.SDL.SDL_Keymod;
using static SDL2.SDL.SDL_WindowEventID;
using SDL3;
using Key = Robust.Client.Input.Keyboard.Key;
using ET = SDL3.SDL.SDL_EventType;
using SDL_Keymod = SDL3.SDL.SDL_Keymod;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl
private sealed partial class Sdl3WindowingImpl
{
public void ProcessEvents(bool single = false)
{
@@ -47,15 +45,18 @@ internal partial class Clyde
case EventWindowCreate wCreate:
FinishWindowCreate(wCreate);
break;
case EventWindow ev:
ProcessEventWindow(ev);
case EventWindowMisc ev:
ProcessEventWindowMisc(ev);
break;
case EventKey ev:
ProcessEventKey(ev);
break;
case EventWindowSize ev:
case EventWindowPixelSize ev:
ProcessEventWindowSize(ev);
break;
case EventWindowContentScale ev:
ProcessEventWindowContentScale(ev);
break;
case EventText ev:
ProcessEventText(ev);
break;
@@ -74,9 +75,6 @@ internal partial class Clyde
case EventMonitorSetup ev:
ProcessSetupMonitor(ev);
break;
case EventWindowsFakeV ev:
ProcessWindowsFakeV(ev);
break;
case EventKeyMapChanged:
ProcessKeyMapChanged();
break;
@@ -84,7 +82,7 @@ internal partial class Clyde
ProcessEventQuit();
break;
default:
_sawmill.Error($"Unknown SDL2 event type: {evb.GetType().Name}");
_sawmill.Error($"Unknown SDL3 event type: {evb.GetType().Name}");
break;
}
}
@@ -96,7 +94,7 @@ internal partial class Clyde
_clyde.SendCloseWindow(window, new WindowRequestClosedEventArgs(window.Handle));
}
private void ProcessEventWindow(EventWindow ev)
private void ProcessEventWindowMisc(EventWindowMisc ev)
{
var window = FindWindow(ev.WindowId);
if (window == null)
@@ -104,33 +102,38 @@ internal partial class Clyde
switch (ev.EventId)
{
case SDL_WINDOWEVENT_CLOSE:
case ET.SDL_EVENT_WINDOW_CLOSE_REQUESTED:
_clyde.SendCloseWindow(window, new WindowRequestClosedEventArgs(window.Handle));
break;
case SDL_WINDOWEVENT_ENTER:
case ET.SDL_EVENT_WINDOW_MOUSE_ENTER:
_clyde._currentHoveredWindow = window;
_clyde.SendMouseEnterLeave(new MouseEnterLeaveEventArgs(window.Handle, true));
break;
case SDL_WINDOWEVENT_LEAVE:
case ET.SDL_EVENT_WINDOW_MOUSE_LEAVE:
if (_clyde._currentHoveredWindow == window)
_clyde._currentHoveredWindow = null;
_clyde.SendMouseEnterLeave(new MouseEnterLeaveEventArgs(window.Handle, false));
break;
case SDL_WINDOWEVENT_MINIMIZED:
case ET.SDL_EVENT_WINDOW_MINIMIZED:
window.IsMinimized = true;
break;
case SDL_WINDOWEVENT_RESTORED:
case ET.SDL_EVENT_WINDOW_RESTORED:
window.IsMinimized = false;
break;
case SDL_WINDOWEVENT_FOCUS_GAINED:
case ET.SDL_EVENT_WINDOW_FOCUS_GAINED:
window.IsFocused = true;
_clyde.SendWindowFocus(new WindowFocusedEventArgs(true, window.Handle));
break;
case SDL_WINDOWEVENT_FOCUS_LOST:
case ET.SDL_EVENT_WINDOW_FOCUS_LOST:
window.IsFocused = false;
_clyde.SendWindowFocus(new WindowFocusedEventArgs(false, window.Handle));
break;
case ET.SDL_EVENT_WINDOW_MOVED:
window.WindowPos = (ev.Data1, ev.Data2);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
@@ -153,10 +156,9 @@ internal partial class Clyde
if (windowReg == null)
return;
var mods = SDL_GetModState();
var button = ConvertSdl2Button(ev.Button);
var button = ConvertSdl3Button(ev.Button);
var key = Mouse.MouseButtonToKey(button);
EmitKeyEvent(key, ev.Type, false, mods, 0);
EmitKeyEvent(key, ev.Type, false, ev.Mods, 0);
}
private void ProcessEventMouseMotion(EventMouseMotion ev)
@@ -166,8 +168,7 @@ internal partial class Clyde
return;
var newPos = new Vector2(ev.X, ev.Y) * windowReg.PixelRatio;
// SDL2 does give us delta positions, but I'm worried about rounding errors thanks to DPI stuff.
var delta = newPos - windowReg.LastMousePos;
var delta = new Vector2(ev.XRel, ev.YRel);
windowReg.LastMousePos = newPos;
_clyde._currentHoveredWindow = windowReg;
@@ -185,7 +186,7 @@ internal partial class Clyde
_clyde.SendTextEditing(new TextEditingEventArgs(ev.Text, ev.Start, ev.Length));
}
private void ProcessEventWindowSize(EventWindowSize ev)
private void ProcessEventWindowSize(EventWindowPixelSize ev)
{
var window = ev.WindowId;
var width = ev.Width;
@@ -205,28 +206,31 @@ internal partial class Clyde
return;
windowReg.PixelRatio = windowReg.FramebufferSize / (Vector2)windowReg.WindowSize;
var newScale = new Vector2(ev.XScale, ev.YScale);
if (!windowReg.WindowScale.Equals(newScale))
{
windowReg.WindowScale = newScale;
_clyde.SendWindowContentScaleChanged(new WindowContentScaleEventArgs(windowReg.Handle));
}
_clyde.SendWindowResized(windowReg, oldSize);
}
private void ProcessEventKey(EventKey ev)
private void ProcessEventWindowContentScale(EventWindowContentScale ev)
{
EmitKeyEvent(ConvertSdl2Scancode(ev.Scancode), ev.Type, ev.Repeat, ev.Mods, ev.Scancode);
var windowReg = FindWindow(ev.WindowId);
if (windowReg == null)
return;
windowReg.WindowScale = new Vector2(ev.Scale, ev.Scale);
_clyde.SendWindowContentScaleChanged(new WindowContentScaleEventArgs(windowReg.Handle));
}
private void EmitKeyEvent(Key key, SDL_EventType type, bool repeat, SDL_Keymod mods, SDL_Scancode scancode)
private void ProcessEventKey(EventKey ev)
{
var shift = (mods & KMOD_SHIFT) != 0;
var alt = (mods & KMOD_ALT) != 0;
var control = (mods & KMOD_CTRL) != 0;
var system = (mods & KMOD_GUI) != 0;
EmitKeyEvent(ConvertSdl3Scancode(ev.Scancode), ev.Type, ev.Repeat, ev.Mods, ev.Scancode);
}
private void EmitKeyEvent(Key key, ET type, bool repeat, SDL.SDL_Keymod mods, SDL.SDL_Scancode scancode)
{
var shift = (mods & SDL_Keymod.SDL_KMOD_SHIFT) != 0;
var alt = (mods & SDL_Keymod.SDL_KMOD_ALT) != 0;
var control = (mods & SDL_Keymod.SDL_KMOD_CTRL) != 0;
var system = (mods & SDL_Keymod.SDL_KMOD_GUI) != 0;
var ev = new KeyEventArgs(
key,
@@ -236,36 +240,17 @@ internal partial class Clyde
switch (type)
{
case SDL_KEYUP:
case SDL_MOUSEBUTTONUP:
case ET.SDL_EVENT_KEY_UP:
case ET.SDL_EVENT_MOUSE_BUTTON_UP:
_clyde.SendKeyUp(ev);
break;
case SDL_KEYDOWN:
case SDL_MOUSEBUTTONDOWN:
case ET.SDL_EVENT_KEY_DOWN:
case ET.SDL_EVENT_MOUSE_BUTTON_DOWN:
_clyde.SendKeyDown(ev);
break;
}
}
private void ProcessWindowsFakeV(EventWindowsFakeV ev)
{
var type = (int)ev.Message switch
{
WM.WM_KEYUP => SDL_KEYUP,
WM.WM_KEYDOWN => SDL_KEYDOWN,
_ => throw new ArgumentOutOfRangeException()
};
var key = (int)ev.WParam switch
{
0x56 /* V */ => Key.V,
VK.VK_CONTROL => Key.Control,
_ => throw new ArgumentOutOfRangeException()
};
EmitKeyEvent(key, type, false, 0, 0);
}
private void ProcessKeyMapChanged()
{
_clyde.SendInputModeChanged();

View File

@@ -0,0 +1,145 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Robust.Client.UserInterface;
using SDL3;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl : IFileDialogManager
{
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
{
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
if (fileName == null)
return null;
return File.OpenRead(fileName);
}
public async Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
{
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
if (fileName == null)
return null;
try
{
return (File.Open(fileName, truncate ? FileMode.Truncate : FileMode.Open), true);
}
catch (FileNotFoundException)
{
return (File.Open(fileName, FileMode.Create), false);
}
}
private unsafe Task<string?> ShowFileDialogOfType(int type, FileDialogFilters? filters)
{
var props = SDL.SDL_CreateProperties();
SDL.SDL_DialogFileFilter* filtersAlloc = null;
if (filters != null)
{
filtersAlloc = (SDL.SDL_DialogFileFilter*)NativeMemory.Alloc(
(UIntPtr)filters.Groups.Count,
(UIntPtr)sizeof(SDL.SDL_DialogFileFilter));
SDL.SDL_SetNumberProperty(props, SDL.SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, filters.Groups.Count);
SDL.SDL_SetPointerProperty(props, SDL.SDL_PROP_FILE_DIALOG_FILTERS_POINTER, (nint)filtersAlloc);
// All these mallocs aren't gonna win any performance awards, but oh well.
for (var i = 0; i < filters.Groups.Count; i++)
{
var (name, pattern) = ConvertFilterGroup(filters.Groups[i]);
filtersAlloc[i].name = StringToNative(name);
filtersAlloc[i].pattern = StringToNative(pattern);
}
}
var task = ShowFileDialogWithProperties(type, props);
SDL.SDL_DestroyProperties(props);
if (filtersAlloc != null)
{
for (var i = 0; i < filters!.Groups.Count; i++)
{
var filter = filtersAlloc[i];
NativeMemory.Free(filter.name);
NativeMemory.Free(filter.pattern);
}
}
return task;
}
private static unsafe byte* StringToNative(string str)
{
var byteCount = Encoding.UTF8.GetByteCount(str);
var mem = (byte*) NativeMemory.Alloc((nuint)(byteCount + 1));
Encoding.UTF8.GetBytes(str, new Span<byte>(mem, byteCount));
mem[byteCount] = 0; // null-terminate
return mem;
}
private (string name, string pattern) ConvertFilterGroup(FileDialogFilters.Group group)
{
var name = string.Join(", ", group.Extensions.Select(e => $"*.{e}"));
var pattern = string.Join(";", group.Extensions);
return (name, pattern);
}
private unsafe Task<string?> ShowFileDialogWithProperties(int type, uint properties)
{
var tcs = new TaskCompletionSource<string?>();
var gcHandle = GCHandle.Alloc(new FileDialogState
{
Parent = this,
Tcs = tcs
});
SDL.SDL_ShowFileDialogWithProperties(
type,
&FileDialogCallback,
(void*)GCHandle.ToIntPtr(gcHandle),
properties);
return tcs.Task;
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe void FileDialogCallback(void* userdata, byte** filelist, int filter)
{
var stateHandle = GCHandle.FromIntPtr((IntPtr)userdata);
var state = (FileDialogState)stateHandle.Target!;
stateHandle.Free();
if (filelist == null)
{
// Error
state.Parent._sawmill.Error("File dialog failed: {error}", SDL.SDL_GetError());
state.Tcs.SetResult(null);
return;
}
// Handles null (cancelled/none selected) transparently.
var str = Marshal.PtrToStringUTF8((nint) filelist[0]);
state.Tcs.SetResult(str);
}
private sealed class FileDialogState
{
public required Sdl3WindowingImpl Parent;
public required TaskCompletionSource<string?> Tcs;
}
}
}

View File

@@ -0,0 +1,225 @@
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using SDL3;
using Key = Robust.Client.Input.Keyboard.Key;
using Button = Robust.Client.Input.Mouse.Button;
using SC = SDL3.SDL.SDL_Scancode;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl
{
// Indices are values of SDL_Scancode
private static readonly Key[] KeyMap;
private static readonly FrozenDictionary<Key, SC> KeyMapReverse;
private static readonly Button[] MouseButtonMap;
// TODO: to avoid having to ask the windowing thread, key names are cached.
private readonly Dictionary<Key, string> _printableKeyNameMap = new();
private void ReloadKeyMap()
{
// This may be ran concurrently from the windowing thread.
lock (_printableKeyNameMap)
{
_printableKeyNameMap.Clear();
// TODO: Validate this is correct in SDL3.
// List of mappable keys from SDL2's source appears to be:
// entries in SDL_default_keymap that aren't an SDLK_ enum reference.
// (the actual logic is more nuanced, but it appears to match the above)
// Comes out to these two ranges:
for (var k = SC.SDL_SCANCODE_A; k <= SC.SDL_SCANCODE_0; k++)
{
CacheKey(k);
}
for (var k = SC.SDL_SCANCODE_MINUS; k <= SC.SDL_SCANCODE_SLASH; k++)
{
CacheKey(k);
}
void CacheKey(SC scancode)
{
var rKey = ConvertSdl3Scancode(scancode);
if (rKey == Key.Unknown)
return;
// TODO: SDL_GetKeyFromScancode correct?
var name = SDL.SDL_GetKeyName(
SDL.SDL_GetKeyFromScancode(scancode, SDL.SDL_Keymod.SDL_KMOD_NONE, false));
if (!string.IsNullOrEmpty(name))
_printableKeyNameMap.Add(rKey, name);
}
}
}
public string? KeyGetName(Key key)
{
lock (_printableKeyNameMap)
{
if (_printableKeyNameMap.TryGetValue(key, out var name))
return name;
return null;
}
}
internal static Key ConvertSdl3Scancode(SC scancode)
{
return KeyMap[(int) scancode];
}
public static Button ConvertSdl3Button(int button)
{
return MouseButtonMap[button];
}
static Sdl3WindowingImpl()
{
MouseButtonMap = new Button[6];
MouseButtonMap[SDL.SDL_BUTTON_LEFT] = Button.Left;
MouseButtonMap[SDL.SDL_BUTTON_RIGHT] = Button.Right;
MouseButtonMap[SDL.SDL_BUTTON_MIDDLE] = Button.Middle;
MouseButtonMap[SDL.SDL_BUTTON_X1] = Button.Button4;
MouseButtonMap[SDL.SDL_BUTTON_X2] = Button.Button5;
KeyMap = new Key[(int) SC.SDL_SCANCODE_COUNT];
MapKey(SC.SDL_SCANCODE_A, Key.A);
MapKey(SC.SDL_SCANCODE_B, Key.B);
MapKey(SC.SDL_SCANCODE_C, Key.C);
MapKey(SC.SDL_SCANCODE_D, Key.D);
MapKey(SC.SDL_SCANCODE_E, Key.E);
MapKey(SC.SDL_SCANCODE_F, Key.F);
MapKey(SC.SDL_SCANCODE_G, Key.G);
MapKey(SC.SDL_SCANCODE_H, Key.H);
MapKey(SC.SDL_SCANCODE_I, Key.I);
MapKey(SC.SDL_SCANCODE_J, Key.J);
MapKey(SC.SDL_SCANCODE_K, Key.K);
MapKey(SC.SDL_SCANCODE_L, Key.L);
MapKey(SC.SDL_SCANCODE_M, Key.M);
MapKey(SC.SDL_SCANCODE_N, Key.N);
MapKey(SC.SDL_SCANCODE_O, Key.O);
MapKey(SC.SDL_SCANCODE_P, Key.P);
MapKey(SC.SDL_SCANCODE_Q, Key.Q);
MapKey(SC.SDL_SCANCODE_R, Key.R);
MapKey(SC.SDL_SCANCODE_S, Key.S);
MapKey(SC.SDL_SCANCODE_T, Key.T);
MapKey(SC.SDL_SCANCODE_U, Key.U);
MapKey(SC.SDL_SCANCODE_V, Key.V);
MapKey(SC.SDL_SCANCODE_W, Key.W);
MapKey(SC.SDL_SCANCODE_X, Key.X);
MapKey(SC.SDL_SCANCODE_Y, Key.Y);
MapKey(SC.SDL_SCANCODE_Z, Key.Z);
MapKey(SC.SDL_SCANCODE_0, Key.Num0);
MapKey(SC.SDL_SCANCODE_1, Key.Num1);
MapKey(SC.SDL_SCANCODE_2, Key.Num2);
MapKey(SC.SDL_SCANCODE_3, Key.Num3);
MapKey(SC.SDL_SCANCODE_4, Key.Num4);
MapKey(SC.SDL_SCANCODE_5, Key.Num5);
MapKey(SC.SDL_SCANCODE_6, Key.Num6);
MapKey(SC.SDL_SCANCODE_7, Key.Num7);
MapKey(SC.SDL_SCANCODE_8, Key.Num8);
MapKey(SC.SDL_SCANCODE_9, Key.Num9);
MapKey(SC.SDL_SCANCODE_KP_0, Key.NumpadNum0);
MapKey(SC.SDL_SCANCODE_KP_1, Key.NumpadNum1);
MapKey(SC.SDL_SCANCODE_KP_2, Key.NumpadNum2);
MapKey(SC.SDL_SCANCODE_KP_3, Key.NumpadNum3);
MapKey(SC.SDL_SCANCODE_KP_4, Key.NumpadNum4);
MapKey(SC.SDL_SCANCODE_KP_5, Key.NumpadNum5);
MapKey(SC.SDL_SCANCODE_KP_6, Key.NumpadNum6);
MapKey(SC.SDL_SCANCODE_KP_7, Key.NumpadNum7);
MapKey(SC.SDL_SCANCODE_KP_8, Key.NumpadNum8);
MapKey(SC.SDL_SCANCODE_KP_9, Key.NumpadNum9);
MapKey(SC.SDL_SCANCODE_ESCAPE, Key.Escape);
MapKey(SC.SDL_SCANCODE_LCTRL, Key.Control);
MapKey(SC.SDL_SCANCODE_RCTRL, Key.Control);
MapKey(SC.SDL_SCANCODE_RSHIFT, Key.Shift);
MapKey(SC.SDL_SCANCODE_LSHIFT, Key.Shift);
MapKey(SC.SDL_SCANCODE_LALT, Key.Alt);
MapKey(SC.SDL_SCANCODE_RALT, Key.Alt);
MapKey(SC.SDL_SCANCODE_LGUI, Key.LSystem);
MapKey(SC.SDL_SCANCODE_RGUI, Key.RSystem);
MapKey(SC.SDL_SCANCODE_MENU, Key.Menu);
MapKey(SC.SDL_SCANCODE_LEFTBRACKET, Key.LBracket);
MapKey(SC.SDL_SCANCODE_RIGHTBRACKET, Key.RBracket);
MapKey(SC.SDL_SCANCODE_SEMICOLON, Key.SemiColon);
MapKey(SC.SDL_SCANCODE_COMMA, Key.Comma);
MapKey(SC.SDL_SCANCODE_PERIOD, Key.Period);
MapKey(SC.SDL_SCANCODE_APOSTROPHE, Key.Apostrophe);
MapKey(SC.SDL_SCANCODE_SLASH, Key.Slash);
MapKey(SC.SDL_SCANCODE_BACKSLASH, Key.BackSlash);
MapKey(SC.SDL_SCANCODE_GRAVE, Key.Tilde);
MapKey(SC.SDL_SCANCODE_EQUALS, Key.Equal);
MapKey(SC.SDL_SCANCODE_SPACE, Key.Space);
MapKey(SC.SDL_SCANCODE_RETURN, Key.Return);
MapKey(SC.SDL_SCANCODE_KP_ENTER, Key.NumpadEnter);
MapKey(SC.SDL_SCANCODE_BACKSPACE, Key.BackSpace);
MapKey(SC.SDL_SCANCODE_TAB, Key.Tab);
MapKey(SC.SDL_SCANCODE_PAGEUP, Key.PageUp);
MapKey(SC.SDL_SCANCODE_PAGEDOWN, Key.PageDown);
MapKey(SC.SDL_SCANCODE_END, Key.End);
MapKey(SC.SDL_SCANCODE_HOME, Key.Home);
MapKey(SC.SDL_SCANCODE_INSERT, Key.Insert);
MapKey(SC.SDL_SCANCODE_DELETE, Key.Delete);
MapKey(SC.SDL_SCANCODE_MINUS, Key.Minus);
MapKey(SC.SDL_SCANCODE_KP_PLUS, Key.NumpadAdd);
MapKey(SC.SDL_SCANCODE_KP_MINUS, Key.NumpadSubtract);
MapKey(SC.SDL_SCANCODE_KP_DIVIDE, Key.NumpadDivide);
MapKey(SC.SDL_SCANCODE_KP_MULTIPLY, Key.NumpadMultiply);
MapKey(SC.SDL_SCANCODE_KP_DECIMAL, Key.NumpadDecimal);
MapKey(SC.SDL_SCANCODE_LEFT, Key.Left);
MapKey(SC.SDL_SCANCODE_RIGHT, Key.Right);
MapKey(SC.SDL_SCANCODE_UP, Key.Up);
MapKey(SC.SDL_SCANCODE_DOWN, Key.Down);
MapKey(SC.SDL_SCANCODE_F1, Key.F1);
MapKey(SC.SDL_SCANCODE_F2, Key.F2);
MapKey(SC.SDL_SCANCODE_F3, Key.F3);
MapKey(SC.SDL_SCANCODE_F4, Key.F4);
MapKey(SC.SDL_SCANCODE_F5, Key.F5);
MapKey(SC.SDL_SCANCODE_F6, Key.F6);
MapKey(SC.SDL_SCANCODE_F7, Key.F7);
MapKey(SC.SDL_SCANCODE_F8, Key.F8);
MapKey(SC.SDL_SCANCODE_F9, Key.F9);
MapKey(SC.SDL_SCANCODE_F10, Key.F10);
MapKey(SC.SDL_SCANCODE_F11, Key.F11);
MapKey(SC.SDL_SCANCODE_F12, Key.F12);
MapKey(SC.SDL_SCANCODE_F13, Key.F13);
MapKey(SC.SDL_SCANCODE_F14, Key.F14);
MapKey(SC.SDL_SCANCODE_F15, Key.F15);
MapKey(SC.SDL_SCANCODE_F16, Key.F16);
MapKey(SC.SDL_SCANCODE_F17, Key.F17);
MapKey(SC.SDL_SCANCODE_F18, Key.F18);
MapKey(SC.SDL_SCANCODE_F19, Key.F19);
MapKey(SC.SDL_SCANCODE_F20, Key.F20);
MapKey(SC.SDL_SCANCODE_F21, Key.F21);
MapKey(SC.SDL_SCANCODE_F22, Key.F22);
MapKey(SC.SDL_SCANCODE_F23, Key.F23);
MapKey(SC.SDL_SCANCODE_F24, Key.F24);
MapKey(SC.SDL_SCANCODE_PAUSE, Key.Pause);
var keyMapReverse = new Dictionary<Key, SC>();
for (var code = 0; code < KeyMap.Length; code++)
{
var key = KeyMap[code];
if (key != Key.Unknown)
keyMapReverse[key] = (SC) code;
}
KeyMapReverse = keyMapReverse.ToFrozenDictionary();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void MapKey(SC code, Key key)
{
KeyMap[(int)code] = key;
}
}
}
}

View File

@@ -0,0 +1,133 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using SDL3;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl
{
// NOTE: SDL3 calls them "displays". GLFW calls them monitors. GLFW's is the one I'm going with.
private int _nextMonitorId = 1;
private readonly Dictionary<int, WinThreadMonitorReg> _winThreadMonitors = new();
private readonly Dictionary<int, Sdl3MonitorReg> _monitors = new();
private unsafe void InitMonitors()
{
var displayList = (uint*)SDL.SDL_GetDisplays(out var count);
for (var i = 0; i < count; i++)
{
WinThreadSetupMonitor(displayList[i]);
}
SDL.SDL_free((nint)displayList);
// Needed so that monitor creation events get processed.
ProcessEvents();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private unsafe void WinThreadSetupMonitor(uint displayId)
{
var id = _nextMonitorId++;
var name = SDL.SDL_GetDisplayName(displayId);
var modePtr = (SDL.SDL_DisplayMode**)SDL.SDL_GetFullscreenDisplayModes(displayId, out var modeCount);
var curMode = (SDL.SDL_DisplayMode*)SDL.SDL_GetCurrentDisplayMode(displayId);
var modes = new VideoMode[modeCount];
for (var i = 0; i < modes.Length; i++)
{
modes[i] = ConvertVideoMode(in *modePtr[i]);
}
SDL.SDL_free((nint)modePtr);
_winThreadMonitors.Add(id, new WinThreadMonitorReg { DisplayId = displayId });
if (SDL.SDL_GetPrimaryDisplay() == displayId)
_clyde._primaryMonitorId = id;
SendEvent(new EventMonitorSetup
{
Id = id,
DisplayId = displayId,
Name = name,
AllModes = modes,
CurrentMode = ConvertVideoMode(in *curMode),
});
}
private static VideoMode ConvertVideoMode(in SDL.SDL_DisplayMode mode)
{
return new()
{
Width = (ushort)mode.w,
Height = (ushort)mode.h,
RefreshRate = (ushort)mode.refresh_rate,
// TODO: set bits count based on format (I'm lazy)
RedBits = 8,
GreenBits = 8,
BlueBits = 8,
};
}
private void ProcessSetupMonitor(EventMonitorSetup ev)
{
var impl = new MonitorHandle(
ev.Id,
ev.Name,
(ev.CurrentMode.Width, ev.CurrentMode.Height),
ev.CurrentMode.RefreshRate,
ev.AllModes);
_clyde._monitorHandles.Add(ev.Id, impl);
_monitors[ev.Id] = new Sdl3MonitorReg
{
DisplayId = ev.DisplayId,
Handle = impl
};
}
private void WinThreadDestroyMonitor(uint displayId)
{
var monitorId = GetMonitorIdFromDisplayId(displayId);
if (monitorId == 0)
return;
_winThreadMonitors.Remove(monitorId);
SendEvent(new EventMonitorDestroy { Id = monitorId });
}
private void ProcessEventDestroyMonitor(EventMonitorDestroy ev)
{
_monitors.Remove(ev.Id);
_clyde._monitorHandles.Remove(ev.Id);
}
private int GetMonitorIdFromDisplayId(uint displayId)
{
foreach (var (id, monitorReg) in _winThreadMonitors)
{
if (monitorReg.DisplayId == displayId)
{
return id;
}
}
return 0;
}
private sealed class Sdl3MonitorReg : MonitorReg
{
public uint DisplayId;
}
private sealed class WinThreadMonitorReg
{
public uint DisplayId;
}
}
}

View File

@@ -0,0 +1,282 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using SDL3;
using ET = SDL3.SDL.SDL_EventType;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl
{
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe byte EventWatch(void* userdata, SDL.SDL_Event* sdlevent)
{
var obj = (Sdl3WindowingImpl)GCHandle.FromIntPtr((IntPtr)userdata).Target!;
obj.ProcessSdl3Event(in *sdlevent);
return 0;
}
private void ProcessSdl3Event(in SDL.SDL_Event ev)
{
switch ((ET)ev.type)
{
case ET.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
ProcessSdl3EventWindowPixelSizeChanged(in ev.window);
break;
case ET.SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
ProcessSdl3EventWindowDisplayScaleChanged(in ev.window);
break;
case ET.SDL_EVENT_WINDOW_CLOSE_REQUESTED:
case ET.SDL_EVENT_WINDOW_MOUSE_ENTER:
case ET.SDL_EVENT_WINDOW_MOUSE_LEAVE:
case ET.SDL_EVENT_WINDOW_MINIMIZED:
case ET.SDL_EVENT_WINDOW_RESTORED:
case ET.SDL_EVENT_WINDOW_FOCUS_GAINED:
case ET.SDL_EVENT_WINDOW_FOCUS_LOST:
case ET.SDL_EVENT_WINDOW_MOVED:
ProcessSdl3EventWindowMisc(in ev.window);
break;
case ET.SDL_EVENT_KEY_DOWN:
case ET.SDL_EVENT_KEY_UP:
ProcessSdl3KeyEvent(in ev.key);
break;
case ET.SDL_EVENT_TEXT_INPUT:
ProcessSdl3EventTextInput(in ev.text);
break;
case ET.SDL_EVENT_TEXT_EDITING:
ProcessSdl3EventTextEditing(in ev.edit);
break;
case ET.SDL_EVENT_KEYMAP_CHANGED:
ProcessSdl3EventKeyMapChanged();
break;
case ET.SDL_EVENT_MOUSE_MOTION:
ProcessSdl3EventMouseMotion(in ev.motion);
break;
case ET.SDL_EVENT_MOUSE_BUTTON_DOWN:
case ET.SDL_EVENT_MOUSE_BUTTON_UP:
ProcessSdl3EventMouseButton(in ev.button);
break;
case ET.SDL_EVENT_MOUSE_WHEEL:
ProcessSdl3EventMouseWheel(in ev.wheel);
break;
case ET.SDL_EVENT_DISPLAY_ADDED:
WinThreadSetupMonitor(ev.display.displayID);
break;
case ET.SDL_EVENT_DISPLAY_REMOVED:
WinThreadDestroyMonitor(ev.display.displayID);
break;
case ET.SDL_EVENT_QUIT:
ProcessSdl3EventQuit();
break;
}
}
private void ProcessSdl3EventQuit()
{
SendEvent(new EventQuit());
}
private void ProcessSdl3EventMouseWheel(in SDL.SDL_MouseWheelEvent ev)
{
SendEvent(new EventWheel { WindowId = ev.windowID, XOffset = ev.x, YOffset = ev.y });
}
private void ProcessSdl3EventMouseButton(in SDL.SDL_MouseButtonEvent ev)
{
var mods = SDL.SDL_GetModState();
SendEvent(new EventMouseButton
{
WindowId = ev.windowID,
Type = ev.type,
Button = ev.button,
Mods = mods
});
}
private void ProcessSdl3EventMouseMotion(in SDL.SDL_MouseMotionEvent ev)
{
SendEvent(new EventMouseMotion
{
WindowId = ev.windowID,
X = ev.x,
Y = ev.y,
XRel = ev.xrel,
YRel = ev.yrel
});
}
private unsafe void ProcessSdl3EventTextInput(in SDL.SDL_TextInputEvent ev)
{
var str = Marshal.PtrToStringUTF8((IntPtr)ev.text) ?? "";
SendEvent(new EventText { WindowId = ev.windowID, Text = str });
}
private unsafe void ProcessSdl3EventTextEditing(in SDL.SDL_TextEditingEvent ev)
{
var str = Marshal.PtrToStringUTF8((IntPtr)ev.text) ?? "";
SendEvent(new EventTextEditing
{
WindowId = ev.windowID,
Text = str,
Start = ev.start,
Length = ev.length
});
}
private void ProcessSdl3EventKeyMapChanged()
{
ReloadKeyMap();
SendEvent(new EventKeyMapChanged());
}
private void ProcessSdl3KeyEvent(in SDL.SDL_KeyboardEvent ev)
{
SendEvent(new EventKey
{
WindowId = ev.windowID,
Scancode = ev.scancode,
Type = ev.type,
Repeat = ev.repeat,
Mods = ev.mod,
});
}
private void ProcessSdl3EventWindowPixelSizeChanged(in SDL.SDL_WindowEvent ev)
{
var window = SDL.SDL_GetWindowFromID(ev.windowID);
SDL.SDL_GetWindowSize(window, out var width, out var height);
var fbW = ev.data1;
var fbH = ev.data2;
SendEvent(new EventWindowPixelSize
{
WindowId = ev.windowID,
Width = width,
Height = height,
FramebufferWidth = fbW,
FramebufferHeight = fbH,
});
}
private void ProcessSdl3EventWindowDisplayScaleChanged(in SDL.SDL_WindowEvent ev)
{
var window = SDL.SDL_GetWindowFromID(ev.windowID);
var scale = SDL.SDL_GetWindowDisplayScale(window);
SendEvent(new EventWindowContentScale { WindowId = ev.windowID, Scale = scale });
}
private void ProcessSdl3EventWindowMisc(in SDL.SDL_WindowEvent ev)
{
SendEvent(new EventWindowMisc
{
WindowId = ev.windowID,
EventId = ev.type,
Data1 = ev.data1,
Data2 = ev.data2
});
}
private abstract class EventBase;
private sealed class EventWindowCreate : EventBase
{
public required Sdl3WindowCreateResult Result;
public required TaskCompletionSource<Sdl3WindowCreateResult> Tcs;
}
private sealed class EventKey : EventBase
{
public uint WindowId;
public SDL.SDL_Scancode Scancode;
public ET Type;
public bool Repeat;
public SDL.SDL_Keymod Mods;
}
private sealed class EventMouseMotion : EventBase
{
public uint WindowId;
public float X;
public float Y;
public float XRel;
public float YRel;
}
private sealed class EventMouseButton : EventBase
{
public uint WindowId;
public ET Type;
public byte Button;
public SDL.SDL_Keymod Mods;
}
private sealed class EventText : EventBase
{
public uint WindowId;
public required string Text;
}
private sealed class EventTextEditing : EventBase
{
public uint WindowId;
public required string Text;
public int Start;
public int Length;
}
private sealed class EventWindowPixelSize : EventBase
{
public uint WindowId;
public int Width;
public int Height;
public int FramebufferWidth;
public int FramebufferHeight;
}
private sealed class EventWindowContentScale : EventBase
{
public uint WindowId;
public float Scale;
}
private sealed class EventWheel : EventBase
{
public uint WindowId;
public float XOffset;
public float YOffset;
}
// SDL_WindowEvents that don't need any handling on the window thread itself.
private sealed class EventWindowMisc : EventBase
{
public uint WindowId;
public ET EventId;
public int Data1;
public int Data2;
}
private sealed class EventMonitorSetup : EventBase
{
public int Id;
public uint DisplayId;
public required string Name;
public VideoMode CurrentMode;
public required VideoMode[] AllModes;
}
private sealed class EventMonitorDestroy : EventBase
{
public int Id;
}
private sealed class EventKeyMapChanged : EventBase;
private sealed class EventQuit : EventBase;
}
}

View File

@@ -0,0 +1,654 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using SDL3;
using TerraFX.Interop.Windows;
using TerraFX.Interop.Xlib;
using BOOL = TerraFX.Interop.Windows.BOOL;
using Windows = TerraFX.Interop.Windows.Windows;
using GLAttr = SDL3.SDL.SDL_GLAttr;
using X11Window = TerraFX.Interop.Xlib.Window;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl
{
private int _nextWindowId = 1;
public (WindowReg?, string? error) WindowCreate(
GLContextSpec? spec,
WindowCreateParameters parameters,
WindowReg? share,
WindowReg? owner)
{
nint shareWindow = 0;
nint shareContext = 0;
if (share is Sdl3WindowReg shareReg)
{
shareWindow = shareReg.Sdl3Window;
shareContext = shareReg.GlContext;
}
nint ownerPtr = 0;
if (owner is Sdl3WindowReg ownerReg)
ownerPtr = ownerReg.Sdl3Window;
var task = SharedWindowCreate(spec, parameters, shareWindow, shareContext, ownerPtr);
// Block the main thread (to avoid stuff like texture uploads being problematic).
WaitWindowCreate(task);
#pragma warning disable RA0004
// Block above ensured task is done, this is safe.
var result = task.Result;
#pragma warning restore RA0004
if (result.Reg != null)
{
result.Reg.Owner = result.Reg.Handle;
}
return (result.Reg, result.Error);
}
private void WaitWindowCreate(Task<Sdl3WindowCreateResult> windowTask)
{
while (!windowTask.IsCompleted)
{
// Keep processing events until the window task gives either an error or success.
WaitEvents();
ProcessEvents(single: true);
}
}
private Task<Sdl3WindowCreateResult> SharedWindowCreate(
GLContextSpec? glSpec,
WindowCreateParameters parameters,
nint shareWindow,
nint shareContext,
nint owner)
{
//
// IF YOU'RE WONDERING WHY THIS IS TASK-BASED:
// I originally wanted this to be async so we could avoid blocking the main thread
// while the OS takes its stupid 100~ms just to initialize a fucking GL context.
// This doesn't *work* because
// we have to release the GL context while the shared context is being created.
// (at least on WGL, I didn't test other platforms and I don't care to.)
// Not worth it to avoid a main thread blockage by allowing Clyde to temporarily release the GL context,
// because rendering would be locked up *anyways*.
//
// Basically what I'm saying is that everything about OpenGL is a fucking mistake
// and I should get on either Veldrid or Vulkan some time.
// Probably Veldrid tbh.
//
// Yes we ping-pong this TCS through the window thread and back, deal with it.
var tcs = new TaskCompletionSource<Sdl3WindowCreateResult>();
SendCmd(new CmdWinCreate
{
GLSpec = glSpec,
Parameters = parameters,
ShareWindow = shareWindow,
ShareContext = shareContext,
OwnerWindow = owner,
Tcs = tcs
});
return tcs.Task;
}
private static void FinishWindowCreate(EventWindowCreate ev)
{
ev.Tcs.TrySetResult(ev.Result);
}
private void WinThreadWinCreate(CmdWinCreate cmd)
{
var (window, context) = CreateSdl3WindowForRenderer(
cmd.GLSpec,
cmd.Parameters,
cmd.ShareWindow,
cmd.ShareContext,
cmd.OwnerWindow);
if (window == 0)
{
var err = SDL.SDL_GetError();
SendEvent(new EventWindowCreate
{
Result = new Sdl3WindowCreateResult { Error = err },
Tcs = cmd.Tcs
});
return;
}
// We can't invoke the TCS directly from the windowing thread because:
// * it'd hit the synchronization context,
// which would make (blocking) main window init more annoying.
// * it'd not be synchronized to other incoming window events correctly which might be icky.
// So we send the TCS back to the game thread
// which processes events in the correct order and has better control of stuff during init.
var reg = WinThreadSetupWindow(window, context);
SendEvent(new EventWindowCreate
{
Result = new Sdl3WindowCreateResult { Reg = reg },
Tcs = cmd.Tcs
});
}
private static void WinThreadWinDestroy(CmdWinDestroy cmd)
{
SDL.SDL_DestroyWindow(cmd.Window);
}
private (nint window, nint context) CreateSdl3WindowForRenderer(
GLContextSpec? spec,
WindowCreateParameters parameters,
nint shareWindow,
nint shareContext,
nint ownerWindow)
{
var createProps = SDL.SDL_CreateProperties();
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, true);
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
if (spec is { } s)
{
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_RED_SIZE, 8);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_GREEN_SIZE, 8);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_BLUE_SIZE, 8);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_ALPHA_SIZE, 8);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_STENCIL_SIZE, 8);
SDL.SDL_GL_SetAttribute(
GLAttr.SDL_GL_FRAMEBUFFER_SRGB_CAPABLE,
s.Profile == GLContextProfile.Es ? 0 : 1);
int ctxFlags = 0;
#if DEBUG
ctxFlags |= SDL.SDL_GL_CONTEXT_DEBUG_FLAG;
#endif
if (s.Profile == GLContextProfile.Core)
ctxFlags |= SDL.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG;
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_FLAGS, (int)ctxFlags);
if (shareContext != 0)
{
SDL.SDL_GL_MakeCurrent(shareWindow, shareContext);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
}
else
{
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
}
var profile = s.Profile switch
{
GLContextProfile.Compatibility => SDL.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
GLContextProfile.Core => SDL.SDL_GL_CONTEXT_PROFILE_CORE,
GLContextProfile.Es => SDL.SDL_GL_CONTEXT_PROFILE_ES,
_ => SDL.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
};
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, profile);
SDL.SDL_SetHint(SDL.SDL_HINT_OPENGL_ES_DRIVER, s.CreationApi == GLContextCreationApi.Egl ? "1" : "0");
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, s.Major);
SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, s.Minor);
if (s.CreationApi == GLContextCreationApi.Egl)
WsiShared.EnsureEglAvailable();
}
if (parameters.Fullscreen)
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);
if ((parameters.Styles & OSWindowStyles.NoTitleBar) != 0)
SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);
if (ownerWindow != 0)
{
SDL.SDL_SetPointerProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_PARENT_POINTER, ownerWindow);
if (parameters.StartupLocation == WindowStartupLocation.CenterOwner)
{
SDL.SDL_GetWindowSize(ownerWindow, out var parentW, out var parentH);
SDL.SDL_GetWindowPosition(ownerWindow, out var parentX, out var parentY);
SDL.SDL_SetNumberProperty(
createProps,
SDL.SDL_PROP_WINDOW_CREATE_X_NUMBER,
parentX + (parentW - parameters.Width) / 2);
SDL.SDL_SetNumberProperty(
createProps,
SDL.SDL_PROP_WINDOW_CREATE_Y_NUMBER,
parentY + (parentH - parameters.Height) / 2);
}
}
SDL.SDL_SetNumberProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, parameters.Width);
SDL.SDL_SetNumberProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, parameters.Height);
SDL.SDL_SetStringProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_TITLE_STRING, parameters.Title);
// ---> CREATE <---
var window = SDL.SDL_CreateWindowWithProperties(createProps);
SDL.SDL_DestroyProperties(createProps);
if (window == 0)
return default;
nint glContext = SDL.SDL_GL_CreateContext(window);
if (glContext == 0)
{
SDL.SDL_DestroyWindow(window);
return default;
}
if ((parameters.Styles & OSWindowStyles.NoTitleOptions) != 0)
{
var props = SDL.SDL_GetWindowProperties(window);
switch (_videoDriver)
{
case SdlVideoDriver.Windows:
{
var hWnd = SDL.SDL_GetPointerProperty(
props,
SDL.SDL_PROP_WINDOW_WIN32_HWND_POINTER,
0);
WsiShared.SetWindowStyleNoTitleOptionsWindows((HWND)hWnd);
break;
}
case SdlVideoDriver.X11:
unsafe
{
var x11Display = (Display*)SDL.SDL_GetPointerProperty(
props,
SDL.SDL_PROP_WINDOW_X11_DISPLAY_POINTER,
0);
var x11Window = (X11Window)SDL.SDL_GetNumberProperty(
props,
SDL.SDL_PROP_WINDOW_X11_WINDOW_NUMBER,
0);
WsiShared.SetWindowStyleNoTitleOptionsX11(x11Display, x11Window);
break;
}
default:
_sawmill.Warning("OSWindowStyles.NoTitleOptions not implemented on this video driver");
break;
}
}
// TODO: Monitors, window maximize.
// Make sure window thread doesn't keep hold of the GL context.
SDL.SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
if (parameters.Visible)
SDL.SDL_ShowWindow(window);
return (window, glContext);
}
private Sdl3WindowReg WinThreadSetupWindow(nint window, nint context)
{
var reg = new Sdl3WindowReg
{
Sdl3Window = window,
GlContext = context,
WindowId = SDL.SDL_GetWindowID(window),
Id = new WindowId(_nextWindowId++)
};
var handle = new WindowHandle(_clyde, reg);
reg.Handle = handle;
var windowProps = SDL.SDL_GetWindowProperties(window);
switch (_videoDriver)
{
case SdlVideoDriver.Windows:
reg.WindowsHwnd = SDL.SDL_GetPointerProperty(
windowProps,
SDL.SDL_PROP_WINDOW_WIN32_HWND_POINTER,
0);
break;
case SdlVideoDriver.X11:
reg.X11Display = SDL.SDL_GetPointerProperty(
windowProps,
SDL.SDL_PROP_WINDOW_X11_DISPLAY_POINTER,
0);
reg.X11Id = (uint)SDL.SDL_GetNumberProperty(windowProps, SDL.SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
break;
}
AssignWindowIconToWindow(window);
SDL.SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
reg.FramebufferSize = (fbW, fbH);
var scale = SDL.SDL_GetWindowDisplayScale(window);
reg.WindowScale = new Vector2(scale, scale);
SDL.SDL_GetWindowSize(window, out var w, out var h);
reg.PrevWindowSize = reg.WindowSize = (w, h);
SDL.SDL_GetWindowPosition(window, out var x, out var y);
reg.PrevWindowPos = reg.WindowPos = (x, y);
reg.PixelRatio = reg.FramebufferSize / (Vector2)reg.WindowSize;
return reg;
}
public void WindowDestroy(WindowReg window)
{
var reg = (Sdl3WindowReg)window;
SendCmd(new CmdWinDestroy
{
Window = reg.Sdl3Window,
HadOwner = window.Owner != null
});
}
public void UpdateMainWindowMode()
{
if (_clyde._mainWindow == null)
return;
var win = (Sdl3WindowReg)_clyde._mainWindow;
if (_clyde._windowMode == WindowMode.Fullscreen)
{
win.PrevWindowSize = win.WindowSize;
win.PrevWindowPos = win.WindowPos;
SendCmd(new CmdWinWinSetFullscreen
{
Window = win.Sdl3Window,
});
}
else
{
SendCmd(new CmdWinSetWindowed
{
Window = win.Sdl3Window,
Width = win.PrevWindowSize.X,
Height = win.PrevWindowSize.Y,
PosX = win.PrevWindowPos.X,
PosY = win.PrevWindowPos.Y
});
}
}
private static void WinThreadWinSetFullscreen(CmdWinWinSetFullscreen cmd)
{
SDL.SDL_SetWindowFullscreen(cmd.Window, true);
}
private static void WinThreadWinSetWindowed(CmdWinSetWindowed cmd)
{
SDL.SDL_SetWindowFullscreen(cmd.Window, false);
SDL.SDL_SetWindowSize(cmd.Window, cmd.Width, cmd.Height);
SDL.SDL_SetWindowPosition(cmd.Window, cmd.PosX, cmd.PosY);
}
public void WindowSetTitle(WindowReg window, string title)
{
SendCmd(new CmdWinSetTitle
{
Window = WinPtr(window),
Title = title,
});
}
private static void WinThreadWinSetTitle(CmdWinSetTitle cmd)
{
SDL.SDL_SetWindowTitle(cmd.Window, cmd.Title);
}
public void WindowSetMonitor(WindowReg window, IClydeMonitor monitor)
{
// API isn't really used and kinda wack, don't feel like figuring it out for SDL3 yet.
_sawmill.Warning("WindowSetMonitor not implemented on SDL3");
}
public void WindowSetSize(WindowReg window, Vector2i size)
{
SendCmd(new CmdWinSetSize { Window = WinPtr(window), W = size.X, H = size.Y });
}
public void WindowSetVisible(WindowReg window, bool visible)
{
window.IsVisible = visible;
SendCmd(new CmdWinSetVisible { Window = WinPtr(window), Visible = visible });
}
private static void WinThreadWinSetSize(CmdWinSetSize cmd)
{
SDL.SDL_SetWindowSize(cmd.Window, cmd.W, cmd.H);
}
private static void WinThreadWinSetVisible(CmdWinSetVisible cmd)
{
if (cmd.Visible)
SDL.SDL_ShowWindow(cmd.Window);
else
SDL.SDL_HideWindow(cmd.Window);
}
public void WindowRequestAttention(WindowReg window)
{
SendCmd(new CmdWinRequestAttention { Window = WinPtr(window) });
}
private void WinThreadWinRequestAttention(CmdWinRequestAttention cmd)
{
var res = SDL.SDL_FlashWindow(cmd.Window, SDL.SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED);
if (!res)
_sawmill.Error("Failed to flash window: {error}", SDL.SDL_GetError());
}
public unsafe void WindowSwapBuffers(WindowReg window)
{
var reg = (Sdl3WindowReg)window;
var windowPtr = WinPtr(reg);
// On Windows, SwapBuffers does not correctly sync to the DWM compositor.
// This means OpenGL vsync is effectively broken by default on Windows.
// We manually sync via DwmFlush(). GLFW does this automatically, SDL3 does not.
//
// Windows DwmFlush logic partly taken from:
// https://github.com/love2d/love/blob/5175b0d1b599ea4c7b929f6b4282dd379fa116b8/src/modules/window/sdl/Window.cpp#L1018
// https://github.com/glfw/glfw/blob/d3ede7b6847b66cf30b067214b2b4b126d4c729b/src/wgl_context.c#L321-L340
// See also: https://github.com/libsdl-org/SDL/issues/5797
var dwmFlush = false;
var swapInterval = 0;
if (OperatingSystem.IsWindows() && !reg.Fullscreen && reg.SwapInterval > 0)
{
BOOL compositing;
// 6.2 is Windows 8
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
if (OperatingSystem.IsWindowsVersionAtLeast(6, 2)
|| Windows.SUCCEEDED(Windows.DwmIsCompositionEnabled(&compositing)) && compositing)
{
var curCtx = SDL.SDL_GL_GetCurrentContext();
var curWin = SDL.SDL_GL_GetCurrentWindow();
if (curCtx != reg.GlContext || curWin != reg.Sdl3Window)
throw new InvalidOperationException("Window context must be current!");
SDL.SDL_GL_SetSwapInterval(0);
dwmFlush = true;
swapInterval = reg.SwapInterval;
}
}
SDL.SDL_GL_SwapWindow(windowPtr);
if (dwmFlush)
{
var i = swapInterval;
while (i-- > 0)
{
Windows.DwmFlush();
}
SDL.SDL_GL_SetSwapInterval(swapInterval);
}
}
public uint? WindowGetX11Id(WindowReg window)
{
CheckWindowDisposed(window);
if (_videoDriver != SdlVideoDriver.X11)
return null;
var reg = (Sdl3WindowReg)window;
return reg.X11Id;
}
public nint? WindowGetX11Display(WindowReg window)
{
CheckWindowDisposed(window);
if (_videoDriver != SdlVideoDriver.X11)
return null;
var reg = (Sdl3WindowReg)window;
return reg.X11Display;
}
public nint? WindowGetWin32Window(WindowReg window)
{
CheckWindowDisposed(window);
if (_videoDriver != SdlVideoDriver.Windows)
return null;
var reg = (Sdl3WindowReg)window;
return reg.WindowsHwnd;
}
public void RunOnWindowThread(Action a)
{
SendCmd(new CmdRunAction { Action = a });
}
public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor)
{
SendCmd(new CmdTextInputSetRect
{
Window = WinPtr(reg),
Rect = new SDL.SDL_Rect
{
x = rect.Left,
y = rect.Top,
w = rect.Width,
h = rect.Height
},
Cursor = cursor
});
}
private static void WinThreadSetTextInputRect(CmdTextInputSetRect cmdTextInput)
{
var rect = cmdTextInput.Rect;
SDL.SDL_SetTextInputArea(cmdTextInput.Window, ref rect, cmdTextInput.Cursor);
}
public void TextInputStart(WindowReg reg)
{
SendCmd(new CmdTextInputStart { Window = WinPtr(reg) });
}
private static void WinThreadStartTextInput(CmdTextInputStart cmd)
{
SDL.SDL_StartTextInput(cmd.Window);
}
public void TextInputStop(WindowReg reg)
{
SendCmd(new CmdTextInputStop { Window = WinPtr(reg) });
}
private static void WinThreadStopTextInput(CmdTextInputStop cmd)
{
SDL.SDL_StopTextInput(cmd.Window);
}
public void ClipboardSetText(WindowReg mainWindow, string text)
{
SendCmd(new CmdSetClipboard { Text = text });
}
private void WinThreadSetClipboard(CmdSetClipboard cmd)
{
var res = SDL.SDL_SetClipboardText(cmd.Text);
if (res)
_sawmill.Error("Failed to set clipboard text: {error}", SDL.SDL_GetError());
}
public Task<string> ClipboardGetText(WindowReg mainWindow)
{
var tcs = new TaskCompletionSource<string>();
SendCmd(new CmdGetClipboard { Tcs = tcs });
return tcs.Task;
}
private static void WinThreadGetClipboard(CmdGetClipboard cmd)
{
cmd.Tcs.TrySetResult(SDL.SDL_GetClipboardText());
}
private static void CheckWindowDisposed(WindowReg reg)
{
if (reg.IsDisposed)
throw new ObjectDisposedException("Window disposed");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nint WinPtr(WindowReg reg) => ((Sdl3WindowReg)reg).Sdl3Window;
private WindowReg? FindWindow(uint windowId)
{
foreach (var windowReg in _clyde._windows)
{
var glfwReg = (Sdl3WindowReg)windowReg;
if (glfwReg.WindowId == windowId)
return windowReg;
}
return null;
}
private sealed class Sdl3WindowReg : WindowReg
{
public nint Sdl3Window;
public uint WindowId;
public nint GlContext;
#pragma warning disable CS0649
public bool Fullscreen;
#pragma warning restore CS0649
public int SwapInterval;
// Kept around to avoid it being GCd.
public CursorImpl? Cursor;
public nint WindowsHwnd;
public nint X11Display;
public uint X11Id;
}
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Robust.Client.Utility;
using SDL3;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using TerraFX.Interop.Windows;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed unsafe partial class Sdl3WindowingImpl
{
// Experimentally on my system, SM_CXICON is 32.
// I doubt MS is ever changing that, so...
// I wish SDL would take care of this instead of us having to figure out what the "main" icon is. Ugh.
private const int MainWindowIconSize = 32;
// Writing this out like this makes me realize we're spending multiple hundred KBs on storing the window icon.
// You know, come to think about it, what if we used LZ4 or Zstd to compress the window icon stored here?
// This is absolutely not worth the optimization but hilarious for me to think about.
// The surface used for the window icon.
// This may store additional surfaces as alternate forms.
private nint _windowIconSurface;
// The data for all the window icons surfaces.
// Must be kept around! Pinned!
// ReSharper disable once CollectionNeverQueried.Local
private byte[][]? _windowIconData;
private void LoadWindowIcons()
{
// Sort such that closest to 64 is first.
// SDL3 doesn't "figure it out itself" as much as GLFW does, which sucks.
var icons = _clyde.LoadWindowIcons().OrderBy(i => Math.Abs(i.Width - MainWindowIconSize)).ToArray();
if (icons.Length == 0)
{
// No window icons at all!
return;
}
_windowIconData = new byte[icons.Length][];
var mainImg = icons[0];
_sawmill.Verbose(
"Have {iconCount} window icons available, choosing {mainIconWidth}x{mainIconHeight} as main",
icons.Length,
mainImg.Width,
mainImg.Height);
(_windowIconSurface, var mainData) = CreateSurfaceFromImage(mainImg);
_windowIconData[0] = mainData;
for (var i = 1; i < icons.Length; i++)
{
var (surface, data) = CreateSurfaceFromImage(icons[i]);
_windowIconData[i] = data;
SDL.SDL_AddSurfaceAlternateImage(_windowIconSurface, surface);
// Kept alive by the main surface.
SDL.SDL_DestroySurface(surface);
}
return;
static (nint, byte[]) CreateSurfaceFromImage(Image<Rgba32> img)
{
var span = MemoryMarshal.AsBytes(img.GetPixelSpan());
var copied = GC.AllocateUninitializedArray<byte>(span.Length, pinned: true);
span.CopyTo(copied);
IntPtr surface;
fixed (byte* ptr = copied)
{
surface = SDL.SDL_CreateSurfaceFrom(
img.Width,
img.Height,
SDL.SDL_PixelFormat.SDL_PIXELFORMAT_ABGR8888,
(IntPtr)ptr,
sizeof(Rgba32) * img.Width);
}
return (surface, copied);
}
}
private void DestroyWindowIcons()
{
SDL.SDL_DestroySurface(_windowIconSurface);
_windowIconSurface = 0;
_windowIconData = null;
}
private void AssignWindowIconToWindow(nint window)
{
if (_windowIconSurface == 0)
return;
SDL.SDL_SetWindowIcon(window, (nint) _windowIconSurface);
}
}
}

View File

@@ -4,15 +4,15 @@ using System.Threading.Channels;
using System.Threading.Tasks;
using Robust.Shared;
using Robust.Shared.Maths;
using SDL3;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using static SDL2.SDL;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl2WindowingImpl
private sealed partial class Sdl3WindowingImpl
{
private bool _windowingRunning;
private ChannelWriter<CmdBase> _cmdWriter = default!;
@@ -29,34 +29,34 @@ internal partial class Clyde
while (_windowingRunning)
{
var res = SDL_WaitEvent(out Unsafe.NullRef<SDL_Event>());
if (res == 0)
var res = SDL.SDL_WaitEventRef(ref Unsafe.NullRef<SDL.SDL_Event>());
if (!res)
{
_sawmill.Error("Error while waiting on SDL2 events: {error}", SDL_GetError());
_sawmill.Error("Error while waiting on SDL3 events: {error}", SDL.SDL_GetError());
continue; // Assume it's a transient failure?
}
while (SDL_PollEvent(out _) == 1)
while (SDL.SDL_PollEvent(out _))
{
// We let callbacks process all events because of stuff like resizing.
}
while (_cmdReader.TryRead(out var cmd) && _windowingRunning)
{
ProcessSdl2Cmd(cmd);
ProcessSdl3Cmd(cmd);
}
}
}
public void PollEvents()
{
while (SDL_PollEvent(out _) == 1)
while (SDL.SDL_PollEvent(out _))
{
// We let callbacks process all events because of stuff like resizing.
}
}
private void ProcessSdl2Cmd(CmdBase cmdb)
private void ProcessSdl3Cmd(CmdBase cmdb)
{
switch (cmdb)
{
@@ -113,20 +113,24 @@ internal partial class Clyde
WinThreadWinCursorSet(cmd);
break;
case CmdWinWinSetMode cmd:
WinThreadWinSetMode(cmd);
case CmdWinWinSetFullscreen cmd:
WinThreadWinSetFullscreen(cmd);
break;
case CmdWinSetWindowed cmd:
WinThreadWinSetWindowed(cmd);
break;
case CmdTextInputSetRect cmd:
WinThreadSetTextInputRect(cmd);
break;
case CmdTextInputStart:
WinThreadStartTextInput();
case CmdTextInputStart cmd:
WinThreadStartTextInput(cmd);
break;
case CmdTextInputStop:
WinThreadStopTextInput();
case CmdTextInputStop cmd:
WinThreadStopTextInput(cmd);
break;
}
}
@@ -171,25 +175,25 @@ internal partial class Clyde
_eventWriter = eventChannel.Writer;
}
private unsafe void SendCmd(CmdBase cmd)
private void SendCmd(CmdBase cmd)
{
if (_clyde._threadWindowApi)
{
_cmdWriter.TryWrite(cmd);
SDL_Event ev = default;
ev.type = (SDL_EventType)_sdlEventWakeup;
SDL.SDL_Event ev = default;
ev.type = _sdlEventWakeup;
// Post empty event to unstuck WaitEvents if necessary.
// This self-registered event type is ignored by the winthread, but it'll still wake it up.
// This can fail if the event queue is full.
// That's not really a problem since in that case something else will be sure to wake the thread up anyways.
// NOTE: have to avoid using PushEvents since that invokes callbacks which causes a deadlock.
SDL_PeepEvents(&ev, 1, SDL_eventaction.SDL_ADDEVENT, ev.type, ev.type);
SDL.SDL_PeepEvents(new Span<SDL.SDL_Event>(ref ev), 1, SDL.SDL_EventAction.SDL_ADDEVENT, ev.type, ev.type);
}
else
{
ProcessSdl2Cmd(cmd);
ProcessSdl3Cmd(cmd);
}
}
@@ -211,93 +215,119 @@ internal partial class Clyde
}
private abstract record CmdBase;
private abstract class CmdBase;
private sealed record CmdTerminate : CmdBase;
private sealed class CmdTerminate : CmdBase;
private sealed record CmdWinCreate(
GLContextSpec? GLSpec,
WindowCreateParameters Parameters,
nint ShareWindow,
nint ShareContext,
nint OwnerWindow,
TaskCompletionSource<Sdl2WindowCreateResult> Tcs
) : CmdBase;
private sealed class CmdWinCreate : CmdBase
{
public required GLContextSpec? GLSpec;
public required WindowCreateParameters Parameters;
public required nint ShareWindow;
public required nint ShareContext;
public required nint OwnerWindow;
public required TaskCompletionSource<Sdl3WindowCreateResult> Tcs;
}
private sealed record CmdWinDestroy(
nint Window,
bool HadOwner
) : CmdBase;
private sealed class CmdWinDestroy : CmdBase
{
public nint Window;
public bool HadOwner;
}
private sealed record Sdl2WindowCreateResult(
Sdl2WindowReg? Reg,
string? Error
);
private sealed class Sdl3WindowCreateResult
{
public Sdl3WindowReg? Reg;
public string? Error;
}
private sealed record CmdRunAction(
Action Action
) : CmdBase;
private sealed class CmdRunAction : CmdBase
{
public required Action Action;
}
private sealed record CmdSetClipboard(
string Text
) : CmdBase;
private sealed class CmdSetClipboard : CmdBase
{
public required string Text;
}
private sealed record CmdGetClipboard(
TaskCompletionSource<string> Tcs
) : CmdBase;
private sealed class CmdGetClipboard : CmdBase
{
public required TaskCompletionSource<string> Tcs;
}
private sealed record CmdWinRequestAttention(
nint Window
) : CmdBase;
private sealed class CmdWinRequestAttention : CmdBase
{
public nint Window;
}
private sealed record CmdWinSetSize(
nint Window,
int W, int H
) : CmdBase;
private sealed class CmdWinSetSize : CmdBase
{
public nint Window;
public int W;
public int H;
}
private sealed record CmdWinSetVisible(
nint Window,
bool Visible
) : CmdBase;
private sealed class CmdWinSetVisible : CmdBase
{
public nint Window;
public bool Visible;
}
private sealed record CmdWinSetTitle(
nint Window,
string Title
) : CmdBase;
private sealed class CmdWinSetTitle : CmdBase
{
public nint Window;
public required string Title;
}
private sealed record CmdCursorCreate(
Image<Rgba32> Bytes,
Vector2i Hotspot,
ClydeHandle Cursor
) : CmdBase;
private sealed class CmdCursorCreate : CmdBase
{
public required Image<Rgba32> Bytes;
public Vector2i Hotspot;
public ClydeHandle Cursor;
}
private sealed record CmdCursorDestroy(
ClydeHandle Cursor
) : CmdBase;
private sealed class CmdCursorDestroy : CmdBase
{
public ClydeHandle Cursor;
}
private sealed record CmdWinCursorSet(
nint Window,
ClydeHandle Cursor
) : CmdBase;
private sealed class CmdWinCursorSet : CmdBase
{
public nint Window;
public ClydeHandle Cursor;
}
private sealed record CmdWinWinSetMode(
nint Window,
WindowMode Mode
) : CmdBase;
private sealed class CmdWinWinSetFullscreen : CmdBase
{
public nint Window;
}
private sealed class CmdWinSetWindowed : CmdBase
{
public nint Window;
public int Width;
public int Height;
public int PosX;
public int PosY;
}
// IME
private sealed record CmdTextInputStart : CmdBase
private sealed class CmdTextInputStart : CmdBase
{
public static readonly CmdTextInputStart Instance = new();
public nint Window;
}
private sealed record CmdTextInputStop : CmdBase
private sealed class CmdTextInputStop : CmdBase
{
public static readonly CmdTextInputStop Instance = new();
public nint Window;
}
private sealed record CmdTextInputSetRect(
SDL_Rect Rect
) : CmdBase;
private sealed class CmdTextInputSetRect : CmdBase
{
public nint Window;
public SDL.SDL_Rect Rect;
public int Cursor;
}
}
}

View File

@@ -0,0 +1,226 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using SDL3;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
namespace Robust.Client.Graphics.Clyde;
internal partial class Clyde
{
private sealed partial class Sdl3WindowingImpl : IWindowingImpl
{
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private readonly Clyde _clyde;
private GCHandle _selfGCHandle;
private readonly ISawmill _sawmill;
private readonly ISawmill _sawmillSdl3;
private SdlVideoDriver _videoDriver;
public Sdl3WindowingImpl(Clyde clyde, IDependencyCollection deps)
{
_clyde = clyde;
deps.InjectDependencies(this, true);
_sawmill = _logManager.GetSawmill("clyde.win");
_sawmillSdl3 = _logManager.GetSawmill("clyde.win.sdl3");
}
public bool Init()
{
InitChannels();
if (!InitSdl3())
return false;
return true;
}
private unsafe bool InitSdl3()
{
CheckThreadApartment();
_selfGCHandle = GCHandle.Alloc(this, GCHandleType.Normal);
SDL.SDL_SetLogPriorities(SDL.SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE);
SDL.SDL_SetLogOutputFunction(&LogOutputFunction, (void*) GCHandle.ToIntPtr(_selfGCHandle));
SDL.SDL_SetHint(SDL.SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
SDL.SDL_SetHint(SDL.SDL_HINT_IME_IMPLEMENTED_UI, "composition");
// SDL3's GameInput support is currently broken and leaving it on
// causes a "that operation is not supported" error to be logged on startup.
// https://github.com/libsdl-org/SDL/issues/11813
SDL.SDL_SetHint(SDL.SDL_HINT_WINDOWS_GAMEINPUT, "0");
var res = SDL.SDL_Init(SDL.SDL_InitFlags.SDL_INIT_VIDEO | SDL.SDL_InitFlags.SDL_INIT_EVENTS);
if (!res)
{
_sawmill.Fatal("Failed to initialize SDL3: {error}", SDL.SDL_GetError());
return false;
}
var version = SDL.SDL_GetVersion();
var videoDriver = SDL.SDL_GetCurrentVideoDriver();
_sawmill.Debug(
"SDL3 initialized, version: {major}.{minor}.{patch}, video driver: {videoDriver}",
SDL.SDL_VERSIONNUM_MAJOR(version),
SDL.SDL_VERSIONNUM_MINOR(version),
SDL.SDL_VERSIONNUM_MICRO(version),
videoDriver);
LoadSdl3VideoDriver();
_sdlEventWakeup = SDL.SDL_RegisterEvents(1);
if (_sdlEventWakeup == 0)
throw new InvalidOperationException("SDL_RegisterEvents failed");
LoadWindowIcons();
InitCursors();
InitMonitors();
ReloadKeyMap();
SDL.SDL_AddEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
return true;
}
private void CheckThreadApartment()
{
if (!OperatingSystem.IsWindows())
return;
if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
_sawmill.Error("Thread apartment state isn't STA. This will likely break things!!!");
}
private void LoadSdl3VideoDriver()
{
_videoDriver = SDL.SDL_GetCurrentVideoDriver() switch
{
"windows" => SdlVideoDriver.Windows,
"x11" => SdlVideoDriver.X11,
_ => SdlVideoDriver.Other,
};
}
public unsafe void Shutdown()
{
if (_selfGCHandle != default)
{
SDL.SDL_RemoveEventWatch(&EventWatch, (void*) GCHandle.ToIntPtr(_selfGCHandle));
_selfGCHandle.Free();
_selfGCHandle = default;
}
SDL.SDL_SetLogOutputFunction(null, null);
if (SDL.SDL_WasInit(0) != 0)
{
_sawmill.Debug("Terminating SDL3");
SDL.SDL_Quit();
}
}
public void FlushDispose()
{
// Not currently used
}
public void GLMakeContextCurrent(WindowReg? reg)
{
SDL.SDLBool res;
if (reg is Sdl3WindowReg sdlReg)
res = SDL.SDL_GL_MakeCurrent(sdlReg.Sdl3Window, sdlReg.GlContext);
else
res = SDL.SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);
if (!res)
_sawmill.Error("SDL_GL_MakeCurrent failed: {error}", SDL.SDL_GetError());
}
public void GLSwapInterval(WindowReg reg, int interval)
{
((Sdl3WindowReg)reg).SwapInterval = interval;
SDL.SDL_GL_SetSwapInterval(interval);
}
public unsafe void* GLGetProcAddress(string procName)
{
return (void*) SDL.SDL_GL_GetProcAddress(procName);
}
public string GetDescription()
{
var version = SDL.SDL_GetVersion();
var major = SDL.SDL_VERSIONNUM_MAJOR(version);
var minor = SDL.SDL_VERSIONNUM_MINOR(version);
var micro = SDL.SDL_VERSIONNUM_MICRO(version);
var videoDriver = SDL.SDL_GetCurrentVideoDriver();
var revision = SDL.SDL_GetRevision();
return $"SDL {major}.{minor}.{micro} (rev: {revision}, video driver: {videoDriver})";
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static unsafe void LogOutputFunction(
void* userdata,
int category,
SDL.SDL_LogPriority priority,
byte* message)
{
var obj = (Sdl3WindowingImpl) GCHandle.FromIntPtr((IntPtr)userdata).Target!;
var level = priority switch
{
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE => LogLevel.Verbose,
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG => LogLevel.Debug,
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_INFO => LogLevel.Info,
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_WARN => LogLevel.Warning,
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_ERROR => LogLevel.Error,
SDL.SDL_LogPriority.SDL_LOG_PRIORITY_CRITICAL => LogLevel.Fatal,
_ => LogLevel.Error
};
var msg = Marshal.PtrToStringUTF8((IntPtr) message) ?? "";
var categoryName = SdlLogCategoryName(category);
obj._sawmillSdl3.Log(level, $"[{categoryName}] {msg}");
}
private static string SdlLogCategoryName(int category)
{
return (SDL.SDL_LogCategory) category switch {
// @formatter:off
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_APPLICATION => "application",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_ERROR => "error",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_ASSERT => "assert",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_SYSTEM => "system",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_AUDIO => "audio",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_VIDEO => "video",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_RENDER => "render",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_INPUT => "input",
SDL.SDL_LogCategory.SDL_LOG_CATEGORY_TEST => "test",
_ => "unknown"
// @formatter:on
};
}
private enum SdlVideoDriver
{
// These are the ones we need to be able to check against.
Other,
Windows,
X11
}
}
}

View File

@@ -2,7 +2,10 @@
using System.Runtime.InteropServices;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
using TerraFX.Interop.Windows;
using TerraFX.Interop.Xlib;
using X11Window = TerraFX.Interop.Xlib.Window;
namespace Robust.Client.Graphics.Clyde;
@@ -33,6 +36,8 @@ internal partial class Clyde
public static unsafe void WindowsSharedWindowCreate(HWND hWnd, IConfigurationManager cfg)
{
// TODO: REMOVE, only used by GLFW, SDL3 does DWMWA_USE_IMMERSIVE_DARK_MODE automatically.
// >= Windows 11 22000 check
if (cfg.GetCVar(CVars.DisplayWin11ImmersiveDarkMode) && Environment.OSVersion.Version.Build >= 22000)
{
@@ -40,5 +45,37 @@ internal partial class Clyde
Windows.DwmSetWindowAttribute(hWnd, 20, &b, (uint) sizeof(BOOL));
}
}
public static void SetWindowStyleNoTitleOptionsWindows(HWND hWnd)
{
DebugTools.Assert(hWnd != HWND.NULL);
Windows.SetWindowLongPtrW(
hWnd,
GWL.GWL_STYLE,
// Cast to long here to work around a bug in rider with nint bitwise operators.
(nint)((long)Windows.GetWindowLongPtrW(hWnd, GWL.GWL_STYLE) & ~WS.WS_SYSMENU));
}
public static unsafe void SetWindowStyleNoTitleOptionsX11(Display* x11Display, X11Window x11Window)
{
DebugTools.Assert(x11Window != X11Window.NULL);
// https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm46181547486832
var newPropValString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE_DIALOG");
var newPropVal = Xlib.XInternAtom(x11Display, (sbyte*)newPropValString, Xlib.False);
DebugTools.Assert(newPropVal != Atom.NULL);
var propNameString = Marshal.StringToCoTaskMemUTF8("_NET_WM_WINDOW_TYPE");
#pragma warning disable CA1806
// [display] [window] [property] [type] [format (8, 16,32)] [mode] [data] [element count]
Xlib.XChangeProperty(x11Display, x11Window,
Xlib.XInternAtom(x11Display, (sbyte*)propNameString, Xlib.False), // should never be null; part of spec
Xlib.XA_ATOM, 32, Xlib.PropModeReplace,
(byte*)&newPropVal, 1);
#pragma warning restore CA1806
Marshal.FreeCoTaskMem(newPropValString);
Marshal.FreeCoTaskMem(propNameString);
}
}
}

View File

@@ -211,6 +211,8 @@ namespace Robust.Client.Graphics
public abstract void DrawLine(Vector2 from, Vector2 to, Color color);
public abstract void RenderInRenderTarget(IRenderTarget target, Action a, Color? clearColor);
public abstract void DrawTexture(Texture texture, Vector2 position, Color? modulate = null);
}
/// <summary>

View File

@@ -92,7 +92,7 @@ namespace Robust.Client.Graphics
public abstract void DrawTextureRectRegion(Texture texture, UIBox2 rect, UIBox2? subRegion = null, Color? modulate = null);
public void DrawTexture(Texture texture, Vector2 position, Color? modulate = null)
public override void DrawTexture(Texture texture, Vector2 position, Color? modulate = null)
{
CheckDisposed();

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