Compare commits

...

149 Commits

Author SHA1 Message Date
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
Pieter-Jan Briers
31c1feca4e Debug console history improvements.
No longer blows up if history cannot be read/written thanks to file locking.

Made it more async so it won't waste main thread init time.
2021-03-09 22:28:58 +01:00
Pieter-Jan Briers
3ed1eef2ab Fix build. 2021-03-09 21:44:46 +01:00
Pieter-Jan Briers
1394a017bb Fix IL verification throwing if a verifier error does not need to be formatted. 2021-03-09 21:41:50 +01:00
metalgearsloth
6b0670d5f1 Break joints on container insertion; semi-related to break pulling on container insertion (#1620)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-09 11:51:57 -08:00
metalgearsloth
f573331541 Fix physics joint disconnect spam (#1619)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-09 11:51:35 -08:00
metalgearsloth
a7218cd3b8 Make Visible a Shared Property for sprites (#1615)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-09 11:51:16 -08:00
Acruid
f7e8178736 Added new ComponentEvents system in IEventBus. (#1601)
* Added new ComponentEventBus, combined it with IEventBus.

* Removed all traces of IEntity from ComponentDependencies.
Removed IEntityManager dependency from ComponentManager.

* Added entity create/delete events to IEntityManager.

* ComponentEvents now use EntitySystemMessages instead of their custom ComponentEvent class.

* Component events are now just overloads of entity events.

* Removed obsolete EntitySystemMessage, now everything uses the base EntityEventArgs.

* Add a bool argument for if the message should be broadcast as well as directed.
Fix ordering and init issues of events in EntityManager.

* Changed names from Component/Entity events to Directed/Broadcast.

* Fix bugs and unit tests.
2021-03-09 11:02:24 -08:00
Pieter-Jan Briers
31f921e4aa Use ProfileOptimization to speed up startup. 2021-03-09 12:29:59 +01:00
Pieter-Jan Briers
aa1c25637c Allow disabling nvidia optimus via env var. 2021-03-09 12:29:59 +01:00
Pieter-Jan Briers
71f2c48463 Call GC.Collect after game init.
Cleans up any gen 2 garbage from init and the stutter shouldn't be the end of the world.
2021-03-09 12:29:59 +01:00
Pieter-Jan Briers
d65f4ca898 RSI & texture preloading.
All RSIs and textures are now loaded ahead of time in client startup. This is well threaded and is extremely fast.
2021-03-09 12:29:59 +01:00
Pieter-Jan Briers
b35568ffe5 Disable path case checks by default.
The idea was that these are Task.Run'd so don't influence performance. That was before we started threading the hell out of startup.

We're getting more stuff like YAML linting now which should hopefully be able to catch 99% of this. And louder because it was always just a warning before.
2021-03-09 12:29:59 +01:00
Acruid
a0d241e551 Removes some things that should not have been in the last PR. 2021-03-09 02:06:13 -08:00
GraniteSidewalk
33a6934582 Large number of changes to Shaders and Overlays (#1387)
* AAAAAAAAAAAAA

* Organization

* Still doesnt work

* Formatting

* It works!!

* More changes to everything

* Beginning of changes to overlays

* Makes the overlay manager GUID based (also it was very messy, still messy but i fixed some of it)

* Stencils are easy

* Questionable changes to overlays

* Minor change to HLR

* Fixed duplicate overlays when calling some commands (Like showbb)

* Fixes misleading message

* Adds a variety of worldspaces for overlays to choose from

* Caching

* Address reviews

* Merging pains

* ah.

* ahhhhh

* minor overlaymanager changes

* Work

* fix

* Merge??

* Fixes null errors

* Force update

* Delete whatever the fuck this is?

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2021-03-09 01:52:16 -08:00
metalgearsloth
f237a8bbbc Optimise static body sleeping (#1618)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-09 19:47:51 +11:00
Pieter-Jan Briers
4bc775c01c RSI loader improvements:
1. Stop using NJsonSchema, it didn't do anything useful.
2. Use System.Text.Json instead of Newtonsoft.Json.
3. General cleanup of the code, using arrays instead of lists, etc...
2021-03-08 11:18:19 +01:00
Pieter-Jan Briers
93b4d81505 Optimize ImageSharp blitting. 2021-03-08 11:15:33 +01:00
Pieter-Jan Briers
0afb85a09e Fix some missing re-pooling of ImageSharp images. 2021-03-08 09:45:22 +01:00
Metal Gear Sloth
7b9315cea4 Significantly lower physics speedcap 2021-03-08 15:46:50 +11:00
Metal Gear Sloth
dc3af45096 Fix anchored message 2021-03-08 15:00:08 +11:00
metalgearsloth
00ce0179ae Allow kinematic controllers to have an impulse applied (#1612)
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
2021-03-08 12:10:20 +11:00
ShadowCommander
81947ba3d8 Fix buckling (#1611) 2021-03-07 15:25:11 -08:00
DrSmugleaf
49327279d0 Fix nullability errors in physics ContactHead code (#1609) 2021-03-07 23:15:55 +01:00
423 changed files with 12601 additions and 6822 deletions

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

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

@@ -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,11 @@
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Benchmarks.Serialization
{
[DataDefinition]
public class DataDefinitionWithString
{
[field: DataField("string")]
private string StringField { get; } = default!;
}
}

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,55 @@
using System.IO;
using BenchmarkDotNet.Attributes;
using Robust.Shared.Serialization.Markdown;
using YamlDotNet.RepresentationModel;
namespace Robust.Benchmarks.Serialization.Read
{
public class SerializationReadBenchmark : SerializationBenchmark
{
public SerializationReadBenchmark()
{
InitializeSerialization();
StringDataDefNode = new MappingDataNode();
StringDataDefNode.AddNode(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,184 @@
using System.Collections.Generic;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Benchmarks.Serialization
{
[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";
private const string SeedPrototype = "SeedBase";
[ViewVariables]
[field: DataField("id", required: true)]
public string ID { get; private init; } = default!;
/// <summary>
/// Unique identifier of this seed. Do NOT set this.
/// </summary>
public int Uid { get; internal set; } = -1;
#region Tracking
[ViewVariables] [DataField("name")] public string Name { get; set; } = string.Empty;
[ViewVariables] [DataField("seedName")] public string SeedName { get; set; } = string.Empty;
[ViewVariables]
[DataField("seedNoun")]
public string SeedNoun { get; set; } = "seeds";
[ViewVariables] [DataField("displayName")] public string DisplayName { get; set; } = string.Empty;
[ViewVariables]
[DataField("roundStart")]
public bool RoundStart { get; private set; } = true;
[ViewVariables] [DataField("mysterious")] public bool Mysterious { get; set; }
[ViewVariables] [DataField("immutable")] public bool Immutable { get; set; }
#endregion
#region Output
[ViewVariables]
[DataField("productPrototypes")]
public List<string> ProductPrototypes { get; set; } = new();
[ViewVariables]
[DataField("chemicals")]
public Dictionary<string, SeedChemQuantity> Chemicals { get; set; } = new();
[ViewVariables]
[DataField("consumeGasses")]
public Dictionary<Gas, float> ConsumeGasses { get; set; } = new();
[ViewVariables]
[DataField("exudeGasses")]
public Dictionary<Gas, float> ExudeGasses { get; set; } = new();
#endregion
#region Tolerances
[ViewVariables]
[DataField("nutrientConsumption")]
public float NutrientConsumption { get; set; } = 0.25f;
[ViewVariables] [DataField("waterConsumption")] public float WaterConsumption { get; set; } = 3f;
[ViewVariables] [DataField("idealHeat")] public float IdealHeat { get; set; } = 293f;
[ViewVariables] [DataField("heatTolerance")] public float HeatTolerance { get; set; } = 20f;
[ViewVariables] [DataField("idealLight")] public float IdealLight { get; set; } = 7f;
[ViewVariables] [DataField("lightTolerance")] public float LightTolerance { get; set; } = 5f;
[ViewVariables] [DataField("toxinsTolerance")] public float ToxinsTolerance { get; set; } = 4f;
[ViewVariables]
[DataField("lowPressureTolerance")]
public float LowPressureTolerance { get; set; } = 25f;
[ViewVariables]
[DataField("highPressureTolerance")]
public float HighPressureTolerance { get; set; } = 200f;
[ViewVariables]
[DataField("pestTolerance")]
public float PestTolerance { get; set; } = 5f;
[ViewVariables]
[DataField("weedTolerance")]
public float WeedTolerance { get; set; } = 5f;
#endregion
#region General traits
[ViewVariables]
[DataField("endurance")]
public float Endurance { get; set; } = 100f;
[ViewVariables] [DataField("yield")] public int Yield { get; set; }
[ViewVariables] [DataField("lifespan")] public float Lifespan { get; set; }
[ViewVariables] [DataField("maturation")] public float Maturation { get; set; }
[ViewVariables] [DataField("production")] public float Production { get; set; }
[ViewVariables] [DataField("growthStages")] public int GrowthStages { get; set; } = 6;
[ViewVariables] [DataField("harvestRepeat")] public HarvestType HarvestRepeat { get; set; } = HarvestType.NoRepeat;
[ViewVariables] [DataField("potency")] public float Potency { get; set; } = 1f;
// No, I'm not removing these.
//public PlantSpread Spread { get; set; }
//public PlantMutation Mutation { get; set; }
//public float AlterTemperature { get; set; }
//public PlantCarnivorous Carnivorous { get; set; }
//public bool Parasite { get; set; }
//public bool Hematophage { get; set; }
//public bool Thorny { get; set; }
//public bool Stinging { get; set; }
[DataField("ligneous")]
public bool Ligneous { get; set; }
// public bool Teleporting { get; set; }
// public PlantJuicy Juicy { get; set; }
#endregion
#region Cosmetics
[ViewVariables]
[DataField("plantRsi", required: true)]
public ResourcePath PlantRsi { get; set; } = default!;
[ViewVariables]
[DataField("plantIconState")]
public string PlantIconState { get; set; } = "produce";
[ViewVariables]
[DataField("bioluminescent")]
public bool Bioluminescent { get; set; }
[ViewVariables]
[DataField("bioluminescentColor")]
public Color BioluminescentColor { get; set; } = Color.White;
[ViewVariables]
[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,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

@@ -11,7 +11,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.Utility;
@@ -321,6 +320,11 @@ namespace Robust.Client.Audio.Midi
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

View File

@@ -8,6 +8,7 @@ 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;
@@ -96,6 +97,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 +152,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;
@@ -150,12 +170,18 @@ namespace Robust.Client
DebugTools.Assert(RunLevel < ClientRunLevel.Connected);
OnRunLevelChanged(ClientRunLevel.Connected);
GameStartedSetup();
PlayerJoinedServer?.Invoke(this, new PlayerEventArgs(session));
}
private void GameStartedSetup()
{
_entityManager.Startup();
_mapManager.Startup();
_timing.ResetSimTime();
_timing.Paused = false;
PlayerJoinedServer?.Invoke(this, new PlayerEventArgs(session));
}
/// <summary>
@@ -189,10 +215,15 @@ namespace Robust.Client
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalPlayer?.Session));
LastDisconnectReason = args.Reason;
GameStoppedReset();
}
private void GameStoppedReset()
{
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
_gameStates.Reset();
_playMan.Shutdown();
IoCManager.Resolve<IEntityLookup>().Shutdown();
_entityManager.Shutdown();
_mapManager.Shutdown();
_discord.ClearPresence();
@@ -249,6 +280,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>

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,8 +38,14 @@ 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, SharedEntityLookup>();
IoCManager.Register<IComponentFactory, ClientComponentFactory>();
IoCManager.Register<ITileDefinitionManager, ClydeTileDefinitionManager>();
IoCManager.Register<IClydeTileDefinitionManager, ClydeTileDefinitionManager>();
@@ -52,7 +58,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 +73,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 +99,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

View File

@@ -0,0 +1,44 @@
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 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

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

@@ -1,15 +1,18 @@
using System;
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
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
@@ -18,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;
@@ -41,14 +43,14 @@ namespace Robust.Client.Debugging
_debugColliders = value;
if (value)
if (value && !_overlayManager.HasOverlay<PhysicsOverlay>())
{
_overlayManager.AddOverlay(new PhysicsOverlay(_componentManager, _eyeManager,
_prototypeManager, _inputManager, _physicsManager));
_overlayManager.AddOverlay(new PhysicsOverlay(_eyeManager,
_prototypeManager, _inputManager, _mapManager));
}
else
{
_overlayManager.RemoveOverlay(nameof(PhysicsOverlay));
_overlayManager.RemoveOverlay<PhysicsOverlay>();
}
}
}
@@ -66,23 +68,22 @@ namespace Robust.Client.Debugging
_debugPositions = value;
if (value)
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
{
_overlayManager.AddOverlay(new EntityPositionOverlay(_entityManager, _eyeManager));
}
else
{
_overlayManager.RemoveOverlay(nameof(EntityPositionOverlay));
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
}
}
}
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;
@@ -91,13 +92,12 @@ namespace Robust.Client.Debugging
private Vector2 _hoverStartScreen = Vector2.Zero;
private List<IPhysBody> _hoverBodies = new();
public PhysicsOverlay(IComponentManager compMan, IEyeManager eyeMan, IPrototypeManager protoMan, IInputManager inputManager, IPhysicsManager physicsManager)
: base(nameof(PhysicsOverlay))
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>();
@@ -105,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;
@@ -133,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++;
@@ -145,8 +146,9 @@ 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);
@@ -160,23 +162,31 @@ namespace Robust.Client.Debugging
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))
@@ -193,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);
}
}
@@ -265,17 +275,17 @@ namespace Robust.Client.Debugging
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityPositionOverlay(IEntityManager entityManager, IEyeManager eyeManager) : base(nameof(EntityPositionOverlay))
public EntityPositionOverlay(IEntityManager entityManager, IEyeManager eyeManager)
{
_entityManager = entityManager;
_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

@@ -1,10 +1,11 @@
using Robust.Shared.IoC;
using Robust.Shared.IoC;
using Robust.Shared.Network.Messages;
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Enums;
using Robust.Shared.Network;
namespace Robust.Client.Debugging
@@ -38,13 +39,13 @@ namespace Robust.Client.Debugging
_debugDrawRays = value;
if (value)
if (value && !_overlayManager.HasOverlay<DebugDrawRayOverlay>())
{
_overlayManager.AddOverlay(new DebugDrawRayOverlay(this));
}
else
{
_overlayManager.RemoveOverlay(nameof(DebugDrawRayOverlay));
_overlayManager.RemoveOverlay<DebugDrawRayOverlay>();
}
}
}
@@ -81,13 +82,14 @@ namespace Robust.Client.Debugging
private readonly DebugDrawingManager _owner;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public DebugDrawRayOverlay(DebugDrawingManager owner) : base(nameof(DebugDrawRayOverlay))
public DebugDrawRayOverlay(DebugDrawingManager owner)
{
_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

@@ -24,6 +24,7 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
@@ -55,7 +56,7 @@ namespace Robust.Client.Debugging
IoCManager.Resolve<IOverlayManager>().AddOverlay(new PhysicsDebugOverlay(this));
if (value == PhysicsDebugFlags.None)
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(nameof(PhysicsDebugOverlay));
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(typeof(PhysicsDebugOverlay));
_flags = value;
}
@@ -119,16 +120,16 @@ namespace Robust.Client.Debugging
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public PhysicsDebugOverlay(DebugPhysicsSystem system) : base(nameof(PhysicsDebugOverlay))
public PhysicsDebugOverlay(DebugPhysicsSystem system)
{
_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,5 +1,6 @@
using System;
using System;
using System.IO;
using System.Management;
using System.Net;
using System.Threading.Tasks;
using Robust.Client.Audio.Midi;
@@ -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;
@@ -80,6 +83,77 @@ namespace Robust.Client
}
public bool Startup(Func<ILogHandler>? logHandlerFactory = null)
{
if (!StartupSystemSplash(logHandlerFactory))
return false;
// 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(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();
IoCManager.Resolve<IEntityLookup>().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 (!Options.DisableCommandLineConnect &&
(_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
&& LaunchState.ConnectEndpoint != null)
{
_client.ConnectToServer(LaunchState.ConnectEndpoint);
}
return true;
}
private bool StartupSystemSplash(Func<ILogHandler>? logHandlerFactory)
{
ReadInitialLaunchState();
@@ -99,7 +173,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.
@@ -119,9 +193,16 @@ namespace Robust.Client
_configurationManager.OverrideConVars(_commandLineArgs.CVars);
}
ProfileOptSetup.Setup(_configurationManager);
_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;
@@ -129,73 +210,22 @@ namespace Robust.Client
_modLoader.VerifierExtraLoadHandler = VerifierExtraLoadHandler;
}
_clyde.TextEntered += TextEntered;
_clyde.MouseMove += MouseMove;
_clyde.KeyUp += KeyUp;
_clyde.KeyDown += KeyDown;
_clyde.MouseWheel += MouseWheel;
_clyde.CloseWindow += Shutdown;
// 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);
_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();
_clyde.Ready();
if ((_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
&& LaunchState.ConnectEndpoint != null)
{
_client.ConnectToServer(LaunchState.ConnectEndpoint);
}
_clyde.SetWindowTitle(Options.DefaultWindowTitle);
_fontManager.SetFontDpi((uint) _configurationManager.GetCVar(CVars.DisplayFontDpi));
return true;
}
@@ -272,17 +302,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);
}
@@ -303,11 +336,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());
@@ -384,6 +412,7 @@ namespace Robust.Client
{
_networkManager.Shutdown("Client shutting down");
_midiManager.Shutdown();
IoCManager.Resolve<IEntityLookup>().Shutdown();
_entityManager.Shutdown();
_clyde.Shutdown();
}

