Compare commits

...

199 Commits
v0.4.9 ... doom

Author SHA1 Message Date
PJB3005
ee330d0ae9 Make DOOM work
I think I lost this work originally
2025-01-15 23:08:44 +01:00
metalgearsloth
6c44dd9665 RenderingTreeSystem cleanup (#1759)
* RenderingTreeSystem cleanup

10% less bad but 100% still boilerplate

* Slight changies

* Even better

* New shit just got made

* Apply revews
2021-05-20 18:57:29 +10:00
Acruid
c81413b0b4 Fixes bug where the net_entityreport red PVS range square was drawn at half the actual range. 2021-05-19 15:52:44 -07:00
Vera Aguilera Puerto
88b3a557da Fixes bug with PrototypeIdListSerializer where lists wouldn't be copied at all. 2021-05-19 12:12:13 +02:00
Vera Aguilera Puerto
572eb01290 Opens SCSI window centered
- Kinda fixes it getting NaN'd on resize... This isn't a proper fix, however.
2021-05-19 11:11:35 +02:00
Vera Aguilera Puerto
9dab74c9d5 Fixes SS14Window going off-screen. 2021-05-19 10:55:41 +02:00
Paul
e1cb1e1b9c fixes xamlui nuking itself when one (1) (singular) partial declaration is missing 2021-05-18 20:19:50 +02:00
Vera Aguilera Puerto
a23da702b1 MidiManager now has a Volume property which changes the general midi volume.
- Adds VV attributes to MidiManager and MidiRenderer.
2021-05-18 20:03:20 +02:00
Vera Aguilera Puerto
ae9c2423ff Fixes EntityLookup not being restarted correctly on reconnect. 2021-05-18 17:55:24 +02:00
metalgearsloth
a6dae8e30a GridId caching (#1678)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2021-05-17 11:43:22 +02:00
Pieter-Jan Briers
96c0a4ae1f Automatically unsubscribe event bus registrations in entity system shutdown. (#1758)
* Automatically unsubscribe event bus registrations in entity system shutdown.

* Fix incorrect unsubscription of local events, obsolete unsubscribe methods.

That's what we got tests for.

* = null instead of .Clear()
2021-05-17 11:01:22 +02:00
Vera Aguilera Puerto
c26ebcbc78 QueueDelete method for entities (#1757) 2021-05-17 09:20:55 +02:00
Vera Aguilera Puerto
8334050411 MIDI improvements (#1756) 2021-05-16 19:28:14 +02:00
Vera Aguilera Puerto
cc67edfc2c Unsubscribe from directed event in UI System. 2021-05-15 12:40:35 +02:00
Vera Aguilera Puerto
943ea9e6c8 Shutdown and dispose of BoundUserInterfaces correctly when the UI component gets shutdown. 2021-05-14 14:25:53 +02:00
Pieter-Jan Briers
3aa5cefe03 Stop using component messages in bound UI code. 2021-05-13 03:29:38 +02:00
Pieter-Jan Briers
c5b34bab69 More obsoletions component messages. 2021-05-13 02:24:35 +02:00
Pieter-Jan Briers
e4f24ec125 Remove IPlayerSession on client. 2021-05-13 02:16:55 +02:00
Pieter-Jan Briers
250971ade7 Remove anchored APIs from physics. 2021-05-13 02:13:18 +02:00
Pieter-Jan Briers
718adf9740 Deprecate component messages harder. 2021-05-13 01:24:20 +02:00
20kdc
5d63aa8c95 Deferred Input Context Switches (fixes spawn entities menu while moving) (#1755) 2021-05-12 18:40:26 +02:00
Vera Aguilera Puerto
17af3612a5 Remove IActorComponent, rename BasicActorComponent to ActorComponent. (#1752)
Rename playerSession to PlayerSession.
2021-05-12 13:40:16 +02:00
Pieter-Jan Briers
d2ecf6b9b1 Remove CollidesOnMask
Only usage was a unit test.
2021-05-12 01:58:12 +02:00
Pieter-Jan Briers
2a1eda0d38 Fix tests 2021-05-12 01:57:03 +02:00
Pieter-Jan Briers
f0180abeb0 Missed a spot while fixing warnings. 2021-05-12 00:26:47 +02:00
Pieter-Jan Briers
720f1b1d05 Fix a bunch of compiler warnings. 2021-05-12 00:16:12 +02:00
ShadowCommander
ae45a96753 Fix error when someone else is shooting outside client PVS 2021-05-11 13:21:30 -07:00
ShadowCommander
74257c72ee Add yaml linting for entity prototype parent (#1749)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2021-05-11 21:54:51 +02:00
ShadowCommander
cea088f4b4 Change maxlinvelocity and maxangvelocity to take tickrate (#1746) 2021-05-11 21:53:36 +02:00
Pieter-Jan Briers
88678e7d58 Entity localization refactoring (#1753) 2021-05-11 21:52:54 +02:00
Pieter-Jan Briers
d222e25d22 Remove outdated comment. 2021-05-11 20:58:21 +02:00
Pieter-Jan Briers
f0d7fbb6f2 Allow enumeration of monitor video modes.
Basically I kept dumping debugging code for this into Clyde startup to test multi-monitor stuff with GLFW so I guess I'm making it official now.
2021-05-11 20:57:27 +02:00
Pieter-Jan Briers
adec0e71ec I can't believe Zumorica would clean up this class in the last 24 hours. 2021-05-11 13:30:38 +02:00
Pieter-Jan Briers
86d9067d62 Report error if prototype has invalid parent ID. 2021-05-11 13:24:42 +02:00
Vera Aguilera Puerto
b989c9dbee Remove unused IEntityQuery methods from EntitySystem. 2021-05-10 20:35:52 +02:00
Vera Aguilera Puerto
3101ec1985 Mark IEntityQuery and implementing classes as obsolete. 2021-05-10 20:25:54 +02:00
Vera Aguilera Puerto
9ec2da1777 PlayerSession comment cleanup. 2021-05-10 20:20:44 +02:00
Vera Aguilera Puerto
0acd28e8f4 Slight PrototypeManager cleanup. 2021-05-10 20:18:08 +02:00
Vera Aguilera Puerto
c340f50ee5 Remove unused dependencies. 2021-05-10 20:14:15 +02:00
Vera Aguilera Puerto
2b589215aa Improves the Filter API. (#1747)
- Recipients is now of type `IEnumerable<ICommonSession>`.
    - This way, people need to use the Filter API to modify the recipients, instead of being allowed to modify the underlying collection directly.
- Underlying collection is now a HashSet.
    - We want no duplicates at all. This will ensure that while being quite performant.
- Removes `IFilter`.
    - Let's face it, everyone is gonna end up using `Filter` instead as it has a much more convenient API, and nothing else will EVER implement `IFilter`. For this reason, I've just gone ahead and removed it. Every method used `Filter` already, anyway.
- Adds EntitySystem `RaiseNetworkEvent` overload that takes in a `Filter`.
    - This deduplicates a lot of code that enumerated the recipients and raised a network event per each one.
- Made AddPlayersByPvs aware of `net.pvs` and `net.maxupdaterange`, has a range multiplier parameter.
    - Now it will simply add all players to the filter if PVS is disabled, and add all players in range if it's enabled.
    - The range multiplier parameter allows us to make the PVS filter a bit more permissive, and generally better. The range is doubled by default, as it was already before.
- Adds `AddInRange` method to filters.
    - This is useful for the AudioSystem, and the PVS methods use it, too!
- Adds `RemoveByVisibility` method to filters.
    - This will remove all recipients that don't have a visibility flag in their entity's EyeComponent from the filter.
    - (This will be VERY useful for Ghost Pointing, for example)
- Adds `Clone` method to filters.
    - This is useful in cases where methods take in a filter and want to modify it without modifying the original instance. (See AudioSystem)
2021-05-08 22:27:47 +02:00
Vera Aguilera Puerto
490a567ad4 Makes some PlayerManager methods thread-safe. (#1744) 2021-05-08 22:27:26 +02:00
DrSmugleaf
32f0c49484 Fix missing Attribute suffix from some serialization attributes (#1741)
* Fix missing Attribute suffix from some serialization attributes

* Rename DataDefinition namespace to Definition
2021-05-07 14:23:21 +02:00
DrSmugleaf
61113d2434 Fix comments on serialization PropertyAndFieldDefinitionTest 2021-05-04 14:33:29 +02:00
DrSmugleaf
6ba1baa88c Add summary to seed data definition in Robust.Benchmarks 2021-05-04 14:26:07 +02:00
DrSmugleaf
07867acb9a Add serialization writing benchmark, optimize writing (#1739)
* Add serialization write benchmark

* Add baseline test and rename AddNode to Add in mapping extensions

* Optimize serialization writing

* Make reader delegate private

* Unhardcode baseline test
2021-05-04 14:25:13 +02:00
DrSmugleaf
3e28b083b9 Cleanup serialization markdown, add extensions for easier mapping node manipulation (#1738) 2021-05-04 13:01:07 +02:00
DrSmugleaf
68d9e13edf Fix nullability in ViewVariablesInstance onValueChanged tuple action 2021-05-04 12:08:53 +02:00
Pieter-Jan Briers
a0c0a722c9 Don't crash if window creation fails due to selected renderer. 2021-05-03 23:37:41 +02:00
Pieter-Jan Briers
bf4aead1e8 Fix margins on debug console background 2021-05-03 12:43:21 +02:00
Pieter-Jan Briers
39f0d2e974 Fix positioning and size handling when the game started in fullscreen and exited it later.
It now actually makes the non-fullscreen windows sized and placed sanely so it isn't placed offscreen and too big.
2021-05-02 23:30:50 +02:00
Pieter-Jan Briers
b20ae98f21 quit command now does soft exit, hardquit command added. 2021-05-02 23:30:50 +02:00
Acruid
7899780543 Unsubscribing from a directed entity event does not require a delegate argument anymore. 2021-05-02 14:23:48 -07:00
Pieter-Jan Briers
0c9e322b3e Fix macOS builds. 2021-05-02 21:48:58 +02:00
Pieter-Jan Briers
6005208285 Use Fluent localization for key names. 2021-05-02 21:48:50 +02:00
Pieter-Jan Briers
2b15831349 Fix fullscreen startup crashing. 2021-05-02 21:36:08 +02:00
Pieter-Jan Briers
1b2450d1cb Make debug.target_fps cvar for frame graph.
So 60 FPS isn't hardcoded anymore.
2021-05-02 20:55:46 +02:00
DrSmugleaf
5f31036ab2 Cleanup serialization code (#1735)
* Cleanup serialization code

* Merge fixes

* american codebase
2021-05-02 14:23:39 +02:00
Pieter-Jan Briers
8efffc471d Multi-window support (#1713) 2021-05-02 14:05:50 +02:00
DrSmugleaf
eb9e842027 Make SpecificPropertyInfo not return attributes on the backing field by default (#1734) 2021-05-02 13:47:32 +02:00
DrSmugleaf
f9cd9ac12a Add serialization copy benchmark, optimize copying and reading with IL generators (#1729)
* Create serialization copy benchmark

* Optimize copy generic method call

Before
|                             Method |         Mean |      Error |     StdDev |       Median |
|----------------------------------- |-------------:|-----------:|-----------:|-------------:|
|                   CreateCopyString |     43.54 ns |   0.918 ns |   0.859 ns |     43.74 ns |
|                  CreateCopyInteger |     19.33 ns |   0.388 ns |   0.860 ns |     19.24 ns |
| CreateCopyDataDefinitionWithString |  3,971.42 ns |  78.974 ns | 213.512 ns |  3,902.54 ns |
|       CreateCopySeedDataDefinition | 49,916.69 ns | 264.992 ns | 221.280 ns | 49,950.53 ns |

After
|                             Method |         Mean |      Error |     StdDev |
|----------------------------------- |-------------:|-----------:|-----------:|
|                   CreateCopyString |     43.15 ns |   0.917 ns |   1.766 ns |
|                  CreateCopyInteger |     17.91 ns |   0.366 ns |   0.305 ns |
| CreateCopyDataDefinitionWithString |    544.20 ns |   8.137 ns |   7.611 ns |
|       CreateCopySeedDataDefinition | 18,233.34 ns | 357.861 ns | 397.761 ns |

* Change populate delegate to use IL

|                       Method |        Mean |     Error |    StdDev |
|----------------------------- |------------:|----------:|----------:|
|                   ReadString |    190.7 ns |   3.48 ns |   5.63 ns |
|                  ReadInteger |    218.8 ns |   4.29 ns |   6.01 ns |
| ReadDataDefinitionWithString |    677.5 ns |  13.34 ns |  24.06 ns |
|       ReadSeedDataDefinition | 11,067.0 ns | 219.19 ns | 260.93 ns |

|                             Method |         Mean |      Error |     StdDev |
|----------------------------------- |-------------:|-----------:|-----------:|
|                   CreateCopyString |     42.69 ns |   0.926 ns |   1.414 ns |
|                  CreateCopyInteger |     18.02 ns |   0.268 ns |   0.237 ns |
| CreateCopyDataDefinitionWithString |    556.80 ns |  11.206 ns |  21.589 ns |
|       CreateCopySeedDataDefinition | 13,797.07 ns | 256.011 ns | 367.163 ns |

* Make copying use IL to get values

|                               Method |        Mean |      Error |     StdDev |
|------------------------------------- |------------:|-----------:|-----------:|
|                     CreateCopyString |    42.55 ns |   0.889 ns |   1.357 ns |
|                    CreateCopyInteger |    18.72 ns |   0.394 ns |   0.740 ns |
|   CreateCopyDataDefinitionWithString |   398.98 ns |   7.853 ns |  12.682 ns |
|         CreateCopySeedDataDefinition | 5,996.60 ns | 118.724 ns | 181.304 ns |
| BaselineCreateCopySeedDataDefinition |   340.85 ns |   6.811 ns |  12.281 ns |

* Rest in peace the fast path
For now

* Fix serialization copying not passing the target ref properly

* Fix field assigners for structs
2021-05-02 12:09:12 +02:00
Pieter-Jan Briers
8fd98c75a9 Fix finalizer stutter from post-process shaders. 2021-05-02 11:23:48 +02:00
Pieter-Jan Briers
7c008e857d Rewrite FileDialogManager to remove #if from it. 2021-05-02 09:35:38 +02:00
Pieter-Jan Briers
4de2e35e66 Add macOS CI for build-test 2021-05-02 09:25:26 +02:00
metalgearsloth
d4467acf93 Fix filedialogmanager 2021-05-02 17:16:27 +10:00
Silver
f3babcc39f Update FileDialogManager.cs 2021-05-01 19:30:45 -06:00
Silver
f491bb5571 Update FileDialogManager.cs 2021-05-01 19:20:40 -06:00
Pieter-Jan Briers
b201f10c76 Reduce some dictionary allocations in RSI loading. 2021-05-02 01:08:51 +02:00
Pieter-Jan Briers
a9208c0d29 Reduce allocations in RSIResource.LoadRsiMetadata somewhat.
Using stackallocs for the meta.json file.
2021-05-02 00:55:01 +02:00
Pieter-Jan Briers
b8cc01d872 Remove unecessary array allocation from ResourcePath constructor. 2021-05-02 00:16:52 +02:00
Pieter-Jan Briers
2d827890e9 Fix nullable errors reported by Rider. 2021-05-02 00:00:40 +02:00
Pieter-Jan Briers
f86d6ccd3c Remvoe unused .dll.config and app.config. 2021-05-02 00:00:04 +02:00
Vera Aguilera Puerto
967b76483a Fix rendering tree system crash involving nullspace.
Nullspace is a valid map, so we need to account for that!
2021-04-30 01:11:16 +02:00
Vera Aguilera Puerto
ef2c0ad8cf Fix the comp lifetime ComponentRemove event
Now, event tables DispatchComponent takes in the component reference directly.
2021-04-30 01:09:50 +02:00
metalgearsloth
9ae1352030 Enable nullables as errors in Robust (#1732) 2021-04-29 15:56:05 -06:00
SweptWasTaken
d8d9b271cc Updates sandbox.yml 2021-04-28 14:31:46 -07:00
Acruid
122acc5fd5 SnapGridComponent Removal (#1720)
* Removed SnapGridOffset, there is only center now.

* SnapGridComponent methods are now static.

* Removed SnapGridComponent.OnPositionChanged.

* Refactored static functions off SnapGridComponent to MapGrid.
Refactored away usages of SnapGridComponent.Position.

* Added Transform.Anchored for checking if an entity is a tile entity.
More refactoring for static MapGrid functions.

* Static snapgrid methods on MapGrid are no longer static.

* Removed IMapGrid.SnapSize, it is now always equal to the IMapGrid.TileSize.

* Add setter to ITransformComponent.Anchored.
Removed direct references to SnapGridComponent from content.

* Grid functions now deal with EntityUids instead of SnapGridComponents.
Began renaming public API functions from SnapGrid to Anchor.

* Add some unit tests.

* SnapGridComponent now anchors itself in startup, instead of Initialize (sending directed events in init is an error).
2021-04-28 10:24:11 -07:00
metalgearsloth
32dea84196 Fix physics build warnings (#1731) 2021-04-28 15:29:51 +02:00
DrSmugleaf
91d58dbca4 Make serialization work with backing fields automatically (#1727)
* Make serialization work with backing fields automatically

* Fix not taking priorities into account, make the test fail when that is the case

* Turn fieldDefs back into a list

* Format it better

* Remove GetInheritanceBehaviour, expose immutable array of base field definitions
2021-04-27 14:11:06 +02:00
MehimoNemo
c54b1572f5 Unbuckling Closes Inventory UI (#1723)
* pain

* Add test for parent changed when not at 0,0

* Fix map loading of grids

Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
2021-04-26 12:26:29 -07:00
DrSmugleaf
1fa979c0f6 Add serialization manager shutdown method and initialize benchmark (#1728) 2021-04-25 13:00:31 -07:00
metalgearsloth
760599171d Obsolete collision interfaces (#1725) 2021-04-25 12:58:30 -07:00
metalgearsloth
10d295d535 Basic Collisionmanager tests (#1722)
* Add some unit tests for CollisionManager
2021-04-25 12:49:45 -07:00
ShadowCommander
0047c5000f Fix placement of grid tiles on the edge of a grid (#1724)
* Fix placement of grid tiles on the edge of a grid
2021-04-25 12:48:26 -07:00
metalgearsloth
810a6d190f Add MapId error to tp command (#1726)
Previously it would just dump the exception
2021-04-25 04:12:28 -07:00
metalgearsloth
197227dcf6 Fix showbb
matrices momento
2021-04-25 19:03:19 +10:00
Swept
fa23ec8fc6 Updates the README.md 2021-04-23 19:23:54 +00:00
Vera Aguilera Puerto
6506171ea0 Server ContentStart.StartLibrary method.
This will skip the FULL_RELEASE check on games specifically using RobustToolbox as a library.
2021-04-23 20:37:45 +02:00
Vera Aguilera Puerto
8bd1e72e9f Adds GameController options for games using RobustToolbox as a library. (#1711) 2021-04-23 00:05:42 +02:00
Acruid
4ce6629ace RSIs fail to load if they have duplicate states defined in their JSON. (#1701) 2021-04-22 15:21:34 -06:00
Vera Aguilera Puerto
f9ef605903 Add optional AwaitEvent methods that take a Type instead of using generics. 2021-04-21 15:29:43 +02:00
Vera Aguilera Puerto
c6b74e998f Use System.Numerics in a few Box2 methods to speed them up (#1708) 2021-04-19 17:18:09 +02:00
Vera Aguilera Puerto
c4946b8466 Viewport Improvements (#1528)
Co-authored-by: 20kdc <asdd2808@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2021-04-19 09:47:20 +02:00
Pieter-Jan Briers
ffa908bf27 More workarounding for Rider Avalonia faking 2021-04-19 01:47:33 +02:00
Pieter-Jan Briers
0d37ff3f20 Fix reporting of fatal GLFW errors on window creation. 2021-04-19 01:44:00 +02:00
Pieter-Jan Briers
7aecdcf70a Improved soft shadows. 2021-04-19 01:42:59 +02:00
Vera Aguilera Puerto
70f82d6db8 Add directed start/end collision events. (#1710) 2021-04-17 13:08:11 +02:00
Vera Aguilera Puerto
20b7870739 PlayerSession is now correctly setup on singleplayer 2021-04-17 01:52:07 +02:00
Vera Aguilera Puerto
172639baea Fix bug where TickUpdate is never ran in singleplayer mode. 2021-04-16 13:37:01 +02:00
metalgearsloth
6038483b1e DebugDrawing for DistanceJoints (#1703) 2021-04-16 10:55:31 +02:00
Vera Aguilera Puerto
39d98d591c Adds singleplayer support (#1704) 2021-04-16 10:53:59 +02:00
Vera Aguilera Puerto
01c2fc0730 ClydeTileDefinitionManager no longer throws when uninitialized, or when there are no tile definitions. (#1705) 2021-04-16 10:53:49 +02:00
ike709
1884bb0067 Fixes DateTimeOffset sandbox whitelisting (#1706) 2021-04-16 01:39:18 +02:00
Vera Aguilera Puerto
1c368bbaa8 Remove useless debug prototypes. 2021-04-15 20:44:25 +02:00
metalgearsloth
d16078a35f Don't reset sleeptimer on building physics islands 2021-04-15 23:18:05 +10:00
Vera Aguilera Puerto
4dd04207ac Shared GameTiming no longer depends on INetManager (#1697)
Adds ClientGameTiming with prediction and local/server time methods.
2021-04-14 11:40:02 -07:00
Vera Aguilera Puerto
02af42da30 Refactors EntityManager to not do any networking. (#1695)
* Refactors EntityManager to not do any networking.
ServerEntityManager and ClientEntityManager now do the networking instead.

* Rename property for "backwards compat."

* Remove comented out code in robust server simulation
2021-04-14 11:39:21 -07:00
Vera Aguilera Puerto
2c75c8b36d Refactors MapManager to not do any networking. (#1696)
* Refactors MapManager to not do any networking.
Now, ServerMapManager and ClientMapManager handle any networking.

* it's christmas in april!

* Remove comented line

* Remove useless seal

* Fix incorrect semicolon

* Event is no longer overriden, has a protected Invoke method instead
2021-04-14 11:13:58 -07:00
Vera Aguilera Puerto
013e6f7ce4 Move INetManager dependency from PrototypeManager to Server/ClientPrototypeManager 2021-04-14 14:55:40 +02:00
Acruid
cbd7b62ad7 Component Lifetime Events (#1660)
* Events are now raised for component OnAdd/Initialize/Startup/Shutdown/OnRemove.
Code cleanup in the Component class.
2021-04-13 17:16:41 -07:00
metalgearsloth
c1396f1c50 Named fixtures (#1684)
* Add named fixtures

Useful for getting specific collisions.

* Final cleanup

* More cleanup
2021-04-13 20:47:13 +10:00
Vera Aguilera Puerto
3ec9e7a734 Remove a few (now irrelevant) comments mentioning Godot. 2021-04-12 20:10:05 +02:00
Vera Aguilera Puerto
3a1e6e84b1 Remove Unused sharer params enum from the Godot era. 2021-04-12 20:09:22 +02:00
Vera Aguilera Puerto
7224419f77 Remove unused AudioMixTarget 2021-04-12 20:03:57 +02:00
Vera Aguilera Puerto
056e4de0c1 Appearance System cleanup.
Removes a nearly 4 years old unused visualizer that had a hardcoded update method in the appearance system, for some reason.
2021-04-12 19:59:25 +02:00
Vera Aguilera Puerto
aa90f22e23 Adds abstract class for entity events that can be cancelled. (#1688) 2021-04-11 18:51:07 +02:00
metalgearsloth
071234095d Don't use linked-list for contact pooling (#1683)
Always seemed icky to me. Aether uses a linked-list (like world contacts) and Farseer just uses a Queue.
2021-04-10 18:33:01 +02:00
Vera Aguilera Puerto
5b06391159 Fix objects of a server-only type not correctly showing up as such in VV. 2021-04-10 17:32:56 +02:00
Vera Aguilera Puerto
8edd44086b AudioSystem and DebugPhysicsIslandSystem unsubscribe from events on shutdown now. 2021-04-09 16:07:44 +02:00
Vera Aguilera Puerto
ccf212e9cb GridTileLookupSystem unsubscribes from events on shutdown. 2021-04-09 13:51:12 +02:00
Vera Aguilera Puerto
493011d1f9 SnapGridSystem uses directed MoveEvent, unsubscribes on shutdown. 2021-04-09 13:24:21 +02:00
Vera Aguilera Puerto
40e193df33 Adds directed/broadcast event for SnapGrid component position change. 2021-04-09 13:21:15 +02:00
Vera Aguilera Puerto
5068294d38 Makes a bunch of TransformComponent events directed. (#1682) 2021-04-08 20:42:46 +02:00
metalgearsloth
24054b5e2f Optimise showbb some more 2021-04-08 18:37:22 +10:00
Paul Ritter
17869c16cd when the bool 2021-04-06 11:58:17 +02:00
metalgearsloth
d8aad89c2f Split entity management from entity queries (#1665)
* Split entity lookups from entitymanager

* Helps if you subscribe dingus

* Handle map changes

* Stacks instead

* Make mapchanges use a queue because it's probably better

Moves likely only care about the latest position

* IoC what you did there

* IoC refactor

* Minor optimisations

* Apply feedback

* My IQ dropped 3 sizes that day

* Rest of acruid's feedback

* final_no_actual commit

* enlightenment?

* Liftoff

* final_commit_v2_actual

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-04-06 13:29:48 +10:00
DrSmugleaf
2a349eb023 Optimize serialization reading, create benchmarks (#1679)
* Add Robust.Benchmarks and read string benchmark

* Separate serialization manager methods, use compiled lambdas to call manager read

4 us > 200 ns

* Add int and data definition with string benchmarks

* Make serialization population use expressions to create definitions

* Make benchmark classes internal and create seed data definition

* Add complex data definition read benchmark

* Create primitive serializers, remove primitive special case

|                 Method |        Mean |     Error |    StdDev |
|----------------------- |------------:|----------:|----------:|
|             ReadString |    227.1 ns |   4.47 ns |   5.65 ns |
|            ReadInteger |    245.4 ns |   4.82 ns |   6.26 ns |
|  ReadDataDefWithString |    804.7 ns |  15.27 ns |  16.34 ns |
| ReadSeedDataDefinition | 15,846.8 ns | 312.89 ns | 773.39 ns |

* Remove testing code

* Setup delegates during initialize

* Revert "Setup delegates during initialize"

This reverts commit 7ff4d4eaaa.

* Store delegates in a concurrent dictionary because I really cannot be arsed to generate them on initialize at this point
2021-04-05 14:50:33 +02:00
Vera Aguilera Puerto
47ad07b3d2 Adds directed event for when an entity's BodyType changes. (#1681)
Removes old Anchored C# event.
2021-04-05 13:17:09 +02:00
ShadowCommander
aacf6522b4 Remove NetId requirement for local event subscriptions (#1675) 2021-04-02 18:45:59 -07:00
ShadowCommander
c73d27b9ae Add RSI path to error log (#1676) 2021-04-02 16:45:21 -07:00
Vera Aguilera Puerto
f068b30a7c Adds Prototype Id Validator for Dictionaries whose keys are prototype IDs. (#1673) 2021-04-02 15:47:48 +02:00
Vera Aguilera Puerto
5400dddcfc Fix ComponentDependencies tests for debug & release 2021-04-02 13:56:42 +02:00
metalgearsloth
6cf5fdc5d6 Grid-trees for rendering (#1666) 2021-04-02 20:25:16 +11:00
Vera Aguilera Puerto
5d46663881 Fix ComponentDependencies tests 2021-03-31 22:17:55 +02:00
Acruid
8e0f227940 PVS Bugfixes 1: The Debuggening (#1671)
* Wrapped the parallel GetMail function in a try/catch.
Added a hack to the ViewCulling leave message that skips ents that don't exist.
Always send ALL map and grid entities to the client.
More info logging about adding/removing maps/grids.

* Will now still send required map critical entities even if client is not attached to an entity.
PvsEnabled and PvsRange are now writeable.
2021-03-31 21:56:11 +02:00
metalgearsloth
73a13fff9a Fix grid bounds upon deserialization 2021-03-31 19:33:19 +11:00
DrSmugleaf
de2e505a12 Make content able to choose which log level leads to test failures (#1670)
* Make content able to choose which log level leads to test failures

* Now make it make sense
2021-03-31 19:26:38 +11:00
DrSmugleaf
a9f7c7a76f Fix no HWId userdata error in integration tests (#1667) 2021-03-30 15:39:43 +02:00
Vera Aguilera Puerto
37401c26c9 Adds a custom editor for Prototypes to ViewVariables. (#1663)
Also improves VV a bit.
2021-03-30 15:33:15 +02:00
Vera Aguilera Puerto
528cd1e0e5 Fix fullscreen crash 2021-03-30 15:32:15 +02:00
Pieter-Jan Briers
2959456bec Fix integration test networking. 2021-03-30 13:28:26 +02:00
Metal Gear Sloth
8951712495 Add NaN guards to physics 2021-03-30 21:59:15 +11:00
metalgearsloth
d8612aff64 Re-implement inertia (#1652)
* Implement inertia

* actual SPEEN

* Sync mass

* bitcoin miner

* I am le dumb

* also dis

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-30 21:42:05 +11:00
Acruid
e16732eb7b Network View Bubble (#1629)
* Adds barbones culling.

* Visibility culling and recursive parent ent additions.
DebugEntityNetView improvements.
Visibility moved from session to eyecomponent.

* Multiple viewport support.

* Perf improvements.

* Removed old netbubble system from ServerEntityManager.
Supports old NaN system for entities leaving view.
Supports old SendFullMap optimization for anchored, non-updating Entities.

* Fixes size of netView box.

* Remove empty EntityManager.Update method.
Switching ViewCulling back to PLINQ.
2021-03-29 16:17:34 -07:00
Acruid
91f61bb9de Reverts component NetId storage in ComponentManager back to the way Acruid originally designed it.
Removes NetId methods from IEntity, content does not need to be messing with them.
Fixes bug in DeleteComponent where the ComponentDeleted event was not being raised if a component did not have a NetId.
2021-03-29 03:40:48 -07:00
Pieter-Jan Briers
ddc91d05ec Some work towards multi-monitor support in Clyde.
Most of this was me experimenting with GLFW, but I figured I'd still commit it.
2021-03-28 21:23:38 +02:00
Acruid
ef22842b90 Fixes bug where FirstTimePredicted was not being set properly for the first predicted frame. 2021-03-27 20:16:04 -07:00
Pieter-Jan Briers
303e2152d2 UIScale now updates dynamically.
So if you move the window between different monitors with different scaling, the game updates.
2021-03-28 01:55:35 +01:00
Vera Aguilera Puerto
37fc0d0d2a Set correct class constrains for prototype id list serializers 2021-03-27 22:47:24 +01:00
Vera Aguilera Puerto
53987e1e5d Adds prototype "Variant" helper methods to IPrototypeManager (#1662) 2021-03-27 22:40:07 +01:00
metalgearsloth
3216d7770b Fix net.rate cvar warning (#1659)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-27 02:14:45 -07:00
Acruid
3203ca2ff4 Removed Control.Update from the UI system. UI Controls have no business running code in simulation updates.
Refactored the client update loop so that the GameStateManager is in full control of the simulation update.
2021-03-26 17:46:34 -07:00
metalgearsloth
e22254cd51 Clear velocities on container insertion (#1653)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-26 22:39:11 +01:00
Acruid
7ed722f669 Visibility moved from session to EyeComponent (#1657) 2021-03-26 22:38:45 +01:00
DrSmugleaf
4864096b2a Add prototype id list serializer and tests (#1658)
* Add prototype id list serializer and tests

* Bring old .Value code back

* Paul made me do this
2021-03-26 20:52:22 +01:00
Acruid
5161385de4 Removed unused Update and Resize code from GameStates. Presenters can get resize events from the interface manager (hint: you won't ever need to), and there is no reason for a UI Presenter to do anything in simulation ticks (UI should be event driven, not polling data every frame). 2021-03-25 14:01:51 -07:00
Acruid
98e009b38f Removed the GameController dependency from Clyde.
Removed the ConfigurationManager dependency from FontManager.
2021-03-25 11:36:57 -07:00
Vera Aguilera Puerto
3863ab8f62 Adds PrototypeIdHashSetSerializer for HashSet<string> prototype ID validation (#1656)
* Adds PrototypeIdHashSetSerializer for HashSet<string> prototype ID validation

* Paul changes

* cleanup, better stuff
2021-03-25 14:26:14 +01:00
Metal Gear Sloth
f576eb5125 Optimise showbb 2021-03-25 23:32:06 +11:00
Acruid
314742ccd8 NullableHelper tests now properly set up their required DI container instead of reusing the container from whatever test was ran before it. Service Locator anti-pattern :( 2021-03-25 02:02:09 -07:00
Acruid
f9074811f9 Adds constructor injection to the IoCManager & DependencyCollection. 2021-03-25 01:16:08 -07:00
Pieter-Jan Briers
5f3e1eb378 Frame graph now shows when GCs occur. 2021-03-25 02:24:38 +01:00
Pieter-Jan Briers
3c1ee20ca1 A 2021-03-25 02:05:28 +01:00
Pieter-Jan Briers
3768f5e68e Remove allocs from ContainerSlot.ContainedEntities. 2021-03-25 01:56:06 +01:00
Pieter-Jan Briers
765a560380 Fix integer overflow breaking Lidgren metrics. 2021-03-25 01:47:45 +01:00
Metal Gear Sloth
39ae3ac653 Optimise physics do not research 2021-03-24 22:35:17 +11:00
Pieter-Jan Briers
e48f4027e5 Probably fix running Robust directly for some people. 2021-03-23 21:28:36 +01:00
ShadowCommander
2fa1e98faf Fix CopyWithTypeSerializer not copying when null (#1651) 2021-03-22 11:02:32 +01:00
Pieter-Jan Briers
cedfa0ee2f Nothing to see here. 2021-03-21 20:37:10 +01:00
Acruid
92f44b390e SoundSystem Improvements (#1649) 2021-03-21 16:35:52 +01:00
Pieter-Jan Briers
65a42f9209 Prototype reloading now fires an event. 2021-03-21 16:25:52 +01:00
Acruid
ebf53248cf TestLogHandler now fails the test if a warning or higher is logged. 2021-03-19 13:42:13 -07:00
Acruid
289f637e8a Entity Lifetime Levels (#1644)
* Added an entity lifetime levels property.
Added exception when recursively deleting an entity.

* Add a directed event 'EntityTerminatingEvent' for right before an entity is deleted.

* Added MapInit lifestage to entities.
2021-03-18 22:53:05 -07:00
metalgearsloth
d7c13f30c8 Fix showbb awake (#1632)
* Fix showbb awake

* Slight tweak

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-17 13:58:26 -07:00
Vera Aguilera Puerto
0dac17ae5e ConfigurationManager.OnValueChanged's invokeImmediately now accounts for overriden values correctly. 2021-03-17 20:22:44 +01:00
Pieter-Jan Briers
9a19a774fa Use stencil test to cull FOV-hidden lights early.
Massive shader optimization.
2021-03-17 13:16:47 +01:00
Pieter-Jan Briers
81f49d5eb2 Fix moving to the end of a textbox. 2021-03-17 01:22:45 +01:00
DrSmugleaf
4f3b4ac2d2 Changes for content server nullability (#1642) 2021-03-16 15:47:49 +01:00
Pieter-Jan Briers
e428056b52 Rldrsc now works with textures. 2021-03-16 12:39:19 +01:00
DrSmugleaf
8dc9d2989a Fix not being able to use shared entity systems in update order (#1638) 2021-03-16 12:38:31 +01:00
Paul
fd8c90dcbb reverting cringe (moved controller metrics cvar get to server) 2021-03-16 12:05:56 +01:00
Vera Aguilera Puerto
ffe4e5a8ab Add Enabled property to CollisionWake component. (#1641)
* Add Enabled property to CollisionWake component.

* Set property in HandleComponentState
2021-03-16 11:38:46 +01:00
Paul Ritter
6e5026d270 adds prometheus logging to physicscontrollers (#1640) 2021-03-16 11:32:46 +01:00
metalgearsloth
946c4166dc Move RootControl frameupdate to after queue (#1625)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-16 09:15:18 +01:00
Paul Ritter
7d2fb85a04 adds custom typeserializers (#1636)
retires DataFieldWithConstantAttribute & DataFieldWithFlagAttribute in favor of new customtypeserializers
adds prototypeidvalidation, just needs to be added to the corresponding fields
fixes some behaviour in yamllinter
2021-03-15 13:24:29 +01:00
metalgearsloth
d6ec078519 Fix static sleeping crash (#1630)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-14 12:44:49 +11:00
DrSmugleaf
32256fc4d9 Remove printing ticks in integration tests (#1627) 2021-03-13 20:12:49 +01:00
DrSmugleaf
37bbdfe7ff Fix serialization logging not printing messages (#1628) 2021-03-13 20:12:41 +01:00
metalgearsloth
c906675cdf Set collidable on CollisionWake removal (#1626)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-13 20:13:00 +11:00
DrSmugleaf
90bb5574c1 Add a CVar to disable texture preloading for tests (#1623) 2021-03-13 13:25:10 +11:00
Acruid
7b50dcd969 Removed IEntityManager.SpawnEntityNoMapInit. Every entity spawned into an uninitialized map does not have mapinit ran, so this is useless. 2021-03-11 22:17:31 -08:00
Pieter-Jan Briers
8d82f48a8f Various GLES fixes. 2021-03-11 13:06:30 +01:00
Pieter-Jan Briers
469f9fd219 Remove #line directives from shaders.
They hurt debugging more than they helped.
2021-03-11 13:06:18 +01:00
Pieter-Jan Briers
1a5783ab4e Probably fix tests 2021-03-11 11:47:58 +01:00
Clyybber
3d25886d79 Set velocity for audio sources, enabling doppler effect (#1622) 2021-03-11 11:44:01 +01:00
Pieter-Jan Briers
516b2cd372 Handle surrogate pairs correctly in LineEdit. 2021-03-10 16:55:12 +01:00
Pieter-Jan Briers
3cfcfa0be2 Render fallback character for unavailable characters. 2021-03-10 16:54:52 +01:00
Pieter-Jan Briers
69328087bd Added AsRune property to TextEventArgs 2021-03-10 16:54:13 +01:00
Pieter-Jan Briers
1bf8b2a52b Use Rune for rendering text instead of char.
Fixes crashes with surrogates.
2021-03-09 23:25:27 +01:00
Pieter-Jan Briers
fc6dc6f4e1 Add/fix Rune APIs for sandbox. 2021-03-09 23:24:33 +01:00
541 changed files with 18127 additions and 9672 deletions

View File

@@ -10,7 +10,7 @@ jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}

View File

@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>

2
Avalonia.Base/README.md Normal file
View File

@@ -0,0 +1,2 @@
See `Robust.Client/UserInterface/XAML/RiderNotes.md` for why this project exists.
We are not actually using Avalonia (yet).

View File

@@ -4,5 +4,6 @@
<TargetFramework>net5.0</TargetFramework>
<LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>
</Project>

View File

@@ -14,12 +14,6 @@
<PackageReference Condition="'$(TargetFramework)' == 'net472'" Include="System.Memory" Version="4.5.3" />
</ItemGroup>
<ItemGroup>
<Content Include="OpenToolkit.GraphicsLibraryFramework.dll.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>

View File

@@ -1,5 +0,0 @@
<configuration>
<!-- I actually have no idea whether this works on FreeBSD but it can't hurt to set it as such. -->
<dllmap os="linux,freebsd" dll="glfw3.dll" target="glfw.so.3" />
<dllmap os="osx" dll="glfw3.dll" target="glfw.3.dylib" />
</configuration>

View File

@@ -1,25 +1,16 @@
![Robust Toolbox](https://raw.githubusercontent.com/space-wizards/asset-dump/3dd3078e49e3a7e06709a6e0fc6e3223d8d44ca2/robust.png)
[![Build status](https://ci.appveyor.com/api/projects/status/ygb7t8hsj3wt7pnm/branch/master?svg=true)](https://ci.appveyor.com/project/Silvertorch5/space-station-14/branch/master)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ss14&metric=coverage)](https://sonarcloud.io/dashboard?id=ss14)
Robust Toolbox is an engine primarily being developed for [Space Station 14](https://github.com/space-wizards/space-station-14), although we're working on making it usable for both [singleplayer](https://github.com/space-wizards/RobustToolboxTemplateSingleplayer) and [multiplayer](https://github.com/space-wizards/RobustToolboxTemplate) projects.
Robust Toolbox is a client/server backend for [Space Station 14](https://github.com/space-wizards/space-station-14).
Use the [content repo](https://github.com/space-wizards/space-station-14) for actual development, even if you're modifying the engine itself.
### *This repository is the *engine* section of SS14. This is the base engine all SS14 servers will be built on. As such, it does not start on its own: it needs the [content repo](https://github.com/space-wizards/space-station-14). Use said repo for actual development, even if you're modifying the engine itself. Think of Robust Toolbox as BYOND in the context of Spacestation 13.*
## Project Links
## Getting in Touch
[Website](https://spacestation14.io/) | [Discord](https://discord.gg/t2jac3p) | [Forum](https://forum.spacestation14.io/) | [Steam](https://store.steampowered.com/app/1255460/Space_Station_14/) | [Standalone Download](https://spacestation14.io/about/nightlies/)
* Website: [spacestation14.io](https://spacestation14.io/)
* Discord: [Invite Link](https://discord.gg/t2jac3p)
* IRC: `irc.rizon.net#spacebus`
* Code Analysis: [Sonar Cloud](https://sonarcloud.io/dashboard?id=ss14)
* Automatic Content Builds: [builds.spacestation14.io](https://builds.spacestation14.io/jenkins/)
## Documentation/Wiki
The IRC is setup to relay back and forth to the Discord server so [IRC nerds](https://xkcd.com/1782/) will not be left out.
## Documentation
We have various technical documentation articles on the [HackMD Wiki](https://hackmd.io/@ss14/docs/%2F%40ss14%2Ftechnical-documentation-overview).
The [HackMD Wiki](https://hackmd.io/@ss14/docs/wiki) has documentation on SS14s content, engine, game design and more. We also have lots of resources for new contributors to the project.
## Contributing
@@ -27,8 +18,8 @@ We are happy to accept contributions from anybody. Get in Discord or IRC if you
## Building
**In practice, you usually don't build this repository directly.**
This repository is the **engine** part of SS14. It's the base engine all SS14 servers will be built on. As such, it does not start on its own: it needs the [content repo](https://github.com/space-wizards/space-station-14). Think of Robust Toolbox as BYOND in the context of Spacestation 13.
## Legal Info
See `legal.md` for licenses and copyright.
See [legal.md](https://github.com/space-wizards/RobustToolbox/blob/master/legal.md) for licenses and copyright.

View File

@@ -0,0 +1 @@
console-line-edit-placeholder = Command Here

View File

@@ -0,0 +1,54 @@
input-key-Escape = Escape
input-key-Control = Control
input-key-Shift = Shift
input-key-Alt = Alt
input-key-Menu = Menu
input-key-F1 = F1
input-key-F2 = F2
input-key-F3 = F3
input-key-F4 = F4
input-key-F5 = F5
input-key-F6 = F6
input-key-F7 = F7
input-key-F8 = F8
input-key-F9 = F9
input-key-F10 = F10
input-key-F11 = F11
input-key-F12 = F12
input-key-F13 = F13
input-key-F14 = F14
input-key-F15 = F15
input-key-Pause = Pause
input-key-Left = Left
input-key-Up = Up
input-key-Down = Down
input-key-Right = Right
input-key-Space = Space
input-key-Return = Return
input-key-NumpadEnter = Num Enter
input-key-BackSpace = Backspace
input-key-Tab = Tab
input-key-PageUp = Page Up
input-key-PageDown = Page Down
input-key-End = End
input-key-Home = Home
input-key-Insert = Insert
input-key-Delete = Delete
input-key-MouseLeft = Mouse Left
input-key-MouseRight = Mouse Right
input-key-MouseMiddle = Mouse Middle
input-key-MouseButton4 = Mouse 4
input-key-MouseButton5 = Mouse 5
input-key-MouseButton6 = Mouse 6
input-key-MouseButton7 = Mouse 7
input-key-MouseButton8 = Mouse 8
input-key-MouseButton9 = Mouse 9
input-key-LSystem-win = Left Win
input-key-RSystem-win = Right Win
input-key-LSystem-mac = Left Cmd
input-key-RSystem-mac = Right Cmd
input-key-LSystem-linux = Left Meta
input-key-RSystem-linux = Right Meta
input-key-unknown = <unknown key>

View File

@@ -1,40 +0,0 @@
- type: entity
id: debugRotation1
name: dbg_rotation1
components:
- type: Clickable
- type: InteractionOutline
- type: Sprite
netsync: false
visible: true
sprite: debugRotation.rsi
state: direction1
placement:
mode: AlignTileAny
- type: entity
id: debugRotation4
name: dbg_rotation4
components:
- type: Clickable
- type: InteractionOutline
- type: Sprite
netsync: false
visible: true
sprite: debugRotation.rsi
state: direction4
placement:
mode: AlignTileAny
- type: entity
id: debugRotationTex
name: dbg_rotationTex
components:
- type: Clickable
- type: InteractionOutline
- type: Sprite
netsync: false
visible: true
texture: debugRotation.rsi/direction1.png
placement:
mode: AlignTileAny

View File

@@ -1,4 +0,0 @@
- type: entity
name: blank entity
id: BlankEntity
abstract: true

View File

@@ -28,5 +28,10 @@ void fragment()
highp float occlusion = ChebyshevUpperBound(occlDist, ourDist);
if (occlusion >= 1.0)
{
discard;
}
COLOR = vec4(0.0, 0.0, 0.0, 1.0 - occlusion);
}

View File

@@ -1,5 +1,11 @@
#include "/Shaders/Internal/light_shared.swsl"
highp vec4 calcGaussianWeights(highp float sigma, highp vec4 offset)
{
highp vec4 eExp = offset * offset / (2.0 * sigma * sigma);
return exp(-eExp) / (sigma * sqrt(2.0 * PI));
}
highp float createOcclusion(highp vec2 diff)
{
// Calculate vector perpendicular to light vector.
@@ -8,23 +14,57 @@ highp float createOcclusion(highp vec2 diff)
highp float ourDist = length(diff);
highp vec2 occlDist = occludeDepth(diff, shadowMap, lightIndex);
// Sample 7 points on a line perpendicular to the light source.
// Depending on the closest point, we change the gaussian weights down below
// to change the "narrowness" of the samples.
perpendicular *= lightSoftness * 1.5;
// Get all the samples we need.
highp vec2 sample1 = occludeDepth(diff, shadowMap, lightIndex);
highp vec2 sample2 = occludeDepth(diff + perpendicular, shadowMap, lightIndex);
highp vec2 sample3 = occludeDepth(diff - perpendicular, shadowMap, lightIndex);
highp vec2 sample4 = occludeDepth(diff + perpendicular * 2.0, shadowMap, lightIndex);
highp vec2 sample5 = occludeDepth(diff - perpendicular * 2.0, shadowMap, lightIndex);
highp vec2 sample6 = occludeDepth(diff + perpendicular * 3.0, shadowMap, lightIndex);
highp vec2 sample7 = occludeDepth(diff - perpendicular * 3.0, shadowMap, lightIndex);
highp float mindist =
min(sample1.x,
min(sample2.x,
min(sample3.x,
min(sample4.x,
min(sample5.x,
min(sample6.x,
sample7.x))))));
mindist = max(0.001, mindist);
// Change soft shadow size based on distance from primary occluder.
highp float distRatio = (ourDist - occlDist.x) / occlDist.x / 2.0;
highp float distRatio = (ourDist - mindist);
perpendicular *= distRatio * lightSoftness;
// Sigma can never be zero so make sure to clamp.
// TODO: Scaling the dist ratio here in a more sane way might make shadows look better buuuut I'm lazy.
// Shadows look pretty nice already.
highp float sigma = max(0.001, distRatio * 0.75);
highp vec4 weights = calcGaussianWeights(sigma, vec4(0.0, 1.0, 2.0, 3.0));
// Totally not hacky PCF on top of VSM.
highp float occlusion = smoothstep(0.1, 1.0, ChebyshevUpperBound(occlDist, ourDist));
// Calculation of gaussian weights here is broken because it doesn't add up to 1.
// Fixing this is hard and if I had to guess too expensive for GPU shaders.
// So instead we add up the total weights and scale the result with that,
// so that we still end up with 0-1.
highp float totalWeigths = weights.x + weights.y * 2.0 + weights.z * 2.0 + weights.w * 2.0;
occlusion += shadowContrib(diff + perpendicular);
occlusion += shadowContrib(diff - perpendicular);
occlusion += shadowContrib(diff + perpendicular * 2.0);
occlusion += shadowContrib(diff - perpendicular * 2.0);
occlusion += shadowContrib(diff + perpendicular * 3.0);
occlusion += shadowContrib(diff - perpendicular * 3.0);
highp float occlusion = 0.0;
return occlusion / 7.0;
// Calculate actual occlusion with new weights.
occlusion += ChebyshevUpperBound(sample1, ourDist) * weights.x;
occlusion += ChebyshevUpperBound(sample2, ourDist) * weights.y;
occlusion += ChebyshevUpperBound(sample3, ourDist) * weights.y;
occlusion += ChebyshevUpperBound(sample4, ourDist) * weights.z;
occlusion += ChebyshevUpperBound(sample5, ourDist) * weights.z;
occlusion += ChebyshevUpperBound(sample6, ourDist) * weights.w;
occlusion += ChebyshevUpperBound(sample7, ourDist) * weights.w;
return occlusion / totalWeigths;
}

View File

@@ -0,0 +1,12 @@
using BenchmarkDotNet.Running;
namespace Robust.Benchmarks
{
internal class Program
{
public static void Main(string[] args)
{
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();
}
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\MSBuild\Robust.Properties.targets" />
<Import Project="..\MSBuild\Robust.Engine.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>../bin/Benchmarks</OutputPath>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Robust.Server\Robust.Server.csproj" />
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
</ItemGroup>
<Import Project="..\MSBuild\Robust.Engine.targets" />
</Project>

View File

@@ -0,0 +1,115 @@
using System.IO;
using System.Linq;
using BenchmarkDotNet.Attributes;
using Robust.Benchmarks.Serialization.Definitions;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Robust.Benchmarks.Serialization.Copy
{
public class SerializationCopyBenchmark : SerializationBenchmark
{
public SerializationCopyBenchmark()
{
InitializeSerialization();
DataDefinitionWithString = new DataDefinitionWithString {StringField = "ABC"};
var yamlStream = new YamlStream();
yamlStream.Load(new StringReader(SeedDataDefinition.Prototype));
var seedMapping = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
Seed = SerializationManager.ReadValueOrThrow<SeedDataDefinition>(seedMapping);
}
private const string String = "ABC";
private const int Integer = 1;
private DataDefinitionWithString DataDefinitionWithString { get; }
private SeedDataDefinition Seed { get; }
[Benchmark]
public string? CreateCopyString()
{
return SerializationManager.CreateCopy(String);
}
[Benchmark]
public int? CreateCopyInteger()
{
return SerializationManager.CreateCopy(Integer);
}
[Benchmark]
public DataDefinitionWithString? CreateCopyDataDefinitionWithString()
{
return SerializationManager.CreateCopy(DataDefinitionWithString);
}
[Benchmark]
public SeedDataDefinition? CreateCopySeedDataDefinition()
{
return SerializationManager.CreateCopy(Seed);
}
[Benchmark]
public SeedDataDefinition BaselineCreateCopySeedDataDefinition()
{
// ReSharper disable once UseObjectOrCollectionInitializer
var copy = new SeedDataDefinition();
copy.ID = Seed.ID;
copy.Name = Seed.Name;
copy.SeedName = Seed.SeedName;
copy.SeedNoun = Seed.SeedNoun;
copy.DisplayName = Seed.DisplayName;
copy.RoundStart = Seed.RoundStart;
copy.Mysterious = Seed.Mysterious;
copy.Immutable = Seed.Immutable;
copy.ProductPrototypes = Seed.ProductPrototypes.ToList();
copy.Chemicals = Seed.Chemicals.ToDictionary(p => p.Key, p => p.Value);
copy.ConsumeGasses = Seed.ConsumeGasses.ToDictionary(p => p.Key, p => p.Value);
copy.ExudeGasses = Seed.ExudeGasses.ToDictionary(p => p.Key, p => p.Value);
copy.NutrientConsumption = Seed.NutrientConsumption;
copy.WaterConsumption = Seed.WaterConsumption;
copy.IdealHeat = Seed.IdealHeat;
copy.HeatTolerance = Seed.HeatTolerance;
copy.IdealLight = Seed.IdealLight;
copy.LightTolerance = Seed.LightTolerance;
copy.ToxinsTolerance = Seed.ToxinsTolerance;
copy.LowPressureTolerance = Seed.LowPressureTolerance;
copy.HighPressureTolerance = Seed.HighPressureTolerance;
copy.PestTolerance = Seed.PestTolerance;
copy.WeedTolerance = Seed.WeedTolerance;
copy.Endurance = Seed.Endurance;
copy.Yield = Seed.Yield;
copy.Lifespan = Seed.Lifespan;
copy.Maturation = Seed.Maturation;
copy.Production = Seed.Production;
copy.GrowthStages = Seed.GrowthStages;
copy.HarvestRepeat = Seed.HarvestRepeat;
copy.Potency = Seed.Potency;
copy.Ligneous = Seed.Ligneous;
copy.PlantRsi = Seed.PlantRsi == null
? null!
: new ResourcePath(Seed.PlantRsi.ToString(), Seed.PlantRsi.Separator);
copy.PlantIconState = Seed.PlantIconState;
copy.Bioluminescent = Seed.Bioluminescent;
copy.BioluminescentColor = Seed.BioluminescentColor;
copy.SplatPrototype = Seed.SplatPrototype;
return copy;
}
}
}

View File

@@ -0,0 +1,11 @@
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Benchmarks.Serialization.Definitions
{
[DataDefinition]
public class DataDefinitionWithString
{
[field: DataField("string")]
public string StringField { get; init; } = default!;
}
}

View File

@@ -0,0 +1,119 @@
using System.Collections.Generic;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Robust.Benchmarks.Serialization.Definitions
{
/// <summary>
/// Arbitrarily large data definition for benchmarks.
/// Taken from content.
/// </summary>
[Prototype("seed")]
public class SeedDataDefinition : IPrototype
{
public const string Prototype = @"
- type: seed
id: tobacco
name: tobacco
seedName: tobacco
displayName: tobacco plant
productPrototypes:
- LeavesTobacco
harvestRepeat: Repeat
lifespan: 75
maturation: 5
production: 5
yield: 2
potency: 20
growthStages: 3
idealLight: 9
idealHeat: 298
chemicals:
chem.Nicotine:
Min: 1
Max: 10
PotencyDivisor: 10";
[DataField("id", required: true)] public string ID { get; set; } = default!;
#region Tracking
[DataField("name")] public string Name { get; set; } = string.Empty;
[DataField("seedName")] public string SeedName { get; set; } = string.Empty;
[DataField("seedNoun")] public string SeedNoun { get; set; } = "seeds";
[DataField("displayName")] public string DisplayName { get; set; } = string.Empty;
[DataField("roundStart")] public bool RoundStart { get; set; } = true;
[DataField("mysterious")] public bool Mysterious { get; set; }
[DataField("immutable")] public bool Immutable { get; set; }
#endregion
#region Output
[DataField("productPrototypes")]
public List<string> ProductPrototypes { get; set; } = new();
[DataField("chemicals")]
public Dictionary<string, SeedChemQuantity> Chemicals { get; set; } = new();
[DataField("consumeGasses")]
public Dictionary<Gas, float> ConsumeGasses { get; set; } = new();
[DataField("exudeGasses")]
public Dictionary<Gas, float> ExudeGasses { get; set; } = new();
#endregion
#region Tolerances
[DataField("nutrientConsumption")] public float NutrientConsumption { get; set; } = 0.25f;
[DataField("waterConsumption")] public float WaterConsumption { get; set; } = 3f;
[DataField("idealHeat")] public float IdealHeat { get; set; } = 293f;
[DataField("heatTolerance")] public float HeatTolerance { get; set; } = 20f;
[DataField("idealLight")] public float IdealLight { get; set; } = 7f;
[DataField("lightTolerance")] public float LightTolerance { get; set; } = 5f;
[DataField("toxinsTolerance")] public float ToxinsTolerance { get; set; } = 4f;
[DataField("lowPressureTolerance")] public float LowPressureTolerance { get; set; } = 25f;
[DataField("highPressureTolerance")] public float HighPressureTolerance { get; set; } = 200f;
[DataField("pestTolerance")] public float PestTolerance { get; set; } = 5f;
[DataField("weedTolerance")] public float WeedTolerance { get; set; } = 5f;
#endregion
#region General traits
[DataField("endurance")] public float Endurance { get; set; } = 100f;
[DataField("yield")] public int Yield { get; set; }
[DataField("lifespan")] public float Lifespan { get; set; }
[DataField("maturation")] public float Maturation { get; set; }
[DataField("production")] public float Production { get; set; }
[DataField("growthStages")] public int GrowthStages { get; set; } = 6;
[DataField("harvestRepeat")] public HarvestType HarvestRepeat { get; set; } = HarvestType.NoRepeat;
[DataField("potency")] public float Potency { get; set; } = 1f;
[DataField("ligneous")] public bool Ligneous { get; set; }
#endregion
#region Cosmetics
[DataField("plantRsi", required: true)] public ResourcePath PlantRsi { get; set; } = default!;
[DataField("plantIconState")] public string PlantIconState { get; set; } = "produce";
[DataField("bioluminescent")] public bool Bioluminescent { get; set; }
[DataField("bioluminescentColor")] public Color BioluminescentColor { get; set; } = Color.White;
[DataField("splatPrototype")] public string? SplatPrototype { get; set; }
#endregion
}
public enum HarvestType
{
NoRepeat,
Repeat
}
public enum Gas
{
}
[DataDefinition]
public struct SeedChemQuantity
{
[DataField("Min")]
public int Min;
[DataField("Max")]
public int Max;
[DataField("PotencyDivisor")]
public int PotencyDivisor;
}
}

View File

@@ -0,0 +1,21 @@
using BenchmarkDotNet.Attributes;
using Robust.Shared.Serialization.Manager;
namespace Robust.Benchmarks.Serialization.Initialize
{
public class SerializationInitializeBenchmark : SerializationBenchmark
{
[IterationCleanup]
public void IterationCleanup()
{
SerializationManager.Shutdown();
}
[Benchmark]
public ISerializationManager Initialize()
{
InitializeSerialization();
return SerializationManager;
}
}
}

View File

@@ -0,0 +1,59 @@
using System.IO;
using BenchmarkDotNet.Attributes;
using Robust.Benchmarks.Serialization.Definitions;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Value;
using YamlDotNet.RepresentationModel;
namespace Robust.Benchmarks.Serialization.Read
{
public class SerializationReadBenchmark : SerializationBenchmark
{
public SerializationReadBenchmark()
{
InitializeSerialization();
StringDataDefNode = new MappingDataNode();
StringDataDefNode.Add(new ValueDataNode("string"), new ValueDataNode("ABC"));
var yamlStream = new YamlStream();
yamlStream.Load(new StringReader(SeedDataDefinition.Prototype));
SeedNode = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
}
private ValueDataNode StringNode { get; } = new("ABC");
private ValueDataNode IntNode { get; } = new("1");
private MappingDataNode StringDataDefNode { get; }
private MappingDataNode SeedNode { get; }
[Benchmark]
public string? ReadString()
{
return SerializationManager.ReadValue<string>(StringNode);
}
[Benchmark]
public int? ReadInteger()
{
return SerializationManager.ReadValue<int>(IntNode);
}
[Benchmark]
public DataDefinitionWithString? ReadDataDefinitionWithString()
{
return SerializationManager.ReadValue<DataDefinitionWithString>(StringDataDefNode);
}
[Benchmark]
public SeedDataDefinition? ReadSeedDataDefinition()
{
return SerializationManager.ReadValue<SeedDataDefinition>(SeedNode);
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using Robust.Server;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Manager;
namespace Robust.Benchmarks.Serialization
{
public abstract class SerializationBenchmark
{
public SerializationBenchmark()
{
IoCManager.InitThread();
ServerIoC.RegisterIoC();
IoCManager.BuildGraph();
var assemblies = new[]
{
AppDomain.CurrentDomain.GetAssemblyByName("Robust.Shared"),
AppDomain.CurrentDomain.GetAssemblyByName("Robust.Server"),
AppDomain.CurrentDomain.GetAssemblyByName("Robust.Benchmarks")
};
foreach (var assembly in assemblies)
{
IoCManager.Resolve<IConfigurationManagerInternal>().LoadCVarsFromAssembly(assembly);
}
IoCManager.Resolve<IReflectionManager>().LoadAssemblies(assemblies);
SerializationManager = IoCManager.Resolve<ISerializationManager>();
}
protected ISerializationManager SerializationManager { get; }
public void InitializeSerialization()
{
SerializationManager.Initialize();
}
}
}

View File

@@ -0,0 +1,98 @@
using System.Globalization;
using System.IO;
using BenchmarkDotNet.Attributes;
using Robust.Benchmarks.Serialization.Definitions;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Value;
using YamlDotNet.RepresentationModel;
namespace Robust.Benchmarks.Serialization.Write
{
public class SerializationWriteBenchmark : SerializationBenchmark
{
public SerializationWriteBenchmark()
{
InitializeSerialization();
DataDefinitionWithString = new DataDefinitionWithString {StringField = "ABC"};
var yamlStream = new YamlStream();
yamlStream.Load(new StringReader(SeedDataDefinition.Prototype));
var seedMapping = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
Seed = SerializationManager.ReadValueOrThrow<SeedDataDefinition>(seedMapping);
}
private const string String = "ABC";
private const int Integer = 1;
private DataDefinitionWithString DataDefinitionWithString { get; }
private SeedDataDefinition Seed { get; }
[Benchmark]
public DataNode WriteString()
{
return SerializationManager.WriteValue(String);
}
[Benchmark]
public DataNode WriteInteger()
{
return SerializationManager.WriteValue(Integer);
}
[Benchmark]
public DataNode WriteDataDefinitionWithString()
{
return SerializationManager.WriteValue(DataDefinitionWithString);
}
[Benchmark]
public DataNode WriteSeedDataDefinition()
{
return SerializationManager.WriteValue(Seed);
}
[Benchmark]
public DataNode BaselineWriteSeedDataDefinition()
{
var mapping = new MappingDataNode();
mapping.Add("id", Seed.ID);
mapping.Add("name", Seed.Name);
mapping.Add("seedName", Seed.SeedName);
mapping.Add("displayName", Seed.DisplayName);
mapping.Add("productPrototypes", Seed.ProductPrototypes);
mapping.Add("harvestRepeat", Seed.HarvestRepeat.ToString());
mapping.Add("lifespan", Seed.Lifespan.ToString(CultureInfo.InvariantCulture));
mapping.Add("maturation", Seed.Maturation.ToString(CultureInfo.InvariantCulture));
mapping.Add("production", Seed.Production.ToString(CultureInfo.InvariantCulture));
mapping.Add("yield", Seed.Yield.ToString(CultureInfo.InvariantCulture));
mapping.Add("potency", Seed.Potency.ToString(CultureInfo.InvariantCulture));
mapping.Add("growthStages", Seed.GrowthStages.ToString(CultureInfo.InvariantCulture));
mapping.Add("idealLight", Seed.IdealLight.ToString(CultureInfo.InvariantCulture));
mapping.Add("idealHeat", Seed.IdealHeat.ToString(CultureInfo.InvariantCulture));
var chemicals = new MappingDataNode();
foreach (var (name, quantity) in Seed.Chemicals)
{
chemicals.Add(name, new MappingDataNode
{
["Min"] = new ValueDataNode(quantity.Min.ToString(CultureInfo.InvariantCulture)),
["Max"] = new ValueDataNode(quantity.Max.ToString(CultureInfo.InvariantCulture)),
["PotencyDivisor"] = new ValueDataNode(quantity.PotencyDivisor.ToString(CultureInfo.InvariantCulture))
});
}
mapping.Add("chemicals", chemicals);
return mapping;
}
}
}

View File

@@ -247,7 +247,6 @@ namespace {nameSpace}
DiagnosticSeverity.Error,
true),
Location.None));
return null;
}
}

View File

@@ -15,6 +15,7 @@ using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using Logger = Robust.Shared.Log.Logger;
namespace Robust.Client.Audio.Midi
@@ -33,24 +34,6 @@ namespace Robust.Client.Audio.Midi
/// </returns>
IMidiRenderer? GetNewRenderer();
/*
/// <summary>
/// Checks whether the file at the given path is a valid midi file or not.
/// </summary>
/// <remarks>
/// We add this here so content doesn't need to reference NFluidsynth.
/// </remarks>
bool IsMidiFile(string filename);
/// <summary>
/// Checks whether the file at the given path is a valid midi file or not.
/// </summary>
/// <remarks>
/// We add this here so content doesn't need to reference NFluidsynth.
/// </remarks>
bool IsSoundfontFile(string filename);
*/
/// <summary>
/// Method called every frame.
/// Should be used to update positional audio.
@@ -58,6 +41,11 @@ namespace Robust.Client.Audio.Midi
/// <param name="frameTime"></param>
void FrameUpdate(float frameTime);
/// <summary>
/// Volume, in db.
/// </summary>
float Volume { get; set; }
/// <summary>
/// If true, MIDI support is available.
/// </summary>
@@ -76,6 +64,7 @@ namespace Robust.Client.Audio.Midi
private SharedBroadPhaseSystem _broadPhaseSystem = default!;
[ViewVariables]
public bool IsAvailable
{
get
@@ -86,12 +75,29 @@ namespace Robust.Client.Audio.Midi
}
}
private readonly List<MidiRenderer> _renderers = new();
[ViewVariables]
private readonly List<IMidiRenderer> _renderers = new();
private bool _alive = true;
private Settings? _settings;
private Thread? _midiThread;
private ISawmill _midiSawmill = default!;
private float _volume = 0f;
private bool _volumeDirty = true;
[ViewVariables(VVAccess.ReadWrite)]
public float Volume
{
get => _volume;
set
{
if (MathHelper.CloseTo(_volume, value))
return;
_volume = value;
_volumeDirty = true;
}
}
private static readonly string[] LinuxSoundfonts =
{
@@ -118,6 +124,7 @@ namespace Robust.Client.Audio.Midi
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
private ISawmill _sawmill = default!;
[ViewVariables(VVAccess.ReadWrite)]
public int OcclusionCollisionMask { get; set; }
private void InitializeFluidsynth()
@@ -176,18 +183,6 @@ namespace Robust.Client.Audio.Midi
_sawmill.Log(rLevel, message);
}
/*
public bool IsMidiFile(string filename)
{
return SoundFont.IsMidiFile(filename);
}
public bool IsSoundfontFile(string filename)
{
return SoundFont.IsSoundFont(filename);
}
*/
public IMidiRenderer? GetNewRenderer()
{
if (!FluidsynthInitialized)
@@ -251,7 +246,9 @@ namespace Robust.Client.Audio.Midi
}
lock (_renderers)
{
_renderers.Add(renderer);
}
return renderer;
}
@@ -269,69 +266,79 @@ namespace Robust.Client.Audio.Midi
}
// Update positions of streams every frame.
lock (_renderers)
foreach (var renderer in _renderers)
foreach (var renderer in _renderers)
{
if (renderer.Disposed)
continue;
if(_volumeDirty)
renderer.Source.SetVolume(Volume);
if (!renderer.Mono)
{
if (renderer.Disposed)
continue;
if (!renderer.Mono)
{
renderer.Source.SetGlobal();
continue;
}
MapCoordinates? mapPos = null;
if (renderer.TrackingCoordinates != null)
{
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
}
else if (renderer.TrackingEntity != null)
{
mapPos = renderer.TrackingEntity.Transform.MapPosition;
}
if (mapPos != null)
{
var pos = mapPos.Value;
if (pos.MapId != _eyeManager.CurrentMap)
{
renderer.Source.SetVolume(-10000000);
}
else
{
var sourceRelative = _eyeManager.CurrentEye.Position.Position - pos.Position;
var occlusion = 0f;
if (sourceRelative.Length > 0)
{
occlusion = _broadPhaseSystem.IntersectRayPenetration(
pos.MapId,
new CollisionRay(
pos.Position,
sourceRelative.Normalized,
OcclusionCollisionMask),
sourceRelative.Length,
renderer.TrackingEntity);
}
renderer.Source.SetOcclusion(occlusion);
}
if (renderer.Source.SetPosition(pos.Position))
{
continue;
}
if (float.IsNaN(pos.Position.X) || float.IsNaN(pos.Position.Y))
{
// just duck out instead of move to NaN
renderer.Source.SetOcclusion(float.MaxValue);
continue;
}
_midiSawmill?.Warning("Interrupting positional audio, can't set position.");
renderer.Source.StopPlaying();
}
renderer.Source.SetGlobal();
continue;
}
MapCoordinates? mapPos = null;
if (renderer.TrackingCoordinates != null)
{
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
}
else if (renderer.TrackingEntity != null)
{
mapPos = renderer.TrackingEntity.Transform.MapPosition;
}
if (mapPos != null)
{
var pos = mapPos.Value;
if (pos.MapId != _eyeManager.CurrentMap)
{
renderer.Source.SetVolume(-10000000);
}
else
{
var sourceRelative = _eyeManager.CurrentEye.Position.Position - pos.Position;
var occlusion = 0f;
if (sourceRelative.Length > 0)
{
occlusion = _broadPhaseSystem.IntersectRayPenetration(
pos.MapId,
new CollisionRay(
pos.Position,
sourceRelative.Normalized,
OcclusionCollisionMask),
sourceRelative.Length,
renderer.TrackingEntity);
}
renderer.Source.SetOcclusion(occlusion);
}
if (renderer.Source.SetPosition(pos.Position))
{
continue;
}
if (renderer.TrackingEntity != null)
{
renderer.Source.SetVelocity(renderer.TrackingEntity.GlobalLinearVelocity());
}
if (float.IsNaN(pos.Position.X) || float.IsNaN(pos.Position.Y))
{
// just duck out instead of move to NaN
renderer.Source.SetOcclusion(float.MaxValue);
continue;
}
_midiSawmill?.Warning("Interrupting positional audio, can't set position.");
renderer.Source.StopPlaying();
}
}
_volumeDirty = false;
}
/// <summary>
@@ -342,6 +349,7 @@ namespace Robust.Client.Audio.Midi
while (_alive)
{
lock (_renderers)
{
for (var i = 0; i < _renderers.Count; i++)
{
var renderer = _renderers[i];
@@ -349,10 +357,11 @@ namespace Robust.Client.Audio.Midi
renderer.Render();
else
{
((IMidiRenderer)renderer).InternalDispose();
renderer.InternalDispose();
_renderers.Remove(renderer);
}
}
}
Thread.Sleep(1);
}
@@ -363,9 +372,13 @@ namespace Robust.Client.Audio.Midi
_alive = false;
_midiThread?.Join();
_settings?.Dispose();
foreach (var renderer in _renderers)
lock (_renderers)
{
renderer?.Dispose();
foreach (var renderer in _renderers)
{
renderer?.Dispose();
}
}
if (FluidsynthInitialized && !_failedInitialize)
@@ -420,6 +433,7 @@ namespace Robust.Client.Audio.Midi
var span = new Span<byte>(buf.ToPointer(), length);
var stream = _openStreams[(int) sfHandle];
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
try
{
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
@@ -443,6 +457,7 @@ namespace Robust.Client.Audio.Midi
{
return -1;
}
return 0;
}
@@ -464,10 +479,12 @@ namespace Robust.Client.Audio.Midi
public override int Close(IntPtr sfHandle)
{
var stream = _openStreams[(int) sfHandle];
if (!_openStreams.Remove((int) sfHandle, out var stream))
return -1;
stream.Dispose();
_openStreams.Remove((int) sfHandle);
return 0;
}
}
}

View File

@@ -8,6 +8,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using MidiEvent = NFluidsynth.MidiEvent;
namespace Robust.Client.Audio.Midi
@@ -21,6 +22,17 @@ namespace Robust.Client.Audio.Midi
public interface IMidiRenderer : IDisposable
{
/// <summary>
/// The buffered audio source of this renderer.
/// </summary>
internal IClydeBufferedAudioSource Source { get; }
/// <summary>
/// Whether this renderer has been disposed or not.
/// </summary>
bool Disposed { get; }
/// <summary>
/// This controls whether the midi file being played will loop or not.
/// </summary>
@@ -110,6 +122,11 @@ namespace Robust.Client.Audio.Midi
/// </summary>
void StopAllNotes();
/// <summary>
/// Render and play MIDI to the audio source.
/// </summary>
internal void Render();
/// <summary>
/// Loads a new soundfont into the renderer.
/// </summary>
@@ -159,7 +176,7 @@ namespace Robust.Client.Audio.Midi
internal void InternalDispose();
}
public class MidiRenderer : IMidiRenderer
internal class MidiRenderer : IMidiRenderer
{
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
@@ -188,8 +205,12 @@ namespace Robust.Client.Audio.Midi
private readonly object _playerStateLock = new();
private SequencerClientId _synthRegister;
public IClydeBufferedAudioSource Source { get; set; }
IClydeBufferedAudioSource IMidiRenderer.Source => Source;
[ViewVariables]
public bool Disposed { get; private set; } = false;
[ViewVariables(VVAccess.ReadWrite)]
public byte MidiProgram
{
get => _midiProgram;
@@ -203,6 +224,7 @@ namespace Robust.Client.Audio.Midi
}
}
[ViewVariables(VVAccess.ReadWrite)]
public byte MidiBank
{
get => _midiBank;
@@ -216,6 +238,7 @@ namespace Robust.Client.Audio.Midi
}
}
[ViewVariables(VVAccess.ReadWrite)]
public uint MidiSoundfont
{
get => _midiSoundfont;
@@ -229,10 +252,16 @@ namespace Robust.Client.Audio.Midi
}
}
[ViewVariables(VVAccess.ReadWrite)]
public bool DisablePercussionChannel { get; set; } = true;
[ViewVariables(VVAccess.ReadWrite)]
public bool DisableProgramChangeEvent { get; set; } = true;
[ViewVariables(VVAccess.ReadWrite)]
public int PlayerTotalTick => _player?.GetTotalTicks ?? 0;
[ViewVariables(VVAccess.ReadWrite)]
public int PlayerTick
{
get => _player?.CurrentTick ?? 0;
@@ -243,12 +272,19 @@ namespace Robust.Client.Audio.Midi
}
}
[ViewVariables(VVAccess.ReadWrite)]
public uint SequencerTick => _sequencer?.Tick ?? 0;
[ViewVariables(VVAccess.ReadWrite)]
public double SequencerTimeScale => _sequencer?.TimeScale ?? 0;
[ViewVariables(VVAccess.ReadWrite)]
public bool Mono { get; set; }
[ViewVariables]
public MidiRendererStatus Status { get; private set; } = MidiRendererStatus.None;
[ViewVariables(VVAccess.ReadWrite)]
public bool LoopMidi
{
get => _loopMidi;
@@ -260,10 +296,11 @@ namespace Robust.Client.Audio.Midi
}
}
[ViewVariables(VVAccess.ReadWrite)]
public IEntity? TrackingEntity { get; set; } = null;
public EntityCoordinates? TrackingCoordinates { get; set; } = null;
internal bool Free { get; set; } = false;
[ViewVariables(VVAccess.ReadWrite)]
public EntityCoordinates? TrackingCoordinates { get; set; } = null;
internal MidiRenderer(Settings settings, SoundFontLoader soundFontLoader, bool mono = true)
{
@@ -294,7 +331,11 @@ namespace Robust.Client.Audio.Midi
Status = MidiRendererStatus.Input;
StopAllNotes();
_driver = new MidiDriver(_settings, MidiDriverEventHandler);
lock (_playerStateLock)
{
_driver = new MidiDriver(_settings, MidiDriverEventHandler);
}
return true;
}
@@ -332,8 +373,13 @@ namespace Robust.Client.Audio.Midi
{
if (Status != MidiRendererStatus.Input) return false;
Status = MidiRendererStatus.None;
_driver?.Dispose();
_driver = null;
lock (_playerStateLock)
{
_driver?.Dispose();
_driver = null;
}
StopAllNotes();
return true;
}
@@ -357,7 +403,8 @@ namespace Robust.Client.Audio.Midi
public void StopAllNotes()
{
_synth.AllNotesOff(-1);
lock(_playerStateLock)
_synth.AllNotesOff(-1);
}
public void LoadSoundfont(string filename, bool resetPresets = false)
@@ -372,7 +419,12 @@ namespace Robust.Client.Audio.Midi
public event Action<Shared.Audio.Midi.MidiEvent>? OnMidiEvent;
public event Action? OnMidiPlayerFinished;
internal void Render(int length = SampleRate / 250)
void IMidiRenderer.Render()
{
Render();
}
private void Render(int length = SampleRate / 250)
{
if (Disposed) return;
@@ -452,6 +504,7 @@ namespace Robust.Client.Audio.Midi
var timestamp = SequencerTick;
var midiEv = (Shared.Audio.Midi.MidiEvent) midiEvent;
midiEv.Tick = timestamp;
midiEvent.Dispose();
SendMidiEvent(midiEv);
return 0;
}
@@ -462,6 +515,7 @@ namespace Robust.Client.Audio.Midi
var timestamp = SequencerTick;
var midiEv = (Shared.Audio.Midi.MidiEvent) midiEvent;
midiEv.Tick = timestamp;
midiEvent.Dispose();
SendMidiEvent(midiEv);
return 0;
}

View File

@@ -8,10 +8,12 @@ using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -24,6 +26,7 @@ namespace Robust.Client
[Dependency] private readonly IPlayerManager _playMan = default!;
[Dependency] private readonly INetConfigurationManager _configManager = default!;
[Dependency] private readonly IClientEntityManager _entityManager = default!;
[Dependency] private readonly IEntityLookup _entityLookup = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IDiscordRichPresence _discord = default!;
[Dependency] private readonly IGameTiming _timing = default!;
@@ -96,6 +99,25 @@ namespace Robust.Client
_net.ClientDisconnect(reason);
}
/// <inheritdoc />
public void StartSinglePlayer()
{
DebugTools.Assert(RunLevel < ClientRunLevel.Connecting);
DebugTools.Assert(!_net.IsConnected);
_playMan.Startup();
_playMan.LocalPlayer!.Name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName);
OnRunLevelChanged(ClientRunLevel.SinglePlayerGame);
GameStartedSetup();
}
/// <inheritdoc />
public void StopSinglePlayer()
{
DebugTools.Assert(RunLevel == ClientRunLevel.SinglePlayerGame);
DebugTools.Assert(!_net.IsConnected);
GameStoppedReset();
}
/// <inheritdoc />
public event EventHandler<RunLevelChangedEventArgs>? RunLevelChanged;
@@ -132,7 +154,7 @@ namespace Robust.Client
var userId = _net.ServerChannel.UserId;
_discord.Update(info.ServerName, userName, info.ServerMaxPlayers.ToString());
// start up player management
_playMan.Startup(_net.ServerChannel!);
_playMan.Startup();
_playMan.LocalPlayer!.UserId = userId;
_playMan.LocalPlayer.Name = userName;
@@ -145,16 +167,13 @@ namespace Robust.Client
/// receiving states when they join the lobby.
/// </summary>
/// <param name="session">Session of the player.</param>
private void OnPlayerJoinedServer(IPlayerSession session)
private void OnPlayerJoinedServer(ICommonSession session)
{
DebugTools.Assert(RunLevel < ClientRunLevel.Connected);
OnRunLevelChanged(ClientRunLevel.Connected);
_entityManager.Startup();
_mapManager.Startup();
GameStartedSetup();
_timing.ResetSimTime();
_timing.Paused = false;
PlayerJoinedServer?.Invoke(this, new PlayerEventArgs(session));
}
@@ -162,7 +181,7 @@ namespace Robust.Client
/// Player is joining the game
/// </summary>
/// <param name="session">Session of the player.</param>
private void OnPlayerJoinedGame(IPlayerSession session)
private void OnPlayerJoinedGame(ICommonSession session)
{
DebugTools.Assert(RunLevel >= ClientRunLevel.Connected);
OnRunLevelChanged(ClientRunLevel.InGame);
@@ -189,10 +208,25 @@ namespace Robust.Client
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalPlayer?.Session));
LastDisconnectReason = args.Reason;
GameStoppedReset();
}
private void GameStartedSetup()
{
_entityManager.Startup();
_mapManager.Startup();
_entityLookup.Startup();
_timing.ResetSimTime();
_timing.Paused = false;
}
private void GameStoppedReset()
{
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
_gameStates.Reset();
_playMan.Shutdown();
_entityLookup.Shutdown();
_entityManager.Shutdown();
_mapManager.Shutdown();
_discord.ClearPresence();
@@ -249,6 +283,11 @@ namespace Robust.Client
/// The client is now in the game, moving around.
/// </summary>
InGame,
/// <summary>
/// The client is now in singleplayer mode, in-game.
/// </summary>
SinglePlayerGame,
}
/// <summary>
@@ -259,12 +298,12 @@ namespace Robust.Client
/// <summary>
/// The session that triggered the event.
/// </summary>
private IPlayerSession? Session { get; }
private ICommonSession? Session { get; }
/// <summary>
/// Constructs a new instance of the class.
/// </summary>
public PlayerEventArgs(IPlayerSession? session)
public PlayerEventArgs(ICommonSession? session)
{
Session = session;
}

View File

@@ -14,6 +14,7 @@ using Robust.Client.Prototypes;
using Robust.Client.Reflection;
using Robust.Client.ResourceManagement;
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Client.UserInterface;
using Robust.Client.Utility;
using Robust.Client.ViewVariables;
@@ -27,8 +28,7 @@ using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Timing;
namespace Robust.Client
{
@@ -38,11 +38,18 @@ namespace Robust.Client
{
SharedIoC.RegisterIoC();
IoCManager.Register<IGameTiming, ClientGameTiming>();
IoCManager.Register<IClientGameTiming, ClientGameTiming>();
IoCManager.Register<IPrototypeManager, ClientPrototypeManager>();
IoCManager.Register<IMapManager, ClientMapManager>();
IoCManager.Register<IMapManagerInternal, ClientMapManager>();
IoCManager.Register<IClientMapManager, ClientMapManager>();
IoCManager.Register<IEntityManager, ClientEntityManager>();
IoCManager.Register<IEntityLookup, EntityLookup>();
IoCManager.Register<IComponentFactory, ClientComponentFactory>();
IoCManager.Register<ITileDefinitionManager, ClydeTileDefinitionManager>();
IoCManager.Register<IClydeTileDefinitionManager, ClydeTileDefinitionManager>();
IoCManager.Register<GameController, GameController>();
IoCManager.Register<IGameController, GameController>();
IoCManager.Register<IGameControllerInternal, GameController>();
IoCManager.Register<IReflectionManager, ClientReflectionManager>();
@@ -52,7 +59,8 @@ namespace Robust.Client
IoCManager.Register<IResourceCacheInternal, ResourceCache>();
IoCManager.Register<IClientNetManager, NetManager>();
IoCManager.Register<IClientEntityManager, ClientEntityManager>();
IoCManager.Register<IEntityNetworkManager, ClientEntityNetworkManager>();
IoCManager.Register<IClientEntityManagerInternal, ClientEntityManager>();
IoCManager.Register<IEntityNetworkManager, ClientEntityManager>();
IoCManager.Register<IClientGameStateManager, ClientGameStateManager>();
IoCManager.Register<IBaseClient, BaseClient>();
IoCManager.Register<IPlayerManager, PlayerManager>();
@@ -66,8 +74,6 @@ namespace Robust.Client
IoCManager.Register<IDiscordRichPresence, DiscordRichPresence>();
IoCManager.Register<IClientConsoleHost, ClientConsoleHost>();
IoCManager.Register<IConsoleHost, ClientConsoleHost>();
IoCManager.Register<IFontManager, FontManager>();
IoCManager.Register<IFontManagerInternal, FontManager>();
IoCManager.Register<IMidiManager, MidiManager>();
IoCManager.Register<IAuthManager, AuthManager>();
switch (mode)
@@ -94,8 +100,9 @@ namespace Robust.Client
throw new ArgumentOutOfRangeException();
}
IoCManager.Register<IFontManager, FontManager>();
IoCManager.Register<IFontManagerInternal, FontManager>();
IoCManager.Register<IEyeManager, EyeManager>();
IoCManager.Register<IPlacementManager, PlacementManager>();
IoCManager.Register<IOverlayManager, OverlayManager>();
IoCManager.Register<IOverlayManagerInternal, OverlayManager>();

View File

@@ -280,12 +280,12 @@ namespace Robust.Client.Console.Commands
internal class SnapGridGetCell : IConsoleCommand
{
public string Command => "sggcell";
public string Help => "sggcell <gridID> <vector2i> [offset]\nThat vector2i param is in the form x<int>,y<int>.";
public string Help => "sggcell <gridID> <vector2i>\nThat vector2i param is in the form x<int>,y<int>.";
public string Description => "Lists entities on a snap grid cell.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2 && args.Length != 3)
if (args.Length != 2)
{
shell.WriteLine(Help);
return;
@@ -293,7 +293,6 @@ namespace Robust.Client.Console.Commands
string gridId = args[0];
string indices = args[1];
string offset = args.Length == 3 ? args[2] : "Center";
if (!int.TryParse(args[0], out var id))
{
@@ -307,29 +306,17 @@ namespace Robust.Client.Console.Commands
return;
}
SnapGridOffset selectedOffset;
if (Enum.IsDefined(typeof(SnapGridOffset), offset))
{
selectedOffset = (SnapGridOffset)Enum.Parse(typeof(SnapGridOffset), offset);
}
else
{
shell.WriteError("given offset type is not defined");
return;
}
var mapMan = IoCManager.Resolve<IMapManager>();
if (mapMan.GridExists(new GridId(int.Parse(gridId, CultureInfo.InvariantCulture))))
{
foreach (var entity in
mapMan.GetGrid(new GridId(int.Parse(gridId, CultureInfo.InvariantCulture))).GetSnapGridCell(
mapMan.GetGrid(new GridId(int.Parse(gridId, CultureInfo.InvariantCulture))).GetAnchoredEntities(
new Vector2i(
int.Parse(indices.Split(',')[0], CultureInfo.InvariantCulture),
int.Parse(indices.Split(',')[1], CultureInfo.InvariantCulture)),
selectedOffset))
int.Parse(indices.Split(',')[1], CultureInfo.InvariantCulture))))
{
shell.WriteLine(entity.Owner.Uid.ToString());
shell.WriteLine(entity.ToString());
}
}
else
@@ -678,10 +665,10 @@ namespace Robust.Client.Console.Commands
public string Description => "Gets the system clipboard";
public string Help => "getclipboard";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public async void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mgr = IoCManager.Resolve<IClipboardManager>();
shell.WriteLine(mgr.GetText());
shell.WriteLine(await mgr.GetText());
}
}
@@ -1097,15 +1084,8 @@ namespace Robust.Client.Console.Commands
var key = (Keyboard.Key) parsed!;
var name = clyde.GetKeyName(key);
var scanCode = clyde.GetKeyScanCode(key);
var nameScanCode = clyde.GetKeyNameScanCode(scanCode);
shell.WriteLine($"name: '{name}' scan code: '{scanCode}' name via scan code: '{nameScanCode}'");
}
else if (int.TryParse(args[0], out var scanCode))
{
var nameScanCode = clyde.GetKeyNameScanCode(scanCode);
shell.WriteLine($"name via scan code: '{nameScanCode}'");
shell.WriteLine($"name: '{name}' ");
}
}
}

View File

@@ -0,0 +1,72 @@
using System.Linq;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
[UsedImplicitly]
public sealed class LsMonitorCommand : IConsoleCommand
{
public string Command => "lsmonitor";
public string Description => "";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var clyde = IoCManager.Resolve<IClyde>();
foreach (var monitor in clyde.EnumerateMonitors())
{
shell.WriteLine(
$"[{monitor.Id}] {monitor.Name}: {monitor.Size.X}x{monitor.Size.Y}@{monitor.RefreshRate}Hz");
}
}
}
[UsedImplicitly]
public sealed class MonitorInfoCommand : IConsoleCommand
{
public string Command => "monitorinfo";
public string Description => "";
public string Help => "Usage: monitorinfo <id>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 1)
{
shell.WriteError("Expected one argument.");
return;
}
var clyde = IoCManager.Resolve<IClyde>();
var monitor = clyde.EnumerateMonitors().Single(c => c.Id == int.Parse(args[0]));
shell.WriteLine($"{monitor.Id}: {monitor.Name}");
shell.WriteLine($"Video modes:");
foreach (var mode in monitor.VideoModes)
{
shell.WriteLine($" {mode.Width}x{mode.Height} {mode.RefreshRate} Hz {mode.RedBits}/{mode.GreenBits}/{mode.BlueBits}");
}
}
}
[UsedImplicitly]
public sealed class SetMonitorCommand : IConsoleCommand
{
public string Command => "setmonitor";
public string Description => "";
public string Help => "Usage: setmonitor <id>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var clyde = IoCManager.Resolve<IClyde>();
var id = int.Parse(args[0]);
var monitor = clyde.EnumerateMonitors().Single(m => m.Id == id);
clyde.SetWindowMonitor(monitor);
}
}
}

View File

@@ -1,11 +1,12 @@
using System;
using Robust.Shared.Console;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
class QuitCommand : IConsoleCommand
class HardQuitCommand : IConsoleCommand
{
public string Command => "quit";
public string Command => "hardquit";
public string Description => "Kills the game client instantly.";
public string Help => "Kills the game client instantly, leaving no traces. No telling the server goodbye";
@@ -14,4 +15,16 @@ namespace Robust.Client.Console.Commands
Environment.Exit(0);
}
}
class QuitCommand : IConsoleCommand
{
public string Command => "quit";
public string Description => "Shuts down the game client gracefully.";
public string Help => "Properly shuts down the game client, notifying the connected server and such.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
IoCManager.Resolve<IGameController>().Shutdown("quit command used");
}
}
}

View File

@@ -30,7 +30,8 @@ namespace Robust.Client.Console
var console = new ScriptConsoleServer(this, session);
_activeConsoles.Add(session, console);
console.Open();
// FIXME: When this is Open(), resizing the window will cause its position to get NaN'd.
console.OpenCentered();
}
private void ReceiveScriptResponse(MsgScriptResponse message)

View File

@@ -5,10 +5,15 @@ namespace Robust.Client
public static void Start(string[] args)
{
#if FULL_RELEASE
throw new System.InvalidOperationException("ContentStart is not available on a full release.");
throw new System.InvalidOperationException("ContentStart.Start is not available on a full release.");
#else
GameController.Start(args, true);
#endif
}
public static void StartLibrary(string[] args, GameControllerOptions options)
{
GameController.Start(args, true, null, options);
}
}
}

View File

@@ -8,9 +8,11 @@ using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Physics.Dynamics.Joints;
using Robust.Shared.Prototypes;
namespace Robust.Client.Debugging
@@ -19,11 +21,10 @@ namespace Robust.Client.Debugging
public class DebugDrawing : IDebugDrawing
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IComponentManager _componentManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPhysicsManager _physicsManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
private bool _debugColliders;
@@ -44,8 +45,8 @@ namespace Robust.Client.Debugging
if (value && !_overlayManager.HasOverlay<PhysicsOverlay>())
{
_overlayManager.AddOverlay(new PhysicsOverlay(_componentManager, _eyeManager,
_prototypeManager, _inputManager, _physicsManager));
_overlayManager.AddOverlay(new PhysicsOverlay(_eyeManager,
_prototypeManager, _inputManager, _mapManager));
}
else
{
@@ -80,10 +81,9 @@ namespace Robust.Client.Debugging
private class PhysicsOverlay : Overlay
{
private readonly IComponentManager _componentManager;
private readonly IEyeManager _eyeManager;
private readonly IMapManager _mapManager;
private readonly IInputManager _inputManager;
private readonly IPhysicsManager _physicsManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace;
private readonly ShaderInstance _shader;
@@ -93,12 +93,11 @@ namespace Robust.Client.Debugging
private List<IPhysBody> _hoverBodies = new();
public PhysicsOverlay(IComponentManager compMan, IEyeManager eyeMan, IPrototypeManager protoMan, IInputManager inputManager, IPhysicsManager physicsManager)
public PhysicsOverlay(IEyeManager eyeMan, IPrototypeManager protoMan, IInputManager inputManager, IMapManager mapManager)
{
_componentManager = compMan;
_eyeManager = eyeMan;
_inputManager = inputManager;
_physicsManager = physicsManager;
_mapManager = mapManager;
_shader = protoMan.Index<ShaderPrototype>("unshaded").Instance();
var cache = IoCManager.Resolve<IResourceCache>();
@@ -106,22 +105,23 @@ namespace Robust.Client.Debugging
}
/// <inheritdoc />
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
protected internal override void Draw(in OverlayDrawArgs args)
{
switch (currentSpace)
switch (args.Space)
{
case OverlaySpace.ScreenSpace:
DrawScreen((DrawingHandleScreen) handle);
DrawScreen(args);
break;
case OverlaySpace.WorldSpace:
DrawWorld((DrawingHandleWorld) handle);
DrawWorld(args);
break;
}
}
private void DrawScreen(DrawingHandleScreen screenHandle)
private void DrawScreen(in OverlayDrawArgs args)
{
var screenHandle = args.ScreenHandle;
var lineHeight = _font.GetLineHeight(1f);
Vector2 drawPos = _hoverStartScreen + new Vector2(20, 0) + new Vector2(0, -(_hoverBodies.Count * 4 * lineHeight / 2f));
int row = 0;
@@ -134,7 +134,7 @@ namespace Robust.Client.Debugging
row++;
}
DrawString(screenHandle, _font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {body.Entity}");
DrawString(screenHandle, _font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {body.Owner}");
row++;
DrawString(screenHandle, _font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
row++;
@@ -146,38 +146,47 @@ namespace Robust.Client.Debugging
}
private void DrawWorld(DrawingHandleWorld worldHandle)
private void DrawWorld(in OverlayDrawArgs args)
{
var worldHandle = args.WorldHandle;
worldHandle.UseShader(_shader);
var drawing = new PhysDrawingAdapter(worldHandle);
_hoverBodies.Clear();
var mouseScreenPos = _inputManager.MouseScreenPosition;
var mouseWorldPos = _eyeManager.ScreenToMap(mouseScreenPos).Position;
_hoverStartScreen = mouseScreenPos;
_hoverStartScreen = mouseScreenPos.Position;
var viewport = _eyeManager.GetWorldViewport();
if (viewport.IsEmpty()) return;
var mapId = _eyeManager.CurrentMap;
var sleepThreshold = IoCManager.Resolve<IConfigurationManager>().GetCVar(CVars.TimeToSleep);
var colorEdge = Color.Red.WithAlpha(0.33f);
var drawnJoints = new HashSet<Joint>();
foreach (var physBody in EntitySystem.Get<SharedBroadPhaseSystem>().GetCollidingEntities(mapId, viewport))
{
// all entities have a TransformComponent
var transform = physBody.Entity.Transform;
var transform = physBody.Owner.Transform;
var worldBox = physBody.GetWorldAABB();
var worldBox = physBody.GetWorldAABB(_mapManager);
if (worldBox.IsEmpty()) continue;
var colorEdge = Color.Red.WithAlpha(0.33f);
var sleepThreshold = IoCManager.Resolve<IConfigurationManager>().GetCVar(CVars.TimeToSleep);
foreach (var fixture in physBody.Fixtures)
{
var shape = fixture.Shape;
var sleepPercent = physBody.Awake ? physBody.SleepTime / sleepThreshold : 1.0f;
shape.DebugDraw(drawing, transform.WorldMatrix, in viewport, sleepPercent);
shape.DebugDraw(drawing, transform.WorldMatrix, in viewport, sleepPercent);
}
foreach (var joint in physBody.Joints)
{
if (drawnJoints.Contains(joint)) continue;
drawnJoints.Add(joint);
joint.DebugDraw(drawing, in viewport);
}
if (worldBox.Contains(mouseWorldPos))
@@ -194,9 +203,9 @@ namespace Robust.Client.Debugging
{
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
foreach (var chr in str)
foreach (var rune in str.EnumerateRunes())
{
var advance = font.DrawChar(handle, chr, baseLine, 1, Color.White);
var advance = font.DrawChar(handle, rune, baseLine, 1, Color.White);
baseLine += new Vector2(advance, 0);
}
}
@@ -272,11 +281,11 @@ namespace Robust.Client.Debugging
_eyeManager = eyeManager;
}
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
protected internal override void Draw(in OverlayDrawArgs args)
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) handle;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
foreach (var entity in _entityManager.GetEntities())
{
var transform = entity.Transform;

View File

@@ -87,8 +87,9 @@ namespace Robust.Client.Debugging
_owner = owner;
}
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
protected internal override void Draw(in OverlayDrawArgs args)
{
var handle = args.WorldHandle;
foreach (var ray in _owner.raysWithLifeTime)
{
handle.DrawLine(

View File

@@ -125,11 +125,11 @@ namespace Robust.Client.Debugging
_physics = system;
}
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
protected internal override void Draw(in OverlayDrawArgs args)
{
if (_physics.Flags == PhysicsDebugFlags.None) return;
var worldHandle = (DrawingHandleWorld) handle;
var worldHandle = args.WorldHandle;
if ((_physics.Flags & PhysicsDebugFlags.Shapes) != 0)
{

View File

@@ -1,25 +1,31 @@
using System;
using System.Threading;
using Robust.LoaderApi;
using Robust.Shared;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client
{
internal partial class GameController
{
private IGameLoop _mainLoop = default!;
private IGameLoop? _mainLoop;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
private static bool _hasStarted;
private Thread? _gameThread;
public static void Main(string[] args)
{
Start(args);
}
public static void Start(string[] args, bool contentStart = false, IMainArgs? loaderArgs=null)
public static void Start(string[] args, bool contentStart = false, IMainArgs? loaderArgs=null, GameControllerOptions? options = null)
{
if (_hasStarted)
{
@@ -30,11 +36,11 @@ namespace Robust.Client
if (CommandLineArgs.TryParse(args, out var parsed))
{
ParsedMain(parsed, contentStart, loaderArgs);
ParsedMain(parsed, contentStart, loaderArgs, options);
}
}
private static void ParsedMain(CommandLineArgs args, bool contentStart, IMainArgs? loaderArgs)
private static void ParsedMain(CommandLineArgs args, bool contentStart, IMainArgs? loaderArgs, GameControllerOptions? options)
{
IoCManager.InitThread();
@@ -42,23 +48,18 @@ namespace Robust.Client
InitIoC(mode);
var gc = (GameController) IoCManager.Resolve<IGameController>();
var gc = IoCManager.Resolve<GameController>();
gc.SetCommandLineArgs(args);
gc._loaderArgs = loaderArgs;
if(options != null)
gc.Options = options;
// When the game is ran with the startup executable being content,
// we have to disable the separate load context.
// Otherwise the content assemblies will be loaded twice which causes *many* fun bugs.
gc._disableAssemblyLoadContext = contentStart;
if (!gc.Startup())
{
Logger.Fatal("Failed to start game controller!");
return;
}
gc.MainLoop(mode);
gc.ContentStart = contentStart;
Logger.Debug("Goodbye");
IoCManager.Clear();
gc.Run(mode);
}
public void OverrideMainLoop(IGameLoop gameLoop)
@@ -66,52 +67,68 @@ namespace Robust.Client
_mainLoop = gameLoop;
}
public void MainLoop(DisplayMode mode)
public void Run(DisplayMode mode, Func<ILogHandler>? logHandlerFactory = null)
{
if (_mainLoop == null)
if (!StartupSystemSplash(logHandlerFactory))
{
_mainLoop = new GameLoop(_gameTiming)
{
SleepMode = mode == DisplayMode.Headless ? SleepMode.Delay : SleepMode.None
};
Logger.Fatal("Failed to start game controller!");
return;
}
_mainLoop.Tick += (sender, args) =>
if (_clyde.SeparateWindowThread)
{
if (_mainLoop.Running)
{
Tick(args);
}
};
var stackSize = _configurationManager.GetCVar(CVars.SysGameThreadStackSize);
var priority = (ThreadPriority) _configurationManager.GetCVar(CVars.SysGameThreadPriority);
_mainLoop.Render += (sender, args) =>
{
if (_mainLoop.Running)
_gameThread = new Thread(() => GameThreadMain(mode), stackSize)
{
_gameTiming.CurFrame++;
_clyde.Render();
}
};
_mainLoop.Input += (sender, args) =>
{
if (_mainLoop.Running)
{
Input(args);
}
};
IsBackground = false,
Priority = priority,
Name = "Game thread",
};
_mainLoop.Update += (sender, args) =>
{
if (_mainLoop.Running)
{
Update(args);
}
};
_gameThread.Start();
// set GameLoop.Running to false to return from this function.
_mainLoop.Run();
// Will block until game exit
_clyde.EnterWindowLoop();
if (_gameThread.IsAlive)
{
Logger.Debug("Window loop exited; waiting for game thread to exit");
_gameThread.Join();
}
}
else
{
ContinueStartupAndLoop(mode);
}
Cleanup();
Logger.Debug("Goodbye");
IoCManager.Clear();
}
private void GameThreadMain(DisplayMode mode)
{
IoCManager.InitThread(_dependencyCollection);
ContinueStartupAndLoop(mode);
// Game thread exited, make sure window thread unblocks to finish shutdown.
_clyde.TerminateWindowLoop();
}
private void ContinueStartupAndLoop(DisplayMode mode)
{
if (!StartupContinue(mode))
{
Logger.Fatal("Failed to start game controller!");
return;
}
DebugTools.AssertNotNull(_mainLoop);
_mainLoop!.Run();
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
@@ -48,6 +49,7 @@ namespace Robust.Client
[Dependency] private readonly IClientConsoleHost _console = default!;
[Dependency] private readonly ITimerManager _timerManager = default!;
[Dependency] private readonly IClientEntityManager _entityManager = default!;
[Dependency] private readonly IEntityLookup _lookup = default!;
[Dependency] private readonly IPlacementManager _placementManager = default!;
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
[Dependency] private readonly IOverlayManagerInternal _overlayManager = default!;
@@ -59,17 +61,18 @@ namespace Robust.Client
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
[Dependency] private readonly IModLoaderInternal _modLoader = default!;
[Dependency] private readonly IScriptClient _scriptClient = default!;
[Dependency] private readonly IComponentManager _componentManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustMappedStringSerializer _stringSerializer = default!;
[Dependency] private readonly IAuthManager _authManager = default!;
[Dependency] private readonly IMidiManager _midiManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
private CommandLineArgs? _commandLineArgs;
private bool _disableAssemblyLoadContext;
// Arguments for loader-load. Not used otherwise.
private IMainArgs? _loaderArgs;
public bool ContentStart { get; set; } = false;
public GameControllerOptions Options { get; private set; } = new();
public InitialLaunchState LaunchState { get; private set; } = default!;
public bool LoadConfigAndUserData { get; set; } = true;
@@ -79,14 +82,126 @@ namespace Robust.Client
_commandLineArgs = args;
}
public bool Startup(Func<ILogHandler>? logHandlerFactory = null)
private bool StartupContinue(DisplayMode displayMode)
{
_clyde.InitializePostWindowing();
_clyde.SetWindowTitle(Options.DefaultWindowTitle);
_taskManager.Initialize();
_fontManager.SetFontDpi((uint) _configurationManager.GetCVar(CVars.DisplayFontDpi));
// Disable load context usage on content start.
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
_modLoader.SetUseLoadContext(!ContentStart);
_modLoader.SetEnableSandboxing(false && Options.Sandboxing);
if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), Options.ContentModulePrefix))
{
Logger.Fatal("Errors while loading content assemblies.");
return false;
}
foreach (var loadedModule in _modLoader.LoadedModules)
{
_configurationManager.LoadCVarsFromAssembly(loadedModule);
}
IoCManager.Resolve<ISerializationManager>().Initialize();
// Call Init in game assemblies.
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
_resourceCache.PreloadTextures();
_userInterfaceManager.Initialize();
_eyeManager.Initialize();
_networkManager.Initialize(false);
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
_serializer.Initialize();
_inputManager.Initialize();
_console.Initialize();
_prototypeManager.Initialize();
_prototypeManager.LoadDirectory(Options.PrototypeDirectory);
_prototypeManager.Resync();
_mapManager.Initialize();
_entityManager.Initialize();
_gameStateManager.Initialize();
_placementManager.Initialize();
_viewVariablesManager.Initialize();
_scriptClient.Initialize();
_client.Initialize();
_discord.Initialize();
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);
if (_commandLineArgs?.Username != null)
{
_client.PlayerNameOverride = _commandLineArgs.Username;
}
_authManager.LoadFromEnv();
GC.Collect();
// Setup main loop
if (_mainLoop == null)
{
_mainLoop = new GameLoop(_gameTiming)
{
SleepMode = displayMode == DisplayMode.Headless ? SleepMode.Delay : SleepMode.None
};
}
_mainLoop.Tick += (sender, args) =>
{
if (_mainLoop.Running)
{
Tick(args);
}
};
_mainLoop.Render += (sender, args) =>
{
if (_mainLoop.Running)
{
_gameTiming.CurFrame++;
_clyde.Render();
}
};
_mainLoop.Input += (sender, args) =>
{
if (_mainLoop.Running)
{
Input(args);
}
};
_mainLoop.Update += (sender, args) =>
{
if (_mainLoop.Running)
{
Update(args);
}
};
_clyde.Ready();
if (!Options.DisableCommandLineConnect &&
(_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
&& LaunchState.ConnectEndpoint != null)
{
_client.ConnectToServer(LaunchState.ConnectEndpoint);
}
return true;
}
private bool StartupSystemSplash(Func<ILogHandler>? logHandlerFactory)
{
ReadInitialLaunchState();
SetupLogging(_logManager, logHandlerFactory ?? (() => new ConsoleLogHandler()));
_taskManager.Initialize();
// Figure out user data directory.
var userDataDir = GetUserDataDir();
@@ -99,7 +214,7 @@ namespace Robust.Client
if (LoadConfigAndUserData)
{
var configFile = Path.Combine(userDataDir, "client_config.toml");
var configFile = Path.Combine(userDataDir, Options.ConfigFileName);
if (File.Exists(configFile))
{
// Load config from user data if available.
@@ -123,7 +238,12 @@ namespace Robust.Client
_resourceCache.Initialize(LoadConfigAndUserData ? userDataDir : null);
ProgramShared.DoMounts(_resourceCache, _commandLineArgs?.MountOptions, "Content.Client", _loaderArgs != null);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;
ProgramShared.DoMounts(_resourceCache, mountOptions, Options.ContentBuildDirectory,
_loaderArgs != null && !Options.ResourceMountDisabled, ContentStart);
if (_loaderArgs != null)
{
_stringSerializer.EnableCaching = false;
@@ -131,77 +251,21 @@ namespace Robust.Client
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
}
_clyde.TextEntered += TextEntered;
_clyde.MouseMove += MouseMove;
_clyde.KeyUp += KeyUp;
_clyde.KeyDown += KeyDown;
_clyde.MouseWheel += MouseWheel;
_clyde.CloseWindow += args =>
{
if (args.Window == _clyde.MainWindow)
{
Shutdown("Main window closed");
}
};
// Bring display up as soon as resources are mounted.
if (!_clyde.Initialize())
{
return false;
}
_clyde.SetWindowTitle("Space Station 14");
_fontManager.Initialize();
// Disable load context usage on content start.
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
_modLoader.SetUseLoadContext(!_disableAssemblyLoadContext);
_modLoader.SetEnableSandboxing(true);
if (!_modLoader.TryLoadModulesFrom(new ResourcePath("/Assemblies/"), "Content."))
{
Logger.Fatal("Errors while loading content assemblies.");
return false;
}
foreach (var loadedModule in _modLoader.LoadedModules)
{
_configurationManager.LoadCVarsFromAssembly(loadedModule);
}
IoCManager.Resolve<ISerializationManager>().Initialize();
// Call Init in game assemblies.
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
_resourceCache.PreloadTextures();
_userInterfaceManager.Initialize();
_networkManager.Initialize(false);
IoCManager.Resolve<INetConfigurationManager>().SetupNetworking();
_serializer.Initialize();
_inputManager.Initialize();
_console.Initialize();
_prototypeManager.Initialize();
_prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes/"));
_prototypeManager.Resync();
_mapManager.Initialize();
_entityManager.Initialize();
_gameStateManager.Initialize();
_placementManager.Initialize();
_viewVariablesManager.Initialize();
_scriptClient.Initialize();
_client.Initialize();
_discord.Initialize();
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);
if (_commandLineArgs?.Username != null)
{
_client.PlayerNameOverride = _commandLineArgs.Username;
}
_authManager.LoadFromEnv();
GC.Collect();
_clyde.Ready();
if ((_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
&& LaunchState.ConnectEndpoint != null)
{
_client.ConnectToServer(LaunchState.ConnectEndpoint);
}
return true;
return _clyde.InitializePreWindowing();
}
private Stream? VerifierExtraLoadHandler(string arg)
@@ -247,8 +311,10 @@ namespace Robust.Client
public void Shutdown(string? reason = null)
{
DebugTools.AssertNotNull(_mainLoop);
// Already got shut down I assume,
if (!_mainLoop.Running)
if (!_mainLoop!.Running)
{
return;
}
@@ -277,17 +343,20 @@ namespace Robust.Client
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
_timerManager.UpdateTimers(frameEventArgs);
_taskManager.ProcessPendingTasks();
_userInterfaceManager.Update(frameEventArgs);
if (_client.RunLevel >= ClientRunLevel.Connected)
// GameStateManager is in full control of the simulation update in multiplayer.
if (_client.RunLevel == ClientRunLevel.InGame || _client.RunLevel == ClientRunLevel.Connected)
{
_componentManager.CullRemovedComponents();
_gameStateManager.ApplyGameState();
_entityManager.Update(frameEventArgs.DeltaSeconds);
_playerManager.Update(frameEventArgs.DeltaSeconds);
}
_stateManager.Update(frameEventArgs);
// In singleplayer, however, we're in full control instead.
else if (_client.RunLevel == ClientRunLevel.SinglePlayerGame)
{
_entityManager.TickUpdate(frameEventArgs.DeltaSeconds);
_lookup.Update();
}
_modLoader.BroadcastUpdate(ModUpdateLevel.PostEngine, frameEventArgs);
}
@@ -308,11 +377,6 @@ namespace Robust.Client
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
}
private void Render()
{
}
internal static void SetupLogging(ILogManager logManager, Func<ILogHandler> logHandlerFactory)
{
logManager.RootSawmill.AddHandler(logHandlerFactory());
@@ -389,6 +453,7 @@ namespace Robust.Client
{
_networkManager.Shutdown("Client shutting down");
_midiManager.Shutdown();
IoCManager.Resolve<IEntityLookup>().Shutdown();
_entityManager.Shutdown();
_clyde.Shutdown();
}

View File

@@ -0,0 +1,60 @@
using Robust.Shared;
using Robust.Shared.Utility;
namespace Robust.Client
{
public class GameControllerOptions
{
/// <summary>
/// Whether content sandboxing will be enabled & enforced.
/// </summary>
public bool Sandboxing { get; init; } = true;
// TODO: Expose mounting methods to games using Robust as a library.
/// <summary>
/// Lists of mount options to mount.
/// </summary>
public MountOptions MountOptions { get; init; } = new();
/// <summary>
/// Name the userdata directory will have.
/// </summary>
public string UserDataDirectoryName { get; init; } = "Space Station 14";
/// <summary>
/// Name of the configuration file in the user data directory.
/// </summary>
public string ConfigFileName { get; init; } = "client_config.toml";
// TODO: Define engine branding from json file in resources.
/// <summary>
/// Default window title.
/// </summary>
public string DefaultWindowTitle { get; init; } = "Space Station 14";
/// <summary>
/// Assemblies with this prefix will be loaded.
/// </summary>
public string ContentModulePrefix { get; init; } = "Content.";
/// <summary>
/// Name of the content build directory, for game pack mounting purposes.
/// </summary>
public string ContentBuildDirectory { get; init; } = "Content.Client";
/// <summary>
/// Directory to load all prototypes from.
/// </summary>
public ResourcePath PrototypeDirectory { get; init; } = new(@"/Prototypes/");
/// <summary>
/// Whether to disable mounting the "Resources/" folder on FULL_RELEASE.
/// </summary>
public bool ResourceMountDisabled { get; init; } = false;
/// <summary>
/// Whether to disable command line args server auto-connecting.
/// </summary>
public bool DisableCommandLineConnect { get; init; } = false;
}
}

View File

@@ -9,58 +9,30 @@ namespace Robust.Client.GameObjects
public ClientComponentFactory()
{
// Required for the engine to work
Register<MetaDataComponent>();
RegisterReference<MetaDataComponent, IMetaDataComponent>();
// Required for the engine to work
Register<TransformComponent>();
RegisterReference<TransformComponent, ITransformComponent>();
Register<MapComponent>();
RegisterReference<MapComponent, IMapComponent>();
Register<MapGridComponent>();
RegisterReference<MapGridComponent, IMapGridComponent>();
Register<PhysicsComponent>();
RegisterReference<PhysicsComponent, IPhysBody>();
Register<CollisionWakeComponent>();
Register<ContainerManagerComponent>();
RegisterReference<ContainerManagerComponent, IContainerManager>();
RegisterIgnore("KeyBindingInput");
Register<InputComponent>();
Register<SpriteComponent>();
RegisterReference<SpriteComponent, SharedSpriteComponent>();
RegisterReference<SpriteComponent, ISpriteComponent>();
Register<ClientOccluderComponent>();
RegisterReference<ClientOccluderComponent, OccluderComponent>();
Register<EyeComponent>();
RegisterReference<EyeComponent, SharedEyeComponent>();
Register<AppearanceComponent>();
RegisterReference<AppearanceComponent, SharedAppearanceComponent>();
Register<AppearanceTestComponent>();
Register<SnapGridComponent>();
Register<ClientUserInterfaceComponent>();
RegisterReference<ClientUserInterfaceComponent, SharedUserInterfaceComponent>();
Register<AnimationPlayerComponent>();
Register<TimerComponent>();
RegisterClass<MetaDataComponent>();
RegisterClass<TransformComponent>();
RegisterClass<MapComponent>();
RegisterClass<MapGridComponent>();
RegisterClass<PhysicsComponent>();
RegisterClass<CollisionWakeComponent>();
RegisterClass<ClientUserInterfaceComponent>();
RegisterClass<ContainerManagerComponent>();
RegisterClass<InputComponent>();
RegisterClass<SpriteComponent>();
RegisterClass<ClientOccluderComponent>();
RegisterClass<EyeComponent>();
RegisterClass<AppearanceComponent>();
RegisterClass<AppearanceTestComponent>();
RegisterClass<SnapGridComponent>();
RegisterClass<AnimationPlayerComponent>();
RegisterClass<TimerComponent>();
#if DEBUG
Register<DebugExceptionOnAddComponent>();
Register<DebugExceptionInitializeComponent>();
Register<DebugExceptionStartupComponent>();
RegisterClass<DebugExceptionOnAddComponent>();
RegisterClass<DebugExceptionInitializeComponent>();
RegisterClass<DebugExceptionStartupComponent>();
#endif
}

View File

@@ -1,11 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Prometheus;
using Robust.Client.GameStates;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
@@ -13,303 +19,153 @@ namespace Robust.Client.GameObjects
/// <summary>
/// Manager for entities -- controls things like template loading and instantiation
/// </summary>
public sealed class ClientEntityManager : EntityManager, IClientEntityManager
public sealed class ClientEntityManager : EntityManager, IClientEntityManagerInternal
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IComponentFactory _compFactory = default!;
#if EXCEPTION_TOLERANCE
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
#endif
[Dependency] private readonly IClientNetManager _networkManager = default!;
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private int _nextClientEntityUid = EntityUid.ClientUid + 1;
protected override int NextEntityUid { get; set; } = EntityUid.ClientUid + 1;
public override void Startup()
public override void Initialize()
{
base.Startup();
SetupNetworking();
ReceivedComponentMessage += (_, compMsg) => DispatchComponentMessage(compMsg);
ReceivedSystemMessage += (_, systemMsg) => EventBus.RaiseEvent(EventSource.Network, systemMsg);
if (Started)
{
throw new InvalidOperationException("Startup() called multiple times");
}
EntitySystemManager.Initialize();
Started = true;
base.Initialize();
}
public List<EntityUid> ApplyEntityStates(EntityState[]? curEntStates, IEnumerable<EntityUid>? deletions,
EntityState[]? nextEntStates)
IEntity IClientEntityManagerInternal.CreateEntity(string? prototypeName, EntityUid? uid)
{
var toApply = new Dictionary<IEntity, (EntityState?, EntityState?)>();
var toInitialize = new List<Entity>();
var created = new List<EntityUid>();
deletions ??= new EntityUid[0];
return base.CreateEntity(prototypeName, uid);
}
if (curEntStates != null && curEntStates.Length != 0)
void IClientEntityManagerInternal.InitializeEntity(IEntity entity)
{
EntityManager.InitializeEntity((Entity)entity);
}
void IClientEntityManagerInternal.StartEntity(IEntity entity)
{
base.StartEntity((Entity)entity);
}
#region IEntityNetworkManager impl
public override IEntityNetworkManager EntityNetManager => this;
/// <inheritdoc />
public event EventHandler<NetworkComponentMessage>? ReceivedComponentMessage;
/// <inheritdoc />
public event EventHandler<object>? ReceivedSystemMessage;
private readonly PriorityQueue<(uint seq, MsgEntity msg)> _queue = new(new MessageTickComparer());
private uint _incomingMsgSequence = 0;
/// <inheritdoc />
public void SetupNetworking()
{
_networkManager.RegisterNetMessage<MsgEntity>(MsgEntity.NAME, HandleEntityNetworkMessage);
}
public override void TickUpdate(float frameTime, Histogram? histogram)
{
using (histogram?.WithLabels("EntityNet").NewTimer())
{
foreach (var es in curEntStates)
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameStateManager.CurServerTick)
{
//Known entities
if (Entities.TryGetValue(es.Uid, out var entity))
{
toApply.Add(entity, (es, null));
}
else //Unknown entities
{
var metaState = (MetaDataComponentState?) es.ComponentStates
?.FirstOrDefault(c => c.NetID == NetIDs.META_DATA);
if (metaState == null)
{
throw new InvalidOperationException($"Server sent new entity state for {es.Uid} without metadata component!");
}
var newEntity = CreateEntity(metaState.PrototypeId, es.Uid);
toApply.Add(newEntity, (es, null));
toInitialize.Add(newEntity);
created.Add(newEntity.Uid);
}
var (_, msg) = _queue.Take();
// Logger.DebugS("net.ent", "Dispatching: {0}: {1}", seq, msg);
DispatchMsgEntity(msg);
}
}
if (nextEntStates != null && nextEntStates.Length != 0)
{
foreach (var es in nextEntStates)
{
if (Entities.TryGetValue(es.Uid, out var entity))
{
if (toApply.TryGetValue(entity, out var state))
{
toApply[entity] = (state.Item1, es);
}
else
{
toApply[entity] = (null, es);
}
}
}
}
// Make sure this is done after all entities have been instantiated.
foreach (var kvStates in toApply)
{
var ent = kvStates.Key;
var entity = (Entity) ent;
HandleEntityState(entity.EntityManager.ComponentManager, entity, kvStates.Value.Item1,
kvStates.Value.Item2);
}
foreach (var kvp in toApply)
{
UpdateEntityTree(kvp.Key);
}
foreach (var id in deletions)
{
DeleteEntity(id);
}
#if EXCEPTION_TOLERANCE
HashSet<Entity> brokenEnts = new HashSet<Entity>();
#endif
foreach (var entity in toInitialize)
{
#if EXCEPTION_TOLERANCE
try
{
#endif
InitializeEntity(entity);
#if EXCEPTION_TOLERANCE
}
catch (Exception e)
{
Logger.ErrorS("state", $"Server entity threw in Init: uid={entity.Uid}, proto={entity.Prototype}\n{e}");
brokenEnts.Add(entity);
}
#endif
}
foreach (var entity in toInitialize)
{
#if EXCEPTION_TOLERANCE
if(brokenEnts.Contains(entity))
continue;
try
{
#endif
StartEntity(entity);
#if EXCEPTION_TOLERANCE
}
catch (Exception e)
{
Logger.ErrorS("state", $"Server entity threw in Start: uid={entity.Uid}, proto={entity.Prototype}\n{e}");
brokenEnts.Add(entity);
}
#endif
}
foreach (var entity in toInitialize)
{
#if EXCEPTION_TOLERANCE
if(brokenEnts.Contains(entity))
continue;
#endif
UpdateEntityTree(entity);
}
#if EXCEPTION_TOLERANCE
foreach (var entity in brokenEnts)
{
entity.Delete();
}
#endif
return created;
base.TickUpdate(frameTime, histogram);
}
/// <inheritdoc />
public override IEntity CreateEntityUninitialized(string? prototypeName)
public void SendSystemNetworkMessage(EntityEventArgs message)
{
return CreateEntity(prototypeName);
SendSystemNetworkMessage(message, default(uint));
}
public void SendSystemNetworkMessage(EntityEventArgs message, uint sequence)
{
var msg = _networkManager.CreateNetMessage<MsgEntity>();
msg.Type = EntityMessageType.SystemMessage;
msg.SystemMessage = message;
msg.SourceTick = _gameTiming.CurTick;
msg.Sequence = sequence;
_networkManager.ClientSendMessage(msg);
}
/// <inheritdoc />
public override IEntity CreateEntityUninitialized(string? prototypeName, EntityCoordinates coordinates)
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel channel)
{
var newEntity = CreateEntity(prototypeName, GenerateEntityUid());
if (TryGetEntity(coordinates.EntityId, out var entity))
{
newEntity.Transform.AttachParent(entity);
newEntity.Transform.Coordinates = coordinates;
}
return newEntity;
throw new NotSupportedException();
}
/// <inheritdoc />
public override IEntity CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates)
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public void SendComponentNetworkMessage(INetChannel? channel, IEntity entity, IComponent component, ComponentMessage message)
{
var newEntity = CreateEntity(prototypeName, GenerateEntityUid());
newEntity.Transform.AttachParent(_mapManager.GetMapEntity(coordinates.MapId));
newEntity.Transform.WorldPosition = coordinates.Position;
return newEntity;
if (!component.NetID.HasValue)
throw new ArgumentException($"Component {component.Name} does not have a NetID.", nameof(component));
var msg = _networkManager.CreateNetMessage<MsgEntity>();
msg.Type = EntityMessageType.ComponentMessage;
msg.EntityUid = entity.Uid;
msg.NetId = component.NetID.Value;
msg.ComponentMessage = message;
msg.SourceTick = _gameTiming.CurTick;
_networkManager.ClientSendMessage(msg);
}
/// <inheritdoc />
public override IEntity SpawnEntity(string? protoName, EntityCoordinates coordinates)
private void HandleEntityNetworkMessage(MsgEntity message)
{
var newEnt = CreateEntityUninitialized(protoName, coordinates);
InitializeAndStartEntity((Entity) newEnt);
UpdateEntityTree(newEnt);
return newEnt;
}
/// <inheritdoc />
public override IEntity SpawnEntity(string? protoName, MapCoordinates coordinates)
{
var entity = CreateEntityUninitialized(protoName, coordinates);
InitializeAndStartEntity((Entity) entity);
UpdateEntityTree(entity);
return entity;
}
/// <inheritdoc />
public override IEntity SpawnEntityNoMapInit(string? protoName, EntityCoordinates coordinates)
{
return SpawnEntity(protoName, coordinates);
}
protected override EntityUid GenerateEntityUid()
{
return new(_nextClientEntityUid++);
}
private void HandleEntityState(IComponentManager compMan, IEntity entity, EntityState? curState,
EntityState? nextState)
{
var compStateWork = new Dictionary<uint, (ComponentState? curState, ComponentState? nextState)>();
var entityUid = entity.Uid;
if (curState?.ComponentChanges != null)
if (message.SourceTick <= _gameStateManager.CurServerTick)
{
foreach (var compChange in curState.ComponentChanges)
{
if (compChange.Deleted)
{
if (compMan.TryGetComponent(entityUid, compChange.NetID, out var comp))
{
compMan.RemoveComponent(entityUid, comp);
}
}
else
{
if (compMan.HasComponent(entityUid, compChange.NetID))
continue;
var newComp = (Component) _compFactory.GetComponent(compChange.ComponentName!);
newComp.Owner = entity;
compMan.AddComponent(entity, newComp, true);
}
}
DispatchMsgEntity(message);
return;
}
if (curState?.ComponentStates != null)
{
foreach (var compState in curState.ComponentStates)
{
compStateWork[compState.NetID] = (compState, null);
}
}
// MsgEntity is sent with ReliableOrdered so Lidgren guarantees ordering of incoming messages.
// We still need to store a sequence input number to ensure ordering remains consistent in
// the priority queue.
_queue.Add((++_incomingMsgSequence, message));
}
if (nextState?.ComponentStates != null)
private void DispatchMsgEntity(MsgEntity message)
{
switch (message.Type)
{
foreach (var compState in nextState.ComponentStates)
{
if (compStateWork.TryGetValue(compState.NetID, out var state))
{
compStateWork[compState.NetID] = (state.curState, compState);
}
else
{
compStateWork[compState.NetID] = (null, compState);
}
}
}
case EntityMessageType.ComponentMessage:
ReceivedComponentMessage?.Invoke(this, new NetworkComponentMessage(message));
return;
foreach (var (netId, (cur, next)) in compStateWork)
{
if (compMan.TryGetComponent(entityUid, netId, out var component))
{
try
{
component.HandleComponentState(cur, next);
}
catch (Exception e)
{
var wrapper = new ComponentStateApplyException(
$"Failed to apply comp state: entity={component.Owner}, comp={component.Name}", e);
#if EXCEPTION_TOLERANCE
_runtimeLog.LogException(wrapper, "Component state apply");
#else
throw wrapper;
#endif
}
}
else
{
// The component can be null here due to interp.
// Because the NEXT state will have a new component, but this one doesn't yet.
// That's fine though.
if (cur == null)
{
continue;
}
var eUid = entityUid;
var eRegisteredNetUidName = _compFactory.GetRegistration(netId).Name;
DebugTools.Assert(
$"Component does not exist for state: entUid={eUid}, expectedNetId={netId}, expectedName={eRegisteredNetUidName}");
}
case EntityMessageType.SystemMessage:
ReceivedSystemMessage?.Invoke(this, message.SystemMessage);
return;
}
}
private sealed class MessageTickComparer : IComparer<(uint seq, MsgEntity msg)>
{
public int Compare((uint seq, MsgEntity msg) x, (uint seq, MsgEntity msg) y)
{
var cmp = y.msg.SourceTick.CompareTo(x.msg.SourceTick);
if (cmp != 0)
{
return cmp;
}
return y.seq.CompareTo(x.seq);
}
}
#endregion
}
}

View File

@@ -1,128 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Client.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
/// <summary>
/// The client implementation of the Entity Network Manager.
/// </summary>
public class ClientEntityNetworkManager : IEntityNetworkManager
{
[Dependency] private readonly IClientNetManager _networkManager = default!;
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
/// <inheritdoc />
public event EventHandler<NetworkComponentMessage>? ReceivedComponentMessage;
/// <inheritdoc />
public event EventHandler<object>? ReceivedSystemMessage;
private readonly PriorityQueue<(uint seq, MsgEntity msg)> _queue = new(new MessageTickComparer());
private uint _incomingMsgSequence = 0;
/// <inheritdoc />
public void SetupNetworking()
{
_networkManager.RegisterNetMessage<MsgEntity>(MsgEntity.NAME, HandleEntityNetworkMessage);
}
public void Update()
{
while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameStateManager.CurServerTick)
{
var (_, msg) = _queue.Take();
// Logger.DebugS("net.ent", "Dispatching: {0}: {1}", seq, msg);
DispatchMsgEntity(msg);
}
}
/// <inheritdoc />
public void SendSystemNetworkMessage(EntityEventArgs message)
{
SendSystemNetworkMessage(message, default(uint));
}
public void SendSystemNetworkMessage(EntityEventArgs message, uint sequence)
{
var msg = _networkManager.CreateNetMessage<MsgEntity>();
msg.Type = EntityMessageType.SystemMessage;
msg.SystemMessage = message;
msg.SourceTick = _gameTiming.CurTick;
msg.Sequence = sequence;
_networkManager.ClientSendMessage(msg);
}
/// <inheritdoc />
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel channel)
{
throw new NotSupportedException();
}
/// <inheritdoc />
public void SendComponentNetworkMessage(INetChannel? channel, IEntity entity, IComponent component, ComponentMessage message)
{
if (!component.NetID.HasValue)
throw new ArgumentException($"Component {component.Name} does not have a NetID.", nameof(component));
var msg = _networkManager.CreateNetMessage<MsgEntity>();
msg.Type = EntityMessageType.ComponentMessage;
msg.EntityUid = entity.Uid;
msg.NetId = component.NetID.Value;
msg.ComponentMessage = message;
msg.SourceTick = _gameTiming.CurTick;
_networkManager.ClientSendMessage(msg);
}
private void HandleEntityNetworkMessage(MsgEntity message)
{
if (message.SourceTick <= _gameStateManager.CurServerTick)
{
DispatchMsgEntity(message);
return;
}
// MsgEntity is sent with ReliableOrdered so Lidgren guarantees ordering of incoming messages.
// We still need to store a sequence input number to ensure ordering remains consistent in
// the priority queue.
_queue.Add((++_incomingMsgSequence, message));
}
private void DispatchMsgEntity(MsgEntity message)
{
switch (message.Type)
{
case EntityMessageType.ComponentMessage:
ReceivedComponentMessage?.Invoke(this, new NetworkComponentMessage(message));
return;
case EntityMessageType.SystemMessage:
ReceivedSystemMessage?.Invoke(this, message.SystemMessage);
return;
}
}
private sealed class MessageTickComparer : IComparer<(uint seq, MsgEntity msg)>
{
public int Compare((uint seq, MsgEntity msg) x, (uint seq, MsgEntity msg) y)
{
var cmp = y.msg.SourceTick.CompareTo(x.msg.SourceTick);
if (cmp != 0)
{
return cmp;
}
return y.seq.CompareTo(x.seq);
}
}
}
}

View File

@@ -10,6 +10,7 @@ using YamlDotNet.RepresentationModel;
namespace Robust.Client.GameObjects
{
[ComponentReference(typeof(SharedAppearanceComponent))]
public sealed class AppearanceComponent : SharedAppearanceComponent
{
[ViewVariables]
@@ -94,8 +95,7 @@ namespace Robust.Client.GameObjects
return;
}
EntitySystem.Get<AppearanceSystem>()
.EnqueueAppearanceUpdate(this);
EntitySystem.Get<AppearanceSystem>().EnqueueUpdate(this);
_appearanceDirty = true;
}
@@ -115,21 +115,6 @@ namespace Robust.Client.GameObjects
MarkDirty();
}
internal class SpriteLayerToggle : AppearanceVisualizer
{
public const string NAME = "sprite_layer_toggle";
public readonly object Key;
public readonly int SpriteLayer;
public SpriteLayerToggle(object key, int spriteLayer)
{
Key = key;
SpriteLayer = spriteLayer;
}
}
}
/// <summary>

View File

@@ -1,4 +1,4 @@
using Robust.Client.Graphics;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
@@ -10,6 +10,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
{
[ComponentReference(typeof(SharedEyeComponent))]
public class EyeComponent : SharedEyeComponent
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
@@ -25,7 +26,7 @@ namespace Robust.Client.GameObjects
[DataField("drawFov")]
private bool _setDrawFovOnInitialize = true;
[DataField("zoom")]
private Vector2 _setZoomOnInitialize = Vector2.One/2f;
private Vector2 _setZoomOnInitialize = Vector2.One;
private Vector2 _offset = Vector2.Zero;
public IEye? Eye => _eye;
@@ -152,6 +153,7 @@ namespace Robust.Client.GameObjects
Zoom = state.Zoom;
Offset = state.Offset;
Rotation = state.Rotation;
VisibilityMask = state.VisibilityMask;
}
public override void OnRemove()

View File

@@ -1,14 +1,17 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
{
[ComponentReference(typeof(OccluderComponent))]
internal sealed class ClientOccluderComponent : OccluderComponent
{
internal SnapGridComponent? SnapGrid { get; private set; }
[Dependency] private readonly IMapManager _mapManager = default!;
[ViewVariables] private (GridId, Vector2i) _lastPosition;
[ViewVariables] internal OccluderDir Occluding { get; private set; }
@@ -29,39 +32,36 @@ namespace Robust.Client.GameObjects
{
base.Startup();
if (Owner.TryGetComponent(out SnapGridComponent? snap))
if (Owner.Transform.Anchored)
{
SnapGrid = snap;
SnapGrid.OnPositionChanged += SnapGridOnPositionChanged;
SnapGridOnPositionChanged();
}
}
private void SnapGridOnPositionChanged()
public void SnapGridOnPositionChanged()
{
SendDirty();
_lastPosition = (Owner.Transform.GridID, SnapGrid!.Position);
if(!Owner.Transform.Anchored)
return;
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
_lastPosition = (Owner.Transform.GridID, grid.TileIndicesFor(Owner.Transform.Coordinates));
}
protected override void Shutdown()
{
base.Shutdown();
if (SnapGrid != null)
{
SnapGrid.OnPositionChanged -= SnapGridOnPositionChanged;
}
SendDirty();
}
private void SendDirty()
{
if (SnapGrid != null)
if (Owner.Transform.Anchored)
{
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local,
new OccluderDirtyEvent(Owner, _lastPosition, SnapGrid.Offset));
new OccluderDirtyEvent(Owner, _lastPosition));
}
}
@@ -69,16 +69,18 @@ namespace Robust.Client.GameObjects
{
Occluding = OccluderDir.None;
if (Deleted || SnapGrid == null)
if (Deleted || !Owner.Transform.Anchored)
{
return;
}
void CheckDir(Direction dir, OccluderDir oclDir)
{
foreach (var neighbor in SnapGrid.GetInDir(dir))
var grid = _mapManager.GetGrid(Owner.Transform.GridID);
var position = Owner.Transform.Coordinates;
foreach (var neighbor in grid.GetInDir(position, dir))
{
if (neighbor.TryGetComponent(out ClientOccluderComponent? comp) && comp.Enabled)
if (Owner.EntityManager.ComponentManager.TryGetComponent(neighbor, out ClientOccluderComponent? comp) && comp.Enabled)
{
Occluding |= oclDir;
break;

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Animations;
@@ -167,7 +168,7 @@ namespace Robust.Client.GameObjects
set
{
_radius = MathF.Max(value, 0.01f); // setting radius to 0 causes exceptions, so just use a value close enough to zero that it's unnoticeable.
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PointLightRadiusChangedMessage(this));
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PointLightRadiusChangedEvent(this));
}
}
@@ -179,6 +180,18 @@ namespace Robust.Client.GameObjects
Mask = null;
}
/// <summary>
/// What MapId we are intersecting for RenderingTreeSystem.
/// </summary>
[ViewVariables]
internal MapId IntersectingMapId { get; set; } = MapId.Nullspace;
/// <summary>
/// What grids we're on for RenderingTreeSystem.
/// </summary>
[ViewVariables]
internal List<GridId> IntersectingGrids = new();
void ISerializationHooks.AfterDeserialization()
{
if (_maskPath != null)
@@ -230,7 +243,7 @@ namespace Robust.Client.GameObjects
if (map != MapId.Nullspace)
{
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local,
new RenderTreeRemoveLightMessage(this, map));
new RenderTreeRemoveLightEvent(this, map));
}
}
@@ -248,11 +261,11 @@ namespace Robust.Client.GameObjects
}
}
public struct PointLightRadiusChangedMessage
public class PointLightRadiusChangedEvent : EntityEventArgs
{
public PointLightComponent PointLightComponent { get; }
public PointLightRadiusChangedMessage(PointLightComponent pointLightComponent)
public PointLightRadiusChangedEvent(PointLightComponent pointLightComponent)
{
PointLightComponent = pointLightComponent;
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -19,6 +19,7 @@ using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -26,6 +27,8 @@ using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
namespace Robust.Client.GameObjects
{
[ComponentReference(typeof(SharedSpriteComponent))]
[ComponentReference(typeof(ISpriteComponent))]
public sealed class SpriteComponent : SharedSpriteComponent, ISpriteComponent,
IComponentDebug, ISerializationHooks
{
@@ -42,7 +45,7 @@ namespace Robust.Client.GameObjects
set => _visible = value;
}
[DataFieldWithConstant("drawdepth", typeof(DrawDepthTag))]
[DataField("drawdepth", customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
private int drawDepth = DrawDepthTag.Default;
/// <summary>
@@ -122,6 +125,18 @@ namespace Robust.Client.GameObjects
[DataField("directional")]
private bool _directional = true;
/// <summary>
/// What MapId we are intersecting for RenderingTreeSystem.
/// </summary>
[ViewVariables]
internal MapId IntersectingMapId { get; set; } = MapId.Nullspace;
/// <summary>
/// What grids we're on for RenderingTreeSystem.
/// </summary>
[ViewVariables]
internal List<GridId> IntersectingGrids { get; } = new();
[DataField("layerDatums")]
private List<PrototypeLayerData> LayerDatums
{
@@ -847,8 +862,8 @@ namespace Robust.Client.GameObjects
}
else
{
Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI. Trace:\n{1}", stateId,
Environment.StackTrace);
Logger.ErrorS(LogCategory, "State '{0}' does not exist in RSI {1}. Trace:\n{2}", stateId,
actualRsi.Path, Environment.StackTrace);
theLayer.Texture = null;
}
}
@@ -1352,7 +1367,7 @@ namespace Robust.Client.GameObjects
if (map != MapId.Nullspace)
{
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local,
new RenderTreeRemoveSpriteMessage(this, map));
new RenderTreeRemoveSpriteEvent(this, map));
}
}
@@ -1575,9 +1590,10 @@ namespace Robust.Client.GameObjects
{
var builder = new StringBuilder();
builder.AppendFormat(
"vis/depth/scl/rot/ofs/col/diral/dir: {0}/{1}/{2}/{3}/{4}/{5}/{6}/{7}\n",
"vis/depth/scl/rot/ofs/col/norot/override/dir: {0}/{1}/{2}/{3}/{4}/{5}/{6}/{8}/{7}\n",
Visible, DrawDepth, Scale, Rotation, Offset,
Color, Directional, GetDir(RSI.State.DirectionType.Dir8, Owner.Transform.WorldRotation)
Color, NoRotation, GetDir(RSI.State.DirectionType.Dir8, Owner.Transform.WorldRotation),
DirectionOverride
);
foreach (var layer in Layers)
@@ -2084,6 +2100,7 @@ namespace Robust.Client.GameObjects
public IEntityManager EntityManager { get; } = null!;
public string Name { get; set; } = string.Empty;
public EntityUid Uid { get; } = EntityUid.Invalid;
EntityLifeStage IEntity.LifeStage { get => _lifeStage; set => _lifeStage = value; }
public bool Initialized { get; } = false;
public bool Initializing { get; } = false;
public bool Deleted { get; } = true;
@@ -2100,6 +2117,7 @@ namespace Robust.Client.GameObjects
public IMetaDataComponent MetaData { get; } = null!;
private Dictionary<Type, IComponent> _components = new();
private EntityLifeStage _lifeStage;
public T AddComponent<T>() where T : Component, new()
{
@@ -2147,11 +2165,6 @@ namespace Robust.Client.GameObjects
return null!;
}
public IComponent GetComponent(uint netID)
{
return null!;
}
public bool TryGetComponent<T>([NotNullWhen(true)] out T? component) where T : class
{
component = null;
@@ -2178,18 +2191,7 @@ namespace Robust.Client.GameObjects
return null;
}
public bool TryGetComponent(uint netId, [NotNullWhen(true)] out IComponent? component)
{
component = null;
return false;
}
public IComponent? GetComponentOrNull(uint netId)
{
return null;
}
public void Shutdown()
public void QueueDelete()
{
}
@@ -2207,10 +2209,12 @@ namespace Robust.Client.GameObjects
return Enumerable.Empty<T>();
}
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public void SendMessage(IComponent? owner, ComponentMessage message)
{
}
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public void SendNetworkMessage(IComponent owner, ComponentMessage message, INetChannel? channel = null)
{
}

View File

@@ -2,14 +2,14 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
{
[ComponentReference(typeof(SharedUserInterfaceComponent))]
public class ClientUserInterfaceComponent : SharedUserInterfaceComponent, ISerializationHooks
{
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
@@ -23,6 +23,9 @@ namespace Robust.Client.GameObjects
[DataField("interfaces", readOnly: true)]
private List<PrototypeData> _interfaceData = new();
[ViewVariables]
public IEnumerable<BoundUserInterface> Interfaces => _openInterfaces.Values;
void ISerializationHooks.AfterDeserialization()
{
_interfaces.Clear();
@@ -33,48 +36,40 @@ namespace Robust.Client.GameObjects
}
}
public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel,
ICommonSession? session = null)
internal void MessageReceived(BoundUIWrapMessage msg)
{
base.HandleNetworkMessage(message, netChannel, session);
switch (message)
switch (msg.Message)
{
case BoundInterfaceMessageWrapMessage wrapped:
// Double nested switches who needs readability anyways.
switch (wrapped.Message)
case OpenBoundInterfaceMessage _:
if (_openInterfaces.ContainsKey(msg.UiKey))
{
case OpenBoundInterfaceMessage _:
if (_openInterfaces.ContainsKey(wrapped.UiKey))
{
return;
}
return;
}
OpenInterface(wrapped);
break;
OpenInterface(msg);
break;
case CloseBoundInterfaceMessage _:
Close(wrapped.UiKey, true);
break;
case CloseBoundInterfaceMessage _:
Close(msg.UiKey, true);
break;
default:
if (_openInterfaces.TryGetValue(wrapped.UiKey, out var bi))
{
bi.InternalReceiveMessage(wrapped.Message);
}
break;
default:
if (_openInterfaces.TryGetValue(msg.UiKey, out var bi))
{
bi.InternalReceiveMessage(msg.Message);
}
break;
}
}
private void OpenInterface(BoundInterfaceMessageWrapMessage wrapped)
private void OpenInterface(BoundUIWrapMessage wrapped)
{
var data = _interfaces[wrapped.UiKey];
// TODO: This type should be cached, but I'm too lazy.
var type = _reflectionManager.LooseGetType(data.ClientType);
var boundInterface = (BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new[]{this, wrapped.UiKey});
var boundInterface =
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new[] {this, wrapped.UiKey});
boundInterface.Open();
_openInterfaces[wrapped.UiKey] = boundInterface;
}
@@ -86,7 +81,7 @@ namespace Robust.Client.GameObjects
return;
}
if(!remoteCall)
if (!remoteCall)
SendMessage(new CloseBoundInterfaceMessage(), uiKey);
_openInterfaces.Remove(uiKey);
boundUserInterface.Dispose();
@@ -94,7 +89,8 @@ namespace Robust.Client.GameObjects
internal void SendMessage(BoundUserInterfaceMessage message, object uiKey)
{
SendNetworkMessage(new BoundInterfaceMessageWrapMessage(message, uiKey));
EntitySystem.Get<UserInterfaceSystem>()
.Send(new BoundUIWrapMessage(Owner.Uid, message, uiKey));
}
}

View File

@@ -15,7 +15,7 @@ namespace Robust.Client.GameObjects
DebugTools.AssertNotNull(localPlayer);
var sequence = IoCManager.Resolve<IClientGameStateManager>().SystemMessageDispatched(msg);
entityManager.EntityNetManager.SendSystemNetworkMessage(msg, sequence);
entityManager.EntityNetManager?.SendSystemNetworkMessage(msg, sequence);
var eventArgs = new EntitySessionEventArgs(localPlayer!.Session);

View File

@@ -1,51 +1,33 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects
{
[UsedImplicitly]
internal sealed class AppearanceSystem : EntitySystem
{
private readonly Queue<AppearanceComponent> _updatesQueued = new();
private readonly Queue<AppearanceComponent> _queuedUpdates = new();
public void EnqueueUpdate(AppearanceComponent component)
{
_queuedUpdates.Enqueue(component);
}
public override void FrameUpdate(float frameTime)
{
while (_updatesQueued.TryDequeue(out var appearance))
while (_queuedUpdates.TryDequeue(out var appearance))
{
UpdateComponent(appearance);
if (appearance.Deleted)
return;
foreach (var visualizer in appearance.Visualizers)
{
visualizer.OnChangeData(appearance);
}
appearance.UnmarkDirty();
}
}
private static void UpdateComponent(AppearanceComponent component)
{
if (component.Deleted)
return;
foreach (var visualizer in component.Visualizers)
{
switch (visualizer)
{
case AppearanceComponent.SpriteLayerToggle spriteLayerToggle:
UpdateSpriteLayerToggle(component, spriteLayerToggle);
break;
default:
visualizer.OnChangeData(component);
break;
}
}
}
private static void UpdateSpriteLayerToggle(AppearanceComponent component, AppearanceComponent.SpriteLayerToggle toggle)
{
component.TryGetData(toggle.Key, out bool visible);
var sprite = component.Owner.GetComponent<SpriteComponent>();
sprite.LayerSetVisible(toggle.SpriteLayer, visible);
}
public void EnqueueAppearanceUpdate(AppearanceComponent component)
{
_updatesQueued.Enqueue(component);
}
}
}

View File

@@ -8,7 +8,6 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Player;
@@ -165,6 +164,12 @@ namespace Robust.Client.GameObjects
Logger.Warning("Interrupting positional audio, can't set position.");
stream.Source.StopPlaying();
}
if (stream.TrackingEntity != null)
{
stream.Source.SetVelocity(stream.TrackingEntity.GlobalLinearVelocity());
}
}
}
}
@@ -188,7 +193,7 @@ namespace Robust.Client.GameObjects
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="audioParams"></param>
public IPlayingAudioStream? Play(string filename, AudioParams? audioParams = null)
private IPlayingAudioStream? Play(string filename, AudioParams? audioParams = null)
{
if (_resourceCache.TryGetResource<AudioResource>(new ResourcePath(filename), out var audio))
{
@@ -204,7 +209,7 @@ namespace Robust.Client.GameObjects
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="audioParams"></param>
public IPlayingAudioStream Play(AudioStream stream, AudioParams? audioParams = null)
private IPlayingAudioStream Play(AudioStream stream, AudioParams? audioParams = null)
{
var source = _clyde.CreateAudioSource(stream);
ApplyAudioParams(audioParams, source);
@@ -226,7 +231,7 @@ namespace Robust.Client.GameObjects
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="audioParams"></param>
public IPlayingAudioStream? Play(string filename, IEntity entity, AudioParams? audioParams = null)
private IPlayingAudioStream? Play(string filename, IEntity entity, AudioParams? audioParams = null)
{
if (_resourceCache.TryGetResource<AudioResource>(new ResourcePath(filename), out var audio))
{
@@ -243,7 +248,7 @@ namespace Robust.Client.GameObjects
/// <param name="stream">The audio stream to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="audioParams"></param>
public IPlayingAudioStream? Play(AudioStream stream, IEntity entity, AudioParams? audioParams = null)
private IPlayingAudioStream? Play(AudioStream stream, IEntity entity, AudioParams? audioParams = null)
{
var source = _clyde.CreateAudioSource(stream);
if (!source.SetPosition(entity.Transform.WorldPosition))
@@ -272,7 +277,7 @@ namespace Robust.Client.GameObjects
/// <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>
public IPlayingAudioStream? Play(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
private IPlayingAudioStream? Play(string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
if (_resourceCache.TryGetResource<AudioResource>(new ResourcePath(filename), out var audio))
{
@@ -289,7 +294,7 @@ namespace Robust.Client.GameObjects
/// <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 IPlayingAudioStream? Play(AudioStream stream, EntityCoordinates coordinates,
private IPlayingAudioStream? Play(AudioStream stream, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
var source = _clyde.CreateAudioSource(stream);

View File

@@ -6,6 +6,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
@@ -20,10 +21,11 @@ namespace Robust.Client.GameObjects
{
[Dependency] private readonly IMapManager _mapManager = default!;
private readonly Queue<IEntity> _dirtyEntities = new();
private readonly Queue<EntityUid> _dirtyEntities = new();
private uint _updateGeneration;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
@@ -32,6 +34,8 @@ namespace Robust.Client.GameObjects
UpdatesAfter.Add(typeof(PhysicsSystem));
SubscribeLocalEvent<OccluderDirtyEvent>(HandleDirtyEvent);
SubscribeLocalEvent<ClientOccluderComponent, SnapGridPositionChangedEvent>(HandleSnapGridMove);
}
public override void FrameUpdate(float frameTime)
@@ -47,8 +51,8 @@ namespace Robust.Client.GameObjects
while (_dirtyEntities.TryDequeue(out var entity))
{
if (!entity.Deleted
&& entity.TryGetComponent(out ClientOccluderComponent? occluder)
if (EntityManager.EntityExists(entity)
&& ComponentManager.TryGetComponent(entity, out ClientOccluderComponent? occluder)
&& occluder.UpdateGeneration != _updateGeneration)
{
occluder.Update();
@@ -58,6 +62,11 @@ namespace Robust.Client.GameObjects
}
}
private static void HandleSnapGridMove(EntityUid uid, ClientOccluderComponent component, SnapGridPositionChangedEvent args)
{
component.SnapGridOnPositionChanged();
}
private void HandleDirtyEvent(OccluderDirtyEvent ev)
{
var sender = ev.Sender;
@@ -65,13 +74,14 @@ namespace Robust.Client.GameObjects
sender.TryGetComponent(out ClientOccluderComponent? iconSmooth)
&& iconSmooth.Running)
{
var snapGrid = sender.GetComponent<SnapGridComponent>();
var grid1 = _mapManager.GetGrid(sender.Transform.GridID);
var coords = sender.Transform.Coordinates;
_dirtyEntities.Enqueue(sender);
AddValidEntities(snapGrid.GetInDir(Direction.North));
AddValidEntities(snapGrid.GetInDir(Direction.South));
AddValidEntities(snapGrid.GetInDir(Direction.East));
AddValidEntities(snapGrid.GetInDir(Direction.West));
_dirtyEntities.Enqueue(sender.Uid);
AddValidEntities(grid1.GetInDir(coords, Direction.North));
AddValidEntities(grid1.GetInDir(coords, Direction.South));
AddValidEntities(grid1.GetInDir(coords, Direction.East));
AddValidEntities(grid1.GetInDir(coords, Direction.West));
}
// Entity is no longer valid, update around the last position it was at.
@@ -79,28 +89,23 @@ namespace Robust.Client.GameObjects
{
var pos = ev.LastPosition.Value.pos;
AddValidEntities(grid.GetSnapGridCell(pos + new Vector2i(1, 0), ev.Offset));
AddValidEntities(grid.GetSnapGridCell(pos + new Vector2i(-1, 0), ev.Offset));
AddValidEntities(grid.GetSnapGridCell(pos + new Vector2i(0, 1), ev.Offset));
AddValidEntities(grid.GetSnapGridCell(pos + new Vector2i(0, -1), ev.Offset));
AddValidEntities(grid.GetAnchoredEntities(pos + new Vector2i(1, 0)));
AddValidEntities(grid.GetAnchoredEntities(pos + new Vector2i(-1, 0)));
AddValidEntities(grid.GetAnchoredEntities(pos + new Vector2i(0, 1)));
AddValidEntities(grid.GetAnchoredEntities(pos + new Vector2i(0, -1)));
}
}
private void AddValidEntities(IEnumerable<IEntity> candidates)
private void AddValidEntities(IEnumerable<EntityUid> candidates)
{
foreach (var entity in candidates)
{
if (entity.HasComponent<ClientOccluderComponent>())
if (ComponentManager.HasComponent<ClientOccluderComponent>(entity))
{
_dirtyEntities.Enqueue(entity);
}
}
}
private void AddValidEntities(IEnumerable<IComponent> candidates)
{
AddValidEntities(candidates.Select(c => c.Owner));
}
}
/// <summary>
@@ -108,15 +113,13 @@ namespace Robust.Client.GameObjects
/// </summary>
internal sealed class OccluderDirtyEvent : EntityEventArgs
{
public OccluderDirtyEvent(IEntity sender, (GridId grid, Vector2i pos)? lastPosition, SnapGridOffset offset)
public OccluderDirtyEvent(IEntity sender, (GridId grid, Vector2i pos)? lastPosition)
{
LastPosition = lastPosition;
Offset = offset;
Sender = sender;
}
public (GridId grid, Vector2i pos)? LastPosition { get; }
public SnapGridOffset Offset { get; }
public IEntity Sender { get; }
}
}

View File

@@ -67,9 +67,10 @@ namespace Robust.Client.GameObjects
//Create effect from creation message
var effect = new Effect(message, resourceCache, _mapManager, _entityManager);
effect.Deathtime = gameTiming.CurTime + message.LifeTime;
if (effect.AttachedEntityUid != null)
if (effect.AttachedEntityUid != null
&& _entityManager.TryGetEntity(effect.AttachedEntityUid.Value, out var attachedEntity))
{
effect.AttachedEntity = _entityManager.GetEntity(effect.AttachedEntityUid.Value);
effect.AttachedEntity = attachedEntity;
}
_Effects.Add(effect);
@@ -346,11 +347,11 @@ namespace Robust.Client.GameObjects
_entityManager = entityManager;
}
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
protected internal override void Draw(in OverlayDrawArgs args)
{
var map = _owner.eyeManager.CurrentMap;
var worldHandle = (DrawingHandleWorld) handle;
var worldHandle = args.WorldHandle;
ShaderInstance? currentShader = null;
var player = _playerManager.LocalPlayer?.ControlledEntity;

View File

@@ -97,7 +97,7 @@ namespace Robust.Client.GameObjects
private void DispatchInputCommand(FullInputCmdMessage message)
{
_stateManager.InputCommandDispatched(message);
EntityNetworkManager.SendSystemNetworkMessage(message, message.InputSequence);
EntityManager.EntityNetManager?.SendSystemNetworkMessage(message, message.InputSequence);
}
public override void Initialize()

View File

@@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using JetBrains.Annotations;
using Robust.Client.Physics;
using Robust.Shared.GameObjects;
@@ -6,6 +8,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
@@ -19,19 +22,19 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IMapManagerInternal _mapManager = default!;
private readonly Dictionary<MapId, MapTrees> _mapTrees = new();
private readonly Dictionary<MapId, Dictionary<GridId, MapTrees>> _gridTrees = new();
private readonly List<SpriteComponent> _spriteQueue = new();
private readonly List<PointLightComponent> _lightQueue = new();
internal DynamicTree<SpriteComponent> GetSpriteTreeForMap(MapId map)
internal DynamicTree<SpriteComponent> GetSpriteTreeForMap(MapId map, GridId grid)
{
return _mapTrees[map].SpriteTree;
return _gridTrees[map][grid].SpriteTree;
}
internal DynamicTree<PointLightComponent> GetLightTreeForMap(MapId map)
internal DynamicTree<PointLightComponent> GetLightTreeForMap(MapId map, GridId grid)
{
return _mapTrees[map].LightTree;
return _gridTrees[map][grid].LightTree;
}
public override void Initialize()
@@ -44,102 +47,180 @@ namespace Robust.Client.GameObjects
_mapManager.MapCreated += MapManagerOnMapCreated;
_mapManager.MapDestroyed += MapManagerOnMapDestroyed;
_mapManager.OnGridCreated += MapManagerOnGridCreated;
_mapManager.OnGridRemoved += MapManagerOnGridRemoved;
SubscribeLocalEvent<EntMapIdChangedMessage>(EntMapIdChanged);
SubscribeLocalEvent<MoveEvent>(EntMoved);
SubscribeLocalEvent<EntParentChangedMessage>(EntParentChanged);
SubscribeLocalEvent<PointLightRadiusChangedMessage>(PointLightRadiusChanged);
SubscribeLocalEvent<RenderTreeRemoveSpriteMessage>(RemoveSprite);
SubscribeLocalEvent<RenderTreeRemoveLightMessage>(RemoveLight);
SubscribeLocalEvent<SpriteComponent, EntMapIdChangedMessage>(SpriteMapChanged);
SubscribeLocalEvent<SpriteComponent, MoveEvent>(SpriteMoved);
SubscribeLocalEvent<SpriteComponent, EntParentChangedMessage>(SpriteParentChanged);
SubscribeLocalEvent<SpriteComponent, RenderTreeRemoveSpriteEvent>(RemoveSprite);
SubscribeLocalEvent<PointLightComponent, EntMapIdChangedMessage>(LightMapChanged);
SubscribeLocalEvent<PointLightComponent, MoveEvent>(LightMoved);
SubscribeLocalEvent<PointLightComponent, EntParentChangedMessage>(LightParentChanged);
SubscribeLocalEvent<PointLightComponent, PointLightRadiusChangedEvent>(PointLightRadiusChanged);
SubscribeLocalEvent<PointLightComponent, RenderTreeRemoveLightEvent>(RemoveLight);
}
// For these next 2 methods (the Remove* ones):
// For the RemoveX methods
// If the Transform is removed BEFORE the Sprite/Light,
// then the MapIdChanged code will handle and remove it (because MapId gets set to nullspace).
// Otherwise these will still have their past MapId and that's all we need..
private void RemoveLight(RenderTreeRemoveLightMessage ev)
#region SpriteHandlers
private void SpriteMapChanged(EntityUid uid, SpriteComponent component, EntMapIdChangedMessage args)
{
_mapTrees[ev.Map].LightTree.Remove(ev.Light);
QueueSpriteUpdate(component);
}
private void RemoveSprite(RenderTreeRemoveSpriteMessage ev)
private void SpriteMoved(EntityUid uid, SpriteComponent component, MoveEvent args)
{
_mapTrees[ev.Map].SpriteTree.Remove(ev.Sprite);
QueueSpriteUpdate(component);
}
private void PointLightRadiusChanged(PointLightRadiusChangedMessage ev)
private void SpriteParentChanged(EntityUid uid, SpriteComponent component, EntParentChangedMessage args)
{
QueueUpdateLight(ev.PointLightComponent);
QueueSpriteUpdate(component);
}
private void EntParentChanged(EntParentChangedMessage ev)
private void RemoveSprite(EntityUid uid, SpriteComponent component, RenderTreeRemoveSpriteEvent args)
{
UpdateEntity(ev.Entity);
ClearSprite(component);
}
private void EntMoved(MoveEvent ev)
private void ClearSprite(SpriteComponent component)
{
UpdateEntity(ev.Sender);
}
private void UpdateEntity(IEntity entity)
{
if (entity.TryGetComponent(out SpriteComponent? spriteComponent))
if (_gridTrees.TryGetValue(component.IntersectingMapId, out var gridTrees))
{
if (!spriteComponent.TreeUpdateQueued)
foreach (var gridId in component.IntersectingGrids)
{
spriteComponent.TreeUpdateQueued = true;
_spriteQueue.Add(spriteComponent);
if (!gridTrees.TryGetValue(gridId, out var tree)) continue;
tree.SpriteTree.Remove(component);
}
}
if (entity.TryGetComponent(out PointLightComponent? light))
{
QueueUpdateLight(light);
}
component.IntersectingGrids.Clear();
}
foreach (var child in entity.Transform.ChildEntityUids)
private void QueueSpriteUpdate(SpriteComponent component)
{
if (component.TreeUpdateQueued) return;
component.TreeUpdateQueued = true;
_spriteQueue.Add(component);
foreach (var child in component.Owner.Transform.Children)
{
UpdateEntity(EntityManager.GetEntity(child));
QueueSpriteUpdate(child.Owner);
}
}
private void QueueUpdateLight(PointLightComponent light)
private void QueueSpriteUpdate(IEntity entity)
{
if (!light.TreeUpdateQueued)
{
light.TreeUpdateQueued = true;
if (!entity.TryGetComponent(out SpriteComponent? spriteComponent)) return;
QueueSpriteUpdate(spriteComponent);
_lightQueue.Add(light);
foreach (var child in entity.Transform.Children)
{
QueueSpriteUpdate(child.Owner);
}
}
#endregion
#region LightHandlers
private void LightMapChanged(EntityUid uid, PointLightComponent component, EntMapIdChangedMessage args)
{
QueueLightUpdate(component);
}
private void LightMoved(EntityUid uid, PointLightComponent component, MoveEvent args)
{
QueueLightUpdate(component);
}
private void LightParentChanged(EntityUid uid, PointLightComponent component, EntParentChangedMessage args)
{
QueueLightUpdate(component);
}
private void PointLightRadiusChanged(EntityUid uid, PointLightComponent component, PointLightRadiusChangedEvent args)
{
QueueLightUpdate(component);
}
private void RemoveLight(EntityUid uid, PointLightComponent component, RenderTreeRemoveLightEvent args)
{
ClearLight(component);
}
private void ClearLight(PointLightComponent component)
{
if (_gridTrees.TryGetValue(component.IntersectingMapId, out var gridTrees))
{
foreach (var gridId in component.IntersectingGrids)
{
if (!gridTrees.TryGetValue(gridId, out var tree)) continue;
tree.LightTree.Remove(component);
}
}
component.IntersectingGrids.Clear();
}
private void QueueLightUpdate(PointLightComponent component)
{
if (component.TreeUpdateQueued) return;
component.TreeUpdateQueued = true;
_lightQueue.Add(component);
foreach (var child in component.Owner.Transform.Children)
{
QueueLightUpdate(child.Owner);
}
}
private void EntMapIdChanged(EntMapIdChangedMessage ev)
private void QueueLightUpdate(IEntity entity)
{
// Nullspace is a valid map ID for stuff to have but we also aren't gonna bother indexing it.
// So that's why there's a GetValueOrDefault.
var oldMapTrees = _mapTrees.GetValueOrDefault(ev.OldMapId);
var newMapTrees = _mapTrees.GetValueOrDefault(ev.Entity.Transform.MapID);
if (!entity.TryGetComponent(out PointLightComponent? lightComponent)) return;
QueueLightUpdate(lightComponent);
if (ev.Entity.TryGetComponent(out SpriteComponent? sprite))
foreach (var child in entity.Transform.Children)
{
oldMapTrees?.SpriteTree.Remove(sprite);
newMapTrees?.SpriteTree.AddOrUpdate(sprite);
QueueLightUpdate(child.Owner);
}
}
#endregion
if (ev.Entity.TryGetComponent(out PointLightComponent? light))
{
oldMapTrees?.LightTree.Remove(light);
newMapTrees?.LightTree.AddOrUpdate(light);
}
public override void Shutdown()
{
base.Shutdown();
_mapManager.MapCreated -= MapManagerOnMapCreated;
_mapManager.MapDestroyed -= MapManagerOnMapDestroyed;
_mapManager.OnGridCreated -= MapManagerOnGridCreated;
_mapManager.OnGridRemoved -= MapManagerOnGridRemoved;
}
private void MapManagerOnMapDestroyed(object? sender, MapEventArgs e)
{
_mapTrees.Remove(e.Map);
foreach (var (_, gridTree) in _gridTrees[e.Map])
{
foreach (var comp in gridTree.LightTree)
{
comp.IntersectingGrids.Clear();
}
foreach (var comp in gridTree.SpriteTree)
{
comp.IntersectingGrids.Clear();
}
// Just in case?
gridTree.LightTree.Clear();
gridTree.SpriteTree.Clear();
}
_gridTrees.Remove(e.Map);
}
private void MapManagerOnMapCreated(object? sender, MapEventArgs e)
@@ -149,37 +230,122 @@ namespace Robust.Client.GameObjects
return;
}
_mapTrees.Add(e.Map, new MapTrees());
_gridTrees.Add(e.Map, new Dictionary<GridId, MapTrees>
{
{GridId.Invalid, new MapTrees()}
});
}
private void MapManagerOnGridCreated(MapId mapId, GridId gridId)
{
_gridTrees[mapId].Add(gridId, new MapTrees());
}
private void MapManagerOnGridRemoved(MapId mapId, GridId gridId)
{
var gridTree = _gridTrees[mapId][gridId];
foreach (var sprite in gridTree.SpriteTree)
{
sprite.IntersectingGrids.Remove(gridId);
}
foreach (var light in gridTree.LightTree)
{
light.IntersectingGrids.Remove(gridId);
}
// Clear in case
gridTree.LightTree.Clear();
gridTree.SpriteTree.Clear();
_gridTrees[mapId].Remove(gridId);
}
public override void FrameUpdate(float frameTime)
{
foreach (var queuedUpdateSprite in _spriteQueue)
foreach (var sprite in _spriteQueue)
{
var transform = queuedUpdateSprite.Owner.Transform;
var map = transform.MapID;
if (map == MapId.Nullspace)
{
continue;
}
var updateMapTree = _mapTrees[map].SpriteTree;
var mapId = sprite.Owner.Transform.MapID;
updateMapTree.AddOrUpdate(queuedUpdateSprite);
queuedUpdateSprite.TreeUpdateQueued = false;
// If we're on a new map then clear the old one.
if (sprite.IntersectingMapId != mapId)
{
ClearSprite(sprite);
}
sprite.IntersectingMapId = mapId;
if (mapId == MapId.Nullspace) continue;
var mapTree = _gridTrees[mapId];
var aabb = MapTrees.SpriteAabbFunc(sprite);
var intersectingGrids = _mapManager.FindGridIdsIntersecting(mapId, aabb, true).ToList();
// Remove from old
foreach (var gridId in sprite.IntersectingGrids)
{
if (intersectingGrids.Contains(gridId)) continue;
mapTree[gridId].SpriteTree.Remove(sprite);
}
// Rebuild in the update below
sprite.IntersectingGrids.Clear();
// Update / add to new
foreach (var gridId in intersectingGrids)
{
var translated = aabb.Translated(gridId == GridId.Invalid
? Vector2.Zero
: -_mapManager.GetGrid(gridId).WorldPosition);
mapTree[gridId].SpriteTree.AddOrUpdate(sprite, translated);
sprite.IntersectingGrids.Add(gridId);
}
sprite.TreeUpdateQueued = false;
}
foreach (var queuedUpdateLight in _lightQueue)
foreach (var light in _lightQueue)
{
var transform = queuedUpdateLight.Owner.Transform;
var map = transform.MapID;
if (map == MapId.Nullspace)
{
continue;
}
var updateMapTree = _mapTrees[map].LightTree;
var mapId = light.Owner.Transform.MapID;
updateMapTree.AddOrUpdate(queuedUpdateLight);
queuedUpdateLight.TreeUpdateQueued = false;
// If we're on a new map then clear the old one.
if (light.IntersectingMapId != mapId)
{
ClearLight(light);
}
light.IntersectingMapId = mapId;
if (mapId == MapId.Nullspace) continue;
var mapTree = _gridTrees[mapId];
var aabb = MapTrees.LightAabbFunc(light);
var intersectingGrids = _mapManager.FindGridIdsIntersecting(mapId, aabb, true).ToList();
// Remove from old
foreach (var gridId in intersectingGrids)
{
if (intersectingGrids.Contains(gridId)) continue;
mapTree[gridId].LightTree.Remove(light);
}
// Rebuild in the update below
light.IntersectingGrids.Clear();
// Update / add to new
foreach (var gridId in intersectingGrids)
{
var translated = aabb.Translated(gridId == GridId.Invalid
? Vector2.Zero
: -_mapManager.GetGrid(gridId).WorldPosition);
mapTree[gridId].LightTree.AddOrUpdate(light, translated);
light.IntersectingGrids.Add(gridId);
}
light.TreeUpdateQueued = false;
}
_spriteQueue.Clear();
@@ -197,14 +363,14 @@ namespace Robust.Client.GameObjects
LightTree = new DynamicTree<PointLightComponent>(LightAabbFunc);
}
private static Box2 SpriteAabbFunc(in SpriteComponent value)
internal static Box2 SpriteAabbFunc(in SpriteComponent value)
{
var worldPos = value.Owner.Transform.WorldPosition;
return new Box2(worldPos, worldPos);
}
private static Box2 LightAabbFunc(in PointLightComponent value)
internal static Box2 LightAabbFunc(in PointLightComponent value)
{
var worldPos = value.Owner.Transform.WorldPosition;
@@ -214,9 +380,9 @@ namespace Robust.Client.GameObjects
}
}
internal struct RenderTreeRemoveSpriteMessage
internal class RenderTreeRemoveSpriteEvent : EntityEventArgs
{
public RenderTreeRemoveSpriteMessage(SpriteComponent sprite, MapId map)
public RenderTreeRemoveSpriteEvent(SpriteComponent sprite, MapId map)
{
Sprite = sprite;
Map = map;
@@ -226,9 +392,9 @@ namespace Robust.Client.GameObjects
public MapId Map { get; }
}
internal struct RenderTreeRemoveLightMessage
internal class RenderTreeRemoveLightEvent : EntityEventArgs
{
public RenderTreeRemoveLightMessage(PointLightComponent light, MapId map)
public RenderTreeRemoveLightEvent(PointLightComponent light, MapId map)
{
Light = light;
Map = map;

View File

@@ -3,6 +3,7 @@ using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects
{
@@ -13,12 +14,19 @@ namespace Robust.Client.GameObjects
public class SpriteSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
private RenderingTreeSystem _treeSystem = default!;
public override void Initialize()
{
base.Initialize();
_treeSystem = Get<RenderingTreeSystem>();
}
/// <inheritdoc />
public override void FrameUpdate(float frameTime)
{
var renderTreeSystem = EntitySystemManager.GetEntitySystem<RenderingTreeSystem>();
// So we could calculate the correct size of the entities based on the contents of their sprite...
// Or we can just assume that no entity is larger than 10x10 and get a stupid easy check.
var pvsBounds = _eyeManager.GetWorldViewport().Enlarged(5);
@@ -29,18 +37,23 @@ namespace Robust.Client.GameObjects
return;
}
var mapTree = renderTreeSystem.GetSpriteTreeForMap(currentMap);
mapTree.QueryAabb(ref frameTime, (ref float state, in SpriteComponent value) =>
foreach (var gridId in _mapManager.FindGridIdsIntersecting(currentMap, pvsBounds, true))
{
if (value.IsInert)
{
return true;
}
var gridBounds = gridId == GridId.Invalid ? pvsBounds : pvsBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
value.FrameUpdate(state);
return true;
}, pvsBounds, approx: true);
var mapTree = _treeSystem.GetSpriteTreeForMap(currentMap, gridId);
mapTree.QueryAabb(ref frameTime, (ref float state, in SpriteComponent value) =>
{
if (value.IsInert)
{
return true;
}
value.FrameUpdate(state);
return true;
}, gridBounds, approx: true);
}
}
}
}

View File

@@ -0,0 +1,37 @@
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects
{
[UsedImplicitly]
public sealed class UserInterfaceSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<BoundUIWrapMessage>(MessageReceived);
SubscribeLocalEvent<ClientUserInterfaceComponent, ComponentShutdown>(OnUserInterfaceShutdown);
}
private void OnUserInterfaceShutdown(EntityUid uid, ClientUserInterfaceComponent component, ComponentShutdown args)
{
foreach (var bui in component.Interfaces)
{
bui.Dispose();
}
}
private void MessageReceived(BoundUIWrapMessage ev)
{
var cmp = ComponentManager.GetComponent<ClientUserInterfaceComponent>(ev.Entity);
cmp.MessageReceived(ev);
}
internal void Send(BoundUIWrapMessage msg)
{
RaiseNetworkEvent(msg);
}
}
}

View File

@@ -3,10 +3,8 @@ using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects
{
public interface IClientEntityManager : IEntityManager
public interface IClientEntityManager : IEntityManager, IEntityNetworkManager
{
/// <returns>The list of new entities created.</returns>
List<EntityUid> ApplyEntityStates(EntityState[]? curEntStates, IEnumerable<EntityUid>? deletions,
EntityState[]? nextEntStates);
}
}

View File

@@ -0,0 +1,15 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects
{
internal interface IClientEntityManagerInternal : IClientEntityManager
{
// These methods are used by the Game State Manager.
IEntity CreateEntity(string? prototypeName, EntityUid? uid = null);
void InitializeEntity(IEntity entity);
void StartEntity(IEntity entity);
}
}

View File

@@ -1,12 +1,15 @@
using System;
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects
{
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public class PlayerAttachedMsg : ComponentMessage
{
}
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public class PlayerDetachedMsg : ComponentMessage
{

View File

@@ -1,14 +1,18 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Robust.Client.GameObjects;
using Robust.Client.Input;
using Robust.Client.Map;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Network.Messages;
using Robust.Client.Player;
using Robust.Client.Timing;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Log;
@@ -31,16 +35,21 @@ namespace Robust.Client.GameStates
_pendingSystemMessages
= new();
[Dependency] private readonly IClientEntityManager _entities = default!;
[Dependency] private readonly IComponentFactory _compFactory = default!;
[Dependency] private readonly IClientEntityManagerInternal _entities = default!;
[Dependency] private readonly IEntityLookup _lookup = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IClientNetManager _network = default!;
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IClientMapManager _mapManager = default!;
[Dependency] private readonly IClientGameTiming _timing = default!;
[Dependency] private readonly INetConfigurationManager _config = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IComponentManager _componentManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
#if EXCEPTION_TOLERANCE
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
#endif
/// <inheritdoc />
public int MinBufferSize => _processor.MinBufferSize;
@@ -242,65 +251,69 @@ namespace Robust.Client.GameStates
if (!Predicting) return;
using var _ = _timing.StartPastPredictionArea();
if (_pendingInputs.Count > 0)
using(var _ = _timing.StartPastPredictionArea())
{
Logger.DebugS(CVars.NetPredict.Name, "CL> Predicted:");
if (_pendingInputs.Count > 0)
{
Logger.DebugS(CVars.NetPredict.Name, "CL> Predicted:");
}
var pendingInputEnumerator = _pendingInputs.GetEnumerator();
var pendingMessagesEnumerator = _pendingSystemMessages.GetEnumerator();
var hasPendingInput = pendingInputEnumerator.MoveNext();
var hasPendingMessage = pendingMessagesEnumerator.MoveNext();
var ping = _network.ServerChannel!.Ping / 1000f + PredictLagBias; // seconds.
var targetTick = _timing.CurTick.Value + _processor.TargetBufferSize +
(int) Math.Ceiling(_timing.TickRate * ping) + PredictTickBias;
// Logger.DebugS("net.predict", $"Predicting from {_lastProcessedTick} to {targetTick}");
for (var t = _lastProcessedTick.Value + 1; t <= targetTick; t++)
{
var tick = new GameTick(t);
_timing.CurTick = tick;
while (hasPendingInput && pendingInputEnumerator.Current.Tick <= tick)
{
var inputCmd = pendingInputEnumerator.Current;
_inputManager.NetworkBindMap.TryGetKeyFunction(inputCmd.InputFunctionId, out var boundFunc);
Logger.DebugS(CVars.NetPredict.Name,
$" seq={inputCmd.InputSequence}, sub={inputCmd.SubTick}, dTick={tick}, func={boundFunc.FunctionName}, " +
$"state={inputCmd.State}");
input.PredictInputCommand(inputCmd);
hasPendingInput = pendingInputEnumerator.MoveNext();
}
while (hasPendingMessage && pendingMessagesEnumerator.Current.sourceTick <= tick)
{
var msg = pendingMessagesEnumerator.Current.msg;
_entities.EventBus.RaiseEvent(EventSource.Local, msg);
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.sessionMsg);
hasPendingMessage = pendingMessagesEnumerator.MoveNext();
}
if (t != targetTick)
{
// Don't run EntitySystemManager.TickUpdate if this is the target tick,
// because the rest of the main loop will call into it with the target tick later,
// and it won't be a past prediction.
_entitySystemManager.TickUpdate((float) _timing.TickPeriod.TotalSeconds);
((IBroadcastEventBusInternal) _entities.EventBus).ProcessEventQueue();
}
}
}
var pendingInputEnumerator = _pendingInputs.GetEnumerator();
var pendingMessagesEnumerator = _pendingSystemMessages.GetEnumerator();
var hasPendingInput = pendingInputEnumerator.MoveNext();
var hasPendingMessage = pendingMessagesEnumerator.MoveNext();
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds);
var ping = _network.ServerChannel!.Ping / 1000f + PredictLagBias; // seconds.
var targetTick = _timing.CurTick.Value + _processor.TargetBufferSize +
(int) Math.Ceiling(_timing.TickRate * ping) + PredictTickBias;
// Logger.DebugS("net.predict", $"Predicting from {_lastProcessedTick} to {targetTick}");
for (var t = _lastProcessedTick.Value + 1; t <= targetTick; t++)
{
var tick = new GameTick(t);
_timing.CurTick = tick;
while (hasPendingInput && pendingInputEnumerator.Current.Tick <= tick)
{
var inputCmd = pendingInputEnumerator.Current;
_inputManager.NetworkBindMap.TryGetKeyFunction(inputCmd.InputFunctionId, out var boundFunc);
Logger.DebugS(CVars.NetPredict.Name,
$" seq={inputCmd.InputSequence}, sub={inputCmd.SubTick}, dTick={tick}, func={boundFunc.FunctionName}, " +
$"state={inputCmd.State}");
input.PredictInputCommand(inputCmd);
hasPendingInput = pendingInputEnumerator.MoveNext();
}
while (hasPendingMessage && pendingMessagesEnumerator.Current.sourceTick <= tick)
{
var msg = pendingMessagesEnumerator.Current.msg;
_entities.EventBus.RaiseEvent(EventSource.Local, msg);
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.sessionMsg);
hasPendingMessage = pendingMessagesEnumerator.MoveNext();
}
if (t != targetTick)
{
// Don't run EntitySystemManager.Update if this is the target tick,
// because the rest of the main loop will call into it with the target tick later,
// and it won't be a past prediction.
_entitySystemManager.Update((float) _timing.TickPeriod.TotalSeconds);
((IBroadcastEventBusInternal) _entities.EventBus).ProcessEventQueue();
}
}
_lookup.Update();
}
private void ResetPredictedEntities(GameTick curTick)
@@ -381,7 +394,7 @@ namespace Robust.Client.GameStates
{
_config.TickProcessMessages();
_mapManager.ApplyGameStatePre(curState.MapData);
var createdEntities = _entities.ApplyEntityStates(curState.EntityStates, curState.EntityDeletions,
var createdEntities = ApplyEntityStates(curState.EntityStates, curState.EntityDeletions,
nextState?.EntityStates);
_players.ApplyPlayerStates(curState.PlayerStates);
_mapManager.ApplyGameStatePost(curState.MapData);
@@ -389,6 +402,218 @@ namespace Robust.Client.GameStates
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState));
return createdEntities;
}
private List<EntityUid> ApplyEntityStates(EntityState[]? curEntStates, IEnumerable<EntityUid>? deletions,
EntityState[]? nextEntStates)
{
var toApply = new Dictionary<IEntity, (EntityState?, EntityState?)>();
var toInitialize = new List<Entity>();
var created = new List<EntityUid>();
deletions ??= new EntityUid[0];
if (curEntStates != null && curEntStates.Length != 0)
{
foreach (var es in curEntStates)
{
//Known entities
if (_entities.TryGetEntity(es.Uid, out var entity))
{
toApply.Add(entity, (es, null));
}
else //Unknown entities
{
var metaState = (MetaDataComponentState?) es.ComponentStates
?.FirstOrDefault(c => c.NetID == NetIDs.META_DATA);
if (metaState == null)
{
throw new InvalidOperationException($"Server sent new entity state for {es.Uid} without metadata component!");
}
var newEntity = (Entity)_entities.CreateEntity(metaState.PrototypeId, es.Uid);
toApply.Add(newEntity, (es, null));
toInitialize.Add(newEntity);
created.Add(newEntity.Uid);
}
}
}
if (nextEntStates != null && nextEntStates.Length != 0)
{
foreach (var es in nextEntStates)
{
if (_entities.TryGetEntity(es.Uid, out var entity))
{
if (toApply.TryGetValue(entity, out var state))
{
toApply[entity] = (state.Item1, es);
}
else
{
toApply[entity] = (null, es);
}
}
}
}
// Make sure this is done after all entities have been instantiated.
foreach (var kvStates in toApply)
{
var ent = kvStates.Key;
var entity = (Entity) ent;
HandleEntityState(entity.EntityManager.ComponentManager, entity, kvStates.Value.Item1,
kvStates.Value.Item2);
}
foreach (var id in deletions)
{
_entities.DeleteEntity(id);
}
#if EXCEPTION_TOLERANCE
HashSet<Entity> brokenEnts = new HashSet<Entity>();
#endif
foreach (var entity in toInitialize)
{
#if EXCEPTION_TOLERANCE
try
{
#endif
_entities.InitializeEntity(entity);
#if EXCEPTION_TOLERANCE
}
catch (Exception e)
{
Logger.ErrorS("state", $"Server entity threw in Init: uid={entity.Uid}, proto={entity.Prototype}\n{e}");
brokenEnts.Add(entity);
}
#endif
}
foreach (var entity in toInitialize)
{
#if EXCEPTION_TOLERANCE
if (brokenEnts.Contains(entity))
continue;
try
{
#endif
_entities.StartEntity(entity);
#if EXCEPTION_TOLERANCE
}
catch (Exception e)
{
Logger.ErrorS("state", $"Server entity threw in Start: uid={entity.Uid}, proto={entity.Prototype}\n{e}");
brokenEnts.Add(entity);
}
#endif
}
foreach (var entity in toInitialize)
{
#if EXCEPTION_TOLERANCE
if (brokenEnts.Contains(entity))
continue;
#endif
}
#if EXCEPTION_TOLERANCE
foreach (var entity in brokenEnts)
{
entity.Delete();
}
#endif
return created;
}
private void HandleEntityState(IComponentManager compMan, IEntity entity, EntityState? curState,
EntityState? nextState)
{
var compStateWork = new Dictionary<uint, (ComponentState? curState, ComponentState? nextState)>();
var entityUid = entity.Uid;
if (curState?.ComponentChanges != null)
{
foreach (var compChange in curState.ComponentChanges)
{
if (compChange.Deleted)
{
if (compMan.TryGetComponent(entityUid, compChange.NetID, out var comp))
{
compMan.RemoveComponent(entityUid, comp);
}
}
else
{
if (compMan.HasComponent(entityUid, compChange.NetID))
continue;
var newComp = (Component) _compFactory.GetComponent(compChange.ComponentName!);
newComp.Owner = entity;
compMan.AddComponent(entity, newComp, true);
}
}
}
if (curState?.ComponentStates != null)
{
foreach (var compState in curState.ComponentStates)
{
compStateWork[compState.NetID] = (compState, null);
}
}
if (nextState?.ComponentStates != null)
{
foreach (var compState in nextState.ComponentStates)
{
if (compStateWork.TryGetValue(compState.NetID, out var state))
{
compStateWork[compState.NetID] = (state.curState, compState);
}
else
{
compStateWork[compState.NetID] = (null, compState);
}
}
}
foreach (var (netId, (cur, next)) in compStateWork)
{
if (compMan.TryGetComponent(entityUid, netId, out var component))
{
try
{
component.HandleComponentState(cur, next);
}
catch (Exception e)
{
var wrapper = new ComponentStateApplyException(
$"Failed to apply comp state: entity={component.Owner}, comp={component.Name}", e);
#if EXCEPTION_TOLERANCE
_runtimeLog.LogException(wrapper, "Component state apply");
#else
throw wrapper;
#endif
}
}
else
{
// The component can be null here due to interp.
// Because the NEXT state will have a new component, but this one doesn't yet.
// That's fine though.
if (cur == null)
{
continue;
}
var eUid = entityUid;
var eRegisteredNetUidName = _compFactory.GetRegistration(netId).Name;
DebugTools.Assert(
$"Component does not exist for state: entUid={eUid}, expectedNetId={netId}, expectedName={eRegisteredNetUidName}");
}
}
}
}
public class GameStateAppliedArgs : EventArgs

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
@@ -9,6 +10,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
@@ -28,8 +30,8 @@ namespace Robust.Client.GameStates
private const int TrafficHistorySize = 64; // Size of the traffic history bar in game ticks.
/// <inheritdoc />
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
private readonly Font _font;
private readonly int _lineHeight;
private readonly List<NetEntity> _netEnts = new();
@@ -43,12 +45,12 @@ namespace Robust.Client.GameStates
_gameStateManager.GameStateApplied += HandleGameStateApplied;
}
private void HandleGameStateApplied(GameStateAppliedArgs args)
{
if(_gameTiming.InPrediction) // we only care about real server states.
return;
// Shift traffic history down one
for (var i = 0; i < _netEnts.Count; i++)
{
@@ -75,7 +77,7 @@ namespace Robust.Client.GameStates
if (netEnt.Id != entityState.Uid)
continue;
//TODO: calculate size of state and record it here.
netEnt.Traffic[^1] = 1;
netEnt.LastUpdate = gameState.ToSequence;
@@ -95,15 +97,15 @@ namespace Robust.Client.GameStates
}
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
float pvsSize = _configurationManager.GetCVar<float>("net.maxupdaterange");
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsSize*2, pvsSize*2));
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsRange*2, pvsRange*2));
int timeout = _gameTiming.TickRate * 3;
for (int i = 0; i < _netEnts.Count; i++)
{
var netEnt = _netEnts[i];
if(_entityManager.EntityExists(netEnt.Id))
{
//TODO: Whoever is working on PVS remake, change the InPVS detection.
@@ -124,22 +126,57 @@ namespace Robust.Client.GameStates
_netEnts[i] = netEnt; // copy struct back
}
}
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
protected internal override void Draw(in OverlayDrawArgs args)
{
if (!_netManager.IsConnected)
return;
switch (args.Space)
{
case OverlaySpace.ScreenSpace:
DrawScreen(args);
break;
case OverlaySpace.WorldSpace:
DrawWorld(args);
break;
}
}
private void DrawWorld(in OverlayDrawArgs args)
{
bool pvsEnabled = _configurationManager.GetCVar<bool>("net.pvs");
if(!pvsEnabled)
return;
float pvsRange = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsRange * 2, pvsRange * 2));
var worldHandle = args.WorldHandle;
worldHandle.DrawRect(pvsBox, Color.Red, false);
}
private void DrawScreen(in OverlayDrawArgs args)
{
// remember, 0,0 is top left of ui with +X right and +Y down
var screenHandle = (DrawingHandleScreen)handle;
var screenHandle = args.ScreenHandle;
for (int i = 0; i < _netEnts.Count; i++)
{
var netEnt = _netEnts[i];
if (!_entityManager.TryGetEntity(netEnt.Id, out var ent))
{
_netEnts.RemoveSwap(i);
i--;
continue;
}
var xPos = 100;
var yPos = 10 + _lineHeight * i;
var name = $"({netEnt.Id}) {_entityManager.GetEntity(netEnt.Id).Prototype?.ID}";
var name = $"({netEnt.Id}) {ent.Prototype?.ID}";
var color = CalcTextColor(ref netEnt);
DrawString(screenHandle, _font, new Vector2(xPos + (TrafficHistorySize + 4), yPos), name, color);
DrawTrafficBox(screenHandle, ref netEnt, xPos, yPos);
@@ -190,9 +227,9 @@ namespace Robust.Client.GameStates
{
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
foreach (var chr in str)
foreach (var rune in str.EnumerateRunes())
{
var advance = font.DrawChar(handle, chr, baseLine, 1, textColor);
var advance = font.DrawChar(handle, rune, baseLine, 1, textColor);
baseLine += new Vector2(advance, 0);
}
}
@@ -225,7 +262,7 @@ namespace Robust.Client.GameStates
{
if (args.Length != 1)
{
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
shell.WriteError("Invalid argument amount. Expected 1 arguments.");
return;
}

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
@@ -36,6 +38,10 @@ namespace Robust.Client.GameStates
private readonly List<(GameTick Tick, int Payload, int lag, int interp)> _history = new(HistorySize+10);
private int _totalHistoryPayload; // sum of all data point sizes in bytes
public EntityUid WatchEntId { get; set; }
public NetGraphOverlay()
{
IoCManager.InjectDependencies(this);
@@ -60,7 +66,73 @@ namespace Robust.Client.GameStates
// calc interp info
var interpBuff = _gameStateManager.CurrentBufferSize - _gameStateManager.MinBufferSize;
_totalHistoryPayload += sz;
_history.Add((toSeq, sz, lag, interpBuff));
// not watching an ent
if(!WatchEntId.IsValid() || WatchEntId.IsClientSide())
return;
string? entStateString = null;
string? entDelString = null;
var conShell = IoCManager.Resolve<IConsoleHost>().LocalShell;
var entStates = args.AppliedState.EntityStates;
if (entStates is not null)
{
var sb = new StringBuilder();
foreach (var entState in entStates)
{
if (entState.Uid == WatchEntId)
{
if(entState.ComponentChanges is not null)
{
sb.Append($"\n Changes:");
foreach (var compChange in entState.ComponentChanges)
{
var del = compChange.Deleted ? 'D' : 'C';
sb.Append($"\n [{del}]{compChange.NetID}:{compChange.ComponentName}");
}
}
if (entState.ComponentStates is not null)
{
sb.Append($"\n States:");
foreach (var compState in entState.ComponentStates)
{
sb.Append($"\n {compState.NetID}:{compState.GetType().Name}");
}
}
}
}
entStateString = sb.ToString();
}
var entDeletes = args.AppliedState.EntityDeletions;
if (entDeletes is not null)
{
var sb = new StringBuilder();
foreach (var entDelete in entDeletes)
{
if (entDelete == WatchEntId)
{
entDelString = "\n Deleted";
}
}
}
if (!string.IsNullOrWhiteSpace(entStateString) || !string.IsNullOrWhiteSpace(entDelString))
{
var fullString = $"watchEnt: from={args.AppliedState.FromSequence}, to={args.AppliedState.ToSequence}, eid={WatchEntId}";
if (!string.IsNullOrWhiteSpace(entStateString))
fullString += entStateString;
if (!string.IsNullOrWhiteSpace(entDelString))
fullString += entDelString;
conShell.WriteLine(fullString + "\n");
}
}
/// <inheritdoc />
@@ -69,19 +141,27 @@ namespace Robust.Client.GameStates
base.FrameUpdate(args);
var over = _history.Count - HistorySize;
if (over > 0)
if (over <= 0)
return;
for (int i = 0; i < over; i++)
{
_history.RemoveRange(0, over);
var point = _history[i];
_totalHistoryPayload -= point.Payload;
}
_history.RemoveRange(0, over);
}
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
protected internal override void Draw(in OverlayDrawArgs args)
{
// remember, 0,0 is top left of ui with +X right and +Y down
var leftMargin = 300;
var width = HistorySize;
var height = 500;
var drawSizeThreshold = Math.Min(_totalHistoryPayload / HistorySize, 300);
var handle = args.ScreenHandle;
// bottom payload line
handle.DrawLine(new Vector2(leftMargin, height), new Vector2(leftMargin + width, height), Color.DarkGray.WithAlpha(0.8f));
@@ -101,6 +181,12 @@ namespace Robust.Client.GameStates
var yoff = height - state.Payload / BytesPerPixel;
handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, yoff), Color.LightGreen.WithAlpha(0.8f));
// Draw size if above average
if (drawSizeThreshold * 1.5 < state.Payload)
{
DrawString(handle, _font, new Vector2(xOff, yoff - _font.GetLineHeight(1)), state.Payload.ToString());
}
// second tick marks
if (state.Tick.Value % _gameTiming.TickRate == 0)
{
@@ -125,6 +211,10 @@ namespace Robust.Client.GameStates
handle.DrawLine(new Vector2(xOff, height + LowerGraphOffset), new Vector2(xOff, height + LowerGraphOffset + state.interp * 6), interpColor.WithAlpha(0.8f));
}
// average payload line
var avgyoff = height - drawSizeThreshold / BytesPerPixel;
handle.DrawLine(new Vector2(leftMargin, avgyoff), new Vector2(leftMargin + width, avgyoff), Color.DarkGray.WithAlpha(0.8f));
// top payload warning line
var warnYoff = height - _warningPayloadSize / BytesPerPixel;
handle.DrawLine(new Vector2(leftMargin, warnYoff), new Vector2(leftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f));
@@ -134,14 +224,14 @@ namespace Robust.Client.GameStates
handle.DrawLine(new Vector2(leftMargin, midYoff), new Vector2(leftMargin + width, midYoff), Color.DarkGray.WithAlpha(0.8f));
// payload text
DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin + width, warnYoff), "56K");
DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin + width, midYoff), "33.6K");
DrawString(handle, _font, new Vector2(leftMargin + width, warnYoff), "56K");
DrawString(handle, _font, new Vector2(leftMargin + width, midYoff), "33.6K");
// interp text info
if(lastLagY != -1)
DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
DrawString(handle, _font, new Vector2(leftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
DrawString(handle, _font, new Vector2(leftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
}
protected override void DisposeBehavior()
@@ -155,9 +245,9 @@ namespace Robust.Client.GameStates
{
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
foreach (var chr in str)
foreach (var rune in str.EnumerateRunes())
{
var advance = font.DrawChar(handle, chr, baseLine, 1, Color.White);
var advance = font.DrawChar(handle, rune, baseLine, 1, Color.White);
baseLine += new Vector2(advance, 0);
}
}
@@ -197,5 +287,36 @@ namespace Robust.Client.GameStates
}
}
}
private class NetWatchEntCommand : IConsoleCommand
{
public string Command => "net_watchent";
public string Help => "net_watchent <0|EntityUid>";
public string Description => "Dumps all network updates for an EntityId to the console.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError("Invalid argument amount. Expected 1 argument.");
return;
}
if (!EntityUid.TryParse(args[0], out var eValue))
{
shell.WriteError("Invalid argument: Needs to be 0 or an entityId.");
return;
}
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if (overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
var netOverlay = overlayMan.GetOverlay<NetGraphOverlay>();
netOverlay.WatchEntId = eValue;
}
}
}
}
}

View File

@@ -26,8 +26,9 @@ namespace Robust.Client.GameStates
_shader = _prototypeManager.Index<ShaderPrototype>("unshaded").Instance();
}
protected override void Draw(DrawingHandleBase handle, OverlaySpace currentSpace)
protected internal override void Draw(in OverlayDrawArgs args)
{
var handle = args.DrawingHandle;
handle.UseShader(_shader);
var worldHandle = (DrawingHandleWorld) handle;
var viewport = _eyeManager.GetWorldViewport();

View File

@@ -50,9 +50,9 @@ namespace Robust.Client.Graphics
}
/// <inheritdoc />
public void GetViewMatrix(out Matrix3 viewMatrix)
public void GetViewMatrix(out Matrix3 viewMatrix, Vector2 renderScale)
{
var scaleMat = Matrix3.CreateScale(_scale.X, _scale.Y);
var scaleMat = Matrix3.CreateScale(_scale.X * renderScale.X, _scale.Y * renderScale.Y);
var rotMat = Matrix3.CreateRotation(_rotation);
var transMat = Matrix3.CreateTranslation(-_coords.Position);
@@ -60,9 +60,9 @@ namespace Robust.Client.Graphics
}
/// <inheritdoc />
public void GetViewMatrixInv(out Matrix3 viewMatrixInv)
public void GetViewMatrixInv(out Matrix3 viewMatrixInv, Vector2 renderScale)
{
GetViewMatrix(out var viewMatrix);
GetViewMatrix(out var viewMatrix, renderScale);
viewMatrixInv = Matrix3.Invert(viewMatrix);
}
}

View File

@@ -1,4 +1,6 @@
using Robust.Shared.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -19,6 +21,7 @@ namespace Robust.Client.Graphics
[Dependency] private readonly IClyde _displayManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
// We default to this when we get set to a null eye.
private readonly FixedEye _defaultEye = new();
@@ -32,11 +35,18 @@ namespace Robust.Client.Graphics
set => _currentEye = value;
}
public IViewportControl MainViewport { get; set; } = default!;
public void ClearCurrentEye()
{
_currentEye = _defaultEye;
}
void IEyeManager.Initialize()
{
MainViewport = _uiManager.MainViewport;
}
/// <inheritdoc />
public MapId CurrentMap => CurrentEye.Position.MapId;
@@ -49,7 +59,7 @@ namespace Robust.Client.Graphics
var topRight = ScreenToMap(new Vector2(vpSize.X, 0));
var bottomRight = ScreenToMap(vpSize);
var bottomLeft = ScreenToMap(new Vector2(0, vpSize.Y));
var left = MathHelper.Min(topLeft.X, topRight.X, bottomRight.X, bottomLeft.X);
var bottom = MathHelper.Min(topLeft.Y, topRight.Y, bottomRight.Y, bottomLeft.Y);
var right = MathHelper.Max(topLeft.X, topRight.X, bottomRight.X, bottomLeft.X);
@@ -61,16 +71,7 @@ namespace Robust.Client.Graphics
/// <inheritdoc />
public Vector2 WorldToScreen(Vector2 point)
{
var newPoint = point;
CurrentEye.GetViewMatrix(out var viewMatrix);
newPoint = viewMatrix * newPoint;
// (inlined version of UiProjMatrix)
newPoint *= new Vector2(1, -1) * PixelsPerMeter;
newPoint += _displayManager.ScreenSize / 2f;
return newPoint;
return MainViewport.WorldToScreen(point);
}
/// <inheritdoc />
@@ -103,29 +104,23 @@ namespace Robust.Client.Graphics
public ScreenCoordinates MapToScreen(MapCoordinates point)
{
return new(WorldToScreen(point.Position));
return new(WorldToScreen(point.Position), MainViewport.Window?.Id ?? default);
}
/// <inheritdoc />
public MapCoordinates ScreenToMap(ScreenCoordinates point)
{
return ScreenToMap(point.Position);
var (pos, window) = point;
if (window != MainViewport.Window?.Id)
return default;
return MainViewport.ScreenToMap(pos);
}
/// <inheritdoc />
public MapCoordinates ScreenToMap(Vector2 point)
{
var newPoint = point;
// (inlined version of UiProjMatrix^-1)
newPoint -= _displayManager.ScreenSize / 2f;
newPoint *= new Vector2(1, -1) / PixelsPerMeter;
// view matrix
CurrentEye.GetViewMatrixInv(out var viewMatrixInv);
newPoint = viewMatrixInv * newPoint;
return new MapCoordinates(newPoint, CurrentMap);
return MainViewport.ScreenToMap(point);
}
}
}

View File

@@ -41,13 +41,15 @@ namespace Robust.Client.Graphics
/// world space to camera space.
/// </summary>
/// <param name="viewMatrix">View matrix for this camera.</param>
void GetViewMatrix(out Matrix3 viewMatrix);
/// <param name="renderScale"></param>
void GetViewMatrix(out Matrix3 viewMatrix, Vector2 renderScale);
/// <summary>
/// Returns the inverted view matrix for this eye, used to convert a point from
/// camera space to world space.
/// </summary>
/// <param name="viewMatrixInv">Inverted view matrix for this camera.</param>
void GetViewMatrixInv(out Matrix3 viewMatrixInv);
/// <param name="renderScale"></param>
void GetViewMatrixInv(out Matrix3 viewMatrixInv, Vector2 renderScale);
}
}

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Map;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
@@ -17,6 +18,8 @@ namespace Robust.Client.Graphics
/// </remarks>
IEye CurrentEye { get; set; }
IViewportControl MainViewport { get; set; }
/// <summary>
/// The ID of the map on which the current eye is "placed".
/// </summary>
@@ -72,5 +75,6 @@ namespace Robust.Client.Graphics
MapCoordinates ScreenToMap(Vector2 point);
void ClearCurrentEye();
void Initialize();
}
}

View File

@@ -53,7 +53,7 @@ namespace Robust.Client.Graphics.Clyde
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_configurationManager.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
}
private void _audioCreateContext()
@@ -81,7 +81,7 @@ namespace Robust.Client.Graphics.Clyde
private void _audioOpenDevice()
{
var preferredDevice = _configurationManager.GetCVar(CVars.AudioDevice);
var preferredDevice = _cfg.GetCVar(CVars.AudioDevice);
// Open device.
if (!string.IsNullOrEmpty(preferredDevice))
@@ -183,6 +183,32 @@ namespace Robust.Client.Graphics.Clyde
if (handles.filterHandle != 0) EFX.DeleteFilter(handles.filterHandle);
}
public AudioStream LoadAudioRaw(short[] samples, int channels, int sampleRate)
{
var buffer = AL.GenBuffer();
unsafe
{
fixed (short* ptr = samples)
{
AL.BufferData(
buffer,
channels == 1 ? ALFormat.Mono16 : ALFormat.Stereo16,
(IntPtr) ptr,
samples.Length * 2,
sampleRate);
}
}
_checkAlError();
var handle = new ClydeHandle(_audioSampleBuffers.Count);
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
// ReSharper disable once PossibleLossOfFraction
var length = TimeSpan.FromSeconds(samples.Length / channels / (double) sampleRate);
return new AudioStream(handle, length, channels);
}
public void SetMasterVolume(float newVolume)
{
AL.Listener(ALListenerf.Gain, _baseGain * newVolume);
@@ -439,6 +465,21 @@ namespace Robust.Client.Graphics.Clyde
_checkAlError();
}
public void SetVolumeDirect(float decibels)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = decibels;
AL.Source(SourceHandle, ALSourcef.Gain, _gain * priorOcclusion);
_checkAlError();
}
public void SetOcclusion(float blocks)
{
_checkDisposed();
@@ -482,7 +523,7 @@ namespace Robust.Client.Graphics.Clyde
var (x, y) = position;
if (!ValidatePosition(x, y))
if (!AreFinite(x, y))
{
return false;
}
@@ -503,7 +544,7 @@ namespace Robust.Client.Graphics.Clyde
return true;
}
private static bool ValidatePosition(float x, float y)
private static bool AreFinite(float x, float y)
{
if (float.IsFinite(x) && float.IsFinite(y))
{
@@ -513,6 +554,22 @@ namespace Robust.Client.Graphics.Clyde
return false;
}
public void SetVelocity(Vector2 velocity)
{
_checkDisposed();
var (x, y) = velocity;
if (!AreFinite(x, y))
{
return;
}
AL.Source(SourceHandle, ALSource3f.Velocity, x, y, 0);
_checkAlError();
}
public void SetPitch(float pitch)
{
_checkDisposed();
@@ -667,7 +724,6 @@ namespace Robust.Client.Graphics.Clyde
_checkAlError();
}
private void SetOcclusionEfx(float gain, float cutoff)
{
if (FilterHandle == 0)
@@ -694,7 +750,7 @@ namespace Robust.Client.Graphics.Clyde
var (x, y) = position;
if (!ValidatePosition(x, y))
if (!AreFinite(x, y))
{
return false;
}
@@ -706,7 +762,7 @@ namespace Robust.Client.Graphics.Clyde
return true;
}
private static bool ValidatePosition(float x, float y)
private static bool AreFinite(float x, float y)
{
if (float.IsFinite(x) && float.IsFinite(y))
{
@@ -716,6 +772,37 @@ namespace Robust.Client.Graphics.Clyde
return false;
}
public void SetVelocity(Vector2 velocity)
{
_checkDisposed();
var (x, y) = velocity;
if (!AreFinite(x, y))
{
return;
}
AL.Source(SourceHandle!.Value, ALSource3f.Velocity, x, y, 0);
_checkAlError();
}
public void SetVolumeDirect(float masterVolumeDecay)
{
_checkDisposed();
var priorOcclusion = 1f;
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle!.Value, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
}
_gain = masterVolumeDecay;
AL.Source(SourceHandle!.Value, ALSourcef.Gain, _gain * priorOcclusion);
_checkAlError();
}
public void SetPitch(float pitch)
{
_checkDisposed();
@@ -739,7 +826,7 @@ namespace Robust.Client.Graphics.Clyde
{
if (SourceHandle == null) return;
if (!disposing || Thread.CurrentThread != _master._mainThread)
if (!disposing || Thread.CurrentThread != _master._gameThread)
{
// We can't run this code inside another thread so tell Clyde to clear it up later.
_master.DeleteBufferedSourceOnMainThread(SourceHandle.Value, FilterHandle);

View File

@@ -1,143 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Client.Utility;
using Robust.Shared.Maths;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using GlfwImage = OpenToolkit.GraphicsLibraryFramework.Image;
namespace Robust.Client.Graphics.Clyde
{
internal sealed partial class Clyde
{
// These are actually Cursor* but we can't do that because no pointer generic arguments.
// Need a queue to dispose cursors since the GLFW methods aren't allowed from non-main thread (finalizers).
// And they also aren't re-entrant.
private readonly ConcurrentQueue<IntPtr> _cursorDisposeQueue = new();
private readonly Dictionary<StandardCursorShape, CursorImpl> _standardCursors =
new();
// Keep current active cursor around so it doesn't get garbage collected.
private CursorImpl? _currentCursor;
public ICursor GetStandardCursor(StandardCursorShape shape)
{
return _standardCursors[shape];
}
public unsafe ICursor CreateCursor(Image<Rgba32> image, Vector2i hotSpot)
{
fixed (Rgba32* pixPtr = image.GetPixelSpan())
{
var gImg = new GlfwImage(image.Width, image.Height, (byte*) pixPtr);
var (hotX, hotY) = hotSpot;
var ptr = GLFW.CreateCursor(gImg, hotX, hotY);
return new CursorImpl(this, ptr, false);
}
}
public unsafe void SetCursor(ICursor? cursor)
{
if (_currentCursor == cursor)
{
// Nothing has to be done!
return;
}
if (cursor == null)
{
_currentCursor = null;
GLFW.SetCursor(_glfwWindow, null);
return;
}
if (!(cursor is CursorImpl impl) || impl.Owner != this)
{
throw new ArgumentException("Cursor is not created by this clyde instance.");
}
if (impl.Cursor == null)
{
throw new ObjectDisposedException(nameof(cursor));
}
_currentCursor = impl;
GLFW.SetCursor(_glfwWindow, impl.Cursor);
}
private unsafe void FlushCursorDispose()
{
while (_cursorDisposeQueue.TryDequeue(out var cursor))
{
var ptr = (Cursor*) cursor;
if (_currentCursor != null && ptr == _currentCursor.Cursor)
{
// Currently active cursor getting disposed.
_currentCursor = null;
}
GLFW.DestroyCursor(ptr);
}
}
private void InitCursors()
{
unsafe void AddStandardCursor(StandardCursorShape standardShape, CursorShape shape)
{
var ptr = GLFW.CreateStandardCursor(shape);
var impl = new CursorImpl(this, ptr, true);
_standardCursors.Add(standardShape, impl);
}
AddStandardCursor(StandardCursorShape.Arrow, CursorShape.Arrow);
AddStandardCursor(StandardCursorShape.IBeam, CursorShape.IBeam);
AddStandardCursor(StandardCursorShape.Crosshair, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.Hand, CursorShape.Hand);
AddStandardCursor(StandardCursorShape.HResize, CursorShape.HResize);
AddStandardCursor(StandardCursorShape.VResize, CursorShape.VResize);
}
private sealed class CursorImpl : ICursor
{
private readonly bool _standard;
public Clyde Owner { get; }
public unsafe Cursor* Cursor { get; private set; }
public unsafe CursorImpl(Clyde clyde, Cursor* pointer, bool standard)
{
_standard = standard;
Owner = clyde;
Cursor = pointer;
}
~CursorImpl()
{
DisposeImpl();
}
private unsafe void DisposeImpl()
{
Owner._cursorDisposeQueue.Enqueue((IntPtr) Cursor);
Cursor = null;
}
public void Dispose()
{
if (_standard)
{
throw new InvalidOperationException("Can't dispose standard cursor shape.");
}
GC.SuppressFinalize(this);
DisposeImpl();
}
}
}
}

View File

@@ -0,0 +1,172 @@
// Makes switching easier.
#if EXCEPTION_TOLERANCE
#define EXCEPTION_TOLERANCE_LOCAL
using System;
using Robust.Shared.Log;
#endif
using System.Collections.Generic;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.Input;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics.Clyde
{
internal sealed partial class Clyde
{
// To avoid re-entrancy bollocks we need ANOTHER queue here to actually dispatch our raw input events.
// Yes, on top of the two queues inside the GLFW impl.
// Because the GLFW-impl queue has to get flushed to avoid deadlocks on window creation
// which is ALSO where key events get raised from in a re-entrant fashion. Yay.
private readonly Queue<DEventBase> _eventDispatchQueue = new();
private void DispatchEvents()
{
while (_eventDispatchQueue.TryDequeue(out var ev))
{
#if EXCEPTION_TOLERANCE_LOCAL
try
#endif
{
DispatchSingleEvent(ev);
}
#if EXCEPTION_TOLERANCE_LOCAL
catch (Exception e)
{
Logger.ErrorS("clyde.win", $"Error dispatching window event {ev.GetType().Name}:\n{e}");
}
#endif
}
}
private void DispatchSingleEvent(DEventBase ev)
{
switch (ev)
{
case DEventKeyDown(var args):
KeyDown?.Invoke(args);
break;
case DEventKeyUp(var args):
KeyUp?.Invoke(args);
break;
case DEventMouseMove(var args):
MouseMove?.Invoke(args);
break;
case DEventMouseEnterLeave(var args):
MouseEnterLeave?.Invoke(args);
break;
case DEventScroll(var args):
MouseWheel?.Invoke(args);
break;
case DEventText(var args):
TextEntered?.Invoke(args);
break;
case DEventWindowClosed(var reg, var args):
reg.Closed?.Invoke(args);
CloseWindow?.Invoke(args);
if (reg.DisposeOnClose && !reg.IsMainWindow)
DoDestroyWindow(reg);
break;
case DEventWindowContentScaleChanged(var args):
OnWindowScaleChanged?.Invoke(args);
break;
case DEventWindowFocus(var args):
OnWindowFocused?.Invoke(args);
break;
case DEventWindowResized(var args):
OnWindowResized?.Invoke(args);
break;
}
}
private void SendKeyUp(KeyEventArgs ev)
{
_eventDispatchQueue.Enqueue(new DEventKeyUp(ev));
}
private void SendKeyDown(KeyEventArgs ev)
{
_eventDispatchQueue.Enqueue(new DEventKeyDown(ev));
}
private void SendScroll(MouseWheelEventArgs ev)
{
_eventDispatchQueue.Enqueue(new DEventScroll(ev));
}
private void SendCloseWindow(WindowReg windowReg, WindowClosedEventArgs ev)
{
_eventDispatchQueue.Enqueue(new DEventWindowClosed(windowReg, ev));
}
private void SendWindowResized(WindowReg reg, Vector2i oldSize)
{
if (reg.IsMainWindow)
{
UpdateMainWindowLoadedRtSize();
GL.Viewport(0, 0, reg.FramebufferSize.X, reg.FramebufferSize.Y);
CheckGlError();
}
else
{
reg.RenderTexture!.Dispose();
CreateWindowRenderTexture(reg);
}
var eventArgs = new WindowResizedEventArgs(
oldSize,
reg.FramebufferSize,
reg.Handle);
_eventDispatchQueue.Enqueue(new DEventWindowResized(eventArgs));
}
private void SendWindowContentScaleChanged(WindowContentScaleEventArgs ev)
{
_eventDispatchQueue.Enqueue(new DEventWindowContentScaleChanged(ev));
}
private void SendWindowFocus(WindowFocusedEventArgs ev)
{
_eventDispatchQueue.Enqueue(new DEventWindowFocus(ev));
}
private void SendText(TextEventArgs ev)
{
_eventDispatchQueue.Enqueue(new DEventText(ev));
}
private void SendMouseMove(MouseMoveEventArgs ev)
{
_eventDispatchQueue.Enqueue(new DEventMouseMove(ev));
}
private void SendMouseEnterLeave(MouseEnterLeaveEventArgs ev)
{
_eventDispatchQueue.Enqueue(new DEventMouseEnterLeave(ev));
}
// D stands for Dispatch
private abstract record DEventBase;
private sealed record DEventKeyUp(KeyEventArgs Args) : DEventBase;
private sealed record DEventKeyDown(KeyEventArgs Args) : DEventBase;
private sealed record DEventScroll(MouseWheelEventArgs Args) : DEventBase;
private sealed record DEventWindowClosed(WindowReg Reg, WindowClosedEventArgs Args) : DEventBase;
private sealed record DEventWindowResized(WindowResizedEventArgs Args) : DEventBase;
private sealed record DEventWindowContentScaleChanged(WindowContentScaleEventArgs Args) : DEventBase;
private sealed record DEventWindowFocus(WindowFocusedEventArgs Args) : DEventBase;
private sealed record DEventText(TextEventArgs Args) : DEventBase;
private sealed record DEventMouseMove(MouseMoveEventArgs Args) : DEventBase;
private sealed record DEventMouseEnterLeave(MouseEnterLeaveEventArgs Args) : DEventBase;
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Log;
@@ -57,7 +57,7 @@ namespace Robust.Client.Graphics.Clyde
{
var extensions = GetGLExtensions();
Logger.DebugS("clyde.ogl", "OpenGL capabilities:");
_sawmillOgl.Debug("OpenGL capabilities:");
if (!_isGLES)
{
@@ -81,7 +81,7 @@ namespace Robust.Client.Graphics.Clyde
{
// We're ES <3.2, KHR_debug is extension and needs KHR suffixes.
_isGLKhrDebugESExtension = true;
Logger.DebugS("clyde.ogl", " khr_debug is ES extension!");
_sawmillOgl.Debug(" khr_debug is ES extension!");
}
CheckGLCap(ref _hasGLVertexArrayObject, "vertex_array_object", (3, 0));
@@ -104,7 +104,7 @@ namespace Robust.Client.Graphics.Clyde
// This is 3.2 or extensions
_hasGLFloatFramebuffers = !_isGLES;
Logger.DebugS("clyde.ogl", $" GLES: {_isGLES}");
_sawmillOgl.Debug($" GLES: {_isGLES}");
void CheckGLCap(ref bool cap, string capName, (int major, int minor)? versionMin = null,
params string[] exts)
@@ -115,15 +115,15 @@ namespace Robust.Client.Graphics.Clyde
var prev = cap;
var cVarName = $"display.ogl_block_{capName}";
var block = _configurationManager.GetCVar<bool>(cVarName);
var block = _cfg.GetCVar<bool>(cVarName);
if (block)
{
cap = false;
Logger.DebugS("clyde.ogl", $" {cVarName} SET, BLOCKING {capName} (was: {prev})");
_sawmillOgl.Debug($" {cVarName} SET, BLOCKING {capName} (was: {prev})");
}
Logger.DebugS("clyde.ogl", $" {capName}: {cap}");
_sawmillOgl.Debug($" {capName}: {cap}");
}
}
@@ -146,7 +146,7 @@ namespace Robust.Client.Graphics.Clyde
foreach (var cvar in cvars)
{
_configurationManager.RegisterCVar($"display.ogl_block_{cvar}", false);
_cfg.RegisterCVar($"display.ogl_block_{cvar}", false);
}
}
@@ -168,14 +168,14 @@ namespace Robust.Client.Graphics.Clyde
extensionsText += extension;
extensions.Add(extension);
}
Logger.DebugS("clyde.ogl", "OpenGL Extensions: {0}", extensionsText);
_sawmillOgl.Debug("OpenGL Extensions: {0}", extensionsText);
return extensions;
}
else
{
// GLES uses the (old?) API
var extensions = GL.GetString(StringName.Extensions);
Logger.DebugS("clyde.ogl", "OpenGL Extensions: {0}", extensions);
_sawmillOgl.Debug("OpenGL Extensions: {0}", extensions);
return new HashSet<string>(extensions.Split(' '));
}
}

View File

@@ -208,12 +208,12 @@ namespace Robust.Client.Graphics.Clyde
_setChunkDirty(grid, chunk);
}
private void _updateOnGridCreated(GridId gridId)
private void _updateOnGridCreated(MapId mapId, GridId gridId)
{
_mapChunkData.Add(gridId, new Dictionary<Vector2i, MapChunkData>());
}
private void _updateOnGridRemoved(GridId gridId)
private void _updateOnGridRemoved(MapId mapId, GridId gridId)
{
var data = _mapChunkData[gridId];
foreach (var chunkDatum in data.Values)

View File

@@ -8,6 +8,7 @@ using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Enums;
namespace Robust.Client.Graphics.Clyde
@@ -24,18 +25,28 @@ namespace Robust.Client.Graphics.Clyde
=
new();
public void Render()
public unsafe void Render()
{
CheckTransferringScreenshots();
var allMinimized = true;
foreach (var windowReg in _windowing!.AllWindows)
{
if (!windowReg.IsMinimized)
{
allMinimized = false;
break;
}
}
var size = ScreenSize;
if (size.X == 0 || size.Y == 0 || _isMinimized)
if (size.X == 0 || size.Y == 0 || allMinimized)
{
ClearFramebuffer(Color.Black);
// We have to keep running swapbuffers here
// or else the user's PC will turn into a heater!!
SwapBuffers();
SwapMainBuffers();
return;
}
@@ -51,7 +62,7 @@ namespace Robust.Client.Graphics.Clyde
ClearFramebuffer(Color.Black);
// Update shared UBOs.
_updateUniformConstants(_framebufferSize);
_updateUniformConstants(_windowing.MainWindow!.FramebufferSize);
{
CalcScreenMatrices(ScreenSize, out var proj, out var view);
@@ -63,103 +74,133 @@ namespace Robust.Client.Graphics.Clyde
{
DrawSplash(_renderHandle);
FlushRenderQueue();
SwapBuffers();
SwapMainBuffers();
return;
}
RenderOverlays(OverlaySpace.ScreenSpaceBelowWorld);
_mainViewport.Eye = _eyeManager.CurrentEye;
RenderViewport(_mainViewport); //Worldspace overlays are rendered here.
foreach (var weak in _viewports.Values)
{
var handle = _renderHandle.DrawingHandleScreen;
var tex = _mainViewport.RenderTarget.Texture;
handle.DrawTexture(tex, (0, 0));
FlushRenderQueue();
if (weak.TryGetTarget(out var viewport) && viewport.AutomaticRender)
RenderViewport(viewport);
}
TakeScreenshot(ScreenshotType.BeforeUI);
RenderOverlays(OverlaySpace.ScreenSpace);
using (DebugGroup("UI"))
{
_userInterfaceManager.Render(_renderHandle);
FlushRenderQueue();
}
TakeScreenshot(ScreenshotType.AfterUI);
TakeScreenshot(ScreenshotType.Final);
BlitSecondaryWindows();
// And finally, swap those buffers!
SwapBuffers();
SwapMainBuffers();
}
private void RenderOverlays(OverlaySpace space)
private void RenderOverlays(Viewport vp, OverlaySpace space, in Box2 worldBox)
{
using (DebugGroup($"Overlays: {space}"))
{
var list = new List<Overlay>();
foreach (var overlay in _overlayManager.AllOverlays)
var list = GetOverlaysForSpace(space);
foreach (var overlay in list)
{
if ((overlay.Space & space) != 0)
if (overlay.RequestScreenTexture)
{
list.Add(overlay);
}
}
FlushRenderQueue();
list.Sort(OverlayComparer.Instance);
foreach (var overlay in list) {
if (overlay.RequestScreenTexture) {
FlushRenderQueue();
UpdateOverlayScreenTexture(space, _mainViewport.RenderTarget);
UpdateOverlayScreenTexture(space, vp.RenderTarget);
}
if (overlay.OverwriteTargetFrameBuffer()) {
if (overlay.OverwriteTargetFrameBuffer())
{
ClearFramebuffer(default);
}
overlay.ClydeRender(_renderHandle, space);
FlushRenderQueue();
overlay.ClydeRender(_renderHandle, space, null, vp, new UIBox2i((0, 0), vp.Size), worldBox);
}
}
}
private void RenderOverlaysDirect(
Viewport vp,
IViewportControl vpControl,
DrawingHandleBase handle,
OverlaySpace space,
in UIBox2i bounds)
{
var list = GetOverlaysForSpace(space);
var worldBounds = CalcWorldBounds(vp);
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, worldBounds);
foreach (var overlay in list)
{
overlay.Draw(args);
}
}
private List<Overlay> GetOverlaysForSpace(OverlaySpace space)
{
var list = new List<Overlay>();
foreach (var overlay in _overlayManager.AllOverlays)
{
if ((overlay.Space & space) != 0)
{
list.Add(overlay);
}
}
list.Sort(OverlayComparer.Instance);
return list;
}
private ClydeTexture? ScreenBufferTexture;
private GLHandle screenBufferHandle;
private Vector2 lastFrameSize;
/// <summary>
/// Sends SCREEN_TEXTURE to all overlays in the given OverlaySpace that request it.
/// </summary>
private bool UpdateOverlayScreenTexture(OverlaySpace space, RenderTexture texture) {
private bool UpdateOverlayScreenTexture(OverlaySpace space, RenderTexture texture)
{
//This currently does NOT consider viewports and just grabs the current screen framebuffer. This will need to be improved upon in the future.
List<Overlay> oTargets = new List<Overlay>();
foreach (var overlay in _overlayManager.AllOverlays) {
if (overlay.RequestScreenTexture && overlay.Space == space) {
foreach (var overlay in _overlayManager.AllOverlays)
{
if (overlay.RequestScreenTexture && overlay.Space == space)
{
oTargets.Add(overlay);
}
}
if (oTargets.Count > 0 && ScreenBufferTexture != null) {
if (lastFrameSize != _framebufferSize) {
if (oTargets.Count > 0 && ScreenBufferTexture != null)
{
if (lastFrameSize != texture.Size)
{
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Srgb8Alpha8, _framebufferSize.X, _framebufferSize.Y, 0,
GL.TexImage2D(TextureTarget.Texture2D, 0,
_hasGLSrgb ? PixelInternalFormat.Srgb8Alpha8 : PixelInternalFormat.Rgba8, texture.Size.X,
texture.Size.Y, 0,
PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
}
lastFrameSize = _framebufferSize;
lastFrameSize = texture.Size;
CopyRenderTextureToTexture(texture, ScreenBufferTexture);
foreach (Overlay overlay in oTargets) {
foreach (Overlay overlay in oTargets)
{
overlay.ScreenTexture = ScreenBufferTexture;
}
oTargets.Clear();
return true;
}
return false;
}
private void DrawEntities(Viewport viewport, Box2 worldBounds)
{
if (_eyeManager.CurrentMap == MapId.Nullspace || !_mapManager.HasMapEntity(_eyeManager.CurrentMap))
@@ -167,7 +208,7 @@ namespace Robust.Client.Graphics.Clyde
return;
}
RenderOverlays(OverlaySpace.WorldSpaceBelowEntities);
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowEntities, worldBounds);
var screenSize = viewport.Size;
@@ -218,7 +259,13 @@ namespace Robust.Client.Graphics.Clyde
flushed = true;
}
overlay.ClydeRender(_renderHandle, OverlaySpace.WorldSpace);
overlay.ClydeRender(
_renderHandle,
OverlaySpace.WorldSpace,
null,
viewport,
new UIBox2i((0, 0), viewport.Size),
worldBounds);
overlayIndex = j;
continue;
}
@@ -237,13 +284,13 @@ namespace Robust.Client.Graphics.Clyde
var spriteRT = spriteBB.TopRight;
// finally we can calculate screen bounding in pixels
var screenLB = _eyeManager.WorldToScreen(spriteLB);
var screenRT = _eyeManager.WorldToScreen(spriteRT);
var screenLB = viewport.WorldToLocal(spriteLB);
var screenRT = viewport.WorldToLocal(spriteRT);
// we need to scale RT a for effects like emission or highlight
// scale can be passed with PostShader as variable in future
var postShadeScale = 1.25f;
var screenSpriteSize = (Vector2i)((screenRT - screenLB) * postShadeScale).Rounded();
var screenSpriteSize = (Vector2i) ((screenRT - screenLB) * postShadeScale).Rounded();
screenSpriteSize.Y = -screenSpriteSize.Y;
// I'm not 100% sure why it works, but without it post-shader
@@ -268,8 +315,8 @@ namespace Robust.Client.Graphics.Clyde
// which is necessary for light application,
// but it's ACTUALLY drawing into the center of the render target.
var spritePos = spriteBB.Center;
var screenPos = _eyeManager.WorldToScreen(spritePos);
var (roundedX, roundedY) = roundedPos = (Vector2i)screenPos;
var screenPos = viewport.WorldToLocal(spritePos);
var (roundedX, roundedY) = roundedPos = (Vector2i) screenPos;
var flippedPos = new Vector2i(roundedX, screenSize.Y - roundedY);
flippedPos -= entityPostRenderTarget.Size / 2;
_renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize));
@@ -303,6 +350,9 @@ namespace Robust.Client.Graphics.Clyde
_renderHandle.SetProjView(oldProj, oldView);
_renderHandle.UseShader(null);
// TODO: cache this properly across frames.
entityPostRenderTarget.DisposeDeferred();
}
}
@@ -318,29 +368,44 @@ namespace Robust.Client.Graphics.Clyde
{
var spriteSystem = _entitySystemManager.GetEntitySystem<RenderingTreeSystem>();
var tree = spriteSystem.GetSpriteTreeForMap(map);
tree.QueryAabb(ref list, ((
ref RefList<(SpriteComponent sprite, Matrix3 matrix, Angle worldRot, float yWorldPos)> state,
in SpriteComponent value) =>
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map, worldBounds, true))
{
if (value.ContainerOccluded || !value.Visible)
Box2 gridBounds;
if (gridId == GridId.Invalid)
{
return true;
gridBounds = worldBounds;
}
else
{
gridBounds = worldBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
}
var entity = value.Owner;
var transform = entity.Transform;
var tree = spriteSystem.GetSpriteTreeForMap(map, gridId);
ref var entry = ref state.AllocAdd();
entry.sprite = value;
entry.worldRot = transform.WorldRotation;
entry.matrix = transform.WorldMatrix;
var worldPos = entry.matrix.Transform(transform.LocalPosition);
entry.yWorldPos = worldPos.Y;
return true;
tree.QueryAabb(ref list, ((
ref RefList<(SpriteComponent sprite, Matrix3 matrix, Angle worldRot, float yWorldPos)> state,
in SpriteComponent value) =>
{
// TODO: Probably value in storing this as its own DynamicTree
if (value.ContainerOccluded || !value.Visible)
{
return true;
}
}), worldBounds, approx: true);
var entity = value.Owner;
var transform = entity.Transform;
ref var entry = ref state.AllocAdd();
entry.sprite = value;
entry.worldRot = transform.WorldRotation;
entry.matrix = transform.WorldMatrix;
var worldPos = entry.matrix.Transform(transform.LocalPosition);
entry.yWorldPos = worldPos.Y;
return true;
}), gridBounds, approx: true);
}
}
private void DrawSplash(IRenderHandle handle)
@@ -350,13 +415,8 @@ namespace Robust.Client.Graphics.Clyde
handle.DrawingHandleScreen.DrawTexture(texture, (ScreenSize - texture.Size) / 2);
}
private void RenderViewport(Viewport viewport)
private void RenderInRenderTarget(RenderTargetBase rt, Action a)
{
if (viewport.Eye == null)
{
return;
}
// TODO: for the love of god all this state pushing/popping needs to be cleaned up.
var oldTransform = _currentMatrixModel;
@@ -365,28 +425,54 @@ namespace Robust.Client.Graphics.Clyde
// Have to flush the render queue so that all commands finish rendering to the previous framebuffer.
FlushRenderQueue();
var eye = viewport.Eye;
var oldVp = _currentViewport;
_currentViewport = viewport;
var state = PushRenderStateFull();
{
// Actual code that isn't just pushing/popping renderer state so we can return safely.
var rt = _currentViewport.RenderTarget;
BindRenderTargetFull(RtToLoaded(rt));
ClearFramebuffer(default);
SetViewportImmediate(Box2i.FromDimensions(Vector2i.Zero, rt.Size));
_updateUniformConstants(viewport.Size);
_updateUniformConstants(rt.Size);
CalcScreenMatrices(rt.Size, out var proj, out var view);
SetProjViewFull(proj, view);
CalcWorldMatrices(rt.Size, eye, out var proj, out var view);
// Smugleaf moment
a();
FlushRenderQueue();
}
FenceRenderTarget(rt);
PopRenderStateFull(state);
_updateUniformConstants(_currentRenderTarget.Size);
SetScissorFull(oldScissor);
_currentMatrixModel = oldTransform;
}
private void RenderViewport(Viewport viewport)
{
if (viewport.Eye == null || viewport.Eye.Position.MapId == MapId.Nullspace)
{
return;
}
RenderInRenderTarget(viewport.RenderTarget, () =>
{
using var _ = DebugGroup($"Viewport: {viewport.Name}");
var oldVp = _currentViewport;
_currentViewport = viewport;
var eye = viewport.Eye;
// Actual code that isn't just pushing/popping renderer state so we can return safely.
CalcWorldMatrices(viewport.RenderTarget.Size, viewport.RenderScale, eye, out var proj, out var view);
SetProjViewFull(proj, view);
// Calculate world-space AABB for camera, to cull off-screen things.
var worldBounds = Box2.CenteredAround(eye.Position.Position,
_framebufferSize / (float) EyeManager.PixelsPerMeter * eye.Zoom);
var worldBounds = CalcWorldBounds(viewport);
if (_eyeManager.CurrentMap != MapId.Nullspace)
{
@@ -395,6 +481,9 @@ namespace Robust.Client.Graphics.Clyde
DrawLightsAndFov(viewport, worldBounds, eye);
}
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowWorld, worldBounds);
FlushRenderQueue();
using (DebugGroup("Grids"))
{
_drawGrids(worldBounds);
@@ -406,18 +495,11 @@ namespace Robust.Client.Graphics.Clyde
DrawEntities(viewport, worldBounds);
}
RenderOverlays(OverlaySpace.WorldSpaceBelowFOV);
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldBounds);
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
{
GL.Clear(ClearBufferMask.StencilBufferBit);
GL.Enable(EnableCap.StencilTest);
GL.StencilOp(OpenToolkit.Graphics.OpenGL4.StencilOp.Keep, OpenToolkit.Graphics.OpenGL4.StencilOp.Keep, OpenToolkit.Graphics.OpenGL4.StencilOp.Replace);
GL.StencilFunc(StencilFunction.Always, 1, 0xFF);
GL.StencilMask(0xFF);
ApplyFovToBuffer(viewport, eye);
GL.StencilMask(0x00);
GL.Disable(EnableCap.StencilTest);
}
}
@@ -433,7 +515,7 @@ namespace Robust.Client.Graphics.Clyde
// So there are distortions from incorrect projection.
_renderHandle.UseShader(_fovDebugShaderInstance);
_renderHandle.DrawingHandleScreen.SetTransform(Matrix3.Identity);
var pos = UIBox2.FromDimensions(ScreenSize / 2 - (200, 200), (400, 400));
var pos = UIBox2.FromDimensions(viewport.Size / 2 - (200, 200), (400, 400));
_renderHandle.DrawingHandleScreen.DrawTextureRect(FovTexture, pos);
}
@@ -443,24 +525,25 @@ namespace Robust.Client.Graphics.Clyde
_renderHandle.DrawingHandleScreen.SetTransform(Matrix3.Identity);
_renderHandle.DrawingHandleScreen.DrawTextureRect(
viewport.WallBleedIntermediateRenderTarget2.Texture,
UIBox2.FromDimensions(Vector2.Zero, ScreenSize), new Color(1, 1, 1, 0.5f));
UIBox2.FromDimensions(Vector2.Zero, viewport.Size), new Color(1, 1, 1, 0.5f));
}
RenderOverlays(viewport, OverlaySpace.WorldSpace, worldBounds);
FlushRenderQueue();
RenderOverlays(OverlaySpace.WorldSpace);
_currentViewport = oldVp;
});
}
GL.StencilFunc(StencilFunction.Notequal, 1, 0xFF);
GL.Disable(EnableCap.DepthTest);
RenderOverlays(OverlaySpace.WorldSpaceFOVStencil);
GL.Disable(EnableCap.StencilTest);
}
private static Box2 CalcWorldBounds(Viewport viewport)
{
var eye = viewport.Eye;
if (eye == null)
return default;
PopRenderStateFull(state);
_updateUniformConstants(oldVp?.Size ?? _framebufferSize);
SetScissorFull(oldScissor);
_currentMatrixModel = oldTransform;
_currentViewport = oldVp;
// TODO: This seems completely unfit by lacking things like rotation handling.
return Box2.CenteredAround(eye.Position.Position,
viewport.Size / viewport.RenderScale / EyeManager.PixelsPerMeter * eye.Zoom);
}
private sealed class OverlayComparer : IComparer<Overlay>

View File

@@ -47,7 +47,7 @@ namespace Robust.Client.Graphics.Clyde
GL.BindTexture(TextureTarget.Texture2D, _loadedTextures[target.TextureId].OpenGLObject.Handle);
CheckGlError();
GL.CopyTexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, 0, 0, _framebufferSize.X, _framebufferSize.Y);
GL.CopyTexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, 0, 0, source.Size.X, source.Size.Y);
CheckGlError();
if (pause && store != null) {
@@ -160,12 +160,12 @@ namespace Robust.Client.Graphics.Clyde
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void CheckGlErrorInternal(string? path, int line)
private void CheckGlErrorInternal(string? path, int line)
{
var err = GL.GetError();
if (err != ErrorCode.NoError)
{
Logger.ErrorS("clyde.ogl", $"OpenGL error: {err} at {path}:{line}\n{Environment.StackTrace}");
_sawmillOgl.Error($"OpenGL error: {err} at {path}:{line}\n{Environment.StackTrace}");
}
}
@@ -276,7 +276,7 @@ namespace Robust.Client.Graphics.Clyde
private void LoadGLProc<T>(string name, out T field) where T : Delegate
{
var proc = _graphicsContext.GetProcAddress(name);
var proc = _windowing!.GraphicsBindingContext.GetProcAddress(name);
if (proc == IntPtr.Zero || proc == new IntPtr(1) || proc == new IntPtr(2))
{
throw new InvalidOperationException($"Unable to load GL function '{name}'!");

View File

@@ -10,6 +10,7 @@ using Robust.Shared.Map;
using Robust.Shared.Maths;
using static Robust.Client.GameObjects.ClientOccluderComponent;
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
namespace Robust.Client.Graphics.Clyde
{
@@ -195,7 +196,6 @@ namespace Robust.Client.Graphics.Clyde
}
}
_lightSoftShaderHandle = LoadShaderHandle("/Shaders/Internal/light-soft.swsl");
_lightHardShaderHandle = LoadShaderHandle("/Shaders/Internal/light-hard.swsl");
_fovShaderHandle = LoadShaderHandle("/Shaders/Internal/fov.swsl");
@@ -361,16 +361,23 @@ namespace Robust.Client.Graphics.Clyde
FinalizeDepthDraw();
}
BindRenderTargetImmediate(RtToLoaded(viewport.LightRenderTarget));
CheckGlError();
GLClearColor(_lightManager.AmbientLightColor);
GL.Clear(ClearBufferMask.ColorBufferBit);
CheckGlError();
GL.Enable(EnableCap.StencilTest);
_isStencilling = true;
var (lightW, lightH) = GetLightMapSize(viewport.Size);
GL.Viewport(0, 0, lightW, lightH);
CheckGlError();
BindRenderTargetImmediate(RtToLoaded(viewport.LightRenderTarget));
CheckGlError();
GLClearColor(_lightManager.AmbientLightColor);
GL.ClearStencil(0xFF);
GL.StencilMask(0xFF);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
CheckGlError();
ApplyLightingFovToBuffer(viewport, eye);
var lightShader = _loadedShaders[_enableSoftShadows ? _lightSoftShaderHandle : _lightHardShaderHandle].Program;
lightShader.Use();
@@ -382,6 +389,11 @@ namespace Robust.Client.Graphics.Clyde
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
CheckGlError();
GL.StencilFunc(StencilFunction.Equal, 0xFF, 0xFF);
CheckGlError();
GL.StencilOp(TKStencilOp.Keep, TKStencilOp.Keep, TKStencilOp.Keep);
CheckGlError();
var lastRange = float.NaN;
var lastPower = float.NaN;
var lastColor = new Color(float.NaN, float.NaN, float.NaN, float.NaN);
@@ -463,11 +475,11 @@ namespace Robust.Client.Graphics.Clyde
}
ResetBlendFunc();
GL.Disable(EnableCap.StencilTest);
_isStencilling = false;
CheckGlError();
ApplyLightingFovToBuffer(viewport, eye);
BlurOntoWalls(viewport, eye);
MergeWallLayer(viewport);
@@ -485,40 +497,55 @@ namespace Robust.Client.Graphics.Clyde
GetLightsToRender(MapId map, in Box2 worldBounds)
{
var renderingTreeSystem = _entitySystemManager.GetEntitySystem<RenderingTreeSystem>();
var lightTree = renderingTreeSystem.GetLightTreeForMap(map);
var state = (this, worldBounds, count: 0);
lightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldBounds, int count) state, in PointLightComponent light) =>
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map, worldBounds, true))
{
var transform = light.Owner.Transform;
Box2 gridBounds;
if (state.count >= LightsToRenderListSize)
if (gridId == GridId.Invalid)
{
// There are too many lights to fit in the static memory.
return false;
gridBounds = worldBounds;
}
else
{
gridBounds = worldBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
}
if (!light.Enabled || light.ContainerOccluded)
var lightTree = renderingTreeSystem.GetLightTreeForMap(map, gridId);
lightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldBounds, int count) state, in PointLightComponent light) =>
{
var transform = light.Owner.Transform;
if (state.count >= LightsToRenderListSize)
{
// There are too many lights to fit in the static memory.
return false;
}
// TODO: Don't insert into trees for these, same as sprites.
if (!light.Enabled || light.ContainerOccluded)
{
return true;
}
var lightPos = transform.WorldMatrix.Transform(light.Offset);
var circle = new Circle(lightPos, light.Radius);
// If the light doesn't touch anywhere the camera can see, it doesn't matter.
if (!circle.Intersects(state.worldBounds))
{
return true;
}
float distanceSquared = (state.worldBounds.Center - lightPos).LengthSquared;
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
return true;
}
var lightPos = transform.WorldMatrix.Transform(light.Offset);
var circle = new Circle(lightPos, light.Radius);
// If the light doesn't touch anywhere the camera can see, it doesn't matter.
if (!circle.Intersects(state.worldBounds))
{
return true;
}
float distanceSquared = (state.worldBounds.Center - lightPos).LengthSquared;
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
return true;
}, worldBounds);
}, gridBounds);
}
if (state.count > _maxLightsPerScene)
{
@@ -577,7 +604,7 @@ namespace Robust.Client.Graphics.Clyde
// Have to scale the blurring radius based on viewport size and camera zoom.
const float refCameraHeight = 14;
var cameraSize = eye.Zoom.Y * viewport.Size.Y / EyeManager.PixelsPerMeter;
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 = 7e-3f * (refCameraHeight / cameraSize);
@@ -645,6 +672,12 @@ namespace Robust.Client.Graphics.Clyde
private void ApplyFovToBuffer(Viewport viewport, IEye eye)
{
GL.Clear(ClearBufferMask.StencilBufferBit);
GL.Enable(EnableCap.StencilTest);
GL.StencilOp(OpenToolkit.Graphics.OpenGL4.StencilOp.Keep, OpenToolkit.Graphics.OpenGL4.StencilOp.Keep, OpenToolkit.Graphics.OpenGL4.StencilOp.Replace);
GL.StencilFunc(StencilFunction.Always, 1, 0xFF);
GL.StencilMask(0xFF);
// Applies FOV to the final framebuffer.
var fovShader = _loadedShaders[_fovShaderHandle].Program;
@@ -658,6 +691,10 @@ namespace Robust.Client.Graphics.Clyde
fovShader.SetUniformMaybe("center", eye.Position.Position);
DrawBlit(viewport, fovShader);
GL.StencilMask(0x00);
GL.Disable(EnableCap.StencilTest);
_isStencilling = false;
}
private void ApplyLightingFovToBuffer(Viewport viewport, IEye eye)
@@ -690,6 +727,13 @@ namespace Robust.Client.Graphics.Clyde
fovShader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
fovShader.SetUniformMaybe("center", eye.Position.Position);
GL.StencilMask(0xFF);
CheckGlError();
GL.StencilFunc(StencilFunction.Always, 0, 0);
CheckGlError();
GL.StencilOp(TKStencilOp.Keep, TKStencilOp.Keep, TKStencilOp.Replace);
CheckGlError();
DrawBlit(viewport, fovShader);
if (_hasGLSamplerObjects)
@@ -939,7 +983,8 @@ namespace Robust.Client.Graphics.Clyde
viewport.WallMaskRenderTarget = CreateRenderTarget(viewport.Size, RenderTargetColorFormat.R8,
name: $"{viewport.Name}-{nameof(viewport.WallMaskRenderTarget)}");
viewport.LightRenderTarget = CreateRenderTarget(lightMapSize, lightMapColorFormat,
viewport.LightRenderTarget = CreateRenderTarget(lightMapSize,
new RenderTargetFormatParameters(lightMapColorFormat, hasDepthStencil: true),
lightMapSampleParameters,
$"{viewport.Name}-{nameof(viewport.LightRenderTarget)}");
@@ -977,13 +1022,13 @@ namespace Robust.Client.Graphics.Clyde
return (w, h);
}
protected override void LightmapDividerChanged(int newValue)
private void LightmapDividerChanged(int newValue)
{
_lightmapDivider = newValue;
RegenAllLightRts();
}
protected override void MaxLightsPerSceneChanged(int newValue)
private void MaxLightsPerSceneChanged(int newValue)
{
_maxLightsPerScene = newValue;
@@ -1002,7 +1047,7 @@ namespace Robust.Client.Graphics.Clyde
nameof(_shadowRenderTarget));
}
protected override void SoftShadowsChanged(bool newValue)
private void SoftShadowsChanged(bool newValue)
{
_enableSoftShadows = newValue;
}

View File

@@ -112,6 +112,11 @@ namespace Robust.Client.Graphics.Clyde
return clydeTexture;
}
public void RenderInRenderTarget(IRenderTarget target, Action a)
{
_clyde.RenderInRenderTarget((RenderTargetBase) target, a);
}
public void SetScissor(UIBox2i? scissorBox)
{
_clyde.DrawSetScissor(scissorBox);

View File

@@ -6,6 +6,7 @@ using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.Log;
using SixLabors.ImageSharp.PixelFormats;
// ReSharper disable once IdentifierTypo
using RTCF = Robust.Client.Graphics.RenderTargetColorFormat;
@@ -23,9 +24,8 @@ namespace Robust.Client.Graphics.Clyde
private readonly ConcurrentQueue<ClydeHandle> _renderTargetDisposeQueue
= new();
IRenderWindow IClyde.MainWindowRenderTarget => _mainWindowRenderTarget;
// Initialized in Clyde's constructor
private readonly RenderWindow _mainWindowRenderTarget;
private readonly RenderMainWindow _mainMainWindowRenderMainTarget;
// This is always kept up-to-date, except in CreateRenderTarget (because it restores the old value)
// It is used for SRGB emulation.
@@ -97,7 +97,7 @@ namespace Robust.Client.Graphics.Clyde
case RTCF.RG32F:
case RTCF.R11FG11FB10F:
case RTCF.Rgba16F:
Logger.WarningS("clyde.ogl", "The framebuffer {0} [{1}] is trying to be floating-point when that's not supported. Forcing Rgba8.", name == null ? "[unnamed]" : name, size);
_sawmillOgl.Warning("The framebuffer {0} [{1}] is trying to be floating-point when that's not supported. Forcing Rgba8.", name == null ? "[unnamed]" : name, size);
colorFormat = RTCF.Rgba8;
break;
}
@@ -191,7 +191,8 @@ namespace Robust.Client.Graphics.Clyde
FramebufferHandle = fbo,
Size = size,
TextureHandle = textureObject.TextureId,
MemoryPressure = pressure
MemoryPressure = pressure,
ColorFormat = format.ColorFormat
};
//GC.AddMemoryPressure(pressure);
@@ -266,10 +267,10 @@ namespace Robust.Client.Graphics.Clyde
}
}
private void UpdateWindowLoadedRtSize()
private void UpdateMainWindowLoadedRtSize()
{
var loadedRt = RtToLoaded(_mainWindowRenderTarget);
loadedRt.Size = _framebufferSize;
var loadedRt = RtToLoaded(_mainMainWindowRenderMainTarget);
loadedRt.Size = _windowing!.MainWindow!.FramebufferSize;
}
private sealed class LoadedRenderTarget
@@ -278,6 +279,8 @@ namespace Robust.Client.Graphics.Clyde
public Vector2i Size;
public bool IsSrgb;
public RTCF ColorFormat;
// Remaining properties only apply if the render target is NOT a window.
// Handle to the framebuffer object.
public GLHandle FramebufferHandle;
@@ -295,6 +298,9 @@ namespace Robust.Client.Graphics.Clyde
protected readonly Clyde Clyde;
private bool _disposed;
public bool MakeGLFence;
public nint LastGLSync;
protected RenderTargetBase(Clyde clyde, ClydeHandle handle)
{
Clyde = clyde;
@@ -302,6 +308,12 @@ namespace Robust.Client.Graphics.Clyde
}
public abstract Vector2i Size { get; }
public void CopyPixelsToMemory<T>(CopyPixelsDelegate<T> callback, UIBox2i? subRegion = null) where T : unmanaged, IPixel<T>
{
Clyde.CopyRenderTargetPixels(Handle, subRegion, callback);
}
public ClydeHandle Handle { get; }
protected virtual void Dispose(bool disposing)
@@ -320,6 +332,23 @@ namespace Robust.Client.Graphics.Clyde
GC.SuppressFinalize(this);
}
public void DisposeDeferred()
{
if (_disposed)
{
return;
}
_disposed = true;
DisposeDeferredImpl();
GC.SuppressFinalize(this);
}
protected virtual void DisposeDeferredImpl()
{
}
~RenderTargetBase()
{
Dispose(false);
@@ -347,16 +376,21 @@ namespace Robust.Client.Graphics.Clyde
}
else
{
Clyde._renderTargetDisposeQueue.Enqueue(Handle);
DisposeDeferredImpl();
}
}
protected override void DisposeDeferredImpl()
{
Clyde._renderTargetDisposeQueue.Enqueue(Handle);
}
}
private sealed class RenderWindow : RenderTargetBase, IRenderWindow
private sealed class RenderMainWindow : RenderTargetBase
{
public override Vector2i Size => Clyde._framebufferSize;
public override Vector2i Size => Clyde._windowing!.MainWindow!.FramebufferSize;
public RenderWindow(Clyde clyde, ClydeHandle handle) : base(clyde, handle)
public RenderMainWindow(Clyde clyde, ClydeHandle handle) : base(clyde, handle)
{
}
}

View File

@@ -1,17 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.GameObjects;
using Robust.Client.Utility;
using Robust.Shared.Log;
using Robust.Shared;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
@@ -120,9 +117,10 @@ namespace Robust.Client.Graphics.Clyde
view = Matrix3.Identity;
}
private static void CalcWorldMatrices(in Vector2i screenSize, IEye eye, out Matrix3 proj, out Matrix3 view)
private static void CalcWorldMatrices(in Vector2i screenSize, in Vector2 renderScale, IEye eye,
out Matrix3 proj, out Matrix3 view)
{
eye.GetViewMatrix(out view);
eye.GetViewMatrix(out view, renderScale);
CalcWorldProjMatrix(screenSize, out proj);
}
@@ -266,8 +264,15 @@ namespace Robust.Client.Graphics.Clyde
private void _drawQuad(Vector2 a, Vector2 b, in Matrix3 modelMatrix, GLShaderProgram program)
{
BindVertexArray(QuadVAO.Handle);
DrawQuadWithVao(QuadVAO, a, b, modelMatrix, program);
}
private void DrawQuadWithVao(GLHandle vao, Vector2 a, Vector2 b, in Matrix3 modelMatrix,
GLShaderProgram program)
{
BindVertexArray(vao.Handle);
CheckGlError();
var rectTransform = Matrix3.Identity;
(rectTransform.R0C0, rectTransform.R1C1) = b - a;
(rectTransform.R0C2, rectTransform.R1C2) = a;
@@ -283,6 +288,16 @@ namespace Robust.Client.Graphics.Clyde
/// Flushes the render handle, processing and re-pooling all the command lists.
/// </summary>
private void FlushRenderQueue()
{
FlushBatchQueue();
// Reset renderer state.
_currentMatrixModel = Matrix3.Identity;
_queuedShader = _defaultShader.Handle;
SetScissorFull(null);
}
private void FlushBatchQueue()
{
// Finish any batches that may have been WiP.
BreakBatch();
@@ -308,11 +323,6 @@ namespace Robust.Client.Graphics.Clyde
ProcessRenderCommands();
_queuedRenderCommands.Clear();
// Reset renderer state.
_currentMatrixModel = Matrix3.Identity;
_queuedShader = _defaultShader.Handle;
SetScissorFull(null);
}
private void SetScissorFull(UIBox2i? state)
@@ -418,8 +428,8 @@ namespace Robust.Client.Graphics.Clyde
//It's important to start at Texture6 here since DrawCommandBatch uses Texture0 and Texture1 immediately after calling this
//function! If passing in textures as uniforms ever stops working it might be since someone made it use all the way up to Texture6 too.
//Might change this in the future?
TextureUnit cTarget = TextureUnit.Texture6+textureUnitVal;
SetTexture(cTarget, ((ClydeTexture)clydeTexture).TextureId);
TextureUnit cTarget = TextureUnit.Texture6 + textureUnitVal;
SetTexture(cTarget, ((ClydeTexture) clydeTexture).TextureId);
program.SetUniformTexture(name, cTarget);
textureUnitVal++;
break;
@@ -500,6 +510,7 @@ namespace Robust.Client.Graphics.Clyde
private void DrawTexture(ClydeHandle texture, Vector2 bl, Vector2 br, Vector2 tl, Vector2 tr, in Color modulate,
in Box2 texCoords)
{
EnsureBatchSpaceAvailable(4, GetQuadBatchIndexCount());
EnsureBatchState(texture, in modulate, true, GetQuadBatchPrimitiveType(), _queuedShader);
bl = _currentMatrixModel.Transform(bl);
@@ -525,6 +536,8 @@ namespace Robust.Client.Graphics.Clyde
FinishBatch();
_batchMetaData = null;
EnsureBatchSpaceAvailable(vertices.Length, indices.Length);
vertices.CopyTo(BatchVertexData.AsSpan(BatchVertexIndex));
// We are weaving this into the batch buffers for performance (and simplicity).
@@ -566,6 +579,8 @@ namespace Robust.Client.Graphics.Clyde
FinishBatch();
_batchMetaData = null;
EnsureBatchSpaceAvailable(vertices.Length, 0);
vertices.CopyTo(BatchVertexData.AsSpan(BatchVertexIndex));
ref var command = ref AllocRenderCommand(RenderCommandType.DrawBatch);
@@ -601,6 +616,7 @@ namespace Robust.Client.Graphics.Clyde
private void DrawLine(Vector2 a, Vector2 b, Color color)
{
EnsureBatchSpaceAvailable(2, 0);
EnsureBatchState(_stockTextureWhite.TextureId, color, false, BatchPrimitiveType.LineList, _queuedShader);
a = _currentMatrixModel.Transform(a);
@@ -615,6 +631,14 @@ namespace Robust.Client.Graphics.Clyde
_debugStats.LastClydeDrawCalls += 1;
}
private void EnsureBatchSpaceAvailable(int vtx, int idx)
{
if (BatchVertexIndex + vtx >= BatchVertexData.Length || BatchIndexIndex + idx > BatchIndexData.Length)
{
FlushBatchQueue();
}
}
private void DrawSetScissor(UIBox2i? scissorBox)
{
BreakBatch();
@@ -773,116 +797,6 @@ namespace Robust.Client.Graphics.Clyde
_batchMetaData = null;
}
private unsafe void TakeScreenshot(ScreenshotType type)
{
if (_queuedScreenshots.Count == 0 || _queuedScreenshots.All(p => p.type != type))
{
return;
}
var delegates = _queuedScreenshots.Where(p => p.type == type).ToList();
_queuedScreenshots.RemoveAll(p => p.type == type);
GL.PixelStore(PixelStoreParameter.PackAlignment, 1);
CheckGlError();
var bufferLength = ScreenSize.X * ScreenSize.Y;
if (!(_hasGLFenceSync && HasGLAnyMapBuffer && _hasGLPixelBufferObjects))
{
Logger.DebugS("clyde.ogl", "Necessary features for async screenshots not available, falling back to blocking path.");
// We need these 3 features to be able to do asynchronous screenshots, if we don't have them,
// we'll have to fall back to a crappy synchronous stalling method of glReadPixels().
var buffer = new Rgba32[bufferLength];
fixed (Rgba32* ptr = buffer)
{
var bufSize = sizeof(Rgba32) * bufferLength;
GL.ReadnPixels(0, 0, ScreenSize.X, ScreenSize.Y, PixelFormat.Rgba, PixelType.UnsignedByte, bufSize,
(IntPtr) ptr);
CheckGlError();
}
var (w, h) = ScreenSize;
var image = new Image<Rgb24>(w, h);
var imageSpan = image.GetPixelSpan();
FlipCopyScreenshot(buffer, imageSpan, w, h);
RunCallback(image);
return;
}
GL.GenBuffers(1, out uint pbo);
CheckGlError();
GL.BindBuffer(BufferTarget.PixelPackBuffer, pbo);
CheckGlError();
GL.BufferData(BufferTarget.PixelPackBuffer, bufferLength * sizeof(Rgba32), IntPtr.Zero,
BufferUsageHint.StreamRead);
CheckGlError();
GL.ReadPixels(0, 0, ScreenSize.X, ScreenSize.Y, PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
CheckGlError();
var fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
CheckGlError();
GL.BindBuffer(BufferTarget.PixelPackBuffer, 0);
CheckGlError();
_transferringScreenshots.Add((pbo, fence, ScreenSize, RunCallback));
void RunCallback(Image<Rgb24> image) => delegates.ForEach(p => p.callback(image));
}
private unsafe void CheckTransferringScreenshots()
{
if (_transferringScreenshots.Count == 0)
{
return;
}
foreach (var screenshot in _transferringScreenshots.ToList())
{
var (pbo, fence, (width, height), callback) = screenshot;
int status;
GL.GetSync(fence, SyncParameterName.SyncStatus, sizeof(int), null, &status);
CheckGlError();
if (status == (int) All.Signaled)
{
var bufLen = width * height;
var bufSize = sizeof(Rgba32) * bufLen;
GL.BindBuffer(BufferTarget.PixelPackBuffer, pbo);
CheckGlError();
var ptr = MapFullBuffer(BufferTarget.PixelPackBuffer, bufSize, BufferAccess.ReadOnly,
BufferAccessMask.MapReadBit);
var packSpan = new ReadOnlySpan<Rgba32>((void*) ptr, width * height);
var image = new Image<Rgb24>(width, height);
var imageSpan = image.GetPixelSpan();
FlipCopyScreenshot(packSpan, imageSpan, width, height);
UnmapBuffer(BufferTarget.PixelPackBuffer);
GL.BindBuffer(BufferTarget.PixelPackBuffer, 0);
CheckGlError();
GL.DeleteBuffer(pbo);
CheckGlError();
GL.DeleteSync(fence);
CheckGlError();
_transferringScreenshots.Remove(screenshot);
// TODO: Don't do unnecessary copy here.
callback(image);
}
}
}
private FullStoredRendererState PushRenderStateFull()
{
return new FullStoredRendererState(_currentMatrixProj, _currentMatrixView, _currentRenderTarget);
@@ -913,7 +827,7 @@ namespace Robust.Client.Graphics.Clyde
_lightingReady = false;
_currentMatrixModel = Matrix3.Identity;
SetScissorFull(null);
BindRenderTargetFull(_mainWindowRenderTarget);
BindRenderTargetFull(_mainMainWindowRenderMainTarget);
_batchMetaData = null;
_queuedShader = _defaultShader.Handle;
}
@@ -927,6 +841,109 @@ namespace Robust.Client.Graphics.Clyde
BlendingFactorDest.OneMinusSrcAlpha);
}
private void BlitSecondaryWindows()
{
// Only got main window.
if (_windowing!.AllWindows.Count == 1)
return;
if (!_hasGLFenceSync && _cfg.GetCVar(CVars.DisplayForceSyncWindows))
{
GL.Finish();
}
if (EffectiveThreadWindowBlit)
{
foreach (var window in _windowing.AllWindows)
{
if (window.IsMainWindow)
continue;
window.BlitDoneEvent!.Reset();
window.BlitStartEvent!.Set();
window.BlitDoneEvent.Wait();
}
}
else
{
foreach (var window in _windowing.AllWindows)
{
if (window.IsMainWindow)
continue;
_windowing.GLMakeContextCurrent(window);
BlitThreadDoSecondaryWindowBlit(window);
}
_windowing.GLMakeContextCurrent(_windowing.MainWindow!);
}
}
private void BlitThreadDoSecondaryWindowBlit(WindowReg window)
{
var rt = window.RenderTexture!;
if (_hasGLFenceSync)
{
// 0xFFFFFFFFFFFFFFFFUL is GL_TIMEOUT_IGNORED
var sync = rt!.LastGLSync;
GL.WaitSync(sync, WaitSyncFlags.None, unchecked((long) 0xFFFFFFFFFFFFFFFFUL));
CheckGlError();
}
GL.Viewport(0, 0, window.FramebufferSize.X, window.FramebufferSize.Y);
CheckGlError();
SetTexture(TextureUnit.Texture0, window.RenderTexture!.Texture);
GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4);
CheckGlError();
window.BlitDoneEvent?.Set();
_windowing!.WindowSwapBuffers(window);
}
private void BlitThreadInit(WindowReg reg)
{
_windowing!.GLMakeContextCurrent(reg);
_windowing.GLSwapInterval(0);
if (!_isGLES)
GL.Enable(EnableCap.FramebufferSrgb);
var vao = GL.GenVertexArray();
GL.BindVertexArray(vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, WindowVBO.ObjectHandle);
// Vertex Coords
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 0);
GL.EnableVertexAttribArray(0);
// Texture Coords.
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 2 * sizeof(float));
GL.EnableVertexAttribArray(1);
var program = _compileProgram(_winBlitShaderVert, _winBlitShaderFrag, new (string, uint)[]
{
("aPos", 0),
("tCoord", 1),
}, includeLib: false);
GL.UseProgram(program.Handle);
var loc = GL.GetUniformLocation(program.Handle, "tex");
SetTexture(TextureUnit.Texture0, reg.RenderTexture!.Texture);
GL.Uniform1(loc, 0);
}
private void FenceRenderTarget(RenderTargetBase rt)
{
if (!_hasGLFenceSync || !rt.MakeGLFence)
return;
if (rt.LastGLSync != 0)
GL.DeleteSync(rt.LastGLSync);
rt.LastGLSync = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
}
[StructLayout(LayoutKind.Explicit)]
private struct RenderCommand
{

View File

@@ -0,0 +1,223 @@
using System;
using System.Collections.Generic;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.Utility;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using PF = OpenToolkit.Graphics.OpenGL4.PixelFormat;
using PT = OpenToolkit.Graphics.OpenGL4.PixelType;
namespace Robust.Client.Graphics.Clyde
{
// Contains primary screenshot and pixel-copying logic.
internal sealed partial class Clyde
{
// Full-framebuffer screenshots undergo the following sequence of events:
// 1. Screenshots are queued by content or whatever.
// 2. When the rendering code reaches the screenshot type,
// we instruct the GPU driver to copy the framebuffer and asynchronously transfer it to host memory.
// 3. Transfer finished asynchronously, we invoke the callback.
//
// On RAW GLES2, we cannot do this asynchronously due to lacking GL features,
// and the game will stutter as a result. This is sadly unavoidable.
//
// For CopyPixels on render targets, the copy and transfer is started immediately when the function is called.
private readonly List<QueuedScreenshot> _queuedScreenshots = new();
private readonly List<TransferringPixelCopy> _transferringPixelCopies = new();
public void Screenshot(ScreenshotType type, CopyPixelsDelegate<Rgb24> callback, UIBox2i? subRegion = null)
{
_queuedScreenshots.Add(new QueuedScreenshot(type, callback, subRegion));
}
private void TakeScreenshot(ScreenshotType type)
{
if (_queuedScreenshots.Count == 0)
{
return;
}
GL.PixelStore(PixelStoreParameter.PackAlignment, 1);
CheckGlError();
for (var i = 0; i < _queuedScreenshots.Count; i++)
{
var (qType, callback, subRegion) = _queuedScreenshots[i];
if (qType != type)
continue;
DoCopyPixels(ScreenSize, subRegion, callback);
_queuedScreenshots.RemoveSwap(i--);
}
}
private void CopyRenderTargetPixels<T>(
ClydeHandle renderTarget,
UIBox2i? subRegion,
CopyPixelsDelegate<T> callback)
where T : unmanaged, IPixel<T>
{
var loaded = _renderTargets[renderTarget];
var original = GL.GetInteger(GetPName.ReadFramebufferBinding);
GL.BindFramebuffer(FramebufferTarget.ReadFramebuffer, loaded.FramebufferHandle.Handle);
DoCopyPixels(loaded.Size, subRegion, callback);
GL.BindFramebuffer(FramebufferTarget.ReadFramebuffer, original);
}
private unsafe void DoCopyPixels<T>(
Vector2i fbSize,
UIBox2i? subRegion,
CopyPixelsDelegate<T> callback)
where T : unmanaged, IPixel<T>
{
var (pf, pt) = default(T) switch
{
Rgba32 => (PF.Rgba, PT.UnsignedByte),
Rgb24 => (PF.Rgb, PT.UnsignedByte),
_ => throw new ArgumentException("Unsupported pixel type.")
};
var size = ClydeBase.ClampSubRegion(fbSize, subRegion);
var bufferLength = size.X * size.Y;
if (!(_hasGLFenceSync && HasGLAnyMapBuffer && _hasGLPixelBufferObjects))
{
_sawmillOgl.Debug("clyde.ogl",
"Necessary features for async screenshots not available, falling back to blocking path.");
// We need these 3 features to be able to do asynchronous screenshots, if we don't have them,
// we'll have to fall back to a crappy synchronous stalling method of glReadnPixels().
var buffer = new T[bufferLength];
fixed (T* ptr = buffer)
{
var bufSize = sizeof(T) * bufferLength;
GL.ReadnPixels(
0, 0,
size.X, size.Y,
pf, pt,
bufSize,
(nint) ptr);
CheckGlError();
}
var image = new Image<T>(size.X, size.Y);
var imageSpan = image.GetPixelSpan();
FlipCopy(buffer, imageSpan, size.X, size.Y);
callback(image);
return;
}
GL.GenBuffers(1, out uint pbo);
CheckGlError();
GL.BindBuffer(BufferTarget.PixelPackBuffer, pbo);
CheckGlError();
GL.BufferData(
BufferTarget.PixelPackBuffer,
bufferLength * sizeof(Rgba32), IntPtr.Zero,
BufferUsageHint.StreamRead);
CheckGlError();
GL.ReadPixels(0, 0, size.X, size.Y, pf, pt, IntPtr.Zero);
CheckGlError();
var fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
CheckGlError();
GL.BindBuffer(BufferTarget.PixelPackBuffer, 0);
CheckGlError();
var transferring = new TransferringPixelCopy(pbo, fence, size, FinishPixelTransfer<T>, callback);
_transferringPixelCopies.Add(transferring);
}
private unsafe void CheckTransferringScreenshots()
{
if (_transferringPixelCopies.Count == 0)
{
return;
}
for (var i = 0; i < _transferringPixelCopies.Count; i++)
{
var transferring = _transferringPixelCopies[i];
// Check if transfer done (sync signalled)
int status;
GL.GetSync(transferring.Sync, SyncParameterName.SyncStatus, sizeof(int), null, &status);
CheckGlError();
if (status != (int) All.Signaled)
continue;
transferring.TransferContinue(transferring);
_transferringPixelCopies.RemoveSwap(i--);
}
}
private unsafe void FinishPixelTransfer<T>(TransferringPixelCopy transferring) where T : unmanaged, IPixel<T>
{
var (pbo, fence, (width, height), _, callback) = transferring;
var bufLen = width * height;
var bufSize = sizeof(T) * bufLen;
GL.BindBuffer(BufferTarget.PixelPackBuffer, pbo);
CheckGlError();
var ptr = MapFullBuffer(
BufferTarget.PixelPackBuffer,
bufSize,
BufferAccess.ReadOnly,
BufferAccessMask.MapReadBit);
var packSpan = new ReadOnlySpan<T>(ptr, width * height);
var image = new Image<T>(width, height);
var imageSpan = image.GetPixelSpan();
FlipCopy(packSpan, imageSpan, width, height);
UnmapBuffer(BufferTarget.PixelPackBuffer);
GL.BindBuffer(BufferTarget.PixelPackBuffer, 0);
CheckGlError();
GL.DeleteBuffer(pbo);
CheckGlError();
GL.DeleteSync(fence);
CheckGlError();
var castCallback = (CopyPixelsDelegate<T>) callback;
castCallback(image);
}
private sealed record QueuedScreenshot(
ScreenshotType Type,
CopyPixelsDelegate<Rgb24> Callback,
UIBox2i? SubRegion);
private sealed record TransferringPixelCopy(
uint Pbo,
nint Sync,
Vector2i Size,
// Funny callback dance to handle the generics.
Action<TransferringPixelCopy> TransferContinue,
Delegate Callback);
}
}

View File

@@ -23,6 +23,9 @@ namespace Robust.Client.Graphics.Clyde
private string _shaderWrapCodeRawFrag = default!;
private string _shaderWrapCodeRawVert = default!;
private string _winBlitShaderVert = default!;
private string _winBlitShaderFrag = default!;
private readonly Dictionary<ClydeHandle, LoadedShader> _loadedShaders =
new();
@@ -117,6 +120,9 @@ namespace Robust.Client.Graphics.Clyde
_shaderWrapCodeRawVert = ReadEmbeddedShader("base-raw.vert");
_shaderWrapCodeRawFrag = ReadEmbeddedShader("base-raw.frag");
_winBlitShaderVert = ReadEmbeddedShader("winblit.vert");
_winBlitShaderFrag = ReadEmbeddedShader("winblit.frag");
var defaultLoadedShader = _resourceCache
.GetResource<ShaderSourceResource>("/Shaders/Internal/default-sprite.swsl").ClydeHandle;
@@ -135,7 +141,7 @@ namespace Robust.Client.Graphics.Clyde
}
private GLShaderProgram _compileProgram(string vertexSource, string fragmentSource,
(string, uint)[] attribLocations, string? name = null)
(string, uint)[] attribLocations, string? name = null, bool includeLib=true)
{
GLShader? vertexShader = null;
GLShader? fragmentShader = null;
@@ -172,8 +178,9 @@ namespace Robust.Client.Graphics.Clyde
versionHeader += "#define HAS_UNIFORM_BUFFERS\n";
}
vertexSource = versionHeader + "#define VERTEX_SHADER\n" + _shaderLibrary + vertexSource;
fragmentSource = versionHeader + "#define FRAGMENT_SHADER\n" + _shaderLibrary + fragmentSource;
var lib = includeLib ? _shaderLibrary : "";
vertexSource = versionHeader + "#define VERTEX_SHADER\n" + lib + vertexSource;
fragmentSource = versionHeader + "#define FRAGMENT_SHADER\n" + lib + fragmentSource;
try
{

View File

@@ -26,10 +26,10 @@ namespace Robust.Client.Graphics.Clyde
private readonly ConcurrentQueue<ClydeHandle> _textureDisposeQueue = new();
public Texture LoadTextureFromPNGStream(Stream stream, string? name = null,
public OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
TextureLoadParameters? loadParams = null)
{
DebugTools.Assert(_mainThread == Thread.CurrentThread);
DebugTools.Assert(_gameThread == Thread.CurrentThread);
// Load using Rgba32.
using var image = Image.Load<Rgba32>(stream);
@@ -37,10 +37,10 @@ namespace Robust.Client.Graphics.Clyde
return LoadTextureFromImage(image, name, loadParams);
}
public Texture LoadTextureFromImage<T>(Image<T> image, string? name = null,
public OwnedTexture LoadTextureFromImage<T>(Image<T> image, string? name = null,
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>
{
DebugTools.Assert(_mainThread == Thread.CurrentThread);
DebugTools.Assert(_gameThread == Thread.CurrentThread);
var actualParams = loadParams ?? TextureLoadParameters.Default;
var pixelType = typeof(T);
@@ -324,11 +324,13 @@ namespace Robust.Client.Graphics.Clyde
if (typeof(T) == typeof(A8))
{
SetSubImage(texture, dstTl, ApplyA8Swizzle((Image<A8>) (object) srcImage), srcBox);
return;
}
if (typeof(T) == typeof(L8))
{
SetSubImage(texture, dstTl, ApplyL8Swizzle((Image<L8>) (object) srcImage), srcBox);
return;
}
}
@@ -450,24 +452,6 @@ namespace Robust.Client.Graphics.Clyde
}
}
private static void FlipCopyScreenshot(ReadOnlySpan<Rgba32> srcSpan, Span<Rgb24> dstSpan, int w, int h)
{
var dr = h - 1;
for (var r = 0; r < h; r++, dr--)
{
var si = r * w;
var di = dr * w;
var srcRow = srcSpan[si..(si + w)];
var dstRow = dstSpan[di..(di + w)];
for (var x = 0; x < w; x++)
{
var src = srcRow[x];
dstRow[x] = new Rgb24(src.R, src.G, src.B);
}
}
}
private static Image<Rgba32> ApplyA8Swizzle(Image<A8> source)
{
var newImage = new Image<Rgba32>(source.Width, source.Height);

View File

@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics.Clyde
@@ -10,7 +13,7 @@ namespace Robust.Client.Graphics.Clyde
private readonly Dictionary<ClydeHandle, WeakReference<Viewport>> _viewports =
new();
private Viewport CreateViewport(Vector2i size, string? name = null)
private Viewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters = default, string? name = null)
{
var handle = AllocRid();
var viewport = new Viewport(handle, name, this)
@@ -18,6 +21,7 @@ namespace Robust.Client.Graphics.Clyde
Size = size,
RenderTarget = CreateRenderTarget(size,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true),
sampleParameters: sampleParameters,
name: $"{name}-MainRenderTarget")
};
@@ -28,9 +32,9 @@ namespace Robust.Client.Graphics.Clyde
return viewport;
}
IClydeViewport IClyde.CreateViewport(Vector2i size, string? name)
IClydeViewport IClyde.CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters, string? name)
{
return CreateViewport(size, name);
return CreateViewport(size, sampleParameters, name);
}
private static Vector2 ScreenToMap(Vector2 point, Viewport vp)
@@ -45,7 +49,7 @@ namespace Robust.Client.Graphics.Clyde
point *= new Vector2(1, -1) / EyeManager.PixelsPerMeter;
// view matrix
vp.Eye.GetViewMatrixInv(out var viewMatrixInv);
vp.Eye.GetViewMatrixInv(out var viewMatrixInv, vp.RenderScale);
point = viewMatrixInv * point;
return point;
@@ -106,12 +110,66 @@ namespace Robust.Client.Graphics.Clyde
}
public Vector2i Size { get; set; }
public Vector2 RenderScale { get; set; } = Vector2.One;
public bool AutomaticRender { get; set; }
void IClydeViewport.Render()
{
_clyde.RenderViewport(this);
}
public MapCoordinates LocalToWorld(Vector2 point)
{
if (Eye == null)
return default;
var newPoint = point;
// (inlined version of UiProjMatrix^-1)
newPoint -= Size / 2f;
newPoint *= new Vector2(1, -1) / EyeManager.PixelsPerMeter;
// view matrix
Eye.GetViewMatrixInv(out var viewMatrixInv, RenderScale);
newPoint = viewMatrixInv * newPoint;
return new MapCoordinates(newPoint, Eye.Position.MapId);
}
public Vector2 WorldToLocal(Vector2 point)
{
if (Eye == null)
return default;
var eye = (IEye) Eye;
var newPoint = point;
eye.GetViewMatrix(out var viewMatrix, RenderScale);
newPoint = viewMatrix * newPoint;
// (inlined version of UiProjMatrix)
newPoint *= new Vector2(1, -1) * EyeManager.PixelsPerMeter;
newPoint += Size / 2f;
return newPoint;
}
public void RenderScreenOverlaysBelow(
DrawingHandleScreen handle,
IViewportControl control,
in UIBox2i viewportBounds)
{
_clyde.RenderOverlaysDirect(this, control, handle, OverlaySpace.ScreenSpaceBelowWorld, viewportBounds);
}
public void RenderScreenOverlaysAbove(
DrawingHandleScreen handle,
IViewportControl control,
in UIBox2i viewportBounds)
{
_clyde.RenderOverlaysDirect(this, control, handle, OverlaySpace.ScreenSpace, viewportBounds);
}
public void Dispose()
{
RenderTarget.Dispose();

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,19 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.Map;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
namespace Robust.Client.Graphics.Clyde
@@ -23,7 +21,7 @@ namespace Robust.Client.Graphics.Clyde
/// <summary>
/// Responsible for most things rendering on OpenGL mode.
/// </summary>
internal sealed partial class Clyde : ClydeBase, IClydeInternal, IClydeAudio, IPostInjectInit
internal sealed partial class Clyde : IClydeInternal, IClydeAudio, IPostInjectInit
{
[Dependency] private readonly IClydeTileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
@@ -35,6 +33,7 @@ namespace Robust.Client.Graphics.Clyde
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private GLUniformBuffer<ProjViewMatrices> ProjViewUBO = default!;
private GLUniformBuffer<UniformConstants> UniformConstantsUBO = default!;
@@ -47,7 +46,9 @@ namespace Robust.Client.Graphics.Clyde
private GLBuffer QuadVBO = default!;
private GLHandle QuadVAO;
private Viewport _mainViewport = default!;
// VBO to blit to the window
// VAO is per-window and not stored (not necessary!)
private GLBuffer WindowVBO = default!;
private bool _drawingSplash = true;
@@ -59,18 +60,15 @@ namespace Robust.Client.Graphics.Clyde
private bool _checkGLErrors;
private readonly List<(ScreenshotType type, Action<Image<Rgb24>> callback)> _queuedScreenshots
= new();
private Thread? _gameThread;
private readonly List<(uint pbo, IntPtr sync, Vector2i size, Action<Image<Rgb24>> callback)>
_transferringScreenshots
= new();
private ISawmill _sawmillOgl = default!;
public Clyde()
{
// Init main window render target.
var windowRid = AllocRid();
var window = new RenderWindow(this, windowRid);
var window = new RenderMainWindow(this, windowRid);
var loadedData = new LoadedRenderTarget
{
IsWindow = true,
@@ -78,33 +76,54 @@ namespace Robust.Client.Graphics.Clyde
};
_renderTargets.Add(windowRid, loadedData);
_mainWindowRenderTarget = window;
_mainMainWindowRenderMainTarget = window;
_currentRenderTarget = RtToLoaded(window);
_currentBoundRenderTarget = _currentRenderTarget;
}
public override bool Initialize()
public bool InitializePreWindowing()
{
base.Initialize();
_sawmillOgl = Logger.GetSawmill("clyde.ogl");
_configurationManager.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
_cfg.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
_cfg.OnValueChanged(CVars.DisplayVSync, VSyncChanged, true);
_cfg.OnValueChanged(CVars.DisplayWindowMode, WindowModeChanged, true);
_cfg.OnValueChanged(CVars.DisplayLightMapDivider, LightmapDividerChanged, true);
_cfg.OnValueChanged(CVars.DisplayMaxLightsPerScene, MaxLightsPerSceneChanged, true);
_cfg.OnValueChanged(CVars.DisplaySoftShadows, SoftShadowsChanged, true);
// I can't be bothered to tear down and set these threads up in a cvar change handler.
_threadWindowBlit = _cfg.GetCVar(CVars.DisplayThreadWindowBlit);
if (!InitWindowing())
{
return InitWindowing();
}
public bool InitializePostWindowing()
{
_gameThread = Thread.CurrentThread;
if (!InitMainWindowAndRenderer())
return false;
}
_initializeAudio();
ReloadConfig();
return true;
}
public bool SeparateWindowThread => true;
public void EnterWindowLoop()
{
_windowing!.EnterWindowLoop();
}
public void TerminateWindowLoop()
{
_windowing!.TerminateWindowLoop();
}
public void FrameProcess(FrameEventArgs eventArgs)
{
_updateAudio();
FlushCursorDispose();
_windowing?.FlushDispose();
FlushShaderInstanceDispose();
FlushRenderTargetDispose();
FlushTextureDispose();
@@ -121,21 +140,6 @@ namespace Robust.Client.Graphics.Clyde
public IClydeDebugInfo DebugInfo { get; private set; } = default!;
public IClydeDebugStats DebugStats => _debugStats;
protected override void ReadConfig()
{
base.ReadConfig();
_lightmapDivider = _configurationManager.GetCVar(CVars.DisplayLightMapDivider);
_maxLightsPerScene = _configurationManager.GetCVar(CVars.DisplayMaxLightsPerScene);
_enableSoftShadows = _configurationManager.GetCVar(CVars.DisplaySoftShadows);
}
protected override void ReloadConfig()
{
base.ReloadConfig();
RegenAllLightRts();
}
public void PostInject()
{
_mapManager.TileChanged += _updateTileMapOnUpdate;
@@ -148,15 +152,6 @@ namespace Robust.Client.Graphics.Clyde
RegisterBlockCVars();
}
public override event Action<WindowResizedEventArgs>? OnWindowResized;
public override event Action<WindowFocusedEventArgs>? OnWindowFocused;
public void Screenshot(ScreenshotType type, Action<Image<Rgb24>> callback)
{
_queuedScreenshots.Add((type, callback));
}
private void InitOpenGL()
{
var vendor = GL.GetString(StringName.Vendor);
@@ -165,16 +160,16 @@ namespace Robust.Client.Graphics.Clyde
var major = GL.GetInteger(GetPName.MajorVersion);
var minor = GL.GetInteger(GetPName.MinorVersion);
Logger.DebugS("clyde.ogl", "OpenGL Vendor: {0}", vendor);
Logger.DebugS("clyde.ogl", "OpenGL Renderer: {0}", renderer);
Logger.DebugS("clyde.ogl", "OpenGL Version: {0}", version);
_sawmillOgl.Debug("OpenGL Vendor: {0}", vendor);
_sawmillOgl.Debug("OpenGL Renderer: {0}", renderer);
_sawmillOgl.Debug("OpenGL Version: {0}", version);
var overrideVersion = ParseGLOverrideVersion();
if (overrideVersion != null)
{
(major, minor) = overrideVersion.Value;
Logger.DebugS("clyde.ogl", "OVERRIDING detected GL version to: {0}.{1}", major, minor);
_sawmillOgl.Debug("OVERRIDING detected GL version to: {0}.{1}", major, minor);
}
DetectOpenGLFeatures(major, minor);
@@ -201,7 +196,7 @@ namespace Robust.Client.Graphics.Clyde
}
if (!HasGLAnyVertexArrayObjects)
{
Logger.WarningS("clyde.ogl", "NO VERTEX ARRAY OBJECTS! Things will probably go terribly, terribly wrong (no fallback path yet)");
_sawmillOgl.Warning("NO VERTEX ARRAY OBJECTS! Things will probably go terribly, terribly wrong (no fallback path yet)");
}
ResetBlendFunc();
@@ -211,23 +206,23 @@ namespace Robust.Client.Graphics.Clyde
// Primitive Restart's presence or lack thereof changes the amount of required memory.
InitRenderingBatchBuffers();
Logger.DebugS("clyde.ogl", "Loading stock textures...");
_sawmillOgl.Debug("Loading stock textures...");
LoadStockTextures();
Logger.DebugS("clyde.ogl", "Loading stock shaders...");
_sawmillOgl.Debug("Loading stock shaders...");
LoadStockShaders();
Logger.DebugS("clyde.ogl", "Creating various GL objects...");
_sawmillOgl.Debug("Creating various GL objects...");
CreateMiscGLObjects();
Logger.DebugS("clyde.ogl", "Setting up RenderHandle...");
_sawmillOgl.Debug("Setting up RenderHandle...");
_renderHandle = new RenderHandle(this);
Logger.DebugS("clyde.ogl", "Setting viewport and rendering splash...");
_sawmillOgl.Debug("Setting viewport and rendering splash...");
GL.Viewport(0, 0, ScreenSize.X, ScreenSize.Y);
CheckGlError();
@@ -238,7 +233,7 @@ namespace Robust.Client.Graphics.Clyde
private (int major, int minor)? ParseGLOverrideVersion()
{
var overrideGLVersion = _configurationManager.GetCVar(CVars.DisplayOGLOverrideVersion);
var overrideGLVersion = _cfg.GetCVar(CVars.DisplayOGLOverrideVersion);
if (string.IsNullOrEmpty(overrideGLVersion))
{
return null;
@@ -247,14 +242,14 @@ namespace Robust.Client.Graphics.Clyde
var split = overrideGLVersion.Split(".");
if (split.Length != 2)
{
Logger.WarningS("clyde.ogl", "display.ogl_override_version is in invalid format");
_sawmillOgl.Warning("display.ogl_override_version is in invalid format");
return null;
}
if (!int.TryParse(split[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var major)
|| !int.TryParse(split[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var minor))
{
Logger.WarningS("clyde.ogl", "display.ogl_override_version is in invalid format");
_sawmillOgl.Warning("display.ogl_override_version is in invalid format");
return null;
}
@@ -277,15 +272,27 @@ namespace Robust.Client.Graphics.Clyde
quadVertices,
nameof(QuadVBO));
QuadVAO = new GLHandle(GenVertexArray());
BindVertexArray(QuadVAO.Handle);
ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, QuadVAO, nameof(QuadVAO));
// Vertex Coords
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 0);
GL.EnableVertexAttribArray(0);
// Texture Coords.
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 2 * sizeof(float));
GL.EnableVertexAttribArray(1);
QuadVAO = MakeQuadVao();
CheckGlError();
}
// Window VBO
{
Span<Vertex2D> winVertices = stackalloc[]
{
new Vertex2D(-1, 1, 0, 1),
new Vertex2D(-1, -1, 0, 0),
new Vertex2D(1, 1, 1, 1),
new Vertex2D(1, -1, 1, 0),
};
WindowVBO = new GLBuffer<Vertex2D>(
this,
BufferTarget.ArrayBuffer,
BufferUsageHint.StaticDraw,
winVertices,
nameof(WindowVBO));
CheckGlError();
}
@@ -314,23 +321,26 @@ namespace Robust.Client.Graphics.Clyde
ProjViewUBO = new GLUniformBuffer<ProjViewMatrices>(this, BindingIndexProjView, nameof(ProjViewUBO));
UniformConstantsUBO = new GLUniformBuffer<UniformConstants>(this, BindingIndexUniformConstants, nameof(UniformConstantsUBO));
CreateMainViewport();
screenBufferHandle = new GLHandle(GL.GenTexture());
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
ApplySampleParameters(TextureSampleParameters.Default);
ScreenBufferTexture = GenTexture(screenBufferHandle, _framebufferSize, true, null, TexturePixelType.Rgba32);
ScreenBufferTexture = GenTexture(screenBufferHandle, _windowing!.MainWindow!.FramebufferSize, true, null, TexturePixelType.Rgba32);
}
private void CreateMainViewport()
private GLHandle MakeQuadVao()
{
var (w, h) = _framebufferSize;
var vao = new GLHandle(GenVertexArray());
BindVertexArray(vao.Handle);
ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, vao, nameof(QuadVAO));
GL.BindBuffer(BufferTarget.ArrayBuffer, QuadVBO.ObjectHandle);
// Vertex Coords
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 0);
GL.EnableVertexAttribArray(0);
// Texture Coords.
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 2 * sizeof(float));
GL.EnableVertexAttribArray(1);
// Ensure viewport size is always even to avoid artifacts.
if (w % 2 == 1) w += 1;
if (h % 2 == 1) h += 1;
_mainViewport = CreateViewport((w, h), nameof(_mainViewport));
return vao;
}
[Conditional("DEBUG")]
@@ -338,7 +348,7 @@ namespace Robust.Client.Graphics.Clyde
{
if (!_hasGLKhrDebug)
{
Logger.DebugS("clyde.ogl", "KHR_debug not present, OpenGL debug logging not enabled.");
_sawmillOgl.Debug("KHR_debug not present, OpenGL debug logging not enabled.");
return;
}

View File

@@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Input;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using SixLabors.ImageSharp;
@@ -15,13 +19,16 @@ namespace Robust.Client.Graphics.Clyde
/// Hey look, it's Clyde's evil twin brother!
/// </summary>
[UsedImplicitly]
internal sealed class ClydeHeadless : ClydeBase, IClydeInternal, IClydeAudio
internal sealed class ClydeHeadless : IClydeInternal, IClydeAudio
{
// Would it make sense to report a fake resolution like 720p here so code doesn't break? idk.
public IRenderWindow MainWindowRenderTarget { get; }
public override Vector2i ScreenSize { get; } = (1280, 720);
public IClydeWindow MainWindow { get; }
public Vector2i ScreenSize => (1280, 720);
public IEnumerable<IClydeWindow> AllWindows => _windows;
public Vector2 DefaultWindowScale => (1, 1);
public override bool IsFocused => true;
public bool IsFocused => true;
private readonly List<IClydeWindow> _windows = new();
private int _nextWindowId = 2;
public ShaderInstance InstanceShader(ClydeHandle handle)
{
@@ -30,13 +37,26 @@ namespace Robust.Client.Graphics.Clyde
public ClydeHeadless()
{
MainWindowRenderTarget = new DummyRenderWindow(this);
var mainRt = new DummyRenderWindow(this);
var window = new DummyWindow(mainRt) {Id = new WindowId(1)};
_windows.Add(window);
MainWindow = window;
}
public Vector2 MouseScreenPosition => ScreenSize / 2;
public ScreenCoordinates MouseScreenPosition => default;
public IClydeDebugInfo DebugInfo { get; } = new DummyDebugInfo();
public IClydeDebugStats DebugStats { get; } = new DummyDebugStats();
public event Action<TextEventArgs>? TextEntered { add { } remove { } }
public event Action<MouseMoveEventArgs>? MouseMove { add { } remove { } }
public event Action<MouseEnterLeaveEventArgs>? MouseEnterLeave { add { } remove { } }
public event Action<KeyEventArgs>? KeyUp { add { } remove { } }
public event Action<KeyEventArgs>? KeyDown { add { } remove { } }
public event Action<MouseWheelEventArgs>? MouseWheel { add { } remove { } }
public event Action<WindowClosedEventArgs>? CloseWindow { add { } remove { } }
public event Action<WindowDestroyedEventArgs>? DestroyWindow { add { } remove { } }
public Texture GetStockTexture(ClydeStockTexture stockTexture)
{
return new DummyTexture((1, 1));
@@ -45,8 +65,6 @@ namespace Robust.Client.Graphics.Clyde
public ClydeDebugLayers DebugLayers { get; set; }
public string GetKeyName(Keyboard.Key key) => string.Empty;
public string GetKeyNameScanCode(int scanCode) => string.Empty;
public int GetKeyScanCode(Keyboard.Key key) => default;
public void Shutdown()
{
@@ -58,7 +76,12 @@ namespace Robust.Client.Graphics.Clyde
return null;
}
public override void SetWindowTitle(string title)
public void SetWindowTitle(string title)
{
// Nada.
}
public void SetWindowMonitor(IClydeMonitor monitor)
{
// Nada.
}
@@ -68,19 +91,19 @@ namespace Robust.Client.Graphics.Clyde
// Nada.
}
public override bool Initialize()
{
base.Initialize();
return true;
}
public override event Action<WindowResizedEventArgs> OnWindowResized
public event Action<WindowResizedEventArgs> OnWindowResized
{
add { }
remove { }
}
public override event Action<WindowFocusedEventArgs> OnWindowFocused
public event Action<WindowFocusedEventArgs> OnWindowFocused
{
add { }
remove { }
}
public event Action<WindowContentScaleEventArgs> OnWindowScaleChanged
{
add { }
remove { }
@@ -101,7 +124,29 @@ namespace Robust.Client.Graphics.Clyde
// Nada.
}
public Texture LoadTextureFromPNGStream(Stream stream, string? name = null,
public bool SeparateWindowThread => false;
public bool InitializePreWindowing()
{
return true;
}
public void TerminateWindowLoop()
{
throw new InvalidOperationException("ClydeHeadless does not use windowing threads");
}
public void EnterWindowLoop()
{
throw new InvalidOperationException("ClydeHeadless does not use windowing threads");
}
public bool InitializePostWindowing()
{
return true;
}
public OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
TextureLoadParameters? loadParams = null)
{
using (var image = Image.Load<Rgba32>(stream))
@@ -110,7 +155,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
public Texture LoadTextureFromImage<T>(Image<T> image, string? name = null,
public OwnedTexture LoadTextureFromImage<T>(Image<T> image, string? name = null,
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>
{
return new DummyTexture((image.Width, image.Height));
@@ -146,14 +191,34 @@ namespace Robust.Client.Graphics.Clyde
// Nada.
}
public void Screenshot(ScreenshotType type, Action<Image<Rgb24>> callback)
public void Screenshot(ScreenshotType type, CopyPixelsDelegate<Rgb24> callback, UIBox2i? subRegion = null)
{
callback(new Image<Rgb24>(ScreenSize.X, ScreenSize.Y));
// Immediately call callback with an empty buffer.
var (x, y) = ClydeBase.ClampSubRegion(ScreenSize, subRegion);
callback(new Image<Rgb24>(x, y));
}
public IClydeViewport CreateViewport(Vector2i size, string? name = null)
public IClydeViewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters,
string? name = null)
{
return new Viewport();
return new Viewport(size);
}
public IEnumerable<IClydeMonitor> EnumerateMonitors()
{
// TODO: Actually return something.
yield break;
}
public Task<IClydeWindow> CreateWindow(WindowCreateParameters parameters)
{
var window = new DummyWindow(CreateRenderTarget((123, 123), default))
{
Id = new WindowId(_nextWindowId++)
};
_windows.Add(window);
return Task.FromResult<IClydeWindow>(window);
}
public ClydeHandle LoadShader(ParsedShader shader, string? name = null)
@@ -183,6 +248,11 @@ namespace Robust.Client.Graphics.Clyde
return new(default, default, 1, name);
}
public AudioStream LoadAudioRaw(short[] samples, int channels, int sampleRate)
{
throw new NotImplementedException();
}
public IClydeAudioSource CreateAudioSource(AudioStream stream)
{
return DummyAudioSource.Instance;
@@ -193,9 +263,9 @@ namespace Robust.Client.Graphics.Clyde
return DummyBufferedAudioSource.Instance;
}
public string GetText()
public Task<string> GetText()
{
return string.Empty;
return Task.FromResult(string.Empty);
}
public void SetText(string text)
@@ -267,6 +337,16 @@ namespace Robust.Client.Graphics.Clyde
{
// Nada.
}
public void SetVelocity(Vector2 velocity)
{
// Nada.
}
public void SetVolumeDirect(float masterVolumeDecay)
{
// Nada.
}
}
private sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource
@@ -402,18 +482,21 @@ namespace Robust.Client.Graphics.Clyde
}
public Vector2i Size { get; }
public Texture Texture { get; }
public void Delete()
public void CopyPixelsToMemory<T>(CopyPixelsDelegate<T> callback, UIBox2i? subRegion) where T : unmanaged, IPixel<T>
{
var (x, y) = ClydeBase.ClampSubRegion(Size, subRegion);
callback(new Image<T>(x, y));
}
public Texture Texture { get; }
public void Dispose()
{
}
}
private sealed class DummyRenderWindow : IRenderWindow
private sealed class DummyRenderWindow : IRenderTarget
{
private readonly ClydeHeadless _clyde;
@@ -424,6 +507,12 @@ namespace Robust.Client.Graphics.Clyde
public Vector2i Size => _clyde.ScreenSize;
public void CopyPixelsToMemory<T>(CopyPixelsDelegate<T> callback, UIBox2i? subRegion) where T : unmanaged, IPixel<T>
{
var (x, y) = ClydeBase.ClampSubRegion(Size, subRegion);
callback(new Image<T>(x, y));
}
public void Dispose()
{
}
@@ -449,6 +538,11 @@ namespace Robust.Client.Graphics.Clyde
private sealed class Viewport : IClydeViewport
{
public Viewport(Vector2i size)
{
Size = size;
}
public void Dispose()
{
}
@@ -458,9 +552,67 @@ namespace Robust.Client.Graphics.Clyde
public IEye? Eye { get; set; }
public Vector2i Size { get; }
public Vector2 RenderScale { get; set; }
public bool AutomaticRender { get; set; }
public void Render()
{
// Nada
}
public MapCoordinates LocalToWorld(Vector2 point)
{
return default;
}
public Vector2 WorldToLocal(Vector2 point)
{
return default;
}
public void RenderScreenOverlaysBelow(
DrawingHandleScreen handle,
IViewportControl control,
in UIBox2i viewportBounds)
{
// Nada
}
public void RenderScreenOverlaysAbove(
DrawingHandleScreen handle,
IViewportControl control,
in UIBox2i viewportBounds)
{
// Nada
}
}
private sealed class DummyWindow : IClydeWindow
{
public DummyWindow(IRenderTarget renderTarget)
{
RenderTarget = renderTarget;
}
public Vector2i Size { get; } = default;
public bool IsDisposed { get; private set; }
public WindowId Id { get; set; }
public IRenderTarget RenderTarget { get; }
public string Title { get; set; } = "";
public bool IsFocused => false;
public bool IsMinimized => false;
public bool IsVisible { get; set; } = true;
public Vector2 ContentScale => Vector2.One;
public bool DisposeOnClose { get; set; }
public event Action<WindowClosedEventArgs>? Closed { add { } remove { } }
public void MaximizeOnMonitor(IClydeMonitor monitor)
{
}
public void Dispose()
{
IsDisposed = true;
}
}
}

View File

@@ -18,7 +18,7 @@ namespace Robust.Client.Graphics.Clyde
{
private readonly sbyte?[] _uniformIntCache = new sbyte?[UniCount];
private readonly Dictionary<string, int> _uniformCache = new();
private uint _handle = 0;
public uint Handle = 0;
private GLShader? _fragmentShader;
private GLShader? _vertexShader;
public string? Name { get; }
@@ -49,22 +49,22 @@ namespace Robust.Client.Graphics.Clyde
public void Link((string, uint)[] attribLocations)
{
ClearCaches();
_handle = (uint) GL.CreateProgram();
Handle = (uint) GL.CreateProgram();
_clyde.CheckGlError();
if (Name != null)
{
_clyde.ObjectLabelMaybe(ObjectLabelIdentifier.Program, _handle, Name);
_clyde.ObjectLabelMaybe(ObjectLabelIdentifier.Program, Handle, Name);
}
if (_vertexShader != null)
{
GL.AttachShader(_handle, _vertexShader.ObjectHandle);
GL.AttachShader(Handle, _vertexShader.ObjectHandle);
_clyde.CheckGlError();
}
if (_fragmentShader != null)
{
GL.AttachShader(_handle, _fragmentShader.ObjectHandle);
GL.AttachShader(Handle, _fragmentShader.ObjectHandle);
_clyde.CheckGlError();
}
@@ -74,45 +74,50 @@ namespace Robust.Client.Graphics.Clyde
// So we have to manually do it here.
// Ugh.
GL.BindAttribLocation(_handle, loc, varName);
GL.BindAttribLocation(Handle, loc, varName);
_clyde.CheckGlError();
}
GL.LinkProgram(_handle);
GL.LinkProgram(Handle);
_clyde.CheckGlError();
GL.GetProgram(_handle, GetProgramParameterName.LinkStatus, out var compiled);
GL.GetProgram(Handle, GetProgramParameterName.LinkStatus, out var compiled);
_clyde.CheckGlError();
if (compiled != 1)
{
throw new ShaderCompilationException(GL.GetProgramInfoLog((int) _handle));
throw new ShaderCompilationException(GL.GetProgramInfoLog((int) Handle));
}
}
public void Use()
{
DebugTools.Assert(_handle != 0);
if (_clyde._currentProgram == this)
{
return;
}
ForceUse();
}
public void ForceUse()
{
DebugTools.Assert(Handle != 0);
_clyde._currentProgram = this;
GL.UseProgram(_handle);
GL.UseProgram(Handle);
_clyde.CheckGlError();
}
public void Delete()
{
if (_handle == 0)
if (Handle == 0)
{
return;
}
GL.DeleteProgram(_handle);
GL.DeleteProgram(Handle);
_clyde.CheckGlError();
_handle = 0;
Handle = 0;
}
public int GetUniform(string name)
@@ -137,14 +142,14 @@ namespace Robust.Client.Graphics.Clyde
public bool TryGetUniform(string name, out int index)
{
DebugTools.Assert(_handle != 0);
DebugTools.Assert(Handle != 0);
if (_uniformCache.TryGetValue(name, out index))
{
return index != -1;
}
index = GL.GetUniformLocation(_handle, name);
index = GL.GetUniformLocation(Handle, name);
_clyde.CheckGlError();
_uniformCache.Add(name, index);
return index != -1;
@@ -152,7 +157,7 @@ namespace Robust.Client.Graphics.Clyde
public bool TryGetUniform(int id, out int index)
{
DebugTools.Assert(_handle != 0);
DebugTools.Assert(Handle != 0);
DebugTools.Assert(id < UniCount);
var value = _uniformIntCache[id];
@@ -192,7 +197,7 @@ namespace Robust.Client.Graphics.Clyde
throw new ArgumentOutOfRangeException();
}
index = GL.GetUniformLocation(_handle, name);
index = GL.GetUniformLocation(Handle, name);
_clyde.CheckGlError();
_uniformIntCache[id] = (sbyte)index;
return index != -1;
@@ -203,9 +208,9 @@ namespace Robust.Client.Graphics.Clyde
public void BindBlock(string blockName, uint blockBinding)
{
var index = (uint) GL.GetUniformBlockIndex(_handle, blockName);
var index = (uint) GL.GetUniformBlockIndex(Handle, blockName);
_clyde.CheckGlError();
GL.UniformBlockBinding(_handle, index, blockBinding);
GL.UniformBlockBinding(Handle, index, blockBinding);
_clyde.CheckGlError();
}

View File

@@ -23,6 +23,7 @@ namespace Robust.Client.Graphics.Clyde
private class GLUniformBuffer<T> where T : unmanaged, IAppliableUniformSet
{
private readonly Clyde _clyde;
private readonly int _index;
/// <summary>
/// GPU Buffer (only used when uniform buffers are available)
@@ -37,13 +38,23 @@ namespace Robust.Client.Graphics.Clyde
public GLUniformBuffer(Clyde clyde, int index, string? name = null)
{
_clyde = clyde;
_index = index;
if (_clyde._hasGLUniformBuffers)
{
_implUBO = new GLBuffer(_clyde, BufferTarget.UniformBuffer, BufferUsageHint.StreamDraw, name);
unsafe {
_implUBO.Reallocate(sizeof(T));
}
GL.BindBufferBase(BufferRangeTarget.UniformBuffer, index, (int) _implUBO.ObjectHandle);
Rebind();
}
}
public void Rebind()
{
if (_implUBO != null)
{
GL.BindBufferBase(BufferRangeTarget.UniformBuffer, _index, (int) _implUBO!.ObjectHandle);
}
}

View File

@@ -4,7 +4,6 @@ varying highp vec2 Pos;
uniform sampler2D lightMap;
uniform highp vec4 modulate;
#line 1000
// [SHADER_HEADER_CODE]
void main()
@@ -13,7 +12,6 @@ void main()
lowp vec4 COLOR;
#line 10000
// [SHADER_CODE]
lowp vec3 lightSample = texture2D(lightMap, Pos).rgb;

View File

@@ -0,0 +1,16 @@
#ifndef HAS_VARYING_ATTRIBUTE
#define texture2D texture
#define varying in
#define attribute in
#define gl_FragColor colourOutput
out highp vec4 colourOutput;
#endif
varying highp vec2 UV;
uniform sampler2D tex;
void main()
{
gl_FragColor = texture2D(tex, UV);
}

View File

@@ -0,0 +1,20 @@
#ifndef HAS_VARYING_ATTRIBUTE
#define texture2D texture
#define varying out
#define attribute in
#endif
// Vertex position.
/*layout (location = 0)*/ attribute vec2 aPos;
// Texture coordinates.
/*layout (location = 1)*/ attribute vec2 tCoord;
varying vec2 UV;
void main()
{
UV = tCoord;
gl_Position = vec4(aPos, 0.0, 1.0);
}

View File

@@ -89,9 +89,10 @@ uniform highp vec2 TEXTURE_PIXEL_SIZE;
// -- srgb emulation --
#ifdef HAS_SRGB
highp vec4 zTexture(highp vec2 uv)
highp vec4 zTextureSpec(sampler2D tex, highp vec2 uv)
{
return texture2D(TEXTURE, uv);
return texture2D(tex, uv);
}
highp vec4 zAdjustResult(highp vec4 col)
@@ -101,9 +102,9 @@ highp vec4 zAdjustResult(highp vec4 col)
#else
uniform lowp vec2 SRGB_EMU_CONFIG;
highp vec4 zTexture(highp vec2 uv)
highp vec4 zTextureSpec(sampler2D tex, highp vec2 uv)
{
highp vec4 col = texture2D(TEXTURE, uv);
highp vec4 col = texture2D(tex, uv);
if (SRGB_EMU_CONFIG.x > 0.5)
{
return zFromSrgb(col);
@@ -121,5 +122,10 @@ highp vec4 zAdjustResult(highp vec4 col)
}
#endif
highp vec4 zTexture(highp vec2 uv)
{
return zTextureSpec(TEXTURE, uv);
}
// -- Utilities End --

View File

@@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Threading;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Client.Utility;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using GlfwImage = OpenToolkit.GraphicsLibraryFramework.Image;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
private sealed unsafe partial class GlfwWindowingImpl
{
private readonly Dictionary<ClydeHandle, WinThreadCursorReg> _winThreadCursors = new();
private readonly Dictionary<StandardCursorShape, CursorImpl> _standardCursors = new();
public ICursor CursorGetStandard(StandardCursorShape shape)
{
return _standardCursors[shape];
}
public ICursor CursorCreate(Image<Rgba32> image, Vector2i hotSpot)
{
var cloneImg = new Image<Rgba32>(image.Width, image.Height);
image.GetPixelSpan().CopyTo(cloneImg.GetPixelSpan());
var id = _clyde.AllocRid();
SendCmd(new CmdCursorCreate(cloneImg, hotSpot, id));
return new CursorImpl(this, id, false);
}
private void WinThreadCursorCreate(CmdCursorCreate cmd)
{
var (img, (hotX, hotY), id) = cmd;
fixed (Rgba32* pixPtr = img.GetPixelSpan())
{
var gImg = new GlfwImage(img.Width, img.Height, (byte*) pixPtr);
var ptr = GLFW.CreateCursor(gImg, hotX, hotY);
_winThreadCursors.Add(id, new WinThreadCursorReg {Ptr = ptr});
}
img.Dispose();
}
public void CursorSet(WindowReg window, ICursor? cursor)
{
CheckWindowDisposed(window);
var reg = (GlfwWindowReg) window;
if (reg.Cursor == cursor)
{
// Nothing has to be done!
return;
}
if (cursor == null)
{
reg.Cursor = null;
SendCmd(new CmdWinCursorSet((nint) reg.GlfwWindow, default));
return;
}
var impl = (CursorImpl) cursor;
DebugTools.Assert(impl.Owner == this);
if (impl.Id == default)
{
throw new ObjectDisposedException(nameof(cursor));
}
reg.Cursor = impl;
SendCmd(new CmdWinCursorSet((nint) reg.GlfwWindow, impl.Id));
}
private void WinThreadWinCursorSet(CmdWinCursorSet cmd)
{
var window = (Window*) cmd.Window;
Cursor* ptr = null;
if (cmd.Cursor != default)
ptr = _winThreadCursors[cmd.Cursor].Ptr;
if (_win32Experience)
{
// Based on a true story.
Thread.Sleep(15);
}
GLFW.SetCursor(window, ptr);
}
private void WinThreadCursorDestroy(CmdCursorDestroy cmd)
{
var cursorReg = _winThreadCursors[cmd.Cursor];
GLFW.DestroyCursor(cursorReg.Ptr);
_winThreadCursors.Remove(cmd.Cursor);
}
private void InitCursors()
{
// Gets ran on window thread don't worry about it.
void AddStandardCursor(StandardCursorShape standardShape, CursorShape shape)
{
var id = _clyde.AllocRid();
var ptr = GLFW.CreateStandardCursor(shape);
var impl = new CursorImpl(this, id, true);
_standardCursors.Add(standardShape, impl);
_winThreadCursors.Add(id, new WinThreadCursorReg {Ptr = ptr});
}
AddStandardCursor(StandardCursorShape.Arrow, CursorShape.Arrow);
AddStandardCursor(StandardCursorShape.IBeam, CursorShape.IBeam);
AddStandardCursor(StandardCursorShape.Crosshair, CursorShape.Crosshair);
AddStandardCursor(StandardCursorShape.Hand, CursorShape.Hand);
AddStandardCursor(StandardCursorShape.HResize, CursorShape.HResize);
AddStandardCursor(StandardCursorShape.VResize, CursorShape.VResize);
}
private sealed class CursorImpl : ICursor
{
private readonly bool _standard;
public GlfwWindowingImpl Owner { get; }
public ClydeHandle Id { get; private set; }
public CursorImpl(GlfwWindowingImpl clyde, ClydeHandle id, bool standard)
{
_standard = standard;
Owner = clyde;
Id = id;
}
~CursorImpl()
{
DisposeImpl();
}
private void DisposeImpl()
{
Owner.SendCmd(new CmdCursorDestroy(Id));
Id = default;
}
public void Dispose()
{
if (_standard)
{
throw new InvalidOperationException("Can't dispose standard cursor shape.");
}
GC.SuppressFinalize(this);
DisposeImpl();
}
}
public sealed class WinThreadCursorReg
{
public Cursor* Ptr;
}
}
}
}

View File

@@ -0,0 +1,256 @@
using System;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Client.Input;
using Robust.Shared.Map;
namespace Robust.Client.Graphics.Clyde
{
partial class Clyde
{
private partial class GlfwWindowingImpl
{
public void ProcessEvents(bool single=false)
{
while (_eventReader.TryRead(out var ev))
{
try
{
ProcessEvent(ev);
}
catch (Exception e)
{
_sawmill.Error(
"clyde.win",
$"Caught exception in windowing event ({ev.GetType()}):\n{e}");
}
if (single)
break;
}
}
// Block waiting on the windowing -> game thread channel.
// I swear to god do not use this unless you know what you are doing.
private void WaitEvents()
{
_eventReader.WaitToReadAsync().AsTask().Wait();
}
private void ProcessEvent(EventBase evb)
{
switch (evb)
{
case EventMouseButton mb:
ProcessEventMouseButton(mb);
break;
case EventCursorPos cp:
ProcessEventCursorPos(cp);
break;
case EventCursorEnter ev:
ProcessEventCursorEnter(ev);
break;
case EventScroll s:
ProcessEventScroll(s);
break;
case EventKey k:
ProcessEventKey(k);
break;
case EventChar c:
ProcessEventChar(c);
break;
case EventMonitorSetup ms:
ProcessSetupMonitor(ms);
break;
case EventMonitorDestroy md:
ProcessEventDestroyMonitor(md);
break;
case EventWindowCreate wCreate:
FinishWindowCreate(wCreate);
break;
case EventWindowClose wc:
ProcessEventWindowClose(wc);
break;
case EventWindowFocus wf:
ProcessEventWindowFocus(wf);
break;
case EventWindowSize ws:
ProcessEventWindowSize(ws);
break;
case EventWindowPos wp:
ProcessEventWindowPos(wp);
break;
case EventWindowIconify wi:
ProcessEventWindowIconify(wi);
break;
case EventWindowContentScale cs:
ProcessEventWindowContentScale(cs);
break;
default:
_sawmill.Error($"Unknown GLFW event type: {evb.GetType()}");
break;
}
}
private void ProcessEventChar(EventChar ev)
{
_clyde.SendText(new TextEventArgs(ev.CodePoint));
}
private void ProcessEventCursorPos(EventCursorPos ev)
{
var windowReg = FindWindow(ev.Window);
if (windowReg == null)
return;
var newPos = ((float) ev.XPos, (float) ev.YPos) * windowReg.PixelRatio;
var delta = newPos - windowReg.LastMousePos;
windowReg.LastMousePos = newPos;
_clyde._currentHoveredWindow = windowReg;
_clyde.SendMouseMove(new MouseMoveEventArgs(delta, new ScreenCoordinates(newPos, windowReg.Id)));
}
private void ProcessEventCursorEnter(EventCursorEnter ev)
{
var windowReg = FindWindow(ev.Window);
if (windowReg == null)
return;
if (ev.Entered)
{
_clyde._currentHoveredWindow = windowReg;
}
else if (_clyde._currentHoveredWindow == windowReg)
{
_clyde._currentHoveredWindow = null;
}
_clyde.SendMouseEnterLeave(new MouseEnterLeaveEventArgs(windowReg.Handle, ev.Entered));
}
private void ProcessEventKey(EventKey ev)
{
EmitKeyEvent(ConvertGlfwKey(ev.Key), ev.Action, ev.Mods);
}
private void EmitKeyEvent(Keyboard.Key key, InputAction action, KeyModifiers mods)
{
var shift = (mods & KeyModifiers.Shift) != 0;
var alt = (mods & KeyModifiers.Alt) != 0;
var control = (mods & KeyModifiers.Control) != 0;
var system = (mods & KeyModifiers.Super) != 0;
var ev = new KeyEventArgs(
key,
action == InputAction.Repeat,
alt, control, shift, system);
switch (action)
{
case InputAction.Release:
_clyde.SendKeyUp(ev);
break;
case InputAction.Press:
case InputAction.Repeat:
_clyde.SendKeyDown(ev);
break;
default:
throw new ArgumentOutOfRangeException(nameof(action), action, null);
}
}
private void ProcessEventMouseButton(EventMouseButton ev)
{
EmitKeyEvent(Mouse.MouseButtonToKey(ConvertGlfwButton(ev.Button)), ev.Action, ev.Mods);
}
private void ProcessEventScroll(EventScroll ev)
{
var windowReg = FindWindow(ev.Window);
if (windowReg == null)
return;
var eventArgs = new MouseWheelEventArgs(
((float) ev.XOffset, (float) ev.YOffset),
new ScreenCoordinates(windowReg.LastMousePos, windowReg.Id));
_clyde.SendScroll(eventArgs);
}
private void ProcessEventWindowClose(EventWindowClose ev)
{
var windowReg = FindWindow(ev.Window);
if (windowReg == null)
return;
_clyde.SendCloseWindow(windowReg, new WindowClosedEventArgs(windowReg.Handle));
}
private void ProcessEventWindowSize(EventWindowSize ev)
{
var window = ev.Window;
var width = ev.Width;
var height = ev.Height;
var fbW = ev.FramebufferWidth;
var fbH = ev.FramebufferHeight;
var windowReg = FindWindow(window);
if (windowReg == null)
return;
var oldSize = windowReg.FramebufferSize;
windowReg.FramebufferSize = (fbW, fbH);
windowReg.WindowSize = (width, height);
if (fbW == 0 || fbH == 0 || width == 0 || height == 0)
return;
windowReg.PixelRatio = windowReg.FramebufferSize / windowReg.WindowSize;
_clyde.SendWindowResized(windowReg, oldSize);
}
private void ProcessEventWindowPos(EventWindowPos ev)
{
var window = ev.Window;
var x = ev.X;
var y = ev.Y;
var windowReg = FindWindow(window);
if (windowReg == null)
return;
windowReg.WindowPos = (x, y);
}
private void ProcessEventWindowContentScale(EventWindowContentScale ev)
{
var windowReg = FindWindow(ev.Window);
if (windowReg == null)
return;
windowReg.WindowScale = (ev.XScale, ev.YScale);
_clyde.SendWindowContentScaleChanged(new WindowContentScaleEventArgs(windowReg.Handle));
}
private void ProcessEventWindowIconify(EventWindowIconify ev)
{
var windowReg = FindWindow(ev.Window);
if (windowReg == null)
return;
windowReg.IsMinimized = ev.Iconified;
}
private void ProcessEventWindowFocus(EventWindowFocus ev)
{
var windowReg = FindWindow(ev.Window);
if (windowReg == null)
return;
windowReg.IsFocused = ev.Focused;
_clyde.SendWindowFocus(new WindowFocusedEventArgs(ev.Focused, windowReg.Handle));
}
}
}
}

View File

@@ -0,0 +1,225 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Client.Input;
using Robust.Shared.Localization;
using GlfwKey = OpenToolkit.GraphicsLibraryFramework.Keys;
using GlfwButton = OpenToolkit.GraphicsLibraryFramework.MouseButton;
using static Robust.Client.Input.Mouse;
using static Robust.Client.Input.Keyboard;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
private sealed partial class GlfwWindowingImpl
{
// TODO: to avoid having to ask the windowing thread, key names are cached.
// This means they don't update correctly if the user switches keyboard mode. RIP.
private readonly Dictionary<Key, string> _printableKeyNameMap = new();
private void InitKeyMap()
{
// From GLFW's source code: this is the actual list of "printable" keys
// that GetKeyName returns something for.
CacheKey(Keys.KeyPadEqual);
for (var k = Keys.KeyPad0; k <= Keys.KeyPadAdd; k++)
{
CacheKey(k);
}
for (var k = Keys.Apostrophe; k <= Keys.World2; k++)
{
CacheKey(k);
}
void CacheKey(GlfwKey key)
{
var rKey = ConvertGlfwKey(key);
if (rKey == Key.Unknown)
return;
var name = GLFW.GetKeyName(key, 0);
_printableKeyNameMap.Add(rKey, name);
}
}
public string KeyGetName(Keyboard.Key key)
{
if (_printableKeyNameMap.TryGetValue(key, out var name))
{
var textInfo = Thread.CurrentThread.CurrentCulture.TextInfo;
return textInfo.ToTitleCase(name);
}
name = Keyboard.GetSpecialKeyName(key, _loc);
if (name != null)
{
return Loc.GetString(name);
}
return Loc.GetString("<unknown key>");
}
public static Button ConvertGlfwButton(GlfwButton button)
{
return MouseButtonMap[button];
}
private static readonly Dictionary<GlfwButton, Button> MouseButtonMap = new()
{
{GlfwButton.Left, Button.Left},
{GlfwButton.Middle, Button.Middle},
{GlfwButton.Right, Button.Right},
{GlfwButton.Button4, Button.Button4},
{GlfwButton.Button5, Button.Button5},
{GlfwButton.Button6, Button.Button6},
{GlfwButton.Button7, Button.Button7},
{GlfwButton.Button8, Button.Button8},
};
private static readonly Dictionary<GlfwKey, Key> KeyMap;
private static readonly Dictionary<Key, GlfwKey> KeyMapReverse;
internal static Key ConvertGlfwKey(GlfwKey key)
{
if (KeyMap.TryGetValue(key, out var result))
{
return result;
}
return Key.Unknown;
}
internal static GlfwKey ConvertGlfwKeyReverse(Key key)
{
if (KeyMapReverse.TryGetValue(key, out var result))
{
return result;
}
return GlfwKey.Unknown;
}
static GlfwWindowingImpl()
{
KeyMap = new Dictionary<GlfwKey, Key>
{
{GlfwKey.A, Key.A},
{GlfwKey.B, Key.B},
{GlfwKey.C, Key.C},
{GlfwKey.D, Key.D},
{GlfwKey.E, Key.E},
{GlfwKey.F, Key.F},
{GlfwKey.G, Key.G},
{GlfwKey.H, Key.H},
{GlfwKey.I, Key.I},
{GlfwKey.J, Key.J},
{GlfwKey.K, Key.K},
{GlfwKey.L, Key.L},
{GlfwKey.M, Key.M},
{GlfwKey.N, Key.N},
{GlfwKey.O, Key.O},
{GlfwKey.P, Key.P},
{GlfwKey.Q, Key.Q},
{GlfwKey.R, Key.R},
{GlfwKey.S, Key.S},
{GlfwKey.T, Key.T},
{GlfwKey.U, Key.U},
{GlfwKey.V, Key.V},
{GlfwKey.W, Key.W},
{GlfwKey.X, Key.X},
{GlfwKey.Y, Key.Y},
{GlfwKey.Z, Key.Z},
{GlfwKey.D0, Key.Num0},
{GlfwKey.D1, Key.Num1},
{GlfwKey.D2, Key.Num2},
{GlfwKey.D3, Key.Num3},
{GlfwKey.D4, Key.Num4},
{GlfwKey.D5, Key.Num5},
{GlfwKey.D6, Key.Num6},
{GlfwKey.D7, Key.Num7},
{GlfwKey.D8, Key.Num8},
{GlfwKey.D9, Key.Num9},
{GlfwKey.KeyPad0, Key.NumpadNum0},
{GlfwKey.KeyPad1, Key.NumpadNum1},
{GlfwKey.KeyPad2, Key.NumpadNum2},
{GlfwKey.KeyPad3, Key.NumpadNum3},
{GlfwKey.KeyPad4, Key.NumpadNum4},
{GlfwKey.KeyPad5, Key.NumpadNum5},
{GlfwKey.KeyPad6, Key.NumpadNum6},
{GlfwKey.KeyPad7, Key.NumpadNum7},
{GlfwKey.KeyPad8, Key.NumpadNum8},
{GlfwKey.KeyPad9, Key.NumpadNum9},
{GlfwKey.Escape, Key.Escape},
{GlfwKey.LeftControl, Key.Control},
{GlfwKey.RightControl, Key.Control},
{GlfwKey.RightShift, Key.Shift},
{GlfwKey.LeftShift, Key.Shift},
{GlfwKey.LeftAlt, Key.Alt},
{GlfwKey.RightAlt, Key.Alt},
{GlfwKey.LeftSuper, Key.LSystem},
{GlfwKey.RightSuper, Key.RSystem},
{GlfwKey.Menu, Key.Menu},
{GlfwKey.LeftBracket, Key.LBracket},
{GlfwKey.RightBracket, Key.RBracket},
{GlfwKey.Semicolon, Key.SemiColon},
{GlfwKey.Comma, Key.Comma},
{GlfwKey.Period, Key.Period},
{GlfwKey.Apostrophe, Key.Apostrophe},
{GlfwKey.Slash, Key.Slash},
{GlfwKey.Backslash, Key.BackSlash},
{GlfwKey.GraveAccent, Key.Tilde},
{GlfwKey.Equal, Key.Equal},
{GlfwKey.Space, Key.Space},
{GlfwKey.Enter, Key.Return},
{GlfwKey.KeyPadEnter, Key.NumpadEnter},
{GlfwKey.Backspace, Key.BackSpace},
{GlfwKey.Tab, Key.Tab},
{GlfwKey.PageUp, Key.PageUp},
{GlfwKey.PageDown, Key.PageDown},
{GlfwKey.End, Key.End},
{GlfwKey.Home, Key.Home},
{GlfwKey.Insert, Key.Insert},
{GlfwKey.Delete, Key.Delete},
{GlfwKey.Minus, Key.Minus},
{GlfwKey.KeyPadAdd, Key.NumpadAdd},
{GlfwKey.KeyPadSubtract, Key.NumpadSubtract},
{GlfwKey.KeyPadDivide, Key.NumpadDivide},
{GlfwKey.KeyPadMultiply, Key.NumpadMultiply},
{GlfwKey.KeyPadDecimal, Key.NumpadDecimal},
{GlfwKey.Left, Key.Left},
{GlfwKey.Right, Key.Right},
{GlfwKey.Up, Key.Up},
{GlfwKey.Down, Key.Down},
{GlfwKey.F1, Key.F1},
{GlfwKey.F2, Key.F2},
{GlfwKey.F3, Key.F3},
{GlfwKey.F4, Key.F4},
{GlfwKey.F5, Key.F5},
{GlfwKey.F6, Key.F6},
{GlfwKey.F7, Key.F7},
{GlfwKey.F8, Key.F8},
{GlfwKey.F9, Key.F9},
{GlfwKey.F10, Key.F10},
{GlfwKey.F11, Key.F11},
{GlfwKey.F12, Key.F12},
{GlfwKey.F13, Key.F13},
{GlfwKey.F14, Key.F14},
{GlfwKey.F15, Key.F15},
{GlfwKey.Pause, Key.Pause},
};
KeyMapReverse = new Dictionary<Key, GlfwKey>();
foreach (var (key, value) in KeyMap)
{
KeyMapReverse[value] = key;
}
}
}
}
}

View File

@@ -0,0 +1,133 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Shared.Utility;
using GlfwVideoMode = OpenToolkit.GraphicsLibraryFramework.VideoMode;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
private sealed unsafe partial class GlfwWindowingImpl
{
// TODO: GLFW doesn't have any events for complex monitor config changes,
// so we need some way to reload stuff if e.g. the primary monitor changes.
// Still better than SDL2 though which doesn't acknowledge monitor changes at all.
// Monitors are created at GLFW's will,
// so we need to make SURE monitors keep existing while operating on them.
// because, you know, async. Don't want a use-after-free.
private readonly Dictionary<int, WinThreadMonitorReg> _winThreadMonitors = new();
// Can't use ClydeHandle because it's 64 bit.
private int _nextMonitorId = 1;
private int _primaryMonitorId;
private readonly Dictionary<int, GlfwMonitorReg> _monitors = new();
private void InitMonitors()
{
var monitors = GLFW.GetMonitorsRaw(out var count);
for (var i = 0; i < count; i++)
{
WinThreadSetupMonitor(monitors[i]);
}
var primaryMonitor = GLFW.GetPrimaryMonitor();
var up = GLFW.GetMonitorUserPointer(primaryMonitor);
_primaryMonitorId = (int) up;
ProcessEvents();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void WinThreadSetupMonitor(Monitor* monitor)
{
var id = _nextMonitorId++;
DebugTools.Assert(GLFW.GetMonitorUserPointer(monitor) == null,
"GLFW window already has user pointer??");
var name = GLFW.GetMonitorName(monitor);
var videoMode = GLFW.GetVideoMode(monitor);
var modesPtr = GLFW.GetVideoModesRaw(monitor, out var modeCount);
var modes = new VideoMode[modeCount];
for (var i = 0; i < modes.Length; i++)
{
modes[i] = ConvertVideoMode(modesPtr[i]);
}
GLFW.SetMonitorUserPointer(monitor, (void*) id);
_winThreadMonitors.Add(id, new WinThreadMonitorReg {Ptr = monitor});
SendEvent(new EventMonitorSetup(id, name, ConvertVideoMode(*videoMode), modes));
}
private static VideoMode ConvertVideoMode(in GlfwVideoMode mode)
{
return new()
{
Width = (ushort) mode.Width,
Height = (ushort) mode.Height,
RedBits = (byte) mode.RedBits,
RefreshRate = (ushort) mode.RefreshRate,
GreenBits = (byte) mode.GreenBits,
BlueBits = (byte) mode.BlueBits,
};
}
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(impl);
_monitors[ev.Id] = new GlfwMonitorReg
{
Id = ev.Id,
Handle = impl
};
}
private void WinThreadDestroyMonitor(Monitor* monitor)
{
var ptr = (int) GLFW.GetMonitorUserPointer(monitor);
if (ptr == 0)
{
var name = GLFW.GetMonitorName(monitor);
_sawmill.Warning($"Monitor '{name}' had no user pointer set??");
return;
}
_winThreadMonitors.Remove(ptr);
GLFW.SetMonitorUserPointer(monitor, null);
SendEvent(new EventMonitorDestroy(ptr));
}
private void ProcessEventDestroyMonitor(EventMonitorDestroy ev)
{
var reg = _monitors[ev.Id];
_monitors.Remove(ev.Id);
_clyde._monitorHandles.Remove(reg.Handle);
}
private sealed class GlfwMonitorReg : MonitorReg
{
public int Id;
}
private sealed class WinThreadMonitorReg
{
public Monitor* Ptr;
}
}
}
}

View File

@@ -0,0 +1,223 @@
using System.Threading.Tasks;
using OpenToolkit.GraphicsLibraryFramework;
namespace Robust.Client.Graphics.Clyde
{
partial class Clyde
{
private unsafe partial class GlfwWindowingImpl
{
// Keep delegates around to prevent GC issues.
private GLFWCallbacks.ErrorCallback? _errorCallback;
private GLFWCallbacks.MonitorCallback? _monitorCallback;
private GLFWCallbacks.CharCallback? _charCallback;
private GLFWCallbacks.CursorPosCallback? _cursorPosCallback;
private GLFWCallbacks.CursorEnterCallback? _cursorEnterCallback;
private GLFWCallbacks.KeyCallback? _keyCallback;
private GLFWCallbacks.MouseButtonCallback? _mouseButtonCallback;
private GLFWCallbacks.ScrollCallback? _scrollCallback;
private GLFWCallbacks.WindowCloseCallback? _windowCloseCallback;
private GLFWCallbacks.WindowPosCallback? _windowPosCallback;
private GLFWCallbacks.WindowSizeCallback? _windowSizeCallback;
private GLFWCallbacks.WindowContentScaleCallback? _windowContentScaleCallback;
private GLFWCallbacks.WindowIconifyCallback? _windowIconifyCallback;
private GLFWCallbacks.WindowFocusCallback? _windowFocusCallback;
private void StoreCallbacks()
{
_errorCallback = OnGlfwError;
_monitorCallback = OnGlfwMonitor;
_charCallback = OnGlfwChar;
_cursorPosCallback = OnGlfwCursorPos;
_cursorEnterCallback = OnGlfwCursorEnter;
_keyCallback = OnGlfwKey;
_mouseButtonCallback = OnGlfwMouseButton;
_scrollCallback = OnGlfwScroll;
_windowCloseCallback = OnGlfwWindowClose;
_windowSizeCallback = OnGlfwWindowSize;
_windowPosCallback = OnGlfwWindowPos;
_windowContentScaleCallback = OnGlfwWindowContentScale;
_windowIconifyCallback = OnGlfwWindowIconify;
_windowFocusCallback = OnGlfwWindowFocus;
}
private void SetupGlobalCallbacks()
{
GLFW.SetMonitorCallback(_monitorCallback);
}
private void OnGlfwMonitor(Monitor* monitor, ConnectedState state)
{
if (state == ConnectedState.Connected)
WinThreadSetupMonitor(monitor);
else
WinThreadDestroyMonitor(monitor);
}
private void OnGlfwChar(Window* window, uint codepoint)
{
SendEvent(new EventChar((nint) window, codepoint));
}
private void OnGlfwCursorPos(Window* window, double x, double y)
{
// System.Console.WriteLine($"{(nint)window:X16}: {x},{y}");
SendEvent(new EventCursorPos((nint) window, x, y));
}
private void OnGlfwCursorEnter(Window* window, bool entered)
{
// System.Console.WriteLine($"{(nint)window:X16}: {entered}");
SendEvent(new EventCursorEnter((nint) window, entered));
}
private void OnGlfwKey(Window* window, Keys key, int scanCode, InputAction action, KeyModifiers mods)
{
SendEvent(new EventKey((nint) window, key, scanCode, action, mods));
}
private void OnGlfwMouseButton(Window* window, MouseButton button, InputAction action, KeyModifiers mods)
{
SendEvent(new EventMouseButton((nint) window, button, action, mods));
}
private void OnGlfwScroll(Window* window, double offsetX, double offsetY)
{
SendEvent(new EventScroll((nint) window, offsetX, offsetY));
}
private void OnGlfwWindowClose(Window* window)
{
SendEvent(new EventWindowClose((nint) window));
}
private void OnGlfwWindowSize(Window* window, int width, int height)
{
GLFW.GetFramebufferSize(window, out var fbW, out var fbH);
SendEvent(new EventWindowSize((nint) window, width, height, fbW, fbH));
}
private void OnGlfwWindowPos(Window* window, int x, int y)
{
SendEvent(new EventWindowPos((nint) window, x, y));
}
private void OnGlfwWindowContentScale(Window* window, float xScale, float yScale)
{
SendEvent(new EventWindowContentScale((nint) window, xScale, yScale));
}
private void OnGlfwWindowIconify(Window* window, bool iconified)
{
SendEvent(new EventWindowIconify((nint) window, iconified));
}
private void OnGlfwWindowFocus(Window* window, bool focused)
{
SendEvent(new EventWindowFocus((nint) window, focused));
}
// NOTE: events do not correspond 1:1 to GLFW events
// This is because they need to pack all the data required
// for the game-thread event handling.
private abstract record EventBase;
private record EventMouseButton(
nint Window,
MouseButton Button,
InputAction Action,
KeyModifiers Mods
) : EventBase;
private record EventCursorPos(
nint Window,
double XPos,
double YPos
) : EventBase;
private record EventCursorEnter(
nint Window,
bool Entered
) : EventBase;
private record EventScroll(
nint Window,
double XOffset,
double YOffset
) : EventBase;
private record EventKey(
nint Window,
Keys Key,
int ScanCode,
InputAction Action,
KeyModifiers Mods
) : EventBase;
private record EventChar
(
nint Window,
uint CodePoint
) : EventBase;
private record EventWindowClose
(
nint Window
) : EventBase;
private record EventWindowCreate(
GlfwWindowCreateResult Result,
TaskCompletionSource<GlfwWindowCreateResult> Tcs
) : EventBase;
private record EventWindowSize
(
nint Window,
int Width,
int Height,
int FramebufferWidth,
int FramebufferHeight
) : EventBase;
private record EventWindowPos
(
nint Window,
int X,
int Y
) : EventBase;
private record EventWindowContentScale
(
nint Window,
float XScale,
float YScale
) : EventBase;
private record EventWindowIconify
(
nint Window,
bool Iconified
) : EventBase;
private record EventWindowFocus
(
nint Window,
bool Focused
) : EventBase;
private record EventMonitorSetup
(
int Id,
string Name,
VideoMode CurrentMode,
VideoMode[] AllModes
) : EventBase;
private record EventMonitorDestroy
(
int Id
) : EventBase;
}
}
}

View File

@@ -0,0 +1,250 @@
using System.Runtime.InteropServices;
using System.Threading.Channels;
using System.Threading.Tasks;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Shared;
using Robust.Shared.Maths;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
private sealed partial class GlfwWindowingImpl
{
// glfwPostEmptyEvent is broken on macOS and crashes when not called from the main thread
// (despite what the docs claim, and yes this makes it useless).
// Because of this, we just forego it and use glfwWaitEventsTimeout on macOS instead.
private static readonly bool IsMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
private bool _windowingRunning;
private ChannelWriter<CmdBase> _cmdWriter = default!;
private ChannelReader<CmdBase> _cmdReader = default!;
private ChannelReader<EventBase> _eventReader = default!;
private ChannelWriter<EventBase> _eventWriter = default!;
//
// Let it be forever recorded that I started work on windowing thread separation
// because win32 SetCursor was taking 15ms spinwaiting inside the kernel.
//
//
// To avoid stutters and solve some other problems like smooth window resizing,
// we (by default) use a separate thread for windowing.
//
// Types like WindowReg are considered to be part of the "game" thread
// and should **NOT** be directly updated/accessed from the windowing thread.
//
// Got that?
//
//
// The windowing -> game channel is bounded so that the OS properly detects the game as locked
// up when it actually locks up. The other way around is not bounded to avoid deadlocks.
// This also means that all operations like clipboard reading, window creation, etc....
// have to be asynchronous.
//
public void EnterWindowLoop()
{
_windowingRunning = true;
while (_windowingRunning)
{
if (IsMacOS)
GLFW.WaitEventsTimeout(0.008);
else
GLFW.WaitEvents();
while (_cmdReader.TryRead(out var cmd))
{
ProcessGlfwCmd(cmd);
}
}
}
private void ProcessGlfwCmd(CmdBase cmdb)
{
switch (cmdb)
{
case CmdTerminate:
_windowingRunning = false;
break;
case CmdWinSetTitle cmd:
WinThreadWinSetTitle(cmd);
break;
case CmdWinSetMonitor cmd:
WinThreadWinSetMonitor(cmd);
break;
case CmdWinSetVisible cmd:
WinThreadWinSetVisible(cmd);
break;
case CmdWinRequestAttention cmd:
WinThreadWinRequestAttention(cmd);
break;
case CmdWinSetFullscreen cmd:
WinThreadWinSetFullscreen(cmd);
break;
case CmdWinCreate cmd:
WinThreadWinCreate(cmd);
break;
case CmdWinDestroy cmd:
WinThreadWinDestroy(cmd);
break;
case CmdSetClipboard cmd:
WinThreadSetClipboard(cmd);
break;
case CmdGetClipboard cmd:
WinThreadGetClipboard(cmd);
break;
case CmdCursorCreate cmd:
WinThreadCursorCreate(cmd);
break;
case CmdCursorDestroy cmd:
WinThreadCursorDestroy(cmd);
break;
case CmdWinCursorSet cmd:
WinThreadWinCursorSet(cmd);
break;
}
}
public void TerminateWindowLoop()
{
SendCmd(new CmdTerminate());
}
private void InitChannels()
{
var cmdChannel = Channel.CreateUnbounded<CmdBase>(new UnboundedChannelOptions
{
SingleReader = true,
// Finalizers can write to this in some cases.
SingleWriter = false
});
_cmdReader = cmdChannel.Reader;
_cmdWriter = cmdChannel.Writer;
var bufferSize = _cfg.GetCVar(CVars.DisplayInputBufferSize);
var eventChannel = Channel.CreateBounded<EventBase>(new BoundedChannelOptions(bufferSize)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = true,
SingleWriter = true
});
_eventReader = eventChannel.Reader;
_eventWriter = eventChannel.Writer;
}
private void SendCmd(CmdBase cmd)
{
_cmdWriter.TryWrite(cmd);
// Post empty event to unstuck WaitEvents if necessary.
if (!IsMacOS)
GLFW.PostEmptyEvent();
}
private void SendEvent(EventBase ev)
{
var task = _eventWriter.WriteAsync(ev);
if (!task.IsCompletedSuccessfully)
{
task.AsTask().Wait();
}
}
private abstract record CmdBase;
private sealed record CmdTerminate : CmdBase;
private sealed record CmdWinSetTitle(
nint Window,
string Title
) : CmdBase;
private sealed record CmdWinSetMonitor(
nint Window,
int MonitorId,
int X, int Y,
int W, int H,
int RefreshRate
) : CmdBase;
private sealed record CmdWinMaximize(
nint Window
) : CmdBase;
private sealed record CmdWinSetFullscreen(
nint Window
) : CmdBase;
private sealed record CmdWinSetVisible(
nint Window,
bool Visible
) : CmdBase;
private sealed record CmdWinRequestAttention(
nint Window
) : CmdBase;
private sealed record CmdWinCreate(
Renderer Renderer,
WindowCreateParameters Parameters,
nint ShareWindow,
TaskCompletionSource<GlfwWindowCreateResult> Tcs
) : CmdBase;
private sealed record CmdWinDestroy(
nint Window
) : CmdBase;
private sealed record GlfwWindowCreateResult(
GlfwWindowReg? Reg,
(string Desc, ErrorCode Code)? Error
);
private sealed record CmdSetClipboard(
nint Window,
string Text
) : CmdBase;
private sealed record CmdGetClipboard(
nint Window,
TaskCompletionSource<string> Tcs
) : CmdBase;
private sealed record CmdWinCursorSet(
nint Window,
ClydeHandle Cursor
) : CmdBase;
private sealed record CmdCursorCreate(
Image<Rgba32> Bytes,
Vector2i Hotspot,
ClydeHandle Cursor
) : CmdBase;
private sealed record CmdCursorDestroy(
ClydeHandle Cursor
) : CmdBase;
}
}
}

View File

@@ -0,0 +1,687 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using OpenToolkit;
using OpenToolkit.Graphics.OpenGL4;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Client.Input;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp.PixelFormats;
using GlfwImage = OpenToolkit.GraphicsLibraryFramework.Image;
using Monitor = OpenToolkit.GraphicsLibraryFramework.Monitor;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
// Wait for it.
private sealed partial class GlfwWindowingImpl
{
private readonly List<GlfwWindowReg> _windows = new();
public IReadOnlyList<WindowReg> AllWindows => _windows;
public IBindingsContext GraphicsBindingContext => _mainGraphicsContext;
public WindowReg? MainWindow => _mainWindow;
private GlfwWindowReg? _mainWindow;
private GlfwBindingsContext _mainGraphicsContext = default!;
private int _nextWindowId = 1;
public async Task<WindowHandle> WindowCreate(WindowCreateParameters parameters)
{
// tfw await not allowed in unsafe contexts
// GL APIs don't take kindly to making a new window without unbinding the main context. Great.
// Leaving code for async path in, in case it works on like GLX.
var unbindContextAndBlock = true;
DebugTools.AssertNotNull(_mainWindow);
Task<GlfwWindowCreateResult> task;
unsafe
{
if (unbindContextAndBlock)
GLFW.MakeContextCurrent(null);
task = SharedWindowCreate(
_clyde._chosenRenderer,
parameters,
_mainWindow!.GlfwWindow);
}
if (unbindContextAndBlock)
{
unsafe
{
// Block the main thread (to avoid stuff like texture uploads being problematic).
WaitWindowCreate(task);
if (unbindContextAndBlock)
GLFW.MakeContextCurrent(_mainWindow.GlfwWindow);
}
}
else
{
await task;
}
var (reg, error) = await task;
if (reg == null)
{
var (desc, errCode) = error!.Value;
throw new GlfwException($"{errCode}: {desc}");
}
_clyde.CreateWindowRenderTexture(reg);
_clyde.InitWindowBlitThread(reg);
unsafe
{
GLFW.MakeContextCurrent(_mainWindow.GlfwWindow);
}
return reg.Handle;
}
}
// Yes, you read that right.
private sealed unsafe partial class GlfwWindowingImpl
{
public bool TryInitMainWindow(Renderer renderer, [NotNullWhen(false)] out string? error)
{
var width = _cfg.GetCVar(CVars.DisplayWidth);
var height = _cfg.GetCVar(CVars.DisplayHeight);
var prevWidth = width;
var prevHeight = height;
IClydeMonitor? monitor = null;
var fullscreen = false;
if (_clyde._windowMode == WindowMode.Fullscreen)
{
monitor = _monitors[_primaryMonitorId].Handle;
width = monitor.Size.X;
height = monitor.Size.Y;
fullscreen = true;
}
var parameters = new WindowCreateParameters
{
Width = width,
Height = height,
Monitor = monitor,
Fullscreen = fullscreen
};
var windowTask = SharedWindowCreate(renderer, parameters, null);
WaitWindowCreate(windowTask);
var (reg, err) = windowTask.Result;
if (reg == null)
{
var (desc, code) = err!.Value;
error = $"[{code}] {desc}";
return false;
}
DebugTools.Assert(reg.Id == WindowId.Main);
_mainWindow = reg;
reg.IsMainWindow = true;
if (fullscreen)
{
reg.PrevWindowSize = (prevWidth, prevHeight);
reg.PrevWindowPos = (50, 50);
}
UpdateVSync();
error = null;
return true;
}
private void WaitWindowCreate(Task<GlfwWindowCreateResult> windowTask)
{
while (!windowTask.IsCompleted)
{
// Keep processing events until the window task gives either an error or success.
WaitEvents();
ProcessEvents(single: true);
}
}
private Task<GlfwWindowCreateResult> SharedWindowCreate(
Renderer renderer,
WindowCreateParameters parameters,
Window* share)
{
// Yes we ping-pong this TCS through the window thread and back, deal with it.
var tcs = new TaskCompletionSource<GlfwWindowCreateResult>();
SendCmd(new CmdWinCreate(
renderer,
parameters,
(nint) share,
tcs));
return tcs.Task;
}
private void FinishWindowCreate(EventWindowCreate ev)
{
var (res, tcs) = ev;
var reg = res.Reg;
if (reg != null)
{
_windows.Add(reg);
_clyde._windowHandles.Add(reg.Handle);
}
tcs.TrySetResult(res);
}
private void WinThreadWinCreate(CmdWinCreate cmd)
{
var (renderer, parameters, share, tcs) = cmd;
var window = CreateGlfwWindowForRenderer(renderer, parameters, (Window*) share);
if (window == null)
{
var err = GLFW.GetError(out var desc);
SendEvent(new EventWindowCreate(new GlfwWindowCreateResult(null, (desc, 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);
SendEvent(new EventWindowCreate(new GlfwWindowCreateResult(reg, null), tcs));
}
private void WinThreadWinDestroy(CmdWinDestroy cmd)
{
GLFW.DestroyWindow((Window*) cmd.Window);
}
public void WindowSetTitle(WindowReg window, string title)
{
CheckWindowDisposed(window);
if (title == null)
{
throw new ArgumentNullException(nameof(title));
}
var reg = (GlfwWindowReg) window;
SendCmd(new CmdWinSetTitle((nint) reg.GlfwWindow, title));
}
private void WinThreadWinSetTitle(CmdWinSetTitle cmd)
{
GLFW.SetWindowTitle((Window*) cmd.Window, cmd.Title);
}
public void WindowSetMonitor(WindowReg window, IClydeMonitor monitor)
{
CheckWindowDisposed(window);
var winReg = (GlfwWindowReg) window;
var monitorImpl = (MonitorHandle) monitor;
SendCmd(new CmdWinSetMonitor(
(nint) winReg.GlfwWindow,
monitorImpl.Id,
0, 0,
monitorImpl.Size.X, monitorImpl.Size.Y,
monitorImpl.RefreshRate));
}
private void WinThreadWinSetMonitor(CmdWinSetMonitor cmd)
{
Monitor* monitorPtr;
if (cmd.MonitorId == 0)
{
monitorPtr = null;
}
else if (_winThreadMonitors.TryGetValue(cmd.MonitorId, out var monitorReg))
{
monitorPtr = monitorReg.Ptr;
}
else
{
return;
}
GLFW.SetWindowMonitor(
(Window*) cmd.Window,
monitorPtr,
cmd.X, cmd.Y,
cmd.W, cmd.H,
cmd.RefreshRate
);
}
public void WindowSetVisible(WindowReg window, bool visible)
{
var reg = (GlfwWindowReg) window;
reg.IsVisible = visible;
SendCmd(new CmdWinSetVisible((nint) reg.GlfwWindow, visible));
}
private void WinThreadWinSetVisible(CmdWinSetVisible cmd)
{
var win = (Window*) cmd.Window;
if (cmd.Visible)
{
GLFW.ShowWindow(win);
}
else
{
GLFW.HideWindow(win);
}
}
public void WindowRequestAttention(WindowReg window)
{
CheckWindowDisposed(window);
var reg = (GlfwWindowReg) window;
SendCmd(new CmdWinRequestAttention((nint) reg.GlfwWindow));
}
private void WinThreadWinRequestAttention(CmdWinRequestAttention cmd)
{
var win = (Window*) cmd.Window;
GLFW.RequestWindowAttention(win);
}
public void WindowSwapBuffers(WindowReg window)
{
CheckWindowDisposed(window);
var reg = (GlfwWindowReg) window;
GLFW.SwapBuffers(reg.GlfwWindow);
}
public void UpdateVSync()
{
if (_mainWindow == null)
return;
GLFW.MakeContextCurrent(_mainWindow!.GlfwWindow);
GLFW.SwapInterval(_clyde._vSync ? 1 : 0);
}
public void UpdateMainWindowMode()
{
if (_mainWindow == null)
{
return;
}
var win = _mainWindow;
if (_clyde._windowMode == WindowMode.Fullscreen)
{
_mainWindow.PrevWindowSize = win.WindowSize;
_mainWindow.PrevWindowPos = win.PrevWindowPos;
SendCmd(new CmdWinSetFullscreen((nint) _mainWindow.GlfwWindow));
}
else
{
SendCmd(new CmdWinSetMonitor(
(nint) _mainWindow.GlfwWindow,
0,
_mainWindow.PrevWindowPos.X, _mainWindow.PrevWindowPos.Y,
_mainWindow.PrevWindowSize.X, _mainWindow.PrevWindowSize.Y,
0
));
}
}
private void WinThreadWinSetFullscreen(CmdWinSetFullscreen cmd)
{
var ptr = (Window*) cmd.Window;
GLFW.GetWindowSize(ptr, out var w, out var h);
GLFW.GetWindowPos(ptr, out var x, out var y);
var monitor = MonitorForWindow(ptr);
var mode = GLFW.GetVideoMode(monitor);
GLFW.SetWindowMonitor(
ptr,
monitor,
0, 0,
mode->Width, mode->Height,
mode->RefreshRate);
}
// glfwGetWindowMonitor only works for fullscreen windows.
// Picks the monitor with the top-left corner of the window.
private Monitor* MonitorForWindow(Window* window)
{
GLFW.GetWindowPos(window, out var winPosX, out var winPosY);
var monitors = GLFW.GetMonitorsRaw(out var count);
for (var i = 0; i < count; i++)
{
var monitor = monitors[i];
GLFW.GetMonitorPos(monitor, out var monPosX, out var monPosY);
var videoMode = GLFW.GetVideoMode(monitor);
var box = Box2i.FromDimensions(monPosX, monPosY, videoMode->Width, videoMode->Height);
if (box.Contains(winPosX, winPosY))
return monitor;
}
// Fallback
return GLFW.GetPrimaryMonitor();
}
public uint? WindowGetX11Id(WindowReg window)
{
CheckWindowDisposed(window);
var reg = (GlfwWindowReg) window;
try
{
return GLFW.GetX11Window(reg.GlfwWindow);
}
catch (EntryPointNotFoundException)
{
return null;
}
}
public void WindowDestroy(WindowReg window)
{
var reg = (GlfwWindowReg) window;
if (reg.IsDisposed)
return;
reg.IsDisposed = true;
SendCmd(new CmdWinDestroy((nint) reg.GlfwWindow));
_windows.Remove(reg);
_clyde._windowHandles.Remove(reg.Handle);
_clyde.DestroyWindow?.Invoke(new WindowDestroyedEventArgs(window.Handle));
}
private Window* CreateGlfwWindowForRenderer(
Renderer r,
WindowCreateParameters parameters,
Window* contextShare)
{
#if DEBUG
GLFW.WindowHint(WindowHintBool.OpenGLDebugContext, true);
#endif
GLFW.WindowHint(WindowHintString.X11ClassName, "SS14");
GLFW.WindowHint(WindowHintString.X11InstanceName, "SS14");
if (r == Renderer.OpenGL33)
{
GLFW.WindowHint(WindowHintInt.ContextVersionMajor, 3);
GLFW.WindowHint(WindowHintInt.ContextVersionMinor, 3);
GLFW.WindowHint(WindowHintBool.OpenGLForwardCompat, true);
GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlApi);
GLFW.WindowHint(WindowHintContextApi.ContextCreationApi, ContextApi.NativeContextApi);
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Core);
GLFW.WindowHint(WindowHintBool.SrgbCapable, true);
}
else if (r == Renderer.OpenGL31)
{
GLFW.WindowHint(WindowHintInt.ContextVersionMajor, 3);
GLFW.WindowHint(WindowHintInt.ContextVersionMinor, 1);
GLFW.WindowHint(WindowHintBool.OpenGLForwardCompat, false);
GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlApi);
GLFW.WindowHint(WindowHintContextApi.ContextCreationApi, ContextApi.NativeContextApi);
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
GLFW.WindowHint(WindowHintBool.SrgbCapable, true);
}
else if (r == Renderer.OpenGLES2)
{
GLFW.WindowHint(WindowHintInt.ContextVersionMajor, 2);
GLFW.WindowHint(WindowHintInt.ContextVersionMinor, 0);
GLFW.WindowHint(WindowHintBool.OpenGLForwardCompat, true);
GLFW.WindowHint(WindowHintClientApi.ClientApi, ClientApi.OpenGlEsApi);
// GLES2 is initialized through EGL to allow ANGLE usage.
// (It may be an idea to make this a configuration cvar)
GLFW.WindowHint(WindowHintContextApi.ContextCreationApi, ContextApi.EglContextApi);
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
GLFW.WindowHint(WindowHintBool.SrgbCapable, false);
}
Monitor* monitor = null;
if (parameters.Monitor != null &&
_winThreadMonitors.TryGetValue(parameters.Monitor.Id, out var monitorReg))
{
monitor = monitorReg.Ptr;
}
GLFW.WindowHint(WindowHintBool.Visible, false);
var window = GLFW.CreateWindow(
parameters.Width, parameters.Height,
parameters.Title,
parameters.Fullscreen ? monitor : null,
contextShare);
// Check if window failed to create.
if (window == null)
return null;
if (parameters.Maximized)
{
GLFW.GetMonitorPos(monitor, out var x, out var y);
GLFW.SetWindowPos(window, x, y);
GLFW.MaximizeWindow(window);
}
if (parameters.Visible)
{
GLFW.ShowWindow(window);
}
return window;
}
private GlfwWindowReg WinThreadSetupWindow(Window* window)
{
var reg = new GlfwWindowReg
{
GlfwWindow = window,
Id = new WindowId(_nextWindowId++)
};
var handle = new WindowHandle(_clyde, reg);
reg.Handle = handle;
LoadWindowIcon(window);
GLFW.SetCharCallback(window, _charCallback);
GLFW.SetKeyCallback(window, _keyCallback);
GLFW.SetWindowCloseCallback(window, _windowCloseCallback);
GLFW.SetCursorPosCallback(window, _cursorPosCallback);
GLFW.SetCursorEnterCallback(window, _cursorEnterCallback);
GLFW.SetWindowSizeCallback(window, _windowSizeCallback);
GLFW.SetWindowPosCallback(window, _windowPosCallback);
GLFW.SetScrollCallback(window, _scrollCallback);
GLFW.SetMouseButtonCallback(window, _mouseButtonCallback);
GLFW.SetWindowContentScaleCallback(window, _windowContentScaleCallback);
GLFW.SetWindowIconifyCallback(window, _windowIconifyCallback);
GLFW.SetWindowFocusCallback(window, _windowFocusCallback);
GLFW.GetFramebufferSize(window, out var fbW, out var fbH);
reg.FramebufferSize = (fbW, fbH);
GLFW.GetWindowContentScale(window, out var scaleX, out var scaleY);
reg.WindowScale = (scaleX, scaleY);
GLFW.GetWindowSize(window, out var w, out var h);
reg.PrevWindowSize = reg.WindowSize = (w, h);
GLFW.GetWindowPos(window, out var x, out var y);
reg.PrevWindowPos = (x, y);
reg.PixelRatio = reg.FramebufferSize / reg.WindowSize;
return reg;
}
private WindowReg? FindWindow(nint window) => FindWindow((Window*) window);
private WindowReg? FindWindow(Window* window)
{
foreach (var windowReg in _windows)
{
if (windowReg.GlfwWindow == window)
{
return windowReg;
}
}
return null;
}
public int KeyGetScanCode(Keyboard.Key key)
{
return GLFW.GetKeyScancode(ConvertGlfwKeyReverse(key));
}
public string KeyGetNameScanCode(int scanCode)
{
return GLFW.GetKeyName(Keys.Unknown, scanCode);
}
public Task<string> ClipboardGetText()
{
var tcs = new TaskCompletionSource<string>();
SendCmd(new CmdGetClipboard((nint) _mainWindow!.GlfwWindow, tcs));
return tcs.Task;
}
private void WinThreadGetClipboard(CmdGetClipboard cmd)
{
var clipboard = GLFW.GetClipboardString((Window*) cmd.Window);
// Don't have to care about synchronization I don't think so just fire this immediately.
cmd.Tcs.TrySetResult(clipboard);
}
public void ClipboardSetText(string text)
{
SendCmd(new CmdSetClipboard((nint) _mainWindow!.GlfwWindow, text));
}
private void WinThreadSetClipboard(CmdSetClipboard cmd)
{
GLFW.SetClipboardString((Window*) cmd.Window, cmd.Text);
}
public void LoadWindowIcon(Window* window)
{
var icons = _clyde.LoadWindowIcons().ToArray();
// Turn each image into a byte[] so we can actually pin their contents.
// Wish I knew a clean way to do this without allocations.
var images = icons
.Select(i => (MemoryMarshal.Cast<Rgba32, byte>(i.GetPixelSpan()).ToArray(), i.Width, i.Height))
.ToList();
// ReSharper disable once SuggestVarOrType_Elsewhere
Span<GCHandle> handles = stackalloc GCHandle[images.Count];
Span<GlfwImage> glfwImages = stackalloc GlfwImage[images.Count];
for (var i = 0; i < images.Count; i++)
{
var image = images[i];
handles[i] = GCHandle.Alloc(image.Item1, GCHandleType.Pinned);
var addrOfPinnedObject = (byte*) handles[i].AddrOfPinnedObject();
glfwImages[i] = new GlfwImage(image.Width, image.Height, addrOfPinnedObject);
}
GLFW.SetWindowIcon(window, glfwImages);
foreach (var handle in handles)
{
handle.Free();
}
}
public void GLInitMainContext(bool gles)
{
_mainGraphicsContext = new GlfwBindingsContext();
GL.LoadBindings(_mainGraphicsContext);
if (gles)
{
// On GLES we use some OES and KHR functions so make sure to initialize them.
OpenToolkit.Graphics.ES20.GL.LoadBindings(_mainGraphicsContext);
}
}
public void GLMakeContextCurrent(WindowReg window)
{
CheckWindowDisposed(window);
var reg = (GlfwWindowReg) window;
GLFW.MakeContextCurrent(reg.GlfwWindow);
}
public void GLSwapInterval(int interval)
{
GLFW.SwapInterval(interval);
}
private void CheckWindowDisposed(WindowReg reg)
{
if (reg.IsDisposed)
throw new ObjectDisposedException("Window disposed");
}
private sealed class GlfwWindowReg : WindowReg
{
public Window* GlfwWindow;
// Kept around to avoid it being GCd.
public CursorImpl? Cursor;
}
private class GlfwBindingsContext : IBindingsContext
{
public IntPtr GetProcAddress(string procName)
{
return GLFW.GetProcAddress(procName);
}
}
}
}
}

View File

@@ -0,0 +1,119 @@
using System;
using System.Runtime.Serialization;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
private sealed partial class GlfwWindowingImpl : IWindowingImpl
{
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
private readonly Clyde _clyde;
private readonly ISawmill _sawmill;
private readonly ISawmill _sawmillGlfw;
private bool _glfwInitialized;
private bool _win32Experience;
public GlfwWindowingImpl(Clyde clyde)
{
_clyde = clyde;
IoCManager.InjectDependencies(this);
_sawmill = _logManager.GetSawmill("clyde.win");
_sawmillGlfw = _logManager.GetSawmill("clyde.win.glfw");
}
public bool Init()
{
#if DEBUG
_cfg.OnValueChanged(CVars.DisplayWin32Experience, b => _win32Experience = b, true);
#endif
InitChannels();
if (!InitGlfw())
{
return false;
}
SetupGlobalCallbacks();
InitMonitors();
InitCursors();
InitKeyMap();
return true;
}
public void Shutdown()
{
if (_glfwInitialized)
{
_sawmill.Debug("Terminating GLFW.");
GLFW.Terminate();
}
}
public void FlushDispose()
{
// Not currently used
}
private bool InitGlfw()
{
StoreCallbacks();
GLFW.SetErrorCallback(_errorCallback);
if (!GLFW.Init())
{
var err = GLFW.GetError(out var desc);
_sawmill.Fatal($"Failed to initialize GLFW! [{err}] {desc}");
return false;
}
_glfwInitialized = true;
var version = GLFW.GetVersionString();
_sawmill.Debug("GLFW initialized, version: {0}.", version);
return true;
}
private void OnGlfwError(ErrorCode code, string description)
{
_sawmillGlfw.Error("GLFW Error: [{0}] {1}", code, description);
}
[Serializable]
public class GlfwException : Exception
{
public GlfwException()
{
}
public GlfwException(string message) : base(message)
{
}
public GlfwException(string message, Exception inner) : base(message, inner)
{
}
protected GlfwException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}
}
}
}

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