Compare commits

..

475 Commits

Author SHA1 Message Date
Visne
2c6941e73b Version: 0.40.3.2 2022-08-15 02:49:34 +02:00
wrexbe
5e5883cb88 Fix map schema (#3138) 2022-08-15 02:40:56 +02:00
ElectroJr
02c504445e Version: 0.40.3.1 2022-08-14 19:21:42 -04:00
wrexbe
4d5075a792 Update mapfile_validators.py (#3136) 2022-08-15 09:20:12 +10:00
ElectroJr
25212cbb4d Version: 0.40.3.0 2022-08-14 17:20:31 -04:00
Leon Friedrich
c56cc24804 Update RGA validator (#3134) 2022-08-15 07:17:36 +10:00
Paul
cf9250f25d Version: 0.40.2.0 2022-08-14 11:19:59 +02:00
Paul
a105e02131 fixes mapfile schema 2022-08-14 11:18:43 +02:00
Paul
cebdca87df Version: 0.40.1.0 2022-08-14 01:35:56 +02:00
Paul Ritter
bbabee3949 fixes struct instantiation (#3129) 2022-08-14 01:34:51 +02:00
Paul
f16d41704b Version: 0.40.0.0 2022-08-13 23:13:17 +02:00
Paul Ritter
6f557ac821 cleans up copy and removes ipopulatedefaultvalues (#3085)
Co-authored-by: Paul <ritter.paul1@gmail.com>
2022-08-13 23:12:04 +02:00
wrexbe
ba70a7c95e Fix ResourcePath serialize copy (#3127) 2022-08-13 22:19:56 +02:00
Leon Friedrich
f3796ee69d Fix nested container insert bug (#3062) 2022-08-13 10:30:07 +10:00
metalgearsloth
b2a71e5ef8 Version: 0.39.1.0 2022-08-11 09:47:26 +10:00
metalgearsloth
c253130c4a Add private ctor for ResourcePath (#3118) 2022-08-11 09:46:21 +10:00
Paul Ritter
f03826b5ec defines yaml schemas for robust generic attribution files and mapfiles (#3089)
Co-authored-by: ike709 <ike709@users.noreply.github.com>
Co-authored-by: Paul <ritter.paul1@gmail.com>
2022-08-10 12:48:24 +02:00
metalgearsloth
69cf93a425 Version: 0.39.0.0 2022-08-10 17:04:56 +10:00
metalgearsloth
a4ea7fed6a Make tiledefinitions use spritespecifier (#3113) 2022-08-10 17:04:12 +10:00
metalgearsloth
7046152ec8 Version: 0.38.1.0 2022-08-10 13:06:28 +10:00
Leon Friedrich
f6f1f76980 Fix nullable yaml validation (#3106) 2022-08-10 01:00:47 +02:00
Rinkashikachi
a98a214fa3 Fix issue with Fluents ??? for empty suffix. (#3114) 2022-08-10 01:00:21 +02:00
metalgearsloth
faadb70de7 Overlay shader cleanup (#3112) 2022-08-10 00:52:32 +02:00
Pieter-Jan Briers
7a6d6c83b1 CVar.CONFIDENTIAL flag.
Hides CVar values from completion results.
2022-08-10 00:40:30 +02:00
Julian Giebel
9e32eb46e5 Update ImageSharp and fix incompatibilities (#3092) 2022-08-09 13:15:55 +02:00
Leon Friedrich
bea91d119e Fix CustomHashSetSerializer bug (#3087)
* Fix CustomHashSetSerializer bug

* Create a copy of source if target is null

* null !
2022-08-08 18:05:23 +02:00
metalgearsloth
9ec725d848 Version: 0.38.0.0 2022-08-08 11:43:52 +10:00
Leon Friedrich
790d523ebd Remove string keys from appearance component (#3086) 2022-08-08 11:43:17 +10:00
metalgearsloth
147ca286ea Version: 0.37.0.0 2022-08-07 13:56:27 +10:00
Leon Friedrich
bc25f5d727 Allow entities to be spawned directly in null-space (#3107) 2022-08-07 13:54:06 +10:00
Pieter-Jan Briers
c65740eba6 Version: 0.36.0.0 2022-08-07 00:31:55 +02:00
Pieter-Jan Briers
59e6a797f1 Revert "Move BUI logic to the UI system (#3102)"
This reverts commit 53c496fe27.
2022-08-07 00:25:20 +02:00
Pieter-Jan Briers
663d901e6b Version: 0.35.0.0 2022-08-07 00:18:03 +02:00
Pieter-Jan Briers
585126bf52 Fix race conditions with handshake messages.
MsgMapStrServerHandshake was being sent as unordered, meaning that it  could actually arrive on the client before the handshake-finished messages. Whoops.
2022-08-07 00:14:55 +02:00
metalgearsloth
e27ed228ad Revert "More BUI changes (#3104)" (#3109) 2022-08-06 16:58:45 +10:00
metalgearsloth
ef35187624 Version: 0.34.0.0 2022-08-06 14:58:38 +10:00
metalgearsloth
1cf31fbea6 Non-generic GetEntityQuery (#3103) 2022-08-06 14:52:32 +10:00
Leon Friedrich
c7a3c43a4f More BUI changes (#3104) 2022-08-06 14:51:23 +10:00
ElectroJr
01b55494e8 Version: 0.33.0.1 2022-08-04 18:31:43 -04:00
ElectroJr
be9ab89b29 Fix serializer copy-ref error 2022-08-04 18:30:44 -04:00
ElectroJr
d4c8631587 Version: 0.33.0.0 2022-08-04 18:18:50 -04:00
Paul Ritter
3d43fc510e refactors copy api to use ref (#3093) 2022-08-05 08:17:02 +10:00
wrexbe
931c38a97d Allow ignore missing to specify postfix (#3035) 2022-08-05 07:37:14 +10:00
Leon Friedrich
53c496fe27 Move BUI logic to the UI system (#3102) 2022-08-05 07:18:22 +10:00
metalgearsloth
c696ac1c73 Version: 0.32.1.0 2022-08-04 12:48:27 +10:00
Leon Friedrich
57e5ceef1a BUI range check changes (#3099) 2022-08-04 09:00:38 +10:00
Leon Friedrich
b5279f3f89 Fix awake-physics map change bug (#3088) 2022-08-04 08:58:50 +10:00
metalgearsloth
fc424309c1 Don't IoC resolve in appearancevisualizerserializer (#3100) 2022-08-03 09:24:37 +02:00
Leon Friedrich
19aac6c461 Wake physics on container removal (#3098) 2022-08-02 01:34:13 +10:00
Paul
2decf31a78 Version: 0.32.0.0 2022-08-01 14:29:28 +02:00
Paul
f33ba17148 Version: 0.34.0.0 2022-08-01 14:24:45 +02:00
Paul Ritter
752d4dfe8b prototype composition (#3070)
* prototype composition

* oops

* better

* fixes build

* should fix

* misc fixes & circle test

* moar circle checks

* adds tests

Co-authored-by: Paul <ritter.paul1@gmail.com>
2022-08-01 14:22:46 +02:00
Júlio César Ueti
92d40cded3 Adds a check to HandleContainerRemoved to handle different canCollide states. (#3072)
* Checks the canCollide of the entity and passes it to SetCanCollide, defaults to true

* Remove added new line

* Requested changes
2022-08-01 01:45:02 +10:00
Leon Friedrich
11f8bf794a More joint fixes (#3091)
* More joint fixes

* Punctuation
2022-08-01 00:09:31 +10:00
Vera Aguilera Puerto
0604464dcc Bandaid MIDI crash involving malicious/malformed MIDI events.
Temporary fix until I have enough time to fix this proper.
2022-07-31 01:13:36 +02:00
ElectroJr
9f25c14707 Version: 0.31.1.0 2022-07-29 12:19:01 -04:00
Leon Friedrich
2c50e4f219 Pitch scale variation fix (#3090) 2022-07-30 02:16:39 +10:00
metalgearsloth
e97d8b9b14 Use scale with placementmanager (#3058)
Bandaid until it gets refactored.
2022-07-29 13:30:41 +10:00
metalgearsloth
f129bd45c5 Fix linear velocity cap rounding (#3051)
Here we use it as a hard cap.
2022-07-29 13:30:05 +10:00
Jacob Tong
f812b307b5 Toggle button updates (#3018)
* Change toggle button to only trigger on UIClick

* Add tests for toggle buttons
2022-07-29 13:28:59 +10:00
ElectroJr
436cb98d53 Version: 0.31.0.0 2022-07-28 22:16:31 -04:00
Leon Friedrich
1c1ced0f8a Joint changes (#3068) 2022-07-29 12:14:21 +10:00
Leon Friedrich
97015569f6 AudioSystem Rejig (#3038) 2022-07-29 12:14:05 +10:00
Leon Friedrich
ee2d266d6b Validate sprite specifier states (#3075) 2022-07-28 09:17:40 +10:00
Leon Friedrich
786ecdb8b1 More window open overrides (#3073) 2022-07-26 06:56:28 -07:00
Pieter-Jan Briers
488260cc64 Windows 11 immersive dark mode.
Dark window titles
2022-07-25 23:53:06 +02:00
metalgearsloth
6ee96873a4 Version: 0.30.0.1 2022-07-25 20:15:16 +10:00
metalgearsloth
2220f3e346 Fix broadphase desync (#3078) 2022-07-25 20:09:07 +10:00
metalgearsloth
137723357c Version: 0.30.0.0 2022-07-25 15:30:50 +10:00
metalgearsloth
db3f3e5b32 Allow maps to be saved (#3055)
* Allow maps to be saved

* I hate myself

* fix?

* Fix maybe?
2022-07-25 15:30:20 +10:00
Leon Friedrich
fc10adfb03 Fix a WorldSpaceEntities overlay bug (#3074) 2022-07-24 19:10:07 -07:00
Pieter-Jan Briers
812809bbfd Fix ILocValue.Format getting invalid LocContext 2022-07-24 02:28:58 +02:00
Pieter-Jan Briers
ec70abe928 Analyzer to make Task`1.Result scary. (#3069) 2022-07-23 12:33:55 +02:00
Vera Aguilera Puerto
0ec6995542 MIDI refactor part 1: MidiRendererState (#3015) 2022-07-23 11:56:33 +02:00
Pieter-Jan Briers
1c28782d5d Fix some grid-related warnings 2022-07-21 17:35:09 +02:00
Leon Friedrich
45472fa342 Make unknown component errors more helpful (#3063) 2022-07-21 16:54:04 +02:00
20kdc
18a5335373 Provide a generic way to expose tags on the status endpoint (#2769)
* Provide a generic way to expose tags on the status endpoint for Launcher to presumably later consume.

* Switch to a JSON array for server tags
2022-07-21 16:28:56 +02:00
metalgearsloth
86590ceb1f Fix audio merge artifacts (#3061) 2022-07-20 09:58:25 +10:00
metalgearsloth
e518abd2be Version: 0.29.1.0 2022-07-19 21:31:12 +10:00
metalgearsloth
13ae59dbf6 Revert "Don't defer moveevents anymore (#3048)" (#3060) 2022-07-19 21:30:41 +10:00
20kdc
0ded41ebe1 Audio occlusion parallelization (#2776)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2022-07-19 19:40:59 +10:00
metalgearsloth
3eb295e3ef Version: 0.29.0.0 2022-07-19 18:37:35 +10:00
Leon Friedrich
1782792d30 Fix cross-grid/map container inserts (#3054) 2022-07-19 18:37:05 +10:00
metalgearsloth
d5c0776278 Don't defer moveevents anymore (#3048) 2022-07-19 18:36:45 +10:00
Pieter-Jan Briers
0822b41155 NetManager cleanup. (#3056)
Just removed a bunch of dead code related to message dispatch and instantiation.
2022-07-19 17:44:43 +10:00
Leon Friedrich
7a81c7f64a Misc Shader changes (#2974) 2022-07-19 00:32:58 +02:00
Leon Friedrich
a925b7f84d Add lookup range assert (#3049)
Co-authored-by: Kara <lunarautomaton6@gmail.com>
2022-07-17 06:25:08 +10:00
Leon Friedrich
557de01532 Dirty PVS chunk when visibility mask changes (#3045)
* Dirty PVS chunk when visibility mask changes

* fix tests?

* aaaaaa

* maybe sorta finally fix tests

* directly return vis mask,

Also defaults to 1 only if visibility component does not exist.

* make sure everything has first bit set
2022-07-17 06:23:35 +10:00
Leon Friedrich
a38cbc7188 Make recursive delete detach parents before children (#3008) 2022-07-17 06:05:10 +10:00
metalgearsloth
5146b1b3c6 Version: 0.28.6.0 2022-07-16 13:50:50 +10:00
metalgearsloth
0e97e3000f Add robust method for no lerp (#3040)
* Add robust method for no lerp

Also updated rotation lerping.

* a

* a

* Fix sleeping while I'm here + context

* I hate contexts I hate contexts
2022-07-16 13:50:18 +10:00
metalgearsloth
0c4a05dcbf Version: 0.28.5.1 2022-07-15 14:27:26 +10:00
Leon Friedrich
565fa9f046 Preserve crossgrid joints (#3050) 2022-07-15 11:40:18 +10:00
Pieter-Jan Briers
2783e89d1f SDL2 windowing backend (#2979)
* SDL2 windowing prototype.

* More stuff idk
2022-07-15 00:07:30 +10:00
metalgearsloth
d9d08dcbe3 Version: 0.28.5.0 2022-07-14 20:25:30 +10:00
metalgearsloth
6abeedae97 Clear crossmap joints properly (#3046) 2022-07-14 20:24:17 +10:00
metalgearsloth
7bc1eaabfd findgridsintersecting + metadata performance (#3047) 2022-07-14 20:23:49 +10:00
Paul Ritter
526997639b validates generic args of typeserializers (#3044)
* validates generic args

* whodunnit grrr im angry

Co-authored-by: Paul <ritter.paul1@gmail.com>
2022-07-14 10:35:38 +02:00
Leon Friedrich
9dc030c764 Better rich text error logging (#3036) 2022-07-13 01:57:21 +10:00
Pieter-Jan Briers
35308975db Client con host improvements:
* Don't disable access check outside FULL_RELEASE.
  * Content allows all commands on +HOST now
* Client-side completions are filtered for perms and sorted.
2022-07-11 23:23:12 +02:00
Pieter-Jan Briers
7649c120e9 Do not show forbidden commands in completion results. 2022-07-11 23:23:12 +02:00
Kara
610f6d88af Version: 0.28.4.0 2022-07-09 02:54:46 -07:00
metalgearsloth
0fb5b59eba Make command line config file not exception if not found (#3030) 2022-07-09 02:52:06 -07:00
Pieter-Jan Briers
77b1fd933c Fix MapLoaderTest having a nullable error in Rider. 2022-07-08 22:38:30 +02:00
Leon Friedrich
4ec019078d Add enum and custom hash set serializers (#2973) 2022-07-08 07:25:23 +10:00
metalgearsloth
fe022d4100 Version: 0.28.3.0 2022-07-06 18:52:02 +10:00
Paul Ritter
1d4eec4855 make serv3 null reading sane and yaml-complaint (#3027) 2022-07-06 14:17:10 +10:00
metalgearsloth
84fceb587b Make loadbp take degrees (#3024) 2022-07-06 14:16:57 +10:00
Paul Ritter
a4ad98519e serv3 now writes proper null values (oops) (#3026) 2022-07-06 00:23:13 +10:00
metalgearsloth
8f55a21cc4 Fix lsgrid (#3025)
* Fix lsgrid

Often worldpos wouldn't show due to formatting.

* Also MapId
2022-07-05 23:39:23 +10:00
metalgearsloth
4e40184c4a Version: 0.28.2.1 2022-07-03 14:52:03 +10:00
metalgearsloth
8bea8237f8 Fix physicsmap recursive changes (#3014)
* Fix physicsmap recursive changes

* Fix recursion
2022-07-03 14:49:32 +10:00
Leon Friedrich
00c4d68db8 Fix server shutdown error (#3011) 2022-07-02 14:21:16 +10:00
metalgearsloth
93e26fd88a Add test for recursive broadphase updates (#3010) 2022-07-02 14:10:57 +10:00
Pieter-Jan Briers
312b879b06 Fix connection timeout always being 30000f (#3013) 2022-07-01 19:43:53 -07:00
metalgearsloth
852d1874db Add test that detachparenttonull mapid is recursive (#3009) 2022-07-01 15:07:25 +10:00
Leon Friedrich
9e14a7e0d3 Lookup errors (#3007) 2022-07-01 13:46:26 +10:00
metalgearsloth
5700969945 Version: 0.28.2.0 2022-07-01 02:31:42 +10:00
metalgearsloth
6feb8b69ee Bump cfg log level to info (#2996) 2022-07-01 02:04:58 +10:00
keronshb
1ab14fa6b7 Moves fixture check in physics (#3004)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2022-07-01 02:04:24 +10:00
Leon Friedrich
445b13791e Lookup & dynamic tree changes (#3006) 2022-06-30 12:37:05 +10:00
metalgearsloth
5605149a63 Fix scale visuals (#3003)
Should be the last time
2022-06-29 22:40:13 +10:00
metalgearsloth
162b069de3 Version: 0.28.1.1 2022-06-29 19:18:15 +10:00
metalgearsloth
1c65686ee0 Fix spinbox overridevalue (#3002) 2022-06-29 19:17:03 +10:00
Pieter-Jan Briers
278755d938 Warn on startup if bad glibc version is detected. (#2998)
Yay.
2022-06-29 15:11:07 +10:00
Leon Friedrich
64b356a9bf Prevent dirty-on-init (#2999)
* prevent dirty-on-init

* Fix test
2022-06-29 15:10:51 +10:00
metalgearsloth
9681dc705e Version: 0.28.1.0 2022-06-29 13:21:06 +10:00
Leon Friedrich
fa2b0233c3 ECS detach to null (#2997)
* ECS detach to null

* fix crash
2022-06-29 13:17:58 +10:00
wrexbe
392e2fefca Fix entity manager shutdown (#2945) 2022-06-29 09:20:51 +10:00
TekuNut
47c56ffcf4 Fix crash in ReflectionManager (#2987) 2022-06-29 03:35:28 +10:00
metalgearsloth
dd602fd6d0 Version: 0.28.0.0 2022-06-28 22:56:26 +10:00
Leon Friedrich
2059d37c8f Remove implicit grid conversion (#2968)
* Remove implicit grid conversion

* fix map test

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2022-06-28 22:55:44 +10:00
metalgearsloth
6b73b765b2 Remove grid logs (#2994)
I added 2 of these and they aren't needed for me anymore.
2022-06-28 17:53:26 +10:00
metalgearsloth
fb32b55584 Sleep bodies when collision turned off (#2993)
May fix them getting through.
2022-06-28 17:53:13 +10:00
metalgearsloth
045aebfc9e Version: 0.27.1.2 2022-06-28 13:01:26 +10:00
metalgearsloth
449418b1d4 Fix cross-map contacts (#2990) 2022-06-28 13:00:51 +10:00
metalgearsloth
06cf2208f4 Bandaid findpairs issue (#2988) 2022-06-28 12:48:40 +10:00
TekuNut
ffbfdfe0b2 Fix client crash on sprite layer state change (#2984) 2022-06-27 09:18:21 +10:00
metalgearsloth
d7caa93cf5 Version: 0.27.1.1 2022-06-26 15:24:18 +10:00
metalgearsloth
0665781dfe Revert parent change (#2982) 2022-06-25 15:48:35 +10:00
metalgearsloth
a91413f1a6 Version: 0.27.1.0 2022-06-24 21:16:04 +10:00
Kara
6b9eeac228 More CompletionOption helpers (#2980) 2022-06-24 21:15:23 +10:00
wrexbe
c86e4e1ab9 Fix BuilderToMemory truncating (#2975) 2022-06-24 11:29:01 +02:00
wrexbe
269d229c2d Fix hotreload (#2976) 2022-06-23 10:40:14 +02:00
metalgearsloth
7e86065c67 Version: 0.27.0.2 2022-06-23 17:54:44 +10:00
metalgearsloth
dcb0c2eb92 Make GetEntitiesIntersecting(uid) use its bounds (#2978)
Was only using worldPos before.
2022-06-23 17:54:23 +10:00
metalgearsloth
fd667a67e7 Log deleted entities for physics (#2977)
Just crashed server and I'm too lazy to try to repro
2022-06-23 17:27:06 +10:00
metalgearsloth
bf966ee49b Version: 0.27.0.1 2022-06-23 14:38:58 +10:00
Leon Friedrich
2f75b74f1e Fix client disconnect crash (#2949) 2022-06-23 14:29:50 +10:00
metalgearsloth
a05b864952 Make transform handlestate set coordinates and not attachparent (#2965)
This makes the EntParentChangedMessage have the new position when it's issued.
2022-06-23 14:29:12 +10:00
Leon Friedrich
2d96e0cda1 "Fix" another client connection crash (#2950)
* Fix another client connection crash

* Add length info to error.

Co-authored-by: Electro <no>
2022-06-23 14:28:39 +10:00
metalgearsloth
cd1d6ee598 Version: 0.27.0.0 2022-06-23 12:13:07 +10:00
metalgearsloth
09d7ad9d62 Don't run client physics in frameupdate (#2953) 2022-06-23 12:12:54 +10:00
Kara
0c9c1366b0 Allow sprites to override container occlusion (#2971) 2022-06-22 16:07:09 -07:00
metalgearsloth
8470cf43b1 Version: 0.26.0.0 2022-06-22 09:53:15 +10:00
metalgearsloth
62a24a320c Make raiselocalevent not broadcast by default (#2972)
* Make raiselocalevent not broadcast by default

* I forgor to commit
2022-06-22 09:52:50 +10:00
Leon Friedrich
578066511f Maybe fix a placement manager crash (#2969) 2022-06-21 12:04:48 +02:00
metalgearsloth
2821492885 Version: 0.25.0.1 2022-06-20 20:51:55 +10:00
metalgearsloth
3806481f81 Fix broadphase crash (#2966) 2022-06-20 20:49:14 +10:00
metalgearsloth
e610b8a286 Hotfix recursive physics contacts (#2964) 2022-06-20 20:45:09 +10:00
ElectroJr
d283043a56 Version: 0.25.0.0 2022-06-19 20:10:37 -04:00
Leon Friedrich
4171b3ec4e Make Grid Uid Nullable (#2942)
* Make Grid Uid Nullable

* Fix recursion

* fix detach to null

* change obsolete text
2022-06-20 10:08:23 +10:00
wrexbe
f90872b085 Various test changes (#2930) 2022-06-20 10:03:17 +10:00
Leon Friedrich
077faebfc5 Fix some grid traversal misprediction (#2932)
* Fix some grid traversal misprediction

* a

* b

* c

* Revert "b"

This reverts commit ca64919a70.
2022-06-20 07:31:11 +10:00
metalgearsloth
526b37804e Expose GetLocalTilesIntersecting methods (#2961) 2022-06-20 07:27:20 +10:00
Leon Friedrich
b8cb9e6b1d Add Overlay.BeforeDraw() (#2958) 2022-06-20 06:28:09 +10:00
metalgearsloth
0dfd71c990 Version: 0.24.0.1 2022-06-17 13:57:44 +10:00
metalgearsloth
18232e946b Make broadphase updates recursive (#2959) 2022-06-17 13:56:52 +10:00
metalgearsloth
c06f3f003c Early out settiles for empty lists (#2957) 2022-06-17 07:10:11 +10:00
metalgearsloth
72cf2f6334 Version: 0.24.0.0 2022-06-16 15:34:32 +10:00
metalgearsloth
e0e47ad545 Add simple raycast method (#2937) 2022-06-16 15:33:35 +10:00
metalgearsloth
70157265b8 Fix prototype icon error textures (#2954) 2022-06-16 04:45:03 +10:00
wrexbe
1e5068f057 Make fixture init event order explicit (#2946)
* Make fixture init event order explicit

* No more physics init event

* No more physics init event

* Fixing some stuff
2022-06-14 19:25:40 +10:00
Leon Friedrich
2bdea420ae Misc rendering changes (#2948)
* Misc rendering changes

* More tiny changes
2022-06-14 19:24:27 +10:00
Leon Friedrich
be906faf42 Cache layer RSI state (#2947) 2022-06-13 17:49:28 +10:00
ElectroJr
a03eecaa36 Version: 0.23.0.0 2022-06-12 19:48:57 -04:00
keronshb
bc12a0af87 Reorders the signature for sound (#2928) 2022-06-13 09:45:26 +10:00
Leon Friedrich
d347641d00 Update save map command to show a success message. (#2944) 2022-06-13 09:45:04 +10:00
wrexbe
221dbf945e Fix localizing color (#2943) 2022-06-13 09:03:26 +10:00
Leon Friedrich
24c900aeae Small CheckFaceEyeVis() performance increase (#2940) 2022-06-13 08:36:35 +10:00
wrexbe
76aaca6260 Expose RemoveComponentDeferred (#2887) 2022-06-12 21:41:42 +02:00
metalgearsloth
d3519d799a Fix grid splitting (#2941) 2022-06-13 05:21:56 +10:00
Pieter-Jan Briers
1ea18b9448 Move analyzer tests over to separate project.
Safe to say the build system and Rider do not like the double-project-reference shenanigans going on in Robust.UnitTesting.
2022-06-12 12:35:57 +02:00
Pieter-Jan Briers
1e7956eaa8 Remove MarshalHelper
Not necessary anymore since .NET Core
2022-06-12 11:30:30 +02:00
metalgearsloth
7655aec413 Version: 0.22.0.0 2022-06-12 11:02:17 +10:00
Pieter-Jan Briers
a36a10c814 Fix deadlock on client shutdown.
This was an existing bug, but due to me moving more cleanup logic to game thread shutdown the window of opportunity for it to trigger just got much greater.
2022-06-11 16:28:14 +02:00
Pieter-Jan Briers
26cdae6877 cvar command shows completion hint showing cvar value.
Like in source!
2022-06-11 13:24:15 +02:00
Pieter-Jan Briers
c96eae620c Fix GetRegisteredCVars including unregistered CVars. 2022-06-11 13:23:35 +02:00
Pieter-Jan Briers
9591d16aed Implement res:// protocol for WebView, MIME type handling. 2022-06-10 17:09:46 +02:00
Pieter-Jan Briers
05bb361eb2 CEF cookie fixes (#2934)
* Trying to get res:// and usr:// cookies to wrok

* Update CefGlue

* Bump CEF

* Seal types in WebView

Fixes warnings

* Move most of client cleanup to game thread.

This used to run in the windowing thread which broke CEF shutdown, most notably cookie saving.

* "flushcookies" command for CEF.

* Remove unecessary res:// code.

* Fix tests

* More request handler fixes.

* Good thing I don't have to care about commit quality in PRs like this.
2022-06-10 14:30:57 +02:00
Pieter-Jan Briers
776669b789 Remove almost all allocations from F3 menu. (#2923) 2022-06-10 12:59:43 +02:00
Pieter-Jan Briers
8d5fa58e1a Avoid NRE if viewport has no eye 2022-06-09 09:39:08 +02:00
Leon Friedrich
dda544223f load & save map command update (#2926) 2022-06-09 13:13:46 +10:00
Leon Friedrich
34106195b0 Better sprite specifier errors (#2925) 2022-06-09 13:08:26 +10:00
Leon Friedrich
5bf3067342 Fix friend visualizer (#2924) 2022-06-09 03:29:32 +10:00
Leon Friedrich
ebeb504347 Make Eye use Matrix3.CreateTransform (#2921) 2022-06-09 03:10:43 +10:00
Leon Friedrich
0b824f2170 Add State application bool to IGameTiming (#2916) 2022-06-09 03:08:50 +10:00
Leon Friedrich
0d04d8a82a Add generic visualizer system (#2914) 2022-06-09 03:08:36 +10:00
Leon Friedrich
b360c4439b Matrix3 and Angle: add readonly and ref->in (#2919)
* Matrix3 and Angle readonly and ref->in

* fix precision
2022-06-08 17:53:17 +02:00
keronshb
d2cb4d102a Adds a filter to all except origin (#2922)
* Adds a filter to all except origin

* Spelling error fix
2022-06-08 14:59:53 +02:00
Leon Friedrich
b44524cd10 Matrix cleanup (#2920) 2022-06-08 14:52:48 +02:00
Vera Aguilera Puerto
7f0feef97c Version 0.21.0.0 2022-06-07 11:28:50 +02:00
Vera Aguilera Puerto
82a67ecd99 Add Access Permissions to friends, friend attributes now work in members too. (#2799) 2022-06-07 11:27:36 +02:00
metalgearsloth
42ad734a46 Version: 0.20.1.0 2022-06-07 12:11:45 +10:00
Pieter-Jan Briers
ac949a4bee Improve "monitor" command.
Has -all and +all options, code cleaned up including IDebugMonitors API and implementation.
2022-06-07 00:44:48 +02:00
Pieter-Jan Briers
60ca5beffa Put debug console below modal and popup root.
This means console completions aren't covered by F3, among other things.
2022-06-06 18:26:03 +02:00
Pieter-Jan Briers
a6ab044582 Fix shadow copies in Matrix3.TransformBoxSlow 2022-06-06 18:21:06 +02:00
Pieter-Jan Briers
d46fb519d5 Fix fullscreen disabling VSync in some cases.
I hate OpenGL.
2022-06-06 18:20:14 +02:00
Pieter-Jan Briers
832672149b Entity creation performance improvements (#2911) 2022-06-06 14:49:36 +02:00
Kara
3941930c2f Add fluent function for indefinite article (#2805) 2022-06-06 12:45:54 +02:00
metalgearsloth
4ab43cc923 Version: 0.20.0.0 2022-06-06 17:15:57 +10:00
metalgearsloth
cb0cb37e3c Remove PhysicsMap ref from physics comp (#2917) 2022-06-06 17:15:01 +10:00
metalgearsloth
df4ebd6c8c No more spoopy invisible ghosts (#2910)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
2022-06-06 16:50:57 +10:00
wrexbe
647aa7ecf2 Expose total fixtures mass on physics (#2908) 2022-06-05 18:28:02 -07:00
Pieter-Jan Briers
6aaf18cb27 Static dispatch ClientEntityManager from ClientGameStateManager.
Speed.
2022-06-05 17:04:25 +02:00
Pieter-Jan Briers
08e295972c TryGetComponent is AggressiveInlining 2022-06-05 17:04:04 +02:00
Pieter-Jan Briers
b4a7c5d47a Miniscule perf improvements to some component state handling.
Basically won't matter but it's a 0.3%.
2022-06-05 16:06:13 +02:00
Pieter-Jan Briers
4226976821 Ordered EventBus optimizations (#2902) 2022-06-05 14:49:59 +02:00
Pieter-Jan Briers
38bc58e77c Reduce dictionary lookups when doing char glyph stuff. 2022-06-05 00:38:00 +02:00
Leon Friedrich
386b0bc232 Cache TransformComponent in trees (#2904)
* Cache TransformComponent in trees

* add deleted check.
2022-06-05 00:25:00 +02:00
Leon Friedrich
487471c2d1 Faster box transform (#2901) 2022-06-05 00:22:00 +02:00
metalgearsloth
e46cfe1601 Ensure maploader sizes (#2906) 2022-06-05 00:14:09 +02:00
Leon Friedrich
fc4eb664e2 Make TryGetComponent output nullable (#2905)
* Make TryGetComponent output nullable

* figs

* ah. there are more. Why do you fail me VS
2022-06-05 00:13:53 +02:00
Pieter-Jan Briers
6c92d246c8 Oops I broke XAML 2022-06-03 02:50:39 +02:00
Pieter-Jan Briers
e86bd33a29 Fix XAML support in Rider EAP 2022-06-02 21:50:39 +02:00
metalgearsloth
c4787b63a8 Version: 0.19.2.0 2022-06-02 21:02:43 +10:00
metalgearsloth
6b305349c9 Fix grid deletion crashing (#2899)
* Fix grid deletion crashing

* polite

* a

* excellent
2022-06-02 21:01:56 +10:00
Alex Evgrashin
3c16779d68 Add helper function in entity prototype (#2868) 2022-06-01 17:28:47 +02:00
Pieter-Jan Briers
38fcadf649 Remove allocations from physics ComputeDistance() (#2896) 2022-06-02 00:52:43 +10:00
metalgearsloth
6cc50013be Version: 0.19.1.3 2022-06-01 22:46:28 +10:00
metalgearsloth
bb7c9ad7fa Fix grid fixtures logger woopsy (#2895) 2022-06-01 22:36:50 +10:00
metalgearsloth
b53d838b14 Version: 0.19.1.2 2022-06-01 20:01:16 +10:00
metalgearsloth
e03831586e Physics tweaks (#2890)
* Tweak awake assetr slightly

It will still log but game shouldn't blow up anymore.

* b

* a

a
2022-06-01 13:13:15 +10:00
metalgearsloth
6acb8f8b1b Add logs to grid splitting (#2888) 2022-05-31 19:18:49 +10:00
metalgearsloth
29bf2cbbc4 Version: 0.19.1.1 2022-05-31 17:53:48 +10:00
metalgearsloth
4fc238864b Update circle solver to box2d's latest (#2883) 2022-05-30 15:38:20 +10:00
Leon Friedrich
cb8d2727f4 Undo all angle lerp changes (#2884) 2022-05-30 15:38:09 +10:00
metalgearsloth
9ef433d3ed Version: 0.19.1.0 2022-05-29 16:24:48 +10:00
Pieter-Jan Briers
02de3cd437 Fix tests with concrete EntityManager 2022-05-29 01:15:52 +02:00
Pieter-Jan Briers
e7211063e7 Remove unecessary Vertex2D.SizeOf 2022-05-29 01:13:12 +02:00
Leon Friedrich
e32ee1a58e Reuse post-shader render target (#2882) 2022-05-29 01:02:19 +02:00
Pieter-Jan Briers
1eb5859f01 Expose concrete EntityManager on IoC, use it in EntitySystem.
REMOVE VIRTUAL DISPATCH.
2022-05-29 01:01:50 +02:00
Pieter-Jan Briers
ff7c3dc45c Do FramePreEngine and FramePostEngine on server 2022-05-29 00:39:54 +02:00
Pieter-Jan Briers
655b5974a3 Move sRGB -> linear conversion for vertex color to vertex shader.
This took almost 2% of CPU time on release. Jeez.
2022-05-28 23:18:05 +02:00
Leon Friedrich
47d7c7cf6c Revert ShortestDistance changes (#2881) 2022-05-29 04:03:31 +10:00
wrexbe
1c39deae6c Fix github action test output spam (#2847) 2022-05-28 16:15:37 +02:00
Pieter-Jan Briers
6c4dc7fc72 Version: 0.19.0.0 2022-05-28 15:36:59 +02:00
Pieter-Jan Briers
3c5618364c Move ValueList<T> to Robust.Shared.Collections 2022-05-28 15:34:46 +02:00
Pieter-Jan Briers
bf1cc8fd7c Minor performance improvements (#2860) 2022-05-28 15:28:08 +02:00
20kdc
72d4d50788 Tile aliases (#2840) 2022-05-28 14:59:58 +02:00
Leon Friedrich
6a86fcdeeb Fix WorldSpace overlay drawing (#2878) 2022-05-28 14:52:01 +02:00
Pieter-Jan Briers
dad95ad73a Fix KeepAspectCovered 2022-05-28 14:46:15 +02:00
20kdc
77f915acf5 Strip Lidgren "Disconnected: " prefix when a connection fails because of interruption by a disconnect with a structured message. (#2880)
Note that this otherwise retains the prefix, which may be important for debugging.
2022-05-28 14:37:17 +02:00
Pieter-Jan Briers
6fc1daf978 Some compiler warning fixes. 2022-05-28 13:36:35 +02:00
Pieter-Jan Briers
934f589778 Put Control FrameUpdate() before style & layout. (#2877) 2022-05-28 13:09:19 +02:00
Clyybber
305ea97d5b Limit audio raycast length (#2851)
* Limit audio raycast length

* Add MidiManager.MaxDistanceForCulling and cull based on MaxDistance.
2022-05-28 13:07:36 +02:00
Pieter-Jan Briers
e3b26e80bf Sync CurTime base. (#2858) 2022-05-28 13:03:42 +02:00
Leon Friedrich
810ef119b0 Remove "A:" log (#2879) 2022-05-28 11:10:41 +02:00
20kdc
393b27679d ACZ general speedups (#2873)
* Cleave the ACZ code into two parts

* SSAZip - faster-performing than ZipArchive, very small codebase

* OnDemandFile to reduce RAM usage of SSAZip

* Modify OnDemandFile to be more amiable for replacing parts of ACManifest

* Make the ACZ Manifest code a lot less reliant on an "internal zip file" for memory usage reasons

* Finally allow disabling legacy client zip creation entirely

* ZIP is now only an ingestion format for ACZ, not actually served anymore

* We do a little renaming

* We do a little namespace adjustment

* We do a little ABOMINATION OF FILE MERGING
2022-05-27 20:52:38 +02:00
Pieter-Jan Briers
a592134caf run XamlIL with internal MSBuild by default.
This only needs to be external when developing XamlIL itself, and I assume doing it external worsens build perf somewhat.
2022-05-27 17:42:27 +02:00
wrexbe
2389df7c6f Ignore missing components on the client (#2846) 2022-05-27 15:30:22 +02:00
Leon Friedrich
422202348e Change SplitContainer MeasureOverride (#2874) 2022-05-27 13:31:54 +02:00
metalgearsloth
284e08ffd9 Version: 0.18.1.3 2022-05-27 10:18:30 +10:00
Leon Friedrich
2ce3d04bba Remove EntitySystem.Get from SpriteView.DrawInternal() (#2867) 2022-05-27 10:18:01 +10:00
Acruid
8e67bfe990 Backwards compatible API additions to support removing GridId from content. (#2855) 2022-05-27 10:17:05 +10:00
Leon Friedrich
27dc9510a3 Remove ambiguity in sprite specifier (#2844) 2022-05-27 10:16:08 +10:00
Leon Friedrich
d4708cb144 Make ignored component errors more explicit (#2875) 2022-05-27 10:15:44 +10:00
Pieter-Jan Briers
986de32043 Don't try to parse JSON structured disconnects if it doesn't look like JSON.
Avoid my debugger getting caught on the exception.
2022-05-27 00:03:39 +02:00
Kara
4e90c291b3 Make ShortestDistance not reduce angle (#2869) 2022-05-25 21:55:31 -07:00
metalgearsloth
7775767687 Version: 0.18.1.2 2022-05-26 14:29:12 +10:00
Leon Friedrich
5624476e2b Reset last modified tick (#2871) 2022-05-26 14:28:37 +10:00
Leon Friedrich
9ef80864b7 Fix completion divide by zero (#2866) 2022-05-26 04:38:08 +10:00
Clyybber
c32ef136a1 Fix completion crashing on a invalid path not starting with '/' (#2861) 2022-05-25 13:39:31 +10:00
metalgearsloth
b2ea0fe1e2 Version: 0.18.1.1 2022-05-25 00:56:51 +10:00
metalgearsloth
be86b12b14 Fix collisionwake on spawn (#2859)
* Fix collisionwake on spawn

Stuff that was immediately slept wasn't checked correctly.

* Testy
2022-05-25 00:55:11 +10:00
Pieter-Jan Briers
8b8e499615 Avoid TLS lookup in IoCManager.Resolve<T>(ref T) if instance provided. 2022-05-23 23:33:01 +02:00
metalgearsloth
36b0d38b70 Version: 0.18.1.0 2022-05-22 18:56:53 +10:00
Kara
78ef6921da Fix two angle lerping bugs (#2850) 2022-05-22 18:56:16 +10:00
metalgearsloth
f6ab67e92c Fix audio being occluded by non-hard fixtures (#2849) 2022-05-22 18:32:32 +10:00
Vera Aguilera Puerto
44ffdcf429 Also look for "default.dls" soundfonts on Linux.
Technically, it still opened them fine if they were called "default.sf2" but having the correct extension is better, honestly.
2022-05-20 21:55:26 +02:00
Vera Aguilera Puerto
53f4d3fab6 Allow soundfonts shipped by Content to override system soundfonts.
If Content wants to override the system soundfont, it should be able to.
2022-05-20 21:41:12 +02:00
metalgearsloth
7dcf543443 Version: 0.18.0.1 2022-05-20 18:30:09 +10:00
metalgearsloth
4e445ee313 Physics command niceties (#2843) 2022-05-20 14:17:20 +10:00
metalgearsloth
c6be3e145a Fix help command crash (#2841) 2022-05-19 18:18:33 +10:00
Pieter-Jan Briers
f90526be22 Bump ANGLE, add ES3OnFL10_0 support (#2834) 2022-05-19 07:55:36 +10:00
metalgearsloth
df09bc2cf0 Version: 0.18.0.0 2022-05-19 07:52:46 +10:00
metalgearsloth
a1affcfd3f BodyTypeChangedEvent changes (#2838) 2022-05-19 07:51:54 +10:00
metalgearsloth
a402f3a880 Version: 0.17.0.0 2022-05-17 13:32:33 +10:00
Leon Friedrich
b0fe9fb1a4 Log error for invalid coordinate conversions (#2835)
* Log error for invalid coordinate conversions

* remove $$$
2022-05-17 13:10:33 +10:00
Pieter-Jan Briers
5057c91dcd Console command completions v1. (#2817)
* Console command completions v1.

I think it works™️

* Unify cvar commands

* Handle no-completions-at-all better.

* Don't crash if you tab complete while no completions available.

* Always show hints if available

* Properly null completion hint over the wire

* Unify help command, localize it.

* Clean up + localize cvar command.

* Remove debug logging

* List command unified & localized.

* Remove server completions debug logging

* Remote execute command.

Had to make everything async for this.

* Don't lower case enums or bools
Why

* GC commands converted and localized.

* Fix remote command completions.

Whoops

* Kick command completions

* lsasm unified & localized.

* Revert "Don't lower case enums or bools"

This reverts commit 2f825347c3.

* ToString gc_mode command enums instead of trying to fix Fluent.

Ah well.

* Unify szr_stats

* Unify log commands, completions

* Fix compile

* Improve completion with complex cases (quotes, escapes)

* Code cleanup, comments.

* Fix tab completion with empty arg ruining everything.

* Fix RegisteredCommand completions

* Add more complex completion options system.

* Refactor content directory entries into a proper resource manager API.

* Implement GetEntries for DirLoader

* Make type hint darker.

* Exec command autocomplete, pulled play global sound code out to engine.
2022-05-17 13:07:25 +10:00
Pieter-Jan Briers
84a80db21f Quick and dirty environment variable to disable sandboxing.
Make game start faster.
2022-05-16 22:07:09 +02:00
20kdc
8657e5516b Structured disconnect messages (#2831)
* net structured disco

* Structured reason into NetChannelArgs/etc.

* Enable redial flag in common cases

* Get it all done

* Disco->DisconnectMessages
2022-05-16 21:48:12 +02:00
Pieter-Jan Briers
6b24da6990 Don't emit array precisions on GLES2 2022-05-16 17:32:45 +02:00
20kdc
0ee87bc771 Colour batch reduction (#2809)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2022-05-16 17:28:37 +02:00
metalgearsloth
6e35c89740 Significantly optimise prediction resetting (#2826)
* Significantly optimise prediction resetting

Turns out just tracking dirty entities and not iterating 30k is much faster.

* cleaner

* Slightly better again

* review

* Fix

* boop

* rebiew

* A
2022-05-16 22:08:17 +10:00
Leon Friedrich
9d69b330b5 Stop double AnchorStateChangedEvent (#2830) 2022-05-16 14:40:48 +10:00
metalgearsloth
22b3f3148c Add default ctor for mousejoint 2022-05-16 13:55:30 +10:00
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
741 changed files with 41303 additions and 13091 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

View File

@@ -28,6 +28,6 @@ jobs:
- name: Build
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- name: Test Engine
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -v n
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -- NUnit.ConsoleOut=0

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.40.3.2</Version></PropertyGroup>
</Project>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<!-- Avoid MSBuild adding a None entry for XAML files because they'd show up TWICE in the project view. -->
<DefaultItemExcludes>**/*.xaml</DefaultItemExcludes>
<RobustUseExternalMSBuild>true</RobustUseExternalMSBuild>
<RobustUseExternalMSBuild>false</RobustUseExternalMSBuild>
<_RobustUseExternalMSBuild>$(RobustUseExternalMSBuild)</_RobustUseExternalMSBuild>
<_RobustUseExternalMSBuild Condition="'$(_RobustForceInternalMSBuild)' == 'true'">false</_RobustUseExternalMSBuild>
</PropertyGroup>

View File

@@ -10,7 +10,6 @@
using System;
using System.Runtime.InteropServices;
using static OpenToolkit.GraphicsLibraryFramework.GLFWNative;
using static Robust.Shared.Utility.MarshalHelper;
namespace OpenToolkit.GraphicsLibraryFramework
{
@@ -211,7 +210,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// <seealso cref="GetVersion"/>
public static unsafe string GetVersionString()
{
return PtrToStringUTF8(glfwGetVersionString());
return Marshal.PtrToStringUTF8((IntPtr) glfwGetVersionString());
}
/// <summary>
@@ -272,7 +271,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
{
byte* desc;
var code = glfwGetError(&desc);
description = PtrToStringUTF8(desc);
description = Marshal.PtrToStringUTF8((IntPtr) desc);
return code;
}
@@ -590,7 +589,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// </remarks>
public static unsafe string GetMonitorName(Monitor* monitor)
{
return PtrToStringUTF8(glfwGetMonitorName(monitor));
return Marshal.PtrToStringUTF8((IntPtr) glfwGetMonitorName(monitor));
}
/// <summary>
@@ -902,7 +901,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// </remarks>
public static unsafe void WindowHint(WindowHintString hint, string value)
{
var ptr = StringToCoTaskMemUTF8(value);
var ptr = Marshal.StringToCoTaskMemUTF8(value);
try
{
@@ -1364,7 +1363,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// </remarks>
public static unsafe string GetKeyName(Keys key, int scanCode)
{
return PtrToStringUTF8(glfwGetKeyName(key, scanCode));
return Marshal.PtrToStringUTF8((IntPtr) glfwGetKeyName(key, scanCode));
}
/// <summary>
@@ -2278,7 +2277,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// </remarks>
public static unsafe string GetJoystickName(int jid)
{
return PtrToStringUTF8(glfwGetJoystickName(jid));
return Marshal.PtrToStringUTF8((IntPtr) glfwGetJoystickName(jid));
}
/// <summary>
@@ -2351,7 +2350,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// </remarks>
public static unsafe string GetJoystickGUID(int jid)
{
return PtrToStringUTF8(glfwGetJoystickGUID(jid));
return Marshal.PtrToStringUTF8((IntPtr) glfwGetJoystickGUID(jid));
}
/// <summary>
@@ -2509,7 +2508,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// </remarks>
public static unsafe bool UpdateGamepadMappings(string newMapping)
{
var ptr = StringToCoTaskMemUTF8(newMapping);
var ptr = Marshal.StringToCoTaskMemUTF8(newMapping);
try
{
return glfwUpdateGamepadMappings((byte*)ptr) == GLFW_TRUE;
@@ -2585,7 +2584,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// </remarks>
public static unsafe string GetGamepadName(int jid)
{
return PtrToStringUTF8(glfwGetGamepadName(jid));
return Marshal.PtrToStringUTF8((IntPtr) glfwGetGamepadName(jid));
}
/// <summary>
@@ -2854,7 +2853,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// </remarks>
public static unsafe bool ExtensionSupported(string extensionName)
{
var ptr = StringToCoTaskMemUTF8(extensionName);
var ptr = Marshal.StringToCoTaskMemUTF8(extensionName);
try
{
@@ -2893,7 +2892,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// <seealso cref="ExtensionSupported" />
public static unsafe IntPtr GetProcAddress(string procName)
{
var ptr = StringToCoTaskMemUTF8(procName);
var ptr = Marshal.StringToCoTaskMemUTF8(procName);
try
{
@@ -3085,7 +3084,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// </remarks>
public static unsafe Window* CreateWindow(int width, int height, string title, Monitor* monitor, Window* share)
{
var ptr = StringToCoTaskMemUTF8(title);
var ptr = Marshal.StringToCoTaskMemUTF8(title);
try
{
@@ -3306,7 +3305,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// <seealso cref="SetClipboardString"/>
public static unsafe string GetClipboardString(Window* window)
{
return PtrToStringUTF8(glfwGetClipboardString(window));
return Marshal.PtrToStringUTF8((IntPtr) glfwGetClipboardString(window));
}
/// <summary>
@@ -3917,7 +3916,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// <seealso cref="GetClipboardString"/>
public static unsafe void SetClipboardString(Window* window, string data)
{
var ptr = StringToCoTaskMemUTF8(data);
var ptr = Marshal.StringToCoTaskMemUTF8(data);
try
{
@@ -4751,7 +4750,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// </remarks>
public static unsafe void SetWindowTitle(Window* window, string title)
{
var ptr = StringToCoTaskMemUTF8(title);
var ptr = Marshal.StringToCoTaskMemUTF8(title);
try
{
@@ -5340,7 +5339,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
var array = new string[count];
for (var i = 0; i < count; i++)
{
array[i] = PtrToStringUTF8(ptr[i]);
array[i] = Marshal.PtrToStringUTF8((IntPtr) ptr[i]);
}
return array;
@@ -5386,7 +5385,7 @@ namespace OpenToolkit.GraphicsLibraryFramework
/// <returns>The address of the function, or <c>null</c> if an error occurred.</returns>
public static unsafe IntPtr GetInstanceProcAddress(VkHandle instance, string procName)
{
var ptr = StringToCoTaskMemUTF8(procName);
var ptr = Marshal.StringToCoTaskMemUTF8(procName);
try
{

View File

@@ -14,7 +14,4 @@
<PackageReference Condition="'$(TargetFramework)' == 'net472'" Include="System.Memory" Version="4.5.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,155 @@
### Localization for engine console commands
## generic
cmd-parse-failure-integer = {$arg} is not a valid integer.
cmd-parse-failure-float = {$arg} is not a valid float.
cmd-parse-failure-bool = {$arg} is not a valid bool.
## 'help' command
cmd-help-desc = Display general help or help text for a specific command
cmd-help-help = Usage: help [command name]
When no command name is provided, displays general-purpose help text. If a command name is provided, displays help text for that command.
cmd-help-no-args = To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'. To search for commands, use 'list <filter>'.
cmd-help-unknown = Unknown command: { $command }
cmd-help-top = { $command } - { $description }
cmd-help-invalid-args = Invalid amount of arguments.
cmd-help-arg-cmdname = [command name]
## 'cvar' command
cmd-cvar-desc = Gets or sets a CVar.
cmd-cvar-help = Usage: cvar <name | ?> [value]
If a value is passed, the value is parsed and stored as the new value of the CVar.
If not, the current value of the CVar is displayed.
Use 'cvar ?' to get a list of all registered CVars.
cmd-cvar-invalid-args = Must provide exactly one or two arguments.
cmd-cvar-not-registered = CVar '{ $cvar }' is not registered. Use 'cvar ?' to get a list of all registered CVars.
cmd-cvar-parse-error = Input value is in incorrect format for type { $type }
cmd-cvar-compl-list = List available CVars
cmd-cvar-arg-name = <name | ?>
cmd-cvar-value-hidden = <value hidden>
## 'list' command
cmd-list-desc = Lists available commands, with optional search filter
cmd-list-help = Usage: list [filter]
Lists all available commands. If an argument is provided, it will be used to filter commands by name.
cmd-list-heading = SIDE NAME DESC{"\u000A"}-------------------------{"\u000A"}
cmd-list-arg-filter = [filter]
## '>' command, aka remote exec
cmd-remoteexec-desc = Executes server-side commands
cmd-remoteexec-help = Usage: > <command> [arg] [arg] [arg...]
Executes a command on the server. This is necessary if a command with the same name exists on the client, as simply running the command would run the client command first.
## 'gc' command
cmd-gc-desc = Run the GC (Garbage Collector)
cmd-gc-help = Usage: gc [generation]
Uses GC.Collect() to execute the Garbage Collector.
If an argument is provided, it is parsed as a GC generation number and GC.Collect(int) is used.
Use the 'gfc' command to do an LOH-compacting full GC.
cmd-gc-failed-parse = Failed to parse argument.
cmd-gc-arg-generation = [generation]
## 'gcf' command
cmd-gcf-desc = Run the GC, fully, compacting LOH and everything.
cmd-gcf-help = Usage: gcf
Does a full GC.Collect(2, GCCollectionMode.Forced, true, true) while also compacting LOH.
This will probably lock up for hundreds of milliseconds, be warned.
## 'gc_mode' command
cmd-gc_mode-desc = Change/Read the GC Latency mode
cmd-gc_mode-help = Usage: gc_mode [type]
If no argument is provided, returns the current GC latency mode.
If an argument is passed, it is parsed as GCLatencyMode and set as the GC latency mode.
cmd-gc_mode-current = current gc latency mode: { $prevMode }
cmd-gc_mode-possible = possible modes:
cmd-gc_mode-option = - { $mode }
cmd-gc_mode-unknown = unknown gc latency mode: { $arg }
cmd-gc_mode-attempt = attempting gc latency mode change: { $prevMode } -> { $mode }
cmd-gc_mode-result = resulting gc latency mode: { $mode }
cmd-gc_mode-arg-type = [type]
## 'mem' command
cmd-mem-desc = Prints managed memory info
cmd-mem-help = Usage: mem
cmd-mem-report = Heap Size: { TOSTRING($heapSize, "N0") }
Total Allocated: { TOSTRING($totalAllocated, "N0") }
## 'physics' command
cmd-physics-overlay = {$overlay} is not a recognised overlay
## 'lsasm' command
cmd-lsasm-desc = Lists loaded assemblies by load context
cmd-lsasm-help = Usage: lsasm
## 'exec' command
cmd-exec-desc = Executes a script file from the game's writeable user data
cmd-exec-help = Usage: exec <fileName>
Each line in the file is executed as a single command, unless it starts with a #
cmd-exec-arg-filename = <fileName>
## 'dump_net_comps' command
cmd-dump_net_comps-desc = Prints the table of networked components.
cmd-dump_net_comps-help = Usage: dump_net-comps
cmd-dump_net_comps-error-writeable = Registration still writeable, network ids have not been generated.
cmd-dump_net_comps-header = Networked Component Registrations:
## 'dump_event_tables' command
cmd-dump_event_tables-desc = Prints directed event tables for an entity.
cmd-dump_event_tables-help = Usage: dump_event_tables <entityUid>
cmd-dump_event_tables-missing-arg-entity = Missing entity argument
cmd-dump_event_tables-error-entity = Invalid entity
cmd-dump_event_tables-arg-entity = <entityUid>
## 'monitor' command
cmd-monitor-desc = Toggles a debug monitor in the F3 menu.
cmd-monitor-help = Usage: monitor <name>
Possible monitors are: { $monitors }
You can also use the special values "-all" and "+all" to hide or show all monitors, respectively.
cmd-monitor-arg-monitor = <monitor>
cmd-monitor-invalid-name = Invalid monitor name
cmd-monitor-arg-count = Missing monitor argument
cmd-monitor-minus-all-hint = Hides all monitors
cmd-monitor-plus-all-hint = Shows all monitors
## Mapping commands
cmd-savemap-desc = Serializes a map to disk. Will not save a post-init map unless forced.
cmd-savemap-help = savemap <MapID> <Path> [force]
cmd-savemap-not-exist = Target map does not exist.
cmd-savemap-init-warning = Attempted to save a post-init map without forcing the save.
cmd-savemap-attempt = Attempting to save map {$mapId} to {$path}.
cmd-savemap-success = Map successfully saved.
cmd-hint-savemap-id = <MapID>
cmd-hint-savemap-path = <Path>
cmd-hint-savemap-force = [bool]
cmd-loadmap-desc = Loads a map from disk into the game.
cmd-loadmap-help = loadmap <MapID> <Path> [x] [y] [rotation] [consistentUids]
cmd-loadmap-nullspace = You cannot load into map 0.
cmd-loadmap-exists = Map {$mapId} already exists.
cmd-loadmap-success = Map {$mapId} has been loaded from {$path}.
cmd-loadmap-error = An error occurred while loading map from {$path}.
cmd-hint-loadmap-x-position = [x-position]
cmd-hint-loadmap-y-position = [y-position]
cmd-hint-loadmap-rotation = [rotation]
cmd-hint-loadmap-uids = [float]
## 'flushcookies' command
# Note: the flushcookies command is from Robust.Client.WebView, it's not in the main engine code.
cmd-flushcookies-desc = Flush CEF cookie storage to disk
cmd-flushcookies-help = This ensure cookies are properly saved to disk in the event of unclean shutdowns.
Note that the actual operation is asynchronous.

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

@@ -0,0 +1,822 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using Robust.Analyzers;
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.AccessAnalyzer>;
using static Microsoft.CodeAnalysis.Testing.DiagnosticResult;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
public sealed class AccessAnalyzer_Test
{
public Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<AccessAnalyzer, NUnitVerifier>()
{
TestState =
{
AdditionalReferences = { typeof(AccessAnalyzer).Assembly },
Sources = { code }
},
};
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
/*
*
*/
[Test]
public async Task ReadTest()
{
const string code = @"
using System;
using Robust.Shared.Analyzers;
// ReSharper disable RedundantAssignment
// ReSharper disable UnusedVariable
// ReSharper disable ArrangeThisQualifier
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
public struct MyData
{
public int MyField;
public static bool operator ==(MyData lhs, MyData rhs) => lhs.MyField == rhs.MyField;
public static bool operator !=(MyData lhs, MyData rhs) => lhs.MyField != rhs.MyField;
}
[Access(typeof(FriendlyClass),
Self = AccessPermissions.None,
Friend = AccessPermissions.None,
Other = AccessPermissions.None)]
public sealed class TypeNobodyCanRead
{
public MyData Data = default;
[Access(typeof(FriendlyClass),
Self = AccessPermissions.Read,
Friend = AccessPermissions.Read,
Other = AccessPermissions.Read)]
public MyData Data2 = default;
public void TestTypeNobodyCanRead(TypeNobodyCanRead obj)
{
// None of these accesses should be allowed.
var copy = Data;
var copy2 = this.Data;
var copy3 = obj.Data;
copy = Data;
copy = this.Data;
copy = obj.Data;
var copy4 = Data.MyField;
var copy5 = this.Data.MyField;
var copy6 = obj.Data.MyField;
if (Data == copy) {}
if (this.Data == copy) {}
if (obj.Data == copy) {}
if(Data.MyField == 0) {}
if(this.Data.MyField == 0) {}
if(obj.Data.MyField == 0) {}
// All of these accesses should be fine.
var copy7 = Data2;
var copy8 = this.Data2;
var copy9 = obj.Data2;
copy = Data2;
copy = this.Data2;
copy = obj.Data2;
var copy10 = Data2.MyField;
var copy11 = this.Data2.MyField;
var copy12 = obj.Data2.MyField;
if (Data2 == copy) {}
if (this.Data2 == copy) {}
if (obj.Data2 == copy) {}
if(Data2.MyField == 0) {}
if(this.Data2.MyField == 0) {}
if(obj.Data2.MyField == 0) {}
}
}
[Access(typeof(FriendlyClass),
Self = AccessPermissions.Read,
Friend = AccessPermissions.Read,
Other = AccessPermissions.Read)]
public sealed class MemberNobodyCanRead
{
[Access(typeof(FriendlyClass),
Self = AccessPermissions.None,
Friend = AccessPermissions.None,
Other = AccessPermissions.None)]
public MyData Data = default;
public MyData Data2 = default;
public void TestMemberNobodyCanRead(TypeNobodyCanRead obj)
{
// None of these accesses should be allowed.
var copy = Data;
var copy2 = this.Data;
var copy3 = obj.Data;
copy = Data;
copy = this.Data;
copy = obj.Data;
var copy4 = Data.MyField;
var copy5 = this.Data.MyField;
var copy6 = obj.Data.MyField;
if (Data == copy) {}
if (this.Data == copy) {}
if (obj.Data == copy) {}
if(Data.MyField == 0) {}
if(this.Data.MyField == 0) {}
if(obj.Data.MyField == 0) {}
// All of these accesses should be fine.
var copy7 = Data2;
var copy8 = this.Data2;
var copy9 = obj.Data2;
copy = Data2;
copy = this.Data2;
copy = obj.Data2;
var copy10 = Data2.MyField;
var copy11 = this.Data2.MyField;
var copy12 = obj.Data2.MyField;
if (Data2 == copy) {}
if (this.Data2 == copy) {}
if (obj.Data2 == copy) {}
if(Data2.MyField == 0) {}
if(this.Data2.MyField == 0) {}
if(obj.Data2.MyField == 0) {}
}
}
public sealed class FriendlyClass
{
public void TestTypeNobodyCanRead(TypeNobodyCanRead obj)
{
// We shouldn't be able to access any of these, even if we're a friend..
var copy = obj.Data;
copy = obj.Data;
var copy2 = obj.Data.MyField;
copy2 = obj.Data.MyField;
if (obj.Data == copy) {}
if(obj.Data.MyField == 0) {}
// We should be allowed to access all of these, we're friends!
var copy3 = obj.Data2;
copy = obj.Data2;
var copy4 = obj.Data2.MyField;
copy4 = obj.Data2.MyField;
if(obj.Data2 == copy) {}
if(obj.Data2.MyField == 0) {}
}
public void TestMemberNobodyCanRead(MemberNobodyCanRead obj)
{
// We shouldn't be able to access any of these, even if we're a friend..
var copy = obj.Data;
copy = obj.Data;
var copy2 = obj.Data.MyField;
copy2 = obj.Data.MyField;
if (obj.Data == copy) {}
if(obj.Data.MyField == 0) {}
// We should be allowed to access all of these, we're friends!
var copy3 = obj.Data2;
copy = obj.Data2;
var copy4 = obj.Data2.MyField;
copy4 = obj.Data2.MyField;
if(obj.Data2 == copy) {}
if(obj.Data2.MyField == 0) {}
}
}
public sealed class OtherClass
{
public void TestTypeNobodyCanRead(TypeNobodyCanRead obj)
{
// We shouldn't be able to access any of these, as 'other types' can't..
var copy = obj.Data;
copy = obj.Data;
var copy2 = obj.Data.MyField;
copy2 = obj.Data.MyField;
if (obj.Data == copy) {}
if(obj.Data.MyField == 0) {}
// We should be allowed to access all of these, they let others read it!
var copy3 = obj.Data2;
copy = obj.Data2;
var copy4 = obj.Data2.MyField;
copy4 = obj.Data2.MyField;
if(obj.Data2 == copy) {}
if(obj.Data2.MyField == 0) {}
}
public void TestMemberNobodyCanRead(MemberNobodyCanRead obj)
{
// We shouldn't be able to access any of these, as 'other types' can't..
var copy = obj.Data;
copy = obj.Data;
var copy2 = obj.Data.MyField;
copy2 = obj.Data.MyField;
if (obj.Data == copy) {}
if(obj.Data.MyField == 0) {}
// We should be allowed to access all of these, they let others read it!
var copy3 = obj.Data2;
copy = obj.Data2;
var copy4 = obj.Data2.MyField;
copy4 = obj.Data2.MyField;
if(obj.Data2 == copy) {}
if(obj.Data2.MyField == 0) {}
}
}";
await Verifier(code,
// AUTO-GENERATED DIAGNOSTICS BELOW //
// /0/Test0.cs(35,20): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(35, 20, 35, 24).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(36,21): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(36, 21, 36, 30).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(37,21): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(37, 21, 37, 29).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(39,16): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(39, 16, 39, 20).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(40,16): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(40, 16, 40, 25).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(41,16): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(41, 16, 41, 24).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(43,21): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(43, 21, 43, 25).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(44,21): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(44, 21, 44, 30).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(45,21): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(45, 21, 45, 29).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(47,13): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(47, 13, 47, 17).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(48,13): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(48, 13, 48, 22).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(49,13): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(49, 13, 49, 21).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(51,12): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(51, 12, 51, 16).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(52,12): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(52, 12, 52, 21).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(53,12): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(53, 12, 53, 20).WithArguments("a 'Read' same-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(95,20): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(95, 20, 95, 24).WithArguments("a 'Read' same-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(96,21): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(96, 21, 96, 30).WithArguments("a 'Read' same-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(97,21): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(97, 21, 97, 29).WithArguments("a 'Read' other-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(99,16): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(99, 16, 99, 20).WithArguments("a 'Read' same-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(100,16): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(100, 16, 100, 25).WithArguments("a 'Read' same-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(101,16): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(101, 16, 101, 24).WithArguments("a 'Read' other-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(103,21): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(103, 21, 103, 25).WithArguments("a 'Read' same-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(104,21): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(104, 21, 104, 30).WithArguments("a 'Read' same-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(105,21): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(105, 21, 105, 29).WithArguments("a 'Read' other-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(107,13): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(107, 13, 107, 17).WithArguments("a 'Read' same-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(108,13): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(108, 13, 108, 22).WithArguments("a 'Read' same-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(109,13): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(109, 13, 109, 21).WithArguments("a 'Read' other-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(111,12): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(111, 12, 111, 16).WithArguments("a 'Read' same-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(112,12): error RA0002: Tried to perform a 'Read' same-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(112, 12, 112, 21).WithArguments("a 'Read' same-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(113,12): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(113, 12, 113, 20).WithArguments("a 'Read' other-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(143,20): error RA0002: Tried to perform a 'Read' friend-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(143, 20, 143, 28).WithArguments("a 'Read' friend-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(144,16): error RA0002: Tried to perform a 'Read' friend-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(144, 16, 144, 24).WithArguments("a 'Read' friend-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(146,21): error RA0002: Tried to perform a 'Read' friend-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(146, 21, 146, 29).WithArguments("a 'Read' friend-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(147,17): error RA0002: Tried to perform a 'Read' friend-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(147, 17, 147, 25).WithArguments("a 'Read' friend-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(149,13): error RA0002: Tried to perform a 'Read' friend-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(149, 13, 149, 21).WithArguments("a 'Read' friend-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(150,12): error RA0002: Tried to perform a 'Read' friend-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(150, 12, 150, 20).WithArguments("a 'Read' friend-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(166,20): error RA0002: Tried to perform a 'Read' friend-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(166, 20, 166, 28).WithArguments("a 'Read' friend-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(167,16): error RA0002: Tried to perform a 'Read' friend-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(167, 16, 167, 24).WithArguments("a 'Read' friend-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(169,21): error RA0002: Tried to perform a 'Read' friend-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(169, 21, 169, 29).WithArguments("a 'Read' friend-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(170,17): error RA0002: Tried to perform a 'Read' friend-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(170, 17, 170, 25).WithArguments("a 'Read' friend-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(172,13): error RA0002: Tried to perform a 'Read' friend-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(172, 13, 172, 21).WithArguments("a 'Read' friend-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(173,12): error RA0002: Tried to perform a 'Read' friend-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(173, 12, 173, 20).WithArguments("a 'Read' friend-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(192,20): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(192, 20, 192, 28).WithArguments("a 'Read' other-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(193,16): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(193, 16, 193, 24).WithArguments("a 'Read' other-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(195,21): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(195, 21, 195, 29).WithArguments("a 'Read' other-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(196,17): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(196, 17, 196, 25).WithArguments("a 'Read' other-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(198,13): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(198, 13, 198, 21).WithArguments("a 'Read' other-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(199,12): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'TypeNobodyCanRead', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(199, 12, 199, 20).WithArguments("a 'Read' other-type", "Data", "TypeNobodyCanRead", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(215,20): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(215, 20, 215, 28).WithArguments("a 'Read' other-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(216,16): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(216, 16, 216, 24).WithArguments("a 'Read' other-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(218,21): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(218, 21, 218, 29).WithArguments("a 'Read' other-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(219,17): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(219, 17, 219, 25).WithArguments("a 'Read' other-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(221,13): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(221, 13, 221, 21).WithArguments("a 'Read' other-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(222,12): error RA0002: Tried to perform a 'Read' other-type access to member 'Data' in type 'MemberNobodyCanRead', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(222, 12, 222, 20).WithArguments("a 'Read' other-type", "Data", "MemberNobodyCanRead", "having no", "Member Permissions: ---------")
);
}
[Test]
public async Task WriteTest()
{
const string code = @"
using System;
using Robust.Shared.Analyzers;
// ReSharper disable RedundantAssignment
// ReSharper disable UnusedVariable
// ReSharper disable ArrangeThisQualifier
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
// ReSharper disable NotAccessedField.Global
// ReSharper disable RedundantDefaultMemberInitializer
public struct MyData
{
public int MyField;
}
[Access(typeof(FriendlyClass),
Self = AccessPermissions.None,
Friend = AccessPermissions.None,
Other = AccessPermissions.None)]
public sealed class TypeNobodyCanWrite
{
public MyData Data = default;
[Access(typeof(FriendlyClass),
Self = AccessPermissions.Write,
Friend = AccessPermissions.Write,
Other = AccessPermissions.Write)]
public MyData Data2 = default;
public void TestTypeNobodyCanWrite(TypeNobodyCanWrite obj)
{
// None of these accesses should be allowed.
Data = default;
this.Data = default;
obj.Data = default;
Data.MyField = 0;
this.Data.MyField = 0;
obj.Data.MyField = 0;
// All of these accesses should be fine.
Data2 = default;
this.Data2 = default;
obj.Data2 = default;
Data2.MyField = 0;
this.Data2.MyField = 0;
obj.Data2.MyField = 0;
}
}
[Access(typeof(FriendlyClass),
Self = AccessPermissions.Write,
Friend = AccessPermissions.Write,
Other = AccessPermissions.Write)]
public sealed class MemberNobodyCanWrite
{
[Access(typeof(FriendlyClass),
Self = AccessPermissions.None,
Friend = AccessPermissions.None,
Other = AccessPermissions.None)]
public MyData Data = default;
public MyData Data2 = default;
public void TestMemberNobodyCanWrite(TypeNobodyCanWrite obj)
{
// None of these accesses should be allowed.
Data = default;
this.Data = default;
obj.Data = default;
Data.MyField = 0;
this.Data.MyField = 0;
obj.Data.MyField = 0;
// All of these accesses should be fine.
Data2 = default;
this.Data2 = default;
obj.Data2 = default;
Data2.MyField = 0;
this.Data2.MyField = 0;
obj.Data2.MyField = 0;
}
}
public sealed class FriendlyClass
{
public void TestTypeNobodyCanWrite(TypeNobodyCanWrite obj)
{
// We shouldn't be able to access any of these, even if we're a friend..
obj.Data = default;
obj.Data.MyField = 0;
// We should be allowed to access all of these, we're friends!
obj.Data2 = default;
obj.Data2.MyField = 0;
}
public void TestMemberNobodyCanWrite(MemberNobodyCanWrite obj)
{
// We shouldn't be able to access any of these, even if we're a friend..
obj.Data = default;
obj.Data.MyField = 0;
// We should be allowed to access all of these, we're friends!
obj.Data2 = default;
obj.Data2.MyField = 0;
}
}
public sealed class OtherClass
{
public void TestTypeNobodyCanWrite(TypeNobodyCanWrite obj)
{
// We shouldn't be able to access any of these, as 'other types' can't..
obj.Data = default;
obj.Data.MyField = 0;
// We should be allowed to access all of these, they let others write!
obj.Data2 = default;
obj.Data2.MyField = 0;
}
public void TestMemberNobodyCanWrite(MemberNobodyCanWrite obj)
{
// We shouldn't be able to access any of these, as 'other types' can't..
obj.Data = default;
obj.Data.MyField = 0;
// We should be allowed to access all of these, they let others write!
obj.Data2 = default;
obj.Data2.MyField = 0;
}
}";
await Verifier(code,
// AUTO-GENERATED DIAGNOSTICS BELOW //
// /0/Test0.cs(34,9): error RA0002: Tried to perform a 'Write' same-type access to member 'Data' in type 'TypeNobodyCanWrite', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(34, 9, 34, 13).WithArguments("a 'Write' same-type", "Data", "TypeNobodyCanWrite", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(35,9): error RA0002: Tried to perform a 'Write' same-type access to member 'Data' in type 'TypeNobodyCanWrite', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(35, 9, 35, 18).WithArguments("a 'Write' same-type", "Data", "TypeNobodyCanWrite", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(36,9): error RA0002: Tried to perform a 'Write' same-type access to member 'Data' in type 'TypeNobodyCanWrite', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(36, 9, 36, 17).WithArguments("a 'Write' same-type", "Data", "TypeNobodyCanWrite", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(38,9): error RA0002: Tried to perform a 'Write' same-type access to member 'Data' in type 'TypeNobodyCanWrite', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(38, 9, 38, 13).WithArguments("a 'Write' same-type", "Data", "TypeNobodyCanWrite", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(39,9): error RA0002: Tried to perform a 'Write' same-type access to member 'Data' in type 'TypeNobodyCanWrite', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(39, 9, 39, 18).WithArguments("a 'Write' same-type", "Data", "TypeNobodyCanWrite", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(40,9): error RA0002: Tried to perform a 'Write' same-type access to member 'Data' in type 'TypeNobodyCanWrite', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(40, 9, 40, 17).WithArguments("a 'Write' same-type", "Data", "TypeNobodyCanWrite", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(70,9): error RA0002: Tried to perform a 'Write' same-type access to member 'Data' in type 'MemberNobodyCanWrite', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(70, 9, 70, 13).WithArguments("a 'Write' same-type", "Data", "MemberNobodyCanWrite", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(71,9): error RA0002: Tried to perform a 'Write' same-type access to member 'Data' in type 'MemberNobodyCanWrite', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(71, 9, 71, 18).WithArguments("a 'Write' same-type", "Data", "MemberNobodyCanWrite", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(72,9): error RA0002: Tried to perform a 'Write' other-type access to member 'Data' in type 'TypeNobodyCanWrite', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(72, 9, 72, 17).WithArguments("a 'Write' other-type", "Data", "TypeNobodyCanWrite", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(74,9): error RA0002: Tried to perform a 'Write' same-type access to member 'Data' in type 'MemberNobodyCanWrite', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(74, 9, 74, 13).WithArguments("a 'Write' same-type", "Data", "MemberNobodyCanWrite", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(75,9): error RA0002: Tried to perform a 'Write' same-type access to member 'Data' in type 'MemberNobodyCanWrite', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(75, 9, 75, 18).WithArguments("a 'Write' same-type", "Data", "MemberNobodyCanWrite", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(76,9): error RA0002: Tried to perform a 'Write' other-type access to member 'Data' in type 'TypeNobodyCanWrite', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(76, 9, 76, 17).WithArguments("a 'Write' other-type", "Data", "TypeNobodyCanWrite", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(94,9): error RA0002: Tried to perform a 'Write' friend-type access to member 'Data' in type 'TypeNobodyCanWrite', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(94, 9, 94, 17).WithArguments("a 'Write' friend-type", "Data", "TypeNobodyCanWrite", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(95,9): error RA0002: Tried to perform a 'Write' friend-type access to member 'Data' in type 'TypeNobodyCanWrite', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(95, 9, 95, 17).WithArguments("a 'Write' friend-type", "Data", "TypeNobodyCanWrite", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(105,9): error RA0002: Tried to perform a 'Write' friend-type access to member 'Data' in type 'MemberNobodyCanWrite', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(105, 9, 105, 17).WithArguments("a 'Write' friend-type", "Data", "MemberNobodyCanWrite", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(106,9): error RA0002: Tried to perform a 'Write' friend-type access to member 'Data' in type 'MemberNobodyCanWrite', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(106, 9, 106, 17).WithArguments("a 'Write' friend-type", "Data", "MemberNobodyCanWrite", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(119,9): error RA0002: Tried to perform a 'Write' other-type access to member 'Data' in type 'TypeNobodyCanWrite', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(119, 9, 119, 17).WithArguments("a 'Write' other-type", "Data", "TypeNobodyCanWrite", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(120,9): error RA0002: Tried to perform a 'Write' other-type access to member 'Data' in type 'TypeNobodyCanWrite', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(120, 9, 120, 17).WithArguments("a 'Write' other-type", "Data", "TypeNobodyCanWrite", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(130,9): error RA0002: Tried to perform a 'Write' other-type access to member 'Data' in type 'MemberNobodyCanWrite', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(130, 9, 130, 17).WithArguments("a 'Write' other-type", "Data", "MemberNobodyCanWrite", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(131,9): error RA0002: Tried to perform a 'Write' other-type access to member 'Data' in type 'MemberNobodyCanWrite', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(131, 9, 131, 17).WithArguments("a 'Write' other-type", "Data", "MemberNobodyCanWrite", "having no", "Member Permissions: ---------")
);
}
[Test]
public async Task ExecuteTest()
{
const string code = @"
using System;
using Robust.Shared.Analyzers;
// ReSharper disable RedundantAssignment
// ReSharper disable UnusedVariable
// ReSharper disable ArrangeThisQualifier
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
// ReSharper disable NotAccessedField.Global
// ReSharper disable RedundantDefaultMemberInitializer
// ReSharper disable ReturnValueOfPureMethodIsNotUsed
public struct MyData
{
public int MyField;
public void MyMethod() {}
}
[Access(typeof(FriendlyClass),
Self = AccessPermissions.None,
Friend = AccessPermissions.None,
Other = AccessPermissions.None)]
public sealed class TypeNobodyCanExecute
{
public MyData Data = default;
[Access(typeof(FriendlyClass),
Self = AccessPermissions.Execute,
Friend = AccessPermissions.Execute,
Other = AccessPermissions.Execute)]
public MyData Data2 = default;
public void MyMethod() {}
[Access(typeof(FriendlyClass),
Self = AccessPermissions.Execute,
Friend = AccessPermissions.Execute,
Other = AccessPermissions.Execute)]
public void MyMethod2() {}
public void TestTypeNobodyCanExecute(TypeNobodyCanExecute obj)
{
// None of these accesses should be allowed.
MyMethod();
this.MyMethod();
obj.MyMethod();
Data.MyMethod();
this.Data.MyMethod();
obj.Data.MyMethod();
Data.MyField.ToString();
this.Data.MyField.ToString();
obj.Data.MyField.ToString();
// All of these accesses should be fine.
MyMethod2();
this.MyMethod2();
obj.MyMethod2();
Data2.MyMethod();
this.Data2.MyMethod();
obj.Data2.MyMethod();
Data2.MyField.ToString();
this.Data2.ToString();
obj.Data2.ToString();
}
}
[Access(typeof(FriendlyClass),
Self = AccessPermissions.Execute,
Friend = AccessPermissions.Execute,
Other = AccessPermissions.Execute)]
public sealed class MemberNobodyCanExecute
{
[Access(typeof(FriendlyClass),
Self = AccessPermissions.None,
Friend = AccessPermissions.None,
Other = AccessPermissions.None)]
public MyData Data = default;
public MyData Data2 = default;
[Access(typeof(FriendlyClass),
Self = AccessPermissions.None,
Friend = AccessPermissions.None,
Other = AccessPermissions.None)]
public void MyMethod() {}
public void MyMethod2() {}
public void TestMemberNobodyCanExecute(TypeNobodyCanExecute obj)
{
// None of these accesses should be allowed.
MyMethod();
this.MyMethod();
obj.MyMethod();
Data.MyMethod();
this.Data.MyMethod();
obj.Data.MyMethod();
Data.MyField.ToString();
this.Data.MyField.ToString();
obj.Data.MyField.ToString();
// All of these accesses should be fine.
MyMethod2();
this.MyMethod2();
obj.MyMethod2();
Data2.MyMethod();
this.Data2.MyMethod();
obj.Data2.MyMethod();
Data2.MyField.ToString();
this.Data2.ToString();
obj.Data2.ToString();
}
}
public sealed class FriendlyClass
{
public void TestTypeNobodyCanExecute(TypeNobodyCanExecute obj)
{
// We shouldn't be able to access any of these, even if we're a friend..
obj.MyMethod();
obj.Data.MyMethod();
obj.Data.MyField.ToString();
// We should be allowed to access all of these, we're friends!
obj.MyMethod2();
obj.Data2.MyMethod();
obj.Data2.MyField.ToString();
}
public void TestMemberNobodyCanExecute(MemberNobodyCanExecute obj)
{
// We shouldn't be able to access any of these, even if we're a friend..
obj.MyMethod();
obj.Data.MyMethod();
obj.Data.MyField.ToString();
// We should be allowed to access all of these, we're friends!
obj.MyMethod2();
obj.Data2.MyMethod();
obj.Data2.MyField.ToString();
}
}
public sealed class OtherClass
{
public void TestTypeNobodyCanExecute(TypeNobodyCanExecute obj)
{
// We shouldn't be able to access any of these, as 'other types' can't..
obj.MyMethod();
obj.Data.MyMethod();
obj.Data.MyField.ToString();
// We should be allowed to access all of these, they let others Execute!
obj.MyMethod2();
obj.Data2.MyMethod();
obj.Data2.MyField.ToString();
}
public void TestMemberNobodyCanExecute(MemberNobodyCanExecute obj)
{
// We shouldn't be able to access any of these, as 'other types' can't..
obj.MyMethod();
obj.Data.MyMethod();
obj.Data.MyField.ToString();
// We should be allowed to access all of these, they let others Execute!
obj.MyMethod2();
obj.Data2.MyMethod();
obj.Data2.MyField.ToString();
}
}";
await Verifier(code,
// AUTO-GENERATED DIAGNOSTICS BELOW //
// /0/Test0.cs(44,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'MyMethod' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(44, 9, 44, 19).WithArguments("an 'Execute' same-type", "MyMethod", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(45,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'MyMethod' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(45, 9, 45, 24).WithArguments("an 'Execute' same-type", "MyMethod", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(46,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'MyMethod' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(46, 9, 46, 23).WithArguments("an 'Execute' same-type", "MyMethod", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(48,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'Data' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(48, 9, 48, 13).WithArguments("an 'Execute' same-type", "Data", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(49,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'Data' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(49, 9, 49, 18).WithArguments("an 'Execute' same-type", "Data", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(50,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'Data' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(50, 9, 50, 17).WithArguments("an 'Execute' same-type", "Data", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(52,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'Data' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(52, 9, 52, 13).WithArguments("an 'Execute' same-type", "Data", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(53,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'Data' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(53, 9, 53, 18).WithArguments("an 'Execute' same-type", "Data", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(54,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'Data' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(54, 9, 54, 17).WithArguments("an 'Execute' same-type", "Data", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(96,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'MyMethod' in type 'MemberNobodyCanExecute', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(96, 9, 96, 19).WithArguments("an 'Execute' same-type", "MyMethod", "MemberNobodyCanExecute", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(97,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'MyMethod' in type 'MemberNobodyCanExecute', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(97, 9, 97, 24).WithArguments("an 'Execute' same-type", "MyMethod", "MemberNobodyCanExecute", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(98,9): error RA0002: Tried to perform an 'Execute' other-type access to member 'MyMethod' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(98, 9, 98, 23).WithArguments("an 'Execute' other-type", "MyMethod", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(100,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'Data' in type 'MemberNobodyCanExecute', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(100, 9, 100, 13).WithArguments("an 'Execute' same-type", "Data", "MemberNobodyCanExecute", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(101,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'Data' in type 'MemberNobodyCanExecute', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(101, 9, 101, 18).WithArguments("an 'Execute' same-type", "Data", "MemberNobodyCanExecute", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(102,9): error RA0002: Tried to perform an 'Execute' other-type access to member 'Data' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(102, 9, 102, 17).WithArguments("an 'Execute' other-type", "Data", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(104,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'Data' in type 'MemberNobodyCanExecute', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(104, 9, 104, 13).WithArguments("an 'Execute' same-type", "Data", "MemberNobodyCanExecute", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(105,9): error RA0002: Tried to perform an 'Execute' same-type access to member 'Data' in type 'MemberNobodyCanExecute', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(105, 9, 105, 18).WithArguments("an 'Execute' same-type", "Data", "MemberNobodyCanExecute", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(106,9): error RA0002: Tried to perform an 'Execute' other-type access to member 'Data' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(106, 9, 106, 17).WithArguments("an 'Execute' other-type", "Data", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(128,9): error RA0002: Tried to perform an 'Execute' friend-type access to member 'MyMethod' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(128, 9, 128, 23).WithArguments("an 'Execute' friend-type", "MyMethod", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(129,9): error RA0002: Tried to perform an 'Execute' friend-type access to member 'Data' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(129, 9, 129, 17).WithArguments("an 'Execute' friend-type", "Data", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(130,9): error RA0002: Tried to perform an 'Execute' friend-type access to member 'Data' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(130, 9, 130, 17).WithArguments("an 'Execute' friend-type", "Data", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(141,9): error RA0002: Tried to perform an 'Execute' friend-type access to member 'MyMethod' in type 'MemberNobodyCanExecute', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(141, 9, 141, 23).WithArguments("an 'Execute' friend-type", "MyMethod", "MemberNobodyCanExecute", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(142,9): error RA0002: Tried to perform an 'Execute' friend-type access to member 'Data' in type 'MemberNobodyCanExecute', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(142, 9, 142, 17).WithArguments("an 'Execute' friend-type", "Data", "MemberNobodyCanExecute", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(143,9): error RA0002: Tried to perform an 'Execute' friend-type access to member 'Data' in type 'MemberNobodyCanExecute', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(143, 9, 143, 17).WithArguments("an 'Execute' friend-type", "Data", "MemberNobodyCanExecute", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(157,9): error RA0002: Tried to perform an 'Execute' other-type access to member 'MyMethod' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(157, 9, 157, 23).WithArguments("an 'Execute' other-type", "MyMethod", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(158,9): error RA0002: Tried to perform an 'Execute' other-type access to member 'Data' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(158, 9, 158, 17).WithArguments("an 'Execute' other-type", "Data", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(159,9): error RA0002: Tried to perform an 'Execute' other-type access to member 'Data' in type 'TypeNobodyCanExecute', despite having no access. Type Permissions: ---------
VerifyCS.Diagnostic().WithSpan(159, 9, 159, 17).WithArguments("an 'Execute' other-type", "Data", "TypeNobodyCanExecute", "having no", "Type Permissions: ---------"),
// /0/Test0.cs(170,9): error RA0002: Tried to perform an 'Execute' other-type access to member 'MyMethod' in type 'MemberNobodyCanExecute', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(170, 9, 170, 23).WithArguments("an 'Execute' other-type", "MyMethod", "MemberNobodyCanExecute", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(171,9): error RA0002: Tried to perform an 'Execute' other-type access to member 'Data' in type 'MemberNobodyCanExecute', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(171, 9, 171, 17).WithArguments("an 'Execute' other-type", "Data", "MemberNobodyCanExecute", "having no", "Member Permissions: ---------"),
// /0/Test0.cs(172,9): error RA0002: Tried to perform an 'Execute' other-type access to member 'Data' in type 'MemberNobodyCanExecute', despite having no access. Member Permissions: ---------
VerifyCS.Diagnostic().WithSpan(172, 9, 172, 17).WithArguments("an 'Execute' other-type", "Data", "MemberNobodyCanExecute", "having no", "Member Permissions: ---------")
);
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\MSBuild\Robust.Properties.targets" />
<Import Project="..\MSBuild\Robust.Engine.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzer.Testing" Version="1.1.1"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.1"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1"/>
<PackageReference Include="NUnit" Version="3.13.2"/>
<PackageReference Include="NUnit.ConsoleRunner" Version="3.15.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
<PackageReference Include="NUnit.Analyzers" Version="3.3.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Robust.Analyzers\Robust.Analyzers.csproj"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,253 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Shared.Analyzers.Implementation;
namespace Robust.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AccessAnalyzer : DiagnosticAnalyzer
{
private const string AccessAttributeType = "Robust.Shared.Analyzers.AccessAttribute";
private const string PureAttributeType = "System.Diagnostics.Contracts.PureAttribute";
[SuppressMessage("ReSharper", "RS2008")]
private static readonly DiagnosticDescriptor AccessRule = new (
Diagnostics.IdAccess,
"Invalid access",
"Tried to perform {0} access to member '{1}' in type '{2}', despite {3} access. {4}.",
"Usage",
DiagnosticSeverity.Error,
true,
"Make sure to give the accessing type the correct access permissions.");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(AccessRule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterOperationAction(CheckFriendship,
OperationKind.FieldReference,
OperationKind.PropertyReference,
OperationKind.MethodReference,
OperationKind.Invocation);
}
private void CheckFriendship(OperationAnalysisContext context)
{
var operation = context.Operation;
// The symbol representing the member being accessed.
ISymbol member;
// The operation to target when determining access type.
IOperation targetAccess;
switch (operation)
{
case IMemberReferenceOperation memberRef:
{
member = memberRef.Member;
targetAccess = memberRef.Parent;
break;
}
case IInvocationOperation invocation:
{
member = invocation.TargetMethod;
targetAccess = invocation;
break;
}
default:
return;
}
// Get the info of the type defining the member, so we can check the attributes later...
var accessedType = member.ContainingType;
// Get the attributes
var friendAttribute = context.Compilation.GetTypeByMetadataName(AccessAttributeType);
// Get the type that is containing this expression, or, the type where this is happening.
if (context.ContainingSymbol?.ContainingType is not {} accessingType)
return;
// Determine which type of access is happening here... Read, write or execute?
var accessAttempt = DetermineAccess(context, targetAccess, operation);
// Check whether this is a "self" access, including inheritors.
var selfAccess = InheritsFromOrEquals(accessingType, accessedType);
// Helper function to deduplicate attribute-checking code.
bool CheckAttributeFriendship(AttributeData attribute, bool isMemberAttribute)
{
// If the attribute isn't the friend attribute, we don't care about it.
if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, friendAttribute))
return false;
var self = AccessAttribute.SelfDefaultPermissions;
var friends = AccessAttribute.FriendDefaultPermissions;
var others = AccessAttribute.OtherDefaultPermissions;
foreach (var kv in attribute.NamedArguments)
{
if (kv.Value.Value is not byte value)
continue;
var permissions = (AccessPermissions) value;
switch (kv.Key)
{
case nameof(AccessAttribute.Self):
{
self = permissions;
break;
}
case nameof(AccessAttribute.Friend):
{
friends = permissions;
break;
}
case nameof(AccessAttribute.Other):
{
others = permissions;
break;
}
default:
continue;
}
}
// By default, we will check the "other" permissions unless we find we're dealing with a friend or self.
var permissionCheck = others;
// Human-readable relation between accessing and accessed types.
var accessingRelation = "other-type";
if (!selfAccess)
{
// This is not a self-access, so we need to determine whether the accessing type is a friend.
// Check all types allowed in the friend attribute. (We assume there's only one constructor arg.)
var types = attribute.ConstructorArguments[0].Values;
foreach (var constant in types)
{
// Check if the value is a type...
if (constant.Value is not INamedTypeSymbol friendType)
continue;
// Check if the accessing type is specified in the attribute...
if (!InheritsFromOrEquals(accessingType, friendType))
continue;
// Set the permissions check to the friend permissions!
permissionCheck = friends;
accessingRelation = "friend-type";
break;
}
}
else
{
// Self-access, so simply set the permissions check to self.
permissionCheck = self;
accessingRelation = "same-type";
}
// If we allow this access, return! All is good.
if ((accessAttempt & permissionCheck) != 0)
return true;
// Access denied! Report an error.
context.ReportDiagnostic(
Diagnostic.Create(AccessRule, operation.Syntax.GetLocation(),
$"a{(accessAttempt == AccessPermissions.Execute ? "n" : "")} '{accessAttempt}' {accessingRelation}",
$"{member.Name}",
$"{accessedType.Name}",
$"{(permissionCheck == AccessPermissions.None ? "having no" : $"only having '{permissionCheck}'")}",
$"{(isMemberAttribute ? "Member" : "Type")} Permissions: {self.ToUnixPermissions()}{friends.ToUnixPermissions()}{others.ToUnixPermissions()}"));
// Only return ONE error.
return true;
}
// Check attributes in the member first, since they take priority and can override type restrictions.
foreach (var attribute in member.GetAttributes())
{
if(CheckAttributeFriendship(attribute, true))
return;
}
// Check attributes in the type containing the member last.
foreach (var attribute in accessedType.GetAttributes())
{
if(CheckAttributeFriendship(attribute, false))
return;
}
}
private static AccessPermissions DetermineAccess(OperationAnalysisContext context, IOperation operation, IOperation original)
{
switch (operation)
{
case IAssignmentOperation assign:
{
return assign.Target.Equals(original) ? AccessPermissions.Write : AccessPermissions.Read;
}
case IInvocationOperation invoke:
{
var pureAttribute = context.Compilation.GetTypeByMetadataName(PureAttributeType);
foreach (var attribute in invoke.TargetMethod.GetAttributes())
{
// Pure methods are treated as read accesses.
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, pureAttribute))
return AccessPermissions.Read;
}
return AccessPermissions.Execute;
}
case IMemberReferenceOperation member:
{
return DetermineAccess(context, member.Parent, operation);
}
default:
{
return AccessPermissions.Read;
}
}
}
private bool InheritsFromOrEquals(INamedTypeSymbol type, INamedTypeSymbol baseType)
{
foreach (var otherType in GetBaseTypesAndThis(type))
{
if (SymbolEqualityComparer.Default.Equals(otherType, baseType))
return true;
}
return false;
}
private IEnumerable<INamedTypeSymbol> GetBaseTypesAndThis(INamedTypeSymbol namedType)
{
var current = namedType;
while (current != null)
{
yield return current;
current = current.BaseType;
}
}
}
}

View File

@@ -6,8 +6,9 @@ public static class Diagnostics
{
public const string IdExplicitInterface = "RA0000";
public const string IdSerializable = "RA0001";
public const string IdFriend = "RA0002";
public const string IdAccess = "RA0002";
public const string IdExplicitVirtual = "RA0003";
public const string IdTaskResult = "RA0004";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");

View File

@@ -1,129 +0,0 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Robust.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class FriendAnalyzer : DiagnosticAnalyzer
{
const string FriendAttribute = "Robust.Shared.Analyzers.FriendAttribute";
[SuppressMessage("ReSharper", "RS2008")]
private static readonly DiagnosticDescriptor Rule = new (
Diagnostics.IdFriend,
"Tried to access friend-only member",
"Tried to access member \"{0}\" in class \"{1}\" which can only be accessed by friend classes",
"Usage",
DiagnosticSeverity.Error,
true,
"Make sure to specify the accessing class in the friends attribute.");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(CheckFriendship, SyntaxKind.SimpleMemberAccessExpression);
}
private void CheckFriendship(SyntaxNodeAnalysisContext context)
{
if (context.Node is not MemberAccessExpressionSyntax memberAccess)
return;
// We only do something if our parent is one of a few types.
switch (context.Node.Parent)
{
// If we're being assigned...
case AssignmentExpressionSyntax assignParent:
{
if (assignParent.Left != memberAccess)
return;
break;
}
// If we're being invoked...
case InvocationExpressionSyntax:
break;
// Otherwise, do nothing.
default:
return;
}
// Get the friend attribute
var friendAttr = context.Compilation.GetTypeByMetadataName(FriendAttribute);
// Get the type that is containing this expression, or, the class where this is happening.
if (context.ContainingSymbol?.ContainingType is not { } containingType)
return;
// We check all of our children and get only the identifiers.
foreach (var identifier in memberAccess.ChildNodes().Select(node => node as IdentifierNameSyntax))
{
if (identifier == null) continue;
// Get the type info of the identifier, so we can check the attributes...
if (context.SemanticModel.GetTypeInfo(identifier).ConvertedType is not { } type)
continue;
// Same-type access is always fine.
if (SymbolEqualityComparer.Default.Equals(type, containingType))
continue;
// Finally, get all attributes of the type, to check if we have any friend classes.
foreach (var attribute in type.GetAttributes())
{
// If the attribute isn't the friend attribute, continue.
if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, friendAttr))
continue;
// Check all types allowed in the friend attribute. (We assume there's only one constructor arg.)
foreach (var constant in attribute.ConstructorArguments[0].Values)
{
// Check if the value is a type...
if (constant.Value is not INamedTypeSymbol t)
continue;
// If we find that the containing class is specified in the attribute, return! All is good.
if (InheritsFromOrEquals(containingType, t))
return;
}
// Not in a friend class! Report an error.
context.ReportDiagnostic(
Diagnostic.Create(Rule, context.Node.GetLocation(),
$"{context.Node.ToString().Split('.').LastOrDefault()}", $"{type.Name}"));
}
}
}
private bool InheritsFromOrEquals(INamedTypeSymbol type, INamedTypeSymbol baseType)
{
foreach (var otherType in GetBaseTypesAndThis(type))
{
if (SymbolEqualityComparer.Default.Equals(otherType, baseType))
return true;
}
return false;
}
private IEnumerable<INamedTypeSymbol> GetBaseTypesAndThis(INamedTypeSymbol namedType)
{
var current = namedType;
while (current != null)
{
yield return current;
current = current.BaseType;
}
}
}
}

View File

@@ -11,4 +11,10 @@
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.0.1" />
</ItemGroup>
<ItemGroup>
<!-- Needed for FriendAnalyzer. -->
<Compile Include="..\Robust.Shared\Analyzers\AccessAttribute.cs" />
<Compile Include="..\Robust.Shared\Analyzers\AccessPermissions.cs" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,44 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class TaskResultAnalyzer : DiagnosticAnalyzer
{
[SuppressMessage("ReSharper", "RS2008")]
private static readonly DiagnosticDescriptor ResultRule = new DiagnosticDescriptor(
Diagnostics.IdTaskResult,
"Risk of deadlock from accessing Task<T>.Result",
"Accessing Task<T>.Result is dangerous and can cause deadlocks in some contexts. If you understand how this works and are certain that you aren't causing a deadlock here, mute this error with #pragma.",
"Usage",
DiagnosticSeverity.Error,
true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(ResultRule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterOperationAction(Check, OperationKind.PropertyReference);
}
private static void Check(OperationAnalysisContext context)
{
var taskType = context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1");
var operation = (IPropertyReferenceOperation) context.Operation;
var member = operation.Member;
if (member.Name == "Result" && taskType.Equals(member.ContainingType.ConstructedFrom, SymbolEqualityComparer.Default))
{
var diag = Diagnostic.Create(ResultRule, operation.Syntax.GetLocation());
context.ReportDiagnostic(diag);
}
}
}

View File

@@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using BenchmarkDotNet.Attributes;
using Robust.Shared.GameObjects;
namespace Robust.Benchmarks.EntityManager;
public class ComponentIndexBenchmark
{
// Just a bunch of types to bloat the test lists.
private readonly CompIndexFetcher _compIndexFetcherDirect;
private readonly IFetcher _compIndexFetcher;
private readonly DictFetcher _dictFetcherDirect;
private readonly IFetcher _dictFetcher;
public ComponentIndexBenchmark()
{
_compIndexFetcherDirect = new CompIndexFetcher();
_compIndexFetcher = _compIndexFetcherDirect;
_dictFetcherDirect = new DictFetcher();
_dictFetcher = _dictFetcherDirect;
}
[GlobalSetup]
public void Setup()
{
var types = typeof(ComponentIndexBenchmark)
.GetNestedTypes(BindingFlags.NonPublic)
.Where(t => t.Name.StartsWith("TestType"))
.ToArray();
_compIndexFetcher.Init(types);
_dictFetcher.Init(types);
}
[Benchmark]
public int BenchCompIndex() => _compIndexFetcher.Get<TestType50>();
[Benchmark]
public int BenchDict() => _dictFetcher.Get<TestType50>();
[Benchmark]
public int BenchCompIndexDirect() => _compIndexFetcherDirect.Get<TestType50>();
[Benchmark]
public int BenchDictDirect() => _dictFetcherDirect.Get<TestType50>();
private static CompIdx ArrayIndexFor<T>() => CompArrayIndex<T>.Idx;
private static int _compIndexMaster = -1;
private static class CompArrayIndex<T>
{
// ReSharper disable once StaticMemberInGenericType
public static readonly CompIdx Idx = new(Interlocked.Increment(ref _compIndexMaster));
}
private static CompIdx GetCompIdIndex(Type type)
{
return (CompIdx)typeof(CompArrayIndex<>)
.MakeGenericType(type)
.GetField(nameof(CompArrayIndex<int>.Idx), BindingFlags.Static | BindingFlags.Public)!
.GetValue(null)!;
}
private interface IFetcher
{
void Init(Type[] types);
int Get<T>();
}
private sealed class CompIndexFetcher : IFetcher
{
private int[] _values = Array.Empty<int>();
public void Init(Type[] types)
{
var max = types.Max(t => GetCompIdIndex(t).Value);
_values = new int[max + 1];
var i = 0;
foreach (var type in types)
{
_values[GetCompIdIndex(type).Value] = i++;
}
}
public int Get<T>()
{
return _values[CompArrayIndex<T>.Idx.Value];
}
}
private sealed class DictFetcher : IFetcher
{
private readonly Dictionary<Type, int> _values = new();
public void Init(Type[] types)
{
var i = 0;
foreach (var type in types)
{
_values[type] = i++;
}
}
public int Get<T>()
{
return _values[typeof(T)];
}
}
// Just a bunch of types to pad the size of the arrays and such.
// @formatter:off
// ReSharper disable UnusedType.Local
private sealed class TestType1{}
private sealed class TestType2{}
private sealed class TestType3{}
private sealed class TestType4{}
private sealed class TestType5{}
private sealed class TestType6{}
private sealed class TestType7{}
private sealed class TestType8{}
private sealed class TestType9{}
private sealed class TestType10{}
private sealed class TestType11{}
private sealed class TestType12{}
private sealed class TestType13{}
private sealed class TestType14{}
private sealed class TestType15{}
private sealed class TestType16{}
private sealed class TestType17{}
private sealed class TestType18{}
private sealed class TestType19{}
private sealed class TestType20{}
private sealed class TestType21{}
private sealed class TestType22{}
private sealed class TestType23{}
private sealed class TestType24{}
private sealed class TestType25{}
private sealed class TestType26{}
private sealed class TestType27{}
private sealed class TestType28{}
private sealed class TestType29{}
private sealed class TestType30{}
private sealed class TestType31{}
private sealed class TestType32{}
private sealed class TestType33{}
private sealed class TestType34{}
private sealed class TestType35{}
private sealed class TestType36{}
private sealed class TestType37{}
private sealed class TestType38{}
private sealed class TestType39{}
private sealed class TestType40{}
private sealed class TestType41{}
private sealed class TestType42{}
private sealed class TestType43{}
private sealed class TestType44{}
private sealed class TestType45{}
private sealed class TestType46{}
private sealed class TestType47{}
private sealed class TestType48{}
private sealed class TestType49{}
private sealed class TestType50{}
private sealed class TestType51{}
private sealed class TestType52{}
private sealed class TestType53{}
private sealed class TestType54{}
private sealed class TestType55{}
private sealed class TestType56{}
private sealed class TestType57{}
private sealed class TestType58{}
private sealed class TestType59{}
private sealed class TestType60{}
private sealed class TestType61{}
private sealed class TestType62{}
private sealed class TestType63{}
private sealed class TestType64{}
private sealed class TestType65{}
private sealed class TestType66{}
private sealed class TestType67{}
private sealed class TestType68{}
private sealed class TestType69{}
private sealed class TestType70{}
private sealed class TestType71{}
private sealed class TestType72{}
private sealed class TestType73{}
private sealed class TestType74{}
private sealed class TestType75{}
private sealed class TestType76{}
private sealed class TestType77{}
private sealed class TestType78{}
private sealed class TestType79{}
private sealed class TestType80{}
private sealed class TestType81{}
private sealed class TestType82{}
private sealed class TestType83{}
private sealed class TestType84{}
private sealed class TestType85{}
private sealed class TestType86{}
private sealed class TestType87{}
private sealed class TestType88{}
private sealed class TestType89{}
private sealed class TestType90{}
private sealed class TestType91{}
private sealed class TestType92{}
private sealed class TestType93{}
private sealed class TestType94{}
private sealed class TestType95{}
private sealed class TestType96{}
private sealed class TestType97{}
private sealed class TestType98{}
private sealed class TestType99{}
// ReSharper restore UnusedType.Local
// @formatter:on
}

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";
@@ -46,25 +46,25 @@ namespace Robust.Benchmarks.Serialization.Copy
[Benchmark]
public string? CreateCopyString()
{
return SerializationManager.CreateCopy(String);
return SerializationManager.Copy(String);
}
[Benchmark]
public int? CreateCopyInteger()
{
return SerializationManager.CreateCopy(Integer);
return SerializationManager.Copy(Integer);
}
[Benchmark]
public DataDefinitionWithString? CreateCopyDataDefinitionWithString()
{
return SerializationManager.CreateCopy(DataDefinitionWithString);
return SerializationManager.Copy(DataDefinitionWithString);
}
[Benchmark]
public SeedDataDefinition? CreateCopySeedDataDefinition()
{
return SerializationManager.CreateCopy(Seed);
return SerializationManager.Copy(Seed);
}
[Benchmark]

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

@@ -76,7 +76,7 @@ namespace Robust.Build.Tasks
},
ContentAttributes =
{
typeSystem.GetType("Robust.Client.UserInterface.XAML.ContentAttribute")
typeSystem.GetType("Avalonia.Metadata.ContentAttribute")
},
UsableDuringInitializationAttributes =
{

View File

@@ -1,7 +1,4 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.ContentPack;
using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
@@ -19,24 +16,11 @@ namespace Robust.Client.WebView.Cef
Array.Copy(args, 0, argv, 1, args.Length);
argv[0] = "-";
}
/*
if (OperatingSystem.IsLinux())
{
// Chromium tries to load libEGL.so and libGLESv2.so relative to the process executable on Linux.
// (Compared to Windows where it is relative to Chromium's *module*)
// There is a TODO "is this correct?" in the Chromium code for this.
// Great.
//CopyDllToExecutableDir("libEGL.so");
//CopyDllToExecutableDir("libGLESv2.so");
// System.Threading.Thread.Sleep(200000);
}
*/
var mainArgs = new CefMainArgs(argv);
// This will block executing until the subprocess is shut down.
var code = CefRuntime.ExecuteProcess(mainArgs, null, IntPtr.Zero);
var code = CefRuntime.ExecuteProcess(mainArgs, new RobustCefApp(), IntPtr.Zero);
if (code != 0)
{
@@ -45,44 +29,5 @@ namespace Robust.Client.WebView.Cef
return code;
}
/* private static void CopyDllToExecutableDir(string dllName)
{
var executableDir = PathHelpers.GetExecutableDirectory();
var targetPath = Path.Combine(executableDir, dllName);
if (File.Exists(targetPath))
return;
// Find source file.
string? srcFile = null;
foreach (var searchDir in WebViewManagerCef.NativeDllSearchDirectories())
{
var searchPath = Path.Combine(searchDir, dllName);
if (File.Exists(searchPath))
{
srcFile = searchPath;
break;
}
}
if (srcFile == null)
return;
for (var i = 0; i < 5; i++)
{
try
{
if (File.Exists(targetPath))
return;
File.Copy(srcFile, targetPath);
return;
}
catch
{
// Catching race condition lock errors and stuff I guess.
}
}
} */
}
}

View File

@@ -5,7 +5,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
internal class RobustCefApp : CefApp
internal sealed class RobustCefApp : CefApp
{
private readonly BrowserProcessHandler _browserProcessHandler = new();
private readonly RenderProcessHandler _renderProcessHandler = new();
@@ -24,12 +24,12 @@ namespace Robust.Client.WebView.Cef
{
// Disable zygote on Linux.
commandLine.AppendSwitch("--no-zygote");
// Work around https://bitbucket.org/chromiumembedded/cef/issues/3213/ozone-egl-initialization-does-not-have
// Desktop GL force makes Chromium not try to load its own ANGLE/Swiftshader so load paths aren't problematic.
if (OperatingSystem.IsLinux())
commandLine.AppendSwitch("--use-gl", "desktop");
// commandLine.AppendSwitch("--single-process");
//commandLine.AppendSwitch("--disable-gpu");
@@ -43,12 +43,18 @@ namespace Robust.Client.WebView.Cef
Logger.Debug($"{commandLine}");
}
private class BrowserProcessHandler : CefBrowserProcessHandler
protected override void OnRegisterCustomSchemes(CefSchemeRegistrar registrar)
{
registrar.AddCustomScheme("res", CefSchemeOptions.Secure | CefSchemeOptions.Standard);
registrar.AddCustomScheme("usr", CefSchemeOptions.Secure | CefSchemeOptions.Standard);
}
private sealed class BrowserProcessHandler : CefBrowserProcessHandler
{
}
// TODO CEF: Research - Is this even needed?
private class RenderProcessHandler : CefRenderProcessHandler
private sealed class RenderProcessHandler : CefRenderProcessHandler
{
}
}

View File

@@ -3,7 +3,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
// Simple CEF client.
internal class RobustCefClient : CefClient
internal sealed class RobustCefClient : CefClient
{
private readonly CefRenderHandler _renderHandler;
private readonly CefRequestHandler _requestHandler;

View File

@@ -57,6 +57,14 @@ namespace Robust.Client.WebView.Cef
string requestInitiator,
ref bool disableDefaultHandling)
{
var url = new Uri(request.Url);
if (url.Scheme == "file")
{
// Deny file:// access.
disableDefaultHandling = true;
return null;
}
lock (_resourceRequestHandlers)
{
_sawmill.Debug($"HANDLING REQUEST: {request.Url}");
@@ -113,7 +121,7 @@ namespace Robust.Client.WebView.Cef
CefFrame frame,
CefRequest request)
{
return null;
return new CookieHandler();
}
protected override CefResourceHandler GetResourceHandler(
@@ -124,5 +132,18 @@ namespace Robust.Client.WebView.Cef
return _handler;
}
}
private sealed class CookieHandler : CefCookieAccessFilter
{
protected override bool CanSendCookie(CefBrowser browser, CefFrame frame, CefRequest request, CefCookie cookie)
{
return true;
}
protected override bool CanSaveCookie(CefBrowser browser, CefFrame frame, CefRequest request, CefResponse response, CefCookie cookie)
{
return true;
}
}
}
}

View File

@@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Utility;
namespace Robust.Client.WebView.Cef;
internal sealed partial class WebViewManagerCef
{
// Loosely based on:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
private readonly Dictionary<string, string> _resourceMimeTypes = new()
{
{ "aac", "audio/aac" },
{ "avif", "image/avif" },
{ "avi", "video/x-msvideo" },
{ "bmp", "image/bmp" },
{ "css", "text/css" },
{ "gif", "image/gif" },
{ "htm", "text/html" },
{ "html", "text/html" },
{ "ico", "image/vnd.microsoft.icon" },
{ "jpeg", "image/jpeg" },
{ "jpg", "image/jpeg" },
{ "js", "text/javascript" },
{ "json", "application/json" },
{ "jsonld", "application/ld+json" },
{ "midi", "audio/midi" },
{ "mid", "audio/midi" },
{ "mjs", "text/javascript" },
{ "mp3", "audio/mpeg" },
{ "mp4", "video/mp4" },
{ "mpeg", "video/mpeg" },
{ "oga", "audio/ogg" },
{ "ogg", "audio/ogg" },
{ "ogv", "video/ogg" },
{ "ogx", "application/ogg" },
{ "opus", "audio/opus" },
{ "otf", "font/otf" },
{ "png", "image/png" },
{ "pdf", "application/pdf" },
{ "svg", "image/svg+xml" },
{ "tiff", "image/tiff" },
{ "tif", "image/tiff" },
{ "ts", "video/mp2t" },
{ "ttf", "font/ttf" },
{ "txt", "text/plain" },
{ "wav", "audio/wav" },
{ "weba", "audio/webm" },
{ "webm", "video/webm" },
{ "webp", "image/webp" },
{ "woff", "font/woff" },
{ "woff2", "font/woff2" },
{ "xhtml", "application/xhtml+xml" },
{ "xml", "application/xml" },
{ "zip", "application/zip" },
};
public void SetResourceMimeType(string extension, string mimeType)
{
DebugTools.Assert(!extension.StartsWith("."), "SetResourceMimeType extension must not include starting dot.");
lock (_resourceMimeTypes)
{
_resourceMimeTypes[extension] = mimeType;
}
}
public bool TryGetResourceMimeType(string extension, [NotNullWhen(true)] out string? mimeType)
{
lock (_resourceMimeTypes)
{
return _resourceMimeTypes.TryGetValue(extension, out mimeType);
}
}
}

View File

@@ -1,15 +1,21 @@
using System;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using Robust.Client.Console;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
internal partial class WebViewManagerCef : IWebViewManagerImpl
internal sealed partial class WebViewManagerCef : IWebViewManagerImpl
{
private static readonly string BasePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!;
@@ -17,11 +23,19 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
public void Initialize()
{
IoCManager.Instance!.InjectDependencies(this, oneOff: true);
_consoleHost.RegisterCommand("flushcookies", Loc.GetString("cmd-flushcookies-desc"), Loc.GetString("cmd-flushcookies-help"), (_, _, _) =>
{
CefCookieManager.GetGlobal(null).FlushStore(null);
});
string subProcessName;
if (OperatingSystem.IsWindows())
subProcessName = "Robust.Client.WebView.exe";
@@ -38,6 +52,10 @@ namespace Robust.Client.WebView.Cef
if (cefResourcesPath == null)
throw new InvalidOperationException("Unable to locate cef_resources directory!");
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
cachePath = userData.GetFullPath(new ResourcePath("/cef_cache"));
var settings = new CefSettings()
{
WindowlessRenderingEnabled = true, // So we can render to our UI controls.
@@ -47,6 +65,8 @@ namespace Robust.Client.WebView.Cef
LocalesDirPath = Path.Combine(cefResourcesPath, "locales"),
ResourcesDirPath = cefResourcesPath,
RemoteDebuggingPort = 9222,
CookieableSchemesList = "usr,res",
CachePath = cachePath,
};
Logger.Info($"CEF Version: {CefRuntime.ChromeVersion}");
@@ -59,6 +79,12 @@ namespace Robust.Client.WebView.Cef
// TODO CEF: After this point, debugging breaks. No, literally. My client crashes but ONLY with the debugger.
// I have tried using the DEBUG and RELEASE versions of libcef.so, stripped or non-stripped...
// And nothing seemed to work. Odd.
if (_cfg.GetCVar(WCVars.WebResProtocol))
{
var handler = new ResourceSchemeFactoryHandler(this, _resourceManager, Logger.GetSawmill("web.res"));
CefRuntime.RegisterSchemeHandlerFactory("res", "", handler);
}
}
private static string? LocateCefResources()
@@ -102,5 +128,48 @@ namespace Robust.Client.WebView.Cef
{
CefRuntime.Shutdown();
}
private sealed class ResourceSchemeFactoryHandler : CefSchemeHandlerFactory
{
private readonly WebViewManagerCef _parent;
private readonly IResourceManager _resourceManager;
private readonly ISawmill _sawmill;
public ResourceSchemeFactoryHandler(
WebViewManagerCef parent,
IResourceManager resourceManager,
ISawmill sawmill)
{
_parent = parent;
_resourceManager = resourceManager;
_sawmill = sawmill;
}
protected override CefResourceHandler Create(
CefBrowser browser,
CefFrame frame,
string schemeName,
CefRequest request)
{
var uri = new Uri(request.Url);
_sawmill.Debug($"HANDLING: {request.Url}");
var resourcePath = new ResourcePath(uri.AbsolutePath);
if (_resourceManager.TryContentFileRead(resourcePath, out var stream))
{
if (!_parent.TryGetResourceMimeType(resourcePath.Extension, out var mime))
mime = "application/octet-stream";
return new RequestResultStream(stream, mime, HttpStatusCode.OK).MakeHandler();
}
var notFoundStream = new MemoryStream();
notFoundStream.Write(Encoding.UTF8.GetBytes("Not found"));
notFoundStream.Position = 0;
return new RequestResultStream(notFoundStream, "text/plain", HttpStatusCode.NotFound).MakeHandler();
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
@@ -11,6 +12,17 @@ namespace Robust.Client.WebView.Headless
return new WebViewWindowDummy();
}
public void SetResourceMimeType(string extension, string mimeType)
{
// Nop
}
public bool TryGetResourceMimeType(string extension, [NotNullWhen(true)] out string? mimeType)
{
mimeType = null;
return false;
}
public IWebViewControlImpl MakeControlImpl(WebViewControl owner)
{
return new WebViewControlImplDummy();

View File

@@ -1,7 +1,40 @@
namespace Robust.Client.WebView
using System.Diagnostics.CodeAnalysis;
namespace Robust.Client.WebView
{
public interface IWebViewManager
{
IWebViewWindow CreateBrowserWindow(BrowserWindowCreateParameters createParams);
/// <summary>
/// Overrides file extension -> mime type mappings for the <c>res://</c> protocol.
/// </summary>
/// <remarks>
/// <para>
/// The built-in <c>res://</c> protocol needs to guess MIME types to report to CEF when resolving files.
/// A limited set of extensions have pre-set MIME types in the engine.
/// This method allows you to replace or add entries if need be.
/// </para>
/// <para>
/// This method is thread safe.
/// </para>
/// </remarks>
/// <param name="extension">
/// The extension to specify the MIME type for.
/// The argument must not include the starting "." of the file extension.
/// </param>
/// <param name="mimeType">The mime type for this file extension.</param>
/// <seealso cref="TryGetResourceMimeType"/>
void SetResourceMimeType(string extension, string mimeType);
/// <summary>
/// Tries to resolve an entry from the <see cref="SetResourceMimeType"/> list.
/// </summary>
/// <remarks>
/// <para>
/// This method is thread safe.
/// </para>
/// </remarks>
bool TryGetResourceMimeType(string extension, [NotNullWhen(true)] out string? mimeType);
}
}

View File

@@ -14,7 +14,7 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" />
<PackageReference Include="Robust.Natives.Cef" Version="95.7.14" />
<PackageReference Include="Robust.Natives.Cef" Version="102.0.9" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,17 @@
using Robust.Shared.Configuration;
namespace Robust.Client.WebView;
// ReSharper disable once InconsistentNaming
/// <summary>
/// CVars for <c>Robust.Client.WebView</c>
/// </summary>
[CVarDefs]
public static class WCVars
{
/// <summary>
/// Enable the <c>res://</c> protocol inside WebView browsers, allowing access to the Robust resources.
/// </summary>
public static readonly CVarDef<bool> WebResProtocol =
CVarDef.Create("web.res_protocol", true, CVar.CLIENTONLY);
}

View File

@@ -1,7 +1,9 @@
using Robust.Client.WebView;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.WebView;
using Robust.Client.WebView.Cef;
using Robust.Client.WebView.Headless;
using Robust.Client.WebViewHook;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
@@ -17,6 +19,9 @@ namespace Robust.Client.WebView
{
DebugTools.Assert(_impl == null, "WebViewManager has already been initialized!");
var cfg = IoCManager.Resolve<IConfigurationManagerInternal>();
cfg.LoadCVarsFromAssembly(typeof(WebViewManager).Assembly);
IoCManager.RegisterInstance<IWebViewManager>(this);
IoCManager.RegisterInstance<IWebViewManagerInternal>(this);
@@ -49,6 +54,20 @@ namespace Robust.Client.WebView
return _impl!.CreateBrowserWindow(createParams);
}
public void SetResourceMimeType(string extension, string mimeType)
{
DebugTools.Assert(_impl != null, "WebViewManager has not yet been initialized!");
_impl!.SetResourceMimeType(extension, mimeType);
}
public bool TryGetResourceMimeType(string extension, [NotNullWhen(true)] out string? mimeType)
{
DebugTools.Assert(_impl != null, "WebViewManager has not yet been initialized!");
return _impl!.TryGetResourceMimeType(extension, out mimeType);
}
public IWebViewControlImpl MakeControlImpl(WebViewControl owner)
{
DebugTools.Assert(_impl != null, "WebViewManager has not yet been initialized!");

View File

@@ -37,7 +37,7 @@ namespace Robust.Client.Animations
var keyFrame = KeyFrames[keyFrameIndex];
SoundSystem.Play(Filter.Local(), keyFrame.Resource, entity, keyFrame.AudioParamsFunc.Invoke());
SoundSystem.Play(keyFrame.Resource, Filter.Local(), entity, keyFrame.AudioParamsFunc.Invoke());
}
return (keyFrameIndex, playingTime);

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Robust.Shared.Animations;
using Robust.Shared.Maths;
@@ -101,7 +101,7 @@ namespace Robust.Client.Animations
case double d:
return MathHelper.Lerp(d, (double) b, t);
case Angle angle:
return (Angle) MathHelper.Lerp(angle, (Angle) b, t);
return Angle.Lerp(angle, (Angle) b, t);
case Color color:
return Color.InterpolateBetween(color, (Color) b, t);
case int i:

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,181 @@
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; }
MidiRendererState RendererState { get; }
/// <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>
/// Apply a certain state to the renderer.
/// </summary>
void ApplyState(MidiRendererState state);
/// <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,269 +16,279 @@ 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/default.dls",
"/usr/share/soundfonts/FluidR3_GM.sf2",
"/usr/share/soundfonts/freepats-general-midi.sf2",
"/usr/share/sounds/sf2/default.sf2",
"/usr/share/sounds/sf2/default.dls",
"/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 const float MaxDistanceForOcclusion = 1000;
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";
return null;
}
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 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
{
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);
}
soundfontLoader.SetCallbacks(_soundfontLoaderCallbacks);
public IMidiRenderer? GetNewRenderer()
{
if (!FluidsynthInitialized)
var renderer = new MidiRenderer(_settings!, soundfontLoader, mono, this, _clydeAudio, _taskManager, _midiSawmill);
// Since the last loaded soundfont takes priority, we load the fallback soundfont before the soundfont.
renderer.LoadSoundfont(FallbackSoundfont);
if (OperatingSystem.IsLinux())
{
InitializeFluidsynth();
if (!FluidsynthInitialized) // init failed
foreach (var filepath in LinuxSoundfonts)
{
return null;
}
}
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath)) continue;
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)
try
{
if (!File.Exists(filepath) || !SoundFont.IsSoundFont(filepath)) continue;
try
{
renderer.LoadSoundfont(filepath, true);
}
catch (Exception)
{
continue;
}
break;
renderer.LoadSoundfont(filepath, true);
}
catch (Exception)
{
continue;
}
}
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);
break;
}
return renderer;
}
finally
else if (OperatingSystem.IsMacOS())
{
handle.Free();
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);
}
// Load content-specific custom soundfonts, which could override the system/fallback soundfont.
foreach (var file in _resourceManager.ContentFindFiles(("/Audio/MidiCustom/")))
{
if (file.Extension != "sf2" && file.Extension != "dls") continue;
renderer.LoadSoundfont(file.ToString());
}
// Load every soundfont from the user data directory last, since those may override any other soundfont.
_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)
{
_renderers.Add(renderer);
}
return renderer;
}
finally
{
handle.Free();
}
}
public void FrameUpdate(float frameTime)
{
if (!FluidsynthInitialized)
{
return;
}
public void FrameUpdate(float frameTime)
// Update positions of streams every frame.
lock (_renderers)
{
if (!FluidsynthInitialized)
{
return;
}
// Update positions of streams every frame.
foreach (var renderer in _renderers)
{
if (renderer.Disposed)
@@ -318,7 +329,7 @@ namespace Robust.Client.Audio.Midi
pos.Position,
sourceRelative.Normalized,
OcclusionCollisionMask),
sourceRelative.Length,
MathF.Min(sourceRelative.Length, MaxDistanceForOcclusion),
renderer.TrackingEntity);
}
@@ -339,80 +350,109 @@ namespace Robust.Client.Audio.Midi
renderer.Source.SetOcclusion(float.MaxValue);
}
}
_volumeDirty = false;
}
/// <summary>
/// Main method for the thread rendering the midi audio.
/// </summary>
private void ThreadUpdate()
_volumeDirty = false;
}
/// <summary>
/// Main method for the thread rendering the midi audio.
/// </summary>
private void ThreadUpdate()
{
while (_alive)
{
while (_alive)
{
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);
}
}
public void Shutdown()
{
_alive = false;
_midiThread?.Join();
_settings?.Dispose();
lock (_renderers)
{
foreach (var renderer in _renderers)
for (var i = 0; i < _renderers.Count; i++)
{
renderer?.Dispose();
var renderer = _renderers[i];
if (!renderer.Disposed)
renderer.Render();
else
{
renderer.InternalDispose();
_renderers.Remove(renderer);
}
}
}
if (FluidsynthInitialized && !_failedInitialize)
Thread.Sleep(1);
}
}
public void Shutdown()
{
_alive = false;
_midiThread?.Join();
_settings?.Dispose();
lock (_renderers)
{
foreach (var renderer in _renderers)
{
NFluidsynth.Logger.SetLoggerMethod(null);
renderer?.Dispose();
}
}
/// <summary>
/// This class is used to load soundfonts.
/// </summary>
private sealed class ResourceLoaderCallbacks : SoundFontLoaderCallbacks
if (FluidsynthInitialized && !_failedInitialize)
{
private readonly Dictionary<int, Stream> _openStreams = new();
private int _nextStreamId = 1;
NFluidsynth.Logger.SetLoggerMethod(null);
}
}
public override IntPtr Open(string filename)
/// <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))
{
if (string.IsNullOrEmpty(filename))
{
return IntPtr.Zero;
}
return IntPtr.Zero;
}
Stream? stream;
var resourceCache = IoCManager.Resolve<IResourceCache>();
var resourcePath = new ResourcePath(filename);
Stream? stream;
var resourceCache = IoCManager.Resolve<IResourceCache>();
var resourcePath = new ResourcePath(filename);
if (resourcePath.IsRooted && resourceCache.ContentFileExists(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 +461,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

@@ -0,0 +1,80 @@
using System;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
namespace Robust.Client.Audio.Midi;
public struct MidiRendererState
{
internal FixedArray16<FixedArray128<byte>> NoteVelocities;
internal FixedArray16<FixedArray128<byte>> Controllers;
internal FixedArray16<byte> Program;
internal FixedArray16<byte> ChannelPressure;
internal FixedArray16<ushort> PitchBend;
internal Span<byte> AsSpan => MemoryMarshal.CreateSpan(ref NoteVelocities._00._00, 4160);
static unsafe MidiRendererState()
{
var s = new MidiRendererState();
DebugTools.Assert(s.AsSpan.Length == sizeof(MidiRendererState),
$"{nameof(MidiRendererState)}'s {nameof(AsSpan)} length does not match struct size! Was: {s.AsSpan.Length} Expected: {sizeof(MidiRendererState)}");
}
public MidiRendererState()
{
NoteVelocities = default;
Program = default;
ChannelPressure = default;
PitchBend = default;
Controllers = default;
// PitchBend is at 8192 by default.
PitchBend.AsSpan.Fill(8192);
// Controller defaults
Controllers.AsSpan.Fill(new FixedArray128<byte>
{
// Bank selection default
_00 = 0,
// Volume controller default
_07 = 100,
// Balance controller default
_08 = 64,
// Pan controller default
_10 = 64,
// Expression controller default
_11 = 127,
// Controller 11 default
_43 = 127,
// Sound controllers 1 to 10 defaults
_70 = 64,
_71 = 64,
_72 = 64,
_73 = 64,
_74 = 64,
_75 = 64,
_76 = 64,
_77 = 64,
_78 = 64,
_79 = 64,
// Portamento default
_84 = 255,
// Non-Registered Parameter Number defaults
_98 = 127, // LSB
_99 = 127, // MSB
// Registered Parameter Number defaults
_100 = 127, // LSB
_101 = 127, // MSB
});
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Net;
using Robust.Client.Debugging;
using Robust.Client.GameObjects;
@@ -13,6 +13,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -45,6 +46,8 @@ namespace Robust.Client
public string? LastDisconnectReason { get; private set; }
private (TimeSpan, GameTick) _timeBase;
/// <inheritdoc />
public void Initialize()
{
@@ -52,20 +55,34 @@ namespace Robust.Client
_net.ConnectFailed += OnConnectFailed;
_net.Disconnect += OnNetDisconnect;
_net.RegisterNetMessage<MsgSyncTimeBase>(
SyncTimeBase,
NetMessageAccept.Handshake | NetMessageAccept.Client);
_configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged, invokeImmediately: true);
_playMan.Initialize();
Reset();
}
private void TickRateChanged(int tickrate)
private void SyncTimeBase(MsgSyncTimeBase message)
{
Logger.DebugS("client", $"Synchronized time base: {message.Tick}: {message.Time}");
if (RunLevel >= ClientRunLevel.Connected)
_timing.TimeBase = (message.Time, message.Tick);
else
_timeBase = (message.Time, message.Tick);
}
private void TickRateChanged(int tickrate, in CVarChangeInfo info)
{
if (GameInfo != null)
{
GameInfo.TickRate = (byte) tickrate;
}
_timing.TickRate = (byte) tickrate;
_timing.SetTickRateAt((byte) tickrate, info.TickChanged);
Logger.InfoS("client", $"Tickrate changed to: {tickrate} on tick {_timing.CurTick}");
}
@@ -74,7 +91,7 @@ namespace Robust.Client
{
if (RunLevel == ClientRunLevel.Connecting)
{
_net.Shutdown("Client mashing that connect button.");
_net.Reset("Client mashing that connect button.");
Reset();
}
@@ -202,7 +219,9 @@ namespace Robust.Client
{
DebugTools.Assert(RunLevel > ClientRunLevel.Initialize);
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalPlayer?.Session));
// Don't invoke PlayerLeaveServer if PlayerJoinedServer & GameStartedSetup hasn't been called yet.
if (RunLevel > ClientRunLevel.Connecting)
PlayerLeaveServer?.Invoke(this, new PlayerEventArgs(_playMan.LocalPlayer?.Session));
LastDisconnectReason = args.Reason;
GameStoppedReset();
@@ -213,7 +232,7 @@ namespace Robust.Client
_entityManager.Startup();
_mapManager.Startup();
_timing.ResetSimTime();
_timing.ResetSimTime(_timeBase);
_timing.Paused = false;
}

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;
@@ -61,6 +62,8 @@ namespace Robust.Client
IoCManager.Register<IResourceCache, ResourceCache>();
IoCManager.Register<IResourceCacheInternal, ResourceCache>();
IoCManager.Register<IClientNetManager, NetManager>();
IoCManager.Register<EntityManager, ClientEntityManager>();
IoCManager.Register<ClientEntityManager>();
IoCManager.Register<IClientEntityManager, ClientEntityManager>();
IoCManager.Register<IClientEntityManagerInternal, ClientEntityManager>();
IoCManager.Register<IEntityNetworkManager, ClientEntityManager>();
@@ -75,6 +78,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

@@ -0,0 +1,98 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Robust.Shared;
using Robust.Shared.Console;
using Robust.Shared.Network.Messages;
namespace Robust.Client.Console;
internal sealed partial class ClientConsoleHost
{
private readonly Dictionary<int, PendingCompletion> _completionsPending = new();
private int _completionSeq;
public async Task<CompletionResult> GetCompletions(List<string> args, CancellationToken cancel)
{
// Last element is the command currently being typed. May be empty.
// Logger.Debug($"Running completions: {string.Join(", ", args)}");
var delay = _cfg.GetCVar(CVars.ConCompletionDelay);
if (delay > 0)
await Task.Delay((int)(delay * 1000), cancel);
return await CalcCompletions(args, cancel);
}
private Task<CompletionResult> CalcCompletions(List<string> args, CancellationToken cancel)
{
if (args.Count == 1)
{
// Typing out command name, handle this ourselves.
var cmdOptions = CompletionResult.FromOptions(
RegisteredCommands.Values
.Where(c => CanExecute(c.Command))
.OrderBy(c => c.Command)
.Select(c => new CompletionOption(c.Command, c.Description)));
return Task.FromResult(cmdOptions);
}
if (!RegisteredCommands.TryGetValue(args[0], out var cmd))
return Task.FromResult(CompletionResult.Empty);
return cmd.GetCompletionAsync(LocalShell, args.ToArray()[1..], cancel).AsTask();
}
private Task<CompletionResult> DoServerCompletions(List<string> args, CancellationToken cancel)
{
var tcs = new TaskCompletionSource<CompletionResult>();
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancel);
var seq = _completionSeq++;
var pending = new PendingCompletion
{
Cts = cts,
Tcs = tcs
};
var msg = new MsgConCompletion
{
Args = args.ToArray(),
Seq = seq
};
cts.Token.Register(() =>
{
tcs.SetCanceled(cts.Token);
cts.Dispose();
_completionsPending.Remove(seq);
}, true);
NetManager.ClientSendMessage(msg);
_completionsPending.Add(seq, pending);
return tcs.Task;
}
private void ProcessCompletionResp(MsgConCompletionResp message)
{
if (!_completionsPending.TryGetValue(message.Seq, out var pending))
return;
pending.Cts.Dispose();
pending.Tcs.SetResult(message.Result);
_completionsPending.Remove(message.Seq);
}
private struct PendingCompletion
{
public TaskCompletionSource<CompletionResult> Tcs;
public CancellationTokenSource Cts;
}
}

View File

@@ -1,10 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Robust.Client.Log;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
@@ -41,18 +46,24 @@ namespace Robust.Client.Console
}
/// <inheritdoc cref="IClientConsoleHost" />
internal sealed class ClientConsoleHost : ConsoleHost, IClientConsoleHost
internal sealed partial class ClientConsoleHost : ConsoleHost, IClientConsoleHost, IConsoleHostInternal
{
[Dependency] private readonly IClientConGroupController _conGroup = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPlayerManager _player = default!;
private bool _requestedCommands;
public ClientConsoleHost() : base(isServer: false) {}
/// <inheritdoc />
public void Initialize()
{
NetManager.RegisterNetMessage<MsgConCmdReg>(HandleConCmdReg);
NetManager.RegisterNetMessage<MsgConCmdAck>(HandleConCmdAck);
NetManager.RegisterNetMessage<MsgConCmd>(ProcessCommand);
NetManager.RegisterNetMessage<MsgConCompletion>();
NetManager.RegisterNetMessage<MsgConCompletionResp>(ProcessCompletionResp);
_requestedCommands = false;
NetManager.Connected += OnNetworkConnected;
@@ -88,6 +99,11 @@ namespace Robust.Client.Console
OutputText(text, true, true);
}
public bool IsCmdServer(IConsoleCommand cmd)
{
return cmd is ServerDummyCommand;
}
public override event ConAnyCommandCallback? AnyCommandExecuted;
/// <inheritdoc />
@@ -108,14 +124,12 @@ namespace Robust.Client.Console
if (AvailableCommands.ContainsKey(commandName))
{
var playerManager = IoCManager.Resolve<IPlayerManager>();
#if !DEBUG
if (!_conGroup.CanCommand(commandName) && playerManager.LocalPlayer?.Session.Status > SessionStatus.Connecting)
if (!CanExecute(commandName))
{
WriteError(null, $"Insufficient perms for command: {commandName}");
return;
}
#endif
var command1 = AvailableCommands[commandName];
args.RemoveAt(0);
var shell = new ConsoleShell(this, null);
@@ -128,13 +142,23 @@ namespace Robust.Client.Console
WriteError(null, "Unknown command: " + commandName);
}
private bool CanExecute(string cmdName)
{
// When not connected to a server, you can run all local commands.
// When connected to a server, you can only run commands according to the con group controller.
return _player.LocalPlayer == null
|| _player.LocalPlayer.Session.Status <= SessionStatus.Connecting
|| _conGroup.CanCommand(cmdName);
}
/// <inheritdoc />
public override void RemoteExecuteCommand(ICommonSession? session, string command)
{
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,36 +222,69 @@ namespace Robust.Client.Console
if (!NetManager.IsConnected)
return;
var msg = NetManager.CreateNetMessage<MsgConCmdReg>();
var msg = new MsgConCmdReg();
NetManager.ClientSendMessage(msg);
_requestedCommands = true;
}
}
/// <summary>
/// These dummies are made purely so list and help can list server-side commands.
/// </summary>
[Reflect(false)]
internal sealed class ServerDummyCommand : IConsoleCommand
{
internal ServerDummyCommand(string command, string help, string description)
/// <summary>
/// These dummies are made purely so list and help can list server-side commands.
/// </summary>
[Reflect(false)]
private sealed class ServerDummyCommand : IConsoleCommand
{
Command = command;
Help = help;
Description = description;
internal ServerDummyCommand(string command, string help, string description)
{
Command = command;
Help = help;
Description = description;
}
public string Command { get; }
public string Description { get; }
public string Help { get; }
// Always forward to server.
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
shell.RemoteExecuteCommand(argStr);
}
public async ValueTask<CompletionResult> GetCompletionAsync(
IConsoleShell shell,
string[] args,
CancellationToken cancel)
{
var host = (ClientConsoleHost)shell.ConsoleHost;
var argsList = args.ToList();
argsList.Insert(0, Command);
return await host.DoServerCompletions(argsList, cancel);
}
}
public string Command { get; }
public string Description { get; }
public string Help { get; }
// Always forward to server.
public void Execute(IConsoleShell shell, string argStr, string[] args)
private sealed class RemoteExecCommand : IConsoleCommand
{
shell.RemoteExecuteCommand(argStr);
public string Command => ">";
public string Description => Loc.GetString("cmd-remoteexec-desc");
public string Help => Loc.GetString("cmd-remoteexec-help");
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
shell.RemoteExecuteCommand(argStr["> ".Length..]);
}
public async ValueTask<CompletionResult> GetCompletionAsync(
IConsoleShell shell,
string[] args,
CancellationToken cancel)
{
var host = (ClientConsoleHost)shell.ConsoleHost;
return await host.DoServerCompletions(args.ToList(), cancel);
}
}
}
}

View File

@@ -1,5 +1,3 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
@@ -7,57 +5,6 @@ using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
[UsedImplicitly]
internal sealed class CVarCommand : SharedCVarCommand, IConsoleCommand
{
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 1 || args.Length > 2)
{
shell.WriteError("Must provide exactly one or two arguments.");
return;
}
var configManager = IoCManager.Resolve<IConfigurationManager>();
var name = args[0];
if (name == "?")
{
var cvars = configManager.GetRegisteredCVars().OrderBy(c => c);
shell.WriteLine(string.Join("\n", cvars));
return;
}
if (!configManager.IsCVarRegistered(name))
{
shell.WriteError($"CVar '{name}' is not registered. Use 'cvar ?' to get a list of all registered CVars.");
return;
}
if (args.Length == 1)
{
// Read CVar
var value = configManager.GetCVar<object>(name);
shell.WriteLine(value.ToString() ?? "");
}
else
{
// Write CVar
var value = args[1];
var type = configManager.GetCVarType(name);
try
{
var parsed = ParseObject(type, value);
configManager.SetCVar(name, parsed);
}
catch (FormatException)
{
shell.WriteLine($"Input value is in incorrect format for type {type}");
}
}
}
}
[UsedImplicitly]
public sealed class SaveConfig : IConsoleCommand
{

View File

@@ -19,6 +19,7 @@ using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
@@ -97,54 +98,72 @@ namespace Robust.Client.Console.Commands
{
public string Command => "monitor";
public string Help =>
"Usage: monitor <name>\nPossible monitors are: fps, net, bandwidth, coord, time, frames, mem, clyde, input";
public string Description => Loc.GetString("cmd-monitor-desc");
public string Description => "Toggles a debug monitor in the F3 menu.";
public string Help
{
get
{
var monitors = string.Join(", ", Enum.GetNames<DebugMonitor>());
return Loc.GetString("cmd-monitor-help", ("monitors", monitors));
}
}
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var monitor = IoCManager.Resolve<IUserInterfaceManager>().DebugMonitors;
var monitors = IoCManager.Resolve<IUserInterfaceManager>().DebugMonitors;
if (args.Length != 1)
{
shell.WriteLine(Help);
shell.WriteLine(Loc.GetString("cmd-monitor-arg-count"));
return;
}
switch (args[0])
var monitorArg = args[0];
if (monitorArg.Equals("-all", StringComparison.OrdinalIgnoreCase))
{
case "fps":
monitor.ShowFPS ^= true;
break;
case "net":
monitor.ShowNet ^= true;
break;
case "bandwidth":
monitor.ShowNetBandwidth ^= true;
break;
case "coord":
monitor.ShowCoords ^= true;
break;
case "time":
monitor.ShowTime ^= true;
break;
case "frames":
monitor.ShowFrameGraph ^= true;
break;
case "mem":
monitor.ShowMemory ^= true;
break;
case "clyde":
monitor.ShowClyde ^= true;
break;
case "input":
monitor.ShowInput ^= true;
break;
default:
shell.WriteLine($"Invalid key: {args[0]}");
break;
foreach (var monitor in Enum.GetValues<DebugMonitor>())
{
monitors.SetMonitor(monitor, false);
}
return;
}
if (monitorArg.Equals("+all", StringComparison.OrdinalIgnoreCase))
{
foreach (var monitor in Enum.GetValues<DebugMonitor>())
{
monitors.SetMonitor(monitor, true);
}
return;
}
if (!Enum.TryParse(monitorArg, true, out DebugMonitor parsedMonitor))
{
shell.WriteError(Loc.GetString("cmd-monitor-invalid-name"));
return;
}
monitors.ToggleMonitor(parsedMonitor);
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
var allOptions = new CompletionOption[]
{
new("-all", Loc.GetString("cmd-monitor-minus-all-hint")),
new("+all", Loc.GetString("cmd-monitor-plus-all-hint"))
};
var options = allOptions.Concat(Enum.GetNames<DebugMonitor>().Select(c => new CompletionOption(c)));
return CompletionResult.FromHintOptions(options, Loc.GetString("cmd-monitor-arg-monitor"));
}
return CompletionResult.Empty;
}
}
@@ -739,103 +758,6 @@ namespace Robust.Client.Console.Commands
}
}
internal sealed class GcCommand : IConsoleCommand
{
public string Command => "gc";
public string Description => "Run the GC.";
public string Help => "gc [generation]";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)
{
GC.Collect();
}
else
{
if (int.TryParse(args[0], out int result))
GC.Collect(result);
else
shell.WriteError("Failed to parse argument.");
}
}
}
internal sealed class GcFullCommand : IConsoleCommand
{
public string Command => "gcf";
public string Description => "Run the GC, fully, compacting LOH and everything.";
public string Help => "gcf";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(2, GCCollectionMode.Forced, true, true);
}
}
internal sealed class GcModeCommand : IConsoleCommand
{
public string Command => "gc_mode";
public string Description => "Change/Read the GC Latency mode.";
public string Help => "gc_mode\nSee current GC Latencymode\ngc_mode [type]\n Change GC Latency mode to [type]";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var prevMode = GCSettings.LatencyMode;
if (args.Length == 0)
{
shell.WriteLine($"current gc latency mode: {(int) prevMode} ({prevMode})");
shell.WriteLine("possible modes:");
foreach (int mode in (int[]) Enum.GetValues(typeof(GCLatencyMode)))
{
shell.WriteLine($" {mode}: {Enum.GetName(typeof(GCLatencyMode), mode)}");
}
}
else
{
GCLatencyMode mode;
if (char.IsDigit(args[0][0]) && int.TryParse(args[0], out var modeNum))
{
mode = (GCLatencyMode) modeNum;
}
else if (!Enum.TryParse(args[0], true, out mode))
{
shell.WriteLine($"unknown gc latency mode: {args[0]}");
return;
}
shell.WriteLine($"attempting gc latency mode change: {(int) prevMode} ({prevMode}) -> {(int) mode} ({mode})");
GCSettings.LatencyMode = mode;
shell.WriteLine($"resulting gc latency mode: {(int) GCSettings.LatencyMode} ({GCSettings.LatencyMode})");
}
}
}
internal sealed class SerializeStatsCommand : IConsoleCommand
{
public string Command => "szr_stats";
public string Description => "Report serializer statistics.";
public string Help => "szr_stats";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
shell.WriteLine($"serialized: {RobustSerializer.BytesSerialized} bytes, {RobustSerializer.ObjectsSerialized} objects");
shell.WriteLine($"largest serialized: {RobustSerializer.LargestObjectSerializedBytes} bytes, {RobustSerializer.LargestObjectSerializedType} objects");
shell.WriteLine($"deserialized: {RobustSerializer.BytesDeserialized} bytes, {RobustSerializer.ObjectsDeserialized} objects");
shell.WriteLine($"largest serialized: {RobustSerializer.LargestObjectDeserializedBytes} bytes, {RobustSerializer.LargestObjectDeserializedType} objects");
}
}
internal sealed class ChunkInfoCommand : IConsoleCommand
{
public string Command => "chunkinfo";

View File

@@ -1,73 +0,0 @@
using System.Linq;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Network;
namespace Robust.Client.Console.Commands
{
sealed class HelpCommand : IConsoleCommand
{
public string Command => "help";
public string Help => "When no arguments are provided, displays a generic help text. When an argument is passed, display the help text for the command with that name.";
public string Description => "Display help text.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
switch (args.Length)
{
case 0:
shell.WriteLine("To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'.");
break;
case 1:
string commandname = args[0];
if (!shell.ConsoleHost.RegisteredCommands.ContainsKey(commandname))
{
if (!IoCManager.Resolve<IClientNetManager>().IsConnected)
{
// No server so nothing to respond with unknown command.
shell.WriteError("Unknown command: " + commandname);
return;
}
// TODO: Maybe have a server side help?
return;
}
IConsoleCommand command = shell.ConsoleHost.RegisteredCommands[commandname];
shell.WriteLine(string.Format("{0} - {1}", command.Command, command.Description));
shell.WriteLine(command.Help);
break;
default:
shell.WriteError("Invalid amount of arguments.");
break;
}
}
}
sealed class ListCommand : IConsoleCommand
{
public string Command => "list";
public string Help => "Usage: list [filter]\n" +
"Lists all available commands, and their short descriptions.\n" +
"If a filter is provided, " +
"only commands that contain the given string in their name will be listed.";
public string Description => "List all commands, optionally with a filter.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var filter = "";
if (args.Length == 1)
{
filter = args[0];
}
var conGroup = IoCManager.Resolve<IClientConGroupController>();
foreach (var command in shell.ConsoleHost.RegisteredCommands.Values
.Where(p => p.Command.Contains(filter) && (p is not ServerDummyCommand || conGroup.CanCommand(p.Command)))
.OrderBy(c => c.Command))
{
shell.WriteLine(command.Command + ": " + command.Description);
}
}
}
}

View File

@@ -1,27 +0,0 @@
using System.Linq;
using System.Runtime.Loader;
using JetBrains.Annotations;
using Robust.Shared.Console;
namespace Robust.Client.Console.Commands
{
[UsedImplicitly]
internal sealed class ListAssembliesCommand : IConsoleCommand
{
public string Command => "lsasm";
public string Description => "Lists loaded assemblies by load context.";
public string Help => Command;
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
foreach (var context in AssemblyLoadContext.All)
{
shell.WriteLine($"{context.Name}:");
foreach (var assembly in context.Assemblies.OrderBy(a => a.FullName))
{
shell.WriteLine($" {assembly.FullName}");
}
}
}
}
}

View File

@@ -1,73 +0,0 @@
using Robust.Shared.Log;
using System;
using Robust.Shared.Console;
namespace Robust.Client.Console.Commands
{
sealed class LogSetLevelCommand : IConsoleCommand
{
public string Command => "loglevel";
public string Description => "Changes the log level for a provided sawmill.";
public string Help => "Usage: loglevel <sawmill> <level>"
+ "\n sawmill: A label prefixing log messages. This is the one you're setting the level for."
+ "\n level: The log level. Must match one of the values of the LogLevel enum.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
return;
}
var name = args[0];
var levelname = args[1];
LogLevel? level;
if (levelname == "null")
{
level = null;
}
else
{
if (!Enum.TryParse<LogLevel>(levelname, out var result))
{
shell.WriteLine("Failed to parse 2nd argument. Must be one of the values of the LogLevel enum.");
return;
}
level = result;
}
Logger.GetSawmill(name).Level = level;
}
}
sealed class TestLog : IConsoleCommand
{
public string Command => "testlog";
public string Description => "Writes a test log to a sawmill.";
public string Help => "Usage: testlog <sawmill> <level> <message>"
+ "\n sawmill: A label prefixing the logged message."
+ "\n level: The log level. Must match one of the values of the LogLevel enum."
+ "\n message: The message to be logged. Wrap this in double quotes if you want to use spaces.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 3)
{
shell.WriteError("Invalid argument amount. Expected 3 arguments.");
return;
}
var name = args[0];
var levelname = args[1];
var message = args[2]; // yes this doesn't support spaces idgaf.
if (!Enum.TryParse<LogLevel>(levelname, out var result))
{
shell.WriteLine("Failed to parse 2nd argument. Must be one of the values of the LogLevel enum.");
return;
}
var level = result;
Logger.LogS(level, name, message);
}
}
}

View File

@@ -1,6 +1,7 @@
using Robust.Client.Debugging;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
namespace Robust.Client.Console.Commands
{
@@ -8,12 +9,13 @@ 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(Loc.GetString("shell-wrong-arguments-number-need-specific", ("properAmount", 1), ("currentAmount", args.Length)));
return;
}
@@ -43,11 +45,27 @@ namespace Robust.Client.Console.Commands
system.Flags ^= PhysicsDebugFlags.Shapes;
break;
default:
shell.WriteLine($"{args[0]} is not a recognised overlay");
shell.WriteError(Loc.GetString("cmd-physics-overlay", ("overlay", args[0])));
return;
}
return;
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length != 1) return CompletionResult.Empty;
return CompletionResult.FromOptions(new[]
{
"aabbs",
"com",
"contactnormals",
"contactpoints",
"joints",
"shapeinfo",
"shapes",
});
}
}
}

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

@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.Console;
using Robust.Shared.Utility;
@@ -15,5 +18,7 @@ namespace Robust.Client.Console
event EventHandler<AddFormattedMessageArgs> AddFormatted;
void AddFormattedLine(FormattedMessage message);
Task<CompletionResult> GetCompletions(List<string> args, CancellationToken cancel);
}
}

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

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Debugging;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
@@ -12,7 +13,7 @@ using Robust.Shared.Timing;
namespace Robust.Client.Debugging
{
internal sealed class DebugRayDrawingSystem : EntitySystem
internal sealed class DebugRayDrawingSystem : SharedDebugRayDrawingSystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IGameTiming _gameTimer = default!;
@@ -57,11 +58,9 @@ namespace Robust.Client.Debugging
{
base.Initialize();
SubscribeNetworkEvent<MsgRay>(HandleDrawRay);
// To catch anything that's client-only and not sent by the server.
SubscribeLocalEvent<DebugDrawRayMessage>(OnDebugDrawRay);
}
private void OnDebugDrawRay(DebugDrawRayMessage ev)
protected override void ReceiveLocalRayAtMainThread(DebugRayData ev)
{
if (!_debugDrawRays)
{
@@ -70,9 +69,9 @@ namespace Robust.Client.Debugging
var newRayWithLifetime = new RayWithLifetime
{
DidActuallyHit = ev.Data.Results != null,
RayOrigin = ev.Data.Ray.Position,
RayHit = ev.Data.Results?.HitPos ?? ev.Data.Ray.Direction * ev.Data.MaxLength + ev.Data.Ray.Position,
DidActuallyHit = ev.Results != null,
RayOrigin = ev.Ray.Position,
RayHit = ev.Results?.HitPos ?? ev.Ray.Direction * ev.MaxLength + ev.Ray.Position,
LifeTime = _gameTimer.RealTime + DebugRayLifetime
};

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)
{
Interlocked.Decrement(ref _hasRedialled);
throw;
}
Shutdown("Redial");
}
}

View File

@@ -32,6 +32,8 @@ namespace Robust.Client
throw new InvalidOperationException("Cannot start twice!");
}
GlibcBug.Check();
_hasStarted = true;
if (CommandLineArgs.TryParse(args, out var parsed))
@@ -101,7 +103,7 @@ namespace Robust.Client
ContinueStartupAndLoop(mode);
}
Cleanup();
CleanupWindowThread();
Logger.Debug("Goodbye");
IoCManager.Clear();
@@ -127,6 +129,8 @@ namespace Robust.Client
DebugTools.AssertNotNull(_mainLoop);
_mainLoop!.Run();
CleanupGameThread();
}
}
}

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));
@@ -102,7 +108,8 @@ namespace Robust.Client
// Disable load context usage on content start.
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
_modLoader.SetUseLoadContext(!ContentStart);
_modLoader.SetEnableSandboxing(Options.Sandboxing);
var disableSandbox = Environment.GetEnvironmentVariable("ROBUST_DISABLE_SANDBOX") == "1";
_modLoader.SetEnableSandboxing(!disableSandbox && Options.Sandboxing);
var assemblyPrefix = Options.ContentModulePrefix ?? _resourceManifest!.AssemblyPrefix ?? "Content.";
if (!_modLoader.TryLoadModulesFrom(Options.AssemblyDirectory, assemblyPrefix))
@@ -131,8 +138,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 +165,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 +205,7 @@ namespace Robust.Client
_clyde.Ready();
if (!Options.DisableCommandLineConnect &&
if (_resourceManifest!.AutoConnect &&
(_commandLineArgs?.Connect == true || _commandLineArgs?.Launcher == true)
&& LaunchState.ConnectEndpoint != null)
{
@@ -213,7 +221,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 +231,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 +266,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 +338,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 +464,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)
@@ -566,15 +659,20 @@ namespace Robust.Client
Clyde,
}
internal void Cleanup()
internal void CleanupGameThread()
{
_modLoader.Shutdown();
// CEF specifically makes a massive silent stink of it if we don't shut it down from the correct thread.
_webViewHook?.Shutdown();
_networkManager.Shutdown("Client shutting down");
_midiManager.Shutdown();
_entityManager.Shutdown();
}
internal void CleanupWindowThread()
{
_clyde.Shutdown();
_clydeAudio.Shutdown();
}
@@ -584,7 +682,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

@@ -1,4 +1,3 @@
using Robust.Shared.Console;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -8,8 +7,8 @@ namespace Robust.Client.GameObjects
{
internal sealed class ClientComponentFactory : ComponentFactory
{
public ClientComponentFactory(IDynamicTypeFactoryInternal typeFactory, IReflectionManager reflectionManager, IConsoleHost conHost)
: base(typeFactory, reflectionManager, conHost)
public ClientComponentFactory(IDynamicTypeFactoryInternal typeFactory, IReflectionManager reflectionManager)
: base(typeFactory, reflectionManager)
{
// Required for the engine to work
RegisterIgnore("KeyBindingInput");

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)
{
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

@@ -10,7 +10,7 @@ namespace Robust.Client.GameObjects;
/// This is the client instance of <see cref="AppearanceComponent"/>.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(AppearanceComponent)), Friend(typeof(AppearanceSystem))]
[ComponentReference(typeof(AppearanceComponent)), Access(typeof(AppearanceSystem))]
public sealed class ClientAppearanceComponent : AppearanceComponent
{
[ViewVariables]

View File

@@ -0,0 +1,25 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using System;
using System.Collections.Generic;
using static Robust.Shared.GameObjects.SharedSpriteComponent;
namespace Robust.Client.GameObjects;
/// <summary>
/// This component can be used to apply generic changes to an entity's sprite component as a result of appearance
/// data changes.
/// </summary>
[RegisterComponent]
[Access(typeof(GenericVisualizerSystem))]
public sealed class GenericVisualizerComponent : Component
{
/// <summary>
/// This is a nested dictionary that maps appearance data keys -> sprite layer keys -> appearance data values -> layer data.
/// While somewhat convoluted, this enables the sprite layer data to be completely modified using only yaml.
///
/// In most instances, each of these dictionaries will probably only have a single entry.
/// </summary>
[DataField("visuals", required:true)]
public Dictionary<Enum, Dictionary<string, Dictionary<string, PrototypeLayerData>>> Visuals = default!;
}

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))
@@ -44,7 +47,7 @@ namespace Robust.Client.GameObjects
return null;
}
return TextureForConfig((IconComponent)compData, resourceCache);
return TextureForConfig((IconComponent)compData.Component, resourceCache);
}
}
}

View File

@@ -81,7 +81,7 @@ namespace Robust.Client.GameObjects
Component = xform,
};
_entityManager.EventBus.RaiseLocalEvent(Owner, ref ev);
_entityManager.EventBus.RaiseLocalEvent(Owner, ref ev, true);
if (ev.Handled)
{

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!;
@@ -34,7 +34,7 @@ namespace Robust.Client.GameObjects
{
if (_enabled == value) return;
base.Enabled = value;
_entityManager.EventBus.RaiseLocalEvent(Owner, new PointLightUpdateEvent());
_entityManager.EventBus.RaiseLocalEvent(Owner, new PointLightUpdateEvent(), true);
}
}
@@ -47,7 +47,7 @@ namespace Robust.Client.GameObjects
if (_containerOccluded == value) return;
_containerOccluded = value;
_entityManager.EventBus.RaiseLocalEvent(Owner, new PointLightUpdateEvent());
_entityManager.EventBus.RaiseLocalEvent(Owner, new PointLightUpdateEvent(), true);
}
}

View File

@@ -75,16 +75,16 @@ namespace Robust.Client.GameObjects
protected internal override void Draw(in OverlayDrawArgs args)
{
var handle = args.WorldHandle;
var currentMap = _eyeManager.CurrentMap;
var viewport = _eyeManager.GetWorldViewbounds();
var currentMap = args.MapId;
var viewport = args.WorldBounds;
foreach (var comp in _renderTree.GetRenderTrees(currentMap, viewport))
{
var localAABB = _entityManager.GetComponent<TransformComponent>(comp.Owner).InvWorldMatrix.TransformBox(viewport);
foreach (var sprite in comp.SpriteTree.QueryAabb(localAABB))
foreach (var (sprite, xform) in comp.SpriteTree.QueryAabb(localAABB))
{
var (worldPos, worldRot) = _entityManager.GetComponent<TransformComponent>(sprite.Owner).GetWorldPositionRotation();
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot);
// Get scaled down bounds used to indicate the "south" of a sprite.

View File

@@ -48,7 +48,7 @@ namespace Robust.Client.GameObjects
if (_visible == value) return;
_visible = value;
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent());
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent(), true);
}
}
@@ -169,11 +169,12 @@ namespace Robust.Client.GameObjects
get => _baseRsi;
set
{
if (value == _baseRsi)
return;
_baseRsi = value;
if (value == null)
{
return;
}
for (var i = 0; i < Layers.Count; i++)
{
@@ -183,6 +184,8 @@ namespace Robust.Client.GameObjects
continue;
}
layer.UpdateActualState();
if (value.TryGetState(layer.State, out var state))
{
layer.AnimationTimeLeft = state.GetDelay(0);
@@ -204,15 +207,22 @@ namespace Robust.Client.GameObjects
[DataField("state", readOnly: true)] private string? state;
[DataField("texture", readOnly: true)] private string? texture;
/// <summary>
/// Should this entity show up in containers regardless of whether the container can show contents?
/// </summary>
[DataField("overrideContainerOcclusion")]
[ViewVariables(VVAccess.ReadWrite)]
public bool OverrideContainerOcclusion;
[ViewVariables(VVAccess.ReadWrite)]
public bool ContainerOccluded
{
get => _containerOccluded;
get => _containerOccluded && !OverrideContainerOcclusion;
set
{
if (_containerOccluded == value) return;
_containerOccluded = value;
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent());
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent(), true);
}
}
@@ -227,26 +237,43 @@ namespace Robust.Client.GameObjects
[ViewVariables(VVAccess.ReadWrite)] private bool _inertUpdateQueued;
/// <summary>
/// Shader instance to use when drawing the final sprite to the world.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public ShaderInstance? PostShader { get; set; }
/// <summary>
/// Whether or not to pass the screen texture to the <see cref="PostShader"/>.
/// </summary>
/// <remarks>
/// Should be false unless you really need it.
/// </remarks>
[DataField("getScreenTexture")]
[ViewVariables(VVAccess.ReadWrite)]
private bool _getScreenTexture = false;
public bool GetScreenTexture
{
get => _getScreenTexture && PostShader != null;
set => _getScreenTexture = value;
}
/// <summary>
/// If true, this raise a entity system event before rendering this sprite, allowing systems to modify the
/// shader parameters. Usually this can just be done via a frame-update, but some shaders require
/// information about the viewport / eye.
/// </summary>
[DataField("raiseShaderEvent")]
[ViewVariables(VVAccess.ReadWrite)]
public bool RaiseShaderEvent = false;
[ViewVariables] private Dictionary<object, int> LayerMap = new();
[ViewVariables] private bool _layerMapShared;
[ViewVariables] private List<Layer> Layers = new();
[ViewVariables(VVAccess.ReadWrite)] public uint RenderOrder { get; set; }
// TODO: this should absolutely not be static.
private static ShaderInstance? _defaultShader;
[ViewVariables]
private ShaderInstance? DefaultShader => _defaultShader ??= prototypes
.Index<ShaderPrototype>("shaded")
.Instance();
public const string LogCategory = "go.comp.sprite";
const string LayerSerializationCache = "spritelayer";
const string LayerMapSerializationCache = "spritelayermap";
[ViewVariables(VVAccess.ReadWrite)] public bool IsInert { get; private set; }
@@ -404,7 +431,7 @@ namespace Robust.Client.GameObjects
public int AddBlankLayer(int? newIndex = null)
{
var layer = new Layer(this) { Visible = false };
var layer = new Layer(this);
return AddLayer(layer, newIndex);
}
@@ -601,8 +628,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 || layer.Blank) continue;
_bounds = _bounds.Union(layer.CalculateBoundingBox());
}
}
@@ -621,8 +650,6 @@ namespace Robust.Client.GameObjects
var layer = Layers[index];
var anyTextureAttempted = false;
if (!string.IsNullOrWhiteSpace(layerDatum.RsiPath))
{
var path = TextureRoot / layerDatum.RsiPath;
@@ -639,7 +666,6 @@ namespace Robust.Client.GameObjects
if (!string.IsNullOrWhiteSpace(layerDatum.State))
{
anyTextureAttempted = true;
var theRsi = layer.RSI ?? BaseRSI;
if (theRsi == null)
{
@@ -655,6 +681,8 @@ namespace Robust.Client.GameObjects
{
// Always use south because this layer will be cached in the serializer.
layer.AnimationTimeLeft = state.GetDelay(0);
layer.AnimationTime = 0;
layer.AnimationFrame = 0;
}
else
{
@@ -667,7 +695,6 @@ namespace Robust.Client.GameObjects
if (!string.IsNullOrWhiteSpace(layerDatum.TexturePath))
{
anyTextureAttempted = true;
if (layer.State.IsValid)
{
Logger.ErrorS(LogCategory,
@@ -722,14 +749,14 @@ namespace Robust.Client.GameObjects
}
}
layer.Color = layerDatum.Color;
layer._rotation = layerDatum.Rotation;
layer._offset = layerDatum.Offset;
layer._scale = layerDatum.Scale;
layer.Color = layerDatum.Color ?? layer.Color;
layer._rotation = layerDatum.Rotation ?? layer._rotation;
layer._offset = layerDatum.Offset ?? layer._offset;
layer._scale = layerDatum.Scale ?? layer._scale;
layer.UpdateLocalMatrix();
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
layer.Visible = anyTextureAttempted && layerDatum.Visible;
layer.Visible = layerDatum.Visible ?? layer.Visible;
RebuildBounds();
}
@@ -1355,7 +1382,7 @@ namespace Robust.Client.GameObjects
// However, at some point later the eye-matrix is applied separately, so we subtract -eye rotation for now:
var entityMatrix = Matrix3.CreateTransform(worldPosition, NoRotation ? -eyeRotation : worldRotation);
Matrix3.Multiply(ref LocalMatrix, ref entityMatrix, out var transform);
Matrix3.Multiply(in LocalMatrix, in entityMatrix, out var transform);
var angle = worldRotation + eyeRotation; // angle on-screen. Used to decide the direction of 4/8 directional RSIs
foreach (var layer in Layers)
@@ -1402,7 +1429,7 @@ namespace Robust.Client.GameObjects
{
var layer = t;
// Since StateId is a struct, we can't null-check it directly.
if (!layer.State.IsValid || !layer.Visible || !layer.AutoAnimated)
if (!layer.State.IsValid || !layer.Visible || !layer.AutoAnimated || layer.Blank)
{
continue;
}
@@ -1501,7 +1528,7 @@ namespace Robust.Client.GameObjects
foreach (var layer in Layers)
{
// Since StateId is a struct, we can't null-check it directly.
if (!layer.State.IsValid || !layer.Visible || !layer.AutoAnimated)
if (!layer.State.IsValid || !layer.Visible || !layer.AutoAnimated || layer.Blank)
{
continue;
}
@@ -1520,6 +1547,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;
@@ -1598,7 +1626,7 @@ namespace Robust.Client.GameObjects
internal void UpdateBounds()
{
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent());
entities.EventBus.RaiseLocalEvent(Owner, new SpriteUpdateEvent(), true);
}
/// <summary>
@@ -1635,12 +1663,41 @@ namespace Robust.Client.GameObjects
[ViewVariables] public ShaderInstance? Shader;
[ViewVariables] public Texture? Texture;
[ViewVariables] public RSI? RSI;
[ViewVariables] public RSI.StateId State;
private RSI? _rsi;
[ViewVariables] public RSI? RSI
{
get => _rsi;
set
{
if (_rsi == value)
return;
_rsi = value;
UpdateActualState();
}
}
private RSI.StateId _state;
[ViewVariables] public RSI.StateId State
{
get => _state;
set
{
if (_state == value)
return;
_state = value;
UpdateActualState();
}
}
[ViewVariables] public float AnimationTimeLeft;
[ViewVariables] public float AnimationTime;
[ViewVariables] public int AnimationFrame;
private RSI.State? _actualState;
[ViewVariables] public RSI.State? ActualState => _actualState;
public Matrix3 LocalMatrix = Matrix3.Identity;
[ViewVariables(VVAccess.ReadWrite)]
@@ -1676,6 +1733,9 @@ namespace Robust.Client.GameObjects
[ViewVariables(VVAccess.ReadWrite)]
public bool Visible = true;
[ViewVariables]
public bool Blank => !State.IsValid && Texture == null;
[ViewVariables(VVAccess.ReadWrite)]
public Color Color { get; set; } = Color.White;
@@ -1968,7 +2028,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.
@@ -1977,13 +2037,11 @@ namespace Robust.Client.GameObjects
return Box2.CenteredAround(Offset, textureSize).Scale(_scale);
}
var rsiState = GetActualState();
var longestSide = MathF.Max(textureSize.X, textureSize.Y);
var longestRotatedSide = Math.Max(longestSide, (textureSize.X + textureSize.Y) / MathF.Sqrt(2));
// Build the bounding box based on how many directions the sprite has
var box = (_rotation != 0, rsiState) switch
var box = (_rotation != 0, _actualState) switch
{
// If this layer has any form of arbitrary rotation, return a bounding box big enough to cover
// any possible rotation.
@@ -2002,19 +2060,23 @@ namespace Robust.Client.GameObjects
return _scale == Vector2.One ? box : box.Scale(_scale);
}
internal RSI.State? GetActualState()
/// <summary>
/// Update Cached RSI state. State is cached to avoid calling this every time an entity gets drawn.
/// </summary>
internal void UpdateActualState()
{
if (!State.IsValid)
return null;
{
_actualState = null;
return;
}
// Pull texture from RSI state
var rsi = RSI ?? _parent.BaseRSI;
if (rsi == null || !rsi.TryGetState(State, out var rsiState))
if (rsi == null || !rsi.TryGetState(State, out _actualState))
{
rsiState = GetFallbackState(_parent.resourceCache);
_actualState = GetFallbackState(_parent.resourceCache);
}
return rsiState;
}
/// <summary>
@@ -2027,7 +2089,7 @@ namespace Robust.Client.GameObjects
layerDrawMatrix = LocalMatrix;
else
{
Matrix3.Multiply(ref _rsiDirectionMatrices[(int)dir], ref LocalMatrix, out layerDrawMatrix);
Matrix3.Multiply(in _rsiDirectionMatrices[(int)dir], in LocalMatrix, out layerDrawMatrix);
}
}
@@ -2046,28 +2108,26 @@ namespace Robust.Client.GameObjects
internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3 spriteMatrix, Angle angle, Direction? overrideDirection)
{
if (!Visible)
if (!Visible || Blank)
return;
var rsiState = GetActualState();
var dir = (rsiState == null || rsiState.Directions == RSI.State.DirectionType.Dir1)
var dir = (_actualState == null || _actualState.Directions == RSI.State.DirectionType.Dir1)
? RSIDirection.South
: angle.ToRsiDirection(rsiState.Directions);
: angle.ToRsiDirection(_actualState.Directions);
// Set the drawing transform for this layer
GetLayerDrawMatrix(dir, out var layerMatrix);
Matrix3.Multiply(ref layerMatrix, ref spriteMatrix, out var transformMatrix);
Matrix3.Multiply(in layerMatrix, in spriteMatrix, out var transformMatrix);
drawingHandle.SetTransform(in transformMatrix);
// The direction used to draw the sprite can differ from the one that the angle would naively suggest,
// due to direction overrides or offsets.
if (overrideDirection != null && rsiState != null)
dir = overrideDirection.Value.Convert(rsiState.Directions);
if (overrideDirection != null && _actualState != null)
dir = overrideDirection.Value.Convert(_actualState.Directions);
dir = dir.OffsetRsiDir(DirOffset);
// Get the correct directional texture from the state, and draw it!
var texture = GetRenderTexture(rsiState, dir);
var texture = GetRenderTexture(_actualState, dir);
RenderTexture(drawingHandle, texture);
}
@@ -2077,10 +2137,8 @@ namespace Robust.Client.GameObjects
drawingHandle.UseShader(Shader);
var layerColor = _parent.color * Color;
var position = -(Vector2)texture.Size / (2f * EyeManager.PixelsPerMeter);
var textureSize = texture.Size / (float)EyeManager.PixelsPerMeter;
var quad = Box2.FromDimensions(position, textureSize);
var quad = Box2.FromDimensions(textureSize/-2, textureSize);
drawingHandle.DrawTextureRectRegion(texture, quad, layerColor);
@@ -2174,14 +2232,19 @@ 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)
{
if (!layer.Visible) continue;
if (layer.Texture != null)
{
results.Add(layer.Texture);
if (!layer.RsiState.IsValid || !layer.Visible) continue;
continue;
}
if (!layer.RsiState.IsValid) continue;
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
if (rsi == null ||
@@ -2189,15 +2252,15 @@ namespace Robust.Client.GameObjects
continue;
results.Add(state);
anyTexture = true;
}
noRot = spriteComponent.NoRotation;
entityManager.DeleteEntity(dummy);
if (!anyTexture)
if (results.Count == 0)
results.Add(resourceCache.GetFallback<TextureResource>().Texture);
return results;
}

View File

@@ -6,7 +6,7 @@ namespace Robust.Client.GameObjects
[RegisterComponent]
public sealed class RenderingTreeComponent : Component
{
internal DynamicTree<SpriteComponent> SpriteTree { get; set; } = default!;
internal DynamicTree<PointLightComponent> LightTree { get; set; } = default!;
internal DynamicTree<ComponentTreeEntry<SpriteComponent>> SpriteTree { get; set; } = default!;
internal DynamicTree<ComponentTreeEntry<PointLightComponent>> LightTree { get; set; } = default!;
}
}

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), true);
}
internal void Close(object uiKey, bool remoteCall)
@@ -91,7 +95,7 @@ namespace Robust.Client.GameObjects
var playerSession = _playerManager.LocalPlayer?.Session;
if(playerSession != null)
_entityManager.EventBus.RaiseLocalEvent(Owner, new BoundUIClosedEvent(uiKey, Owner, playerSession));
_entityManager.EventBus.RaiseLocalEvent(Owner, new BoundUIClosedEvent(uiKey, Owner, playerSession), true);
}
internal void SendMessage(BoundUserInterfaceMessage message, object uiKey)

View File

@@ -46,7 +46,7 @@ namespace Robust.Client.GameObjects
foreach (var key in remie)
{
component.PlayingAnimations.Remove(key);
EntityManager.EventBus.RaiseLocalEvent(component.Owner, new AnimationCompletedEvent {Uid = component.Owner, Key = key});
EntityManager.EventBus.RaiseLocalEvent(component.Owner, new AnimationCompletedEvent {Uid = component.Owner, Key = key}, true);
component.AnimationComplete(key);
}

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;
@@ -61,9 +62,9 @@ namespace Robust.Client.GameObjects
/// <remarks>
/// As some appearance data values are not simple value-type objects, this is not just a shallow clone.
/// </remarks>
private Dictionary<object, object> CloneAppearanceData(Dictionary<object, object> data)
private Dictionary<Enum, object> CloneAppearanceData(Dictionary<Enum, object> data)
{
Dictionary<object, object> newDict = new(data.Count);
Dictionary<Enum, object> newDict = new(data.Count);
foreach (var (key, value) in data)
{
@@ -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.
@@ -132,6 +138,7 @@ namespace Robust.Client.GameObjects
public struct AppearanceChangeEvent
{
public AppearanceComponent Component;
public IReadOnlyDictionary<object, object> AppearanceData;
public IReadOnlyDictionary<Enum, object> AppearanceData;
public SpriteComponent? Sprite;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Audio;
@@ -9,22 +10,25 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.Threading;
namespace Robust.Client.GameObjects
{
[UsedImplicitly]
public sealed class AudioSystem : SharedAudioSystem, IAudioSystem
public sealed class AudioSystem : SharedAudioSystem
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IClydeAudio _clyde = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedPhysicsSystem _broadPhaseSystem = default!;
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
private readonly List<PlayingStream> _playingClydeStreams = new();
@@ -36,8 +40,6 @@ namespace Robust.Client.GameObjects
SubscribeNetworkEvent<PlayAudioGlobalMessage>(PlayAudioGlobalHandler);
SubscribeNetworkEvent<PlayAudioPositionalMessage>(PlayAudioPositionalHandler);
SubscribeNetworkEvent<StopAudioMessageClient>(StopAudioMessageHandler);
SubscribeLocalEvent<SoundSystem.QueryAudioSystem>((ev => ev.Audio = this));
}
private void StopAudioMessageHandler(StopAudioMessageClient ev)
@@ -54,7 +56,7 @@ namespace Robust.Client.GameObjects
private void PlayAudioPositionalHandler(PlayAudioPositionalMessage ev)
{
var mapId = ev.Coordinates.GetMapId(_entityManager);
var mapId = ev.Coordinates.GetMapId(EntityManager);
if (!_mapManager.MapExists(mapId))
{
@@ -63,7 +65,7 @@ namespace Robust.Client.GameObjects
return;
}
var stream = (PlayingStream?) Play(ev.FileName, ev.Coordinates, ev.FallbackCoordinates, ev.AudioParams);
var stream = (PlayingStream?)Play(ev.FileName, ev.Coordinates, ev.FallbackCoordinates, ev.AudioParams);
if (stream != null)
{
stream.NetIdentifier = ev.Identifier;
@@ -72,7 +74,7 @@ namespace Robust.Client.GameObjects
private void PlayAudioGlobalHandler(PlayAudioGlobalMessage ev)
{
var stream = (PlayingStream?) Play(ev.FileName, ev.AudioParams);
var stream = (PlayingStream?)Play(ev.FileName, ev.AudioParams);
if (stream != null)
{
stream.NetIdentifier = ev.Identifier;
@@ -82,8 +84,8 @@ namespace Robust.Client.GameObjects
private void PlayAudioEntityHandler(PlayAudioEntityMessage ev)
{
var stream = EntityManager.EntityExists(ev.EntityUid) ?
(PlayingStream?) Play(ev.FileName, ev.EntityUid, ev.FallbackCoordinates, ev.AudioParams)
: (PlayingStream?) Play(ev.FileName, ev.Coordinates, ev.FallbackCoordinates, ev.AudioParams);
(PlayingStream?)Play(ev.FileName, ev.EntityUid, ev.FallbackCoordinates, ev.AudioParams)
: (PlayingStream?)Play(ev.FileName, ev.Coordinates, ev.FallbackCoordinates, ev.AudioParams);
if (stream != null)
{
@@ -95,12 +97,24 @@ namespace Robust.Client.GameObjects
public override void FrameUpdate(float frameTime)
{
// Update positions of streams every frame.
// Start with an initial pass to cull streams that need to be removed, and sort stuff out.
Span<int> validIndices = stackalloc int[_playingClydeStreams.Count];
int validCount = 0;
// Initial clearing pass
try
{
var ourPos = _eyeManager.CurrentEye.Position.Position;
int streamIndexOut = 0;
foreach (var stream in _playingClydeStreams)
{
// Note: continue; in here is expected to have one of two outcomes:
// + StreamDone
// + streamIndexOut++
// Occlusion recalculation parallel needs a way to know which targets to actually recalculate for.
// That in mind start by setting this to false (it's set to true later when relevant)
stream.OcclusionValidTemporary = false;
if (!stream.Source.IsPlaying)
{
StreamDone(stream);
@@ -111,9 +125,9 @@ namespace Robust.Client.GameObjects
if (stream.TrackingCoordinates != null)
{
var coords = stream.TrackingCoordinates.Value;
if (_mapManager.MapExists(coords.GetMapId(_entityManager)))
if (_mapManager.MapExists(coords.GetMapId(EntityManager)))
{
mapPos = stream.TrackingCoordinates.Value.ToMap(_entityManager);
mapPos = stream.TrackingCoordinates.Value.ToMap(EntityManager);
}
else
{
@@ -135,86 +149,20 @@ namespace Robust.Client.GameObjects
// TODO Remove when coordinates can't be NaN
if (mapPos == null || !float.IsFinite(mapPos.Value.X) || !float.IsFinite(mapPos.Value.Y))
mapPos = stream.TrackingFallbackCoordinates?.ToMap(_entityManager);
mapPos = stream.TrackingFallbackCoordinates?.ToMap(EntityManager);
if (mapPos != null)
{
var pos = mapPos.Value;
if (pos.MapId != _eyeManager.CurrentMap)
{
stream.Source.SetVolume(-10000000);
}
else
{
var sourceRelative = ourPos - pos.Position;
var occlusion = 0f;
if (sourceRelative.Length > 0)
{
occlusion = _broadPhaseSystem.IntersectRayPenetration(
pos.MapId,
new CollisionRay(
pos.Position,
sourceRelative.Normalized,
OcclusionCollisionMask),
sourceRelative.Length,
stream.TrackingEntity);
}
var distance = MathF.Max(stream.ReferenceDistance, MathF.Min(sourceRelative.Length, stream.MaxDistance));
float gain;
// Technically these are formulas for gain not decibels but EHHHHHHHH.
switch (stream.Attenuation)
{
case Attenuation.Default:
gain = 1f;
break;
// You thought I'd implement clamping per source? Hell no that's just for the overall OpenAL setting
// I didn't even wanna implement this much for linear but figured it'd be cleaner.
case Attenuation.InverseDistanceClamped:
case Attenuation.InverseDistance:
gain = stream.ReferenceDistance /
(stream.ReferenceDistance + stream.RolloffFactor * (distance - stream.ReferenceDistance));
break;
case Attenuation.LinearDistanceClamped:
case Attenuation.LinearDistance:
gain = 1f - stream.RolloffFactor * (distance - stream.ReferenceDistance) /
(stream.MaxDistance - stream.ReferenceDistance);
break;
case Attenuation.ExponentDistanceClamped:
case Attenuation.ExponentDistance:
gain = MathF.Pow((distance / stream.ReferenceDistance),
(-stream.RolloffFactor));
break;
default:
throw new ArgumentOutOfRangeException($"No implemented attenuation for {stream.Attenuation.ToString()}");
}
var volume = MathF.Pow(10, stream.Volume / 10);
var actualGain = MathF.Max(0f, volume * gain);
stream.Source.SetVolumeDirect(actualGain);
stream.Source.SetOcclusion(occlusion);
}
SetAudioPos(stream, stream.Attenuation != Attenuation.NoAttenuation ? pos.Position : ourPos);
void SetAudioPos(PlayingStream stream, Vector2 pos)
{
if (!stream.Source.SetPosition(pos))
{
Logger.Warning("Interrupting positional audio, can't set position.");
stream.Source.StopPlaying();
}
}
if (stream.TrackingEntity != default)
{
stream.Source.SetVelocity(stream.TrackingEntity.GlobalLinearVelocity());
}
stream.MapCoordinatesTemporary = mapPos.Value;
// this has a map position so it's good to go to the other processes
validIndices[validCount] = streamIndexOut;
// check for occlusion recalc
stream.OcclusionValidTemporary = mapPos.Value.MapId == _eyeManager.CurrentMap;
validCount++;
}
// This stream gets to live!
streamIndexOut++;
}
}
finally
@@ -224,6 +172,122 @@ namespace Robust.Client.GameObjects
// meaning it'll break the entire audio system.
_playingClydeStreams.RemoveAll(p => p.Done);
}
var ourPos = _eyeManager.CurrentEye.Position.Position;
// Occlusion calculation pass
Parallel.For(
0, _playingClydeStreams.Count,
(i) =>
{
var stream = _playingClydeStreams[i];
// As set earlier.
if (stream.OcclusionValidTemporary)
{
var pos = stream.MapCoordinatesTemporary;
var sourceRelative = ourPos - pos.Position;
var occlusion = 0f;
if (sourceRelative.Length > 0)
{
occlusion = _broadPhaseSystem.IntersectRayPenetration(
pos.MapId,
new CollisionRay(
pos.Position,
sourceRelative.Normalized,
OcclusionCollisionMask),
sourceRelative.Length,
stream.TrackingEntity);
}
stream.OcclusionTemporary = occlusion;
}
}
);
// Occlusion apply / Attenuation / position / velocity pass
// Note that for streams for which MapCoordinatesTemporary isn't updated, they don't get here
for (var i = 0; i < validCount; i++)
{
var stream = _playingClydeStreams[validIndices[i]];
var pos = stream.MapCoordinatesTemporary;
if (stream.OcclusionValidTemporary)
{
stream.Source.SetOcclusion(stream.OcclusionTemporary);
}
if (pos.MapId != _eyeManager.CurrentMap)
{
stream.Source.SetVolume(-10000000);
}
else
{
var sourceRelative = ourPos - pos.Position;
// OpenAL uses MaxDistance to limit how much attenuation can *reduce* the gain,
// and doesn't do any culling. We however cull based on MaxDistance, because
// this is what all current code that uses MaxDistance expects and because
// we don't need the OpenAL behaviour.
if (sourceRelative.Length > stream.MaxDistance)
{
stream.Source.SetVolume(-10000000);
}
else
{
// OpenAL also limits the distance to <= AL_MAX_DISTANCE, but since we cull
// sources that are further away than stream.MaxDistance, we don't do that.
var distance = MathF.Max(stream.ReferenceDistance, sourceRelative.Length);
float gain;
// Technically these are formulas for gain not decibels but EHHHHHHHH.
switch (stream.Attenuation)
{
case Attenuation.Default:
gain = 1f;
break;
// You thought I'd implement clamping per source? Hell no that's just for the overall OpenAL setting
// I didn't even wanna implement this much for linear but figured it'd be cleaner.
case Attenuation.InverseDistanceClamped:
case Attenuation.InverseDistance:
gain = stream.ReferenceDistance /
(stream.ReferenceDistance + stream.RolloffFactor *
(distance - stream.ReferenceDistance));
break;
case Attenuation.LinearDistanceClamped:
case Attenuation.LinearDistance:
gain = 1f - stream.RolloffFactor * (distance - stream.ReferenceDistance) /
(stream.MaxDistance - stream.ReferenceDistance);
break;
case Attenuation.ExponentDistanceClamped:
case Attenuation.ExponentDistance:
gain = MathF.Pow((distance / stream.ReferenceDistance),
(-stream.RolloffFactor));
break;
default:
throw new ArgumentOutOfRangeException(
$"No implemented attenuation for {stream.Attenuation.ToString()}");
}
var volume = MathF.Pow(10, stream.Volume / 10);
var actualGain = MathF.Max(0f, volume * gain);
stream.Source.SetVolumeDirect(actualGain);
var audioPos = stream.Attenuation != Attenuation.NoAttenuation ? pos.Position : ourPos;
if (!stream.Source.SetPosition(audioPos))
{
Logger.Warning($"Interrupting positional audio, can't set position.");
stream.Source.StopPlaying();
}
if (stream.TrackingEntity != default)
{
stream.Source.SetVelocity(stream.TrackingEntity.GlobalLinearVelocity());
}
}
}
}
}
private static void StreamDone(PlayingStream stream)
@@ -266,6 +330,7 @@ namespace Robust.Client.GameObjects
source.SetGlobal();
source.StartPlaying();
// These defaults differ from AudioParams.Default
var playing = new PlayingStream
{
Source = source,
@@ -305,7 +370,7 @@ namespace Robust.Client.GameObjects
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="fallbackCoordinates">The map or grid coordinates at which to play the audio when entity is invalid.</param>
/// <param name="audioParams"></param>
private IPlayingAudioStream? Play(AudioStream stream, EntityUid entity, EntityCoordinates fallbackCoordinates,
private IPlayingAudioStream? Play(AudioStream stream, EntityUid entity, EntityCoordinates? fallback = null,
AudioParams? audioParams = null)
{
var source = _clyde.CreateAudioSource(stream);
@@ -315,9 +380,14 @@ namespace Robust.Client.GameObjects
return null;
}
if (!source.SetPosition(EntityManager.GetComponent<TransformComponent>(entity).WorldPosition))
var query = GetEntityQuery<TransformComponent>();
var xform = query.GetComponent(entity);
var worldPos = _xformSys.GetWorldPosition(xform, query);
fallback ??= GetFallbackCoordinates(new(worldPos, xform.MapID));
if (!source.SetPosition(worldPos))
{
return Play(stream, fallbackCoordinates, fallbackCoordinates, audioParams);
return Play(stream, fallback.Value, fallback.Value, audioParams);
}
ApplyAudioParams(audioParams, source);
@@ -327,7 +397,7 @@ namespace Robust.Client.GameObjects
{
Source = source,
TrackingEntity = entity,
TrackingFallbackCoordinates = fallbackCoordinates != EntityCoordinates.Invalid ? fallbackCoordinates : null,
TrackingFallbackCoordinates = fallback != EntityCoordinates.Invalid ? fallback : null,
Attenuation = audioParams?.Attenuation ?? Attenuation.Default,
MaxDistance = audioParams?.MaxDistance ?? float.MaxValue,
ReferenceDistance = audioParams?.ReferenceDistance ?? 1f,
@@ -381,7 +451,7 @@ namespace Robust.Client.GameObjects
return null;
}
if (!coordinates.IsValid(_entityManager))
if (!coordinates.IsValid(EntityManager))
{
coordinates = fallbackCoordinates;
}
@@ -404,14 +474,27 @@ namespace Robust.Client.GameObjects
return playing;
}
private static void ApplyAudioParams(AudioParams? audioParams, IClydeAudioSource source)
/// <inheritdoc />
public override IPlayingAudioStream? PlayPredicted(SoundSpecifier sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null)
{
if (_timing.IsFirstTimePredicted)
return Play(sound, Filter.Local(), source, audioParams);
else
return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio....
}
private void ApplyAudioParams(AudioParams? audioParams, IClydeAudioSource source)
{
if (!audioParams.HasValue)
{
return;
}
source.SetPitch(audioParams.Value.PitchScale);
if (audioParams.Value.Variation.HasValue)
source.SetPitch(audioParams.Value.PitchScale * (float)RandMan.NextGaussian(1, audioParams.Value.Variation.Value));
else
source.SetPitch(audioParams.Value.PitchScale);
source.SetVolume(audioParams.Value.Volume);
source.SetRolloffFactor(audioParams.Value.RolloffFactor);
source.SetMaxDistance(audioParams.Value.MaxDistance);
@@ -430,6 +513,25 @@ namespace Robust.Client.GameObjects
public bool Done;
public float Volume;
/// <summary>
/// Temporary holding value to determine if calculating occlusion for this stream is a good idea.
/// Because some of this stuff is parallelized for performance, these can't be stackalloc'd arrays.
/// </summary>
public bool OcclusionValidTemporary;
/// <summary>
/// Temporary holding value containing the occlusion value of the stream.
/// Because some of this stuff is parallelized for performance, these can't be stackalloc'd arrays.
/// </summary>
public float OcclusionTemporary;
/// <summary>
/// Temporary holding value containing the map coordinates of the stream.
/// Because some of this stuff is parallelized for performance, these can't be stackalloc'd arrays.
/// Note that if the map coordinates aren't available, this isn't updated.
/// Only streams for which map coordinates are available go into the "valid" stackalloc'd array.
/// (Occlusion uses the OcclusionValidTemporary field as it can't access stackalloc'd arrays.)
/// </summary>
public MapCoordinates MapCoordinatesTemporary;
public float MaxDistance;
public float ReferenceDistance;
public float RolloffFactor;
@@ -458,24 +560,29 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc />
public int OcclusionCollisionMask { get; set; }
/// <inheritdoc />
public IPlayingAudioStream? Play(Filter playerFilter, string filename, AudioParams? audioParams = null)
public override IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, AudioParams? audioParams = null)
{
return Play(filename, audioParams);
}
/// <inheritdoc />
public IPlayingAudioStream? Play(Filter playerFilter, string filename, EntityUid entity, AudioParams? audioParams = null)
public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid entity,
AudioParams? audioParams = null)
{
return Play(filename, entity, GetFallbackCoordinates(EntityManager.GetComponent<TransformComponent>(entity).MapPosition), audioParams);
if (_resourceCache.TryGetResource<AudioResource>(new ResourcePath(filename), out var audio))
{
return Play(audio, entity, null, audioParams);
}
Logger.Error($"Server tried to play audio file {filename} which does not exist.");
return default;
}
/// <inheritdoc />
public IPlayingAudioStream? Play(Filter playerFilter, string filename, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(_entityManager)), audioParams);
return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams);
}
}
}

View File

@@ -0,0 +1,8 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects;
public sealed class ClientMetaDataSystem : MetaDataSystem
{
// Howdy.
}

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

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
@@ -25,18 +26,12 @@ namespace Robust.Client.GameObjects
{
base.Initialize();
SubscribeLocalEvent<UpdateContainerOcclusionMessage>(UpdateContainerOcclusion);
SubscribeLocalEvent<EntityInitializedMessage>(HandleEntityInitialized);
SubscribeLocalEvent<ContainerManagerComponent, ComponentHandleState>(HandleComponentState);
UpdatesBefore.Add(typeof(SpriteSystem));
}
private void UpdateContainerOcclusion(UpdateContainerOcclusionMessage ev)
{
_updateQueue.Add(ev.Entity);
}
private void HandleEntityInitialized(EntityInitializedMessage ev)
{
if (!ExpectedEntities.TryGetValue(ev.Entity, out var container))
@@ -56,24 +51,26 @@ namespace Robust.Client.GameObjects
return;
// Delete now-gone containers.
List<string>? toDelete = null;
var toDelete = new ValueList<string>();
foreach (var (id, container) in component.Containers)
{
if (!cast.ContainerSet.Any(data => data.Id == id))
// TODO: This is usually O(n^2) to the amount of containers.
foreach (var stateContainer in cast.ContainerSet)
{
container.EmptyContainer(true, entMan: EntityManager);
container.Shutdown();
toDelete ??= new List<string>();
toDelete.Add(id);
if (stateContainer.Id == id)
goto skip;
}
container.EmptyContainer(true, entMan: EntityManager);
container.Shutdown();
toDelete.Add(id);
skip: ;
}
if (toDelete != null)
foreach (var dead in toDelete)
{
foreach (var dead in toDelete)
{
component.Containers.Remove(dead);
}
component.Containers.Remove(dead);
}
// Add new containers and update existing contents.
@@ -91,37 +88,33 @@ namespace Robust.Client.GameObjects
container.OccludesLight = occludesLight;
// Remove gone entities.
List<EntityUid>? toRemove = null;
var toRemove = new ValueList<EntityUid>();
foreach (var entity in container.ContainedEntities)
{
if (!entityUids.Contains(entity))
{
toRemove ??= new List<EntityUid>();
toRemove.Add(entity);
}
}
if (toRemove != null)
foreach (var goner in toRemove)
{
foreach (var goner in toRemove)
container.Remove(goner);
container.Remove(goner);
}
// Remove entities that were expected, but have been removed from the container.
List<EntityUid>? removedExpected = null;
var removedExpected = new ValueList<EntityUid>();
foreach (var entityUid in container.ExpectedEntities)
{
if (!entityUids.Contains(entityUid))
{
removedExpected ??= new List<EntityUid>();
removedExpected.Add(entityUid);
}
}
if (removedExpected != null)
foreach (var entityUid in removedExpected)
{
foreach (var entityUid in removedExpected)
RemoveExpectedEntity(entityUid);
RemoveExpectedEntity(entityUid);
}
// Add new entities.
@@ -152,9 +145,14 @@ namespace Robust.Client.GameObjects
}
}
protected override void HandleParentChanged(ref EntParentChangedMessage message)
protected override void OnParentChanged(ref EntParentChangedMessage message)
{
base.HandleParentChanged(ref message);
base.OnParentChanged(ref message);
var xform = message.Transform;
if (xform.MapID != MapId.Nullspace)
_updateQueue.Add(message.Entity);
// If an entity warped in from null-space (i.e., re-entered PVS) and got attached to a container, do the same checks as for newly initialized entities.
if (message.OldParent != null && message.OldParent.Value.IsValid())
@@ -163,7 +161,7 @@ namespace Robust.Client.GameObjects
if (!ExpectedEntities.TryGetValue(message.Entity, out var container))
return;
if (Transform(message.Entity).ParentUid != container.Owner)
if (xform.ParentUid != container.Owner)
{
// This container is expecting an entity... but it got parented to some other entity???
// Ah well, the sever should send a new container state that updates expected entities so just ignore it for now.
@@ -240,7 +238,7 @@ namespace Robust.Client.GameObjects
var spriteOccluded = false;
var lightOccluded = false;
while (parent.IsValid() && !spriteOccluded && !lightOccluded)
while (parent.IsValid() && (!spriteOccluded || !lightOccluded))
{
var parentXform = xformQuery.GetComponent(parent);
if (TryComp<ContainerManagerComponent>(parent, out var manager) && manager.TryGetContainer(child, out var container))

View File

@@ -0,0 +1,107 @@
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.Maths;
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));
}
}
worldHandle.SetTransform(Matrix3.Identity);
}
}

View File

@@ -65,15 +65,12 @@ namespace Robust.Client.GameObjects
var map = _eyeManager.CurrentMap;
if (map == MapId.Nullspace) return;
var viewport = _eyeManager.GetWorldViewbounds();
var worldBounds = viewport.CalcBoundingBox();
foreach (var tree in _tree.GetRenderTrees(map, viewport))
foreach (var tree in _tree.GetRenderTrees(map, args.WorldBounds))
{
foreach (var light in tree.LightTree)
foreach (var (light, xform) in tree.LightTree)
{
var aabb = _lookup.GetWorldAABB(light.Owner);
if (!aabb.Intersects(worldBounds)) continue;
var aabb = _lookup.GetWorldAABB(light.Owner, xform);
if (!aabb.Intersects(args.WorldAABB)) continue;
args.WorldHandle.DrawRect(aabb, Color.Green.WithAlpha(0.1f));
}

View File

@@ -0,0 +1,43 @@
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
namespace Robust.Client.GameObjects;
/// <summary>
/// A generic visualizer system that modifies sprite layer data.
/// </summary>
public sealed class GenericVisualizerSystem : VisualizerSystem<GenericVisualizerComponent>
{
[Dependency] private readonly IReflectionManager _refMan = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSys = default!;
protected override void OnAppearanceChange(EntityUid uid, GenericVisualizerComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
foreach (var (appearanceKey, layerDict) in component.Visuals)
{
if (!_appearanceSys.TryGetData(uid, appearanceKey, out var appearanceValue, args.Component))
continue;
var valueString = appearanceValue.ToString();
if (string.IsNullOrEmpty(valueString))
continue;
foreach (var (layerKeyRaw, layerDataDict) in layerDict)
{
if (!layerDataDict.TryGetValue(valueString, out var layerData))
continue;
object layerKey = _refMan.TryParseEnumReference(layerKeyRaw, out var @enum)
? @enum
: layerKeyRaw;
var layerIndex = args.Sprite.LayerMapReserveBlank(layerKey);
args.Sprite.LayerSetData(layerIndex, layerData);
}
}
}
}

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))
{
@@ -102,6 +102,8 @@ namespace Robust.Client.GameObjects
}
}
}
worldHandle.SetTransform(Matrix3.Identity);
}
}
}

View File

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

View File

@@ -20,7 +20,7 @@ namespace Robust.Client.GameObjects
private void HandleInit(EntityUid uid, PointLightComponent component, ComponentInit args)
{
UpdateMask(component);
RaiseLocalEvent(uid, new PointLightUpdateEvent());
RaiseLocalEvent(uid, new PointLightUpdateEvent(), true);
}
private void HandleRemove(EntityUid uid, PointLightComponent component, ComponentRemove args)

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Physics;
@@ -18,6 +19,8 @@ namespace Robust.Client.GameObjects
[UsedImplicitly]
public sealed class RenderingTreeSystem : EntitySystem
{
[Dependency] private readonly TransformSystem _xformSystem = default!;
internal const string LoggerSawmill = "rendertree";
// Nullspace is not indexed. Keep that in mind.
@@ -94,8 +97,8 @@ namespace Robust.Client.GameObjects
private void OnTreeInit(EntityUid uid, RenderingTreeComponent component, ComponentInit args)
{
component.LightTree = new DynamicTree<PointLightComponent>(LightAabbFunc);
component.SpriteTree = new DynamicTree<SpriteComponent>(SpriteAabbFunc);
component.LightTree = new(LightAabbFunc);
component.SpriteTree = new(SpriteAabbFunc);
}
private void HandleLightUpdate(EntityUid uid, PointLightComponent component, PointLightUpdateEvent args)
@@ -169,7 +172,7 @@ namespace Robust.Client.GameObjects
{
if (component.RenderTree == null) return;
component.RenderTree.SpriteTree.Remove(component);
component.RenderTree.SpriteTree.Remove(new() { Component = component });
component.RenderTree = null;
}
@@ -198,7 +201,7 @@ namespace Robust.Client.GameObjects
{
if (component.RenderTree == null) return;
component.RenderTree.LightTree.Remove(component);
component.RenderTree.LightTree.Remove(new() { Component = component });
component.RenderTree = null;
}
@@ -215,12 +218,12 @@ namespace Robust.Client.GameObjects
{
foreach (var sprite in component.SpriteTree)
{
sprite.RenderTree = null;
sprite.Component.RenderTree = null;
}
foreach (var light in component.LightTree)
{
light.RenderTree = null;
light.Component.RenderTree = null;
}
component.SpriteTree.Clear();
@@ -242,12 +245,11 @@ namespace Robust.Client.GameObjects
EntityManager.EnsureComponent<RenderingTreeComponent>(_mapManager.GetGrid(ev.GridId).GridEntityId);
}
private RenderingTreeComponent? GetRenderTree(EntityUid entity, EntityQuery<TransformComponent> xforms)
private RenderingTreeComponent? GetRenderTree(EntityUid entity, TransformComponent xform, EntityQuery<TransformComponent> xforms)
{
var lookups = EntityManager.GetEntityQuery<RenderingTreeComponent>();
if (!EntityManager.EntityExists(entity) ||
!xforms.TryGetComponent(entity, out var xform) ||
xform.MapID == MapId.Nullspace ||
lookups.HasComponent(entity)) return null;
@@ -282,11 +284,11 @@ namespace Robust.Client.GameObjects
continue;
}
var oldMapTree = sprite.RenderTree;
var newMapTree = GetRenderTree(sprite.Owner, xforms);
// TODO: Temp PVS guard
var xform = xforms.GetComponent(sprite.Owner);
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
var oldMapTree = sprite.RenderTree;
var newMapTree = GetRenderTree(sprite.Owner, xform, xforms);
// TODO: Temp PVS guard
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform, xforms);
if (float.IsNaN(worldPos.X) || float.IsNaN(worldPos.Y))
{
@@ -294,17 +296,17 @@ namespace Robust.Client.GameObjects
continue;
}
var aabb = SpriteAabbFunc(sprite, worldPos, worldRot, xforms);
var aabb = SpriteAabbFunc(sprite, xform, worldPos, worldRot, xforms);
// If we're on a new map then clear the old one.
if (oldMapTree != newMapTree)
{
ClearSprite(sprite);
newMapTree?.SpriteTree.Add(sprite, aabb);
newMapTree?.SpriteTree.Add((sprite,xform) , aabb);
}
else
{
newMapTree?.SpriteTree.Update(sprite, aabb);
newMapTree?.SpriteTree.Update((sprite, xform), aabb);
}
sprite.RenderTree = newMapTree;
@@ -320,10 +322,11 @@ namespace Robust.Client.GameObjects
continue;
}
var xform = xforms.GetComponent(light.Owner);
var oldMapTree = light.RenderTree;
var newMapTree = GetRenderTree(light.Owner, xforms);
var newMapTree = GetRenderTree(light.Owner, xform, xforms);
// TODO: Temp PVS guard
var worldPos = xforms.GetComponent(light.Owner).WorldPosition;
var worldPos = _xformSystem.GetWorldPosition(xform, xforms);
if (float.IsNaN(worldPos.X) || float.IsNaN(worldPos.Y))
{
@@ -337,18 +340,17 @@ namespace Robust.Client.GameObjects
{
Logger.WarningS(LoggerSawmill, $"Light radius for {light.Owner} set above max radius of {MaxLightRadius}. This may lead to pop-in.");
}
var aabb = LightAabbFunc(light, worldPos, xforms);
var aabb = LightAabbFunc(light, xform, worldPos, xforms);
// If we're on a new map then clear the old one.
if (oldMapTree != newMapTree)
{
ClearLight(light);
newMapTree?.LightTree.Add(light, aabb);
newMapTree?.LightTree.Add((light, xform), aabb);
}
else
{
newMapTree?.LightTree.Update(light, aabb);
newMapTree?.LightTree.Update((light, xform), aabb);
}
light.RenderTree = newMapTree;
@@ -358,39 +360,38 @@ namespace Robust.Client.GameObjects
_lightQueue.Clear();
}
private Box2 SpriteAabbFunc(in SpriteComponent value)
private Box2 SpriteAabbFunc(in ComponentTreeEntry<SpriteComponent> entry)
{
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
var xform = xforms.GetComponent(value.Owner);
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
return SpriteAabbFunc(value, worldPos, worldRot, xforms);
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(entry.Transform, xforms);
return SpriteAabbFunc(entry.Component, entry.Transform, worldPos, worldRot, xforms);
}
private Box2 LightAabbFunc(in PointLightComponent value)
private Box2 LightAabbFunc(in ComponentTreeEntry<PointLightComponent> entry)
{
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
var worldPos = _xformSystem.GetWorldPosition(entry.Transform, xforms);
var tree = GetRenderTree(entry.Uid, entry.Transform, xforms);
var boxSize = entry.Component.Radius * 2;
var worldPos = xforms.GetComponent(value.Owner).WorldPosition;
var tree = GetRenderTree(value.Owner, xforms);
var boxSize = value.Radius * 2;
var localPos = tree == null ? worldPos : xforms.GetComponent(tree.Owner).InvWorldMatrix.Transform(worldPos);
var localPos = tree == null ? worldPos : _xformSystem.GetInvWorldMatrix(tree.Owner, xforms).Transform(worldPos);
return Box2.CenteredAround(localPos, (boxSize, boxSize));
}
private Box2 SpriteAabbFunc(SpriteComponent value, Vector2 worldPos, Angle worldRot, EntityQuery<TransformComponent> xforms)
private Box2 SpriteAabbFunc(SpriteComponent value, TransformComponent xform, Vector2 worldPos, Angle worldRot, EntityQuery<TransformComponent> xforms)
{
var bounds = value.CalculateRotatedBoundingBox(worldPos, worldRot);
var tree = GetRenderTree(value.Owner, xforms);
var tree = GetRenderTree(value.Owner, xform, xforms);
return tree == null ? bounds.CalcBoundingBox() : xforms.GetComponent(tree.Owner).InvWorldMatrix.TransformBox(bounds);
return tree == null ? bounds.CalcBoundingBox() : _xformSystem.GetInvWorldMatrix(tree.Owner, xforms).TransformBox(bounds);
}
private Box2 LightAabbFunc(PointLightComponent value, Vector2 worldPos, EntityQuery<TransformComponent> xforms)
private Box2 LightAabbFunc(PointLightComponent value, TransformComponent xform, Vector2 worldPos, EntityQuery<TransformComponent> xforms)
{
// Lights are circles so don't need entity's rotation
var tree = GetRenderTree(value.Owner, xforms);
var tree = GetRenderTree(value.Owner, xform, xforms);
var boxSize = value.Radius * 2;
var localPos = tree == null ? worldPos : xforms.GetComponent(tree.Owner).InvWorldMatrix.Transform(worldPos);

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<ScaleVisualsComponent, AppearanceChangeEvent>(OnChangeData);
}
private void OnChangeData(EntityUid uid, ScaleVisualsComponent component, 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.Component 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

@@ -1,9 +1,10 @@
using System.Collections.Generic;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics;
namespace Robust.Client.GameObjects
{
@@ -23,9 +24,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);
@@ -59,15 +67,15 @@ namespace Robust.Client.GameObjects
{
var bounds = xforms.GetComponent(comp.Owner).InvWorldMatrix.TransformBox(pvsBounds);
comp.SpriteTree.QueryAabb(ref frameTime, (ref float state, in SpriteComponent value) =>
comp.SpriteTree.QueryAabb(ref frameTime, (ref float state, in ComponentTreeEntry<SpriteComponent> value) =>
{
if (value.IsInert)
if (value.Component.IsInert)
{
return true;
}
if (!_manualUpdate.Contains(value))
value.FrameUpdate(state);
if (!_manualUpdate.Contains(value.Component))
value.Component.FrameUpdate(state);
return true;
}, bounds, true);
}
@@ -83,4 +91,19 @@ namespace Robust.Client.GameObjects
_manualUpdate.Add(sprite);
}
}
/// <summary>
/// This event gets raised before a sprite gets drawn using it's post-shader.
/// </summary>
public sealed class BeforePostShaderRenderEvent : EntityEventArgs
{
public readonly SpriteComponent Sprite;
public readonly IClydeViewport Viewport;
public BeforePostShaderRenderEvent(SpriteComponent sprite, IClydeViewport viewport)
{
Sprite = sprite;
Viewport = viewport;
}
}
}

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