View File

@@ -19,7 +19,7 @@ namespace Robust.Client
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 +30,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();
@@ -45,11 +45,13 @@ namespace Robust.Client
var gc = (GameController) IoCManager.Resolve<IGameController>();
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;
gc.ContentStart = contentStart;
if (!gc.Startup())
{
Logger.Fatal("Failed to start game controller!");

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

@@ -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,152 @@ 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)
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(EntitySystemMessage message)
{
SendSystemNetworkMessage(message, default(uint));
}
public void SendSystemNetworkMessage(EntitySystemMessage 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(EntitySystemMessage 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

@@ -94,8 +94,7 @@ namespace Robust.Client.GameObjects
return;
}
EntitySystem.Get<AppearanceSystem>()
.EnqueueAppearanceUpdate(this);
EntitySystem.Get<AppearanceSystem>().EnqueueUpdate(this);
_appearanceDirty = true;
}
@@ -115,21 +114,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;
@@ -25,7 +25,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 +152,7 @@ namespace Robust.Client.GameObjects
Zoom = state.Zoom;
Offset = state.Offset;
Rotation = state.Rotation;
VisibilityMask = state.VisibilityMask;
}
public override void OnRemove()

View File

@@ -1,5 +1,7 @@
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;
@@ -8,8 +10,8 @@ namespace Robust.Client.GameObjects
{
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; }
[ViewVariables] internal uint UpdateGeneration { get; set; }
@@ -29,39 +31,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 +68,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,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;
@@ -36,13 +37,13 @@ namespace Robust.Client.GameObjects
private bool _visible = true;
[ViewVariables(VVAccess.ReadWrite)]
public bool Visible
public override bool Visible
{
get => _visible;
set => _visible = value;
}
[DataFieldWithConstant("drawdepth", typeof(DrawDepthTag))]
[DataField("drawdepth", customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
private int drawDepth = DrawDepthTag.Default;
/// <summary>
@@ -847,8 +848,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;
}
}
@@ -2084,6 +2085,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 +2102,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 +2150,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,21 +2176,6 @@ 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 Delete()
{
}

View File

@@ -9,13 +9,13 @@ namespace Robust.Client.GameObjects
public static class EntityManagerExt
{
public static void RaisePredictiveEvent<T>(this IEntityManager entityManager, T msg)
where T : EntitySystemMessage
where T : EntityEventArgs
{
var localPlayer = IoCManager.Resolve<IPlayerManager>().LocalPlayer;
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;
@@ -42,6 +41,17 @@ namespace Robust.Client.GameObjects
_broadPhaseSystem = Get<SharedBroadPhaseSystem>();
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeNetworkEvent<PlayAudioEntityMessage>();
UnsubscribeNetworkEvent<PlayAudioGlobalMessage>();
UnsubscribeNetworkEvent<PlayAudioPositionalMessage>();
UnsubscribeNetworkEvent<StopAudioMessageClient>();
UnsubscribeLocalEvent<SoundSystem.QueryAudioSystem>();
}
private void StopAudioMessageHandler(StopAudioMessageClient ev)
{
var stream = _playingClydeStreams.Find(p => p.NetIdentifier == ev.Identifier);
@@ -165,6 +175,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 +204,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 +220,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 +242,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 +259,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 +288,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 +305,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,18 @@ namespace Robust.Client.GameObjects
UpdatesAfter.Add(typeof(PhysicsSystem));
SubscribeLocalEvent<OccluderDirtyEvent>(HandleDirtyEvent);
SubscribeLocalEvent<ClientOccluderComponent, SnapGridPositionChangedEvent>(HandleSnapGridMove);
}
/// <inheritdoc />
public override void Shutdown()
{
UnsubscribeLocalEvent<OccluderDirtyEvent>();
UnsubscribeLocalEvent<ClientOccluderComponent, SnapGridPositionChangedEvent>(HandleSnapGridMove);
base.Shutdown();
}
public override void FrameUpdate(float frameTime)
@@ -47,8 +61,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 +72,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 +84,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,44 +99,37 @@ 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>
/// Event raised by a <see cref="ClientOccluderComponent"/> when it needs to be recalculated.
/// </summary>
internal sealed class OccluderDirtyEvent : EntitySystemMessage
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

@@ -11,6 +11,7 @@ using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.Enums;
namespace Robust.Client.GameObjects
{
@@ -42,7 +43,7 @@ namespace Robust.Client.GameObjects
{
base.Shutdown();
overlayManager.RemoveOverlay("EffectSystem");
overlayManager.RemoveOverlay(typeof(EffectOverlay));
}
public void CreateEffect(EffectSystemMessage message)
@@ -329,7 +330,6 @@ namespace Robust.Client.GameObjects
{
private readonly IPlayerManager _playerManager;
public override bool AlwaysDirty => true;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly ShaderInstance _unshadedShader;
@@ -337,8 +337,7 @@ namespace Robust.Client.GameObjects
private readonly IMapManager _mapManager;
private readonly IEntityManager _entityManager;
public EffectOverlay(EffectSystem owner, IPrototypeManager protoMan, IMapManager mapMan, IPlayerManager playerMan, IEntityManager entityManager) : base(
"EffectSystem")
public EffectOverlay(EffectSystem owner, IPrototypeManager protoMan, IMapManager mapMan, IPlayerManager playerMan, IEntityManager entityManager)
{
_owner = owner;
_unshadedShader = protoMan.Index<ShaderPrototype>("unshaded").Instance();
@@ -347,11 +346,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()
@@ -157,7 +157,7 @@ namespace Robust.Client.GameObjects
/// <summary>
/// Entity system message that is raised when the player changes attached entities.
/// </summary>
public class PlayerAttachSysMessage : EntitySystemMessage
public class PlayerAttachSysMessage : EntityEventArgs
{
/// <summary>
/// New entity the player is attached to.

View File

@@ -19,19 +19,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,6 +44,8 @@ namespace Robust.Client.GameObjects
_mapManager.MapCreated += MapManagerOnMapCreated;
_mapManager.MapDestroyed += MapManagerOnMapDestroyed;
_mapManager.OnGridCreated += MapManagerOnGridCreated;
_mapManager.OnGridRemoved += MapManagerOnGridRemoved;
SubscribeLocalEvent<EntMapIdChangedMessage>(EntMapIdChanged);
SubscribeLocalEvent<MoveEvent>(EntMoved);
@@ -53,18 +55,40 @@ namespace Robust.Client.GameObjects
SubscribeLocalEvent<RenderTreeRemoveLightMessage>(RemoveLight);
}
public override void Shutdown()
{
base.Shutdown();
_mapManager.MapCreated -= MapManagerOnMapCreated;
_mapManager.MapDestroyed -= MapManagerOnMapDestroyed;
_mapManager.OnGridCreated -= MapManagerOnGridCreated;
_mapManager.OnGridRemoved -= MapManagerOnGridRemoved;
UnsubscribeLocalEvent<EntMapIdChangedMessage>();
UnsubscribeLocalEvent<MoveEvent>();
UnsubscribeLocalEvent<EntParentChangedMessage>();
UnsubscribeLocalEvent<PointLightRadiusChangedMessage>();
UnsubscribeLocalEvent<RenderTreeRemoveSpriteMessage>();
UnsubscribeLocalEvent<RenderTreeRemoveLightMessage>();
}
// For these next 2 methods (the Remove* ones):
// 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)
{
_mapTrees[ev.Map].LightTree.Remove(ev.Light);
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Map, MapTrees.LightAabbFunc(ev.Light), true))
{
_gridTrees[ev.Map][gridId].LightTree.Remove(ev.Light);
}
}
private void RemoveSprite(RenderTreeRemoveSpriteMessage ev)
{
_mapTrees[ev.Map].SpriteTree.Remove(ev.Sprite);
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Map, MapTrees.SpriteAabbFunc(ev.Sprite), true))
{
_gridTrees[ev.Map][gridId].SpriteTree.Remove(ev.Sprite);
}
}
private void PointLightRadiusChanged(PointLightRadiusChangedMessage ev)
@@ -119,27 +143,69 @@ namespace Robust.Client.GameObjects
{
// 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);
var oldMapTrees = _gridTrees.GetValueOrDefault(ev.OldMapId);
// TODO: MMMM probably a better way to do this.
if (ev.Entity.TryGetComponent(out SpriteComponent? sprite))
{
oldMapTrees?.SpriteTree.Remove(sprite);
if (oldMapTrees != null)
{
foreach (var (_, gridTree) in oldMapTrees)
{
gridTree.SpriteTree.Remove(sprite);
}
}
newMapTrees?.SpriteTree.AddOrUpdate(sprite);
var bounds = MapTrees.SpriteAabbFunc(sprite);
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Entity.Transform.MapID, bounds, true))
{
Box2 gridBounds;
if (gridId == GridId.Invalid)
{
gridBounds = bounds;
}
else
{
gridBounds = bounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
}
_gridTrees[ev.Entity.Transform.MapID][gridId].SpriteTree.AddOrUpdate(sprite, gridBounds);
}
}
if (ev.Entity.TryGetComponent(out PointLightComponent? light))
{
oldMapTrees?.LightTree.Remove(light);
if (oldMapTrees != null)
{
foreach (var (_, gridTree) in oldMapTrees)
{
gridTree.LightTree.Remove(light);
}
}
newMapTrees?.LightTree.AddOrUpdate(light);
var bounds = MapTrees.LightAabbFunc(light);
foreach (var gridId in _mapManager.FindGridIdsIntersecting(ev.Entity.Transform.MapID, bounds, true))
{
Box2 gridBounds;
if (gridId == GridId.Invalid)
{
gridBounds = bounds;
}
else
{
gridBounds = bounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
}
_gridTrees[ev.Entity.Transform.MapID][gridId].LightTree.AddOrUpdate(light, gridBounds);
}
}
}
private void MapManagerOnMapDestroyed(object? sender, MapEventArgs e)
{
_mapTrees.Remove(e.Map);
_gridTrees.Remove(e.Map);
}
private void MapManagerOnMapCreated(object? sender, MapEventArgs e)
@@ -149,36 +215,59 @@ 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)
{
_gridTrees[mapId].Remove(gridId);
}
public override void FrameUpdate(float frameTime)
{
foreach (var queuedUpdateSprite in _spriteQueue)
{
var transform = queuedUpdateSprite.Owner.Transform;
var map = transform.MapID;
var map = queuedUpdateSprite.Owner.Transform.MapID;
if (map == MapId.Nullspace)
{
continue;
}
var updateMapTree = _mapTrees[map].SpriteTree;
updateMapTree.AddOrUpdate(queuedUpdateSprite);
var mapTree = _gridTrees[map];
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map,
MapTrees.SpriteAabbFunc(queuedUpdateSprite), true))
{
mapTree[gridId].SpriteTree.AddOrUpdate(queuedUpdateSprite);
}
queuedUpdateSprite.TreeUpdateQueued = false;
}
foreach (var queuedUpdateLight in _lightQueue)
{
var transform = queuedUpdateLight.Owner.Transform;
var map = transform.MapID;
var map = queuedUpdateLight.Owner.Transform.MapID;
if (map == MapId.Nullspace)
{
continue;
}
var updateMapTree = _mapTrees[map].LightTree;
updateMapTree.AddOrUpdate(queuedUpdateLight);
var mapTree = _gridTrees[map];
foreach (var gridId in _mapManager.FindGridIdsIntersecting(map,
MapTrees.LightAabbFunc(queuedUpdateLight), true))
{
mapTree[gridId].LightTree.AddOrUpdate(queuedUpdateLight);
}
queuedUpdateLight.TreeUpdateQueued = false;
}
@@ -197,14 +286,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;

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,6 +14,7 @@ namespace Robust.Client.GameObjects
public class SpriteSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
/// <inheritdoc />
public override void FrameUpdate(float frameTime)
@@ -29,18 +31,32 @@ 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)
Box2 gridBounds;
if (gridId == GridId.Invalid)
{
return true;
gridBounds = pvsBounds;
}
else
{
gridBounds = pvsBounds.Translated(-_mapManager.GetGrid(gridId).WorldPosition);
}
value.FrameUpdate(state);
return true;
}, pvsBounds, approx: true);
var mapTree = renderTreeSystem.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

