Compare commits

..

178 Commits

Author SHA1 Message Date
metalgearsloth
b73b3007f2 Version: 0.16.0.0 2022-05-16 13:20:12 +10:00
Leon Friedrich
37dd4b0893 Pass sprite component in AppearanceChangeEvent (#2829) 2022-05-16 13:19:40 +10:00
metalgearsloth
07c5a38582 Issue collision change events on entity spawns again (#2828)
I thought I'd think of something better for pathfinding but I did not.
2022-05-16 13:17:48 +10:00
metalgearsloth
c866c6b59b Optimise grid traversal for pop-in (#2825)
This was like 1/3 of the frame spikes from PVS pop-in. The main benefit is just checking if the entity is anchored for grid traversal and the secondary was earlying-out handling component state if it's sent to nullspace.
2022-05-16 07:37:01 +10:00
ElectroJr
c3e97cb97e Version: 0.15.1.0 2022-05-15 15:25:38 +12:00
Leon Friedrich
52ffb97369 Change overlay screen texture wrapping (#2749) 2022-05-15 13:20:52 +10:00
Leon Friedrich
bad5657725 MapLoader and mapping command changes (#2744) 2022-05-15 12:28:38 +10:00
wrexbe
f8dc3c8a0e Add some CodeAnalysis attributes to whitelist (#2811) 2022-05-15 12:25:57 +10:00
Paul Ritter
77493b5d14 caches reflection in serv3 datadefinition delegates (#2790)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2022-05-14 16:37:18 +10:00
metalgearsloth
b1ceafbe4f Version: 0.15.0.0 2022-05-14 15:14:10 +10:00
Pieter-Jan Briers
41e9d9b08b Client profiling system (#2783)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2022-05-14 15:08:46 +10:00
metalgearsloth
dc32549c75 Version: 0.14.7.0 2022-05-14 14:55:56 +10:00
metalgearsloth
17dd3af3c7 ECS metadata pausing (#2800) 2022-05-14 14:55:42 +10:00
metalgearsloth
32bdc19fe9 Final grid movement optimisation (#2711) 2022-05-14 14:54:31 +10:00
metalgearsloth
70fbefbe62 Fix joints crash (#2816) 2022-05-14 14:51:45 +10:00
Vera Aguilera Puerto
64cd6ea9be Adds GridUid field to TileRef. (#2815)
* Adds GridUid field to TileRef.

* actually compiles
2022-05-13 18:00:33 +10:00
metalgearsloth
aa92c2444d Version: 0.14.6.0 2022-05-13 12:07:59 +10:00
metalgearsloth
9b19626f1b Port mousejoint from box2d (#2814) 2022-05-13 00:21:39 +10:00
Moony
2bbe5193ee Moves that disable connect option nobody can use to somewhere people can use it. (#2812) 2022-05-12 16:02:12 +02:00
metalgearsloth
d11318f082 Make tile placement much less jank (#2785) 2022-05-12 22:27:06 +10:00
wrexbe
237f948d99 BinaryPrimitives > BitConverter (#2813)
* BinaryPrimitives > BitConverter

* Forgot a :
2022-05-12 07:57:21 +02:00
Kara D
f3449be1e9 Version: 0.14.5.0 2022-05-11 13:43:58 -07:00
Vera Aguilera Puerto
b95ffda711 EntityCoordinates methods for getting grid/map EntityUid.
Uses the new Transform properties that return grid/map EntityUids.
2022-05-11 14:24:46 +02:00
Moony
0932fa0058 Add the concurrent collections and BitConverter to the allow list. (#2810) 2022-05-11 00:11:32 +02:00
Pieter-Jan Briers
da2e46b81b Increase command timeout on benchmark SSh action 2022-05-10 16:05:53 +02:00
Pieter-Jan Briers
d4a1fcc9c6 Fix database code for benchmarks. 2022-05-10 15:44:25 +02:00
Pieter-Jan Briers
c7e7f4f28f make run_benchmarks.py handle errors better. 2022-05-10 14:44:53 +02:00
Pieter-Jan Briers
87e191b8d7 Benchmarks CI attempt 6 2022-05-10 14:35:12 +02:00
Pieter-Jan Briers
05b21fcfcc Benchmark CI attempt 5 2022-05-10 11:54:25 +02:00
Pieter-Jan Briers
48e4ae87d8 Benchmark CI attempt 4(?) 2022-05-10 11:18:04 +02:00
Pieter-Jan Briers
8b090034f1 Benchmarks CI fix attempt 3 2022-05-10 11:14:17 +02:00
Pieter-Jan Briers
41c183ac09 Fix GITHUB_SHA? 2022-05-10 11:10:57 +02:00
Pieter-Jan Briers
4380b95451 Fix benchmarks attempt 1 2022-05-10 11:06:57 +02:00
Pieter-Jan Briers
90fc486a20 Allow manual dispatch of benchmarks 2022-05-10 11:00:52 +02:00
Paul Ritter
f254153962 benchmark script (#2745)
* benchmark script

* more changes

* oopsie

* adjust workflow

* Put a concurrency limit on the benchmarks action

* Update run_benchmarks.py

Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2022-05-10 10:58:42 +02:00
Moony
a2538d1905 Add the setter for images to the sandbox list (#2802) 2022-05-10 09:35:55 +02:00
metalgearsloth
5d7a2879a7 Fix scaling consistency between sprite and bounds (#2803) 2022-05-10 00:34:07 -07:00
20kdc
986adf6494 RobustToolbox side of Redial API. (#2804)
* RobustToolbox side of Redial API.

Please be aware that as far as I know testing this code is nigh-impossible until it ships.

* a smidge of paranoia

* paranoia on the paranoia please
2022-05-10 09:24:46 +02:00
Pieter-Jan Briers
15932fb9aa DebugTools.Assert(bool, string) does not allocate if string is a format string anymore. 2022-05-10 00:59:36 +02:00
Pieter-Jan Briers
3ae4bb03f5 Enable NetManager latency sim on release, add no-bye to INetChannel.
A minute amount of tomfoolery.
2022-05-09 15:43:30 +02:00
metalgearsloth
7f47478997 Make physics testbed slightly less annoying to use (#2794) 2022-05-08 23:28:54 +10:00
metalgearsloth
a2923450ea Optimise occluder dirtying (#2801)
Mostly when stuff spawning in.
2022-05-08 15:16:56 +10:00
metalgearsloth
5366b9a35b Cleanup collisionmanager (#2792)
1. Split it into partials because my scroll wheel was hating me.
2. Removed AABB shapetypes because contacts were made fast enough it doesn't matter.
2022-05-08 14:41:36 +10:00
metalgearsloth
5ca37c68e2 Optimise physics init (#2786) 2022-05-08 14:39:54 +10:00
Vera Aguilera Puerto
22aa274d03 Add GridUid and MapUid properties to TransformComponent. (#2798) 2022-05-07 22:43:00 +10:00
metalgearsloth
54bbe55bb9 Version: 0.14.4.0 2022-05-07 21:42:57 +10:00
metalgearsloth
9706576e66 Fix grid splits for empty chunks (#2797) 2022-05-07 13:26:26 +10:00
metalgearsloth
b2ccd65da4 Fix showgridnodes colours (#2796) 2022-05-07 11:17:28 +10:00
Paul
c587cd2e12 hm 2022-05-06 18:36:50 +02:00
Paul Ritter
ba23a989a9 make pvs account for lastack properly (#2733) 2022-05-06 18:09:30 +02:00
metalgearsloth
c6c69668c8 Fix polyshape centroids (#2793) 2022-05-05 22:42:35 +10:00
Leon Friedrich
b0a4593f44 Speed up map init, and unpause map on init (#2781)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2022-05-05 19:21:03 +10:00
metalgearsloth
2d6e699e2c Mark component name as obsolete (#2791) 2022-05-05 18:42:38 +10:00
metalgearsloth
4307509402 ECS even more transform (#2742)
Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
2022-05-05 13:20:23 +10:00
Paul
ed165b1e4c removes some iocresolves in mapmanager 2022-05-04 23:17:47 +02:00
Paul
3503300a23 minor serv3 optimization 2022-05-04 23:05:28 +02:00
Paul
3ab3d255ed fix seed benchmark 2022-05-04 18:43:17 +02:00
metalgearsloth
fd625e0b30 Fix scale command (#2784)
* Fix scale command

Everything will be visualizers.

* comment for future coders
2022-05-04 22:49:42 +10:00
ElectroJr
53af37cd56 Version: 0.14.3.1 2022-04-30 01:51:51 +12:00
Leon Friedrich
6bf4945181 Allow int uniforms to specify precision (#2779) 2022-04-29 23:48:08 +10:00
metalgearsloth
868320c513 Fix VV lineedit crash 2022-04-29 00:41:14 +10:00
Leon Friedrich
577b836420 Fix grid-traversal parenting (#2777) 2022-04-28 18:08:56 +10:00
metalgearsloth
4a36b52657 Nuke an xform log
Probably not worth it.
2022-04-27 00:35:38 +10:00
metalgearsloth
8dc944d22d Stop invalid broadphase change from crashing the game (#2717) 2022-04-26 23:25:28 +10:00
metalgearsloth
0a10170155 Fix map change crash (#2716)
* Fix map change crash

Follower really out here exposing all the ordering bugs.

* Log AttachToGridOrMap
2022-04-26 23:21:58 +10:00
metalgearsloth
dfa2c222d1 Version: 0.14.3.0 2022-04-26 01:18:07 +10:00
metalgearsloth
e288af162c Add event for post-grid-split (#2774) 2022-04-26 01:17:22 +10:00
Kara
6280820c86 Merge pull request #2768 from ShadowCommander/errorcheck-vv-lineedit
Add null check to VV string parsing
2022-04-24 22:51:49 -07:00
ShadowCommander
102b31f83d Add null check to VV string parsing 2022-04-24 02:16:42 -07:00
metalgearsloth
37ca9ca19a Version: 0.14.2.4 2022-04-24 16:40:10 +10:00
metalgearsloth
0c151ad456 Fix grid deletion for SetTiles (#2767) 2022-04-24 16:39:57 +10:00
metalgearsloth
55e3f749b4 Version: 0.14.2.3 2022-04-24 16:07:19 +10:00
metalgearsloth
16902d7fbc Hotfix anchoring (#2764) 2022-04-24 16:06:50 +10:00
metalgearsloth
c37f53e083 Issue re-parent event on split (#2765) 2022-04-24 15:25:55 +10:00
metalgearsloth
4cc499afdf Version: 0.14.2.2 2022-04-24 13:22:39 +10:00
20kdc
48895efad7 Flexible object pool for ZStd compression contexts rather than the array-based version (#2763) 2022-04-23 20:17:59 +02:00
metalgearsloth
eb46b04c0e Version: 0.14.2.1 2022-04-24 00:58:38 +10:00
metalgearsloth
442de12b99 Grid splitting (#2743)
Co-authored-by: Vera Aguilera Puerto <gradientvera@outlook.com>
2022-04-24 00:57:56 +10:00
Vera Aguilera Puerto
712c1f3559 Adds "ressys" and IEntitySystemManager dependency to scripting globals. (#2762)
"ressys" resolves an `EntitySystem`.
2022-04-24 00:55:31 +10:00
Vera Aguilera Puerto
5a5cfa1eba Adds GetGridOrMapTilePosition to SharedTransformSystem. (#2759)
A little helper method that would be VERY useful for many content things... Such as atmos.
2022-04-24 00:55:11 +10:00
metalgearsloth
7988386bc3 Version: 0.14.1.1 2022-04-24 00:53:41 +10:00
Leon Friedrich
76bfe2905d Add support for float[] and vec2[] in shaders (#2751)
* huh it works.

* vector arrays
2022-04-22 22:43:17 +02:00
ShadowCommander
23893bcacd Change LineEdit to keep cursor position (#2752)
* Change LineEdit to keep cursor position

* Revert console changes
2022-04-22 22:42:24 +02:00
Pieter-Jan Briers
c1da159e8f Rely on SharpZstd -Bsymbolic 2022-04-22 22:39:59 +02:00
20kdc
13889acb37 Fix crash caused by passing uninitialized structure to ZSTD_inBuffer (#2760) 2022-04-22 14:10:20 +02:00
Vera Aguilera Puerto
ffb3de3f2c Fix loading soundfonts from filesystem on Linux 2022-04-21 17:52:27 +02:00
Flipp Syder
87cd45fc64 Adds custom midi soundfont support (sandboxed) (#2746) 2022-04-21 10:05:57 +02:00
Leon Friedrich
1cf914813c Add PVSOverrideSystem (#2747)
* Add PVSOverrideSystem

* add session and clear functions
2022-04-20 16:00:18 +02:00
Pieter-Jan Briers
86d61f8d03 Version: 0.14.0.1 2022-04-20 15:39:32 +02:00
Pieter-Jan Briers
8f638fbf9e HOTFIX ParallelManager out of bounds
I could have sworn I made this -1.

Completely untested
2022-04-20 15:21:17 +02:00
metalgearsloth
95ac134a07 Comment out msgstate logger (#2754)
Getting spammed and I don't think we want to log here long-term anyway.
2022-04-20 13:13:15 +02:00
metalgearsloth
4e65f9b2a1 Version: 0.14.0.0 2022-04-20 18:48:06 +10:00
Paul
d60bbe9fe9 misc client optimizations 2022-04-18 22:34:49 +02:00
Pieter-Jan Briers
dec1495e1e Remove reference to ManagedHttpListener.
How did this ever compile?
2022-04-17 19:13:24 +02:00
Paul
dc72c6fe22 fix typo 2022-04-17 18:15:41 +02:00
Paul
141b1205c6 removes some todos 2022-04-17 18:09:46 +02:00
Pieter-Jan Briers
65f4a09ad5 Try to fix ZStd stuff for servers. 2022-04-17 18:02:35 +02:00
Pieter-Jan Briers
3d1545c0b9 Fix incorrect unsubscription to AssemblyLoadContext.Default.Resolving.
+= instead of -=.
2022-04-17 18:02:35 +02:00
metalgearsloth
ec26dd622b DetachParentToNull tweaks (#2741)
* DetachParentToNull tweaks

The other parent change message already has the mapid and gridid updated when issuing the event.

We'll also guard the event by checking if they're already in nullspace.

* woops
2022-04-17 17:28:07 +10:00
metalgearsloth
0ab3131964 ECS transform states (#2710)
* ECS transform states

Turns out it also saves us a GetComponent on the parent too which is nice.

* Fix test
2022-04-17 14:55:29 +10:00
Pieter-Jan Briers
588a9e9f63 ZSTD game states + other improvements (#2737) 2022-04-16 14:36:59 +02:00
Pieter-Jan Briers
f2fa930edd Block explicit layout types in sandboxing. 2022-04-16 14:17:37 +02:00
Kara D
ec47229a37 Version: 0.13.0.0 2022-04-15 14:36:50 -07:00
mirrorcult
bf5d1d58a8 Merge pull request #2715 from moonheart08/better-map-loading 2022-04-15 14:34:48 -07:00
mirrorcult
8b4da24ee7 Merge pull request #2722 from PaulRitter/2022_04_12_abstract_validation 2022-04-15 14:34:22 -07:00
mirrorcult
3fba108d70 Merge pull request #2734 from PJB3005/22-04-14-spacewizards-http 2022-04-15 14:34:01 -07:00
mirrorcult
35029f0eed Merge pull request #2692 from vulppine/color-sliders 2022-04-15 14:33:21 -07:00
mirrorcult
b66ab9d7c6 Merge pull request #2727 from ElectroJr/update-mapping-except 2022-04-15 14:31:30 -07:00
mirrorcult
5e0b745ba9 Merge pull request #2739 from PJB3005/22-04-15-remove-createnetmsg 2022-04-15 14:30:32 -07:00
Pieter-Jan Briers
45d906ba7e Deprecate CreateNetMessage<T>. 2022-04-15 18:47:42 +02:00
Pieter-Jan Briers
24b124fb17 Fix client reconnect 2022-04-15 15:28:45 +02:00
Vera Aguilera Puerto
7cb0978468 Revert "Adds custom MIDI soundfont support (#2720)"
This reverts commit 9ff46b9aad.

Sandbox violation, oops.
2022-04-15 12:46:30 +02:00
mirrorcult
44cb135a1d Even shorter pretty-print type abbreviations for VV (#2735) 2022-04-15 09:15:10 +02:00
Pieter-Jan Briers
146b673203 Move string serializer to ZStd and BLAKE2b.
Faster and smaller
2022-04-15 01:16:35 +02:00
Pieter-Jan Briers
b0d23c5665 Remove zstd & libsodium natives from client publishes.
Save a good megabyte.
2022-04-15 00:39:56 +02:00
Pieter-Jan Briers
68f89c8958 SkipIfSandboxedAttribute
Should allow something like OpenDream to provide unsandboxed-only DLLs easily.
2022-04-15 00:31:17 +02:00
Pieter-Jan Briers
1327d6bf25 Replace ManagedHttpListener submodule in favor of NuGet package.
It's SpaceWizards.HttpListener now!
2022-04-14 23:53:57 +02:00
Pieter-Jan Briers
237e37ff30 Update Lidgren submodule 2022-04-14 23:47:17 +02:00
Pieter-Jan Briers
4d707c86cb Version: 0.12.1.0 2022-04-14 17:17:17 +02:00
Pieter-Jan Briers
c7027c6e00 ACZ manifest delta downloads (#2698) 2022-04-14 17:15:54 +02:00
Paul Ritter
81ec61bcc8 overflowqueue (#2721)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2022-04-14 10:57:28 +02:00
DrSmugleaf
649178fa54 Rename engine prototypes folder to EnginePrototypes (#2723) 2022-04-14 05:46:29 +02:00
Flipp Syder
9ff46b9aad Adds custom MIDI soundfont support (#2720)
Co-authored-by: Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
2022-04-13 11:59:43 +02:00
ElectroJr
cdcbb60ca7 more comments 2022-04-13 21:46:40 +12:00
ElectroJr
d8cdb2b312 comments 2022-04-13 21:43:47 +12:00
ElectroJr
fa1c1b8e6e better equality 2022-04-13 21:39:48 +12:00
ElectroJr
2ded835602 Make MappingDataNode.Except() not recursive 2022-04-13 21:08:59 +12:00
moonheart08
a43f04818d fix map commands. 2022-04-12 09:48:45 -05:00
moonheart08
278dc60119 use a query for xform. 2022-04-12 09:43:59 -05:00
moonheart08
8a202be0cf naming. 2022-04-12 09:34:18 -05:00
moonheart08
58940a3cd7 bp/map load now take arguments. 2022-04-11 23:54:15 -05:00
moonheart08
aa9721d146 fix tests 2022-04-11 23:15:37 -05:00
Paul
79f114ad5b work 2022-04-12 02:04:41 +02:00
Leon Friedrich
197a6ddd9d Fix sprite bb calculation (#2714) 2022-04-11 18:13:32 +02:00
metalgearsloth
4a4fb15e06 Add xform comp ref to parent change message (#2709) 2022-04-11 17:01:07 +10:00
metalgearsloth
e7e83ce6e8 Optimise fixture init slightly (#2707) 2022-04-11 16:58:48 +10:00
Leon Friedrich
a82b293452 Make RemoveComp not error on missing component (#2689) 2022-04-11 16:57:35 +10:00
Vera Aguilera Puerto
bb483a3a38 Add "midipanic" command. (#2701) 2022-04-11 16:55:12 +10:00
moonheart08
2ca3d0e395 Fix map loading once and for all. 2022-04-11 00:44:28 -05:00
metalgearsloth
bda79b1e82 Cache serialized types (#2706) 2022-04-11 12:50:34 +10:00
metalgearsloth
4f4b754e2d Don't allow velocity to be set where an obj can't collide (#2691) 2022-04-11 12:49:35 +10:00
mirrorcult
214cabac43 Merge pull request #2713 from oappiah/minor_config_update 2022-04-10 13:35:00 -07:00
Appiah
7b6229c222 Corrected the map example 2022-04-10 22:24:21 +02:00
Paul
1a14a75897 nullspace viewer crash fix 2022-04-10 17:52:12 +02:00
metalgearsloth
76b75fd9b3 Remove LINQ from sprite bounds (#2708) 2022-04-10 17:48:45 +10:00
Pieter-Jan Briers
b969fd22f7 Version: 0.12.0.1 2022-04-09 21:02:56 +02:00
Profane McBane
8d27d091af Hotfix - change PVS pools to use MaxVisPoolSize (#2704) 2022-04-09 20:54:41 +02:00
DrSmugleaf
1eb7393a60 Revert "pvsrange vec2 + eyezoom (#2676)" (#2703)
This reverts commit 582d8a5587.
2022-04-09 19:41:11 +02:00
Vera Aguilera Puerto
4cf88507c2 Version: 0.11.0.1 2022-04-09 13:21:38 +02:00
Vera Aguilera Puerto
3565d8b321 Fix synth state getting reset every MIDI player loop. 2022-04-09 13:21:09 +02:00
Vera Aguilera Puerto
7094c29b2e Version: 0.11.0.0 2022-04-08 16:07:42 +02:00
Vera Aguilera Puerto
63004b270f Cleans up and improves MIDI code significantly. (#2666) 2022-04-08 15:58:15 +02:00
metalgearsloth
6714a99b38 EntityLookup anchor flag test (#2699) 2022-04-08 13:36:24 +10:00
metalgearsloth
4c3b8df1e7 Fix anchor query (#2697) 2022-04-08 09:56:30 +10:00
metalgearsloth
4bb695121f Version: 0.10.0.0 2022-04-06 19:34:47 +10:00
metalgearsloth
09fd47c421 Don't store contained entities on entitylookup (#2662) 2022-04-06 19:31:34 +10:00
vulppine
d201d9c688 adds color sliders, fixes issues with RGB->HSL/V 2022-04-05 12:21:59 -07:00
Paul
fd1e25c584 Version: 0.9.3.2 2022-04-05 18:51:03 +02:00
Paul Ritter
6bb66ae70e fixes loc (#2685) 2022-04-05 18:50:27 +02:00
Paul
cc82d6b1d9 Version: 0.9.3.1 2022-04-05 18:36:19 +02:00
Paul
956be749b6 fix prototype reload 2022-04-05 18:36:09 +02:00
Paul Ritter
6585a00608 readds expandpvsevent (#2684) 2022-04-05 19:47:14 +10:00
metalgearsloth
c0525f710f Sprite subscription shutdown (#2688) 2022-04-05 19:45:23 +10:00
Vera Aguilera Puerto
d3672807d2 Improves SpriteSystem.GetPrototypeIcon method significantly. (#2680) 2022-04-05 16:36:16 +10:00
ElectroJr
60f18d5f36 Version: 0.9.3.0 2022-04-05 18:06:43 +12:00
mirrorcult
e72d3de256 Local event for BUI opening (#2687) 2022-04-05 15:52:02 +10:00
Moony
ba9846b9c4 Fix vector2 CVars (#2686) 2022-04-05 15:03:10 +10:00
Paul
09586284dc Version: 0.9.2.0 2022-04-04 20:28:53 +02:00
Paul
a1ee4374b2 add a check for major version == 0 2022-04-04 20:28:26 +02:00
Paul Ritter
4de6f25f11 updates version-script to use the new version format (#2683) 2022-04-04 20:25:12 +02:00
Paul Ritter
582d8a5587 pvsrange vec2 + eyezoom (#2676)
Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
2022-04-04 20:20:13 +02:00
Leon Friedrich
ec53b04f99 Add NotNullWhen attribute to TryComp (#2681) 2022-04-04 11:29:02 +02:00
metalgearsloth
950fc94408 Physicsmap tweaks (#2663) 2022-04-04 17:10:15 +10:00
Leon Friedrich
58d12e6e09 Remove Component.OnAdd() (#2660) 2022-04-04 16:11:47 +10:00
ElectroJr
94323005c4 Version: 0.9.1 2022-04-04 17:43:01 +12:00
metalgearsloth
4989842057 Remove IgnorePause (#2649) 2022-04-04 15:41:38 +10:00
Paul
80172636a8 version 0.9 - serv3 refactor 2022-04-03 02:00:41 +02:00
Paul Ritter
8491f7be24 New Serv3 api just dropped (#2605)
Co-authored-by: Paul Ritter <ritter.paul1@gmail.com>
2022-04-03 01:59:48 +02:00
407 changed files with 16258 additions and 7269 deletions

View File

@@ -1,11 +1,13 @@
name: Benchmarks
#on:
# push
#schedule:
# - cron: '0 5 * * *'
#push:
# tags:
# - 'v*'
on:
workflow_dispatch:
schedule:
- cron: '0 5 * * *'
push:
tags:
- 'v*'
concurrency: benchmarks
env:
ROBUST_BENCHMARKS_ENABLE_SQL: 1
@@ -20,14 +22,14 @@ jobs:
name: Run Benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Install dependencies
run: dotnet restore
- name: Run benchmark
run: cd Robust.Benchmarks && sudo dotnet run --filter '*' --configuration Release
- name: Run script on centcomm
uses: appleboy/ssh-action@master
with:
host: centcomm.spacestation14.io
username: robust-benchmark-runner
key: ${{ secrets.CENTCOMM_ROBUST_BENCHMARK_RUNNER_KEY }}
command_timeout: 100000m
script: |
wget https://raw.githubusercontent.com/space-wizards/RobustToolbox/${{ github.sha }}/Tools/run_benchmarks.py
python3 run_benchmarks.py "${{ secrets.BENCHMARKS_WRITE_ADDRESS }}" "${{ secrets.BENCHMARKS_WRITE_PORT }}" "${{ secrets.BENCHMARKS_WRITE_USER }}" "${{ secrets.BENCHMARKS_WRITE_PASSWORD }}" "${{ github.sha }}"
rm run_benchmarks.py

3
.gitmodules vendored
View File

@@ -10,9 +10,6 @@
[submodule "Robust.LoaderApi"]
path = Robust.LoaderApi
url = https://github.com/space-wizards/Robust.LoaderApi.git
[submodule "ManagedHttpListener"]
path = ManagedHttpListener
url = https://github.com/space-wizards/ManagedHttpListener.git
[submodule "cefglue"]
path = cefglue
url = https://github.com/space-wizards/cefglue.git

View File

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

View File

@@ -0,0 +1,10 @@
color-selector-sliders-red = R
color-selector-sliders-green = G
color-selector-sliders-blue = B
color-selector-sliders-hue = H
color-selector-sliders-saturation = S
color-selector-sliders-value = V
color-selector-sliders-alpha = A
color-selector-sliders-rgb = RGB
color-selector-sliders-hsv = HSV

View File

@@ -0,0 +1 @@
midi-panic-command-description = Turns off every note for every active MIDI renderer.

View File

@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Mathematics;
@@ -11,6 +12,9 @@ using BenchmarkDotNet.Reports;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Npgsql;
using Npgsql.Internal;
using Npgsql.Internal.TypeHandlers;
using Npgsql.Internal.TypeHandling;
namespace Robust.Benchmarks.Exporters;
@@ -68,6 +72,13 @@ public sealed class SQLExporter : IExporter
using var ctx = new BenchmarkContext(builder.Options);
try
{
ctx.Database.OpenConnection();
var con = (NpgsqlConnection) ctx.Database.GetDbConnection();
con.TypeMapper.AddTypeResolverFactory(new JsonOverrideTypeHandlerResolverFactory(new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
}));
ctx.Database.Migrate();
ctx.BenchmarkRuns.Add(BenchmarkRun.FromSummary(summary, gitHash));
ctx.SaveChanges();
@@ -81,6 +92,45 @@ public sealed class SQLExporter : IExporter
public string Name => "sql";
}
// https://github.com/npgsql/efcore.pg/issues/1107#issuecomment-945126627
class JsonOverrideTypeHandlerResolverFactory : TypeHandlerResolverFactory
{
private readonly JsonSerializerOptions _options;
public JsonOverrideTypeHandlerResolverFactory(JsonSerializerOptions options)
=> _options = options;
public override TypeHandlerResolver Create(NpgsqlConnector connector)
=> new JsonOverrideTypeHandlerResolver(connector, _options);
public override string? GetDataTypeNameByClrType(Type clrType)
=> null;
public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName)
=> null;
class JsonOverrideTypeHandlerResolver : TypeHandlerResolver
{
readonly JsonHandler _jsonbHandler;
internal JsonOverrideTypeHandlerResolver(NpgsqlConnector connector, JsonSerializerOptions options)
=> _jsonbHandler ??= new JsonHandler(
connector.DatabaseInfo.GetPostgresTypeByName("jsonb"),
connector.TextEncoding,
isJsonb: true,
options);
public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName)
=> typeName == "jsonb" ? _jsonbHandler : null;
public override NpgsqlTypeHandler? ResolveByClrType(Type type)
// You can add any user-defined CLR types which you want mapped to jsonb
=> type == typeof(JsonDocument) ? _jsonbHandler : null;
public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName)
=> null; // Let the built-in resolver do this
}
}
public sealed class DesignTimeContextFactoryPostgres : IDesignTimeDbContextFactory<BenchmarkContext>
{
public BenchmarkContext CreateDbContext(string[] args)
@@ -101,12 +151,14 @@ public class BenchmarkContext : DbContext
public class BenchmarkRun
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public ulong Id { get; set; }
public int Id { get; set; }
public string GitHash { get; set; } = string.Empty;
[Column(TypeName = "timestamptz")]
public DateTime RunDate { get; set; }
public string Name { get; set; } = string.Empty;
[Column(TypeName = "jsonb")]
public BenchmarkRunReport[] Reports { get; set; } = Array.Empty<BenchmarkRunReport>();

View File

@@ -0,0 +1,57 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Robust.Benchmarks.Exporters;
#nullable disable
namespace Robust.Benchmarks.Migrations
{
[DbContext(typeof(BenchmarkContext))]
[Migration("20220510131430_fix-pk")]
partial class fixpk
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Robust.Benchmarks.Exporters.BenchmarkRun", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("GitHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<BenchmarkRunReport[]>("Reports")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTime>("RunDate")
.HasColumnType("timestamptz");
b.HasKey("Id");
b.ToTable("BenchmarkRuns");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Robust.Benchmarks.Migrations
{
public partial class fixpk : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "RunDate",
table: "BenchmarkRuns",
type: "timestamptz",
nullable: false,
oldClrType: typeof(DateTime),
oldType: "Date");
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "BenchmarkRuns",
type: "integer",
nullable: false,
oldClrType: typeof(decimal),
oldType: "numeric(20,0)")
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "RunDate",
table: "BenchmarkRuns",
type: "Date",
nullable: false,
oldClrType: typeof(DateTime),
oldType: "timestamptz");
migrationBuilder.AlterColumn<decimal>(
name: "Id",
table: "BenchmarkRuns",
type: "numeric(20,0)",
nullable: false,
oldClrType: typeof(int),
oldType: "integer")
.OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
}
}
}

View File

@@ -24,9 +24,11 @@ namespace Robust.Benchmarks.Migrations
modelBuilder.Entity("Robust.Benchmarks.Exporters.BenchmarkRun", b =>
{
b.Property<decimal>("Id")
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(20,0)");
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("GitHash")
.IsRequired()
@@ -41,7 +43,7 @@ namespace Robust.Benchmarks.Migrations
.HasColumnType("jsonb");
b.Property<DateTime>("RunDate")
.HasColumnType("Date");
.HasColumnType("timestamptz");
b.HasKey("Id");

View File

@@ -6,6 +6,7 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>../bin/Benchmarks</OutputPath>
<OutputType>Exe</OutputType>
<NoWarn>RA0003</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Robust.Server\Robust.Server.csproj" />
@@ -18,7 +19,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.0-rc.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
</ItemGroup>
<Import Project="..\MSBuild\Robust.Engine.targets" />
</Project>

View File

@@ -1,7 +1,6 @@
using System.Globalization;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
@@ -19,10 +18,10 @@ namespace Robust.Benchmarks.Serialization
: new ErrorNode(node, $"Failed parsing int value: {node.Value}");
}
public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node,
IDependencyCollection dependencies, bool skipHook, ISerializationContext? context = null)
public int Read(ISerializationManager serializationManager, ValueDataNode node,
IDependencyCollection dependencies, bool skipHook, ISerializationContext? context = null, int _ = default)
{
return new DeserializedValue<int>(int.Parse(node.Value, CultureInfo.InvariantCulture));
return int.Parse(node.Value, CultureInfo.InvariantCulture);
}
public DataNode Write(ISerializationManager serializationManager, int value, bool alwaysWrite = false,

View File

@@ -28,7 +28,7 @@ namespace Robust.Benchmarks.Serialization.Copy
var seedMapping = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
Seed = SerializationManager.ReadValueOrThrow<SeedDataDefinition>(seedMapping);
Seed = SerializationManager.Read<SeedDataDefinition>(seedMapping);
}
private const string String = "ABC";

View File

@@ -19,6 +19,7 @@ namespace Robust.Benchmarks.Serialization.Definitions
name: tobacco
seedName: tobacco
displayName: tobacco plant
plantRsi: Objects/Specific/Hydroponics/tobacco.rsi
productPrototypes:
- LeavesTobacco
harvestRepeat: Repeat
@@ -36,7 +37,7 @@ namespace Robust.Benchmarks.Serialization.Definitions
Max: 10
PotencyDivisor: 10";
[DataField("id", required: true)] public string ID { get; set; } = default!;
[IdDataFieldAttribute] public string ID { get; set; } = default!;
#region Tracking
[DataField("name")] public string Name { get; set; } = string.Empty;

View File

@@ -2,7 +2,6 @@
using BenchmarkDotNet.Attributes;
using Robust.Benchmarks.Serialization.Definitions;
using Robust.Shared.Analyzers;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
@@ -42,32 +41,32 @@ namespace Robust.Benchmarks.Serialization.Read
private ValueDataNode FlagThirtyOne { get; } = new("ThirtyOne");
[Benchmark]
public string? ReadString()
public string ReadString()
{
return SerializationManager.ReadValue<string>(StringNode);
return SerializationManager.Read<string>(StringNode);
}
[Benchmark]
public int? ReadInteger()
public int ReadInteger()
{
return SerializationManager.ReadValue<int>(IntNode);
return SerializationManager.Read<int>(IntNode);
}
[Benchmark]
public DataDefinitionWithString? ReadDataDefinitionWithString()
public DataDefinitionWithString ReadDataDefinitionWithString()
{
return SerializationManager.ReadValue<DataDefinitionWithString>(StringDataDefNode);
return SerializationManager.Read<DataDefinitionWithString>(StringDataDefNode);
}
[Benchmark]
public SeedDataDefinition? ReadSeedDataDefinition()
public SeedDataDefinition ReadSeedDataDefinition()
{
return SerializationManager.ReadValue<SeedDataDefinition>(SeedNode);
return SerializationManager.Read<SeedDataDefinition>(SeedNode);
}
[Benchmark]
[BenchmarkCategory("flag")]
public DeserializationResult ReadFlagZero()
public object? ReadFlagZero()
{
return SerializationManager.ReadWithTypeSerializer(
typeof(int),
@@ -77,7 +76,7 @@ namespace Robust.Benchmarks.Serialization.Read
[Benchmark]
[BenchmarkCategory("flag")]
public DeserializationResult ReadThirtyOne()
public object? ReadThirtyOne()
{
return SerializationManager.ReadWithTypeSerializer(
typeof(int),
@@ -87,7 +86,7 @@ namespace Robust.Benchmarks.Serialization.Read
[Benchmark]
[BenchmarkCategory("customTypeSerializer")]
public DeserializationResult ReadIntegerCustomSerializer()
public object? ReadIntegerCustomSerializer()
{
return SerializationManager.ReadWithTypeSerializer(
typeof(int),

View File

@@ -46,84 +46,84 @@ namespace Robust.Benchmarks.Serialization
[BenchmarkCategory("read")]
public string[]? ReadEmptyString()
{
return SerializationManager.ReadValue<string[]>(EmptyNode);
return SerializationManager.Read<string[]>(EmptyNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public string[]? ReadOneString()
{
return SerializationManager.ReadValue<string[]>(OneIntNode);
return SerializationManager.Read<string[]>(OneIntNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public string[]? ReadTenStrings()
{
return SerializationManager.ReadValue<string[]>(TenIntsNode);
return SerializationManager.Read<string[]>(TenIntsNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public int[]? ReadEmptyInt()
{
return SerializationManager.ReadValue<int[]>(EmptyNode);
return SerializationManager.Read<int[]>(EmptyNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public int[]? ReadOneInt()
{
return SerializationManager.ReadValue<int[]>(OneIntNode);
return SerializationManager.Read<int[]>(OneIntNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public int[]? ReadTenInts()
{
return SerializationManager.ReadValue<int[]>(TenIntsNode);
return SerializationManager.Read<int[]>(TenIntsNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public DataDefinitionWithString[]? ReadEmptyStringDataDef()
{
return SerializationManager.ReadValue<DataDefinitionWithString[]>(EmptyNode);
return SerializationManager.Read<DataDefinitionWithString[]>(EmptyNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public DataDefinitionWithString[]? ReadOneStringDataDef()
{
return SerializationManager.ReadValue<DataDefinitionWithString[]>(OneStringDefNode);
return SerializationManager.Read<DataDefinitionWithString[]>(OneStringDefNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public DataDefinitionWithString[]? ReadTenStringDataDefs()
{
return SerializationManager.ReadValue<DataDefinitionWithString[]>(TenStringDefsNode);
return SerializationManager.Read<DataDefinitionWithString[]>(TenStringDefsNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public SealedDataDefinitionWithString[]? ReadEmptySealedStringDataDef()
{
return SerializationManager.ReadValue<SealedDataDefinitionWithString[]>(EmptyNode);
return SerializationManager.Read<SealedDataDefinitionWithString[]>(EmptyNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public SealedDataDefinitionWithString[]? ReadOneSealedStringDataDef()
{
return SerializationManager.ReadValue<SealedDataDefinitionWithString[]>(OneStringDefNode);
return SerializationManager.Read<SealedDataDefinitionWithString[]>(OneStringDefNode);
}
[Benchmark]
[BenchmarkCategory("read")]
public SealedDataDefinitionWithString[]? ReadTenSealedStringDataDefs()
{
return SerializationManager.ReadValue<SealedDataDefinitionWithString[]>(TenStringDefsNode);
return SerializationManager.Read<SealedDataDefinitionWithString[]>(TenStringDefsNode);
}
}
}

View File

@@ -28,7 +28,7 @@ namespace Robust.Benchmarks.Serialization.Write
var seedMapping = yamlStream.Documents[0].RootNode.ToDataNodeCast<SequenceDataNode>().Cast<MappingDataNode>(0);
Seed = SerializationManager.ReadValueOrThrow<SeedDataDefinition>(seedMapping);
Seed = SerializationManager.Read<SeedDataDefinition>(seedMapping);
}
private const string String = "ABC";

View File

@@ -0,0 +1,22 @@
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Robust.Client.Audio.Midi.Commands;
public sealed class MidiPanicCommand : IConsoleCommand
{
[Dependency] private readonly IMidiManager _midiManager = default!;
public string Command => "midipanic";
public string Description => Loc.GetString("midi-panic-command-description");
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
foreach (var renderer in _midiManager.Renderers)
{
renderer.StopAllNotes();
}
}
}

View File

@@ -0,0 +1,62 @@
using System.Collections.Generic;
using NFluidsynth;
using Robust.Shared.Audio.Midi;
namespace Robust.Client.Audio.Midi;
public interface IMidiManager
{
/// <summary>
/// A read-only list of all existing MIDI Renderers.
/// </summary>
IReadOnlyList<IMidiRenderer> Renderers { get; }
/// <summary>
/// If true, MIDI support is available.
/// </summary>
bool IsAvailable { get; }
/// <summary>
/// Volume, in db.
/// </summary>
float Volume { get; set; }
public int OcclusionCollisionMask { get; set; }
/// <summary>
/// This method tries to return a midi renderer ready to be used.
/// You only need to set the <see cref="IMidiRenderer.MidiProgram"/> afterwards.
/// </summary>
/// <remarks>
/// This method can fail if MIDI support is not available.
/// </remarks>
/// <returns>
/// <c>null</c> if MIDI support is not available.
/// </returns>
IMidiRenderer? GetNewRenderer(bool mono = true);
/// <summary>
/// Creates a <see cref="RobustMidiEvent"/> given a <see cref="MidiEvent"/> and a sequencer tick.
/// </summary>
RobustMidiEvent FromFluidEvent(MidiEvent midiEvent, uint tick);
/// <summary>
/// Creates a <see cref="SequencerEvent"/> given a <see cref="RobustMidiEvent"/>.
/// Be sure to dispose of the result after you've used it.
/// </summary>
SequencerEvent ToSequencerEvent(RobustMidiEvent midiEvent);
/// <summary>
/// Creates a <see cref="RobustMidiEvent"/> given a <see cref="SequencerEvent"/> and a sequencer tick.
/// </summary>
RobustMidiEvent FromSequencerEvent(SequencerEvent midiEvent, uint tick);
/// <summary>
/// Method called every frame.
/// Should be used to update positional audio.
/// </summary>
/// <param name="frameTime"></param>
void FrameUpdate(float frameTime);
void Shutdown();
}

View File

@@ -0,0 +1,174 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.Audio.Midi;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Robust.Client.Audio.Midi;
public enum MidiRendererStatus : byte
{
None,
Input,
File,
}
public interface IMidiRenderer : IDisposable
{
/// <summary>
/// The buffered audio source of this renderer.
/// </summary>
internal IClydeBufferedAudioSource Source { get; }
/// <summary>
/// Whether this renderer has been disposed or not.
/// </summary>
bool Disposed { get; }
/// <summary>
/// This controls whether the midi file being played will loop or not.
/// </summary>
bool LoopMidi { get; set; }
/// <summary>
/// This increases all note on velocities to 127.
/// </summary>
bool VolumeBoost { get; set; }
/// <summary>
/// The midi program (instrument) the renderer is using.
/// </summary>
byte MidiProgram { get; set; }
/// <summary>
/// The instrument bank the renderer is using.
/// </summary>
byte MidiBank { get; set; }
/// <summary>
/// The soundfont currently selected by the renderer.
/// </summary>
uint MidiSoundfont { get; set; }
/// <summary>
/// The current status of the renderer.
/// "None" if the renderer isn't playing from input or a midi file.
/// "Input" if the renderer is playing from midi input.
/// "File" if the renderer is playing from a midi file.
/// </summary>
MidiRendererStatus Status { get; }
/// <summary>
/// Whether the sound will play in stereo or mono.
/// </summary>
bool Mono { get; set; }
/// <summary>
/// Whether to drop messages on the percussion channel.
/// </summary>
bool DisablePercussionChannel { get; set; }
/// <summary>
/// Whether to drop messages for program change events.
/// </summary>
bool DisableProgramChangeEvent { get; set; }
/// <summary>
/// Gets the total number of ticks possible for the MIDI player.
/// </summary>
int PlayerTotalTick { get; }
/// <summary>
/// Gets or sets (seeks) the current tick of the MIDI player.
/// </summary>
int PlayerTick { get; set; }
/// <summary>
/// Gets the current tick of the sequencer.
/// </summary>
uint SequencerTick { get; }
/// <summary>
/// Gets the Time Scale of the sequencer in ticks per second. Default is 1000 for 1 tick per millisecond.
/// </summary>
double SequencerTimeScale { get; }
/// <summary>
/// Start listening for midi input.
/// </summary>
bool OpenInput();
/// <summary>
/// Start playing a midi file.
/// </summary>
/// <param name="buffer">Bytes of the midi file</param>
bool OpenMidi(ReadOnlySpan<byte> buffer);
/// <summary>
/// Stops listening for midi input.
/// </summary>
bool CloseInput();
/// <summary>
/// Stops playing midi files.
/// </summary>
bool CloseMidi();
/// <summary>
/// Stops all notes being played currently.
/// </summary>
void StopAllNotes();
/// <summary>
/// Render and play MIDI to the audio source.
/// </summary>
internal void Render();
/// <summary>
/// Loads a new soundfont into the renderer.
/// </summary>
void LoadSoundfont(string filename, bool resetPresets = false);
/// <summary>
/// Invoked whenever a new midi event is registered.
/// </summary>
event Action<RobustMidiEvent> OnMidiEvent;
/// <summary>
/// Invoked when the midi player finishes playing a song.
/// </summary>
event Action OnMidiPlayerFinished;
/// <summary>
/// The entity whose position will be used for positional audio.
/// This is only used if <see cref="Mono"/> is set to True.
/// </summary>
EntityUid? TrackingEntity { get; set; }
/// <summary>
/// The position that will be used for positional audio.
/// This is only used if <see cref="Mono"/> is set to True
/// and <see cref="TrackingEntity"/> is null.
/// </summary>
EntityCoordinates? TrackingCoordinates { get; set; }
/// <summary>
/// Send a midi event for the renderer to play.
/// </summary>
/// <param name="midiEvent">The midi event to be played</param>
void SendMidiEvent(RobustMidiEvent midiEvent);
/// <summary>
/// Schedule a MIDI event to be played at a later time.
/// </summary>
/// <param name="midiEvent">the midi event in question</param>
/// <param name="time"></param>
/// <param name="absolute"></param>
void ScheduleMidiEvent(RobustMidiEvent midiEvent, uint time, bool absolute);
/// <summary>
/// Actually disposes of this renderer. Do NOT use outside the MIDI thread.
/// </summary>
internal void InternalDispose();
}

View File

@@ -0,0 +1,155 @@
using NFluidsynth;
using Robust.Shared.Audio.Midi;
namespace Robust.Client.Audio.Midi;
internal sealed partial class MidiManager
{
public RobustMidiEvent FromFluidEvent(MidiEvent midiEvent, uint tick)
{
var status = RobustMidiEvent.MakeStatus((byte) midiEvent.Channel, (byte) midiEvent.Type);
// Control is always the first data byte. Value is always the second data byte. Fluidsynth's API ain't great.
var data1 = (byte) midiEvent.Control;
var data2 = (byte) midiEvent.Value;
// PitchBend is handled specially.
if (midiEvent.Type == (int) RobustMidiCommand.PitchBend)
{
// We pack pitch into both data values.
var pitch = (ushort) midiEvent.Pitch;
data1 = (byte) pitch;
data2 = (byte) (pitch >> 8);
}
return new RobustMidiEvent(status, data1, data2, tick);
}
public SequencerEvent ToSequencerEvent(RobustMidiEvent midiEvent)
{
var sequencerEvent = new SequencerEvent();
switch (midiEvent.MidiCommand)
{
case RobustMidiCommand.NoteOff:
sequencerEvent.NoteOff(midiEvent.Channel, midiEvent.Key);
break;
case RobustMidiCommand.NoteOn:
sequencerEvent.NoteOn(midiEvent.Channel, midiEvent.Key, midiEvent.Velocity);
break;
case RobustMidiCommand.AfterTouch:
sequencerEvent.KeyPressure(midiEvent.Channel, midiEvent.Key, midiEvent.Value);
break;
case RobustMidiCommand.ControlChange:
sequencerEvent.ControlChange(midiEvent.Channel, midiEvent.Control, midiEvent.Value);
break;
case RobustMidiCommand.ProgramChange:
sequencerEvent.ProgramChange(midiEvent.Channel, midiEvent.Program);
break;
case RobustMidiCommand.ChannelPressure:
sequencerEvent.ChannelPressure(midiEvent.Channel, midiEvent.Value);
break;
case RobustMidiCommand.PitchBend:
sequencerEvent.PitchBend(midiEvent.Channel, midiEvent.Pitch);
break;
case RobustMidiCommand.SystemMessage:
switch (midiEvent.Control)
{
case 0x0 when midiEvent.Status == 0xFF:
sequencerEvent.SystemReset();
break;
case 0x0B:
sequencerEvent.AllNotesOff(midiEvent.Channel);
break;
default:
_midiSawmill.Warning($"Tried to convert unsupported event to sequencer event:\n{midiEvent}");
break;
}
break;
default:
_midiSawmill.Warning($"Tried to convert unsupported event to sequencer event:\n{midiEvent}");
break;
}
return sequencerEvent;
}
public RobustMidiEvent FromSequencerEvent(SequencerEvent midiEvent, uint tick)
{
byte channel = (byte) midiEvent.Channel;
RobustMidiCommand command = 0x0;
byte data1 = 0;
byte data2 = 0;
switch (midiEvent.Type)
{
case FluidSequencerEventType.NoteOn:
command = RobustMidiCommand.NoteOn;
data1 = (byte) midiEvent.Key;
data2 = (byte) midiEvent.Velocity;
break;
case FluidSequencerEventType.NoteOff:
command = RobustMidiCommand.NoteOff;
data1 = (byte) midiEvent.Key;
break;
case FluidSequencerEventType.PitchBend:
command = RobustMidiCommand.PitchBend;
// We pack pitch into both data values
var pitch = (ushort) midiEvent.Pitch;
data1 = (byte) pitch;
data2 = (byte) (pitch >> 8);
break;
case FluidSequencerEventType.ProgramChange:
command = RobustMidiCommand.ProgramChange;
data1 = (byte) midiEvent.Program;
break;
case FluidSequencerEventType.KeyPressure:
command = RobustMidiCommand.AfterTouch;
data1 = (byte) midiEvent.Key;
data2 = (byte) midiEvent.Value;
break;
case FluidSequencerEventType.ControlChange:
command = RobustMidiCommand.ControlChange;
data1 = (byte) midiEvent.Control;
data2 = (byte) midiEvent.Value;
break;
case FluidSequencerEventType.ChannelPressure:
command = RobustMidiCommand.ChannelPressure;
data1 = (byte) midiEvent.Value;
break;
case FluidSequencerEventType.AllNotesOff:
command = RobustMidiCommand.SystemMessage;
data1 = 0x0B;
break;
case FluidSequencerEventType.SystemReset:
command = RobustMidiCommand.SystemMessage;
channel = 0x0F;
break;
default:
_midiSawmill.Error($"Unsupported Sequencer Event: {tick:D8}: {SequencerEventToString(midiEvent)}");
break;
}
return new RobustMidiEvent(RobustMidiEvent.MakeStatus(channel, (byte)command), data1, data2, tick);
}
}

View File

@@ -7,6 +7,7 @@ using NFluidsynth;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
@@ -15,404 +16,435 @@ 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;
using Robust.Shared.ViewVariables;
using Logger = Robust.Shared.Log.Logger;
namespace Robust.Client.Audio.Midi
namespace Robust.Client.Audio.Midi;
internal sealed partial class MidiManager : IMidiManager
{
public interface IMidiManager
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _cfgMan = default!;
[Dependency] private readonly IClydeAudio _clydeAudio = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly ILogManager _logger = default!;
private SharedPhysicsSystem _broadPhaseSystem = default!;
public IReadOnlyList<IMidiRenderer> Renderers
{
/// <summary>
/// This method tries to return a midi renderer ready to be used.
/// You only need to set the <see cref="IMidiRenderer.MidiProgram"/> afterwards.
/// </summary>
/// <remarks>
/// This method can fail if MIDI support is not available.
/// </remarks>
/// <returns>
/// <c>null</c> if MIDI support is not available.
/// </returns>
IMidiRenderer? GetNewRenderer();
/// <summary>
/// Method called every frame.
/// Should be used to update positional audio.
/// </summary>
/// <param name="frameTime"></param>
void FrameUpdate(float frameTime);
/// <summary>
/// Volume, in db.
/// </summary>
float Volume { get; set; }
/// <summary>
/// If true, MIDI support is available.
/// </summary>
bool IsAvailable { get; }
public int OcclusionCollisionMask { get; set; }
void Shutdown();
get
{
lock (_renderers)
{
// Perform a copy. Sadly, we can't return a reference to the original list due to threading concerns.
return _renderers.ToArray();
}
}
}
internal sealed class MidiManager : IMidiManager
[ViewVariables]
public bool IsAvailable
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _cfgMan = default!;
private SharedPhysicsSystem _broadPhaseSystem = default!;
[ViewVariables]
public bool IsAvailable
get
{
get
{
InitializeFluidsynth();
InitializeFluidsynth();
return FluidsynthInitialized;
}
return FluidsynthInitialized;
}
}
[ViewVariables]
private readonly List<IMidiRenderer> _renderers = new();
private bool _alive = true;
private Settings? _settings;
private Thread? _midiThread;
private ISawmill _midiSawmill = default!;
private float _volume = 0f;
private bool _volumeDirty = true;
// Not reliable until Fluidsynth is initialized!
[ViewVariables(VVAccess.ReadWrite)]
public float Volume
{
get => _volume;
set
{
if (MathHelper.CloseToPercent(_volume, value))
return;
_cfgMan.SetCVar(CVars.MidiVolume, value);
_volumeDirty = true;
}
}
private static readonly string[] LinuxSoundfonts =
{
"/usr/share/soundfonts/default.sf2",
"/usr/share/soundfonts/FluidR3_GM.sf2",
"/usr/share/soundfonts/freepats-general-midi.sf2",
"/usr/share/sounds/sf2/default.sf2",
"/usr/share/sounds/sf2/FluidR3_GM.sf2",
"/usr/share/sounds/sf2/TimGM6mb.sf2",
};
private const string WindowsSoundfont = @"C:\WINDOWS\system32\drivers\gm.dls";
private const string OsxSoundfont =
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
private const string FallbackSoundfont = "/Midi/fallback.sf2";
private static ResourcePath CustomSoundfontDirectory = new ResourcePath("/soundfonts/");
private readonly ResourceLoaderCallbacks _soundfontLoaderCallbacks = new();
private bool FluidsynthInitialized;
private bool _failedInitialize;
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
private ISawmill _sawmill = default!;
[ViewVariables(VVAccess.ReadWrite)]
public int OcclusionCollisionMask { get; set; }
private void InitializeFluidsynth()
{
if (FluidsynthInitialized || _failedInitialize) return;
_volume = _cfgMan.GetCVar(CVars.MidiVolume);
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
{
_volume = value;
_volumeDirty = true;
}, true);
_midiSawmill = _logger.GetSawmill("midi");
_midiSawmill.Level = LogLevel.Info;
_sawmill = _logger.GetSawmill("midi.fluidsynth");
_loggerDelegate = LoggerDelegate;
if (!_resourceManager.UserData.Exists(CustomSoundfontDirectory))
{
_resourceManager.UserData.CreateDir(CustomSoundfontDirectory);
}
// not a directory, preserve the old file and create an actual directory
else if (!_resourceManager.UserData.IsDir(CustomSoundfontDirectory))
{
_resourceManager.UserData.Rename(CustomSoundfontDirectory, CustomSoundfontDirectory.WithName(CustomSoundfontDirectory.Filename + ".old"));
_resourceManager.UserData.CreateDir(CustomSoundfontDirectory);
}
[ViewVariables]
private readonly List<IMidiRenderer> _renderers = new();
private bool _alive = true;
private Settings? _settings;
private Thread? _midiThread;
private ISawmill _midiSawmill = default!;
private float _volume = 0f;
private bool _volumeDirty = true;
// Not reliable until Fluidsynth is initialized!
[ViewVariables(VVAccess.ReadWrite)]
public float Volume
try
{
get => _volume;
set
{
if (MathHelper.CloseToPercent(_volume, value))
return;
NFluidsynth.Logger.SetLoggerMethod(_loggerDelegate); // Will cause a safe DllNotFoundException if not available.
_cfgMan.SetCVar(CVars.MidiVolume, value);
_volumeDirty = true;
}
_settings = new Settings();
_settings["synth.sample-rate"].DoubleValue = 44100;
_settings["player.timing-source"].StringValue = "sample";
_settings["synth.lock-memory"].IntValue = 0;
_settings["synth.threadsafe-api"].IntValue = 1;
_settings["synth.gain"].DoubleValue = 1.0d;
_settings["synth.polyphony"].IntValue = 1024;
_settings["synth.cpu-cores"].IntValue = 2;
_settings["synth.midi-channels"].IntValue = 16;
_settings["synth.overflow.age"].DoubleValue = 3000;
_settings["audio.driver"].StringValue = "file";
_settings["audio.periods"].IntValue = 8;
_settings["audio.period-size"].IntValue = 4096;
_settings["midi.autoconnect"].IntValue = 1;
_settings["player.reset-synth"].IntValue = 0;
_settings["synth.midi-bank-select"].StringValue = "gm";
//_settings["synth.verbose"].IntValue = 1; // Useful for debugging.
}
catch (Exception e)
{
_midiSawmill.Warning(
"Failed to initialize fluidsynth due to exception, disabling MIDI support:\n{0}", e);
_failedInitialize = true;
return;
}
private static readonly string[] LinuxSoundfonts =
{
"/usr/share/soundfonts/default.sf2",
"/usr/share/soundfonts/FluidR3_GM.sf2",
"/usr/share/soundfonts/freepats-general-midi.sf2",
"/usr/share/sounds/sf2/default.sf2",
"/usr/share/sounds/sf2/FluidR3_GM.sf2",
"/usr/share/sounds/sf2/TimGM6mb.sf2",
_midiThread = new Thread(ThreadUpdate);
_midiThread.Start();
_broadPhaseSystem = EntitySystem.Get<SharedPhysicsSystem>();
FluidsynthInitialized = true;
}
private void LoggerDelegate(NFluidsynth.Logger.LogLevel level, string message, IntPtr data)
{
var rLevel = level switch {
NFluidsynth.Logger.LogLevel.Panic => LogLevel.Error,
NFluidsynth.Logger.LogLevel.Error => LogLevel.Error,
NFluidsynth.Logger.LogLevel.Warning => LogLevel.Warning,
NFluidsynth.Logger.LogLevel.Information => LogLevel.Info,
NFluidsynth.Logger.LogLevel.Debug => LogLevel.Debug,
_ => LogLevel.Debug
};
_sawmill.Log(rLevel, message);
}
private const string WindowsSoundfont = @"C:\WINDOWS\system32\drivers\gm.dls";
private const string OsxSoundfont =
"/System/Library/Components/CoreAudio.component/Contents/Resources/gs_instruments.dls";
private const string FallbackSoundfont = "/Midi/fallback.sf2";
private readonly ResourceLoaderCallbacks _soundfontLoaderCallbacks = new();
private bool FluidsynthInitialized;
private bool _failedInitialize;
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
private ISawmill _sawmill = default!;
[ViewVariables(VVAccess.ReadWrite)]
public int OcclusionCollisionMask { get; set; }
private void InitializeFluidsynth()
public IMidiRenderer? GetNewRenderer(bool mono = true)
{
if (!FluidsynthInitialized)
{
if (FluidsynthInitialized || _failedInitialize) return;
InitializeFluidsynth();
_volume = _cfgMan.GetCVar(CVars.MidiVolume);
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
if (!FluidsynthInitialized) // init failed
{
_volume = value;
_volumeDirty = true;
}, true);
_midiSawmill = Logger.GetSawmill("midi");
_sawmill = Logger.GetSawmill("midi.fluidsynth");
_loggerDelegate = LoggerDelegate;
try
{
NFluidsynth.Logger.SetLoggerMethod(_loggerDelegate); // Will cause a safe DllNotFoundException if not available.
_settings = new Settings();
_settings["synth.sample-rate"].DoubleValue = 44100;
_settings["player.timing-source"].StringValue = "sample";
_settings["synth.lock-memory"].IntValue = 0;
_settings["synth.threadsafe-api"].IntValue = 1;
_settings["synth.gain"].DoubleValue = 1.0d;
_settings["synth.polyphony"].IntValue = 1024;
_settings["synth.cpu-cores"].IntValue = 2;
_settings["synth.overflow.age"].DoubleValue = 3000;
_settings["audio.driver"].StringValue = "file";
_settings["audio.periods"].IntValue = 8;
_settings["audio.period-size"].IntValue = 4096;
_settings["midi.autoconnect"].IntValue = 1;
_settings["player.reset-synth"].IntValue = 0;
_settings["synth.midi-bank-select"].StringValue = "gm";
}
catch (Exception e)
{
Logger.WarningS("midi",
"Failed to initialize fluidsynth due to exception, disabling MIDI support:\n{0}", e);
_failedInitialize = true;
return;
}
_midiThread = new Thread(ThreadUpdate);
_midiThread.Start();
_broadPhaseSystem = EntitySystem.Get<SharedPhysicsSystem>();
FluidsynthInitialized = true;
}
private void LoggerDelegate(NFluidsynth.Logger.LogLevel level, string message, IntPtr data)
{
var rLevel = level switch {
NFluidsynth.Logger.LogLevel.Panic => LogLevel.Error,
NFluidsynth.Logger.LogLevel.Error => LogLevel.Error,
NFluidsynth.Logger.LogLevel.Warning => LogLevel.Warning,
NFluidsynth.Logger.LogLevel.Information => LogLevel.Info,
NFluidsynth.Logger.LogLevel.Debug => LogLevel.Debug,
_ => LogLevel.Debug
};
_sawmill.Log(rLevel, message);
}
public IMidiRenderer? GetNewRenderer()
{
if (!FluidsynthInitialized)
{
InitializeFluidsynth();
if (!FluidsynthInitialized) // init failed
{
return null;
}
}
var soundfontLoader = SoundFontLoader.NewDefaultSoundFontLoader(_settings);
// Just making double sure these don't get GC'd.
// They shouldn't, MidiRenderer keeps a ref, but making sure...
var handle = GCHandle.Alloc(soundfontLoader);
try
{
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
var renderer = new MidiRenderer(_settings!, soundfontLoader);
foreach (var file in _resourceManager.ContentFindFiles(("/Audio/MidiCustom/")))
{
if (file.Extension != "sf2" && file.Extension != "dls") continue;
renderer.LoadSoundfont(file.ToString());
}
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
renderer.LoadSoundfont(FallbackSoundfont);
if (OperatingSystem.IsLinux())
{
foreach (var filepath in LinuxSoundfonts)
{
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath)) continue;
try
{
renderer.LoadSoundfont(filepath, true);
}
catch (Exception)
{
continue;
}
break;
}
}
else if (OperatingSystem.IsMacOS())
{
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
renderer.LoadSoundfont(OsxSoundfont, true);
}
else if (OperatingSystem.IsWindows())
{
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
renderer.LoadSoundfont(WindowsSoundfont, true);
}
renderer.Source.SetVolume(Volume);
lock (_renderers)
{
_renderers.Add(renderer);
}
return renderer;
}
finally
{
handle.Free();
return null;
}
}
public void FrameUpdate(float frameTime)
var soundfontLoader = SoundFontLoader.NewDefaultSoundFontLoader(_settings);
// Just making double sure these don't get GC'd.
// They shouldn't, MidiRenderer keeps a ref, but making sure...
var handle = GCHandle.Alloc(soundfontLoader);
try
{
if (!FluidsynthInitialized)
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _clydeAudio, _taskManager, _midiSawmill);
foreach (var file in _resourceManager.ContentFindFiles(("/Audio/MidiCustom/")))
{
return;
if (file.Extension != "sf2" && file.Extension != "dls") continue;
renderer.LoadSoundfont(file.ToString());
}
// Update positions of streams every frame.
foreach (var renderer in _renderers)
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
renderer.LoadSoundfont(FallbackSoundfont);
if (OperatingSystem.IsLinux())
{
if (renderer.Disposed)
continue;
if(_volumeDirty)
renderer.Source.SetVolume(Volume);
if (!renderer.Mono)
foreach (var filepath in LinuxSoundfonts)
{
renderer.Source.SetGlobal();
continue;
}
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath)) continue;
MapCoordinates? mapPos = null;
var trackingEntity = renderer.TrackingEntity != null && !_entityManager.Deleted(renderer.TrackingEntity);
if (trackingEntity)
{
renderer.TrackingCoordinates = _entityManager.GetComponent<TransformComponent>(renderer.TrackingEntity!.Value).Coordinates;
}
if (renderer.TrackingCoordinates != null)
{
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
}
if (mapPos != null && mapPos.Value.MapId == _eyeManager.CurrentMap)
{
var pos = mapPos.Value;
var sourceRelative = _eyeManager.CurrentEye.Position.Position - pos.Position;
var occlusion = 0f;
if (sourceRelative.Length > 0)
try
{
occlusion = _broadPhaseSystem.IntersectRayPenetration(
pos.MapId,
new CollisionRay(
pos.Position,
sourceRelative.Normalized,
OcclusionCollisionMask),
sourceRelative.Length,
renderer.TrackingEntity);
renderer.LoadSoundfont(filepath, true);
}
catch (Exception)
{
continue;
}
renderer.Source.SetOcclusion(occlusion);
if (!renderer.Source.SetPosition(pos.Position))
{
return;
}
if (trackingEntity)
{
renderer.Source.SetVelocity(renderer.TrackingEntity!.Value.GlobalLinearVelocity());
}
}
else
{
renderer.Source.SetOcclusion(float.MaxValue);
break;
}
}
_volumeDirty = false;
}
/// <summary>
/// Main method for the thread rendering the midi audio.
/// </summary>
private void ThreadUpdate()
{
while (_alive)
else if (OperatingSystem.IsMacOS())
{
lock (_renderers)
{
for (var i = 0; i < _renderers.Count; i++)
{
var renderer = _renderers[i];
if (!renderer.Disposed)
renderer.Render();
else
{
renderer.InternalDispose();
_renderers.Remove(renderer);
}
}
}
Thread.Sleep(1);
if (File.Exists(OsxSoundfont) && SoundFont.IsSoundFont(OsxSoundfont))
renderer.LoadSoundfont(OsxSoundfont, true);
}
else if (OperatingSystem.IsWindows())
{
if (File.Exists(WindowsSoundfont) && SoundFont.IsSoundFont(WindowsSoundfont))
renderer.LoadSoundfont(WindowsSoundfont, true);
}
}
public void Shutdown()
{
_alive = false;
_midiThread?.Join();
_settings?.Dispose();
// load every soundfont from the user data directory
_midiSawmill.Debug($"loading soundfonts from {CustomSoundfontDirectory.ToRelativePath().ToString()}/*");
var enumerator = _resourceManager.UserData.Find($"{CustomSoundfontDirectory.ToRelativePath().ToString()}/*").Item1;
foreach (var soundfont in enumerator)
{
if (soundfont.Extension != "sf2" && soundfont.Extension != "dls") continue;
_midiSawmill.Debug($"loading soundfont {soundfont}");
renderer.LoadSoundfont(soundfont.ToString());
}
renderer.Source.SetVolume(Volume);
lock (_renderers)
{
foreach (var renderer in _renderers)
{
renderer?.Dispose();
}
_renderers.Add(renderer);
}
return renderer;
}
finally
{
handle.Free();
}
}
public void FrameUpdate(float frameTime)
{
if (!FluidsynthInitialized)
{
return;
}
// Update positions of streams every frame.
foreach (var renderer in _renderers)
{
if (renderer.Disposed)
continue;
if(_volumeDirty)
renderer.Source.SetVolume(Volume);
if (!renderer.Mono)
{
renderer.Source.SetGlobal();
continue;
}
if (FluidsynthInitialized && !_failedInitialize)
MapCoordinates? mapPos = null;
var trackingEntity = renderer.TrackingEntity != null && !_entityManager.Deleted(renderer.TrackingEntity);
if (trackingEntity)
{
NFluidsynth.Logger.SetLoggerMethod(null);
renderer.TrackingCoordinates = _entityManager.GetComponent<TransformComponent>(renderer.TrackingEntity!.Value).Coordinates;
}
if (renderer.TrackingCoordinates != null)
{
mapPos = renderer.TrackingCoordinates.Value.ToMap(_entityManager);
}
if (mapPos != null && mapPos.Value.MapId == _eyeManager.CurrentMap)
{
var pos = mapPos.Value;
var sourceRelative = _eyeManager.CurrentEye.Position.Position - pos.Position;
var occlusion = 0f;
if (sourceRelative.Length > 0)
{
occlusion = _broadPhaseSystem.IntersectRayPenetration(
pos.MapId,
new CollisionRay(
pos.Position,
sourceRelative.Normalized,
OcclusionCollisionMask),
sourceRelative.Length,
renderer.TrackingEntity);
}
renderer.Source.SetOcclusion(occlusion);
if (!renderer.Source.SetPosition(pos.Position))
{
return;
}
if (trackingEntity)
{
renderer.Source.SetVelocity(renderer.TrackingEntity!.Value.GlobalLinearVelocity());
}
}
else
{
renderer.Source.SetOcclusion(float.MaxValue);
}
}
/// <summary>
/// This class is used to load soundfonts.
/// </summary>
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
_volumeDirty = false;
}
/// <summary>
/// Main method for the thread rendering the midi audio.
/// </summary>
private void ThreadUpdate()
{
while (_alive)
{
private readonly Dictionary<int, Stream> _openStreams = new();
private int _nextStreamId = 1;
public override IntPtr Open(string filename)
lock (_renderers)
{
if (string.IsNullOrEmpty(filename))
for (var i = 0; i < _renderers.Count; i++)
{
return IntPtr.Zero;
var renderer = _renderers[i];
if (!renderer.Disposed)
renderer.Render();
else
{
renderer.InternalDispose();
_renderers.Remove(renderer);
}
}
}
Stream? stream;
var resourceCache = IoCManager.Resolve<IResourceCache>();
var resourcePath = new ResourcePath(filename);
Thread.Sleep(1);
}
}
if (resourcePath.IsRooted && resourceCache.ContentFileExists(filename))
public void Shutdown()
{
_alive = false;
_midiThread?.Join();
_settings?.Dispose();
lock (_renderers)
{
foreach (var renderer in _renderers)
{
renderer?.Dispose();
}
}
if (FluidsynthInitialized && !_failedInitialize)
{
NFluidsynth.Logger.SetLoggerMethod(null);
}
}
/// <summary>
/// Internal method to get a human-readable representation of a <see cref="SequencerEvent"/>.
/// </summary>
internal static string SequencerEventToString(SequencerEvent midiEvent)
{
// ReSharper disable once UseStringInterpolation
return string.Format(
"{0} chan:{1:D2} key:{2:D5} bank:{3:D2} ctrl:{4:D5} dur:{5:D5} pitch:{6:D5} prog:{7:D3} val:{8:D5} vel:{9:D5}",
midiEvent.Type.ToString().PadLeft(22),
midiEvent.Channel,
midiEvent.Key,
midiEvent.Bank,
midiEvent.Control,
midiEvent.Duration,
midiEvent.Pitch,
midiEvent.Program,
midiEvent.Value,
midiEvent.Velocity);
}
/// <summary>
/// This class is used to load soundfonts.
/// </summary>
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
{
private readonly Dictionary<int, Stream> _openStreams = new();
private int _nextStreamId = 1;
public override IntPtr Open(string filename)
{
if (string.IsNullOrEmpty(filename))
{
return IntPtr.Zero;
}
Stream? stream;
var resourceCache = IoCManager.Resolve<IResourceCache>();
var resourcePath = new ResourcePath(filename);
if (resourcePath.IsRooted)
{
// is it in content?
if (resourceCache.ContentFileExists(filename))
{
if (!resourceCache.TryContentFileRead(filename, out stream))
return IntPtr.Zero;
}
// is it in userdata?
else if (resourceCache.UserData.Exists(resourcePath))
{
stream = resourceCache.UserData.OpenRead(resourcePath);
}
else if (File.Exists(filename))
{
stream = File.OpenRead(filename);
@@ -421,73 +453,81 @@ namespace Robust.Client.Audio.Midi
{
return IntPtr.Zero;
}
var id = _nextStreamId++;
_openStreams.Add(id, stream);
return (IntPtr) id;
}
else if (File.Exists(filename))
{
stream = File.OpenRead(filename);
}
else
{
return IntPtr.Zero;
}
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
{
var length = (int) count;
var span = new Span<byte>(buf.ToPointer(), length);
var stream = _openStreams[(int) sfHandle];
var id = _nextStreamId++;
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
try
_openStreams.Add(id, stream);
return (IntPtr) id;
}
public override unsafe int Read(IntPtr buf, long count, IntPtr sfHandle)
{
var length = (int) count;
var span = new Span<byte>(buf.ToPointer(), length);
var stream = _openStreams[(int) sfHandle];
// Fluidsynth's docs state that this method should leave the buffer unmodified if it fails. (returns -1)
try
{
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
if (count < 1024)
{
// Fluidsynth does a LOT of tiny allocations (frankly, way too much).
if (count < 1024)
{
// ReSharper disable once SuggestVarOrType_Elsewhere
Span<byte> buffer = stackalloc byte[(int)count];
// ReSharper disable once SuggestVarOrType_Elsewhere
Span<byte> buffer = stackalloc byte[(int)count];
stream.ReadExact(buffer);
stream.ReadExact(buffer);
buffer.CopyTo(span);
}
else
{
var buffer = stream.ReadExact(length);
buffer.CopyTo(span);
}
buffer.CopyTo(span);
}
catch (EndOfStreamException)
else
{
return -1;
var buffer = stream.ReadExact(length);
buffer.CopyTo(span);
}
return 0;
}
public override int Seek(IntPtr sfHandle, int offset, SeekOrigin origin)
catch (EndOfStreamException)
{
var stream = _openStreams[(int) sfHandle];
stream.Seek(offset, origin);
return 0;
return -1;
}
public override int Tell(IntPtr sfHandle)
{
var stream = _openStreams[(int) sfHandle];
return 0;
}
return (int) stream.Position;
}
public override int Seek(IntPtr sfHandle, int offset, SeekOrigin origin)
{
var stream = _openStreams[(int) sfHandle];
public override int Close(IntPtr sfHandle)
{
if (!_openStreams.Remove((int) sfHandle, out var stream))
return -1;
stream.Seek(offset, origin);
stream.Dispose();
return 0;
return 0;
}
public override int Tell(IntPtr sfHandle)
{
var stream = _openStreams[(int) sfHandle];
return (int) stream.Position;
}
public override int Close(IntPtr sfHandle)
{
if (!_openStreams.Remove((int) sfHandle, out var stream))
return -1;
stream.Dispose();
return 0;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -74,7 +74,7 @@ namespace Robust.Client
{
if (RunLevel == ClientRunLevel.Connecting)
{
_net.Shutdown("Client mashing that connect button.");
_net.Reset("Client mashing that connect button.");
Reset();
}

View File

@@ -11,6 +11,7 @@ using Robust.Client.Input;
using Robust.Client.Map;
using Robust.Client.Placement;
using Robust.Client.Player;
using Robust.Client.Profiling;
using Robust.Client.Prototypes;
using Robust.Client.Reflection;
using Robust.Client.ResourceManagement;
@@ -75,6 +76,7 @@ namespace Robust.Client
IoCManager.Register<IDiscordRichPresence, DiscordRichPresence>();
IoCManager.Register<IMidiManager, MidiManager>();
IoCManager.Register<IAuthManager, AuthManager>();
IoCManager.Register<ProfViewManager>();
IoCManager.Register<IPhysicsManager, PhysicsManager>();
switch (mode)
{

View File

@@ -134,7 +134,7 @@ namespace Robust.Client.Console
if (!NetManager.IsConnected) // we don't care about session on client
return;
var msg = NetManager.CreateNetMessage<MsgConCmd>();
var msg = new MsgConCmd();
msg.Text = command;
NetManager.ClientSendMessage(msg);
}
@@ -198,7 +198,7 @@ namespace Robust.Client.Console
if (!NetManager.IsConnected)
return;
var msg = NetManager.CreateNetMessage<MsgConCmdReg>();
var msg = new MsgConCmdReg();
NetManager.ClientSendMessage(msg);
_requestedCommands = true;

View File

@@ -8,12 +8,12 @@ namespace Robust.Client.Console.Commands
{
public string Command => "physics";
public string Description => $"Shows a debug physics overlay. The arg supplied specifies the overlay.";
public string Help => $"{Command} <aabbs / contactnormals / contactpoints / joints / shapeinfo / shapes>";
public string Help => $"{Command} <aabbs / com / contactnormals / contactpoints / joints / shapeinfo / shapes>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteLine($"Invalid number of args supplied");
shell.WriteError($"Invalid number of args supplied");
return;
}
@@ -43,7 +43,7 @@ namespace Robust.Client.Console.Commands
system.Flags ^= PhysicsDebugFlags.Shapes;
break;
default:
shell.WriteLine($"{args[0]} is not a recognised overlay");
shell.WriteError($"{args[0]} is not a recognised overlay");
return;
}

View File

@@ -15,7 +15,7 @@ namespace Robust.Client.Console.Commands
// MsgStringTableEntries is registered as NetMessageAccept.Client so the server will immediately deny it.
// And kick us.
var net = IoCManager.Resolve<IClientNetManager>();
var msg = net.CreateNetMessage<MsgStringTableEntries>();
var msg = new MsgStringTableEntries();
msg.Entries = new MsgStringTableEntries.Entry[0];
net.ClientSendMessage(msg);
}

View File

@@ -35,7 +35,7 @@ namespace Robust.Client.Console
RunButton.Disabled = true;
var msg = _client._netManager.CreateNetMessage<MsgScriptEval>();
var msg = new MsgScriptEval();
msg.ScriptSession = _session;
msg.Code = _lastEnteredText = InputBar.Text;
@@ -48,7 +48,7 @@ namespace Robust.Client.Console
protected override void Complete()
{
var msg = _client._netManager.CreateNetMessage<MsgScriptCompletion>();
var msg = new MsgScriptCompletion();
msg.ScriptSession = _session;
msg.Code = InputBar.Text;
msg.Cursor = InputBar.CursorPosition;

View File

@@ -65,7 +65,7 @@ namespace Robust.Client.Console
throw new InvalidOperationException("We do not have scripting permission.");
}
var msg = _netManager.CreateNetMessage<MsgScriptStart>();
var msg = new MsgScriptStart();
msg.ScriptSession = _nextSessionId++;
_netManager.ClientSendMessage(msg);
}
@@ -74,7 +74,7 @@ namespace Robust.Client.Console
{
_activeConsoles.Remove(session);
var msg = _netManager.CreateNetMessage<MsgScriptStop>();
var msg = new MsgScriptStop();
msg.ScriptSession = session;
_netManager.ClientSendMessage(msg);
}

View File

@@ -52,6 +52,7 @@ using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision;
@@ -88,6 +89,7 @@ namespace Robust.Client.Debugging
EntityManager,
IoCManager.Resolve<IEyeManager>(),
IoCManager.Resolve<IInputManager>(),
IoCManager.Resolve<IMapManager>(),
IoCManager.Resolve<IResourceCache>(),
this,
Get<SharedPhysicsSystem>()));
@@ -151,10 +153,12 @@ namespace Robust.Client.Debugging
/// Shows the world point for each contact in the viewport.
/// </summary>
ContactPoints = 1 << 0,
/// <summary>
/// Shows the world normal for each contact in the viewport.
/// </summary>
ContactNormals = 1 << 1,
/// <summary>
/// Shows all physics shapes in the viewport.
/// </summary>
@@ -162,6 +166,10 @@ namespace Robust.Client.Debugging
ShapeInfo = 1 << 3,
Joints = 1 << 4,
AABBs = 1 << 5,
/// <summary>
/// Shows Center of Mass for all bodies in the viewport.
/// </summary>
COM = 1 << 6,
}
@@ -170,6 +178,7 @@ namespace Robust.Client.Debugging
private IEntityManager _entityManager = default!;
private IEyeManager _eyeManager = default!;
private IInputManager _inputManager = default!;
private IMapManager _mapManager = default!;
private DebugPhysicsSystem _debugPhysicsSystem = default!;
private SharedPhysicsSystem _physicsSystem = default!;
@@ -181,11 +190,12 @@ namespace Robust.Client.Debugging
private HashSet<Joint> _drawnJoints = new();
public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IResourceCache cache, DebugPhysicsSystem system, SharedPhysicsSystem physicsSystem)
public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IMapManager mapManager, IResourceCache cache, DebugPhysicsSystem system, SharedPhysicsSystem physicsSystem)
{
_entityManager = entityManager;
_eyeManager = eyeManager;
_inputManager = inputManager;
_mapManager = mapManager;
_debugPhysicsSystem = system;
_physicsSystem = physicsSystem;
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
@@ -240,26 +250,21 @@ namespace Robust.Client.Debugging
if ((_debugPhysicsSystem.Flags & PhysicsDebugFlags.COM) != 0)
{
const float Alpha = 0.25f;
foreach (var physBody in _physicsSystem.GetCollidingEntities(mapId, viewBounds))
{
Color color;
const float Alpha = 0.25f;
float size;
if (_entityManager.HasComponent<MapGridComponent>(physBody.Owner))
{
color = Color.Orange.WithAlpha(Alpha);
size = 1f;
}
else
{
color = Color.Purple.WithAlpha(Alpha);
size = 0.2f;
}
var color = Color.Purple.WithAlpha(Alpha);
var transform = physBody.GetTransform();
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), 0.2f, color);
}
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), size, color);
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, viewBounds))
{
var physBody = _entityManager.GetComponent<PhysicsComponent>(grid.GridEntityId);
var color = Color.Orange.WithAlpha(Alpha);
var transform = physBody.GetTransform();
worldHandle.DrawCircle(Transform.Mul(transform, physBody.LocalCenter), 1f, color);
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Threading;
using Robust.LoaderApi;
using Robust.Shared;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client;
internal partial class GameController
{
// A little paranoia goes a long way.
// This is static on purpose - the whole process shouldn't redial more than once, ever.
private static int _hasRedialled = 0;
public void Redial(string address, string? text = null)
{
// -- ATTENTION, YE WOULD-BE TRESPASSERS! --
// This code is the least testable code ever (because it's in-engine code that requires the Launcher), so don't make it do too much.
// This checks for some obvious requirements and then forwards to RedialApi which does the actual work.
// Testing of RedialApi is doable in the SS14.Launcher project, this is why it has a fake RobustToolbox build.
// -- THANK YE, NOW KINDLY MOVE ALONG! --
// We don't ever want to successfully redial more than once, and we want to shutdown once we've redialled.
// Otherwise abuse could happen.
DebugTools.AssertNotNull(_mainLoop);
Logger.Info($"Attempting redial of {address}: {text ?? "no reason given"}");
if (!_mainLoop!.Running)
{
throw new Exception("Attempted a redial during shutdown, this is not acceptable - redial first and if you succeed it'll shutdown anyway.");
}
if (_loaderArgs == null)
{
throw new Exception("Attempted a redial when the game was not run from the launcher (_loaderArgs == null)");
}
if (_loaderArgs!.RedialApi == null)
{
throw new Exception("Attempted a redial when redialling was not supported by the loader (Outdated launcher?)");
}
if (Interlocked.Increment(ref _hasRedialled) != 1)
{
// Don't let it overflow or anything
Interlocked.Decrement(ref _hasRedialled);
throw new Exception("Attempted a redial after one already succeeded or while one is in progress, this is never acceptable");
}
try
{
_loaderArgs!.RedialApi!.Redial(new Uri(address), text ?? "");
}
catch (Exception ex)
{
Interlocked.Decrement(ref _hasRedialled);
throw ex;
}
Shutdown("Redial");
}
}

View File

@@ -21,14 +21,16 @@ using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.Exceptions;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
@@ -66,6 +68,9 @@ namespace Robust.Client
[Dependency] private readonly IAuthManager _authManager = default!;
[Dependency] private readonly IMidiManager _midiManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IParallelManagerInternal _parallelMgr = default!;
[Dependency] private readonly ProfManager _prof = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
private IWebViewManagerHook? _webViewHook;
@@ -91,7 +96,8 @@ namespace Robust.Client
_clyde.InitializePostWindowing();
_clydeAudio.InitializePostWindowing();
_clyde.SetWindowTitle(Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox");
_clyde.SetWindowTitle(
Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox");
_taskManager.Initialize();
_fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi));
@@ -131,8 +137,9 @@ namespace Robust.Client
_inputManager.Initialize();
_console.Initialize();
_prototypeManager.Initialize();
_prototypeManager.LoadDirectory(new ResourcePath("/EnginePrototypes/"));
_prototypeManager.LoadDirectory(Options.PrototypeDirectory);
_prototypeManager.Resync();
_prototypeManager.ResolveResults();
_entityManager.Initialize();
_mapManager.Initialize();
_gameStateManager.Initialize();
@@ -157,7 +164,7 @@ namespace Robust.Client
// Setup main loop
if (_mainLoop == null)
{
_mainLoop = new GameLoop(_gameTiming)
_mainLoop = new GameLoop(_gameTiming, _runtimeLog, _prof)
{
SleepMode = displayMode == DisplayMode.Headless ? SleepMode.Delay : SleepMode.None
};
@@ -197,7 +204,7 @@ namespace Robust.Client
_clyde.Ready();
if (!Options.DisableCommandLineConnect &&
if (_resourceManifest!.AutoConnect &&
(_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
&& LaunchState.ConnectEndpoint != null)
{
@@ -213,7 +220,7 @@ namespace Robust.Client
{
// Parses /manifest.yml for game-specific settings that cannot be exclusively set up by content code.
if (!_resourceCache.TryContentFileRead("/manifest.yml", out var stream))
return new ResourceManifestData(Array.Empty<string>(), null, null, null, null);
return new ResourceManifestData(Array.Empty<string>(), null, null, null, null, true);
var yamlStream = new YamlStream();
using (stream)
@@ -223,7 +230,7 @@ namespace Robust.Client
}
if (yamlStream.Documents.Count == 0)
return new ResourceManifestData(Array.Empty<string>(), null, null, null, null);
return new ResourceManifestData(Array.Empty<string>(), null, null, null, null, true);
if (yamlStream.Documents.Count != 1 || yamlStream.Documents[0].RootNode is not YamlMappingNode mapping)
{
@@ -258,7 +265,11 @@ namespace Robust.Client
if (mapping.TryGetNode("splashLogo", out var splashNode))
splashLogo = splashNode.AsString();
return new ResourceManifestData(modules, assemblyPrefix, defaultWindowTitle, windowIconSet, splashLogo);
bool autoConnect = true;
if (mapping.TryGetNode("autoConnect", out var autoConnectNode))
autoConnect = autoConnectNode.AsBool();
return new ResourceManifestData(modules, assemblyPrefix, defaultWindowTitle, windowIconSet, splashLogo, autoConnect);
}
internal bool StartupSystemSplash(GameControllerOptions options, Func<ILogHandler>? logHandlerFactory)
@@ -326,6 +337,12 @@ namespace Robust.Client
ProfileOptSetup.Setup(_configurationManager);
_parallelMgr.Initialize();
_prof.Initialize();
#if !FULL_RELEASE
_configurationManager.OverrideDefault(CVars.ProfEnabled, true);
#endif
_resourceCache.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
var mountOptions = _commandLineArgs != null
@@ -446,52 +463,127 @@ namespace Robust.Client
private void Input(FrameEventArgs frameEventArgs)
{
_clyde.ProcessInput(frameEventArgs);
_networkManager.ProcessPackets();
_taskManager.ProcessPendingTasks(); // tasks like connect
using (_prof.Group("Input Events"))
{
_clyde.ProcessInput(frameEventArgs);
}
using (_prof.Group("Network"))
{
_networkManager.ProcessPackets();
}
using (_prof.Group("Async"))
{
_taskManager.ProcessPendingTasks(); // tasks like connect
}
}
private void Tick(FrameEventArgs frameEventArgs)
{
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
_console.CommandBufferExecute();
_timerManager.UpdateTimers(frameEventArgs);
_taskManager.ProcessPendingTasks();
using (_prof.Group("Content pre engine"))
{
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
}
using (_prof.Group("Console"))
{
_console.CommandBufferExecute();
}
using (_prof.Group("Timers"))
{
_timerManager.UpdateTimers(frameEventArgs);
}
using (_prof.Group("Async"))
{
_taskManager.ProcessPendingTasks();
}
// GameStateManager is in full control of the simulation update in multiplayer.
if (_client.RunLevel == ClientRunLevel.InGame || _client.RunLevel == ClientRunLevel.Connected)
{
_gameStateManager.ApplyGameState();
using (_prof.Group("Game state"))
{
_gameStateManager.ApplyGameState();
}
}
// In singleplayer, however, we're in full control instead.
else if (_client.RunLevel == ClientRunLevel.SinglePlayerGame)
{
// The last real tick is the current tick! This way we won't be in "prediction" mode.
_gameTiming.LastRealTick = _gameTiming.CurTick;
_entityManager.TickUpdate(frameEventArgs.DeltaSeconds, noPredictions: false);
using (_prof.Group("Entity"))
{
// The last real tick is the current tick! This way we won't be in "prediction" mode.
_gameTiming.LastRealTick = _gameTiming.CurTick;
_entityManager.TickUpdate(frameEventArgs.DeltaSeconds, noPredictions: false);
}
}
_modLoader.BroadcastUpdate(ModUpdateLevel.PostEngine, frameEventArgs);
using (_prof.Group("Content post engine"))
{
_modLoader.BroadcastUpdate(ModUpdateLevel.PostEngine, frameEventArgs);
}
}
private void Update(FrameEventArgs frameEventArgs)
{
_webViewHook?.Update();
_clydeAudio.FrameProcess(frameEventArgs);
_clyde.FrameProcess(frameEventArgs);
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePreEngine, frameEventArgs);
_stateManager.FrameUpdate(frameEventArgs);
if (_webViewHook != null)
{
using (_prof.Group("WebView"))
{
_webViewHook?.Update();
}
}
using (_prof.Group("ClydeAudio"))
{
_clydeAudio.FrameProcess(frameEventArgs);
}
using (_prof.Group("Clyde"))
{
_clyde.FrameProcess(frameEventArgs);
}
using (_prof.Group("Content Pre Engine"))
{
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePreEngine, frameEventArgs);
}
using (_prof.Group("State"))
{
_stateManager.FrameUpdate(frameEventArgs);
}
if (_client.RunLevel >= ClientRunLevel.Connected)
{
_placementManager.FrameUpdate(frameEventArgs);
_entityManager.FrameUpdate(frameEventArgs.DeltaSeconds);
using (_prof.Group("Placement"))
{
_placementManager.FrameUpdate(frameEventArgs);
}
using (_prof.Group("Entity"))
{
_entityManager.FrameUpdate(frameEventArgs.DeltaSeconds);
}
}
_overlayManager.FrameUpdate(frameEventArgs);
_userInterfaceManager.FrameUpdate(frameEventArgs);
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
using (_prof.Group("Overlay"))
{
_overlayManager.FrameUpdate(frameEventArgs);
}
using (_prof.Group("UI"))
{
_userInterfaceManager.FrameUpdate(frameEventArgs);
}
using (_prof.Group("Content Post Engine"))
{
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
}
}
internal static void SetupLogging(ILogManager logManager, Func<ILogHandler> logHandlerFactory)
@@ -584,7 +676,8 @@ namespace Robust.Client
string? AssemblyPrefix,
string? DefaultWindowTitle,
string? WindowIconSet,
string? SplashLogo
string? SplashLogo,
bool AutoConnect
);
}
}

View File

@@ -82,10 +82,5 @@ namespace Robust.Client
/// Whether to load config and user data.
/// </summary>
public bool LoadConfigAndUserData { get; init; } = true;
/// <summary>
/// Whether to disable command line args server auto-connecting.
/// </summary>
public bool DisableCommandLineConnect { get; init; } = false;
}
}

View File

@@ -37,9 +37,9 @@ namespace Robust.Client.GameObjects
return base.CreateEntity(prototypeName, uid);
}
void IClientEntityManagerInternal.InitializeEntity(EntityUid entity)
void IClientEntityManagerInternal.InitializeEntity(EntityUid entity, MetaDataComponent? meta = null)
{
base.InitializeEntity(entity);
base.InitializeEntity(entity, meta);
}
void IClientEntityManagerInternal.StartEntity(EntityUid entity)
@@ -86,7 +86,7 @@ namespace Robust.Client.GameObjects
public void SendSystemNetworkMessage(EntityEventArgs message, uint sequence)
{
var msg = _networkManager.CreateNetMessage<MsgEntity>();
var msg = new MsgEntity();
msg.Type = EntityMessageType.SystemMessage;
msg.SystemMessage = message;
msg.SourceTick = _gameTiming.CurTick;

View File

@@ -1,3 +1,4 @@
using System;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
@@ -32,11 +33,13 @@ namespace Robust.Client.GameObjects
public const string LogCategory = "go.comp.icon";
const string SerializationCache = "icon";
[Obsolete("Use SpriteSystem instead.")]
private static IRsiStateLike TextureForConfig(IconComponent compData, IResourceCache resourceCache)
{
return compData.Icon?.Default ?? resourceCache.GetFallback<TextureResource>().Texture;
}
[Obsolete("Use SpriteSystem instead.")]
public static IRsiStateLike? GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
if (!prototype.Components.TryGetValue("Icon", out var compData))

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.GameObjects
{
[RegisterComponent]
[ComponentReference(typeof(SharedPointLightComponent))]
public sealed class PointLightComponent : SharedPointLightComponent, ISerializationHooks
public sealed class PointLightComponent : SharedPointLightComponent
{
[Dependency] private readonly IEntityManager _entityManager = default!;

View File

@@ -601,8 +601,10 @@ namespace Robust.Client.GameObjects
private void RebuildBounds()
{
_bounds = new Box2();
foreach (var layer in Layers.Where(layer => layer.Visible))
foreach (var layer in Layers)
{
if (!layer.Visible) continue;
_bounds = _bounds.Union(layer.CalculateBoundingBox());
}
}
@@ -1520,6 +1522,7 @@ namespace Robust.Client.GameObjects
}
}
[Obsolete("Use SpriteSystem instead.")]
internal static RSI.State GetFallbackState(IResourceCache cache)
{
var rsi = cache.GetResource<RSIResource>("/Textures/error.rsi").RSI;
@@ -1968,7 +1971,7 @@ namespace Robust.Client.GameObjects
/// <inheritdoc/>
public Box2 CalculateBoundingBox()
{
var textureSize = PixelSize / EyeManager.PixelsPerMeter;
var textureSize = (Vector2) PixelSize / EyeManager.PixelsPerMeter;
// If the parent has locked rotation and we don't have any rotation,
// we can take the quick path of just making a box the size of the texture.
@@ -2174,7 +2177,7 @@ namespace Robust.Client.GameObjects
var entityManager = IoCManager.Resolve<IEntityManager>();
var dummy = entityManager.SpawnEntity(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = entityManager.EnsureComponent<SpriteComponent>(dummy);
EntitySystem.Get<AppearanceSystem>().OnChangeData(dummy);
EntitySystem.Get<AppearanceSystem>().OnChangeData(dummy, spriteComponent);
var anyTexture = false;
foreach (var layer in spriteComponent.AllLayers)

View File

@@ -75,6 +75,10 @@ namespace Robust.Client.GameObjects
(BoundUserInterface) _dynamicTypeFactory.CreateInstance(type, new[] {this, wrapped.UiKey});
boundInterface.Open();
_openInterfaces[wrapped.UiKey] = boundInterface;
var playerSession = _playerManager.LocalPlayer?.Session;
if(playerSession != null)
_entityManager.EventBus.RaiseLocalEvent(Owner, new BoundUIOpenedEvent(wrapped.UiKey, Owner, playerSession));
}
internal void Close(object uiKey, bool remoteCall)

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
@@ -94,17 +95,21 @@ namespace Robust.Client.GameObjects
public override void FrameUpdate(float frameTime)
{
var spriteQuery = GetEntityQuery<SpriteComponent>();
while (_queuedUpdates.TryDequeue(out var appearance))
{
if (appearance.Deleted)
continue;
OnChangeData(appearance.Owner, appearance);
// Sprite comp is allowed to be null, so that things like spriteless point-lights can use this system
spriteQuery.TryGetComponent(appearance.Owner, out var sprite);
OnChangeData(appearance.Owner, sprite, appearance);
UnmarkDirty(appearance);
}
}
public void OnChangeData(EntityUid uid, ClientAppearanceComponent? appearanceComponent = null)
public void OnChangeData(EntityUid uid, SpriteComponent? sprite, ClientAppearanceComponent? appearanceComponent = null)
{
if (!Resolve(uid, ref appearanceComponent, false)) return;
@@ -112,6 +117,7 @@ namespace Robust.Client.GameObjects
{
Component = appearanceComponent,
AppearanceData = appearanceComponent.AppearanceData,
Sprite = sprite,
};
// Give it AppearanceData so we can still keep the friend attribute on the component.
@@ -133,5 +139,6 @@ namespace Robust.Client.GameObjects
{
public AppearanceComponent Component;
public IReadOnlyDictionary<object, object> AppearanceData;
public SpriteComponent? Sprite;
}
}

View File

@@ -1,12 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Robust.Client.Physics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
@@ -33,9 +31,10 @@ namespace Robust.Client.GameObjects
UpdatesAfter.Add(typeof(TransformSystem));
UpdatesAfter.Add(typeof(PhysicsSystem));
SubscribeLocalEvent<OccluderDirtyEvent>(HandleDirtyEvent);
SubscribeLocalEvent<OccluderDirtyEvent>(OnOccluderDirty);
SubscribeLocalEvent<ClientOccluderComponent, AnchorStateChangedEvent>(HandleAnchorChanged);
SubscribeLocalEvent<ClientOccluderComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<ClientOccluderComponent, ReAnchorEvent>(OnReAnchor);
}
public override void FrameUpdate(float frameTime)
@@ -62,51 +61,58 @@ namespace Robust.Client.GameObjects
}
}
private static void HandleAnchorChanged(EntityUid uid, ClientOccluderComponent component, ref AnchorStateChangedEvent args)
private static void OnAnchorChanged(EntityUid uid, ClientOccluderComponent component, ref AnchorStateChangedEvent args)
{
component.AnchorStateChanged();
}
private void HandleDirtyEvent(OccluderDirtyEvent ev)
private void OnReAnchor(EntityUid uid, ClientOccluderComponent component, ref ReAnchorEvent args)
{
component.AnchorStateChanged();
}
private void OnOccluderDirty(OccluderDirtyEvent ev)
{
var sender = ev.Sender;
IMapGrid? grid;
var occluderQuery = GetEntityQuery<ClientOccluderComponent>();
if (EntityManager.EntityExists(sender) &&
EntityManager.TryGetComponent(sender, out ClientOccluderComponent? iconSmooth)
&& iconSmooth.Initialized)
occluderQuery.HasComponent(sender))
{
var xform = EntityManager.GetComponent<TransformComponent>(sender);
if (!_mapManager.TryGetGrid(xform.GridID, out var grid1))
if (!_mapManager.TryGetGrid(xform.GridID, out grid))
return;
var coords = xform.Coordinates;
var localGrid = grid.TileIndicesFor(coords);
_dirtyEntities.Enqueue(sender);
AddValidEntities(grid1.GetInDir(coords, Direction.North));
AddValidEntities(grid1.GetInDir(coords, Direction.South));
AddValidEntities(grid1.GetInDir(coords, Direction.East));
AddValidEntities(grid1.GetInDir(coords, Direction.West));
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(0, 1)), occluderQuery);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(0, -1)), occluderQuery);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(1, 0)), occluderQuery);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(-1, 0)), occluderQuery);
}
// Entity is no longer valid, update around the last position it was at.
else if (ev.LastPosition.HasValue && _mapManager.TryGetGrid(ev.LastPosition.Value.grid, out var grid))
else if (ev.LastPosition.HasValue && _mapManager.TryGetGrid(ev.LastPosition.Value.grid, out grid))
{
var pos = ev.LastPosition.Value.pos;
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)));
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, 1)), occluderQuery);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, -1)), occluderQuery);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, 0)), occluderQuery);
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, 0)), occluderQuery);
}
}
private void AddValidEntities(IEnumerable<EntityUid> candidates)
private void AddValidEntities(AnchoredEntitiesEnumerator enumerator, EntityQuery<ClientOccluderComponent> occluderQuery)
{
foreach (var entity in candidates)
while (enumerator.MoveNext(out var entity))
{
if (EntityManager.HasComponent<ClientOccluderComponent>(entity))
{
_dirtyEntities.Enqueue(entity);
}
if (!occluderQuery.HasComponent(entity.Value)) continue;
_dirtyEntities.Enqueue(entity.Value);
}
}
}

View File

@@ -0,0 +1,105 @@
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.Containers;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.GameObjects;
public sealed class DebugEntityLookupCommand : IConsoleCommand
{
public string Command => "togglelookup";
public string Description => "Shows / hides entitylookup bounds via an overlay";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntitySystem.Get<DebugEntityLookupSystem>().Enabled ^= true;
}
}
public sealed class DebugEntityLookupSystem : EntitySystem
{
public bool Enabled
{
get => _enabled;
set
{
if (_enabled == value) return;
_enabled = value;
if (_enabled)
{
IoCManager.Resolve<IOverlayManager>().AddOverlay(
new EntityLookupOverlay(
EntityManager,
Get<EntityLookupSystem>()));
}
else
{
IoCManager.Resolve<IOverlayManager>().RemoveOverlay<EntityLookupOverlay>();
}
}
}
private bool _enabled;
}
public sealed class EntityLookupOverlay : Overlay
{
private IEntityManager _entityManager = default!;
private EntityLookupSystem _lookup = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityLookupOverlay(IEntityManager entManager, EntityLookupSystem lookup)
{
_entityManager = entManager;
_lookup = lookup;
}
protected internal override void Draw(in OverlayDrawArgs args)
{
var worldHandle = args.WorldHandle;
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
foreach (var lookup in _lookup.FindLookupsIntersecting(args.MapId, args.WorldBounds))
{
var lookupXform = xformQuery.GetComponent(lookup.Owner);
var (_, rotation, matrix, invMatrix) = lookupXform.GetWorldPositionRotationMatrixWithInv();
worldHandle.SetTransform(matrix);
var lookupAABB = invMatrix.TransformBox(args.WorldBounds);
var ents = new List<EntityUid>();
// Gonna allocate a lot but debug overlay sooo
lookup.Tree._b2Tree.FastQuery(ref lookupAABB, (ref EntityUid data) =>
{
ents.Add(data);
});
foreach (var ent in ents)
{
if (_entityManager.Deleted(ent)) continue;
var xform = xformQuery.GetComponent(ent);
//DebugTools.Assert(!ent.IsInContainer(_entityManager));
var (entPos, entRot) = xform.GetWorldPositionRotation();
var lookupPos = invMatrix.Transform(entPos);
var lookupRot = entRot - rotation;
var aabb = _lookup.GetAABB(ent, lookupPos, lookupRot, xform, xformQuery);
worldHandle.DrawRect(aabb, Color.Blue.WithAlpha(0.2f));
}
}
}
}

View File

@@ -76,7 +76,7 @@ namespace Robust.Client.GameObjects
worldHandle.SetTransform(worldMatrix);
var transform = new Transform(Vector2.Zero, Angle.Zero);
gridInternal.GetMapChunks(viewport, out var chunkEnumerator);
var chunkEnumerator = gridInternal.GetMapChunks(viewport);
while (chunkEnumerator.MoveNext(out var chunk))
{

View File

@@ -1,9 +0,0 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects
{
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
{
}
}

View File

@@ -0,0 +1,24 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Robust.Client.GameObjects;
public sealed class ScaleVisualsSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AppearanceChangeEvent>(OnChangeData);
}
private void OnChangeData(ref AppearanceChangeEvent ev)
{
if (!ev.AppearanceData.TryGetValue(ScaleVisuals.Scale, out var scale) ||
ev.Sprite == null) return;
var vecScale = (Vector2)scale;
// Set it directly because prediction may call this multiple times.
ev.Sprite.Scale = vecScale;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
@@ -17,13 +18,13 @@ public sealed partial class SpriteSystem
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Pure]
private readonly Dictionary<string, IRsiStateLike> _cachedPrototypeIcons = new();
public Texture Frame0(SpriteSpecifier specifier)
{
return RsiStateLike(specifier).Default;
}
[Pure]
public IRsiStateLike RsiStateLike(SpriteSpecifier specifier)
{
switch (specifier)
@@ -35,38 +36,71 @@ public sealed partial class SpriteSystem
return GetState(rsi);
case SpriteSpecifier.EntityPrototype prototypeIcon:
if (!_proto.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
{
Logger.Error("Failed to load PrototypeIcon {0}", prototypeIcon.EntityPrototypeId);
return SpriteComponent.GetFallbackState(_resourceCache);
}
return SpriteComponent.GetPrototypeIcon(prototype, _resourceCache);
return GetPrototypeIcon(prototypeIcon.EntityPrototypeId);
default:
throw new NotSupportedException();
}
}
[Pure]
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
/// <summary>
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
/// This method caches the result based on the prototype identifier.
/// </summary>
public IRsiStateLike GetPrototypeIcon(string prototype)
{
var icon = IconComponent.GetPrototypeIcon(prototype, _resourceCache);
if (icon != null) return icon;
// Check if this prototype has been cached before, and if so return the result.
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
return cachedResult;
if (!prototype.Components.ContainsKey("Sprite"))
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
{
return SpriteComponent.GetFallbackState(resourceCache);
// The specified prototype doesn't exist, return the fallback "error" sprite.
Logger.Error("Failed to load PrototypeIcon {0}", prototype);
return GetFallbackState();
}
// Generate the icon and cache it in case it's ever needed again.
var result = GetPrototypeIcon(entityPrototype);
_cachedPrototypeIcons[prototype] = result;
return result;
}
/// <summary>
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
/// This method does NOT cache the result.
/// </summary>
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
{
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
if (prototype.Components.TryGetValue("Icon", out var compData)
&& compData is IconComponent {Icon: {} icon})
{
return icon.Default;
}
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
if (!prototype.Components.ContainsKey("Sprite"))
{
return GetFallbackState();
}
// Finally, we use spawn a dummy entity to get its icon.
var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
var result = spriteComponent.Icon ?? SpriteComponent.GetFallbackState(resourceCache);
var result = spriteComponent.Icon ?? GetFallbackState();
Del(dummy);
return result;
}
[Pure]
public RSI.State GetFallbackState()
{
return _resourceCache.GetFallback<RSIResource>().RSI["error"];
}
[Pure]
public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier)
{
@@ -79,6 +113,20 @@ public sealed partial class SpriteSystem
}
Logger.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
return SpriteComponent.GetFallbackState(_resourceCache);
return GetFallbackState();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs protoReloaded)
{
// Check if any EntityPrototype has been changed.
if (!protoReloaded.ByType.TryGetValue(typeof(EntityPrototype), out var changedSet))
return;
// Remove all changed prototypes from the cache, if they're there.
foreach (var (prototype, _) in changedSet.Modified)
{
// Let's be lazy and not regenerate them until something needs them again.
_cachedPrototypeIcons.Remove(prototype);
}
}
}

View File

@@ -23,9 +23,16 @@ namespace Robust.Client.GameObjects
{
base.Initialize();
_proto.PrototypesReloaded += OnPrototypesReloaded;
SubscribeLocalEvent<SpriteUpdateInertEvent>(QueueUpdateInert);
}
public override void Shutdown()
{
base.Shutdown();
_proto.PrototypesReloaded -= OnPrototypesReloaded;
}
private void QueueUpdateInert(SpriteUpdateInertEvent ev)
{
_inertUpdateQueue.Enqueue(ev.Sprite);

View File

@@ -52,7 +52,8 @@ namespace Robust.Client.GameObjects
// Only lerp if parent didn't change.
// E.g. entering lockers would do it.
if (transform.LerpParent == transform.ParentUid)
if (transform.LerpParent == transform.ParentUid &&
transform.ParentUid.IsValid())
{
if (transform.LerpDestination != null)
{

View File

@@ -8,7 +8,7 @@ namespace Robust.Client.GameObjects
EntityUid CreateEntity(string? prototypeName, EntityUid uid = default);
void InitializeEntity(EntityUid entity);
void InitializeEntity(EntityUid entity, MetaDataComponent? meta = null);
void StartEntity(EntityUid entity);
}

View File

@@ -21,6 +21,7 @@ using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Profiling;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -54,6 +55,7 @@ namespace Robust.Client.GameStates
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IClientEntityManager _entityManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly ProfManager _prof = default!;
#if EXCEPTION_TOLERANCE
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
#endif
@@ -224,18 +226,28 @@ namespace Robust.Client.GameStates
ResetPredictedEntities(_timing.CurTick);
}
if (!curState.Extrapolated)
using (_prof.Group("FullRep"))
{
_processor.UpdateFullRep(curState);
if (!curState.Extrapolated)
{
_processor.UpdateFullRep(curState);
}
}
// Store last tick we got from the GameStateProcessor.
_lastProcessedTick = _timing.CurTick;
// apply current state
var createdEntities = ApplyGameState(curState, nextState);
List<EntityUid> createdEntities;
using (_prof.Group("ApplyGameState"))
{
createdEntities = ApplyGameState(curState, nextState);
}
MergeImplicitData(createdEntities);
using (_prof.Group("MergeImplicitData"))
{
MergeImplicitData(createdEntities);
}
if (_lastProcessedSeq < curState.LastProcessedInput)
{
@@ -270,6 +282,7 @@ namespace Robust.Client.GameStates
if (IsPredictionEnabled)
{
using var _p = _prof.Group("Prediction");
using var _ = _timing.StartPastPredictionArea();
if (_pendingInputs.Count > 0)
@@ -290,6 +303,8 @@ namespace Robust.Client.GameStates
for (var t = _lastProcessedTick.Value + 1; t <= targetTick; t++)
{
var groupStart = _prof.WriteGroupStart();
var tick = new GameTick(t);
_timing.CurTick = tick;
@@ -321,20 +336,36 @@ namespace Robust.Client.GameStates
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, noPredictions: false);
((IBroadcastEventBusInternal) _entities.EventBus).ProcessEventQueue();
using (_prof.Group("Systems"))
{
// 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, noPredictions: false);
}
using (_prof.Group("Event queue"))
{
((IBroadcastEventBusInternal) _entities.EventBus).ProcessEventQueue();
}
}
_prof.WriteGroupEnd(groupStart, "Prediction tick", ProfData.Int64(t));
}
}
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: !IsPredictionEnabled);
using (_prof.Group("Tick"))
{
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: !IsPredictionEnabled);
}
}
private void ResetPredictedEntities(GameTick curTick)
{
using var _ = _prof.Group("ResetPredictedEntities");
var countReset = 0;
foreach (var meta in _entityManager.EntityQuery<MetaDataComponent>(true))
{
var entity = meta.Owner;
@@ -355,6 +386,8 @@ namespace Robust.Client.GameStates
continue;
}
countReset += 1;
// TODO: handle component deletions/creations.
foreach (var (netId, comp) in _entityManager.GetNetComponents(entity))
{
@@ -374,6 +407,8 @@ namespace Robust.Client.GameStates
comp.HandleComponentState(compState, null);
}
}
_prof.WriteValue("Reset count", ProfData.Int32(countReset));
}
private void MergeImplicitData(List<EntityUid> createdEntities)
@@ -409,27 +444,47 @@ namespace Robust.Client.GameStates
private void AckGameState(GameTick sequence)
{
var msg = _network.CreateNetMessage<MsgStateAck>();
var msg = new MsgStateAck();
msg.Sequence = sequence;
_network.ClientSendMessage(msg);
}
private List<EntityUid> ApplyGameState(GameState curState, GameState? nextState)
{
_config.TickProcessMessages();
_mapManager.ApplyGameStatePre(curState.MapData, curState.EntityStates.Span);
var createdEntities = ApplyEntityStates(curState.EntityStates.Span, curState.EntityDeletions.Span,
nextState != null ? nextState.EntityStates.Span : default);
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<PlayerState>());
using (_prof.Group("Config"))
{
_config.TickProcessMessages();
}
using (_prof.Group("Map Pre"))
{
_mapManager.ApplyGameStatePre(curState.MapData, curState.EntityStates.Span);
}
List<EntityUid> createdEntities;
using (_prof.Group("Entity"))
{
createdEntities = ApplyEntityStates(curState.EntityStates.Span, curState.EntityDeletions.Span,
nextState != null ? nextState.EntityStates.Span : default);
}
using (_prof.Group("Player"))
{
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<PlayerState>());
}
using (_prof.Group("Callback"))
{
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState));
}
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState));
return createdEntities;
}
private List<EntityUid> ApplyEntityStates(ReadOnlySpan<EntityState> curEntStates, ReadOnlySpan<EntityUid> deletions,
ReadOnlySpan<EntityState> nextEntStates)
{
var toApply = new Dictionary<EntityUid, (EntityState?, EntityState?)>();
var toApply = new Dictionary<EntityUid, (EntityState?, EntityState?)>(curEntStates.Length);
var toInitialize = new List<EntityUid>();
var created = new List<EntityUid>();
@@ -537,6 +592,9 @@ namespace Robust.Client.GameStates
}
#endif
_prof.WriteValue("Created", ProfData.Int32(created.Count));
_prof.WriteValue("Applied", ProfData.Int32(toApply.Count));
return created;
}
@@ -548,6 +606,8 @@ namespace Robust.Client.GameStates
if (curState != null)
{
compStateWork.EnsureCapacity(curState.ComponentChanges.Span.Length);
foreach (var compChange in curState.ComponentChanges.Span)
{
if (compChange.Deleted)
@@ -580,6 +640,8 @@ namespace Robust.Client.GameStates
if (nextState != null)
{
compStateWork.EnsureCapacity(compStateWork.Count + nextState.ComponentChanges.Span.Length);
foreach (var compState in nextState.ComponentChanges.Span)
{
if (compStateWork.TryGetValue(compState.NetID, out var state))

View File

@@ -50,7 +50,7 @@ namespace Robust.Client.Graphics.Clyde
var transform = _entityManager.GetComponent<TransformComponent>(grid.GridEntityId);
gridProgram.SetUniform(UniIModelMatrix, transform.WorldMatrix);
grid.GetMapChunks(worldBounds, out var enumerator);
var enumerator = grid.GetMapChunks(worldBounds);
while (enumerator.MoveNext(out var chunk))
{

View File

@@ -14,6 +14,8 @@ using Robust.Shared;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Profiling;
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
namespace Robust.Client.Graphics.Clyde
{
@@ -97,6 +99,7 @@ namespace Robust.Client.Graphics.Clyde
ClearFramebuffer(_userInterfaceManager.GetMainClearColor());
using (DebugGroup("UI"))
using (_prof.Group("UI"))
{
_userInterfaceManager.Render(_renderHandle);
FlushRenderQueue();
@@ -104,8 +107,21 @@ namespace Robust.Client.Graphics.Clyde
TakeScreenshot(ScreenshotType.Final);
// And finally, swap those buffers!
SwapAllBuffers();
using (_prof.Group("Swap buffers"))
{
// And finally, swap those buffers!
SwapAllBuffers();
}
using (_prof.Group("Stats"))
{
_prof.WriteValue("GL Draw Calls", ProfData.Int32(_debugStats.LastGLDrawCalls));
_prof.WriteValue("Clyde Draw Calls", ProfData.Int32(_debugStats.LastClydeDrawCalls));
_prof.WriteValue("Batches", ProfData.Int32(_debugStats.LastBatches));
_prof.WriteValue("Max Batch Verts", ProfData.Int32(_debugStats.LargestBatchVertices));
_prof.WriteValue("Max Batch Idxes", ProfData.Int32(_debugStats.LargestBatchIndices));
_prof.WriteValue("Lights", ProfData.Int32(_debugStats.TotalLights));
}
}
private void RenderOverlays(Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
@@ -140,11 +156,13 @@ namespace Robust.Client.Graphics.Clyde
OverlaySpace space,
in UIBox2i bounds)
{
using var _ = _prof.Group($"Overlays SS {space}");
var list = GetOverlaysForSpace(space);
var worldAABB = CalcWorldAABB(vp);
var worldBounds = CalcWorldBounds(vp);
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, worldAABB, worldBounds);
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, vp.Eye!.Position.MapId, worldAABB, worldBounds);
foreach (var overlay in list)
{
@@ -193,6 +211,12 @@ namespace Robust.Client.Graphics.Clyde
if (lastFrameSize != texture.Size)
{
GL.BindTexture(TextureTarget.Texture2D, screenBufferHandle.Handle);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS,
(int)OGLTextureWrapMode.MirroredRepeat);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,
(int)OGLTextureWrapMode.MirroredRepeat);
GL.TexImage2D(TextureTarget.Texture2D, 0,
_hasGLSrgb ? PixelInternalFormat.Srgb8Alpha8 : PixelInternalFormat.Rgba8, texture.Size.X,
texture.Size.Y, 0,
@@ -450,6 +474,8 @@ namespace Robust.Client.Graphics.Clyde
return;
}
using var _ = _prof.Group("Viewport");
RenderInRenderTarget(viewport.RenderTarget, () =>
{
using var _ = DebugGroup($"Viewport: {viewport.Name}");
@@ -471,24 +497,33 @@ namespace Robust.Client.Graphics.Clyde
if (_eyeManager.CurrentMap != MapId.Nullspace)
{
using (DebugGroup("Lights"))
using (_prof.Group("Lights"))
{
DrawLightsAndFov(viewport, worldBounds, worldAABB, eye);
}
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowWorld, worldAABB, worldBounds);
using (_prof.Group("Overlays WSBW"))
{
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowWorld, worldAABB, worldBounds);
}
using (DebugGroup("Grids"))
using (_prof.Group("Grids"))
{
_drawGrids(viewport, worldBounds, eye);
}
// We will also render worldspace overlays here so we can do them under / above entities as necessary
using (DebugGroup("Entities"))
using (_prof.Group("Entities"))
{
DrawEntities(viewport, worldBounds, worldAABB, eye);
}
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldAABB, worldBounds);
using (_prof.Group("Overlays WSBFOV"))
{
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldAABB, worldBounds);
}
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
{
@@ -521,7 +556,10 @@ namespace Robust.Client.Graphics.Clyde
UIBox2.FromDimensions(Vector2.Zero, viewport.Size), new Color(1, 1, 1, 0.5f));
}
RenderOverlays(viewport, OverlaySpace.WorldSpace, worldAABB, worldBounds);
using (_prof.Group("Overlays WS"))
{
RenderOverlays(viewport, OverlaySpace.WorldSpace, worldAABB, worldBounds);
}
_currentViewport = oldVp;
}, viewport.ClearColor);

View File

@@ -272,7 +272,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
private void LoadGLProc<T>(string name, out T field) where T : Delegate
private nint LoadGLProc(string name)
{
var proc = _glBindingsContext.GetProcAddress(name);
if (proc == IntPtr.Zero || proc == new IntPtr(1) || proc == new IntPtr(2))
@@ -280,7 +280,7 @@ namespace Robust.Client.Graphics.Clyde
throw new InvalidOperationException($"Unable to load GL function '{name}'!");
}
field = Marshal.GetDelegateForFunctionPointer<T>(proc);
return proc;
}
}
}

View File

@@ -84,6 +84,7 @@ namespace Robust.Client.Graphics.Clyde
// For depth calculation of lighting shadows.
private RenderTexture _shadowRenderTarget = default!;
// Used because otherwise a MaxLightsPerScene change callback getting hit on startup causes interesting issues (read: bugs)
private bool _shadowRenderTargetCanInitializeSafely = false;
@@ -91,7 +92,8 @@ namespace Robust.Client.Graphics.Clyde
private ClydeTexture FovTexture => _fovRenderTarget.Texture;
private ClydeTexture ShadowTexture => _shadowRenderTarget.Texture;
private (PointLightComponent light, Vector2 pos, float distanceSquared)[] _lightsToRenderList = new (PointLightComponent light, Vector2 pos, float distanceSquared)[LightsToRenderListSize];
private (PointLightComponent light, Vector2 pos, float distanceSquared)[] _lightsToRenderList =
new (PointLightComponent light, Vector2 pos, float distanceSquared)[LightsToRenderListSize];
private unsafe void InitLighting()
{
@@ -151,7 +153,8 @@ namespace Robust.Client.Graphics.Clyde
// FOV FBO.
_fovRenderTarget = CreateRenderTarget((FovMapSize, 2),
new RenderTargetFormatParameters(_hasGLFloatFramebuffers ? RenderTargetColorFormat.RG32F : RenderTargetColorFormat.Rgba8, true),
new RenderTargetFormatParameters(
_hasGLFloatFramebuffers ? RenderTargetColorFormat.RG32F : RenderTargetColorFormat.Rgba8, true),
new TextureSampleParameters { WrapMode = TextureWrapMode.Repeat },
nameof(_fovRenderTarget));
@@ -175,7 +178,8 @@ namespace Robust.Client.Graphics.Clyde
var depthVert = ReadEmbeddedShader("shadow-depth.vert");
var depthFrag = ReadEmbeddedShader("shadow-depth.frag");
(string, uint)[] attribLocations = {
(string, uint)[] attribLocations =
{
("aPos", 0),
("subVertex", 1)
};
@@ -208,6 +212,7 @@ namespace Robust.Client.Graphics.Clyde
private void DrawFov(Viewport viewport, IEye eye)
{
using var _ = DebugGroup(nameof(DrawFov));
using var _p = _prof.Group("DrawFov");
PrepareDepthDraw(RtToLoaded(_fovRenderTarget));
@@ -294,6 +299,7 @@ namespace Robust.Client.Graphics.Clyde
{
GL.ClearColor(1, 1, 1, 1);
}
CheckGlError();
GL.Clear(ClearBufferMask.DepthBufferBit | ClearBufferMask.ColorBufferBit);
CheckGlError();
@@ -336,7 +342,14 @@ namespace Robust.Client.Graphics.Clyde
return;
}
var (lights, count, expandedBounds) = GetLightsToRender(mapId, worldBounds, worldAABB);
(PointLightComponent light, Vector2 pos, float distanceSquared)[] lights;
int count;
Box2 expandedBounds;
using (_prof.Group("LightsToRender"))
{
(lights, count, expandedBounds) = GetLightsToRender(mapId, worldBounds, worldAABB);
}
eye.GetViewMatrixNoOffset(out var eyeTransform, eye.Scale);
UpdateOcclusionGeometry(mapId, expandedBounds, eyeTransform);
@@ -352,6 +365,7 @@ namespace Robust.Client.Graphics.Clyde
}
using (DebugGroup("Draw shadow depth"))
using (_prof.Group("Draw shadow depth"))
{
PrepareDepthDraw(RtToLoaded(_shadowRenderTarget));
GL.CullFace(CullFaceMode.Back);
@@ -389,7 +403,8 @@ namespace Robust.Client.Graphics.Clyde
ApplyLightingFovToBuffer(viewport, eye);
var lightShader = _loadedShaders[_enableSoftShadows ? _lightSoftShaderHandle : _lightHardShaderHandle].Program;
var lightShader = _loadedShaders[_enableSoftShadows ? _lightSoftShaderHandle : _lightHardShaderHandle]
.Program;
lightShader.Use();
SetupGlobalUniformsImmediate(lightShader, ShadowTexture);
@@ -411,78 +426,82 @@ namespace Robust.Client.Graphics.Clyde
var lastSoftness = float.NaN;
Texture? lastMask = null;
for (var i = 0; i < count; i++)
using (_prof.Group("Draw Lights"))
{
var (component, lightPos, _) = lights[i];
var transform = _entityManager.GetComponent<TransformComponent>(component.Owner);
Texture? mask = null;
var rotation = Angle.Zero;
if (component.Mask != null)
for (var i = 0; i < count; i++)
{
mask = component.Mask;
rotation = component.Rotation;
var (component, lightPos, _) = lights[i];
if (component.MaskAutoRotate)
var transform = _entityManager.GetComponent<TransformComponent>(component.Owner);
Texture? mask = null;
var rotation = Angle.Zero;
if (component.Mask != null)
{
rotation += transform.WorldRotation;
mask = component.Mask;
rotation = component.Rotation;
if (component.MaskAutoRotate)
{
rotation += transform.WorldRotation;
}
}
var maskTexture = mask ?? Texture.White;
if (lastMask != maskTexture)
{
SetTexture(TextureUnit.Texture0, maskTexture);
lastMask = maskTexture;
lightShader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
}
if (!MathHelper.CloseToPercent(lastRange, component.Radius))
{
lastRange = component.Radius;
lightShader.SetUniformMaybe("lightRange", lastRange);
}
if (!MathHelper.CloseToPercent(lastPower, component.Energy))
{
lastPower = component.Energy;
lightShader.SetUniformMaybe("lightPower", lastPower);
}
if (lastColor != component.Color)
{
lastColor = component.Color;
lightShader.SetUniformMaybe("lightColor", lastColor);
}
if (_enableSoftShadows && !MathHelper.CloseToPercent(lastSoftness, component.Softness))
{
lastSoftness = component.Softness;
lightShader.SetUniformMaybe("lightSoftness", lastSoftness);
}
lightShader.SetUniformMaybe("lightCenter", lightPos);
lightShader.SetUniformMaybe("lightIndex",
component.CastShadows ? (i + 0.5f) / ShadowTexture.Height : -1);
var offset = new Vector2(component.Radius, component.Radius);
Matrix3 matrix;
if (mask == null)
{
matrix = Matrix3.Identity;
}
else
{
// Only apply rotation if a mask is said, because else it doesn't matter.
matrix = Matrix3.CreateRotation(rotation);
}
(matrix.R0C2, matrix.R1C2) = lightPos;
_drawQuad(-offset, offset, matrix, lightShader);
_debugStats.TotalLights += 1;
}
var maskTexture = mask ?? Texture.White;
if (lastMask != maskTexture)
{
SetTexture(TextureUnit.Texture0, maskTexture);
lastMask = maskTexture;
lightShader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
}
if (!MathHelper.CloseToPercent(lastRange, component.Radius))
{
lastRange = component.Radius;
lightShader.SetUniformMaybe("lightRange", lastRange);
}
if (!MathHelper.CloseToPercent(lastPower, component.Energy))
{
lastPower = component.Energy;
lightShader.SetUniformMaybe("lightPower", lastPower);
}
if (lastColor != component.Color)
{
lastColor = component.Color;
lightShader.SetUniformMaybe("lightColor", lastColor);
}
if (_enableSoftShadows && !MathHelper.CloseToPercent(lastSoftness, component.Softness))
{
lastSoftness = component.Softness;
lightShader.SetUniformMaybe("lightSoftness", lastSoftness);
}
lightShader.SetUniformMaybe("lightCenter", lightPos);
lightShader.SetUniformMaybe("lightIndex", component.CastShadows ? (i + 0.5f) / ShadowTexture.Height : -1);
var offset = new Vector2(component.Radius, component.Radius);
Matrix3 matrix;
if (mask == null)
{
matrix = Matrix3.Identity;
}
else
{
// Only apply rotation if a mask is said, because else it doesn't matter.
matrix = Matrix3.CreateRotation(rotation);
}
(matrix.R0C2, matrix.R1C2) = lightPos;
_drawQuad(-offset, offset, matrix, lightShader);
_debugStats.TotalLights += 1;
}
ResetBlendFunc();
@@ -494,9 +513,15 @@ namespace Robust.Client.Graphics.Clyde
if (_cfg.GetCVar(CVars.DisplayBlurLight))
BlurLights(viewport, eye);
BlurOntoWalls(viewport, eye);
using (_prof.Group("BlurOntoWalls"))
{
BlurOntoWalls(viewport, eye);
}
MergeWallLayer(viewport);
using (_prof.Group("MergeWallLayer"))
{
MergeWallLayer(viewport);
}
BindRenderTargetFull(viewport.RenderTarget);
GL.Viewport(0, 0, viewport.Size.X, viewport.Size.Y);
@@ -507,7 +532,8 @@ namespace Robust.Client.Graphics.Clyde
_lightingReady = true;
}
private ((PointLightComponent light, Vector2 pos, float distanceSquared)[] lights, int count, Box2 expandedBounds)
private ((PointLightComponent light, Vector2 pos, float distanceSquared)[] lights, int count, Box2
expandedBounds)
GetLightsToRender(MapId map, in Box2Rotated worldBounds, in Box2 worldAABB)
{
var renderingTreeSystem = _entitySystemManager.GetEntitySystem<RenderingTreeSystem>();
@@ -521,36 +547,39 @@ namespace Robust.Client.Graphics.Clyde
{
var bounds = xforms.GetComponent(comp.Owner).InvWorldMatrix.TransformBox(worldBounds);
comp.LightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldAABB, int count, int shadowCastingCount) state, in PointLightComponent light) =>
{
if (state.count >= LightsToRenderListSize)
comp.LightTree.QueryAabb(ref state,
(ref (Clyde clyde, Box2 worldAABB, int count, int shadowCastingCount) state,
in PointLightComponent light) =>
{
// There are too many lights to fit in the static memory.
return false;
}
if (state.count >= LightsToRenderListSize)
{
// There are too many lights to fit in the static memory.
return false;
}
var transform = xforms.GetComponent(light.Owner);
var transform = xforms.GetComponent(light.Owner);
if (float.IsNaN(transform.LocalPosition.X) || float.IsNaN(transform.LocalPosition.Y)) return true;
if (float.IsNaN(transform.LocalPosition.X) || float.IsNaN(transform.LocalPosition.Y))
return true;
var lightPos = transform.WorldMatrix.Transform(light.Offset);
var lightPos = transform.WorldMatrix.Transform(light.Offset);
var circle = new Circle(lightPos, light.Radius);
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.worldAABB))
{
return true;
}
// If the light is a shadow casting light, keep a separate track of that
if (light.CastShadows) state.shadowCastingCount++;
float distanceSquared = (state.worldAABB.Center - lightPos).LengthSquared;
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
// If the light doesn't touch anywhere the camera can see, it doesn't matter.
if (!circle.Intersects(state.worldAABB))
{
return true;
}
// If the light is a shadow casting light, keep a separate track of that
if (light.CastShadows) state.shadowCastingCount++;
float distanceSquared = (state.worldAABB.Center - lightPos).LengthSquared;
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
return true;
}, bounds);
}, bounds);
}
if (state.shadowCastingCount > _maxLightsPerScene)
@@ -560,18 +589,20 @@ namespace Robust.Client.Graphics.Clyde
// First, partition the array based on whether the lights are shadow casting or not
// (non shadow casting lights should be the first partition, shadow casting lights the second)
Array.Sort(_lightsToRenderList, 0, state.count, Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
{
if (x.light.CastShadows && !y.light.CastShadows) return 1;
else if (!x.light.CastShadows && y.light.CastShadows) return -1;
else return 0;
}));
Array.Sort(_lightsToRenderList, 0, state.count,
Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
{
if (x.light.CastShadows && !y.light.CastShadows) return 1;
else if (!x.light.CastShadows && y.light.CastShadows) return -1;
else return 0;
}));
// Next, sort just the shadow casting lights by distance.
Array.Sort(_lightsToRenderList, state.count - state.shadowCastingCount, state.shadowCastingCount, Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
{
return x.distanceSquared.CompareTo(y.distanceSquared);
}));
Array.Sort(_lightsToRenderList, state.count - state.shadowCastingCount, state.shadowCastingCount,
Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
{
return x.distanceSquared.CompareTo(y.distanceSquared);
}));
// Then effectively delete the furthest lights, by setting the end of the array to exclude N
// number of shadow casting lights (where N is the number above the max number per scene.)
@@ -755,7 +786,8 @@ namespace Robust.Client.Graphics.Clyde
{
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.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);
@@ -857,6 +889,9 @@ namespace Robust.Client.Graphics.Clyde
private void UpdateOcclusionGeometry(MapId map, Box2 expandedBounds, Matrix3 eyeTransform)
{
using var _ = _prof.Group("UpdateOcclusionGeometry");
using var _p = DebugGroup(nameof(UpdateOcclusionGeometry));
// This method generates two sets of occlusion geometry:
// 3D geometry used during depth projection.
// 2D mask geometry used to apply wall bleed.
@@ -865,8 +900,6 @@ namespace Robust.Client.Graphics.Clyde
const int maxOccluders = 2048;
using var _ = DebugGroup(nameof(UpdateOcclusionGeometry));
// 16 = 4 vertices * 4 directions
var arrayBuffer = ArrayPool<Vector4>.Shared.Rent(maxOccluders * 4 * 4);
// multiplied by 2 (it's a vector2 of bytes)
@@ -971,6 +1004,7 @@ namespace Robust.Client.Graphics.Clyde
// I don't like this, but rotated occluders started happening
return Vector2.Dot(normal, a) <= 0;
}
var nV = ((!no) && CheckFaceEyeVis(dTl, dTr));
var sV = ((!so) && CheckFaceEyeVis(dBr, dBl));
var eV = ((!eo) && CheckFaceEyeVis(dTr, dBr));
@@ -999,6 +1033,7 @@ namespace Robust.Client.Graphics.Clyde
// height
arrayVIBuffer[avi++] = (byte)(((vi & 2) != 0) ? 0 : 255);
}
QuadBatchIndexWrite(indexBuffer, ref ii, (ushort)aiBase);
}
@@ -1074,7 +1109,9 @@ namespace Robust.Client.Graphics.Clyde
var lightMapSize = GetLightMapSize(viewport.Size);
var lightMapSizeQuart = GetLightMapSize(viewport.Size, true);
var lightMapColorFormat = _hasGLFloatFramebuffers ? RenderTargetColorFormat.R11FG11FB10F : RenderTargetColorFormat.Rgba8;
var lightMapColorFormat = _hasGLFloatFramebuffers
? RenderTargetColorFormat.R11FG11FB10F
: RenderTargetColorFormat.Rgba8;
var lightMapSampleParameters = new TextureSampleParameters { Filter = true };
viewport.LightRenderTarget?.Dispose();
@@ -1147,9 +1184,11 @@ namespace Robust.Client.Graphics.Clyde
{
DeleteRenderTexture(_shadowRenderTarget.Handle);
}
// Shadow FBO.
_shadowRenderTarget = CreateRenderTarget((ShadowMapSize, _maxLightsPerScene),
new RenderTargetFormatParameters(_hasGLFloatFramebuffers ? RenderTargetColorFormat.RG32F : RenderTargetColorFormat.Rgba8, true),
new RenderTargetFormatParameters(
_hasGLFloatFramebuffers ? RenderTargetColorFormat.RG32F : RenderTargetColorFormat.Rgba8, true),
new TextureSampleParameters { WrapMode = TextureWrapMode.Repeat, Filter = true },
nameof(_shadowRenderTarget));
}

View File

@@ -415,9 +415,15 @@ namespace Robust.Client.Graphics.Clyde
case float f:
program.SetUniform(name, f);
break;
case float[] fArr:
program.SetUniform(name, fArr);
break;
case Vector2 vector2:
program.SetUniform(name, vector2);
break;
case Vector2[] vector2Arr:
program.SetUniform(name, vector2Arr);
break;
case Vector3 vector3:
program.SetUniform(name, vector3);
break;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@@ -395,12 +395,24 @@ namespace Robust.Client.Graphics.Clyde
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, float[] value)
{
var data = Parent._shaderInstances[Handle];
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, Vector2 value)
{
var data = Parent._shaderInstances[Handle];
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, Vector2[] value)
{
var data = Parent._shaderInstances[Handle];
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, Vector3 value)
{
var data = Parent._shaderInstances[Handle];

View File

@@ -14,6 +14,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Profiling;
using Robust.Shared.Timing;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
@@ -35,6 +36,7 @@ namespace Robust.Client.Graphics.Clyde
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ProfManager _prof = default!;
private GLUniformBuffer<ProjViewMatrices> ProjViewUBO = default!;
private GLUniformBuffer<UniformConstants> UniformConstantsUBO = default!;
@@ -356,7 +358,7 @@ namespace Robust.Client.Graphics.Clyde
}
[Conditional("DEBUG")]
private void SetupDebugCallback()
private unsafe void SetupDebugCallback()
{
if (!_hasGLKhrDebug)
{
@@ -367,19 +369,16 @@ namespace Robust.Client.Graphics.Clyde
GL.Enable(EnableCap.DebugOutput);
GL.Enable(EnableCap.DebugOutputSynchronous);
GCHandle.Alloc(_debugMessageCallbackInstance);
_debugMessageCallbackInstance ??= DebugMessageCallback;
// OpenTK seemed to have trouble marshalling the delegate so do it manually.
var procName = _isGLKhrDebugESExtension ? "glDebugMessageCallbackKHR" : "glDebugMessageCallback";
LoadGLProc(procName, out DebugMessageCallbackDelegate proc);
_debugMessageCallbackInstance = DebugMessageCallback;
var glDebugMessageCallback = (delegate* unmanaged[Stdcall] <nint, nint, void>) LoadGLProc(procName);
var funcPtr = Marshal.GetFunctionPointerForDelegate(_debugMessageCallbackInstance);
proc(funcPtr, new IntPtr(0x3005));
glDebugMessageCallback(funcPtr, new IntPtr(0x3005));
}
private delegate void DebugMessageCallbackDelegate(IntPtr funcPtr, IntPtr userParam);
private void DebugMessageCallback(DebugSource source, DebugType type, int id, DebugSeverity severity,
int length, IntPtr message, IntPtr userParam)
{

View File

@@ -412,10 +412,18 @@ namespace Robust.Client.Graphics.Clyde
{
}
private protected override void SetParameterImpl(string name, float[] value)
{
}
private protected override void SetParameterImpl(string name, Vector2 value)
{
}
private protected override void SetParameterImpl(string name, Vector2[] value)
{
}
private protected override void SetParameterImpl(string name, Vector3 value)
{
}

View File

@@ -195,11 +195,10 @@ namespace Robust.Client.Graphics.Clyde
private void BlitThreadDoSecondaryWindowBlit(WindowData window)
{
var rt = window.RenderTexture!;
if (Clyde._hasGLFenceSync)
{
// 0xFFFFFFFFFFFFFFFFUL is GL_TIMEOUT_IGNORED
var rt = window.Reg.RenderTarget;
var sync = rt.LastGLSync;
GL.WaitSync(sync, WaitSyncFlags.None, unchecked((long) 0xFFFFFFFFFFFFFFFFUL));
Clyde.CheckGlError();
@@ -222,6 +221,8 @@ namespace Robust.Client.Graphics.Clyde
Clyde._windowing!.GLMakeContextCurrent(reg.Reg);
Clyde._windowing.GLSwapInterval(0);
Clyde.SetupDebugCallback();
if (!Clyde._isGLES)
GL.Enable(EnableCap.FramebufferSrgb);

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using OpenToolkit.Graphics.OpenGL4;
@@ -234,6 +234,19 @@ namespace Robust.Client.Graphics.Clyde
GL.Uniform1(uniformId, single);
}
public void SetUniform(string uniformName, float[] singles)
{
var uniformId = GetUniform(uniformName);
GL.Uniform1(uniformId, singles.Length, singles);
_clyde.CheckGlError();
}
public void SetUniform(int uniformName, float[] singles)
{
var uniformId = GetUniform(uniformName);
GL.Uniform1(uniformId, singles.Length, singles);
}
public void SetUniform(string uniformName, in Matrix3 matrix)
{
var uniformId = GetUniform(uniformName);
@@ -375,6 +388,31 @@ namespace Robust.Client.Graphics.Clyde
}
}
public void SetUniform(string uniformName, Vector2[] vector)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, vector);
}
public void SetUniform(int uniformName, Vector2[] vector)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, vector);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, Vector2[] vectors)
{
unsafe
{
fixed (Vector2* ptr = &vectors[0])
{
GL.Uniform2(slot, vectors.Length, (float*)ptr);
_clyde.CheckGlError();
}
}
}
public void SetUniformTexture(string uniformName, TextureUnit textureUnit)
{
var uniformId = GetUniform(uniformName);

View File

@@ -56,28 +56,7 @@ namespace Robust.Client.Graphics
/// <param name="str">The text to draw.</param>
/// <param name="color">The color of text to draw.</param>
public Vector2 DrawString(Font font, Vector2 pos, string str, Color color)
{
var advanceTotal = Vector2.Zero;
var baseLine = new Vector2(pos.X, font.GetAscent(1) + pos.Y);
var lineHeight = font.GetLineHeight(1);
foreach (var rune in str.EnumerateRunes())
{
if (rune == new Rune('\n'))
{
baseLine.X = pos.X;
baseLine.Y += lineHeight;
advanceTotal.Y += lineHeight;
continue;
}
var advance = font.DrawChar(this, rune, baseLine, 1, color);
advanceTotal.X += advance;
baseLine += new Vector2(advance, 0);
}
return advanceTotal;
}
=> DrawString(font, pos, str, 1, color);
/// <summary>
/// Draw a simple string to the screen at the specified position.
@@ -94,6 +73,30 @@ namespace Robust.Client.Graphics
public Vector2 DrawString(Font font, Vector2 pos, string str)
=> DrawString(font, pos, str, Color.White);
public Vector2 DrawString(Font font, Vector2 pos, ReadOnlySpan<char> str, float scale, Color color)
{
var advanceTotal = Vector2.Zero;
var baseLine = new Vector2(pos.X, font.GetAscent(scale) + pos.Y);
var lineHeight = font.GetLineHeight(scale);
foreach (var rune in str.EnumerateRunes())
{
if (rune == new Rune('\n'))
{
baseLine.X = pos.X;
baseLine.Y += lineHeight;
advanceTotal.Y += lineHeight;
continue;
}
var advance = font.DrawChar(this, rune, baseLine, scale, color);
advanceTotal.X += advance;
baseLine += new Vector2(advance, 0);
}
return advanceTotal;
}
public abstract void DrawEntity(EntityUid entity, Vector2 position, Vector2 scale, Direction? overrideDirection);
}
}

View File

@@ -98,7 +98,7 @@ namespace Robust.Client.Graphics
handle = renderHandle.DrawingHandleWorld;
}
var args = new OverlayDrawArgs(currentSpace, vpControl, vp, handle, screenBox, worldBox, worldBounds);
var args = new OverlayDrawArgs(currentSpace, vpControl, vp, handle, screenBox, vp.Eye!.Position.MapId, worldBox, worldBounds);
Draw(args);
}

View File

@@ -1,6 +1,7 @@
using JetBrains.Annotations;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
@@ -38,6 +39,11 @@ namespace Robust.Client.Graphics
/// </summary>
public readonly UIBox2i ViewportBounds;
/// <summary>
/// <see cref="MapId"/> of the viewport's eye.
/// </summary>
public readonly MapId MapId;
/// <summary>
/// AABB enclosing the area visible in the viewport.
/// </summary>
@@ -57,6 +63,7 @@ namespace Robust.Client.Graphics
IClydeViewport viewport,
DrawingHandleBase drawingHandle,
in UIBox2i viewportBounds,
in MapId mapId,
in Box2 worldAabb,
in Box2Rotated worldBounds)
{
@@ -65,6 +72,7 @@ namespace Robust.Client.Graphics
Viewport = viewport;
DrawingHandle = drawingHandle;
ViewportBounds = viewportBounds;
MapId = mapId;
WorldAABB = worldAabb;
WorldBounds = worldBounds;
}

View File

@@ -135,36 +135,42 @@ namespace Robust.Client.Graphics
internal sealed class ShaderDataTypeFull
{
public ShaderDataTypeFull(ShaderDataType type, ShaderPrecisionQualifier prec)
public ShaderDataTypeFull(ShaderDataType type, ShaderPrecisionQualifier prec, int? count = null)
{
Type = type;
Precision = prec;
Count = count;
}
public ShaderDataType Type { get; }
public ShaderPrecisionQualifier Precision { get; }
public int? Count;
public bool IsArray => Count != null;
public string GetNativeType()
{
if (Precision == ShaderPrecisionQualifier.Low)
string? precision = Precision switch
{
return "lowp " + Type.GetNativeType();
}
else if (Precision == ShaderPrecisionQualifier.Medium)
{
return "mediump " + Type.GetNativeType();
}
else if (Precision == ShaderPrecisionQualifier.High)
{
return "highp " + Type.GetNativeType();
}
return Type.GetNativeType();
ShaderPrecisionQualifier.Low => "lowp ",
ShaderPrecisionQualifier.Medium => "mediump ",
ShaderPrecisionQualifier.High => "highp ",
_ => null,
};
return IsArray ? $"{precision}{Type.GetNativeType()}[{Count}]" : $"{precision}{Type.GetNativeType()}";
}
public bool TypePrecisionConsistent()
{
return Type.TypeHasPrecision() == (Precision != ShaderPrecisionQualifier.None);
}
public bool TypeCountConsistent()
{
return Count == null || Type.TypeSupportsArrays();
}
}
internal static class ShaderEnumExt
@@ -195,6 +201,7 @@ namespace Robust.Client.Graphics
{
return
(type == ShaderDataType.Float) ||
(type == ShaderDataType.Int) ||
(type == ShaderDataType.Vec2) ||
(type == ShaderDataType.Vec3) ||
(type == ShaderDataType.Vec4) ||
@@ -203,6 +210,14 @@ namespace Robust.Client.Graphics
(type == ShaderDataType.Mat4);
}
public static bool TypeSupportsArrays(this ShaderDataType type)
{
// TODO: add support for int, and vec3/4 arrays
return
(type == ShaderDataType.Float) ||
(type == ShaderDataType.Vec2);
}
[SuppressMessage("ReSharper", "StringLiteralTypo")]
private static readonly Dictionary<ShaderDataType, string> _nativeTypes = new()
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics
@@ -119,6 +119,13 @@ namespace Robust.Client.Graphics
SetParameterImpl(name, value);
}
public void SetParameter(string name, float[] value)
{
EnsureAlive();
EnsureMutable();
SetParameterImpl(name, value);
}
public void SetParameter(string name, Vector2 value)
{
EnsureAlive();
@@ -126,6 +133,13 @@ namespace Robust.Client.Graphics
SetParameterImpl(name, value);
}
public void SetParameter(string name, Vector2[] value)
{
EnsureAlive();
EnsureMutable();
SetParameterImpl(name, value);
}
public void SetParameter(string name, Vector3 value)
{
EnsureAlive();
@@ -244,7 +258,9 @@ namespace Robust.Client.Graphics
private protected abstract ShaderInstance DuplicateImpl();
private protected abstract void SetParameterImpl(string name, float value);
private protected abstract void SetParameterImpl(string name, float[] value);
private protected abstract void SetParameterImpl(string name, Vector2 value);
private protected abstract void SetParameterImpl(string name, Vector2[] value);
private protected abstract void SetParameterImpl(string name, Vector3 value);
private protected abstract void SetParameterImpl(string name, Vector4 value);
private protected abstract void SetParameterImpl(string name, Color value);

View File

@@ -504,17 +504,33 @@ namespace Robust.Client.Graphics
continue;
}
if (_shaderTypeMap.TryGetValue(wordType.Word, out var ret))
if (!_shaderTypeMap.TryGetValue(wordType.Word, out var ret))
throw new ShaderParseException("Expected type", wordType.Position);
var result = new ShaderDataTypeFull(ret, precision);
if (!result.TypePrecisionConsistent())
{
var result = new ShaderDataTypeFull(ret, precision);
if (!result.TypePrecisionConsistent())
{
throw new ShaderParseException($"Type {ret} cannot accept precision {precision}", wordType.Position);
}
return result;
throw new ShaderParseException($"Type {ret} cannot accept precision {precision}", wordType.Position);
}
throw new ShaderParseException("Expected type or precision", wordType.Position);
// is this type meant to be an array?
if (_peekToken() is TokenSymbol bracketOpen && bracketOpen.Symbol == Symbols.BracketOpen)
{
_takeToken();
if (_takeToken() is not TokenNumber number || !int.TryParse(number.Number, out var count))
throw new ShaderParseException($"Failed to parse array length", bracketOpen.Position);
if (_takeToken() is not TokenSymbol bracketClose || bracketClose.Symbol != Symbols.BracketClosed)
throw new ShaderParseException($"Array length definition missing closing bracket", number.Position);
result.Count = count;
// are arrays supported by this type?
if (!result.TypeCountConsistent())
throw new ShaderParseException($"Type {ret} does not support arrays", wordType.Position);
}
return result;
}
}

View File

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

View File

@@ -1,9 +1,19 @@
namespace Robust.Client
{
public interface IGameController
{
InitialLaunchState LaunchState { get; }
namespace Robust.Client;
public interface IGameController
{
InitialLaunchState LaunchState { get; }
void Shutdown(string? reason=null);
/// <summary>
/// Try to cause the launcher to either reconnect to the same server or connect to a new server.
/// *The engine will shutdown on success.*
/// Will throw an exception if contacting the launcher failed (success indicates it is now the launcher's responsibility).
/// To redial the same server, retrieve the server's address from `LaunchState.Ss14Address`.
/// </summary>
/// <param name="address">The server address, such as "ss14://localhost:1212/".</param>
/// <param name="text">Informational text on the cause of the reconnect. Empty or null gives a default reason.</param>
void Redial(string address, string? text = null);
}
void Shutdown(string? reason=null);
}
}

View File

@@ -497,7 +497,7 @@ namespace Robust.Client.Input
if (robustMapping.TryGet("binds", out var BaseKeyRegsNode))
{
var baseKeyRegs = serializationManager.ReadValueOrThrow<KeyBindingRegistration[]>(BaseKeyRegsNode);
var baseKeyRegs = serializationManager.Read<KeyBindingRegistration[]>(BaseKeyRegsNode);
foreach (var reg in baseKeyRegs)
{
@@ -526,7 +526,7 @@ namespace Robust.Client.Input
if (userData && robustMapping.TryGet("leaveEmpty", out var node))
{
var leaveEmpty = serializationManager.ReadValueOrThrow<BoundKeyFunction[]>(node);
var leaveEmpty = serializationManager.Read<BoundKeyFunction[]>(node);
if (leaveEmpty.Length > 0)
{

View File

@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.Physics
{
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
{
public bool EnableDebug
{
get => _enableDebug;
set
{
if (_enableDebug == value) return;
_enableDebug = value;
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (_enableDebug)
{
var overlay = new GridSplitNodeOverlay(EntityManager, IoCManager.Resolve<IMapManager>(), this);
overlayManager.AddOverlay(overlay);
RaiseNetworkEvent(new RequestGridNodesMessage());
}
else
{
overlayManager.RemoveOverlay<GridSplitNodeOverlay>();
RaiseNetworkEvent(new StopGridNodesMessage());
}
}
}
private bool _enableDebug = false;
private readonly Dictionary<EntityUid, Dictionary<Vector2i, List<List<Vector2i>>>> _nodes = new();
private readonly Dictionary<EntityUid, List<(Vector2, Vector2)>> _connections = new();
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<ChunkSplitDebugMessage>(OnDebugMessage);
}
public override void Shutdown()
{
base.Shutdown();
_nodes.Clear();
_connections.Clear();
}
private void OnDebugMessage(ChunkSplitDebugMessage ev)
{
if (!_enableDebug) return;
_nodes[ev.Grid] = ev.Nodes;
_connections[ev.Grid] = ev.Connections;
}
private sealed class GridSplitNodeOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private IEntityManager _entManager;
private IMapManager _mapManager;
private GridFixtureSystem _system;
public GridSplitNodeOverlay(IEntityManager entManager, IMapManager mapManager, GridFixtureSystem system)
{
_entManager = entManager;
_mapManager = mapManager;
_system = system;
}
protected internal override void Draw(in OverlayDrawArgs args)
{
var worldHandle = args.WorldHandle;
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
foreach (var iGrid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
{
// May not have received nodes yet.
if (!_system._nodes.TryGetValue(iGrid.GridEntityId, out var nodes)) continue;
var gridXform = xformQuery.GetComponent(iGrid.GridEntityId);
worldHandle.SetTransform(gridXform.WorldMatrix);
var grid = (MapGrid)iGrid;
var chunkEnumerator = grid.GetMapChunks(args.WorldBounds);
while (chunkEnumerator.MoveNext(out var chunk))
{
if (!nodes.TryGetValue(chunk.Indices, out var chunkNodes)) continue;
for (var i = 0; i < chunkNodes.Count; i++)
{
var group = chunkNodes[i];
var offset = chunk.Indices * chunk.ChunkSize;
var color = GetColor(chunk, i);
foreach (var index in group)
{
worldHandle.DrawRect(new Box2(offset + index, offset + index + 1).Enlarged(-0.1f), color);
}
}
}
var connections = _system._connections[iGrid.GridEntityId];
foreach (var (start, end) in connections)
{
worldHandle.DrawLine(start, end, Color.Aquamarine);
}
}
}
private Color GetColor(MapChunk chunk, int index)
{
// Just want something that doesn't give similar indices at 0,0 but is also deterministic.
// Add an offset to yIndex so we at least have some colour that isn't grey at 0,0
var actualIndex = chunk.Indices.X * 20 + (chunk.Indices.Y + 20) * 35 + index * 50;
var red = (byte) (actualIndex % 255);
var green = (byte) (actualIndex * 20 % 255);
var blue = (byte) (actualIndex * 30 % 255);
return new Color(red, green, blue, 85);
}
}
}
}

View File

@@ -0,0 +1,18 @@
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Physics;
public sealed class GridSplitVisualCommand : IConsoleCommand
{
public string Command => SharedGridFixtureSystem.ShowGridNodesCommand;
public string Description => "Shows the nodes for grid split purposes";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GridFixtureSystem>();
system.EnableDebug ^= true;
shell.WriteLine($"Toggled gridsplit node visuals");
}
}

View File

@@ -49,7 +49,7 @@ namespace Robust.Client.Physics
// Also need to suss out having the client build the island anyway and just... not solving it?
foreach (var body in AwakeBodies)
{
if (body.Island || body.LinearVelocity.Length > _linSleepTolerance / 2f || body.AngularVelocity > _angSleepTolerance / 2f) continue;
if (body.LinearVelocity.Length > _linSleepTolerance / 2f || body.AngularVelocity > _angSleepTolerance / 2f) continue;
body.SleepTime += frameTime;
if (body.SleepTime > _timeToSleep)
{

View File

@@ -418,7 +418,7 @@ namespace Robust.Client.Placement
if (!IsActive || !Eraser) return;
if (Hijack != null && Hijack.HijackDeletion(entity)) return;
var msg = NetworkManager.CreateNetMessage<MsgPlacement>();
var msg = new MsgPlacement();
msg.PlaceType = PlacementManagerMessage.RequestEntRemove;
msg.EntityUid = entity;
NetworkManager.ClientSendMessage(msg);
@@ -426,7 +426,7 @@ namespace Robust.Client.Placement
public void HandleRectDeletion(EntityCoordinates start, Box2 rect)
{
var msg = NetworkManager.CreateNetMessage<MsgPlacement>();
var msg = new MsgPlacement();
msg.PlaceType = PlacementManagerMessage.RequestRectRemove;
msg.EntityCoordinates = new EntityCoordinates(StartPoint.EntityId, rect.BottomLeft);
msg.RectSize = rect.Size;
@@ -748,7 +748,7 @@ namespace Robust.Client.Placement
_pendingTileChanges.Add(tuple);
}
var message = NetworkManager.CreateNetMessage<MsgPlacement>();
var message = new MsgPlacement();
message.PlaceType = PlacementManagerMessage.RequestPlacement;
message.Align = CurrentMode.ModeName;

View File

@@ -170,10 +170,10 @@ namespace Robust.Client.Placement
/// </summary>
public TileRef GetTileRef(EntityCoordinates coordinates)
{
var mapCoords = coordinates.ToMap(pManager.EntityManager);
var gridId = coordinates.GetGridId(pManager.EntityManager);
var gridUid = coordinates.GetGridUid(pManager.EntityManager);
return gridId.IsValid() ? pManager.MapManager.GetGrid(gridId).GetTileRef(MouseCoords)
: new TileRef(mapCoords.MapId, gridId,
: new TileRef(gridId, gridUid.GetValueOrDefault(),
MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager), Tile.Empty);
}

View File

@@ -89,7 +89,7 @@ namespace Robust.Client.Player
{
LocalPlayer = new LocalPlayer();
var msgList = _network.CreateNetMessage<MsgPlayerListReq>();
var msgList = new MsgPlayerListReq();
// message is empty
_network.ClientSendMessage(msgList);
}

View File

@@ -0,0 +1,169 @@
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
namespace Robust.Client.Profiling;
public sealed class LiveProfileViewControl : Control
{
[Dependency] private readonly ProfManager _profManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
public int MaxDepth { get; set; } = 2;
public LiveProfileViewControl()
{
IoCManager.InjectDependencies(this);
}
protected internal override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (!_profManager.IsEnabled)
return;
var font = _resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf").MakeDefault();
var baseLine = new Vector2(0, font.GetAscent(UIScale));
ref readonly var buffer = ref _profManager.Buffer;
ref readonly var index = ref buffer.Index(buffer.IndexWriteOffset - 1);
DrawData drawData = default;
drawData.Font = font;
drawData.Buffer = buffer;
drawData.Index = index;
drawData.Handle = handle;
var i = index.StartPos;
DrawCmds(in drawData, ref i, ref baseLine, 0);
}
private void DrawCmds(
in DrawData data,
ref long i,
ref Vector2 baseline,
int depth)
{
for (; i < data.Index.EndPos; i++)
{
ref var cmd = ref data.Buffer.Log(i);
DrawCmd(in data, ref i, ref baseline, depth, in cmd);
}
}
private void DrawCmdSample(
in DrawData data,
ref Vector2 baseline,
in ProfLogValue value)
{
DrawSample(in data, baseline, value.StringId, value.Value);
baseline += (0, data.Font.GetLineHeight(UIScale));
}
private void DrawSample(
in DrawData data,
Vector2 baseline,
int stringId,
in ProfValue value)
{
var cmdString = _profManager.GetString(stringId);
baseline += data.Handle.DrawString(data.Font, baseline, cmdString, UIScale, Color.White);
baseline += data.Handle.DrawString(data.Font, baseline, ": ", UIScale, Color.White);
var str = value.Type switch
{
ProfValueType.TimeAllocSample =>
$"{value.TimeAllocSample.Time * 1000:N2} ms, {value.TimeAllocSample.Alloc} B",
ProfValueType.Int32 => value.Int32.ToString(),
ProfValueType.Int64 => value.Int64.ToString(),
_ => "???"
};
data.Handle.DrawString(data.Font, baseline, str, UIScale, Color.White);
}
private void DrawCmd(
in DrawData data,
ref long i,
ref Vector2 baseline,
int depth,
in ProfLog log)
{
switch (log.Type)
{
case ProfLogType.Value:
DrawCmdSample(in data, ref baseline, in log.Value);
break;
case ProfLogType.GroupStart:
DrawEnterGroup(in data, ref i, ref baseline, depth);
break;
}
}
private void DrawEnterGroup(
in DrawData data,
ref long i,
ref Vector2 baseline,
int depth)
{
depth += 1;
var indentSize = 12 * UIScale;
var startBaseline = baseline;
baseline += (indentSize, data.Font.GetLineHeight(UIScale));
if (depth > MaxDepth)
{
var startIdx = i;
// Skip contents of this group.
for (; i < data.Index.EndPos; i++)
{
ref var cmd = ref data.Buffer.Log(i);
if (cmd.Type != ProfLogType.GroupEnd)
continue;
if (cmd.GroupEnd.StartIndex <= startIdx)
break;
}
}
else
{
i += 1;
for (; i < data.Index.EndPos; i++)
{
ref var cmd = ref data.Buffer.Log(i);
if (cmd.Type == ProfLogType.GroupEnd)
break;
DrawCmd(in data, ref i, ref baseline, depth, in cmd);
}
}
// Gone through entire list = unmatched begin/end pair
if (i == data.Index.EndPos)
return;
ref var cmdEnd = ref data.Buffer.Log(i).GroupEnd;
baseline -= (indentSize, 0);
DrawSample(in data, startBaseline, cmdEnd.StringId, cmdEnd.Value);
}
private struct DrawData
{
public ProfBuffer Buffer;
public ProfIndex Index;
public Font Font;
public DrawingHandleScreen Handle;
}
}

View File

@@ -0,0 +1,136 @@
using System;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Profiling;
using Robust.Shared.Timing;
using Robust.Shared.Utility.Collections;
namespace Robust.Client.Profiling;
/// <summary>
/// Manager for managing recording and snapshots of profiles, consuming shared <see cref="ProfManager"/>
/// </summary>
public sealed class ProfViewManager
{
[Dependency]
private readonly ProfManager _profManager = default!;
public ValueList<Snapshot> Snapshots;
public event Action? SnapshotsUpdated;
private int _nextId = 1;
public void Snap()
{
var buffer = _profManager.Buffer.Snapshot();
var lowestIndex = CalcLowestIndex(buffer);
var highestIndex = buffer.IndexWriteOffset - 1;
long start;
long end;
if (lowestIndex == buffer.IndexWriteOffset)
{
// Literally not a single valid index. Time to give up.
start = 0;
end = 0;
}
else
{
(start, end) = CalcBufferRange(lowestIndex, highestIndex, buffer);
}
Snapshots.Add(new Snapshot
{
StartFrame = start,
EndFrame = end,
LowestValidIndex = lowestIndex,
Buffer = buffer,
Identifier = _nextId++
});
SnapshotsUpdated?.Invoke();
}
public void DeleteSnapshot(Snapshot snapshot)
{
if (Snapshots.Remove(snapshot))
SnapshotsUpdated?.Invoke();
}
private static long CalcLowestIndex(in ProfBuffer buffer)
{
// Any commands further back than this index are invalid.
// Indices in their range are thus also invalid and to be ignored.
var cmdBufferStartValid = buffer.LogWriteOffset - buffer.LogBuffer.LongLength;
// Just scan over the index ring buffer in reverse to find the earliest index that's still valid.
var min = buffer.IndexWriteOffset - buffer.IndexBuffer.LongLength;
for (var i = buffer.IndexWriteOffset - 1; i >= min; i--)
{
ref var index = ref buffer.Index(i);
if (index.StartPos < cmdBufferStartValid)
return i + 1;
}
return min;
}
private (long start, long end) CalcBufferRange(long lowestIndex, long highestIndex, in ProfBuffer buffer)
{
return (GetFrameOfIndex(lowestIndex, buffer), GetFrameOfIndex(highestIndex, buffer));
}
private long GetFrameOfIndex(long index, in ProfBuffer buffer)
{
ref var indexRef = ref buffer.Index(index);
ref var firstLog = ref buffer.Log(indexRef.StartPos);
if (firstLog.Type != ProfLogType.Value ||
_profManager.GetString(firstLog.Value.StringId) != GameLoop.ProfTextStartFrame)
throw new InvalidOperationException("Unable to find start frame");
if (firstLog.Value.Value.Type != ProfValueType.Int64)
throw new InvalidOperationException("Start frame has incorrect value type");
return firstLog.Value.Value.Int64;
}
public long GetIndexOfFrame(long frame, Snapshot snapshot)
{
for (var i = snapshot.Buffer.IndexWriteOffset - 1; i >= snapshot.LowestValidIndex; i--)
{
if (GetFrameOfIndex(i, snapshot.Buffer) == frame)
return i;
}
return 0;
}
public sealed class Snapshot
{
public int Identifier;
public long LowestValidIndex;
public long StartFrame;
public long EndFrame;
public ProfBuffer Buffer;
public long FrameCount => EndFrame - StartFrame + 1;
}
}
public sealed class ProfSnapshotCommand : IConsoleCommand
{
// ReSharper disable once StringLiteralTypo
public string Command => "profsnap";
public string Description => "Make a profiling snapshot";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var profViewManager = IoCManager.Resolve<ProfViewManager>();
profViewManager.Snap();
}
}

View File

@@ -57,7 +57,7 @@ namespace Robust.Client.Prototypes
#if !FULL_RELEASE
var sw = Stopwatch.StartNew();
var msg = _netManager.CreateNetMessage<MsgReloadPrototypes>();
var msg = new MsgReloadPrototypes();
msg.Paths = _reloadQueue.ToArray();
_netManager.ClientSendMessage(msg);

View File

@@ -22,6 +22,8 @@ namespace Robust.Client.ResourceManagement
{
private static readonly float[] OneArray = {1};
public override ResourcePath? Fallback => new("/Textures/error.rsi");
private static readonly JsonSerializerOptions SerializerOptions =
new JsonSerializerOptions(JsonSerializerDefaults.Web)
{

View File

@@ -1,10 +1,10 @@
using System;
using Robust.Client.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Manager.Result;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Validation;
@@ -16,26 +16,40 @@ namespace Robust.Client.Serialization
[TypeSerializer]
public sealed class AppearanceVisualizerSerializer : ITypeSerializer<AppearanceVisualizer, MappingDataNode>
{
public DeserializationResult Read(ISerializationManager serializationManager, MappingDataNode node,
public AppearanceVisualizer Read(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies,
bool skipHook,
ISerializationContext? context = null)
ISerializationContext? context = null, AppearanceVisualizer? value = null)
{
Type? type = null;
if (!node.TryGet("type", out var typeNode))
throw new InvalidMappingException("No type specified for AppearanceVisualizer!");
{
if (value == null)
throw new InvalidMappingException("No type specified for AppearanceVisualizer!");
if (typeNode is not ValueDataNode typeValueDataNode)
throw new InvalidMappingException("Type node not a value node for AppearanceVisualizer!");
type = value.GetType();
}
else
{
if (typeNode is not ValueDataNode typeValueDataNode)
throw new InvalidMappingException("Type node not a value node for AppearanceVisualizer!");
var type = IoCManager.Resolve<IReflectionManager>()
.YamlTypeTagLookup(typeof(AppearanceVisualizer), typeValueDataNode.Value);
if (type == null)
throw new InvalidMappingException(
$"Invalid type {typeValueDataNode.Value} specified for AppearanceVisualizer!");
type = IoCManager.Resolve<IReflectionManager>()
.YamlTypeTagLookup(typeof(AppearanceVisualizer), typeValueDataNode.Value);
if (type == null)
throw new InvalidMappingException(
$"Invalid type {typeValueDataNode.Value} specified for AppearanceVisualizer!");
if(value != null && !type.IsInstanceOfType(value))
{
throw new InvalidMappingException(
$"Specified Type does not match type of provided Value for AppearanceVisualizer! (TypeOfValue: {value.GetType()}, ProvidedValue: {type})");
}
}
var newNode = node.Copy();
newNode.Remove("type");
return serializationManager.Read(type, newNode, context, skipHook);
return (AppearanceVisualizer) serializationManager.Read(type, newNode, context, skipHook, value)!;
}
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node,

View File

@@ -847,13 +847,17 @@ namespace Robust.Client.UserInterface
OnResized?.Invoke();
}
internal void DoFrameUpdate(FrameEventArgs args)
internal int DoFrameUpdateRecursive(FrameEventArgs args)
{
var total = 1;
FrameUpdate(args);
foreach (var child in Children)
{
child.DoFrameUpdate(args);
total += child.DoFrameUpdateRecursive(args);
}
return total;
}
/// <summary>

View File

@@ -0,0 +1,395 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Log;
namespace Robust.Client.UserInterface.Controls;
// condensed version of the original ColorSlider set
public sealed class ColorSelectorSliders : Control
{
public Color Color
{
get => _currentColor;
set
{
_updating = true;
_currentColor = value;
Update();
_updating = false;
}
}
public ColorSelectorType SelectorType
{
get => _currentType;
set
{
_updating = true;
_currentType = value;
UpdateType();
Update();
_updating = false;
}
}
public bool IsAlphaVisible
{
get => _isAlphaVisible;
set
{
_isAlphaVisible = value;
_alphaSliderBox.Visible = _isAlphaVisible;
}
}
public Action<Color>? OnColorChanged;
private bool _updating = false;
private Color _currentColor = Color.White;
private ColorSelectorType _currentType = ColorSelectorType.Rgb;
private bool _isAlphaVisible = false;
private ColorableSlider _topColorSlider;
private ColorableSlider _middleColorSlider;
private ColorableSlider _bottomColorSlider;
private Slider _alphaSlider;
private BoxContainer _alphaSliderBox = new();
private FloatSpinBox _topInputBox;
private FloatSpinBox _middleInputBox;
private FloatSpinBox _bottomInputBox;
private FloatSpinBox _alphaInputBox;
private Label _topSliderLabel = new();
private Label _middleSliderLabel = new();
private Label _bottomSliderLabel = new();
private Label _alphaSliderLabel = new();
private OptionButton _typeSelector;
private List<ColorSelectorType> _types = new();
public ColorSelectorSliders()
{
_topColorSlider = new ColorableSlider
{
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
MaxValue = 1.0f
};
_middleColorSlider = new ColorableSlider
{
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
MaxValue = 1.0f
};
_bottomColorSlider = new ColorableSlider
{
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
MaxValue = 1.0f
};
_alphaSlider = new Slider
{
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
MaxValue = 1.0f,
};
_topColorSlider.OnValueChanged += _ => { OnColorSet(); };
_middleColorSlider.OnValueChanged += _ => { OnColorSet(); };
_bottomColorSlider.OnValueChanged += _ => { OnColorSet(); };
_alphaSlider.OnValueChanged += _ => { OnColorSet(); };
_topInputBox = new FloatSpinBox(1f, 2)
{
IsValid = value => IsSpinBoxValid(value, ColorSliderOrder.Top)
};
_middleInputBox = new FloatSpinBox(1f, 2)
{
IsValid = value => IsSpinBoxValid(value, ColorSliderOrder.Middle)
};
_bottomInputBox = new FloatSpinBox(1f, 2)
{
IsValid = value => IsSpinBoxValid(value, ColorSliderOrder.Bottom)
};
_alphaInputBox = new FloatSpinBox(1f, 2)
{
IsValid = value => IsSpinBoxValid(value, ColorSliderOrder.Alpha)
};
_topInputBox.OnValueChanged += value =>
{
_topColorSlider.Value = value.Value / GetColorValueDivisor(ColorSliderOrder.Top);
};
_middleInputBox.OnValueChanged += value =>
{
_middleColorSlider.Value = value.Value / GetColorValueDivisor(ColorSliderOrder.Middle);
};
_bottomInputBox.OnValueChanged += value =>
{
_bottomColorSlider.Value = value.Value / GetColorValueDivisor(ColorSliderOrder.Bottom);
};
_alphaInputBox.OnValueChanged += value =>
{
_alphaSlider.Value = value.Value / GetColorValueDivisor(ColorSliderOrder.Alpha);
};
_alphaSliderLabel.Text = Loc.GetString("color-selector-sliders-alpha");
_typeSelector = new OptionButton();
foreach (var ty in Enum.GetValues<ColorSelectorType>())
{
_typeSelector.AddItem(Loc.GetString($"color-selector-sliders-{ty.ToString().ToLower()}"));
_types.Add(ty);
}
_typeSelector.OnItemSelected += args =>
{
SelectorType = _types[args.Id];
_typeSelector.Select(args.Id);
};
// TODO: Maybe some engine widgets could be laid out in XAML?
var rootBox = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical
};
AddChild(rootBox);
var headerBox = new BoxContainer();
rootBox.AddChild(headerBox);
headerBox.AddChild(_typeSelector);
var bodyBox = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Vertical
};
// pita
var topSliderBox = new BoxContainer();
topSliderBox.AddChild(_topSliderLabel);
topSliderBox.AddChild(_topColorSlider);
topSliderBox.AddChild(_topInputBox);
var middleSliderBox = new BoxContainer();
middleSliderBox.AddChild(_middleSliderLabel);
middleSliderBox.AddChild(_middleColorSlider);
middleSliderBox.AddChild(_middleInputBox);
var bottomSliderBox = new BoxContainer();
bottomSliderBox.AddChild(_bottomSliderLabel);
bottomSliderBox.AddChild(_bottomColorSlider);
bottomSliderBox.AddChild(_bottomInputBox);
_alphaSliderBox.Visible = IsAlphaVisible;
_alphaSliderBox.AddChild(_alphaSliderLabel);
_alphaSliderBox.AddChild(_alphaSlider);
_alphaSliderBox.AddChild(_alphaInputBox);
bodyBox.AddChild(topSliderBox);
bodyBox.AddChild(middleSliderBox);
bodyBox.AddChild(bottomSliderBox);
bodyBox.AddChild(_alphaSliderBox);
rootBox.AddChild(bodyBox);
_updating = true;
UpdateType();
Update();
_updating = false;
}
private void UpdateType()
{
(string topLabel, string middleLabel, string bottomLabel) labels = GetSliderLabels();
_topSliderLabel.Text = labels.topLabel;
_middleSliderLabel.Text = labels.middleLabel;
_bottomSliderLabel.Text = labels.bottomLabel;
}
private void Update()
{
_topColorSlider.SetColor(_currentColor);
_middleColorSlider.SetColor(_currentColor);
_bottomColorSlider.SetColor(_currentColor);
switch (SelectorType)
{
case ColorSelectorType.Rgb:
_topColorSlider.Value = Color.R;
_middleColorSlider.Value = Color.G;
_bottomColorSlider.Value = Color.B;
_topInputBox.Value = Color.R * 255.0f;
_middleInputBox.Value = Color.G * 255.0f;
_bottomInputBox.Value = Color.B * 255.0f;
break;
case ColorSelectorType.Hsv:
Vector4 color = Color.ToHsv(Color);
// dumb workaround because the formula for
// HSV calculation results in a negative
// number in any value past 300 degrees
if (color.X > 0)
{
_topColorSlider.Value = color.X;
_topInputBox.Value = color.X * 360.0f;
}
else
{
_topInputBox.Value = _topColorSlider.Value * 360.0f;
}
_middleColorSlider.Value = color.Y;
_bottomColorSlider.Value = color.Z;
_middleInputBox.Value = color.Y * 100.0f;
_bottomInputBox.Value = color.Z * 100.0f;
break;
}
_alphaSlider.Value = Color.A;
_alphaInputBox.Value = Color.A * 100.0f;
}
private bool IsSpinBoxValid(float value, ColorSliderOrder ordering)
{
if (value < 0)
{
return false;
}
if (ordering == ColorSliderOrder.Alpha)
{
return value <= 100.0f;
}
switch (SelectorType)
{
case ColorSelectorType.Rgb:
return value <= byte.MaxValue;
case ColorSelectorType.Hsv:
switch (ordering)
{
case ColorSliderOrder.Top:
return value <= 360.0f;
default:
return value <= 100.0f;
}
}
return false;
}
private (string, string, string) GetSliderLabels()
{
switch (SelectorType)
{
case ColorSelectorType.Rgb:
return (
Loc.GetString("color-selector-sliders-red"),
Loc.GetString("color-selector-sliders-green"),
Loc.GetString("color-selector-sliders-blue")
);
case ColorSelectorType.Hsv:
return (
Loc.GetString("color-selector-sliders-hue"),
Loc.GetString("color-selector-sliders-saturation"),
Loc.GetString("color-selector-sliders-value")
);
}
return ("ERR", "ERR", "ERR");
}
private float GetColorValueDivisor(ColorSliderOrder order)
{
if (order == ColorSliderOrder.Alpha)
{
return 100.0f;
}
switch (SelectorType)
{
case ColorSelectorType.Rgb:
return 255.0f;
case ColorSelectorType.Hsv:
switch (order)
{
case ColorSliderOrder.Top:
return 360.0f;
default:
return 100.0f;
}
}
return 0.0f;
}
private void OnColorSet()
{
// stack overflow otherwise due to value sets
if (_updating)
{
return;
}
switch (SelectorType)
{
case ColorSelectorType.Rgb:
Color rgbColor = new Color(_topColorSlider.Value, _middleColorSlider.Value, _bottomColorSlider.Value, _alphaSlider.Value);
_currentColor = rgbColor;
Update();
OnColorChanged!(rgbColor);
break;
case ColorSelectorType.Hsv:
Color hsvColor = Color.FromHsv(new Vector4(_topColorSlider.Value, _middleColorSlider.Value, _bottomColorSlider.Value, _alphaSlider.Value));
_currentColor = hsvColor;
Update();
OnColorChanged!(hsvColor);
break;
}
}
private enum ColorSliderOrder
{
Top,
Middle,
Bottom,
Alpha
}
public enum ColorSelectorType
{
Rgb,
Hsv,
}
}

View File

@@ -0,0 +1,81 @@
using Robust.Client.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Log;
namespace Robust.Client.UserInterface.Controls;
public sealed class ColorableSlider : Slider
{
public const string StylePropertyFillWhite = "fillWhite"; // needs to be filled with white
public const string StylePropertyBackgroundWhite = "backgroundWhite"; // also needs to be filled with white
public Color Color { get; private set; } = Color.White;
public PartSelector Part
{
get => _currentPartSelector;
set
{
_currentPartSelector = value;
UpdateStyleBoxes();
}
}
private PartSelector _currentPartSelector = PartSelector.Background;
public void SetColor(Color color)
{
Color = color;
switch (Part)
{
case PartSelector.Fill:
_fillPanel.Modulate = Color;
break;
case PartSelector.Background:
_backgroundPanel.Modulate = Color;
break;
}
}
protected override void UpdateStyleBoxes()
{
StyleBox? GetStyleBox(string name)
{
if (TryGetStyleProperty<StyleBox>(name, out var box))
{
return box;
}
return null;
}
string backBox = StylePropertyBackground;
string fillBox = StylePropertyFill;
switch (Part)
{
case PartSelector.Fill:
fillBox = StylePropertyFillWhite;
_fillPanel.Modulate = Color;
break;
case PartSelector.Background:
backBox = StylePropertyBackgroundWhite;
_fillPanel.Modulate = Color.Transparent; // make this transparent
_backgroundPanel.Modulate = Color;
break;
}
_backgroundPanel.PanelOverride = BackgroundStyleBoxOverride ?? GetStyleBox(backBox);
_foregroundPanel.PanelOverride = ForegroundStyleBoxOverride ?? GetStyleBox(StylePropertyForeground);
_fillPanel.PanelOverride = FillStyleBoxOverride ?? GetStyleBox(fillBox);
_grabber.PanelOverride = GrabberStyleBoxOverride ?? GetStyleBox(StylePropertyGrabber);
}
public enum PartSelector
{
Fill,
Background
}
}

View File

@@ -65,18 +65,22 @@ namespace Robust.Client.UserInterface.Controls
get => _text;
set
{
if (value == null)
{
value = "";
}
// Save cursor position or -1 for end
var cursorTarget = CursorPosition == _text.Length ? -1 : CursorPosition;
if (!SetText(value))
if (!InternalSetText(value))
{
return;
}
_cursorPosition = 0;
_selectionStart = 0;
var clamped = MathHelper.Clamp(cursorTarget == -1 ? _text.Length : cursorTarget, 0, _text.Length);
while (clamped < _text.Length && !Rune.TryGetRuneAt(_text, clamped, out _))
{
clamped++;
}
_cursorPosition = clamped;
_selectionStart = _cursorPosition;
_updatePseudoClass();
}
}
@@ -203,7 +207,7 @@ namespace Robust.Client.UserInterface.Controls
var lower = SelectionLower;
var newContents = Text[..lower] + text + Text[SelectionUpper..];
if (!SetText(newContents))
if (!InternalSetText(newContents))
{
return;
}
@@ -216,7 +220,7 @@ namespace Robust.Client.UserInterface.Controls
/// <remarks>
/// Does not fix cursor positions, those will have to be adjusted manually.
/// </remarks>>
protected bool SetText(string newText)
private bool InternalSetText(string newText)
{
if (IsValid != null && !IsValid(newText))
{

View File

@@ -13,10 +13,10 @@ namespace Robust.Client.UserInterface.Controls
public const string StylePropertyFill = "fill";
public const string StylePropertyGrabber = "grabber";
private readonly PanelContainer _foregroundPanel;
private readonly PanelContainer _backgroundPanel;
private readonly PanelContainer _fillPanel;
private readonly PanelContainer _grabber;
protected readonly PanelContainer _foregroundPanel;
protected readonly PanelContainer _backgroundPanel;
protected readonly PanelContainer _fillPanel;
protected readonly PanelContainer _grabber;
private bool _grabbed;
@@ -169,7 +169,7 @@ namespace Robust.Client.UserInterface.Controls
UpdateStyleBoxes();
}
private void UpdateStyleBoxes()
protected virtual void UpdateStyleBoxes()
{
StyleBox? GetStyleBox(string name)
{
@@ -182,7 +182,7 @@ namespace Robust.Client.UserInterface.Controls
}
_backgroundPanel.PanelOverride = BackgroundStyleBoxOverride ?? GetStyleBox(StylePropertyBackground);
_foregroundPanel.PanelOverride = BackgroundStyleBoxOverride ?? GetStyleBox(StylePropertyForeground);
_foregroundPanel.PanelOverride = ForegroundStyleBoxOverride ?? GetStyleBox(StylePropertyForeground);
_fillPanel.PanelOverride = FillStyleBoxOverride ?? GetStyleBox(StylePropertyFill);
_grabber.PanelOverride = GrabberStyleBoxOverride ?? GetStyleBox(StylePropertyGrabber);
}

View File

@@ -73,7 +73,7 @@ namespace Robust.Client.UserInterface.CustomControls
{
mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
mouseWorldMap.Position);
tile = new TileRef(mouseWorldMap.MapId, GridId.Invalid,
tile = new TileRef(GridId.Invalid, EntityUid.Invalid,
mouseGridPos.ToVector2i(_entityManager, _mapManager), Tile.Empty);
}

View File

@@ -2,6 +2,7 @@
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.Profiling;
using Robust.Client.State;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
@@ -72,6 +73,8 @@ namespace Robust.Client.UserInterface.CustomControls
{
HorizontalAlignment = HAlignment.Left
});
AddChild(new LiveProfileViewControl());
}
}
}

View File

@@ -228,7 +228,7 @@ namespace Robust.Client.UserInterface.CustomControls
foreach (var prototype in prototypeManager.EnumeratePrototypes<EntityPrototype>())
{
if (prototype.Abstract)
if (prototype.NoSpawn || prototype.Abstract)
{
continue;
}

View File

@@ -86,20 +86,7 @@ namespace Robust.Client.UserInterface.CustomControls
var x = FrameWidth * UIScale * (TrackedFrames - 1 - i);
var rect = new UIBox2(x, PixelHeight - (frameHeight * ratio), x + FrameWidth * UIScale, PixelHeight);
Color color;
if (frameTime > 1f / (target / 2 - 1))
{
color = Color.Red;
}
else if (frameTime > 1f / (target - 1))
{
color = Color.Yellow;
}
else
{
color = Color.Lime;
}
handle.DrawRect(rect, color);
handle.DrawRect(rect, FrameTimeColor(frameTime, target));
var gc = _gcMarkers[currentFrameIndex];
if (gc)
@@ -112,5 +99,16 @@ namespace Robust.Client.UserInterface.CustomControls
}
}
}
internal static Color FrameTimeColor(float time, int target)
{
if (time > 1f / (target / 2 - 1))
return Color.Red;
if (time > 1f / (target - 1))
return Color.Yellow;
return Color.Lime;
}
}
}

View File

@@ -4,5 +4,6 @@
<TabContainer>
<DebugConsole Name="DebugConsole" />
<DevWindowTabUI Name="UI" />
<DevWindowTabPerf Name="Perf" />
</TabContainer>
</Control>

View File

@@ -23,6 +23,7 @@ namespace Robust.Client.UserInterface
TabContainer.SetTabTitle(DebugConsole, "Debug Console");
TabContainer.SetTabTitle(UI, "User Interface");
TabContainer.SetTabTitle(Perf, "Profiling");
}
}
@@ -45,9 +46,9 @@ namespace Robust.Client.UserInterface
var window = clyde.CreateWindow(new WindowCreateParameters
{
Maximized = true,
//Maximized = true,
Title = "Robust Debug Window",
Monitor = monitor,
//Monitor = monitor,
});
var root = IoCManager.Resolve<IUserInterfaceManager>().CreateWindowRoot(window);
window.DisposeOnClose = true;

View File

@@ -0,0 +1,41 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Robust.Client.UserInterface.DevWindowTabPerf">
<SplitContainer Orientation="Horizontal">
<!-- Snapshot list and settings overview -->
<BoxContainer Orientation="Vertical" Margin="2" MinWidth="250">
<Label Text="Snapshot list" Margin="0 0 0 4" />
<BoxContainer Orientation="Horizontal" Margin="0 0 0 4">
<Button Text="Snap" Name="SnapButton" />
</BoxContainer>
<ScrollContainer HScrollEnabled="False" VScrollEnabled="True"
VerticalExpand="True">
<BoxContainer Orientation="Vertical" Name="SnapList" />
</ScrollContainer>
<!-- Settings overview -->
<ProfSettings />
</BoxContainer>
<!-- Main right panel -->
<Control Margin="2">
<BoxContainer Orientation="Vertical">
<Label Text="Snapshot view"></Label>
<Control VerticalExpand="True">
<Label Text="Make a snapshot on the left to get started"
HorizontalAlignment="Center" VerticalAlignment="Center"
Name="RightPanelPlaceholder" />
<BoxContainer Orientation="Vertical" Name="RightPanel" Visible="False">
<BoxContainer Orientation="Horizontal">
<Label Name="LabelTopText" HorizontalExpand="True" />
<Button Name="ButtonDeleteSnap" Text="Delete snapshot" />
</BoxContainer>
<ProfGraphView Name="GraphView" MinHeight="96" MouseFilter="Stop" Margin="0 4" />
<ProfTree Name="Tree" VerticalExpand="True" />
</BoxContainer>
</Control>
</BoxContainer>
</Control>
</SplitContainer>
</Control>

View File

@@ -0,0 +1,115 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Profiling;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
namespace Robust.Client.UserInterface;
[GenerateTypedNameReferences]
public sealed partial class DevWindowTabPerf : Control
{
[Dependency] private readonly ProfViewManager _profViewMgr = default!;
private ProfViewManager.Snapshot? _currentSnapshot;
public DevWindowTabPerf()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
SnapButton.OnPressed += SnapButtonOnOnPressed;
ButtonDeleteSnap.OnPressed += ButtonDeleteSnapOnOnPressed;
GraphView.FrameSelected += SelectFrame;
}
private void SelectFrame(long obj)
{
if (_currentSnapshot == null)
return;
GraphView.HighlightFrame = obj;
Tree.Frame = obj;
Tree.RebuildTree();
}
protected override void EnteredTree()
{
_profViewMgr.SnapshotsUpdated += ProfViewMgrOnSnapshotsUpdated;
UpdateSnapshotList();
}
protected override void ExitedTree()
{
_profViewMgr.SnapshotsUpdated -= ProfViewMgrOnSnapshotsUpdated;
}
private void ProfViewMgrOnSnapshotsUpdated()
{
UpdateSnapshotList();
}
private void SnapButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
{
_profViewMgr.Snap();
SnapshotSelected(_profViewMgr.Snapshots[^1]);
}
private void ButtonDeleteSnapOnOnPressed(BaseButton.ButtonEventArgs obj)
{
if (_currentSnapshot == null)
return;
_profViewMgr.DeleteSnapshot(_currentSnapshot);
_currentSnapshot = null;
UpdateRightPanel();
}
private void UpdateSnapshotList()
{
SnapList.RemoveAllChildren();
for (var i = _profViewMgr.Snapshots.Count - 1; i >= 0; i--)
{
var snap = _profViewMgr.Snapshots[i];
var button = new Button
{
Text = $"{snap.Identifier}: {snap.StartFrame} - {snap.EndFrame}",
ToggleMode = true
};
if (snap == _currentSnapshot)
button.Pressed = true;
button.OnPressed += _ => SnapshotSelected(snap);
SnapList.AddChild(button);
}
}
private void SnapshotSelected(ProfViewManager.Snapshot snapshot)
{
_currentSnapshot = snapshot;
UpdateSnapshotList();
UpdateRightPanel();
SelectFrame(_currentSnapshot.EndFrame);
Tree.RebuildTree();
}
private void UpdateRightPanel()
{
RightPanel.Visible = _currentSnapshot != null;
RightPanelPlaceholder.Visible = _currentSnapshot == null;
if (_currentSnapshot == null)
return;
LabelTopText.Text =
$"Snapshot: {_currentSnapshot.StartFrame} - {_currentSnapshot.EndFrame} Frame count: {_currentSnapshot.FrameCount}";
GraphView.LoadSnapshot(_currentSnapshot);
Tree.LoadSnapshot(_currentSnapshot);
}
}

View File

@@ -0,0 +1,32 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface;
internal sealed class ExpanderArrow : Control
{
public bool Rotated { get; set; }
protected internal override void Draw(DrawingHandleScreen handle)
{
Span<Vector2> triangle = stackalloc Vector2[3];
triangle[0] = (0.8f, 0f);
triangle[1] = (-0.8f, 1f);
triangle[2] = (-0.8f, -1f);
foreach (ref var t in triangle)
{
if (Rotated)
{
t = Angle.FromDegrees(90).RotateVec(t);
}
t /= 2;
t += 0.5f;
t *= PixelSize;
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, triangle, Color.White);
}
}

View File

@@ -0,0 +1,15 @@
using Robust.Client.Graphics;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface;
internal sealed class ProfAltBackground : Control
{
public bool IsAltBackground { get; set; }
protected internal override void Draw(DrawingHandleScreen handle)
{
if (IsAltBackground)
handle.DrawRect(PixelSizeBox, new Color(0, 0, 0, 128));
}
}

View File

@@ -0,0 +1,123 @@
using System;
using Robust.Client.Graphics;
using Robust.Client.Profiling;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
namespace Robust.Client.UserInterface;
internal sealed class ProfGraphView : Control
{
private const int HeightFps = 15;
private const float MaxHeightMs = 1 / (float) HeightFps;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private ProfViewManager.Snapshot? _snapshot;
public event Action<long>? FrameSelected;
private bool _dragging;
public long HighlightFrame;
public ProfGraphView()
{
RectClipContent = true;
IoCManager.InjectDependencies(this);
}
public void LoadSnapshot(ProfViewManager.Snapshot snapshot)
{
_snapshot = snapshot;
}
protected internal override void KeyBindDown(GUIBoundKeyEventArgs args)
{
if (args.Function != EngineKeyFunctions.UIClick)
return;
_dragging = true;
args.Handle();
DoFrameSelect(args.RelativePosition);
}
protected internal override void MouseMove(GUIMouseMoveEventArgs args)
{
if (!_dragging)
return;
DoFrameSelect(args.RelativePosition);
}
protected internal override void KeyBindUp(GUIBoundKeyEventArgs args)
{
if (args.Function != EngineKeyFunctions.UIClick)
return;
args.Handle();
_dragging = false;
}
private void DoFrameSelect(Vector2 pos)
{
if (_snapshot == null)
return;
var trackedFrameCount = _snapshot.EndFrame - _snapshot.StartFrame + 1;
var frame = MathHelper.Clamp((int)(pos.X / Width * trackedFrameCount), 0, trackedFrameCount-1);
FrameSelected?.Invoke(frame + _snapshot.StartFrame);
}
protected internal override void Draw(DrawingHandleScreen handle)
{
if (_snapshot == null)
return;
var targetFps = _cfg.GetCVar(CVars.DebugTargetFps);
var trackedFrameCount = _snapshot.EndFrame - _snapshot.StartFrame + 1;
ref var buffer = ref _snapshot.Buffer;
var barWidth = PixelWidth / (float) trackedFrameCount;
var x = PixelWidth - barWidth;
var controlHeight = PixelHeight;
var frame = _snapshot.EndFrame;
for (var i = _snapshot.Buffer.IndexWriteOffset - 1; i >= _snapshot.LowestValidIndex; i--)
{
var valueSeconds = GetFrameTime(buffer, buffer.Index(i)).Time;
var height = controlHeight * (valueSeconds / MaxHeightMs);
var rect = UIBox2.FromDimensions(x, (controlHeight - height), barWidth, height);
var color = HighlightFrame == frame ? Color.Pink : FrameGraph.FrameTimeColor(valueSeconds, targetFps);
handle.DrawRect(rect, color);
x -= barWidth;
frame -= 1;
}
}
internal static TimeAndAllocSample GetFrameTime(in ProfBuffer buffer, in ProfIndex index)
{
var endPos = index.EndPos;
ref var endGroup = ref buffer.Log(endPos - 1);
if (endGroup.Type != ProfLogType.GroupEnd ||
endGroup.GroupEnd.Value.Type != ProfValueType.TimeAllocSample)
return default;
return endGroup.GroupEnd.Value.TimeAllocSample;
}
}

View File

@@ -0,0 +1,18 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Robust.Client.UserInterface.ProfSettings">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" SeparationOverride="8">
<Label Text="enabled" HorizontalExpand="True" />
<CheckBox Name="SettingEnabled" Disabled="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" SeparationOverride="8">
<Label Text="buffer_size" HorizontalExpand="True" />
<Label Name="SettingBufferSize" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" SeparationOverride="8">
<Label Text="index_size" HorizontalExpand="True" />
<Label Name="SettingIndexSize" />
</BoxContainer>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,52 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Profiling;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface;
[GenerateTypedNameReferences]
internal sealed partial class ProfSettings : Control
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public ProfSettings()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
}
protected override void EnteredTree()
{
_cfg.OnValueChanged(CVars.ProfEnabled, CfgEnabledChanged, true);
_cfg.OnValueChanged(CVars.ProfBufferSize, CfgBufferSizeChanged, true);
_cfg.OnValueChanged(CVars.ProfIndexSize, CfgIndexSizeChanged, true);
}
protected override void ExitedTree()
{
_cfg.UnsubValueChanged(CVars.ProfEnabled, CfgEnabledChanged);
_cfg.UnsubValueChanged(CVars.ProfBufferSize, CfgBufferSizeChanged);
_cfg.UnsubValueChanged(CVars.ProfIndexSize, CfgIndexSizeChanged);
}
private void CfgEnabledChanged(bool obj)
{
SettingEnabled.Pressed = obj;
}
private unsafe void CfgBufferSizeChanged(int obj)
{
var bytes = BufferHelpers.FittingPowerOfTwo(obj) * sizeof(ProfLog);
SettingBufferSize.Text = $"{obj} ({ByteHelpers.FormatBytes(bytes)})";
}
private unsafe void CfgIndexSizeChanged(int obj)
{
var bytes = BufferHelpers.FittingPowerOfTwo(obj) * sizeof(ProfIndex);
SettingIndexSize.Text = $"{obj} ({ByteHelpers.FormatBytes(bytes)})";
}
}

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