@@ -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,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;
@@ -27,20 +31,25 @@ namespace Robust.Client.GameStates
private uint _nextInputCmdSeq = 1;
private readonly Queue<FullInputCmdMessage> _pendingInputs = new();
private readonly Queue<(uint sequence, GameTick sourceTick, EntitySystemMessage msg, object sessionMsg)>
private readonly Queue<(uint sequence, GameTick sourceTick, EntityEventArgs msg, object sessionMsg)>
_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;
@@ -126,7 +135,7 @@ namespace Robust.Client.GameStates
_nextInputCmdSeq++;
}
public uint SystemMessageDispatched<T>(T message) where T : EntitySystemMessage
public uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs
{
if (!Predicting)
{
@@ -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);
((IEntityEventBus) _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

@@ -70,6 +70,6 @@ namespace Robust.Client.GameStates
/// <param name="message">Message being dispatched.</param>
void InputCommandDispatched(FullInputCmdMessage message);
uint SystemMessageDispatched<T>(T message) where T : EntitySystemMessage;
uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs;
}
}

View File

@@ -1,13 +1,16 @@
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
@@ -27,13 +30,13 @@ 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();
public NetEntityOverlay() : base(nameof(NetEntityOverlay))
public NetEntityOverlay()
{
IoCManager.InjectDependencies(this);
var cache = IoCManager.Resolve<IResourceCache>();
@@ -42,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++)
{
@@ -74,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;
@@ -94,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.
@@ -123,22 +126,58 @@ 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 pvsSize = _configurationManager.GetCVar<float>("net.maxupdaterange");
var pvsCenter = _eyeManager.CurrentEye.Position;
Box2 pvsBox = Box2.CenteredAround(pvsCenter.Position, new Vector2(pvsSize, pvsSize));
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);
@@ -179,20 +218,19 @@ namespace Robust.Client.GameStates
return Color.Green; // Entity in PVS, but not updated recently.
}
protected override void Dispose(bool disposing)
protected override void DisposeBehavior()
{
_gameStateManager.GameStateApplied -= HandleGameStateApplied;
base.Dispose(disposing);
base.DisposeBehavior();
}
private static void DrawString(DrawingHandleScreen handle, Font font, Vector2 pos, string str, Color textColor)
{
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 +263,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;
}
@@ -238,14 +276,14 @@ namespace Robust.Client.GameStates
var bValue = iValue > 0;
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if(bValue && !overlayMan.HasOverlay(nameof(NetEntityOverlay)))
if(bValue && !overlayMan.HasOverlay(typeof(NetEntityOverlay)))
{
overlayMan.AddOverlay(new NetEntityOverlay());
shell.WriteLine("Enabled network entity report overlay.");
}
else if(!bValue && overlayMan.HasOverlay(nameof(NetEntityOverlay)))
else if(!bValue && overlayMan.HasOverlay(typeof(NetEntityOverlay)))
{
overlayMan.RemoveOverlay(nameof(NetEntityOverlay));
overlayMan.RemoveOverlay(typeof(NetEntityOverlay));
shell.WriteLine("Disabled network entity report overlay.");
}
}

View File

@@ -1,7 +1,11 @@
using System.Collections.Generic;
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;
@@ -34,7 +38,11 @@ namespace Robust.Client.GameStates
private readonly List<(GameTick Tick, int Payload, int lag, int interp)> _history = new(HistorySize+10);
public NetGraphOverlay() : base(nameof(NetGraphOverlay))
private int _totalHistoryPayload; // sum of all data point sizes in bytes
public EntityUid WatchEntId { get; set; }
public NetGraphOverlay()
{
IoCManager.InjectDependencies(this);
var cache = IoCManager.Resolve<IResourceCache>();
@@ -58,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 />
@@ -67,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));
@@ -99,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)
{
@@ -123,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));
@@ -132,30 +224,30 @@ 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 Dispose(bool disposing)
protected override void DisposeBehavior()
{
_gameStateManager.GameStateApplied -= HandleGameStateApplied;
base.Dispose(disposing);
base.DisposeBehavior();
}
private void DrawString(DrawingHandleScreen handle, Font font, Vector2 pos, string str)
{
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);
}
}
@@ -183,17 +275,48 @@ namespace Robust.Client.GameStates
var bValue = iValue > 0;
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if(bValue && !overlayMan.HasOverlay(nameof(NetGraphOverlay)))
if(bValue && !overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
overlayMan.AddOverlay(new NetGraphOverlay());
shell.WriteLine("Enabled network overlay.");
}
else if(overlayMan.HasOverlay(nameof(NetGraphOverlay)))
else if(overlayMan.HasOverlay(typeof(NetGraphOverlay)))
{
overlayMan.RemoveOverlay(nameof(NetGraphOverlay));
overlayMan.RemoveOverlay(typeof(NetGraphOverlay));
shell.WriteLine("Disabled network overlay.");
}
}
}
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

@@ -1,3 +1,4 @@
using Robust.Shared.Enums;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
@@ -5,6 +6,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using System;
using Robust.Shared.Timing;
namespace Robust.Client.GameStates
@@ -18,14 +20,15 @@ namespace Robust.Client.GameStates
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly ShaderInstance _shader;
public NetInterpOverlay() : base(nameof(NetInterpOverlay))
public NetInterpOverlay()
{
IoCManager.InjectDependencies(this);
_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();
@@ -85,14 +88,14 @@ namespace Robust.Client.GameStates
var bValue = iValue > 0;
var overlayMan = IoCManager.Resolve<IOverlayManager>();
if (bValue && !overlayMan.HasOverlay(nameof(NetInterpOverlay)))
if (bValue && !overlayMan.HasOverlay<NetInterpOverlay>())
{
overlayMan.AddOverlay(new NetInterpOverlay());
shell.WriteLine("Enabled network interp overlay.");
}
else if (overlayMan.HasOverlay(nameof(NetInterpOverlay)))
else if (overlayMan.HasOverlay<NetInterpOverlay>())
{
overlayMan.RemoveOverlay(nameof(NetInterpOverlay));
overlayMan.RemoveOverlay<NetInterpOverlay>();
shell.WriteLine("Disabled network interp overlay.");
}
}

View File

@@ -1,4 +1,4 @@
using JetBrains.Annotations;
using JetBrains.Annotations;
using Robust.Shared.Maths;
using Robust.Shared.Utility;

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 />
@@ -115,17 +116,7 @@ namespace Robust.Client.Graphics
/// <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);
ConfigurationManager.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 = ConfigurationManager.GetCVar(CVars.AudioDevice);
// Open device.
if (!string.IsNullOrEmpty(preferredDevice))
@@ -482,7 +482,7 @@ namespace Robust.Client.Graphics.Clyde
var (x, y) = position;
if (!ValidatePosition(x, y))
if (!AreFinite(x, y))
{
return false;
}
@@ -503,7 +503,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 +513,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 +683,6 @@ namespace Robust.Client.Graphics.Clyde
_checkAlError();
}
private void SetOcclusionEfx(float gain, float cutoff)
{
if (FilterHandle == 0)
@@ -694,7 +709,7 @@ namespace Robust.Client.Graphics.Clyde
var (x, y) = position;
if (!ValidatePosition(x, y))
if (!AreFinite(x, y))
{
return false;
}
@@ -706,7 +721,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 +731,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!.Value, ALSource3f.Velocity, x, y, 0);
_checkAlError();
}
public void SetPitch(float pitch)
{
_checkDisposed();

View File

@@ -12,7 +12,8 @@ namespace Robust.Client.Graphics.Clyde
static Clyde()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
RuntimeInformation.ProcessArchitecture == Architecture.X64)
RuntimeInformation.ProcessArchitecture == Architecture.X64 &&
Environment.GetEnvironmentVariable("ROBUST_INTEGRATED_GPU") != "1")
{
try
{

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;
@@ -115,7 +115,7 @@ namespace Robust.Client.Graphics.Clyde
var prev = cap;
var cVarName = $"display.ogl_block_{capName}";
var block = _configurationManager.GetCVar<bool>(cVarName);
var block = ConfigurationManager.GetCVar<bool>(cVarName);
if (block)
{
@@ -146,7 +146,7 @@ namespace Robust.Client.Graphics.Clyde
foreach (var cvar in cvars)
{
_configurationManager.RegisterCVar($"display.ogl_block_{cvar}", false);
ConfigurationManager.RegisterCVar($"display.ogl_block_{cvar}", false);
}
}

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

@@ -7,6 +7,9 @@ using Robust.Client.ResourceManagement;
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
{
@@ -65,67 +68,136 @@ namespace Robust.Client.Graphics.Clyde
return;
}
RenderOverlays(OverlaySpace.ScreenSpaceBelowWorld);
_mainViewport.Eye = _eyeManager.CurrentEye;
RenderViewport(_mainViewport);
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);
// And finally, swap those buffers!
SwapBuffers();
}
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)
{
if ((overlay.Space & space) != 0)
{
list.Add(overlay);
}
}
list.Sort(OverlayComparer.Instance);
var list = GetOverlaysForSpace(space);
foreach (var overlay in list)
{
overlay.ClydeRender(_renderHandle, space);
}
if (overlay.RequestScreenTexture)
{
FlushRenderQueue();
UpdateOverlayScreenTexture(space, vp.RenderTarget);
}
FlushRenderQueue();
if (overlay.OverwriteTargetFrameBuffer())
{
ClearFramebuffer(default);
}
overlay.ClydeRender(_renderHandle, space, null, vp, new UIBox2i((0, 0), vp.Size), worldBox);
}
}
}
private void DrawEntitiesAndWorldOverlay(Viewport viewport, Box2 worldBounds)
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)
{
//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)
{
oTargets.Add(overlay);
}
}
if (oTargets.Count > 0 && ScreenBufferTexture != null)
{
if (lastFrameSize != _framebufferSize)
{
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
GL.TexImage2D(TextureTarget.Texture2D, 0,
_hasGLSrgb ? PixelInternalFormat.Srgb8Alpha8 : PixelInternalFormat.Rgba8, _framebufferSize.X,
_framebufferSize.Y, 0,
PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
}
lastFrameSize = _framebufferSize;
CopyRenderTextureToTexture(texture, ScreenBufferTexture);
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))
{
return;
}
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowEntities, worldBounds);
var screenSize = viewport.Size;
// So we could calculate the correct size of the entities based on the contents of their sprite...
@@ -175,7 +247,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;
}
@@ -194,13 +272,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
@@ -225,8 +303,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));
@@ -267,14 +345,6 @@ namespace Robust.Client.Graphics.Clyde
_drawingSpriteList.Clear();
FlushRenderQueue();
// Cleanup remainders
foreach (var overlay in worldOverlays)
{
overlay.ClydeRender(_renderHandle, OverlaySpace.WorldSpace);
}
FlushRenderQueue();
}
[MethodImpl(MethodImplOptions.NoInlining)]
@@ -283,29 +353,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)
@@ -317,11 +402,13 @@ namespace Robust.Client.Graphics.Clyde
private void RenderViewport(Viewport viewport)
{
if (viewport.Eye == null)
if (viewport.Eye == null || viewport.Eye.Position.MapId == MapId.Nullspace)
{
return;
}
using var _ = DebugGroup($"Viewport: {viewport.Name}");
// TODO: for the love of god all this state pushing/popping needs to be cleaned up.
var oldTransform = _currentMatrixModel;
@@ -346,12 +433,11 @@ namespace Robust.Client.Graphics.Clyde
SetViewportImmediate(Box2i.FromDimensions(Vector2i.Zero, rt.Size));
_updateUniformConstants(viewport.Size);
CalcWorldMatrices(rt.Size, eye, out var proj, out var view);
CalcWorldMatrices(rt.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)
{
@@ -360,6 +446,9 @@ namespace Robust.Client.Graphics.Clyde
DrawLightsAndFov(viewport, worldBounds, eye);
}
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowWorld, worldBounds);
FlushRenderQueue();
using (DebugGroup("Grids"))
{
_drawGrids(worldBounds);
@@ -368,9 +457,11 @@ namespace Robust.Client.Graphics.Clyde
// We will also render worldspace overlays here so we can do them under / above entities as necessary
using (DebugGroup("Entities"))
{
DrawEntitiesAndWorldOverlay(viewport, worldBounds);
DrawEntities(viewport, worldBounds);
}
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldBounds);
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
{
ApplyFovToBuffer(viewport, eye);
@@ -401,6 +492,9 @@ namespace Robust.Client.Graphics.Clyde
viewport.WallBleedIntermediateRenderTarget2.Texture,
UIBox2.FromDimensions(Vector2.Zero, ScreenSize), new Color(1, 1, 1, 0.5f));
}
RenderOverlays(viewport, OverlaySpace.WorldSpace, worldBounds);
FlushRenderQueue();
}
PopRenderStateFull(state);
@@ -411,6 +505,17 @@ namespace Robust.Client.Graphics.Clyde
_currentViewport = oldVp;
}
private static Box2 CalcWorldBounds(Viewport viewport)
{
var eye = viewport.Eye;
if (eye == null)
return default;
// 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>
{
public static readonly OverlayComparer Instance = new();

View File

@@ -5,6 +5,7 @@ using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp.PixelFormats;
using ES20 = OpenToolkit.Graphics.ES20;
namespace Robust.Client.Graphics.Clyde
@@ -31,6 +32,27 @@ namespace Robust.Client.Graphics.Clyde
CheckGlError();
GL.BindTexture(TextureTarget.Texture2D, glHandle.Handle);
CheckGlError();
GL.ActiveTexture(TextureUnit.Texture0);
}
private void CopyRenderTextureToTexture(RenderTexture source, ClydeTexture target) {
LoadedRenderTarget sourceLoaded = RtToLoaded(source);
bool pause = sourceLoaded != _currentBoundRenderTarget;
FullStoredRendererState? store = null;
if (pause) {
store = PushRenderStateFull();
BindRenderTargetFull(source);
CheckGlError();
}
GL.BindTexture(TextureTarget.Texture2D, _loadedTextures[target.TextureId].OpenGLObject.Handle);
CheckGlError();
GL.CopyTexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, 0, 0, _framebufferSize.X, _framebufferSize.Y);
CheckGlError();
if (pause && store != null) {
PopRenderStateFull((FullStoredRendererState)store);
}
}
private static long EstPixelSize(PixelInternalFormat format)

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)}");

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@@ -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;
@@ -191,7 +192,8 @@ namespace Robust.Client.Graphics.Clyde
FramebufferHandle = fbo,
Size = size,
TextureHandle = textureObject.TextureId,
MemoryPressure = pressure
MemoryPressure = pressure,
ColorFormat = format.ColorFormat
};
//GC.AddMemoryPressure(pressure);
@@ -278,6 +280,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;
@@ -302,6 +306,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)

View File

@@ -1,17 +1,13 @@
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.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 +116,9 @@ 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);
}
@@ -280,9 +276,19 @@ namespace Robust.Client.Graphics.Clyde
}
/// <summary>
/// Flush the render handle, processing and re-pooling all the command lists.
/// 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 +314,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)
@@ -371,6 +372,7 @@ namespace Robust.Client.Graphics.Clyde
program.Use();
int textureUnitVal = 0;
// Assign shader parameters to uniform since they may be dirty.
foreach (var (name, value) in instance.Parameters)
{
@@ -413,6 +415,15 @@ namespace Robust.Client.Graphics.Clyde
case Matrix4 matrix4:
program.SetUniform(name, matrix4);
break;
case ClydeTexture clydeTexture:
//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);
program.SetUniformTexture(name, cTarget);
textureUnitVal++;
break;
default:
throw new InvalidOperationException($"Unable to handle shader parameter {name}: {value}");
}
@@ -490,6 +501,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);
@@ -515,6 +527,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).
@@ -556,6 +570,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);
@@ -591,6 +607,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);
@@ -605,6 +622,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();
@@ -763,116 +788,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);

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 = ClampSubRegion(fbSize, subRegion);
var bufferLength = size.X * size.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 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

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@@ -428,7 +428,8 @@ namespace Robust.Client.Graphics.Clyde
private protected override void SetParameterImpl(string name, Texture value)
{
throw new NotImplementedException();
var data = Parent._shaderInstances[Handle];
data.Parameters[name] = value;
}
private protected override void SetStencilOpImpl(StencilOp op)

View File

@@ -26,7 +26,7 @@ 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);
@@ -37,7 +37,7 @@ 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);
@@ -56,19 +56,19 @@ namespace Robust.Client.Graphics.Clyde
{
// Disable sRGB so stuff doesn't get interpreter wrong.
actualParams.Srgb = false;
var img = ApplyA8Swizzle((Image<A8>) (object) image);
using var img = ApplyA8Swizzle((Image<A8>) (object) image);
return LoadTextureFromImage(img, name, loadParams);
}
if (pixelType == typeof(L8) && !actualParams.Srgb)
{
var img = ApplyL8Swizzle((Image<L8>) (object) image);
using var img = ApplyL8Swizzle((Image<L8>) (object) image);
return LoadTextureFromImage(img, name, loadParams);
}
}
// Flip image because OpenGL reads images upside down.
var copy = FlipClone(image);
using var copy = FlipClone(image);
var texture = CreateBaseTextureInternal<T>(image.Width, image.Height, actualParams, name);
@@ -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();

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
using System.Threading;
using OpenToolkit;
using OpenToolkit.Graphics.OpenGL4;
using OpenToolkit.GraphicsLibraryFramework;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Client.Utility;
@@ -14,6 +15,7 @@ using Robust.Shared;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using static Robust.Client.Utility.LiterallyJustMessageBox;
@@ -46,6 +48,7 @@ namespace Robust.Client.Graphics.Clyde
{
// Keep delegates around to prevent GC issues.
private GLFWCallbacks.ErrorCallback _errorCallback = default!;
private GLFWCallbacks.MonitorCallback _monitorCallback = default!;
private GLFWCallbacks.CharCallback _charCallback = default!;
private GLFWCallbacks.CursorPosCallback _cursorPosCallback = default!;
private GLFWCallbacks.KeyCallback _keyCallback = default!;
@@ -73,6 +76,18 @@ namespace Robust.Client.Graphics.Clyde
private Vector2 _lastMousePos;
// Can't use ClydeHandle because it's 64 bit.
private int _nextWindowId = 1;
private readonly Dictionary<int, MonitorReg> _monitors = new();
public event Action<TextEventArgs>? TextEntered;
public event Action<MouseMoveEventArgs>? MouseMove;
public event Action<KeyEventArgs>? KeyUp;
public event Action<KeyEventArgs>? KeyDown;
public event Action<MouseWheelEventArgs>? MouseWheel;
public event Action<string>? CloseWindow;
public event Action? OnWindowScaleChanged;
// NOTE: in engine we pretend the framebuffer size is the screen size..
// For practical reasons like UI rendering.
public override Vector2i ScreenSize => _framebufferSize;
@@ -148,15 +163,60 @@ namespace Robust.Client.Graphics.Clyde
return false;
}
InitMonitors();
InitCursors();
return InitWindow();
}
private void InitMonitors()
{
var monitors = GLFW.GetMonitorsRaw(out var count);
for (var i = 0; i < count; i++)
{
SetupMonitor(monitors[i]);
}
}
private void SetupMonitor(Monitor* monitor)
{
var handle = _nextWindowId++;
DebugTools.Assert(GLFW.GetMonitorUserPointer(monitor) == null, "GLFW window already has user pointer??");
var name = GLFW.GetMonitorName(monitor);
var videoMode = GLFW.GetVideoMode(monitor);
var impl = new ClydeMonitorImpl(handle, name, (videoMode->Width, videoMode->Height), videoMode->RefreshRate);
GLFW.SetMonitorUserPointer(monitor, (void*) handle);
_monitors[handle] = new MonitorReg
{
Id = handle,
Impl = impl,
Monitor = monitor
};
}
private void DestroyMonitor(Monitor* monitor)
{
var ptr = GLFW.GetMonitorUserPointer(monitor);
if (ptr == null)
{
var name = GLFW.GetMonitorName(monitor);
Logger.WarningS("clyde.win", $"Monitor '{name}' had no user pointer set??");
return;
}
_monitors.Remove((int) ptr);
GLFW.SetMonitorUserPointer(monitor, null);
}
private bool InitWindow()
{
var width = _configurationManager.GetCVar(CVars.DisplayWidth);
var height = _configurationManager.GetCVar(CVars.DisplayHeight);
var width = ConfigurationManager.GetCVar(CVars.DisplayWidth);
var height = ConfigurationManager.GetCVar(CVars.DisplayHeight);
Monitor* monitor = null;
@@ -166,6 +226,8 @@ namespace Robust.Client.Graphics.Clyde
var mode = GLFW.GetVideoMode(monitor);
width = mode->Width;
height = mode->Height;
GLFW.WindowHint(WindowHintInt.RefreshRate, mode->RefreshRate);
}
#if DEBUG
@@ -174,13 +236,19 @@ namespace Robust.Client.Graphics.Clyde
GLFW.WindowHint(WindowHintString.X11ClassName, "SS14");
GLFW.WindowHint(WindowHintString.X11InstanceName, "SS14");
var renderer = (Renderer) _configurationManager.GetCVar<int>(CVars.DisplayRenderer);
var renderer = (Renderer) ConfigurationManager.GetCVar<int>(CVars.DisplayRenderer);
Span<Renderer> renderers = (renderer == Renderer.Default) ? stackalloc Renderer[] {
Renderer.OpenGL33,
Renderer.OpenGL31,
Renderer.OpenGLES2
} : stackalloc Renderer[] {renderer};
Span<Renderer> renderers = (renderer == Renderer.Default)
? stackalloc Renderer[]
{
Renderer.OpenGL33,
Renderer.OpenGL31,
Renderer.OpenGLES2
}
: stackalloc Renderer[] {renderer};
ErrorCode lastGlfwError = default;
string? lastGlfwErrorDesc = default;
foreach (Renderer r in renderers)
{
@@ -193,22 +261,22 @@ namespace Robust.Client.Graphics.Clyde
_isCore = renderer == Renderer.OpenGL33;
break;
}
// Window failed to init due to error.
// Try not to treat the error code seriously.
var code = GLFW.GetError(out string desc);
Logger.DebugS("clyde.win", $"{r} unsupported: [${code}] ${desc}");
lastGlfwError = GLFW.GetError(out lastGlfwErrorDesc);
Logger.DebugS("clyde.win", $"{r} unsupported: [{lastGlfwError}] ${lastGlfwErrorDesc}");
}
if (_glfwWindow == null)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var code = GLFW.GetError(out string desc);
var errorContent = "Failed to create the game window. " +
"This probably means your GPU is too old to play the game. " +
"That or update your graphic drivers\n" +
$"The exact error is: [{code}]\n {desc}";
"This probably means your GPU is too old to play the game. " +
"Try to update your graphics drivers, " +
"or enable compatibility mode in the launcher if that fails.\n" +
$"The exact error is: {lastGlfwError}\n{lastGlfwErrorDesc}";
MessageBoxW(null,
errorContent,
@@ -301,6 +369,7 @@ namespace Robust.Client.Graphics.Clyde
GLFW.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Any);
GLFW.WindowHint(WindowHintBool.SrgbCapable, false);
}
_glfwWindow = GLFW.CreateWindow(width, height, string.Empty, monitor, null);
}
}
@@ -393,11 +462,30 @@ namespace Robust.Client.Graphics.Clyde
Logger.ErrorS("clyde.win.glfw", "GLFW Error: [{0}] {1}", code, description);
}
private void OnGlfwMonitor(Monitor* monitor, ConnectedState state)
{
try
{
if (state == ConnectedState.Connected)
{
SetupMonitor(monitor);
}
else
{
DestroyMonitor(monitor);
}
}
catch (Exception e)
{
CatchCallbackException(e);
}
}
private void OnGlfwChar(Window* window, uint codepoint)
{
try
{
_gameController.TextEntered(new TextEventArgs(codepoint));
TextEntered?.Invoke(new TextEventArgs(codepoint));
}
catch (Exception e)
{
@@ -413,8 +501,7 @@ namespace Robust.Client.Graphics.Clyde
var delta = newPos - _lastMousePos;
_lastMousePos = newPos;
var ev = new MouseMoveEventArgs(delta, newPos);
_gameController.MouseMove(ev);
MouseMove?.Invoke(new MouseMoveEventArgs(delta, newPos));
}
catch (Exception e)
{
@@ -461,11 +548,11 @@ namespace Robust.Client.Graphics.Clyde
switch (action)
{
case InputAction.Release:
_gameController.KeyUp(ev);
KeyUp?.Invoke(ev);
break;
case InputAction.Press:
case InputAction.Repeat:
_gameController.KeyDown(ev);
KeyDown?.Invoke(ev);
break;
default:
throw new ArgumentOutOfRangeException(nameof(action), action, null);
@@ -477,7 +564,7 @@ namespace Robust.Client.Graphics.Clyde
try
{
var ev = new MouseWheelEventArgs(((float) offsetX, (float) offsetY), _lastMousePos);
_gameController.MouseWheel(ev);
MouseWheel?.Invoke(ev);
}
catch (Exception e)
{
@@ -489,7 +576,7 @@ namespace Robust.Client.Graphics.Clyde
{
try
{
_gameController.Shutdown("Window closed");
CloseWindow?.Invoke("Window closed");
}
catch (Exception e)
{
@@ -514,11 +601,6 @@ namespace Robust.Client.Graphics.Clyde
GL.Viewport(0, 0, fbW, fbH);
CheckGlError();
if (fbW != 0 && fbH != 0)
{
_mainViewport.Dispose();
CreateMainViewport();
}
OnWindowResized?.Invoke(new WindowResizedEventArgs(oldSize, _framebufferSize));
}
@@ -533,6 +615,7 @@ namespace Robust.Client.Graphics.Clyde
try
{
_windowScale = (xScale, yScale);
OnWindowScaleChanged?.Invoke();
}
catch (Exception e)
{
@@ -568,6 +651,7 @@ namespace Robust.Client.Graphics.Clyde
private void StoreCallbacks()
{
_errorCallback = OnGlfwError;
_monitorCallback = OnGlfwMonitor;
_charCallback = OnGlfwChar;
_cursorPosCallback = OnGlfwCursorPos;
_keyCallback = OnGlfwKey;
@@ -590,6 +674,19 @@ namespace Robust.Client.Graphics.Clyde
GLFW.SetWindowTitle(_glfwWindow, title);
}
public void SetWindowMonitor(IClydeMonitor monitor)
{
var monitorImpl = (ClydeMonitorImpl) monitor;
var reg = _monitors[monitorImpl.Id];
GLFW.SetWindowMonitor(
_glfwWindow,
reg.Monitor,
0, 0,
monitorImpl.Size.X, monitorImpl.Size.Y,
monitorImpl.RefreshRate);
}
public void RequestWindowAttention()
{
GLFW.RequestWindowAttention(_glfwWindow);
@@ -654,18 +751,40 @@ namespace Robust.Client.Graphics.Clyde
GLFW.GetWindowPos(_glfwWindow, out var x, out var y);
_prevWindowPos = (x, y);
var monitor = GLFW.GetPrimaryMonitor();
var monitor = MonitorForWindow(_glfwWindow);
var mode = GLFW.GetVideoMode(monitor);
GLFW.SetWindowMonitor(_glfwWindow, GLFW.GetPrimaryMonitor(), 0, 0, mode->Width, mode->Height,
GLFW.SetWindowMonitor(_glfwWindow, monitor, 0, 0, mode->Width, mode->Height,
mode->RefreshRate);
}
else
{
GLFW.SetWindowMonitor(_glfwWindow, null, _prevWindowPos.X, _prevWindowPos.Y, _prevWindowSize.X, _prevWindowSize.Y, 0);
GLFW.SetWindowMonitor(_glfwWindow, null, _prevWindowPos.X, _prevWindowPos.Y, _prevWindowSize.X,
_prevWindowSize.Y, 0);
}
}
// 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();
}
string IClipboardManager.GetText()
{
return GLFW.GetClipboardString(_glfwWindow);
@@ -676,6 +795,11 @@ namespace Robust.Client.Graphics.Clyde
GLFW.SetClipboardString(_glfwWindow, text);
}
public IEnumerable<IClydeMonitor> EnumerateMonitors()
{
return _monitors.Values.Select(c => c.Impl);
}
// We can't let exceptions unwind into GLFW, as that can cause the CLR to crash.
// And it probably messes up GLFW too.
// So all the callbacks are passed to this method.
@@ -689,5 +813,28 @@ namespace Robust.Client.Graphics.Clyde
_glfwExceptionList.Add(e);
}
private sealed class MonitorReg
{
public int Id;
public Monitor* Monitor;
public ClydeMonitorImpl Impl = default!;
}
private sealed class ClydeMonitorImpl : IClydeMonitor
{
public ClydeMonitorImpl(int id, string name, Vector2i size, int refreshRate)
{
Id = id;
Name = name;
Size = size;
RefreshRate = refreshRate;
}
public int Id { get; }
public string Name { get; }
public Vector2i Size { get; }
public int RefreshRate { get; }
}
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
@@ -12,10 +11,7 @@ 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
@@ -47,8 +43,6 @@ namespace Robust.Client.Graphics.Clyde
private GLBuffer QuadVBO = default!;
private GLHandle QuadVAO;
private Viewport _mainViewport = default!;
private bool _drawingSplash = true;
private GLShaderProgram? _currentProgram;
@@ -59,13 +53,6 @@ namespace Robust.Client.Graphics.Clyde
private bool _checkGLErrors;
private readonly List<(ScreenshotType type, Action<Image<Rgb24>> callback)> _queuedScreenshots
= new();
private readonly List<(uint pbo, IntPtr sync, Vector2i size, Action<Image<Rgb24>> callback)>
_transferringScreenshots
= new();
public Clyde()
{
// Init main window render target.
@@ -87,7 +74,7 @@ namespace Robust.Client.Graphics.Clyde
{
base.Initialize();
_configurationManager.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
ConfigurationManager.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
if (!InitWindowing())
{
@@ -124,9 +111,9 @@ namespace Robust.Client.Graphics.Clyde
protected override void ReadConfig()
{
base.ReadConfig();
_lightmapDivider = _configurationManager.GetCVar(CVars.DisplayLightMapDivider);
_maxLightsPerScene = _configurationManager.GetCVar(CVars.DisplayMaxLightsPerScene);
_enableSoftShadows = _configurationManager.GetCVar(CVars.DisplaySoftShadows);
_lightmapDivider = ConfigurationManager.GetCVar(CVars.DisplayLightMapDivider);
_maxLightsPerScene = ConfigurationManager.GetCVar(CVars.DisplayMaxLightsPerScene);
_enableSoftShadows = ConfigurationManager.GetCVar(CVars.DisplaySoftShadows);
}
protected override void ReloadConfig()
@@ -152,11 +139,6 @@ namespace Robust.Client.Graphics.Clyde
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);
@@ -238,7 +220,7 @@ namespace Robust.Client.Graphics.Clyde
private (int major, int minor)? ParseGLOverrideVersion()
{
var overrideGLVersion = _configurationManager.GetCVar(CVars.DisplayOGLOverrideVersion);
var overrideGLVersion = ConfigurationManager.GetCVar(CVars.DisplayOGLOverrideVersion);
if (string.IsNullOrEmpty(overrideGLVersion))
{
return null;
@@ -314,18 +296,10 @@ namespace Robust.Client.Graphics.Clyde
ProjViewUBO = new GLUniformBuffer<ProjViewMatrices>(this, BindingIndexProjView, nameof(ProjViewUBO));
UniformConstantsUBO = new GLUniformBuffer<UniformConstants>(this, BindingIndexUniformConstants, nameof(UniformConstantsUBO));
CreateMainViewport();
}
private void CreateMainViewport()
{
var (w, h) = _framebufferSize;
// 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));
screenBufferHandle = new GLHandle(GL.GenTexture());
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
ApplySampleParameters(TextureSampleParameters.Default);
ScreenBufferTexture = GenTexture(screenBufferHandle, _framebufferSize, true, null, TexturePixelType.Rgba32);
}
[Conditional("DEBUG")]

View File

@@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
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;
@@ -37,6 +40,13 @@ namespace Robust.Client.Graphics.Clyde
public IClydeDebugInfo DebugInfo { get; } = new DummyDebugInfo();
public IClydeDebugStats DebugStats { get; } = new DummyDebugStats();
public event Action<TextEventArgs>? TextEntered;
public event Action<MouseMoveEventArgs>? MouseMove;
public event Action<KeyEventArgs>? KeyUp;
public event Action<KeyEventArgs>? KeyDown;
public event Action<MouseWheelEventArgs>? MouseWheel;
public event Action<string>? CloseWindow;
public Texture GetStockTexture(ClydeStockTexture stockTexture)
{
return new DummyTexture((1, 1));
@@ -63,6 +73,11 @@ namespace Robust.Client.Graphics.Clyde
// Nada.
}
public void SetWindowMonitor(IClydeMonitor monitor)
{
// Nada.
}
public void RequestWindowAttention()
{
// Nada.
@@ -86,6 +101,12 @@ namespace Robust.Client.Graphics.Clyde
remove { }
}
public event Action OnWindowScaleChanged
{
add { }
remove { }
}
public void Render()
{
// Nada.
@@ -101,7 +122,7 @@ namespace Robust.Client.Graphics.Clyde
// Nada.
}
public Texture LoadTextureFromPNGStream(Stream stream, string? name = null,
public OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
TextureLoadParameters? loadParams = null)
{
using (var image = Image.Load<Rgba32>(stream))
@@ -110,7 +131,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 +167,23 @@ 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) = 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 ClydeHandle LoadShader(ParsedShader shader, string? name = null)
@@ -267,6 +297,11 @@ namespace Robust.Client.Graphics.Clyde
{
// Nada.
}
public void SetVelocity(Vector2 velocity)
{
// Nada.
}
}
private sealed class DummyBufferedAudioSource : DummyAudioSource, IClydeBufferedAudioSource
@@ -402,12 +437,15 @@ 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) = ClampSubRegion(Size, subRegion);
callback(new Image<T>(x, y));
}
public Texture Texture { get; }
public void Dispose()
{
}
@@ -424,6 +462,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) = ClampSubRegion(Size, subRegion);
callback(new Image<T>(x, y));
}
public void Dispose()
{
}
@@ -449,6 +493,11 @@ namespace Robust.Client.Graphics.Clyde
private sealed class Viewport : IClydeViewport
{
public Viewport(Vector2i size)
{
Size = size;
}
public void Dispose()
{
}
@@ -458,9 +507,38 @@ 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
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using OpenToolkit.Graphics.OpenGL4;

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

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

@@ -18,8 +18,7 @@ namespace Robust.Client.Graphics
/// </summary>
internal abstract class ClydeBase
{
[Dependency] protected readonly IConfigurationManager _configurationManager = default!;
[Dependency] protected readonly IGameControllerInternal _gameController = default!;
[Dependency] protected readonly IConfigurationManager ConfigurationManager = default!;
protected WindowMode WindowMode { get; private set; } = WindowMode.Windowed;
protected bool VSync { get; private set; } = true;
@@ -31,11 +30,11 @@ namespace Robust.Client.Graphics
public virtual bool Initialize()
{
_configurationManager.OnValueChanged(CVars.DisplayVSync, _vSyncChanged, true);
_configurationManager.OnValueChanged(CVars.DisplayWindowMode, _windowModeChanged, true);
_configurationManager.OnValueChanged(CVars.DisplayLightMapDivider, LightmapDividerChanged, true);
_configurationManager.OnValueChanged(CVars.DisplayMaxLightsPerScene, MaxLightsPerSceneChanged, true);
_configurationManager.OnValueChanged(CVars.DisplaySoftShadows, SoftShadowsChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplayVSync, _vSyncChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplayWindowMode, _windowModeChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplayLightMapDivider, LightmapDividerChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplayMaxLightsPerScene, MaxLightsPerSceneChanged, true);
ConfigurationManager.OnValueChanged(CVars.DisplaySoftShadows, SoftShadowsChanged, true);
return true;
}
@@ -51,8 +50,8 @@ namespace Robust.Client.Graphics
protected virtual void ReadConfig()
{
WindowMode = (WindowMode) _configurationManager.GetCVar(CVars.DisplayWindowMode);
VSync = _configurationManager.GetCVar(CVars.DisplayVSync);
WindowMode = (WindowMode) ConfigurationManager.GetCVar(CVars.DisplayWindowMode);
VSync = ConfigurationManager.GetCVar(CVars.DisplayVSync);
}
private void _vSyncChanged(bool newValue)
@@ -86,5 +85,12 @@ namespace Robust.Client.Graphics
protected virtual void SoftShadowsChanged(bool newValue)
{
}
protected static Vector2i ClampSubRegion(Vector2i size, UIBox2i? subRegionSpecified)
{
return subRegionSpecified == null
? size
: UIBox2i.FromDimensions(Vector2i.Zero, size).Intersection(subRegionSpecified.Value)?.Size ?? default;
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Text;
using Robust.Client.ResourceManagement;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
@@ -40,63 +41,48 @@ namespace Robust.Client.Graphics
return GetLineHeight(scale) - GetHeight(scale);
}
[Obsolete("Use GetAscent")] public int Ascent => GetAscent(1);
[Obsolete("Use GetHeight")] public int Height => GetHeight(1);
[Obsolete("Use GetDescent")] public int Descent => GetDescent(1);
[Obsolete("Use GetLineHeight")] public int LineHeight => GetLineHeight(1);
[Obsolete("Use GetLineSeparation")] public int LineSeparation => GetLineSeparation(1);
// Yes, I am aware that using char is bad.
// At the same time the font system is nowhere close to rendering Unicode so...
/// <summary>
/// Draw a character at a certain baseline position on screen.
/// </summary>
/// <param name="handle">The drawing handle to draw to.</param>
/// <param name="chr">
/// The Unicode code point to draw. Yes I'm aware about UTF-16 being crap,
/// do you think this system can draw anything except ASCII?
/// </param>
/// <param name="rune">The Unicode code point to draw.</param>
/// <param name="baseline">The baseline from which to draw the character.</param>
/// <param name="scale">DPI scale factor to render the font at.</param>
/// <param name="color">The color of the character to draw.</param>
/// <param name="fallback">If the character is not available, render "<22>" instead.</param>
/// <returns>How much to advance the cursor to draw the next character.</returns>
[Obsolete("Use DrawChar with scale support.")]
public float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, Color color)
{
return DrawChar(handle, chr, baseline, 1, color);
}
public abstract float DrawChar(
DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale,
Color color, bool fallback=true);
public abstract float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale,
Color color);
[Obsolete("Use Rune variant instead")]
public float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale, Color color)
{
return DrawChar(handle, (Rune) chr, baseline, scale, color);
}
/// <summary>
/// Gets metrics describing the dimensions and positioning of a single glyph in the font.
/// </summary>
/// <param name="chr">The character to fetch the glyph metrics for.</param>
/// <param name="rune">The unicode codepoint to fetch the glyph metrics for.</param>
/// <param name="scale">DPI scale factor to render the font at.</param>
/// <param name="fallback">
/// If the character is not available, return data for "<22>" instead.
/// This can still fail if the font does not define <20> itself.
/// </param>
/// <returns>
/// <c>null</c> if this font does not have a glyph for the specified character,
/// otherwise the metrics you asked for.
/// </returns>
/// <seealso cref="TryGetCharMetrics"/>
[Obsolete("Use GetCharMetrics with scale support.")]
public CharMetrics? GetCharMetrics(char chr)
{
return GetCharMetrics(chr, 1);
}
public abstract CharMetrics? GetCharMetrics(char chr, float scale);
public abstract CharMetrics? GetCharMetrics(Rune rune, float scale, bool fallback=true);
/// <summary>
/// Try-pattern version of <see cref="GetCharMetrics"/>.
/// </summary>
[Obsolete("Use TryGetCharMetrics with scale support.")]
public bool TryGetCharMetrics(char chr, out CharMetrics metrics)
public bool TryGetCharMetrics(Rune rune, float scale, out CharMetrics metrics, bool fallback=true)
{
return TryGetCharMetrics(chr, 1, out metrics);
}
public bool TryGetCharMetrics(char chr, float scale, out CharMetrics metrics)
{
var maybe = GetCharMetrics(chr, scale);
var maybe = GetCharMetrics(rune, scale);
if (maybe.HasValue)
{
metrics = maybe.Value;
@@ -128,15 +114,23 @@ namespace Robust.Client.Graphics
public override int GetDescent(float scale) => Handle.GetDescent(scale);
public override int GetLineHeight(float scale) => Handle.GetLineHeight(scale);
public override float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale, Color color)
public override float DrawChar(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
{
var metrics = Handle.GetCharMetrics(chr, scale);
var metrics = Handle.GetCharMetrics(rune, scale);
if (!metrics.HasValue)
{
return 0;
if (fallback && !Rune.IsWhiteSpace(rune))
{
rune = new Rune('<27>');
metrics = Handle.GetCharMetrics(rune, scale);
if (!metrics.HasValue)
return 0;
}
else
return 0;
}
var texture = Handle.GetCharTexture(chr, scale);
var texture = Handle.GetCharTexture(rune, scale);
if (texture == null)
{
return metrics.Value.Advance;
@@ -147,9 +141,12 @@ namespace Robust.Client.Graphics
return metrics.Value.Advance;
}
public override CharMetrics? GetCharMetrics(char chr, float scale)
public override CharMetrics? GetCharMetrics(Rune rune, float scale, bool fallback=true)
{
return Handle.GetCharMetrics(chr, scale);
var metrics = Handle.GetCharMetrics(rune, scale);
if (metrics == null && !Rune.IsWhiteSpace(rune) && fallback)
return Handle.GetCharMetrics(new Rune('<27>'), scale);
return metrics;
}
}
@@ -160,13 +157,13 @@ namespace Robust.Client.Graphics
public override int GetDescent(float scale) => default;
public override int GetLineHeight(float scale) => default;
public override float DrawChar(DrawingHandleScreen handle, char chr, Vector2 baseline, float scale, Color color)
public override float DrawChar(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
{
// Nada, it's a dummy after all.
return 0;
}
public override CharMetrics? GetCharMetrics(char chr, float scale)
public override CharMetrics? GetCharMetrics(Rune rune, float scale, bool fallback=true)
{
// Nada, it's a dummy after all.
return null;

View File

@@ -4,9 +4,6 @@ using System.IO;
using System.Text;
using JetBrains.Annotations;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SharpFont;
@@ -20,18 +17,18 @@ namespace Robust.Client.Graphics
private const int SheetWidth = 256;
private const int SheetHeight = 256;
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IClyde _clyde = default!;
private readonly IClyde _clyde;
private uint BaseFontDPI;
private uint _baseFontDpi = 96;
private readonly Library _library;
private readonly Dictionary<(FontFaceHandle, int fontSize), FontInstanceHandle> _loadedInstances =
new();
public FontManager()
public FontManager(IClyde clyde)
{
_clyde = clyde;
_library = new Library();
}
@@ -42,9 +39,9 @@ namespace Robust.Client.Graphics
return handle;
}
void IFontManagerInternal.Initialize()
void IFontManagerInternal.SetFontDpi(uint fontDpi)
{
BaseFontDPI = (uint) _configuration.GetCVar(CVars.DisplayFontDpi);
_baseFontDpi = fontDpi;
}
public IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size)
@@ -64,7 +61,7 @@ namespace Robust.Client.Graphics
private ScaledFontData _generateScaledDatum(FontInstanceHandle instance, float scale)
{
var ftFace = instance.FaceHandle.Face;
ftFace.SetCharSize(0, instance.Size, 0, (uint) (BaseFontDPI * scale));
ftFace.SetCharSize(0, instance.Size, 0, (uint) (_baseFontDpi * scale));
var ascent = ftFace.Size.Metrics.Ascender.ToInt32();
var descent = -ftFace.Size.Metrics.Descender.ToInt32();
@@ -83,7 +80,7 @@ namespace Robust.Client.Graphics
return;
var face = instance.FaceHandle.Face;
face.SetCharSize(0, instance.Size, 0, (uint) (BaseFontDPI * scale));
face.SetCharSize(0, instance.Size, 0, (uint) (_baseFontDpi * scale));
face.LoadGlyph(glyph, LoadFlags.Default, LoadTarget.Normal);
face.Glyph.RenderGlyph(RenderMode.Normal);
@@ -189,7 +186,7 @@ namespace Robust.Client.Graphics
OwnedTexture GenSheet()
{
var sheet = _clyde.CreateBlankTexture<A8>((SheetWidth, SheetHeight),
$"font-{face.FamilyName}-{instance.Size}-{(uint) (BaseFontDPI * scale)}-sheet{scaled.AtlasTextures.Count}");
$"font-{face.FamilyName}-{instance.Size}-{(uint) (_baseFontDpi * scale)}-sheet{scaled.AtlasTextures.Count}");
scaled.AtlasTextures.Add(sheet);
return sheet;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Robust.Shared.Maths;
@@ -7,6 +8,8 @@ using SixLabors.ImageSharp.PixelFormats;
namespace Robust.Client.Graphics
{
public delegate void CopyPixelsDelegate<T>(Image<T> pixels) where T : unmanaged, IPixel<T>;
public interface IClyde
{
IRenderWindow MainWindowRenderTarget { get; }
@@ -21,6 +24,7 @@ namespace Robust.Client.Graphics
Vector2 DefaultWindowScale { get; }
void SetWindowTitle(string title);
void SetWindowMonitor(IClydeMonitor monitor);
/// <summary>
/// This is the magic method to make the game window ping you in the task bar.
@@ -31,10 +35,12 @@ namespace Robust.Client.Graphics
event Action<WindowFocusedEventArgs> OnWindowFocused;
Texture LoadTextureFromPNGStream(Stream stream, string? name = null,
event Action OnWindowScaleChanged;
OwnedTexture LoadTextureFromPNGStream(Stream stream, string? name = null,
TextureLoadParameters? loadParams = null);
Texture LoadTextureFromImage<T>(Image<T> image, string? name = null,
OwnedTexture LoadTextureFromImage<T>(Image<T> image, string? name = null,
TextureLoadParameters? loadParams = null) where T : unmanaged, IPixel<T>;
/// <summary>
@@ -92,9 +98,18 @@ namespace Robust.Client.Graphics
/// </summary>
/// <param name="type">What kind of screenshot to take</param>
/// <param name="callback">The callback to run when the screenshot has been made.</param>
void Screenshot(ScreenshotType type, Action<Image<Rgb24>> callback);
/// <param name="subRegion">
/// The subregion of the framebuffer to copy.
/// If null, the whole framebuffer is copied.
/// </param>
/// <seealso cref="ScreenshotAsync"/>
/// <seealso cref="IRenderTarget.CopyPixelsToMemory{T}"/>
void Screenshot(ScreenshotType type, CopyPixelsDelegate<Rgb24> callback, UIBox2i? subRegion = null);
Task<Image<Rgb24>> ScreenshotAsync(ScreenshotType type)
/// <summary>
/// Async version of <see cref="Screenshot"/>.
/// </summary>
Task<Image<Rgb24>> ScreenshotAsync(ScreenshotType type, UIBox2i? subRegion = null)
{
var tcs = new TaskCompletionSource<Image<Rgb24>>();
@@ -103,7 +118,14 @@ namespace Robust.Client.Graphics
return tcs.Task;
}
IClydeViewport CreateViewport(Vector2i size, string? name = null);
IClydeViewport CreateViewport(Vector2i size, string? name = null)
{
return CreateViewport(size, default, name);
}
IClydeViewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters, string? name = null);
IEnumerable<IClydeMonitor> EnumerateMonitors();
}
// TODO: Maybe implement IDisposable for render targets. I got lazy and didn't.

View File

@@ -20,5 +20,6 @@ namespace Robust.Client.Graphics
void SetVolume(float decibels);
void SetOcclusion(float blocks);
void SetPlaybackPosition(float seconds);
void SetVelocity(Vector2 velocity);
}
}

View File

@@ -1,3 +1,4 @@
using System;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Shared.Maths;
@@ -16,6 +17,13 @@ namespace Robust.Client.Graphics
bool Initialize();
void Ready();
event Action<TextEventArgs> TextEntered;
event Action<MouseMoveEventArgs> MouseMove;
event Action<KeyEventArgs> KeyUp;
event Action<KeyEventArgs> KeyDown;
event Action<MouseWheelEventArgs> MouseWheel;
event Action<string> CloseWindow;
ClydeHandle LoadShader(ParsedShader shader, string? name = null);
void ReloadShader(ClydeHandle handle, ParsedShader newShader);

View File

@@ -0,0 +1,18 @@
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
{
/// <summary>
/// Represents a connected monitor on the user's system.
/// </summary>
public interface IClydeMonitor
{
/// <summary>
/// This ID is not consistent between startups of the game.
/// </summary>
int Id { get; }
string Name { get; }
Vector2i Size { get; }
int RefreshRate { get; }
}
}

View File

@@ -1,4 +1,6 @@
using System;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
@@ -16,9 +18,57 @@ namespace Robust.Client.Graphics
IEye? Eye { get; set; }
Vector2i Size { get; }
/// <summary>
/// This is, effectively, a multiplier to the eye's zoom.
/// </summary>
Vector2 RenderScale { get; set; }
/// <summary>
/// If true, <see cref="Render"/> will be automatically called at the start of the frame.
/// </summary>
bool AutomaticRender { get; set; }
/// <summary>
/// Render the state of the world in this viewport, updating the texture inside the render target.
/// </summary>
void Render();
/// <summary>
/// Converts a point in the viewport's screen to world coordinates.
/// </summary>
MapCoordinates LocalToWorld(Vector2 point);
/// <summary>
/// Converts a point in world-space to the viewport's screen coordinates.
/// </summary>
Vector2 WorldToLocal(Vector2 point);
/// <summary>
/// Draw below screen-space overlays for this viewport in UI space.
/// </summary>
/// <param name="handle">The drawing handle to draw with.</param>
/// <param name="control">The control rendering.</param>
/// <param name="viewportBounds">
/// Absolute screen-space bounds to draw the control at.
/// Not relative to the current transform of <see cref="handle"/>.
/// </param>
public void RenderScreenOverlaysBelow(
DrawingHandleScreen handle,
IViewportControl control,
in UIBox2i viewportBounds);
/// <summary>
/// Draw above screen-space overlays for this viewport in UI space.
/// </summary>
/// <param name="handle">The drawing handle to draw with.</param>
/// <param name="control">The control rendering.</param>
/// <param name="viewportBounds">
/// Absolute screen-space bounds to draw the control at.
/// Not relative to the current transform of <see cref="handle"/>.
/// </param>
public void RenderScreenOverlaysAbove(
DrawingHandleScreen handle,
IViewportControl control,
in UIBox2i viewportBounds);
}
}

View File

@@ -12,7 +12,7 @@ namespace Robust.Client.Graphics
{
IFontFaceHandle Load(Stream stream);
IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size);
void Initialize();
void SetFontDpi(uint fontDpi);
}
internal interface IFontFaceHandle

View File

@@ -1,5 +1,6 @@
using System;
using Robust.Shared.Maths;
using SixLabors.ImageSharp.PixelFormats;
namespace Robust.Client.Graphics
{
@@ -12,5 +13,7 @@ namespace Robust.Client.Graphics
/// The size of the render target, in physical pixels.
/// </summary>
Vector2i Size { get; }
void CopyPixelsToMemory<T>(CopyPixelsDelegate<T> callback, UIBox2i? subRegion = null) where T : unmanaged, IPixel<T>;
}
}

View File

@@ -1,22 +1,29 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Robust.Shared.Timing;
namespace Robust.Client.Graphics
{
[PublicAPI]
public interface IOverlayManager
{
void AddOverlay(Overlay overlay);
void RemoveOverlay(string id);
bool HasOverlay(string id);
bool AddOverlay(Overlay overlay);
Overlay GetOverlay(string id);
T GetOverlay<T>(string id) where T : Overlay;
bool RemoveOverlay(Overlay overlay);
bool RemoveOverlay(Type overlayClass);
bool RemoveOverlay<T>() where T : Overlay;
bool TryGetOverlay(string id, [NotNullWhen(true)] out Overlay? overlay);
bool TryGetOverlay<T>(string id, [NotNullWhen(true)] out T? overlay) where T : Overlay;
bool TryGetOverlay(Type overlayClass, out Overlay? overlay);
bool TryGetOverlay<T>(out T? overlay) where T : Overlay;
Overlay GetOverlay(Type overlayClass);
T GetOverlay<T>() where T : Overlay;
bool HasOverlay(Type overlayClass);
bool HasOverlay<T>() where T : Overlay;
IEnumerable<Overlay> AllOverlays { get; }
}

View File

@@ -1,118 +1,105 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.IoC;
using JetBrains.Annotations;
using Robust.Shared.Timing;
using Robust.Shared.Enums;
using System;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics
{
/// <summary>
/// An overlay is used for fullscreen drawing in the game, for example parallax.
/// An overlay is used for fullscreen drawing in the game. This can range from drawing parallax to a full screen shader.
/// </summary>
[PublicAPI]
public abstract class Overlay
{
/// <summary>
/// The ID of this overlay. This is used to identify it inside the <see cref="IOverlayManager"/>.
/// Determines when this overlay is drawn in the rendering queue.
/// </summary>
public string ID { get; }
public virtual bool AlwaysDirty => false;
public bool IsDirty => AlwaysDirty || _isDirty;
public bool Drawing { get; private set; }
public virtual OverlaySpace Space => OverlaySpace.ScreenSpace;
/// <summary>
/// If set to true, <see cref="ScreenTexture"/> will be set to the current frame (at the moment before the overlay is rendered). This can be costly to performance, but
/// some shaders will require it as a passed in uniform to operate.
/// </summary>
public virtual bool RequestScreenTexture => false;
/// <summary>
/// If <see cref="RequestScreenTexture"> is true, then this will be set to the texture corresponding to the current frame. If false, it will always be null.
/// </summary>
public Texture? ScreenTexture = null;
/// <summary>
/// Overlays on the same OverlaySpace will be drawn from lowest ZIndex to highest ZIndex. As an example, ZIndex -1 will be drawn before ZIndex 2.
/// This value is 0 by default. Overlays with same ZIndex will be drawn in an random order.
/// </summary>
public int? ZIndex { get; set; }
protected IOverlayManager OverlayManager { get; }
public int? ZIndex { get; set; }
private bool Disposed = false;
public virtual bool SubHandlesUseMainShader { get; } = true;
private bool _isDirty = true;
private readonly List<DrawingHandleBase> TempHandles = new();
private bool Disposed;
protected Overlay(string id)
public Overlay()
{
OverlayManager = IoCManager.Resolve<IOverlayManager>();
ID = id;
}
public void Dispose()
{
if (Disposed)
{
return;
}
Dispose(true);
Disposed = true;
GC.SuppressFinalize(this);
}
~Overlay()
{
Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
/// <summary>
/// If this function returns true, the target framebuffer will be wiped before applying this overlay to it.
/// </summary>
public virtual bool OverwriteTargetFrameBuffer(){
return false;
}
/// <summary>
/// Draws this overlay to the current space.
/// </summary>
/// <param name="handle">Current drawing handle that the overlay should be drawing with. Do not hold a reference to this in the overlay.</param>
/// <param name="currentSpace">Current space that is being drawn. This function is called for every space that was set up in initialization.</param>
protected abstract void Draw(DrawingHandleBase handle, OverlaySpace currentSpace);
public void Dirty()
{
_isDirty = true;
}
protected internal abstract void Draw(in OverlayDrawArgs args);
protected internal virtual void FrameUpdate(FrameEventArgs args) { }
internal void ClydeRender(IRenderHandle renderHandle, OverlaySpace currentSpace)
~Overlay() {
Dispose();
}
public void Dispose() {
if (Disposed)
return;
else
DisposeBehavior();
}
protected virtual void DisposeBehavior(){
Disposed = true;
GC.SuppressFinalize(this);
}
internal void ClydeRender(
IRenderHandle renderHandle,
OverlaySpace currentSpace,
IViewportControl? vpControl,
IClydeViewport vp,
in UIBox2i screenBox,
in Box2 worldBox)
{
DrawingHandleBase handle;
if (currentSpace == OverlaySpace.WorldSpace)
handle = renderHandle.DrawingHandleWorld;
else
if (currentSpace == OverlaySpace.ScreenSpace || currentSpace == OverlaySpace.ScreenSpaceBelowWorld)
{
DebugTools.AssertNotNull(vpControl);
handle = renderHandle.DrawingHandleScreen;
}
else
{
handle = renderHandle.DrawingHandleWorld;
}
Draw(handle, currentSpace);
var args = new OverlayDrawArgs(currentSpace, vpControl, vp, handle, screenBox, worldBox);
Draw(args);
}
}
/// <summary>
/// Determines in which canvas layers an overlay gets drawn.
/// </summary>
[Flags]
public enum OverlaySpace : byte
{
/// <summary>
/// Used for matching bit flags.
/// </summary>
None = 0b0000,
/// <summary>
/// This overlay will be drawn in the UI root, thus being in screen space.
/// </summary>
ScreenSpace = 0b0001,
/// <summary>
/// This overlay will be drawn in the world root, thus being in world space.
/// </summary>
WorldSpace = 0b0010,
/// <summary>
/// Drawn in screen coordinates, but behind the world.
/// </summary>
ScreenSpaceBelowWorld = 0b0100,
}
}

View File

@@ -0,0 +1,65 @@
using JetBrains.Annotations;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Enums;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
{
/// <summary>
/// Parameters passed to <see cref="Overlay.Draw"/>.
/// </summary>
[PublicAPI]
public readonly ref struct OverlayDrawArgs
{
/// <summary>
/// The overlay space that currently is being rendered for.
/// </summary>
public readonly OverlaySpace Space;
/// <summary>
/// The viewport control that is rendering this viewport.
/// Only available for screen-space overlays.
/// </summary>
public readonly IViewportControl? ViewportControl;
/// <summary>
/// The viewport that is rendering this viewport.
/// </summary>
public readonly IClydeViewport Viewport;
/// <summary>
/// The drawing handle that you can draw with.
/// </summary>
public readonly DrawingHandleBase DrawingHandle;
/// <summary>
/// The screen-space coordinates available to render within.
/// Relevant for screen-space overlay rendering.
/// </summary>
public readonly UIBox2i ViewportBounds;
/// <summary>
/// AABB enclosing the area visible in the viewport.
/// </summary>
public readonly Box2 WorldBounds;
public DrawingHandleScreen ScreenHandle => (DrawingHandleScreen) DrawingHandle;
public DrawingHandleWorld WorldHandle => (DrawingHandleWorld) DrawingHandle;
public OverlayDrawArgs(
OverlaySpace space,
IViewportControl? viewportControl,
IClydeViewport viewport,
DrawingHandleBase drawingHandle,
in UIBox2i viewportBounds,
in Box2 worldBounds)
{
Space = space;
ViewportControl = viewportControl;
Viewport = viewport;
DrawingHandle = drawingHandle;
ViewportBounds = viewportBounds;
WorldBounds = worldBounds;
}
}
}

View File

@@ -1,13 +1,15 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Robust.Client.Graphics
{
internal class OverlayManager : IOverlayManagerInternal
{
private readonly Dictionary<string, Overlay> _overlays = new();
private readonly Dictionary<Type, Overlay> _overlays = new Dictionary<Type, Overlay>();
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
public void FrameUpdate(FrameEventArgs args)
{
@@ -17,59 +19,80 @@ namespace Robust.Client.Graphics
}
}
public void AddOverlay(Overlay overlay)
public bool AddOverlay(Overlay overlay)
{
if (_overlays.ContainsKey(overlay.ID))
{
throw new InvalidOperationException($"We already have an overlay with ID '{overlay.ID}'");
if(_overlays.ContainsKey(overlay.GetType()))
return false;
_overlays.Add(overlay.GetType(), overlay);
return true;
}
public bool RemoveOverlay(Type overlayClass)
{
if(!overlayClass.IsSubclassOf(typeof(Overlay))){
Logger.Error("RemoveOverlay was called with arg: " + overlayClass.ToString() + ", which is not a subclass of Overlay!");
return false;
}
_overlays.Add(overlay.ID, overlay);
return _overlays.Remove(overlayClass);
}
public Overlay GetOverlay(string id)
{
return _overlays[id];
public bool RemoveOverlay<T>() where T : Overlay{
return RemoveOverlay(typeof(T));
}
public T GetOverlay<T>(string id) where T : Overlay
{
return (T) GetOverlay(id);
public bool RemoveOverlay(Overlay overlay) {
return _overlays.Remove(overlay.GetType());
}
public bool HasOverlay(string id)
{
return _overlays.ContainsKey(id);
}
public void RemoveOverlay(string id)
public bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay)
{
if (!_overlays.TryGetValue(id, out var overlay))
{
return;
overlay = null;
if (!overlayClass.IsSubclassOf(typeof(Overlay))){
Logger.Error("TryGetOverlay was called with arg: " + overlayClass.ToString() + ", which is not a subclass of Overlay!");
return false;
}
overlay.Dispose();
_overlays.Remove(id);
return _overlays.TryGetValue(overlayClass, out overlay);
}
public bool TryGetOverlay(string id, [NotNullWhen(true)] out Overlay? overlay)
{
return _overlays.TryGetValue(id, out overlay);
}
public bool TryGetOverlay<T>(string id, [NotNullWhen(true)] out T? overlay) where T : Overlay
{
if (_overlays.TryGetValue(id, out var value))
{
overlay = (T) value;
public bool TryGetOverlay<T>([NotNullWhen(true)] out T? overlay) where T : Overlay {
overlay = null;
if(_overlays.TryGetValue(typeof(T), out Overlay? toReturn)){
overlay = (T)toReturn;
return true;
}
overlay = default;
return false;
}
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
public Overlay GetOverlay(Type overlayClass) {
return _overlays[overlayClass];
}
public T GetOverlay<T>() where T : Overlay {
return (T)_overlays[typeof(T)];
}
public bool HasOverlay(Type overlayClass) {
if (!overlayClass.IsSubclassOf(typeof(Overlay)))
Logger.Error("HasOverlay was called with arg: " + overlayClass.ToString() + ", which is not a subclass of Overlay!");
return _overlays.ContainsKey(overlayClass);
}
public bool HasOverlay<T>() where T : Overlay {
return _overlays.ContainsKey(typeof(T));
}
}
}

View File

@@ -19,10 +19,10 @@ namespace Robust.Client.Graphics
public sealed class State : IRsiStateLike
{
// List of delays for the frame to reach the next frame.
private readonly float[] Delays;
public readonly float[] Delays;
// 2D array for the texture to use for each animation frame at each direction.
private readonly Texture[][] Icons;
public readonly Texture[][] Icons;
internal State(Vector2i size, StateId stateId, DirectionType direction, float[] delays, Texture[][] icons)
{

View File

@@ -1,47 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "RSI Image Format Validation Schema V1",
"description": "Robust Station Image",
"type": "object",
"definitions": {
"size": {
"type": "object",
"properties": {
"x": {"type": "integer", "minimum": 1},
"y": {"type": "integer", "minimum": 1}
},
"required": ["x","y"]
},
"directions": {
"type": "integer",
"enum": [1,4,8]
},
"state": {
"type": "object",
"properties": {
"name": {"type": "string"},
"flags": {"type": "object"}, //To be de-serialized as a Dictionary
"directions": {"$ref": "#/definitions/directions"},
"delays": {
"type": "array",
"minItems": 1,
"items": {
"type": "array",
"items": {"type": "number", "minimum": 0, "exclusiveMinimum": true} //number == float
}
}
},
"required": ["name"] //'delays' is marked as optional in the spec
}
},
"properties": {
"version": {"type": "integer", "minimum": 1, "maximum": 1},
"size": {"$ref": "#/definitions/size"},
"states": {
"type": "array",
"items": {"$ref": "#/definitions/state"},
"minItems": 1
}
},
"required": ["version","size","states"]
}

View File

@@ -1,8 +1,13 @@
namespace Robust.Client.Graphics
{
/// <summary>
/// Types of screenshots to take, at various stages of the render pipeline.
/// </summary>
public enum ScreenshotType : byte
{
BeforeUI,
AfterUI
/// <summary>
/// Final framebuffer that will be presented to the window.
/// </summary>
Final
}
}

View File

@@ -1,33 +0,0 @@
namespace Robust.Client.Graphics
{
public enum ShaderParamType : byte
{
// Can this even happen?
Void = 0,
Bool,
BVec2,
BVec3,
BVec4,
UInt,
/// <summary>
/// While Godot supports all (u)int vectors,
/// It doesn't specify which is used from get params.
/// So ivec2, ivec3, ivec4... are all this guy.
/// </summary>
IntVec,
Int,
Float,
Vec2,
Vec3,
Vec4,
Mat2,
Mat3,
Mat4,
/// <summary>
/// Godot supports u, i and b samplers too,
/// but we can't tell in code.
/// </summary>
Sampler2D,
SamplerCube
}
}

View File

@@ -19,7 +19,7 @@ namespace Robust.Client.Graphics
[Dependency] private readonly IResourceCache _resourceCache = default!;
[ViewVariables]
[field: DataField("id", required: true)]
[DataField("id", required: true)]
public string ID { get; } = default!;
private ShaderKind Kind;

View File

@@ -70,5 +70,15 @@ namespace Robust.Client
/// Disconnects the connected BaseClient from a remote server.
/// </summary>
void DisconnectFromServer(string reason);
/// <summary>
/// Starts the single player mode.
/// </summary>
void StartSinglePlayer();
/// <summary>
/// Stops the single player mode.
/// </summary>
void StopSinglePlayer();
}
}

View File

@@ -7,6 +7,8 @@ namespace Robust.Client
{
internal interface IGameControllerInternal : IGameController
{
GameControllerOptions Options { get; }
bool ContentStart { get; set; }
void SetCommandLineArgs(CommandLineArgs args);
bool LoadConfigAndUserData { get; set; }
bool Startup(Func<ILogHandler>? logHandlerFactory = null);
@@ -18,4 +20,4 @@ namespace Robust.Client
void MouseWheel(MouseWheelEventArgs mouseWheelEventArgs);
void OverrideMainLoop(IGameLoop gameLoop);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Text;
using Robust.Shared.Maths;
namespace Robust.Client.Input
@@ -58,6 +59,7 @@ namespace Robust.Client.Input
}
public uint CodePoint { get; }
public Rune AsRune => new Rune(CodePoint);
}
public class KeyEventArgs : ModifierInputEventArgs

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.UserInterface;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Maths;
@@ -14,6 +15,16 @@ namespace Robust.Client.Input
{
bool Enabled { get; set; }
/// <summary>
/// Relay a key event that hit a viewport further down the input stack.
/// </summary>
/// <remarks>
/// This is ONLY intended to be used in key handler from viewport controls.
/// </remarks>
/// <param name="control">The viewport control that relayed the input.</param>
/// <param name="eventArgs">The key event args triggering the input.</param>
void ViewportKeyEvent(Control? control, BoundKeyEventArgs eventArgs);
Vector2 MouseScreenPosition { get; }
BoundKeyMap NetworkBindMap { get; }
@@ -60,12 +71,12 @@ namespace Robust.Client.Input
/// <summary>
/// UIKeyBindStateChanged is called when a keybind is found.
/// </summary>
event Func<BoundKeyEventArgs, bool> UIKeyBindStateChanged;
event Func<BoundKeyEventArgs, bool>? UIKeyBindStateChanged;
/// <summary>
/// If UIKeyBindStateChanged did not handle the BoundKeyEvent, KeyBindStateChanged is called.
/// </summary>
event Action<BoundKeyEventArgs> KeyBindStateChanged;
event Action<ViewportBoundKeyEventArgs>? KeyBindStateChanged;
IEnumerable<BoundKeyFunction> DownKeyFunctions { get; }

View File

@@ -42,6 +42,8 @@ namespace Robust.Client.Input
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManagerInternal = default!;
private bool _currentlyFindingViewport;
private readonly List<KeyBindingRegistration> _defaultRegistrations = new();
private readonly Dictionary<BoundKeyFunction, InputCmdHandler> _commands =
@@ -68,7 +70,7 @@ namespace Robust.Client.Input
public event Func<BoundKeyEventArgs, bool>? UIKeyBindStateChanged;
/// <inheritdoc />
public event Action<BoundKeyEventArgs>? KeyBindStateChanged;
public event Action<ViewportBoundKeyEventArgs>? KeyBindStateChanged;
public IEnumerable<BoundKeyFunction> DownKeyFunctions => _bindings
.Where(x => x.State == BoundKeyState.Down)
@@ -329,36 +331,67 @@ namespace Robust.Client.Input
private bool SetBindState(KeyBinding binding, BoundKeyState state, bool uiOnly = false)
{
binding.State = state;
// christ this crap *is* re-entrant thanks to PlacementManager and
// I honestly have no idea what the best solution here is.
DebugTools.Assert(!_currentlyFindingViewport, "Re-entrant key events??");
var eventArgs = new BoundKeyEventArgs(binding.Function, binding.State,
new ScreenCoordinates(MouseScreenPosition), binding.CanFocus);
var handled = UIKeyBindStateChanged?.Invoke(eventArgs);
if (state == BoundKeyState.Up
|| !(handled == true || eventArgs.Handled)
&& !uiOnly)
try
{
var cmd = GetInputCommand(binding.Function);
// TODO: Allow input commands to still get forwarded to server if necessary.
if (cmd != null)
// This is terrible but anyways.
// This flag keeps track of "did a viewport fire the key up for us" so we know we don't do it again.
_currentlyFindingViewport = true;
binding.State = state;
var eventArgs = new BoundKeyEventArgs(binding.Function, binding.State,
new ScreenCoordinates(MouseScreenPosition), binding.CanFocus);
// UI returns true here into blockPass if it wants to prevent us from giving input events
// to the viewport, but doesn't want it hard-handled so we keep processing possible key actions.
var blockPass = UIKeyBindStateChanged?.Invoke(eventArgs);
if ((state == BoundKeyState.Up || (!(blockPass == true || eventArgs.Handled) && !uiOnly))
&& _currentlyFindingViewport)
{
if (state == BoundKeyState.Up)
{
cmd.Disabled(null);
}
else
{
cmd.Enabled(null);
}
ViewportKeyEvent(null, eventArgs);
}
return eventArgs.Handled;
}
finally
{
_currentlyFindingViewport = false;
}
}
public void ViewportKeyEvent(Control? viewport, BoundKeyEventArgs eventArgs)
{
_currentlyFindingViewport = false;
var cmd = GetInputCommand(eventArgs.Function);
// TODO: Allow input commands to still get forwarded to server if necessary.
if (cmd != null)
{
// Out-of-simulation input event
if (eventArgs.State == BoundKeyState.Up)
{
cmd.Disabled(null);
}
else
{
KeyBindStateChanged?.Invoke(eventArgs);
cmd.Enabled(null);
}
}
else
{
var viewportEventArgs = new ViewportBoundKeyEventArgs(eventArgs, viewport);
// In-simulation input event (through content to InputSystem)
KeyBindStateChanged?.Invoke(viewportEventArgs);
return eventArgs.Handled;
if (viewportEventArgs.KeyEventArgs.Handled)
{
eventArgs.Handle();
}
}
}
private bool PackedMatchesPressedState(PackedKeyCombo packed)

View File

@@ -0,0 +1,17 @@
using Robust.Client.UserInterface;
using Robust.Shared.Input;
namespace Robust.Client.Input
{
public sealed class ViewportBoundKeyEventArgs
{
public BoundKeyEventArgs KeyEventArgs { get; }
public Control? Viewport { get; }
public ViewportBoundKeyEventArgs(BoundKeyEventArgs keyEventArgs, Control? viewport)
{
KeyEventArgs = keyEventArgs;
Viewport = viewport;
}
}
}

View File

@@ -1,92 +1,23 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Shared.Map
namespace Robust.Client.Map
{
internal partial class MapManager
internal class ClientMapManager : MapManager, IClientMapManager
{
[Dependency] private readonly INetManager _netManager = default!;
public GameStateMapData? GetStateData(GameTick fromTick)
{
var gridDatums = new Dictionary<GridId, GameStateMapData.GridDatum>();
foreach (var grid in _grids.Values)
{
if (grid.LastModifiedTick < fromTick)
{
continue;
}
var chunkData = new List<GameStateMapData.ChunkDatum>();
foreach (var (index, chunk) in grid.GetMapChunks())
{
if (chunk.LastModifiedTick < fromTick)
{
continue;
}
var tileBuffer = new Tile[grid.ChunkSize * (uint) grid.ChunkSize];
// Flatten the tile array.
// NetSerializer doesn't do multi-dimensional arrays.
// This is probably really expensive.
for (var x = 0; x < grid.ChunkSize; x++)
{
for (var y = 0; y < grid.ChunkSize; y++)
{
tileBuffer[x * grid.ChunkSize + y] = chunk.GetTile((ushort)x, (ushort)y);
}
}
chunkData.Add(new GameStateMapData.ChunkDatum(index, tileBuffer));
}
var gridDatum =
new GameStateMapData.GridDatum(chunkData.ToArray(), new MapCoordinates(grid.WorldPosition, grid.ParentMapId));
gridDatums.Add(grid.Index, gridDatum);
}
var mapDeletionsData = _mapDeletionHistory.Where(d => d.tick >= fromTick).Select(d => d.mapId).ToList();
var gridDeletionsData = _gridDeletionHistory.Where(d => d.tick >= fromTick).Select(d => d.gridId).ToList();
var mapCreations = _mapCreationTick.Where(kv => kv.Value >= fromTick && kv.Key != MapId.Nullspace)
.Select(kv => kv.Key).ToArray();
var gridCreations = _grids.Values.Where(g => g.CreatedTick >= fromTick && g.ParentMapId != MapId.Nullspace).ToDictionary(g => g.Index,
grid => new GameStateMapData.GridCreationDatum(grid.ChunkSize, grid.SnapSize));
// no point sending empty collections
if (gridDatums.Count == 0) gridDatums = default;
if (gridDeletionsData.Count == 0) gridDeletionsData = default;
if (mapDeletionsData.Count == 0) mapDeletionsData = default;
if (mapCreations.Length == 0) mapCreations = default;
if (gridCreations.Count == 0) gridCreations = default;
// no point even creating an empty map state if no data
if (gridDatums == null && gridDeletionsData == null && mapDeletionsData == null && mapCreations == null && gridCreations == null)
return default;
return new GameStateMapData(gridDatums?.ToArray(), gridDeletionsData?.ToArray(), mapDeletionsData?.ToArray(), mapCreations?.ToArray(), gridCreations?.ToArray());
}
public void CullDeletionHistory(GameTick uptoTick)
{
_mapDeletionHistory.RemoveAll(t => t.tick < uptoTick);
_gridDeletionHistory.RemoveAll(t => t.tick < uptoTick);
}
public void ApplyGameStatePre(GameStateMapData? data)
{
DebugTools.Assert(_netManager.IsClient, "Only the client should call this.");
// There was no map data this tick, so nothing to do.
if(data == null)
return;
@@ -125,8 +56,7 @@ namespace Robust.Shared.Map
continue;
}
CreateGrid(gridData![gridId].Coordinates.MapId, gridId, creationDatum.ChunkSize,
creationDatum.SnapSize);
CreateGrid(gridData![gridId].Coordinates.MapId, gridId, creationDatum.ChunkSize);
}
}
@@ -172,7 +102,7 @@ namespace Robust.Shared.Map
if (modified.Count != 0)
{
GridChanged?.Invoke(this, new GridChangedEventArgs(grid, modified));
InvokeGridChanged(this, new GridChangedEventArgs(grid, modified));
}
}
@@ -202,11 +132,11 @@ namespace Robust.Shared.Map
continue;
// get the existing client entity for the map.
var cEntity = _entityManager.GetEntity(_mapEntities[mapId]);
var cEntity = EntityManager.GetEntity(_mapEntities[mapId]);
// locate the entity that represents this map that was just sent to us
IEntity? sharedMapEntity = null;
var mapComps = _entityManager.ComponentManager.EntityQuery<IMapComponent>(true);
var mapComps = EntityManager.ComponentManager.EntityQuery<IMapComponent>(true);
foreach (var mapComp in mapComps)
{
if (!mapComp.Owner.Uid.IsClientSide() && mapComp.WorldMap == mapId)
@@ -251,12 +181,16 @@ namespace Robust.Shared.Map
continue;
// remove the existing client entity.
var cEntity = _entityManager.GetEntity(grid.GridEntityId);
var cEntity = EntityManager.GetEntity(grid.GridEntityId);
var cGridComp = cEntity.GetComponent<IMapGridComponent>();
cGridComp.ClearGridId();
// prevents us from deleting the grid when deleting the grid entity
if(cEntity.Uid.IsClientSide())
cGridComp.ClearGridId();
cEntity.Delete(); // normal entities are already parented to the shared comp, client comp has no children
var gridComps = _entityManager.ComponentManager.EntityQuery<IMapGridComponent>(true);
var gridComps = EntityManager.ComponentManager.EntityQuery<IMapGridComponent>(true);
foreach (var gridComp in gridComps)
{
if (gridComp.GridIndex == kvNewGrid.Key)

View File

@@ -16,7 +16,9 @@ namespace Robust.Client.Map
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
public Texture TileTextureAtlas { get; private set; } = default!;
private Texture? _tileTextureAtlas;
public Texture TileTextureAtlas => _tileTextureAtlas ?? Texture.Transparent;
private readonly Dictionary<ushort, Box2> _tileRegions = new();
@@ -40,6 +42,11 @@ namespace Robust.Client.Map
private void _genTextureAtlas()
{
var defList = TileDefs.Where(t => !string.IsNullOrEmpty(t.SpriteName)).ToList();
// If there are no tile definitions, we do nothing.
if (defList.Count <= 0)
return;
const int tileSize = EyeManager.PixelsPerMeter;
var dimensionX = (int) Math.Ceiling(Math.Sqrt(defList.Count));
@@ -77,7 +84,7 @@ namespace Robust.Client.Map
tileSize / w, tileSize / h));
}
TileTextureAtlas = Texture.LoadFromImage(sheet, "Tile Atlas");
_tileTextureAtlas = Texture.LoadFromImage(sheet, "Tile Atlas");
}
}
}

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