Compare commits

...

165 Commits

Author SHA1 Message Date
PJB3005
b422d3fb3e Version: 237.2.2 2025-09-19 09:17:39 +02:00
Skye
e4101aae8b Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:39 +02:00
PJB3005
74fe177985 Version: 237.2.1 2025-09-14 14:58:28 +02:00
PJB3005
bb90d79a3f Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

    This (and the other couple past commits) reported by Elelzedel.

commit 7654d38612
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 22:50:51 2025 +0200

    Move CEF cache out of data directory

    Don't want content messing with this...

commit cdcc255123
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:11:16 2025 +0200

    Make Robust.Client.WebView.Cef.Program internal.

commit 2f56a6a110
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:10:46 2025 +0200

    Update SpaceWizards.NFluidSynth to 0.2.2

commit 16fc48cef2
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:09:43 2025 +0200

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
(cherry picked from commit 4413695c77fb705054c2f81fa18ec0a189b685dd)
2025-09-14 14:58:27 +02:00
Pieter-Jan Briers
92b0e7f1a8 Version: 237.2.0 2024-11-21 00:03:19 +01:00
Pieter-Jan Briers
47d1c372b2 Mute null error from Rider 2024-11-20 23:45:47 +01:00
SpaceManiac
453f763128 Fix minor layout bugs in SplitContainer and BoxContainer (#5529)
* Fix SplitContainer using invalidated measures when clamping SplitCenter

* Fix BoxContainer adding separation for invisible children
2024-11-20 23:41:10 +01:00
Nikolai Korolev
65a7942d63 Remove unused variable, local function and private field (#5528)
* Remove unused local function

* Remove unused variable

* Remove private field
2024-11-20 02:51:46 +01:00
Saphire Lattice
f1f3c60d1f Improve Toolshed type intersection mechanism, add WithCommand for ProtoId (#5515) 2024-11-20 01:00:13 +01:00
SpaceManiac
d4e8a27c23 Add FormattedMessage.TrimEnd (#5524)
* Add FormattedMessage.TrimTrailingNewlines

* Trim all trailing whitespace
2024-11-19 19:55:19 +01:00
Pieter-Jan Briers
18a17da8fa .NET 9 forward compatibility changes
This doesn't switch the projects over to .NET 9, but it does make them work on .NET 9 when we decide to switch in the future.
2024-11-19 19:54:01 +01:00
Nikolai Korolev
90a8c66e96 Fix System.ArgumentException: '0' cannot be greater than -0.01 for very fast audios (#5521)
* Fix `System.ArgumentException: '0' cannot be greater than -0.01`

In case of playing empty audio files

* Add semicolon
2024-11-18 18:46:01 +01:00
MilenVolf
45c14b2bc3 Replace remaining obsolete TileAccess methods (#5519)
* Replace remaining TileAccess methods

* Small fix
2024-11-18 17:19:57 +01:00
SpaceManiac
d227613997 Fix cursor getting stuck when click-dragging off of a control (#5523) 2024-11-16 23:51:32 +01:00
Partmedia
7557cc703c Add FreeBSD packaging target (#5522) 2024-11-16 02:01:17 +01:00
Leon Friedrich
7b81d0d881 Make PVS ignore duplicate view subscriber (#5502) 2024-11-13 00:07:53 +01:00
Leon Friedrich
b59f7801ac More UniqueIndex fixes (#5501) 2024-11-12 23:40:01 +01:00
FluffMe
d724c5b3eb Add conditional formatting to SpinBox buttons text (#5511) 2024-11-12 23:32:14 +01:00
Saphire Lattice
f812dc4dac Hopefully fix the dreaded VV refresh blink (#5517) 2024-11-12 23:31:39 +01:00
MilenVolf
2a1bcb6f1e Replace some obsolete TileAccess methods (#5516)
* Replace some obsolete TileAccess methods

* Guh
2024-11-12 23:17:40 +01:00
slarticodefast
fa9030e59c correct sandbox whitelist for Regex.Matches Method (#5513) 2024-11-12 21:48:39 +01:00
Pieter-Jan Briers
8dcae8631b Update NetSerializer
This enables sending of ImmutableArray<T>
2024-11-11 21:36:41 +01:00
Pieter-Jan Briers
21c3535486 Avoid unhandled exception handlers logging into disposed sawmills on shutdown
This caused a crash & exception swallow on Salamander.
2024-11-11 16:26:04 +01:00
lzk
4e100d96bc Add dative case function to loc manager (#5510)
* dative

* slipped it

* slipped it twice
2024-11-05 19:55:30 +01:00
qrtDaniil
14d3699ae2 Fix for server consoles without width and length (#5507)
* Update SystemConsoleManager.cs

* Update SystemConsoleManager.cs
2024-11-01 23:39:30 +01:00
Amy
350fa8736d add ref readonly to sandbox (#5506) 2024-10-30 02:49:25 +01:00
eoineoineoin
5a82df216d Fixes for rendering in multiple windows (#5497)
* Fix race condition when swapping buffers of secondary windows

* Avoid creating opengl 3.3 windows, to avoid Steam overlay bug

* Revert "Avoid creating opengl 3.3 windows, to avoid Steam overlay bug"

This reverts commit 97b5e7f461.

* Add CVar to perform unlocking test
2024-10-19 16:13:29 +02:00
Pieter-Jan Briers
32bca7cfd4 Version: 237.1.0 2024-10-19 12:03:52 +02:00
wixoa
008babebc6 Fix some window UIScale bugs (#5499)
* Fix some window UIScale bugs

* Use CalculateAutoScale()
2024-10-19 00:08:16 +02:00
Pieter-Jan Briers
c65c4ba57e Made csi reflection helpers get members up the inheritance chain too 2024-10-18 18:40:39 +02:00
Pieter-Jan Briers
eb5b838e61 Made csi type auto-completion aware of generic types 2024-10-18 18:40:39 +02:00
Pieter-Jan Briers
6b43036c9d Fix UniqueIndexHkm memory leaking
Yeah that's just great this goddamn data structure had no damn API to ever remove anything from it. Incredible.
2024-10-18 18:40:39 +02:00
ElectroJr
f23a55793d Version: 237.0.0 2024-10-18 16:11:33 +13:00
wixoa
46143d2589 Separate window creation in OSWindow.Show() to allow creation in the background (#5489)
* OSWindow rework
OSWindow now created ClydeWindow and WindowRoot immediately, but non-visible in the background
Also added the ability to programatically resize an open window

* Implement window resizing on SDL2

* Revert OSWindow changes

* Split `Show()` into `Create()` and `Show()`

* Formatting
2024-10-17 17:21:38 +02:00
MilenVolf
ba7d1452c1 Add Erase button for TileSpawnWindow (#5488)
* Add Erase button for TileSpawnWindow

Small QoL for mappers. Basically, it just selects space tile on "Erase" button toggled.

* Remove copy paste. Conevrt this into method
2024-10-13 15:55:57 +02:00
Pieter-Jan Briers
1c1343466e Improve docs for IConsoleShell.Player
Just realized this relation of "no player = server console" is not clearly documented.
2024-10-11 15:14:38 +02:00
Pieter-Jan Briers
0d534e8bcd Allow watchdog to specify more information about why the server should restart.
Had a plan to use this, but realized for what I'm doing immediately I don't quite need it yet.

/update server endpoint can now receive a Reason code and Message field. These are available with IWatchdogApi.RestartRequested.

Cleaned up IWatchdogApi: Added comments, moved symbols that should only be called by the engine to an internal interface. Also cleaned up some code in WatchdogApi to remove some IDE warnings.
2024-10-11 00:26:37 +02:00
Pieter-Jan Briers
c83c6f9592 Fix RobustSerializer breaking for non-seekable streams.
Shows up in replay loading from zip files, as the stream may be compressed. The statistics code in RobustSerializer assumes the stream is always seekable (by accessing .Position).

Now we don't run the statistics logic when reading/writing non-seekable-streams.
2024-10-10 04:31:37 +02:00
Pieter-Jan Briers
c794bd84bf Replay load: remove unnecessary bufferSize parameter.
This usage really doesn't make sense, and it makes the usage invalid if the size is zero. Now realistically I don't think this happens except in edge-case replay files, but it's still silly.

Removed if for no other reason than spite for making me look at this code and reason about it.
2024-10-10 04:06:39 +02:00
Pieter-Jan Briers
d1d43f834b Version: 236.1.0 2024-10-08 22:59:51 +02:00
Pieter-Jan Briers
9505cb68df Add SwitchExpressionException to sandbox
Fixes #5450
2024-10-07 19:05:17 +02:00
Ed
9763f5fdf4 filter entities (#5473) 2024-10-07 18:48:39 +02:00
Mervill
80a963ec05 Replace obsolete functions in MapSystem (#5483) 2024-10-07 18:48:09 +02:00
Mervill
3a670ec25e Replace obsolete functions in EntityLookup Test (#5482) 2024-10-07 18:34:22 +02:00
mhamster
e45950a557 Update BaseServer.cs (#5487)
+ Server now gives a proper reason of shutdown when shutting down before main loop has been started
2024-10-07 14:06:49 +02:00
eoineoineoin
6f1427ef3c Interface to remove a controls child by index (#5485)
Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es>
2024-10-06 22:51:53 +02:00
Mervill
9a7d1a39c1 Replace obsolete functions in GridFixtureSystem (#5484) 2024-10-06 02:03:01 +02:00
Mervill
9be903ee56 Replace obsolete functions in GridRotation Tests (#5481) 2024-10-05 13:21:26 +10:00
Mervill
72f9f9c343 Trim unused method variables (#5480) 2024-10-05 13:19:21 +10:00
metalgearsloth
3ad760a99e Add another lookup overload (#5477) 2024-10-03 18:56:44 +02:00
metalgearsloth
e2f3722ce9 Set sprite flicks immediately (#5467)
So on content we have an issue where the animation is played in doorsystem but sprite visibility is controlled by airlocksystem. The issue then is that we get a single frame where the incorrect sprite is shown before it corrects itself. The easiest way to reproduce this is to walk into a door that denies you and observe it shows the incorrect sprite then flickers to the denied one.

There might be more systems with these issues which is why I did this here instead.
2024-10-03 18:56:06 +02:00
Vasilis
b4beca6562 Expose GameTitle, WindowIconSet and SplashLogo to content (#5475)
* Expose GameTitle to public

Requirement for upstream ss14 pr

* Missing method implemented

* Add windowiconset and splashlogo (I think this is what pjb meant?)

I don't think its worth it to add the other stuff (modules, assemblyprefix, autocnnect, clientassemblies)

* Docs
2024-10-01 12:05:44 +02:00
Pieter-Jan Briers
ea02260230 Add LineEdit.SelectAllOnFocus 2024-10-01 01:07:08 +02:00
Leon Friedrich
3b243e487d Add required keyword attributes to sandbox whitelist (#5474)
* Add `required` keyword attributes to sandbox

* Release notes
2024-09-30 16:00:24 +02:00
Pieter-Jan Briers
f40ccb7558 New HWID system prep (#5446)
* New HWID system prep

* Allow HWID to be disabled.

Both client and server can now request HWID to be disabled.

On the server via CVar, if disabled the client won't send it.

On the client via env var, if disabled it won't be sent to the client.

This involved moving legacy HWID to be sent in MsgEncryptionResponse instead of MsgLoginStart. This means the legacy HWID won't be available anymore if the connection isn't authenticated.

* Fix tests

* Fix another test

* Review

* Thanks Rider
2024-09-29 00:29:02 +02:00
Stalen
f467a7027b Added MuteSounds property for BaseButton control (#5465) 2024-09-29 00:25:56 +02:00
eoineoineoin
c9d7d442d9 Make IPlayerManager accessible to derived classes (#5471)
Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es>
2024-09-28 23:48:07 +02:00
eoineoineoin
342626ad9b Account for scale when calculating sprite offset (#5470)
Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es>
2024-09-28 17:31:24 +02:00
metalgearsloth
1c3ea968e4 Version: 236.0.0 2024-09-28 19:16:35 +10:00
Pieter-Jan Briers
f0ed3537ee Duplicate dependency field analyzer (#5463)
* Duplicate dependency field analyzer

Detects cases of duplicate [Dependency] fields in a type. We apparently have 27 of these across RT + SS14.

* Fix duplicate dependencies in Robust
2024-09-28 15:35:18 +10:00
metalgearsloth
74e7e61a98 Revert "Make resetting contacts on the client only set is touching if it is true" (#5469)
This reverts commit cdb94748c8.
2024-09-28 14:33:37 +10:00
metalgearsloth
fb9b0ae89b Remove IsTouching set on physics prediction (#5468)
Just because an entity sleeps doesn't mean it's not touching necessarily. This causes client to mispredict against server and continuously fire collision events if we try to move into an entity.

Easiest way to reproduced is to walk into a locked airlock and watch it flicker constantly.
2024-09-28 14:13:05 +10:00
Stalen
dbe297b1fc Activate XAML hot reload on file rename (for VS support) (#5429) 2024-09-24 09:43:00 +10:00
Leon Friedrich
b84917e8e4 Obsolete some static localization methods (#5460) 2024-09-24 09:40:42 +10:00
Leon Friedrich
abb3f65fe4 Make EnsureEntityDictionary use TryAdd (#5461) 2024-09-24 09:40:16 +10:00
Leon Friedrich
41ec2dc131 Try improve PVS exception tolerance a bit more (#5454) 2024-09-24 09:39:33 +10:00
eoineoineoin
e714dcc83c Fix TabContainer click detection when UIScale was not == 1.0 (#5456)
* Fix tabcontainer click detection when UIScale was not == 1.0

* Remove whitespace

---------

Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es>
2024-09-22 14:41:27 +02:00
ShadowCommander
46291af1be Add ProtoId parser to Toolshed (#5220)
* Add ProtoId parser to Toolshed

* Change obsolete FromMarkup to FromMarkupOrThrow
2024-09-21 21:57:08 +10:00
Leon Friedrich
ad929c9955 Fix ICommonSession.Ping (#5453) 2024-09-20 16:43:12 +02:00
metalgearsloth
c86cb0b795 Version: 235.0.0 2024-09-18 12:13:35 +10:00
metalgearsloth
8d03feb84f Transform precision thing (#5451)
Just noticed it but probably doesn't affect anything really, we'll go from 64bit to 32bit after the math operations and not before.
2024-09-18 12:08:36 +10:00
Plykiya
0fa21ee2d2 Completely obsolete noSpawn (#5364) 2024-09-18 11:48:13 +10:00
eoineoineoin
9be0f032e8 Fix DistanceJoints drawn by physics debug system (#5439)
Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es>
2024-09-18 11:45:01 +10:00
Leon Friedrich
afffb33446 Stop empty audio system filters from playing sounds for all players (#5444)
* Fix audio system empty filter bug

* The nullable attributes are lying
2024-09-18 11:44:16 +10:00
Leon Friedrich
19a87fb67a Remove incorrect NotNullIfNotNull attributes in SharedAudioSystem (#5449) 2024-09-18 11:43:49 +10:00
DrSmugleaf
2fda62a274 Fix physics.maxlinvelocity not being a replicated cvar (#5445) 2024-09-17 12:51:54 +10:00
ike709
5218bf70b0 Bump cefglue (#5441)
* Bump cefglue

* Another bump

* Third time's the charm

---------

Co-authored-by: ike709 <ike709@github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2024-09-16 22:31:10 +02:00
Pieter-Jan Briers
4f95c07ab3 Add missing Roslyn components to solution 2024-09-16 21:34:59 +02:00
Lgibb18
786acae47a Fix tags with controls in RichText and OutputPanel (#5428)
* Controls in RichText fixes

* useless

* Get FormattedMessage from RichTextLabel

* dont go through nodes

* Comments and minor changes

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2024-09-16 14:13:59 +10:00
Leon Friedrich
f81e30a031 Try fix invalid PVS index bug (#5422)
* Try fix invalid PVS index bug

* bounds check

* More Asserts

* fix assert?

* remove deletion

* a

* A!
2024-09-16 14:12:20 +10:00
Leon Friedrich
f5c1d870f9 Improve FlushEntities() error logs (#5427)
* Improve FlushEntities() error logs

* log count before flush
2024-09-16 14:06:04 +10:00
Leon Friedrich
4949b34c88 Fix "to" and "take" toolshed commands (#5438) 2024-09-13 22:34:30 +10:00
metalgearsloth
0f60ad9018 Version: 234.1.0 2024-09-12 17:56:50 +10:00
metalgearsloth
f7287b181d Fix audioparams for playglobal (#5437) 2024-09-12 17:48:37 +10:00
Fildrance
45b7500d93 feat: added audio system predicted method for only one receiver (#5435)
* feat: added audio system predicted method for only one receiver

* renamed to PlayLocal

* tweak

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2024-09-12 17:41:40 +10:00
metalgearsloth
dbe6f65880 Version: 234.0.0 2024-09-12 13:56:27 +10:00
metalgearsloth
4faef1bfd3 Add another lookup override (#5436) 2024-09-12 13:53:36 +10:00
Kara
48d70a09c6 Remove most fully-obsoleted code (#5433) 2024-09-11 19:38:26 +10:00
Pieter-Jan Briers
f682fb9cc7 Obsolete some useless type proxies on IResourceCache
These aren't even used, but they're pretty objectively bad ideas so let's obsolete them so we can get rid of them later.
2024-09-09 11:22:07 +02:00
Pieter-Jan Briers
814e5bcf17 Mark large replays as requiring Server GC.
This should significantly improve loading performance of large replays.

System can be controlled by replay.server_gc_size_threshold, which defaults to 50 MiB.

This is the engine-side of https://github.com/space-wizards/SS14.Launcher/issues/177
2024-09-09 08:23:20 +02:00
metalgearsloth
dbc4e80e61 Version: 233.1.0 2024-09-08 17:55:56 +10:00
metalgearsloth
5eb5ddd96e Add some entitylookup methods (#5431) 2024-09-08 17:22:48 +10:00
Leon Friedrich
405ed378c0 Re-attempt FlushEntities() on failure (#5423) 2024-09-06 20:51:34 +10:00
Leon Friedrich
be9db264dd Minor toolshed fixes / tweaks (#5315)
* Don't use markup for type names

* Cache TypeTypeParser completions

* Cache all type parsers

* Release notes

* More IConError fixes

* a
2024-09-06 10:48:50 +10:00
Pieter-Jan Briers
2f73f6190d Fix warning in ScriptGlobalsShared.cs 2024-09-04 21:31:37 +02:00
metalgearsloth
f3dfa1f666 Move testbed command to benchmarks (#5424) 2024-09-03 22:02:00 +10:00
Pieter-Jan Briers
b0d17e9527 Fix dead code equals method in Polygon
Fixes #5420

Sloth clarified it's dead code from copy pasting.
2024-09-02 13:17:02 +02:00
Pieter-Jan Briers
4c81e68bf1 Remove last FormattedMessage.FromMarkup calls 2024-09-02 07:36:34 +02:00
Pieter-Jan Briers
4490751001 Fix warnings in FormattedMessageSerializerTest.cs 2024-09-02 07:33:26 +02:00
Pieter-Jan Briers
bc8d2c154c Fix warnings in EntityManager_Components_Tests.cs 2024-09-02 07:32:47 +02:00
Pieter-Jan Briers
3c83f8e62a Make Rider not complain about Is. in Robust.UnitTesting, globally. 2024-09-02 07:27:23 +02:00
Pieter-Jan Briers
c36919d76a Fix warnings in ArithmeticTest.cs 2024-09-02 05:45:08 +02:00
Pieter-Jan Briers
70a853cdd5 Fix most warnings in AnchoredSystemTests.cs
Remaining warnings are cases where AnchorEntity, SetWorldPosition and SetLocalPosition subtly different from their component counterparts, and this triggers a test failure. Some help would be appreciated here.
2024-09-02 05:38:40 +02:00
Pieter-Jan Briers
fd3eb092cc EntityUid-only overloads for some TransformSystem methods
AnchorEntity and Unanchor
2024-09-02 05:37:49 +02:00
Pieter-Jan Briers
c740026014 Entity<T> overloads for some MapSystem methods.
GetTileRef, TileIndicesFor, and GetAnchoredEntities
2024-09-02 05:36:11 +02:00
Pieter-Jan Briers
44f9262d1a SimulationExtensions helpers for RobustServerSimulation
No more need to manually resolve IEntityManager in every test.
2024-09-02 05:30:10 +02:00
Pieter-Jan Briers
df2160b151 Fix warnings in Broadphase_Test.cs 2024-09-02 04:10:54 +02:00
Pieter-Jan Briers
5c7b1e6823 Fix warnings in ToolshedTest.cs 2024-09-02 04:01:24 +02:00
Pieter-Jan Briers
eaf7a6ba0f Fix warnings in ToolshedTypesTest.cs 2024-09-02 03:59:55 +02:00
Pieter-Jan Briers
9ab4286592 Wait why is that even returning a task in the first place 2024-09-02 03:59:25 +02:00
Pieter-Jan Briers
3f02ef3730 Fix warnings in PvsSystemTests.cs 2024-09-02 03:57:54 +02:00
Pieter-Jan Briers
2f17cbb1dc Fix warnings in ToolshedTypesTest.BugCheck.cs 2024-09-02 03:57:09 +02:00
Pieter-Jan Briers
c2657812f5 Fix warnings in GridTraversalTest.cs 2024-09-02 03:56:42 +02:00
Pieter-Jan Briers
f17f077849 Fix warnings in FormattedMessage_Test.cs 2024-09-02 03:55:14 +02:00
Pieter-Jan Briers
306deddbd2 Fix warnings in TransformComponent_Tests.cs 2024-09-02 03:54:52 +02:00
Pieter-Jan Briers
cdd8df743a Fix warnings in Transform_Test.cs 2024-09-02 03:49:06 +02:00
Pieter-Jan Briers
e7ac5ad047 Fix warnings in UserInterfaceManagerTest.cs 2024-09-02 03:45:46 +02:00
Pieter-Jan Briers
0e621a26be Fix warnings in ControlTest.cs 2024-09-02 03:45:46 +02:00
Pieter-Jan Briers
bbcc7cfe1f Fix warnings in GameLoop_Test.cs 2024-09-02 03:45:46 +02:00
Mervill
1208c25dcd resolve instances of the CS8974 warning (#5418) 2024-09-02 03:29:10 +02:00
Pieter-Jan Briers
38c227b692 Fix MarkupNode equality
Implement GetHashCode()

Fix Equals doing reference equality on the attributes.

Small code cleanup.

Actually mark it as IEquatable<MarkupNode> because we already implement Equals() so there's no reason not to.
2024-09-02 03:27:25 +02:00
Mervill
4e73d72753 Remove unused IoC dependencies (#5419) 2024-09-01 23:23:02 +02:00
Pieter-Jan Briers
b1e1a0cd88 Quick warning fixes (#5417) 2024-09-01 04:54:28 +02:00
metalgearsloth
6e25ead588 Version: 233.0.2 2024-08-31 18:39:01 +10:00
metalgearsloth
cfae6e1f95 Don't rely on client for grid fixture rebuilds (#5348)
* Don't rely on client for grid fixture rebuilds

Server is already networking fixture data and this has a chance to go bad.
Easier to just stop this entirely and remove the fixture references to just network the relevant ones for each chunk. Performance impact should pretty much be non-existent and it should be less buggy.

* a

* weh notes

* fix aabb update

* Fix AABB gen

* weh

* More networking
2024-08-31 18:37:43 +10:00
metalgearsloth
da56851846 Version: 233.0.1 2024-08-31 18:22:39 +10:00
metalgearsloth
69ed2c3c33 Fix IsHardCollidable (#5416) 2024-08-31 17:49:59 +10:00
metalgearsloth
c558a0327b Version: 233.0.0 2024-08-31 14:37:23 +10:00
metalgearsloth
3bb7df3254 Relative lookup fix (#5415)
* Relative lookup fix

Some of the transforms weren't being transformed, added another test.

* test

* better test

* Reduce any-entities-intersecting tests
2024-08-31 14:35:17 +10:00
metalgearsloth
ab6bd19817 Fix mouse hover not updating for new controls (#5313)
* Fix mouse hover not updating for new controls

It only ever updated on movement previously. The issue is for example if a new window shows up where the mouse is (or any control) it doesn't handle it. Just checking it every frame AFAIK shouldn't be that expensive. Worst case we just have some flag to check it that gets set on <mouse movement OR controls changed>.

* review
2024-08-31 11:45:59 +10:00
Leon Friedrich
73ef69aa94 Try and ensure that parents are always initialized before children (#5343)
* Try and ensure that parents are always initialized before children

* release notes
2024-08-31 11:05:56 +10:00
nikthechampiongr
656835e7fa Change EntityRenamedEvents arguments and make it broadcast (#5413) 2024-08-31 11:04:44 +10:00
Pieter-Jan Briers
26c87b5858 Make tests run parallelizable (#5412)
Hope this won't cause issues.

Massively improves test speed.
2024-08-31 11:04:21 +10:00
Pieter-Jan Briers
be36001ab8 Add thread check assert to core entity mutation commands. (#5411)
* Add thread check assert to core entity mutation commands.

Creation of entities and components is now checked to happen from the main thread.

This is already catching multiple buggy integration tests, these will be addressed in separate commits.

* Fix broken tests directly mutating entities from wrong thread.
2024-08-31 11:04:02 +10:00
Pieter-Jan Briers
2002402af8 Version script now supports dash versions 2024-08-29 12:52:52 +02:00
metalgearsloth
f659b2b58c Version: 232.0.0 2024-08-29 12:55:51 +10:00
metalgearsloth
b1e13f5b13 Fix BUI interfaces not deep copying (#5410)
* Fix BUI interfaces not deep copying

Didn't think I'd need to on a getstate but client state moment.

* less shitcodey
2024-08-29 12:47:32 +10:00
SlamBamActionman
e5995d4edc Add ObjectSerializer, AppearanceComponent.AppearanceDataInit, and AppearanceSystem.AppendData (#5324)
* V1 commit

* V2 Commit, ObjectSerializer

* Make sure write for objects have the !type:<T> set

* Added AppearanceDataInit

* Change to AppearanceDataInit setting to AppearanceData the moment it itself gets set; ComponentInit is too late. Forgive me sloth.

* RELEASE-NOTES.md

* Fix release notes

* Fix release-notes for realsies
2024-08-28 22:43:58 +10:00
Winkarst
6eb080a277 Add Robust.Xaml.csproj to the solution (#5408) 2024-08-28 13:49:42 +02:00
metalgearsloth
b0cb41e94a Version: 231.1.1 2024-08-28 12:23:04 +10:00
Leon Friedrich
23a23f7c22 Misc toolshed fixes (#5340)
* Prevent map/emplace command errors from locking up the server

* Fix EmplaceCommand

* Fix sort commands

* Fix JoinCommand

* changelog

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2024-08-28 12:22:47 +10:00
Pieter-Jan Briers
ec3a74d268 Version: 231.1.0 2024-08-27 17:47:25 +02:00
Pieter-Jan Briers
12b0bc4e0a Add way for content to write arbitrary files into replay. (#5405)
Added a new RecordingStopped2 event that receives a IReplayFileWriter object that can be used to write arbitrary files into the replay zip file.

Fixes #5261
2024-08-27 17:38:48 +02:00
metalgearsloth
903041dfd1 Add storage BUI bandaid (#5401) 2024-08-27 17:36:54 +02:00
metalgearsloth
b96419f0b2 Add mapmanager query tests (#5403)
Sanity
2024-08-28 00:24:24 +10:00
metalgearsloth
fe33ad2652 Add physicshull tests (#5404) 2024-08-27 23:24:23 +10:00
metalgearsloth
057a68b366 Minor allocs reductions (#5330)
* Minor allocs reductions

Added a poly struct with the intention of replacing the existing one whenever I finish box2c port.

* fix merges

* Revert some stuff

* Poly tests
2024-08-27 22:58:42 +10:00
metalgearsloth
1a2c9008fe Add Box matrix tests (#5402)
Thought we had but apparently not.
2024-08-27 22:21:48 +10:00
metalgearsloth
cd95929ebe Heavily optimise entitylookup (#5400)
* Heavily optimise entitylookup

Previously I made it so MOST of entitylookup goes through the same 4 or 5 codepaths and uses shapes. The downside was some codepaths were made much slower in the process.

This fixes most of the going up and down lookups that some codepaths did. It should also be faster than the pre-shapes version because GetLocalPhysicsTransform is being used for the non-approx queries and most entities are parented directly to their broadphase.

* Tests and confidence

* code

* dang
2024-08-27 21:32:50 +10:00
Nyeogmi
6396ec472d XAML hot reloading (#5350)
* Move RobustXaml to a shared package

In a near-future change, I'll make it possible to optionally link to
this from Robust.Client, which will allow JIT compiling XAML.

Also upgrade it to a version of .NET that supports nullability
annotations.

* Re-namespace packages

* Add a JIT compiler, plus hooks that call into it

In Debug, after this change, all XAML will be hot reloaded once every
time an assembly is reloaded.

The new code is compiled with SRE and is _not_ sandboxed -- this is not
suitable to run against prod.

In Release, the hot reload path is totally skipped, using the same trick
as SmugLeaf used in an earlier attempt to implement this functionality.

* Hot reload: watcher

This is a bit of a horror, but there's not in-engine support for
identifying the source tree or the XAML files in it.

* Put everything dangerous behind conditional comp

* Code cleanup, docs

* Fix a bad comment

* Deal a little better with crashes in the watcher

* Make reload failures Info, since they're expected

They were previously causing the integration tests to flag, even though
"a few types fail hot reloading because they're internal" is expected
behavior.

* Fix an unnecessary null check

I removed the ability for CompileCore to return null.

* injectors: null! strings, default primitives

* Tidy documentation (thanks, PJB!)

* Reinstate netstandard2.0, abolish Pidgin

* Internal-ize all of Robust.Xaml

* Add a cautionary note to Sandbox.yml

* Shuffle around where conditional compilation occurs

* Privatize fields in XamlImplementationStorage

* Internalize XamlJitDelegate

* Inline some remarks. No cond. comp in Robust.Xaml

* Use file-scoped namespaces

They aren't allowed at Language Level 8.0. (which I arbitrarily picked
for Robust.Xaml because it's the oldest one that would work)

* Bump language level for R.Xaml, file namespaces

* Force hot reloading off for integration tests

* Fix bizarre comment/behavior in XamlImplementationStorage

* Consistently use interfaces, even in generated code

* Update Robust.Client/ClientIoC.cs

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2024-08-27 02:16:57 +02:00
Stalen
d7aa5daf6a Add decimal type to sandbox whitelist (#5396) 2024-08-27 01:35:42 +02:00
metalgearsloth
e3819f8245 Network interfacedata (#5399)
If UIs are dynamically changed this fixes it.
2024-08-26 18:48:15 +10:00
metalgearsloth
57f133b742 Version: 231.0.1 2024-08-26 14:41:40 +10:00
metalgearsloth
04344ffe19 Make PVS exception log better (#5397) 2024-08-26 14:31:05 +10:00
metalgearsloth
f2ee9a43f9 Version: 231.0.0 2024-08-25 22:48:51 +10:00
metalgearsloth
8d5ebd830a Add CompRegistry methods to EntManager / CompFac (#5379)
* Add CompRegistry methods to EntManager / CompFac

CompRegistries are nice to use and this just makes it a bit easier to extend functionality.

* fix bad pull
2024-08-25 21:37:14 +10:00
metalgearsloth
4d265b2210 Add methods to get entity sprite position (#5381)
* Add methods to get entity sprite position

No easy way to get this in world-terms and I want a control to track it.

* invalid
2024-08-25 21:24:37 +10:00
metalgearsloth
e04caf7eb4 Add OpenScreenAt for windows (#5387)
If I want to open it at a particular position. Takes in clyde ref so not every single screen needs to keep the ref.
2024-08-25 20:32:28 +10:00
metalgearsloth
a4c54d3602 Make pointlight setting use an attempt event (#5378)
Makes it easy for content to add functionality if multiple things try to set it without having to funnel every piece of code through a content system.
2024-08-25 20:27:58 +10:00
metalgearsloth
43fd6bc764 Add FixturesChangeComponent (#5383)
* Add FixturesChangeComponent

Adds / removes fixtures. Useful when used in conjunction with EntProtoId to dynamically add / remove fixtures.

* Move to system

* Fix allcomps test
2024-08-25 20:25:19 +10:00
metalgearsloth
ff056552fe Don't spam BUI closing in state handling (#5382)
We just do what content does and defer it until update. Saves performance + we don't have some BUIs that do special logic on open re-running it constantly (e.g. open on mouse position).
2024-08-25 20:08:19 +10:00
metalgearsloth
3014d9880e Fix prototype flag add not actually working (#5376)
tryindex was inverted and causing issues with tools.
2024-08-25 20:03:09 +10:00
metalgearsloth
679c31199d Add comments to sermanager attributes (#5384)
I always forget which is which.
2024-08-25 20:02:31 +10:00
metalgearsloth
0bc3c51707 Contact QOL stuff (#5385)
* Contact QOL stuff

Only just made the enumerator version need to test with AI branch had an IEnumerable before.

* Fix
2024-08-25 11:54:05 +10:00
metalgearsloth
140767c262 Move viewsubscriber to shared + handle eye targets (#5362)
* Move viewsubscriber to shared + handle eye targets

Need this stuff for AI.

* Fix dumb

* review
2024-08-23 18:29:57 +10:00
371 changed files with 7192 additions and 3109 deletions

View File

@@ -58,7 +58,7 @@
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="System.Numerics.Vectors" Version="4.5.0" />

View File

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

View File

@@ -54,6 +54,321 @@ END TEMPLATE-->
*None yet*
## 237.2.2
## 237.2.1
## 237.2.0
### Breaking changes
* `SharedEyeSystem..SetTarget()` will now also automatically remove the old target from the session's ViewSubscriptions
### New features
* `ImmutableArray<T>` can now be serialized by `RobustSerializer`.
* `RequiresLocationAttribute`, used by `ref readonly`, is now allowed by the sandbox.
* Added `DAT-OBJ()` localization function, for the dative case in certain languages.
* Client builds for FreeBSD are now made.
* Added `FormattedMessage.TrimEnd()`.
* Added Toolshed `with` for `ProtoId<T>`.
### Bugfixes
* Fix `UniqueIndex<,>.RemoveRange()` and`UniqueIndexHkm<,>.RemoveRange()` clearing the whole set instead of just removing the specified values.
* Avoid server crashes on some weird console setups (notably Pterodactyl).
* Avoid unhandled exceptions during server shutdown getting swallowed due logging into a disposed logger.
* Fix sandbox definitions for `Regex` functions returning `MatchCollection`.
* Fix minor layout bugs with `SplitContainer` and `BoxContainer`.
### Other
* Changed how multi-window rendering presents to the screen with a new CVar `display.thread_unlock_before_swap`. This is an experiment to see if it solves some synchronization issues.
* View Variables no longer clears the window on refresh while waiting on response from server.
* `SpinBox` buttons now have a `+` prefix for the positive ones.
* Improve Toolshed type intersection mechanism
### Internal
* Warning cleanup.
## 237.1.0
### New features
* csi's auto import-system can now handle generic types.
* csi's reflection helpers (like `fld()`) handle private members up the inheritance chain.
### Bugfixes
* Fix `UniqueIndexHkm<,>` and, by extension, entity data storage memory leaking.
* Fix bugs related to UIScale on `OSWindow`s.
## 237.0.0
### Breaking changes
* `IClydeWindow.Size` is now settable, allowing window sizes to be changed after creation.
### New features
* The game server's `/update` endpoint now supports passing more information on why an update is available.
* This information is accessible via `IWatchdogApi.RestartRequested`.
* Information can be specified by passing a JSON object with a `Reason` code and `Message` field.
* Added an "Erase" button to the tile spawn menu.
* Added `OSWindow.Create()`, which allows OS windows to be created & initialised without immediately opening/showing them.
### Other
* Made `WatchdogApi` and some members of `IWatchdogApi` private. These symbols should never have been accessed by content.
## 236.1.0
### New features
* `RequiredMemberAttribute` and `SetsRequiredMembersAttribute` have been added to the sandbox whitelist. I.e., you can now use the `required` keyword in client/shared code.
* Added `SwitchExpressionException` to sandbox. This type gets used if you have a `switch` expression with no default case.
* Added `LineEdit.SelectAllOnFocus`.
* `GameTitle`, `WindowIconSet` and `SplashLogo` are exposed in `IGameController`. These will return said information set in game options or whatever is set in `manifest.yml`.
* `BoundUserInterface` inheritors now have access to `PlayerManager`.
* Added `MuteSounds` bool to `BaseButton`.
* The engine has a new future-proof HWID system.
* The auth server now manages HWIDs. This avoids HWID impersonation attacks.
* The auth server can return multiple HWIDs. They are accessible in `NetUserData.ModernHWIds`.
* The auth server also returns a trust score factor, accessible as `NetUserData.Trust`.
* HWID can be disabled client side (`ROBUST_AUTH_ALLOW_HWID` env var) or server side (`net.hwid` cvar).
* The old HWID system is still in place. It is intended that content switches to placing new bans against the new HWIDs.
* Old HWIDs no longer work if the connection is not authenticated.
* `launchauth` command now recognizes `SS14_LAUNCHER_APPDATA_NAME`.
* Added new overload to `EntityLookupSystem.GetEntitiesIntersecting`.
* Added `Control.RemoveChild(int childIndex)`.
* `build.entities_category_filter` allows filtering the entity spawn panel to a specific category.
### Bugfixes
* Fixed `SpriteView` offset calculations when scaled.
### Other
* Sprite flicks are applied immediately when started.
* More warning fixes.
* If the server gets shut down before finishing startup, the reason is now logged properly.
## 236.0.0
### Breaking changes
* Revert IsTouching only being set to true if the contact were laready touching in clientside physics prediction.
* Don't touch IsTouching if both bodies are asleep for clientside physics contacts. This change and the one above should fix a lot of clientside contact issues, particularly around repeated incorrect clientside contact events.
### New features
* Added an analyzer to detect duplicate Dependency fields.
### Bugfixes
* Auto-networked dictionaries now use `TryAdd()` to avoid duplicate key errors when a dictionary contains multiple unknown networked entities.
* Fixed `ICommonSession.Ping` always returning zero instead of the ping. Note that this will still return zero for client-side code when trying to get the ping of other players.
* Hot reload XAML files on rename to fix them potentially not being reloaded with Visual Studio.
* Fix TabContainer click detection for non-1.0 UI scales.
### Other
* Obsolete some static localization methods.
* Tried to improve PVS tolerance to exceptions occurring.
## 235.0.0
### Breaking changes
* Several different `AudioSystem` methods were incorrectly given a `[return: NotNullIfNotNull]` attribute. Content code that uses these methods needs to be updated to perform null checks.
* noSpawn is no longer obsolete and is now removed in lieu of the EntityCategory HideSpawnMenu.
### Bugfixes
* physics.maxlinvelocity is now a replicated cvar.
* Fix DistanceJoint debug drawing in physics not using the local anchors.
* Fixed filtered AudioSystem methods playing a sound for all players when given an empty filter.
* Fixed equality checks for `MarkupNode` not properly handling attributes.
* Fixed `MarkupNode` not having a `GetHashCode()` implementation.
* Fixed a PVS error that could occur when trying to delete the first entity that gets created in a round.
* Fixed the "to" and "take" toolshed commands not working as intended.
* Rich text controls within an `OutputPanel` control will now become invisible when they are out of view.
### Other
* Improve precision for Quaternion2D constructor from angles.
## 234.1.0
### New features
* SharedAudioSystem now has PlayLocal which only runs audio locally on the client.
### Bugfixes
* Fix AudioParams not being passed through on PlayGlobal methods.
## 234.0.0
### Breaking changes
* Remove a lot of obsoleted code that has been obsoleted for a while.
### New features
* Add another GetLocalEntitiesIntersecting override.
### Other
* Mark large replays as requiring Server GC.
* Obsolete some IResourceCache proxies.
## 233.1.0
### New features
* Add GetGridEntities and another GetEntitiesIntersecting overload to EntityLookupSystem.
* `MarkupNode` is now `IEquatable<MarkupNode>`. It already supported equality checks, now it implements the interface.
* Added `Entity<T>` overloads to the following `SharedMapSystem` methods: `GetTileRef`, `GetAnchoredEntities`, `TileIndicesFor`.
* Added `EntityUid`-only overloads to the following `SharedTransformSystem` methods: `AnchorEntity`, `Unanchor`.
### Bugfixes
* Fixed equality checks for `MarkupNode` not properly handling attributes.
* Fixed toolshed commands failing to generate error messages when working with array types
* Fixed `MarkupNode` not having a `GetHashCode()` implementation.
### Other
* If `EntityManager.FlushEntities()` fails to delete all entities, it will now attempt to do so a second time before throwing an exception.
## 233.0.2
### Bugfixes
* Fix exceptions in client game state handling for grids. Now they will rely upon the networked fixture data and not try to rebuild in the grid state handler.
## 233.0.1
### Bugfixes
* Fix IsHardCollidable component to EntityUid references.
## 233.0.0
### Breaking changes
* Made EntityRenamed a broadcast event & added additional args.
* Made test runs parallelizable.
* Added a debug assert that other threads aren't touching entities.
### Bugfixes
* Fix some entitylookup method transformations and add more tests.
* Fix mousehover not updating if new controls showed up under the mouse.
### Internal
* `ClientGameStateManager` now only initialises or starts entities after their parents have already been initialized. There are also some new debug asserts to try ensure that this rule isn't broken elsewhere.
* Engine version script now supports dashes.
## 232.0.0
### Breaking changes
* Obsolete method `AppearanceComponent.TryGetData` is now access-restricted to `SharedAppearanceSystem`; use `SharedAppearanceSystem.TryGetData` instead.
### New features
* Added `SharedAppearanceSystem.AppendData`, which appends non-existing `AppearanceData` from one `AppearanceComponent` to another.
* Added `AppearanceComponent.AppearanceDataInit`, which can be used to set initial `AppearanceData` entries in .yaml.
### Bugfixes
* Fix BUI interfaces not deep-copying in state handling.
* Add Robust.Xaml.csproj to the solution to fix some XAML issues.
### Other
* Serialization will now add type tags (`!type:<T>`) for necessary `NodeData` when writing (currently only for `object` nodes).
### Internal
* Added `ObjectSerializer`, which handles serialization of the generic `object` type.
## 231.1.1
### Bugfixes
* Fixed a bug where the client might not add entities to the broadphase/lookup components.
* Fixed various toolshed commands not working, including `sort`, `sortdown` `join` (for strings), and `emplace`
### Other
* Toolshed command blocks now stop executing if previous errors were not handled / cleared.
## 231.1.0
### New features
* Network `InterfaceData` on `UserInterfaceComponent`.
* Added `System.Decimal` to sandbox.
* Added XAML hot reloading.
* Added API for content to write custom files into replay through `IReplayFileWriter`.
### Other
* Optimized `EntityLookup` and other physics systems.
### Internal
* Added more tests related to physics.
## 231.0.1
### Other
* Add better logging to failed PVS sends.
## 231.0.0
### Breaking changes
* ViewSubscriber has been moved to shared; it doesn't actually do anything on the client but makes shared code easier.
### New features
* ContactEnumreator exists to iterate the contacts of a particular entity.
* Add FixturesChangeComponent as a generic way to add and remove fixtures easily.
* PointLightComponent enabling / disabling now has an attempt event if you wish to block it on content side.
* There's an OpenScreenAt overload for screen-relative coordinates.
* SpriteSystem has methods to get an entity's position in sprite terms.
* EntityManager and ComponentFactory now have additional methods that interact with ComponentRegistry and ComponentRegistryEntry.
### Bugfixes
* Fix PrototypeFlags Add not actually working.
* Fix BUIs going BRRT opening and closing repeatedly upon prediction. The closing now gets deferred to the update loop if it's still closed at the end of prediction.
## 230.2.0
### New features

View File

@@ -18,3 +18,9 @@
description: entity-category-desc-hide
hideSpawnMenu: true
inheritable: false
# Entity prototypes added by the fork. With CVar you can hide all entities without this category
- type: entityCategory
id: ForkFiltered
name: entity-category-name-fork
description: entity-category-desc-fork

View File

@@ -20,6 +20,15 @@ zzzz-object-pronoun = { GENDER($ent) ->
*[neuter] it
}
# Used internally by the DAT-OBJ() function.
# Not used in en-US. Created for supporting other languages.
zzzz-dat-object = { GENDER($ent) ->
[male] him
[female] her
[epicene] them
*[neuter] it
}
# Used internally by the POSS-PRONOUN() function.
zzzz-possessive-pronoun = { GENDER($ent) ->
[male] his

View File

@@ -4,9 +4,16 @@ entity-spawn-window-title = Entity Spawn Panel
entity-spawn-window-search-bar-placeholder = search
entity-spawn-window-clear-button = Clear
entity-spawn-window-replace-button-text = Replace
entity-spawn-window-erase-button-text = Erase Mode
entity-spawn-window-override-menu-tooltip = Override placement
## TileSpawnWindow
tile-spawn-window-title = Place Tiles
## Console
console-line-edit-placeholder = Command Here
## Common Used
window-erase-button-text = Erase Mode

View File

@@ -7,3 +7,6 @@ entity-category-desc-spawner = Entity prototypes that spawn other entities.
entity-category-name-hide = Hidden
entity-category-desc-hide = Entity prototypes that should be hidden from entity spawn menus
entity-category-name-fork = Fork Filtered
entity-category-desc-fork = Entity prototypes added by the fork. With CVar you can hide all entities without this category

View File

@@ -219,9 +219,9 @@ command-description-MulVecCommand =
command-description-DivVecCommand =
Divides every element in the input by a scalar (single value).
command-description-rng-to =
Returns a number from its input to its argument (i.e. n..m inclusive)
Returns a number between the input (inclusive) and the argument (exclusive).
command-description-rng-from =
Returns a number to its input from its argument (i.e. m..n inclusive)
Returns a number between the argument (inclusive) and the input (exclusive))
command-description-rng-prob =
Returns a boolean based on the input probability/chance (from 0 to 1)
command-description-sum =

View File

@@ -0,0 +1,63 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier<Robust.Analyzers.DuplicateDependencyAnalyzer>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
[TestOf(typeof(DuplicateDependencyAnalyzer))]
public sealed class DuplicateDependencyAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<DuplicateDependencyAnalyzer, NUnitVerifier>()
{
TestState =
{
Sources = { code }
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.IoC.DependencyAttribute.cs"
);
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task Test()
{
const string code = """
using Robust.Shared.IoC;
public sealed class Foo
{
[Dependency]
private object? Field;
[Dependency]
private object? Field2;
[Dependency]
private string? DifferentField;
private string? NonDependency1;
private string? NonDependency2;
}
""";
await Verifier(code,
// /0/Test0.cs(9,21): warning RA0032: Another [Dependency] field of type 'object?' already exists in this type as field 'Field'
VerifyCS.Diagnostic().WithSpan(9, 21, 9, 27).WithArguments("object?", "Field"));
}
}

View File

@@ -0,0 +1,126 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
#nullable enable
/// <summary>
/// Analyzer that detects duplicate <c>[Dependency]</c> fields inside a single type.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DuplicateDependencyAnalyzer : DiagnosticAnalyzer
{
private const string DependencyAttributeType = "Robust.Shared.IoC.DependencyAttribute";
private static readonly DiagnosticDescriptor Rule = new(
Diagnostics.IdDuplicateDependency,
"Duplicate dependency field",
"Another [Dependency] field of type '{0}' already exists in this type with field '{1}'",
"Usage",
DiagnosticSeverity.Warning,
true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(compilationContext =>
{
var dependencyAttributeType = compilationContext.Compilation.GetTypeByMetadataName(DependencyAttributeType);
if (dependencyAttributeType == null)
return;
compilationContext.RegisterSymbolStartAction(symbolContext =>
{
var typeSymbol = (INamedTypeSymbol)symbolContext.Symbol;
// Only deal with non-static classes, doesn't make sense to have dependencies in anything else.
if (typeSymbol.TypeKind != TypeKind.Class || typeSymbol.IsStatic)
return;
var state = new AnalyzerState(dependencyAttributeType);
symbolContext.RegisterSyntaxNodeAction(state.AnalyzeField, SyntaxKind.FieldDeclaration);
symbolContext.RegisterSymbolEndAction(state.End);
},
SymbolKind.NamedType);
});
}
private sealed class AnalyzerState(INamedTypeSymbol dependencyAttributeType)
{
private readonly Dictionary<ITypeSymbol, List<IFieldSymbol>> _dependencyFields = new(SymbolEqualityComparer.Default);
public void AnalyzeField(SyntaxNodeAnalysisContext context)
{
var field = (FieldDeclarationSyntax)context.Node;
if (field.AttributeLists.Count == 0)
return;
if (context.ContainingSymbol is not IFieldSymbol fieldSymbol)
return;
// Can't have [Dependency]s for non-reference types.
if (!fieldSymbol.Type.IsReferenceType)
return;
if (!IsDependency(context.ContainingSymbol))
return;
lock (_dependencyFields)
{
if (!_dependencyFields.TryGetValue(fieldSymbol.Type, out var dependencyFields))
{
dependencyFields = [];
_dependencyFields.Add(fieldSymbol.Type, dependencyFields);
}
dependencyFields.Add(fieldSymbol);
}
}
private bool IsDependency(ISymbol symbol)
{
foreach (var attributeData in symbol.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, dependencyAttributeType))
return true;
}
return false;
}
public void End(SymbolAnalysisContext context)
{
lock (_dependencyFields)
{
foreach (var pair in _dependencyFields)
{
var fieldType = pair.Key;
var fields = pair.Value;
if (fields.Count <= 1)
continue;
// Sort so we can have deterministic order to skip reporting for a single field.
// Whichever sorts first doesn't get reported.
fields.Sort(static (a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
// Start at index 1 to skip first field.
var firstField = fields[0];
for (var i = 1; i < fields.Count; i++)
{
var field = fields[i];
context.ReportDiagnostic(
Diagnostic.Create(Rule, field.Locations[0], fieldType.ToDisplayString(), firstField.Name));
}
}
}
}
}
}

View File

@@ -0,0 +1,96 @@
using System;
using System.Numerics;
using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.Configuration;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.Benchmarks.Physics;
[Virtual]
[MediumRunJob]
public class PhysicsBoxStackBenchmark
{
private ISimulation _sim = default!;
[GlobalSetup]
public void Setup()
{
_sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = _sim.Resolve<IEntityManager>();
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
SetupTumbler(entManager, mapId);
for (var i = 0; i < 30; i++)
{
entManager.TickUpdate(0.016f, false);
}
}
[Benchmark]
public void BoxStack()
{
var entManager = _sim.Resolve<IEntityManager>();
for (var i = 0; i < 10000; i++)
{
entManager.TickUpdate(0.016f, false);
}
}
private void SetupTumbler(IEntityManager entManager, MapId mapId)
{
var physics = entManager.System<SharedPhysicsSystem>();
var fixtures = entManager.System<FixtureSystem>();
var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var ground = entManager.AddComponent<PhysicsComponent>(groundUid);
var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0));
fixtures.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 2, 2, true), body: ground);
var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10));
fixtures.CreateFixture(groundUid, "fix2", new Fixture(vertical, 2, 2, true), body: ground);
var xs = new[]
{
0.0f, -10.0f, -5.0f, 5.0f, 10.0f
};
var columnCount = 1;
var rowCount = 15;
PolygonShape shape;
for (var j = 0; j < columnCount; j++)
{
for (var i = 0; i < rowCount; i++)
{
var x = 0.0f;
var boxUid = entManager.SpawnEntity(null,
new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 1.1f * i), mapId));
var box = entManager.AddComponent<PhysicsComponent>(boxUid);
physics.SetBodyType(boxUid, BodyType.Dynamic, body: box);
shape = new PolygonShape();
shape.SetAsBox(0.5f, 0.5f);
physics.SetFixedRotation(boxUid, false, body: box);
fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true), body: box);
physics.WakeBody(boxUid, body: box);
physics.SetSleepingAllowed(boxUid, box, false);
}
}
physics.WakeBody(groundUid, body: ground);
}
}

View File

@@ -0,0 +1,92 @@
using System.Numerics;
using BenchmarkDotNet.Attributes;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.Benchmarks.Physics;
[Virtual]
public class PhysicsCircleStackBenchmark
{
private ISimulation _sim = default!;
[GlobalSetup]
public void Setup()
{
_sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = _sim.Resolve<IEntityManager>();
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
SetupTumbler(entManager, mapId);
for (var i = 0; i < 30; i++)
{
entManager.TickUpdate(0.016f, false);
}
}
[Benchmark]
public void CircleStack()
{
var entManager = _sim.Resolve<IEntityManager>();
for (var i = 0; i < 10000; i++)
{
entManager.TickUpdate(0.016f, false);
}
}
private void SetupTumbler(IEntityManager entManager, MapId mapId)
{
var physics = entManager.System<SharedPhysicsSystem>();
var fixtures = entManager.System<FixtureSystem>();
var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var ground = entManager.AddComponent<PhysicsComponent>(groundUid);
var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0));
fixtures.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 2, 2, true), body: ground);
var vertical = new EdgeShape(new Vector2(20, 0), new Vector2(20, 20));
fixtures.CreateFixture(groundUid, "fix2", new Fixture(vertical, 2, 2, true), body: ground);
var xs = new[]
{
0.0f, -10.0f, -5.0f, 5.0f, 10.0f
};
var columnCount = 1;
var rowCount = 15;
PhysShapeCircle shape;
for (var j = 0; j < columnCount; j++)
{
for (var i = 0; i < rowCount; i++)
{
var x = 0.0f;
var boxUid = entManager.SpawnEntity(null,
new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 2.1f * i), mapId));
var box = entManager.AddComponent<PhysicsComponent>(boxUid);
physics.SetBodyType(boxUid, BodyType.Dynamic, body: box);
shape = new PhysShapeCircle(0.5f);
physics.SetFixedRotation(boxUid, false, body: box);
// TODO: Need to detect shape and work out if we need to use fixedrotation
fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true, 5f));
physics.WakeBody(boxUid, body: box);
physics.SetSleepingAllowed(boxUid, box, false);
}
}
physics.WakeBody(groundUid, body: ground);
}
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Numerics;
using BenchmarkDotNet.Attributes;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.Benchmarks.Physics;
[Virtual]
public class PhysicsPyramidBenchmark
{
private ISimulation _sim = default!;
[GlobalSetup]
public void Setup()
{
_sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = _sim.Resolve<IEntityManager>();
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
SetupTumbler(entManager, mapId);
for (var i = 0; i < 300; i++)
{
entManager.TickUpdate(0.016f, false);
}
}
[Benchmark]
public void Pyramid()
{
var entManager = _sim.Resolve<IEntityManager>();
for (var i = 0; i < 5000; i++)
{
entManager.TickUpdate(0.016f, false);
}
}
private void SetupTumbler(IEntityManager entManager, MapId mapId)
{
const byte count = 20;
// Setup ground
var physics = entManager.System<SharedPhysicsSystem>();
var fixtures = entManager.System<FixtureSystem>();
var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var ground = entManager.AddComponent<PhysicsComponent>(groundUid);
var horizontal = new EdgeShape(new Vector2(40, 0), new Vector2(-40, 0));
fixtures.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 2, 2, true), body: ground);
physics.WakeBody(groundUid, body: ground);
// Setup boxes
float a = 0.5f;
PolygonShape shape = new();
shape.SetAsBox(a, a);
var x = new Vector2(-7.0f, 0.75f);
Vector2 y;
Vector2 deltaX = new Vector2(0.5625f, 1.25f);
Vector2 deltaY = new Vector2(1.125f, 0.0f);
for (var i = 0; i < count; ++i)
{
y = x;
for (var j = i; j < count; ++j)
{
var boxUid = entManager.SpawnEntity(null, new MapCoordinates(y, mapId));
var box = entManager.AddComponent<PhysicsComponent>(boxUid);
physics.SetBodyType(boxUid, BodyType.Dynamic, body: box);
fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true, 5f), body: box);
y += deltaY;
physics.WakeBody(boxUid, body: box);
physics.SetSleepingAllowed(boxUid, box, false);
}
x += deltaX;
}
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Numerics;
using BenchmarkDotNet.Attributes;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.Benchmarks.Physics;
[Virtual]
public class PhysicsTumblerBenchmark
{
private ISimulation _sim = default!;
[GlobalSetup]
public void Setup()
{
_sim = RobustServerSimulation.NewSimulation().InitializeInstance();
var entManager = _sim.Resolve<IEntityManager>();
var physics = entManager.System<SharedPhysicsSystem>();
var fixtures = entManager.System<FixtureSystem>();
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
SetupTumbler(entManager, mapId);
for (var i = 0; i < 800; i++)
{
entManager.TickUpdate(0.016f, false);
var boxUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 10f, mapId));
var box = entManager.AddComponent<PhysicsComponent>(boxUid);
physics.SetBodyType(boxUid, BodyType.Dynamic, body: box);
physics.SetFixedRotation(boxUid, false, body: box);
var shape = new PolygonShape();
shape.SetAsBox(0.125f, 0.125f);
fixtures.CreateFixture(boxUid, "fix1", new Fixture(shape, 2, 2, true, 0.0625f), body: box);
physics.WakeBody(boxUid, body: box);
physics.SetSleepingAllowed(boxUid, box, false);
}
}
[Benchmark]
public void Tumbler()
{
var entManager = _sim.Resolve<IEntityManager>();
for (var i = 0; i < 5000; i++)
{
entManager.TickUpdate(0.016f, false);
}
}
private void SetupTumbler(IEntityManager entManager, MapId mapId)
{
var physics = entManager.System<SharedPhysicsSystem>();
var fixtures = entManager.System<FixtureSystem>();
var joints = entManager.System<SharedJointSystem>();
var groundUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 0f, mapId));
var ground = entManager.AddComponent<PhysicsComponent>(groundUid);
// Due to lookup changes fixtureless bodies are invalid, so
var cShape = new PhysShapeCircle(1f);
fixtures.CreateFixture(groundUid, "fix1", new Fixture(cShape, 0, 0, false));
var bodyUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 10f, mapId));
var body = entManager.AddComponent<PhysicsComponent>(bodyUid);
physics.SetBodyType(bodyUid, BodyType.Dynamic, body: body);
physics.SetSleepingAllowed(bodyUid, body, false);
physics.SetFixedRotation(bodyUid, false, body: body);
// TODO: Box2D just deref, bleh shape structs someday
var shape1 = new PolygonShape();
shape1.SetAsBox(0.5f, 10.0f, new Vector2(10.0f, 0.0f), 0.0f);
fixtures.CreateFixture(bodyUid, "fix1", new Fixture(shape1, 2, 0, true, 20f));
var shape2 = new PolygonShape();
shape2.SetAsBox(0.5f, 10.0f, new Vector2(-10.0f, 0.0f), 0f);
fixtures.CreateFixture(bodyUid, "fix2", new Fixture(shape2, 2, 0, true, 20f));
var shape3 = new PolygonShape();
shape3.SetAsBox(10.0f, 0.5f, new Vector2(0.0f, 10.0f), 0f);
fixtures.CreateFixture(bodyUid, "fix3", new Fixture(shape3, 2, 0, true, 20f));
var shape4 = new PolygonShape();
shape4.SetAsBox(10.0f, 0.5f, new Vector2(0.0f, -10.0f), 0f);
fixtures.CreateFixture(bodyUid, "fix4", new Fixture(shape4, 2, 0, true, 20f));
physics.WakeBody(groundUid, body: ground);
physics.WakeBody(bodyUid, body: body);
var revolute = joints.CreateRevoluteJoint(groundUid, bodyUid);
revolute.LocalAnchorA = new Vector2(0f, 10f);
revolute.LocalAnchorB = new Vector2(0f, 0f);
revolute.ReferenceAngle = 0f;
revolute.MotorSpeed = 0.05f * MathF.PI;
revolute.MaxMotorTorque = 100000000f;
revolute.EnableMotor = true;
}
}

View File

@@ -1,8 +1,8 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Robust.Xaml;
namespace Robust.Build.Tasks
{
@@ -37,10 +37,12 @@ namespace Robust.Build.Tasks
var msg = $"CompileRobustXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}";
BuildEngine.LogMessage(msg, MessageImportance.High);
var res = XamlCompiler.Compile(BuildEngine, input,
var res = XamlAotCompiler.Compile(
BuildEngine, input,
File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(),
ProjectDirectory, OutputPath,
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null);
OutputPath,
(SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null
);
if (!res.success)
return false;
if (!res.writtentofile)
@@ -65,22 +67,24 @@ namespace Robust.Build.Tasks
return true;
}
// PYREX NOTE: This project was comically null-unsafe before I touched it. I'm just marking what it did accurately
[Required]
public string ReferencesFilePath { get; set; }
public string ReferencesFilePath { get; set; } = null!;
[Required]
public string ProjectDirectory { get; set; }
public string ProjectDirectory { get; set; } = null!;
[Required]
public string AssemblyFile { get; set; }
public string AssemblyFile { get; set; } = null!;
[Required]
public string OriginalCopyPath { get; set; }
public string? OriginalCopyPath { get; set; } = null;
public string OutputPath { get; set; }
public string UpdateBuildIndicator { get; set; }
public string? OutputPath { get; set; }
public string UpdateBuildIndicator { get; set; } = null!;
public string AssemblyOriginatorKeyFile { get; set; }
public string AssemblyOriginatorKeyFile { get; set; } = null!;
public bool SignAssembly { get; set; }
public bool DelaySign { get; set; }
@@ -95,7 +99,7 @@ namespace Robust.Build.Tasks
return rv;
}
public IBuildEngine BuildEngine { get; set; }
public ITaskHost HostObject { get; set; }
public IBuildEngine BuildEngine { get; set; } = null!;
public ITaskHost HostObject { get; set; } = null!;
}
}

View File

@@ -1,37 +0,0 @@
using System.Linq;
using Pidgin;
using static Pidgin.Parser;
namespace Robust.Build.Tasks
{
public static class MathParsing
{
public static Parser<char, float> Single { get; } = Real.Select(c => (float) c);
public static Parser<char, float> Single1 { get; }
= Single.Between(SkipWhitespaces);
public static Parser<char, (float, float)> Single2 { get; }
= Single.Before(SkipWhitespaces).Repeat(2).Select(e =>
{
var arr = e.ToArray();
return (arr[0], arr[1]);
});
public static Parser<char, (float, float, float, float)> Single4 { get; }
= Single.Before(SkipWhitespaces).Repeat(4).Select(e =>
{
var arr = e.ToArray();
return (arr[0], arr[1], arr[2], arr[3]);
});
public static Parser<char, float[]> Thickness { get; }
= SkipWhitespaces.Then(
OneOf(
Try(Single4.Select(c => new[] {c.Item1, c.Item2, c.Item3, c.Item4})),
Try(Single2.Select(c => new[] {c.Item1, c.Item2})),
Try(Single1.Select(c => new[] {c}))
));
}
}

View File

@@ -55,9 +55,11 @@ namespace Robust.Build.Tasks
public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties,
IDictionary targetOutputs) => throw new NotSupportedException();
public bool ContinueOnError { get; }
public int LineNumberOfTaskNode { get; }
public int ColumnNumberOfTaskNode { get; }
public string ProjectFileOfTaskNode { get; }
// PYREX NOTE: This project was extremely null-unsafe before I touched it. I'm just marking what it did already
// Here's the broken interface of IBuildEngine that we started with
public bool ContinueOnError => default;
public int LineNumberOfTaskNode => default;
public int ColumnNumberOfTaskNode => default;
public string ProjectFileOfTaskNode => null!;
}
}

View File

@@ -1,17 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\MSBuild\Robust.Engine.props" />
<!--
PJB3005 (2024-08-24)
So the reason that Robust.Client.Injectors is NS2.0 is that Visual Studio
still ships a .NET FX based MSBuild for some godforsaken reason. This means
that when having Robust.Client.Injectors loaded directly by the main MSBuild
process... that would break.
Except we don't do that anyways right now due to file locking issues, so maybe
it's fine to give up on that. Whatever.
-->
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<TargetFramework>netstandard2.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="17.8.3" />
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
<PackageReference Include="Pidgin" Version="2.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\XamlX\src\XamlX.IL.Cecil\XamlX.IL.Cecil.csproj" />
<ProjectReference Include="..\Robust.Xaml\Robust.Xaml.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,389 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using Pidgin;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Parsers;
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Robust.Build.Tasks
{
/// <summary>
/// Based on https://github.com/AvaloniaUI/Avalonia/blob/c85fa2b9977d251a31886c2534613b4730fbaeaf/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
/// Adjusted for our UI-Framework
/// </summary>
public partial class XamlCompiler
{
public static (bool success, bool writtentofile) Compile(IBuildEngine engine, string input, string[] references,
string projectDirectory, string output, string strongNameKey)
{
var typeSystem = new CecilTypeSystem(references
.Where(r => !r.ToLowerInvariant().EndsWith("robust.build.tasks.dll"))
.Concat(new[] { input }), input);
var asm = typeSystem.TargetAssemblyDefinition;
if (asm.MainModule.GetType("CompiledRobustXaml", "XamlIlContext") != null)
{
// If this type exists, the assembly has already been processed by us.
// Do not run again, it would corrupt the file.
// This *shouldn't* be possible due to Inputs/Outputs dependencies in the build system,
// but better safe than sorry eh?
engine.LogWarningEvent(new BuildWarningEventArgs("XAMLIL", "", "", 0, 0, 0, 0, "Ran twice on same assembly file; ignoring.", "", ""));
return (true, false);
}
var compileRes = CompileCore(engine, typeSystem);
if (compileRes == null)
return (true, false);
if (compileRes == false)
return (false, false);
var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols };
if (!string.IsNullOrWhiteSpace(strongNameKey))
writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey);
asm.Write(output, writerParameters);
return (true, true);
}
static bool? CompileCore(IBuildEngine engine, CecilTypeSystem typeSystem)
{
var asm = typeSystem.TargetAssemblyDefinition;
var embrsc = new EmbeddedResources(asm);
if (embrsc.Resources.Count(CheckXamlName) == 0)
// Nothing to do
return null;
var xamlLanguage = new XamlLanguageTypeMappings(typeSystem)
{
XmlnsAttributes =
{
typeSystem.GetType("Avalonia.Metadata.XmlnsDefinitionAttribute"),
},
ContentAttributes =
{
typeSystem.GetType("Avalonia.Metadata.ContentAttribute")
},
UsableDuringInitializationAttributes =
{
typeSystem.GetType("Robust.Client.UserInterface.XAML.UsableDuringInitializationAttribute")
},
DeferredContentPropertyAttributes =
{
typeSystem.GetType("Robust.Client.UserInterface.XAML.DeferredContentAttribute")
},
RootObjectProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestRootObjectProvider"),
UriContextProvider = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestUriContext"),
ProvideValueTarget = typeSystem.GetType("Robust.Client.UserInterface.XAML.ITestProvideValueTarget"),
};
var emitConfig = new XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult>
{
ContextTypeBuilderCallback = (b,c) => EmitNameScopeField(xamlLanguage, typeSystem, b, c)
};
var transformerconfig = new TransformerConfiguration(
typeSystem,
typeSystem.TargetAssembly,
xamlLanguage,
XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage), CustomValueConverter);
var contextDef = new TypeDefinition("CompiledRobustXaml", "XamlIlContext",
TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
asm.MainModule.Types.Add(contextDef);
var contextClass = XamlILContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem,
xamlLanguage, emitConfig);
var compiler =
new RobustXamlILCompiler(transformerconfig, emitConfig, true);
bool CompileGroup(IResourceGroup group)
{
var typeDef = new TypeDefinition("CompiledRobustXaml", "!" + group.Name, TypeAttributes.Class,
asm.MainModule.TypeSystem.Object);
//typeDef.CustomAttributes.Add(new CustomAttribute(ed));
asm.MainModule.Types.Add(typeDef);
var builder = typeSystem.CreateTypeBuilder(typeDef);
foreach (var res in group.Resources.Where(CheckXamlName))
{
try
{
engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", MessageImportance.Low);
var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
var parsed = XDocumentXamlParser.Parse(xaml);
var initialRoot = (XamlAstObjectNode) parsed.Root;
var classDirective = initialRoot.Children.OfType<XamlAstXmlDirective>()
.FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Class");
string classname;
if (classDirective != null && classDirective.Values[0] is XamlAstTextNode tn)
{
classname = tn.Text;
}
else
{
classname = res.Name.Replace(".xaml","");
}
var classType = typeSystem.TargetAssembly.FindType(classname);
if (classType == null)
throw new Exception($"Unable to find type '{classname}'");
compiler.Transform(parsed);
var populateName = $"Populate:{res.Name}";
var buildName = $"Build:{res.Name}";
var classTypeDefinition = typeSystem.GetTypeReference(classType).Resolve();
var populateBuilder = typeSystem.CreateTypeBuilder(classTypeDefinition);
compiler.Compile(parsed, contextClass,
compiler.DefinePopulateMethod(populateBuilder, parsed, populateName,
classTypeDefinition == null),
compiler.DefineBuildMethod(builder, parsed, buildName, true),
null,
(closureName, closureBaseType) =>
populateBuilder.DefineSubType(closureBaseType, closureName, false),
res.Uri, res
);
//add compiled populate method
var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve().Methods
.First(m => m.Name == populateName);
const string TrampolineName = "!XamlIlPopulateTrampoline";
var trampoline = new MethodDefinition(TrampolineName,
MethodAttributes.Static | MethodAttributes.Private, asm.MainModule.TypeSystem.Void);
trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition));
classTypeDefinition.Methods.Add(trampoline);
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldnull));
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Call, compiledPopulateMethod));
trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
var foundXamlLoader = false;
// Find RobustXamlLoader.Load(this) and replace it with !XamlIlPopulateTrampoline(this)
foreach (var method in classTypeDefinition.Methods
.Where(m => !m.Attributes.HasFlag(MethodAttributes.Static)))
{
var i = method.Body.Instructions;
for (var c = 1; c < i.Count; c++)
{
if (i[c].OpCode == OpCodes.Call)
{
var op = i[c].Operand as MethodReference;
if (op != null
&& op.Name == TrampolineName)
{
foundXamlLoader = true;
break;
}
if (op != null
&& op.Name == "Load"
&& op.Parameters.Count == 1
&& op.Parameters[0].ParameterType.FullName == "System.Object"
&& op.DeclaringType.FullName == "Robust.Client.UserInterface.XAML.RobustXamlLoader")
{
if (MatchThisCall(i, c - 1))
{
i[c].Operand = trampoline;
foundXamlLoader = true;
}
}
}
}
}
if (!foundXamlLoader)
{
var ctors = classTypeDefinition.GetConstructors()
.Where(c => !c.IsStatic).ToList();
// We can inject xaml loader into default constructor
if (ctors.Count == 1 && ctors[0].Body.Instructions.Count(o=>o.OpCode != OpCodes.Nop) == 3)
{
var i = ctors[0].Body.Instructions;
var retIdx = i.IndexOf(i.Last(x => x.OpCode == OpCodes.Ret));
i.Insert(retIdx, Instruction.Create(OpCodes.Call, trampoline));
i.Insert(retIdx, Instruction.Create(OpCodes.Ldarg_0));
}
else
{
throw new InvalidProgramException(
$"No call to RobustXamlLoader.Load(this) call found anywhere in the type {classType.FullName} and type seems to have custom constructors.");
}
}
}
catch (Exception e)
{
engine.LogErrorEvent(new BuildErrorEventArgs("XAMLIL", "", res.FilePath, 0, 0, 0, 0,
$"{res.FilePath}: {e.Message}", "", "CompileRobustXaml"));
}
res.Remove();
}
return true;
}
if (embrsc.Resources.Count(CheckXamlName) != 0)
{
if (!CompileGroup(embrsc))
return false;
}
return true;
}
private static bool CustomValueConverter(
AstTransformationContext context,
IXamlAstValueNode node,
IXamlType type,
out IXamlAstValueNode result)
{
if (!(node is XamlAstTextNode textNode))
{
result = null;
return false;
}
var text = textNode.Text;
var types = context.GetRobustTypes();
if (type.Equals(types.Vector2))
{
var foo = MathParsing.Single2.Parse(text);
if (!foo.Success)
throw new XamlLoadException($"Unable to parse \"{text}\" as a Vector2", node);
var (x, y) = foo.Value;
result = new RXamlSingleVecLikeConstAstNode(
node,
types.Vector2, types.Vector2ConstructorFull,
types.Single, new[] {x, y});
return true;
}
if (type.Equals(types.Thickness))
{
var foo = MathParsing.Thickness.Parse(text);
if (!foo.Success)
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
var val = foo.Value;
float[] full;
if (val.Length == 1)
{
var u = val[0];
full = new[] {u, u, u, u};
}
else if (val.Length == 2)
{
var h = val[0];
var v = val[1];
full = new[] {h, v, h, v};
}
else // 4
{
full = val;
}
result = new RXamlSingleVecLikeConstAstNode(
node,
types.Thickness, types.ThicknessConstructorFull,
types.Single, full);
return true;
}
if (type.Equals(types.Thickness))
{
var foo = MathParsing.Thickness.Parse(text);
if (!foo.Success)
throw new XamlLoadException($"Unable to parse \"{text}\" as a Thickness", node);
var val = foo.Value;
float[] full;
if (val.Length == 1)
{
var u = val[0];
full = new[] {u, u, u, u};
}
else if (val.Length == 2)
{
var h = val[0];
var v = val[1];
full = new[] {h, v, h, v};
}
else // 4
{
full = val;
}
result = new RXamlSingleVecLikeConstAstNode(
node,
types.Thickness, types.ThicknessConstructorFull,
types.Single, full);
return true;
}
if (type.Equals(types.Color))
{
// TODO: Interpret these colors at XAML compile time instead of at runtime.
result = new RXamlColorAstNode(node, types, text);
return true;
}
result = null;
return false;
}
public const string ContextNameScopeFieldName = "RobustNameScope";
private static void EmitNameScopeField(XamlLanguageTypeMappings xamlLanguage, CecilTypeSystem typeSystem, IXamlTypeBuilder<IXamlILEmitter> typeBuilder, IXamlILEmitter constructor)
{
var nameScopeType = typeSystem.FindType("Robust.Client.UserInterface.XAML.NameScope");
var field = typeBuilder.DefineField(nameScopeType,
ContextNameScopeFieldName, true, false);
constructor
.Ldarg_0()
.Newobj(nameScopeType.GetConstructor())
.Stfld(field);
}
}
interface IResource : IFileSource
{
string Uri { get; }
string Name { get; }
void Remove();
}
interface IResourceGroup
{
string Name { get; }
IEnumerable<IResource> Resources { get; }
}
}

View File

@@ -6,7 +6,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
public static class Program
internal static class Program
{
// This was supposed to be the main entry for the subprocess program... It doesn't work.
public static int Main(string[] args)

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Reflection;
using System.Text;
using Robust.Client.Console;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
@@ -24,6 +25,7 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameControllerInternal _gameController = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -61,7 +63,10 @@ namespace Robust.Client.WebView.Cef
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
var settings = new CefSettings()
{

View File

@@ -453,6 +453,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio....
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayLocal(
SoundSpecifier? sound,
EntityUid source,
EntityUid? soundInitiator,
AudioParams? audioParams = null
)
{
return PlayPredicted(sound, source, soundInitiator, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayPredicted(SoundSpecifier? sound, EntityCoordinates coordinates, EntityUid? user, AudioParams? audioParams = null)
{
if (Timing.IsFirstTimePredicted && sound != null)
@@ -658,7 +669,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
// TODO clamp the offset inside of SetPlaybackPosition() itself.
var offset = audioP.PlayOffsetSeconds;
offset = Math.Clamp(offset, 0f, (float) stream.Length.TotalSeconds - 0.01f);
var maxOffset = Math.Max((float) stream.Length.TotalSeconds - 0.01f, 0f);
offset = Math.Clamp(offset, 0f, maxOffset);
source.PlaybackPosition = offset;
// For server we will rely on the adjusted one but locally we will have to adjust it ourselves.

View File

@@ -33,12 +33,6 @@ public interface IMidiRenderer : IDisposable
/// </summary>
bool LoopMidi { get; set; }
/// <summary>
/// This increases all note on velocities to 127.
/// </summary>
[Obsolete($"Use {nameof(VelocityOverride)} instead, you can set it to 127 to achieve the same effect.")]
bool VolumeBoost { get; set; }
/// <summary>
/// The midi program (instrument) the renderer is using.
/// </summary>

View File

@@ -205,14 +205,6 @@ internal sealed class MidiRenderer : IMidiRenderer
}
}
[ViewVariables(VVAccess.ReadWrite)]
[Obsolete($"Use {nameof(VelocityOverride)} instead, you can set it to 127 to achieve the same effect.")]
public bool VolumeBoost
{
get => VelocityOverride == 127;
set => VelocityOverride = value ? 127 : null;
}
[ViewVariables(VVAccess.ReadWrite)]
public EntityUid? TrackingEntity { get; set; } = null;

View File

@@ -8,6 +8,7 @@ using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Client.HWId;
using Robust.Client.Input;
using Robust.Client.Map;
using Robust.Client.Placement;
@@ -26,6 +27,7 @@ using Robust.Client.Upload;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.RichText;
using Robust.Client.UserInterface.Themes;
using Robust.Client.UserInterface.XAML.Proxy;
using Robust.Client.Utility;
using Robust.Client.ViewVariables;
using Robust.Shared;
@@ -146,7 +148,18 @@ namespace Robust.Client
deps.Register<IConfigurationManagerInternal, ClientNetConfigurationManager>();
deps.Register<IClientNetConfigurationManager, ClientNetConfigurationManager>();
deps.Register<INetConfigurationManagerInternal, ClientNetConfigurationManager>();
#if TOOLS
deps.Register<IXamlProxyManager, XamlProxyManager>();
deps.Register<IXamlHotReloadManager, XamlHotReloadManager>();
#else
deps.Register<IXamlProxyManager, XamlProxyManagerStub>();
deps.Register<IXamlHotReloadManager, XamlHotReloadManagerStub>();
#endif
deps.Register<IXamlProxyHelper, XamlProxyHelper>();
deps.Register<MarkupTagManager>();
deps.Register<IHWId, BasicHWId>();
}
}
}

View File

@@ -20,8 +20,9 @@ namespace Robust.Client.Console.Commands
{
var wantName = args.Length > 0 ? args[0] : null;
var basePath = Path.GetDirectoryName(UserDataDir.GetUserDataDir(_gameController))!;
var dbPath = Path.Combine(basePath, "launcher", "settings.db");
var basePath = UserDataDir.GetRootUserDataDir(_gameController);
var launcherDirName = Environment.GetEnvironmentVariable("SS14_LAUNCHER_APPDATA_NAME") ?? "launcher";
var dbPath = Path.Combine(basePath, launcherDirName, "settings.db");
#if USE_SYSTEM_SQLITE
SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlite3());

View File

@@ -14,15 +14,6 @@ namespace Robust.Client.Credits
/// </summary>
public static class CreditsManager
{
/// <summary>
/// Gets a list of open source software used in the engine and their license.
/// </summary>
[Obsolete("Use overload that takes in an explicit resource manager instead.")]
public static IEnumerable<LicenseEntry> GetLicenses()
{
return GetLicenses(IoCManager.Resolve<IResourceManager>());
}
/// <summary>
/// Gets a list of open source software used in the engine and their license.
/// </summary>

View File

@@ -544,7 +544,7 @@ namespace Robust.Client.Debugging
switch (joint)
{
case DistanceJoint:
worldHandle.DrawLine(xf1, xf2, JointColor);
worldHandle.DrawLine(p1, p2, JointColor);
break;
case PrismaticJoint prisma:
var pA = Transform.Mul(xfa, joint.LocalAnchorA);

View File

@@ -19,6 +19,7 @@ using Robust.Client.State;
using Robust.Client.Upload;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.RichText;
using Robust.Client.UserInterface.XAML.Proxy;
using Robust.Client.Utility;
using Robust.Client.ViewVariables;
using Robust.Client.WebViewHook;
@@ -53,6 +54,8 @@ namespace Robust.Client
[Dependency] private readonly IResourceCacheInternal _resourceCache = default!;
[Dependency] private readonly IResourceManagerInternal _resManager = default!;
[Dependency] private readonly IRobustSerializer _serializer = default!;
[Dependency] private readonly IXamlProxyManager _xamlProxyManager = default!;
[Dependency] private readonly IXamlHotReloadManager _xamlHotReloadManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IClientNetManager _networkManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
@@ -109,14 +112,28 @@ namespace Robust.Client
_commandLineArgs = args;
}
public string GameTitle()
{
return Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox";
}
public string WindowIconSet()
{
return Options.WindowIconSet?.ToString() ?? _resourceManifest!.WindowIconSet ?? "";
}
public string SplashLogo()
{
return Options.SplashLogo?.ToString() ?? _resourceManifest!.SplashLogo ?? "";
}
internal bool StartupContinue(DisplayMode displayMode)
{
DebugTools.AssertNotNull(_resourceManifest);
_clyde.InitializePostWindowing();
_audio.InitializePostWindowing();
_clyde.SetWindowTitle(
Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox");
_clyde.SetWindowTitle(GameTitle());
_taskManager.Initialize();
_parallelMgr.Initialize();
@@ -171,6 +188,8 @@ namespace Robust.Client
_reflectionManager.Initialize();
_prototypeManager.Initialize();
_prototypeManager.LoadDefaultPrototypes();
_xamlProxyManager.Initialize();
_xamlHotReloadManager.Initialize();
_userInterfaceManager.Initialize();
_eyeManager.Initialize();
_entityManager.Initialize();
@@ -363,7 +382,7 @@ namespace Robust.Client
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
@@ -394,10 +413,8 @@ namespace Robust.Client
// Handle GameControllerOptions implicit CVar overrides.
_configurationManager.OverrideConVars(new[]
{
(CVars.DisplayWindowIconSet.Name,
options.WindowIconSet?.ToString() ?? _resourceManifest.WindowIconSet ?? ""),
(CVars.DisplaySplashLogo.Name,
options.SplashLogo?.ToString() ?? _resourceManifest.SplashLogo ?? "")
(CVars.DisplayWindowIconSet.Name, WindowIconSet()),
(CVars.DisplaySplashLogo.Name, SplashLogo())
});
}

View File

@@ -99,15 +99,6 @@ namespace Robust.Client.GameObjects
Play(new Entity<AnimationPlayerComponent>(uid, component), animation, key);
}
/// <summary>
/// Start playing an animation.
/// </summary>
[Obsolete("Use Play(EntityUid<AnimationPlayerComponent> ent, Animation animation, string key) instead")]
public void Play(AnimationPlayerComponent component, Animation animation, string key)
{
Play(new Entity<AnimationPlayerComponent>(component.Owner, component), animation, key);
}
public void Play(Entity<AnimationPlayerComponent> ent, Animation animation, string key)
{
AddComponent(ent);
@@ -152,6 +143,14 @@ namespace Robust.Client.GameObjects
}
#endif
foreach (var track in animation.AnimationTracks)
{
if (track is not AnimationTrackSpriteFlick)
continue;
track.AdvancePlayback(ent.Owner, 0, 0, 0f);
}
ent.Comp.PlayingAnimations.Add(key, playback);
}

View File

@@ -17,7 +17,6 @@ namespace Robust.Client.GameObjects
{
public sealed class ContainerSystem : SharedContainerSystem
{
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly IRobustSerializer _serializer = default!;
[Dependency] private readonly IDynamicTypeFactoryInternal _dynFactory = default!;
[Dependency] private readonly PointLightSystem _lightSys = default!;

View File

@@ -82,6 +82,7 @@ namespace Robust.Client.GameObjects
var viewport = args.WorldBounds;
var worldHandle = args.WorldHandle;
var fixturesQuery = _entityManager.GetEntityQuery<FixturesComponent>();
_grids.Clear();
_mapManager.FindGridsIntersecting(currentMap, viewport, ref _grids);
foreach (var grid in _grids)
@@ -89,13 +90,15 @@ namespace Robust.Client.GameObjects
var worldMatrix = _transformSystem.GetWorldMatrix(grid);
worldHandle.SetTransform(worldMatrix);
var transform = new Transform(Vector2.Zero, Angle.Zero);
var fixtures = fixturesQuery.Comp(grid.Owner);
var chunkEnumerator = _mapSystem.GetMapChunks(grid.Owner, grid.Comp, viewport);
while (chunkEnumerator.MoveNext(out var chunk))
{
foreach (var fixture in chunk.Fixtures.Values)
foreach (var id in chunk.Fixtures)
{
var fixture = fixtures.Fixtures[id];
var poly = (PolygonShape) fixture.Shape;
var verts = new Vector2[poly.VertexCount];

View File

@@ -20,7 +20,6 @@ namespace Robust.Client.GameObjects
/// </summary>
public sealed class InputSystem : SharedInputSystem, IPostInjectInit
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientGameStateManager _stateManager = default!;

View File

@@ -0,0 +1,43 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Robust.Client.GameObjects;
public sealed partial class SpriteSystem
{
/// <summary>
/// Gets an entity's sprite position in world terms.
/// </summary>
public Vector2 GetSpriteWorldPosition(Entity<SpriteComponent?, TransformComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp2))
return Vector2.Zero;
var (worldPos, worldRot) = _xforms.GetWorldPositionRotation(entity.Owner);
if (!Resolve(entity, ref entity.Comp1, false))
{
return worldPos;
}
if (entity.Comp1.NoRotation)
{
return worldPos + entity.Comp1.Offset;
}
return worldPos + worldRot.RotateVec(entity.Comp1.Rotation.RotateVec(entity.Comp1.Offset));
}
/// <summary>
/// Gets an entity's sprite position in screen coordinates.
/// </summary>
public ScreenCoordinates GetSpriteScreenCoordinates(Entity<SpriteComponent?, TransformComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp2))
return ScreenCoordinates.Invalid;
var spriteCoords = GetSpriteWorldPosition(entity);
return _eye.MapToScreen(new MapCoordinates(spriteCoords, entity.Comp2.MapID));
}
}

View File

@@ -30,10 +30,12 @@ namespace Robust.Client.GameObjects
public sealed partial class SpriteSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IEyeManager _eye = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly SharedTransformSystem _xforms = default!;
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();

View File

@@ -0,0 +1,5 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects;
public sealed class ViewSubscriberSystem : SharedViewSubscriberSystem;

View File

@@ -960,7 +960,7 @@ namespace Robust.Client.GameStates
// Initialize and start the newly created entities.
if (_toCreate.Count > 0)
InitializeAndStart(_toCreate);
InitializeAndStart(_toCreate, metas, xforms);
_prof.WriteValue("State Size", ProfData.Int32(curSpan.Length));
_prof.WriteValue("Entered PVS", ProfData.Int32(enteringPvs));
@@ -1188,7 +1188,10 @@ namespace Robust.Client.GameStates
}
}
private void InitializeAndStart(Dictionary<NetEntity, EntityState> toCreate)
private void InitializeAndStart(
Dictionary<NetEntity, EntityState> toCreate,
EntityQuery<MetaDataComponent> metas,
EntityQuery<TransformComponent> xforms)
{
_toStart.Clear();
@@ -1197,22 +1200,8 @@ namespace Robust.Client.GameStates
EntityUid entity = default;
foreach (var netEntity in toCreate.Keys)
{
try
{
(entity, var meta) = _entityManager.GetEntityData(netEntity);
_entities.InitializeEntity(entity, meta);
_toStart.Add((entity, netEntity));
}
catch (Exception e)
{
_sawmill.Error($"Server entity threw in Init: nent={netEntity}, ent={_entities.ToPrettyString(entity)}");
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
_toCreate.Remove(netEntity);
_brokenEnts.Add(entity);
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
(entity, var meta) = _entityManager.GetEntityData(netEntity);
InitializeRecursive(entity, meta, metas, xforms);
}
}
@@ -1244,6 +1233,44 @@ namespace Robust.Client.GameStates
_brokenEnts.Clear();
}
private void InitializeRecursive(
EntityUid entity,
MetaDataComponent meta,
EntityQuery<MetaDataComponent> metas,
EntityQuery<TransformComponent> xforms)
{
var xform = xforms.GetComponent(entity);
if (xform.ParentUid is {Valid: true} parent)
{
var parentMeta = metas.GetComponent(parent);
if (parentMeta.EntityLifeStage < EntityLifeStage.Initialized)
InitializeRecursive(parent, parentMeta, metas, xforms);
}
if (meta.EntityLifeStage >= EntityLifeStage.Initialized)
{
// Was probably already initialized because one of its children appeared earlier in the list.
DebugTools.AssertEqual(_toStart.Count(x => x.Item1 == entity), 1);
return;
}
try
{
_entities.InitializeEntity(entity, meta);
_toStart.Add((entity, meta.NetEntity));
}
catch (Exception e)
{
_sawmill.Error($"Server entity threw in Init: nent={meta.NetEntity}, ent={_entities.ToPrettyString(entity)}");
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
_toCreate.Remove(meta.NetEntity);
_brokenEnts.Add(entity);
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
}
private void HandleEntityState(EntityUid uid, NetEntity netEntity, MetaDataComponent meta, IEventBus bus, EntityState? curState,
EntityState? nextState, GameTick lastApplied, GameTick toTick, bool enteringPvs)
{

View File

@@ -34,9 +34,6 @@ namespace Robust.Client.GameStates
/// </summary>
int GetApplicableStateCount();
[Obsolete("use GetApplicableStateCount()")]
int CurrentBufferSize => GetApplicableStateCount();
/// <summary>
/// Total number of game states currently in the state buffer.
/// </summary>

View File

@@ -46,7 +46,6 @@ namespace Robust.Client.GameStates
// sum of all data point sizes in bytes
private int _totalHistoryPayload;
private int _totalUncompressed;
public EntityUid WatchEntId { get; set; }

View File

@@ -15,7 +15,6 @@ namespace Robust.Client.GameStates
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly EntityLookupSystem _lookup;

View File

@@ -109,6 +109,9 @@ namespace Robust.Client.Graphics.Clyde
private void SendWindowResized(WindowReg reg, Vector2i oldSize)
{
if (!reg.IsVisible) // Only send this for open windows
return;
var loaded = RtToLoaded(reg.RenderTarget);
loaded.Size = reg.FramebufferSize;

View File

@@ -343,6 +343,8 @@ namespace Robust.Client.Graphics.Clyde
if (isMain)
_mainWindow = reg;
reg.IsVisible = parameters.Visible;
_windows.Add(reg);
_windowHandles.Add(reg.Handle);
@@ -444,6 +446,12 @@ namespace Robust.Client.Graphics.Clyde
_windowing!.CursorSet(_mainWindow!, cursor);
}
private void SetWindowSize(WindowReg reg, Vector2i size)
{
DebugTools.AssertNotNull(_windowing);
_windowing!.WindowSetSize(reg, size);
}
private void SetWindowVisible(WindowReg reg, bool visible)
{
@@ -533,7 +541,11 @@ namespace Robust.Client.Graphics.Clyde
_clyde.DoDestroyWindow(Reg);
}
public Vector2i Size => Reg.FramebufferSize;
public Vector2i Size
{
get => Reg.FramebufferSize;
set => _clyde.SetWindowSize(Reg, value);
}
public IRenderTarget RenderTarget => Reg.RenderTarget;

View File

@@ -32,7 +32,6 @@ namespace Robust.Client.Graphics.Clyde
internal sealed partial class Clyde : IClydeInternal, IPostInjectInit, IEntityEventSubscriber
{
[Dependency] private readonly IClydeTileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly ILightManager _lightManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;

View File

@@ -517,7 +517,7 @@ namespace Robust.Client.Graphics.Clyde
RenderTarget = renderTarget;
}
public Vector2i Size { get; } = default;
public Vector2i Size { get; set; } = default;
public bool IsDisposed { get; private set; }
public WindowId Id { get; set; }
public IRenderTarget RenderTarget { get; }

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
@@ -176,6 +176,7 @@ namespace Robust.Client.Graphics.Clyde
window.BlitDoneEvent!.Reset();
window.BlitStartEvent!.Set();
window.BlitDoneEvent.Wait();
window.UnlockBeforeSwap = Clyde._cfg.GetCVar(CVars.DisplayThreadUnlockBeforeSwap);
}
}
else
@@ -212,8 +213,15 @@ namespace Robust.Client.Graphics.Clyde
GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4);
Clyde.CheckGlError();
window.BlitDoneEvent?.Set();
if (window.UnlockBeforeSwap)
{
window.BlitDoneEvent?.Set();
}
Clyde._windowing!.WindowSwapBuffers(window.Reg);
if (!window.UnlockBeforeSwap)
{
window.BlitDoneEvent?.Set();
}
}
private unsafe void BlitThreadInit(WindowData reg)
@@ -336,6 +344,7 @@ namespace Robust.Client.Graphics.Clyde
public Thread? BlitThread;
public ManualResetEventSlim? BlitStartEvent;
public ManualResetEventSlim? BlitDoneEvent;
public bool UnlockBeforeSwap;
}
}
}

View File

@@ -85,6 +85,10 @@ namespace Robust.Client.Graphics.Clyde
WinThreadWinSetMonitor(cmd);
break;
case CmdWinSetSize cmd:
WinThreadWinSetSize(cmd);
break;
case CmdWinSetVisible cmd:
WinThreadWinSetVisible(cmd);
break;
@@ -234,6 +238,11 @@ namespace Robust.Client.Graphics.Clyde
nint Window
) : CmdBase;
private sealed record CmdWinSetSize(
nint Window,
int W, int H
) : CmdBase;
private sealed record CmdWinSetVisible(
nint Window,
bool Visible

View File

@@ -84,6 +84,13 @@ namespace Robust.Client.Graphics.Clyde
);
}
public void WindowSetSize(WindowReg window, Vector2i size)
{
var reg = (GlfwWindowReg) window;
SendCmd(new CmdWinSetSize((nint) reg.GlfwWindow, size.X, size.Y));
}
public void WindowSetVisible(WindowReg window, bool visible)
{
var reg = (GlfwWindowReg) window;
@@ -92,6 +99,13 @@ namespace Robust.Client.Graphics.Clyde
SendCmd(new CmdWinSetVisible((nint) reg.GlfwWindow, visible));
}
private void WinThreadWinSetSize(CmdWinSetSize cmd)
{
var win = (Window*) cmd.Window;
GLFW.SetWindowSize(win, cmd.W, cmd.H);
}
private void WinThreadWinSetVisible(CmdWinSetVisible cmd)
{
var win = (Window*) cmd.Window;

View File

@@ -39,6 +39,7 @@ namespace Robust.Client.Graphics.Clyde
void WindowDestroy(WindowReg reg);
void WindowSetTitle(WindowReg window, string title);
void WindowSetMonitor(WindowReg window, IClydeMonitor monitor);
void WindowSetSize(WindowReg window, Vector2i size);
void WindowSetVisible(WindowReg window, bool visible);
void WindowRequestAttention(WindowReg window);
void WindowSwapBuffers(WindowReg window);

View File

@@ -93,6 +93,10 @@ internal partial class Clyde
WinThreadWinRequestAttention(cmd);
break;
case CmdWinSetSize cmd:
WinThreadWinSetSize(cmd);
break;
case CmdWinSetVisible cmd:
WinThreadWinSetVisible(cmd);
break;
@@ -246,6 +250,11 @@ internal partial class Clyde
nint Window
) : CmdBase;
private sealed record CmdWinSetSize(
nint Window,
int W, int H
) : CmdBase;
private sealed record CmdWinSetVisible(
nint Window,
bool Visible

View File

@@ -336,11 +336,22 @@ internal partial class Clyde
_sawmill.Warning("WindowSetMonitor not implemented on SDL2");
}
public void WindowSetSize(WindowReg window, Vector2i size)
{
SendCmd(new CmdWinSetSize(WinPtr(window), size.X, size.Y));
}
public void WindowSetVisible(WindowReg window, bool visible)
{
window.IsVisible = visible;
SendCmd(new CmdWinSetVisible(WinPtr(window), visible));
}
private static void WinThreadWinSetSize(CmdWinSetSize cmd)
{
SDL_SetWindowSize(cmd.Window, cmd.W, cmd.H);
}
private static void WinThreadWinSetVisible(CmdWinSetVisible cmd)
{
if (cmd.Visible)

View File

@@ -14,7 +14,7 @@ namespace Robust.Client.Graphics
WindowId Id { get; }
IRenderTarget RenderTarget { get; }
string Title { get; set; }
Vector2i Size { get; }
Vector2i Size { get; set; }
bool IsFocused { get; }
bool IsMinimized { get; }
bool IsVisible { get; set; }

View File

@@ -0,0 +1,86 @@
using System;
using System.IO;
using System.Security.Cryptography;
using Microsoft.Win32;
using Robust.Client.Utility;
using Robust.Shared.IoC;
using Robust.Shared.Network;
namespace Robust.Client.HWId;
internal sealed class BasicHWId : IHWId
{
[Dependency] private readonly IGameControllerInternal _gameController = default!;
public const int LengthHwid = 32;
public byte[] GetLegacy()
{
if (OperatingSystem.IsWindows())
return GetWindowsHWid("Hwid");
return [];
}
public byte[] GetModern()
{
byte[] raw;
if (OperatingSystem.IsWindows())
raw = GetWindowsHWid("Hwid2");
else
raw = GetFileHWid();
return [0, ..raw];
}
private static byte[] GetWindowsHWid(string keyName)
{
const string keyPath = @"HKEY_CURRENT_USER\SOFTWARE\Space Wizards\Robust";
var regKey = Registry.GetValue(keyPath, keyName, null);
if (regKey is byte[] { Length: LengthHwid } bytes)
return bytes;
var newId = new byte[LengthHwid];
RandomNumberGenerator.Fill(newId);
Registry.SetValue(
keyPath,
keyName,
newId,
RegistryValueKind.Binary);
return newId;
}
private byte[] GetFileHWid()
{
var path = UserDataDir.GetRootUserDataDir(_gameController);
var hwidPath = Path.Combine(path, ".hwid");
var value = ReadHWidFile(hwidPath);
if (value != null)
return value;
value = RandomNumberGenerator.GetBytes(LengthHwid);
File.WriteAllBytes(hwidPath, value);
return value;
}
private static byte[]? ReadHWidFile(string path)
{
try
{
var value = File.ReadAllBytes(path);
if (value.Length == LengthHwid)
return value;
}
catch (FileNotFoundException)
{
// First time the file won't exist.
}
return null;
}
}

View File

@@ -26,5 +26,20 @@ public interface IGameController
/// This exists to give content module more control over tick updating.
/// </summary>
event Action<FrameEventArgs>? TickUpdateOverride;
/// <summary>
/// Get the games Title, if Options.DefaultWindowTitle or if defaultWindowTitle is not set in the manifest.yml, it will default to RobustToolbox.
/// </summary>
string GameTitle();
/// <summary>
/// Get the games Window Icon set, if Options.WindowIconSet or if windowIconSet is not set in the manifest.yml, it will default to an empty string.
/// </summary>
string WindowIconSet();
/// <summary>
/// Get the games Splash Logo, if Options.SplashLogo or if splashLogo is not set in the manifest.yml, it will default to an empty string.
/// </summary>
string SplashLogo();
}

View File

@@ -156,7 +156,6 @@ public sealed partial class PhysicsSystem
if (activeA == false && activeB == false)
{
contact.IsTouching = false;
continue;
}

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@@ -14,16 +15,16 @@ namespace Robust.Client.Placement.Modes
public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
{
// Go over diagonal size so when placing in a line it doesn't stop snapping.
const float SearchBoxSize = 2f; // size of search box in meters
const float searchBoxSize = 2f; // size of search box in meters
MouseCoords = ScreenToCursorGrid(mouseScreen).AlignWithClosestGridTile(SearchBoxSize, pManager.EntityManager, pManager.MapManager);
MouseCoords = ScreenToCursorGrid(mouseScreen).AlignWithClosestGridTile(searchBoxSize, pManager.EntityManager, pManager.MapManager);
var gridId = MouseCoords.GetGridUid(pManager.EntityManager);
var gridId = pManager.EntityManager.System<SharedTransformSystem>().GetGrid(MouseCoords);
if (!pManager.EntityManager.TryGetComponent<MapGridComponent>(gridId, out var mapGrid))
return;
CurrentTile = mapGrid.GetTileRef(MouseCoords);
CurrentTile = pManager.EntityManager.System<SharedMapSystem>().GetTileRef(gridId.Value, mapGrid, MouseCoords);
float tileSize = mapGrid.TileSize; //convert from ushort to float
GridDistancing = tileSize;

View File

@@ -1,6 +1,5 @@
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
@@ -19,12 +18,12 @@ namespace Robust.Client.Placement.Modes
MouseCoords = ScreenToCursorGrid(mouseScreen);
var tileSize = 1f;
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
var gridIdOpt = pManager.EntityManager.System<SharedTransformSystem>().GetGrid(MouseCoords);
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
if (gridIdOpt is { } gridId && gridId.IsValid())
{
var mapGrid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
CurrentTile = mapGrid.GetTileRef(MouseCoords);
CurrentTile = pManager.EntityManager.System<SharedMapSystem>().GetTileRef(gridId, mapGrid ,MouseCoords);
tileSize = mapGrid.TileSize; //convert from ushort to float
}
@@ -50,12 +49,12 @@ namespace Robust.Client.Placement.Modes
return false;
}
var map = MouseCoords.GetMapId(pManager.EntityManager);
var map = pManager.EntityManager.System<SharedTransformSystem>().GetMapId(MouseCoords);
var bottomLeft = new Vector2(CurrentTile.X, CurrentTile.Y);
var topRight = new Vector2(CurrentTile.X + 0.99f, CurrentTile.Y + 0.99f);
var box = new Box2(bottomLeft, topRight);
return !EntitySystem.Get<EntityLookupSystem>().AnyEntitiesIntersecting(map, box);
return !pManager.EntityManager.System<EntityLookupSystem>().AnyEntitiesIntersecting(map, box);
}
}
}

View File

@@ -754,14 +754,14 @@ namespace Robust.Client.Placement
if (CurrentPermission.IsTile)
{
var gridIdOpt = coordinates.GetGridUid(EntityManager);
var gridIdOpt = EntityManager.System<SharedTransformSystem>().GetGrid(coordinates);
// If we have actually placed something on a valid grid...
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
if (gridIdOpt is { } gridId && gridId.IsValid())
{
var grid = EntityManager.GetComponent<MapGridComponent>(gridId);
// no point changing the tile to the same thing.
if (grid.GetTileRef(coordinates).Tile.TypeId == CurrentPermission.TileType)
if (EntityManager.System<SharedMapSystem>().GetTileRef(gridId, grid, coordinates).Tile.TypeId == CurrentPermission.TileType)
return;
}

View File

@@ -197,8 +197,9 @@ namespace Robust.Client.Placement
/// </summary>
public TileRef GetTileRef(EntityCoordinates coordinates)
{
var gridUidOpt = coordinates.GetGridUid(pManager.EntityManager);
return gridUidOpt is EntityUid gridUid && gridUid.IsValid() ? pManager.EntityManager.GetComponent<MapGridComponent>(gridUid).GetTileRef(MouseCoords)
var gridUidOpt = pManager.EntityManager.System<SharedTransformSystem>().GetGrid(coordinates);
return gridUidOpt is { } gridUid && gridUid.IsValid()
? pManager.EntityManager.System<SharedMapSystem>().GetTileRef(gridUid, pManager.EntityManager.GetComponent<MapGridComponent>(gridUid), MouseCoords)
: new TileRef(gridUidOpt ?? EntityUid.Invalid,
MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager, pManager.EntityManager.System<SharedTransformSystem>()), Tile.Empty);
}

View File

@@ -261,7 +261,6 @@ namespace Robust.Client.Player
// This is a new userid, so we create a new session.
DebugTools.Assert(state.UserId != LocalPlayer?.UserId);
var newSession = (ICommonSessionInternal)CreateAndAddSession(state.UserId, state.Name);
newSession.SetPing(state.Ping);
SetStatus(newSession, state.Status);
SetAttachedEntity(newSession, controlled, out _, true);
dirty = true;
@@ -271,7 +270,6 @@ namespace Robust.Client.Player
// Check if the data is actually different
if (session.Name == state.Name
&& session.Status == state.Status
&& session.Ping == state.Ping
&& session.AttachedEntity == controlled)
{
continue;
@@ -280,7 +278,6 @@ namespace Robust.Client.Player
dirty = true;
var local = (ICommonSessionInternal)session;
local.SetName(state.Name);
local.SetPing(state.Ping);
SetStatus(local, state.Status);
SetAttachedEntity(local, controlled, out _, true);
}

View File

@@ -23,7 +23,7 @@ public sealed partial class ReplayLoadManager
// Scratch data used by UpdateEntityStates.
// Avoids copying changes for every change to an entity between checkpoints, instead copies once per checkpoint on
// first change. We can also use this to avoid building a dictionary of ComponentChange inside the inner loop.
private class UpdateScratchData
private sealed class UpdateScratchData
{
public Dictionary<ushort, ComponentChange> Changes;
public EntityState lastChange;

View File

@@ -52,7 +52,7 @@ public sealed partial class ReplayLoadManager
var uncompressedSize = BitConverter.ToInt32(intBuf);
var decompressedStream = new MemoryStream(uncompressedSize);
decompressStream.CopyTo(decompressedStream, uncompressedSize);
decompressStream.CopyTo(decompressedStream);
decompressedStream.Position = 0;
DebugTools.Assert(uncompressedSize == decompressedStream.Length);

View File

@@ -48,7 +48,10 @@ public interface IResourceCache : IResourceManager
event Action<TextureLoadedEventArgs> OnRawTextureLoaded;
event Action<RsiLoadedEventArgs> OnRsiLoaded;
[Obsolete("Fetch these through IoC directly instead")]
IClyde Clyde { get; }
[Obsolete("Fetch these through IoC directly instead")]
IFontManager FontManager { get; }
}

View File

@@ -44,6 +44,10 @@
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(RobustToolsBuild)' == 'True'">
<ProjectReference Include="..\Robust.Xaml\Robust.Xaml.csproj" />
</ItemGroup>
<!-- Shader embedding -->
<ItemGroup>
<EmbeddedResource Include="Graphics\Clyde\Shaders\*" />

View File

@@ -762,7 +762,23 @@ namespace Robust.Client.UserInterface
throw new InvalidOperationException("The provided control is not a direct child of this control.");
}
_orderedChildren.Remove(child);
var childIndex = _orderedChildren.IndexOf(child);
RemoveChild(childIndex);
}
/// <summary>
/// Removes the child at a specific index from this control.
/// </summary>
/// <param name="childIndex">The index of the child to remove.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the provided child index is out of range
/// </exception>
public void RemoveChild(int childIndex)
{
DebugTools.Assert(!Disposed, "Control has been disposed.");
var child = _orderedChildren[childIndex];
_orderedChildren.RemoveAt(childIndex);
child.Parent = null;

View File

@@ -7,6 +7,8 @@ using Robust.Client.Placement;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
@@ -19,9 +21,9 @@ namespace Robust.Client.UserInterface.Controllers.Implementations;
public sealed class EntitySpawningUIController : UIController
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPlacementManager _placement = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly IResourceCache _resources = default!;
private EntitySpawnWindow? _window;
private readonly List<EntityPrototype> _shownEntities = new();
@@ -193,6 +195,9 @@ public sealed class EntitySpawningUIController : UIController
_window.SelectedButton = null;
searchStr = searchStr?.ToLowerInvariant();
var categoryFilter = _cfg.GetCVar(CVars.EntitiesCategoryFilter);
_prototypes.TryIndex<EntityCategoryPrototype>(categoryFilter, out var filter);
foreach (var prototype in _prototypes.EnumeratePrototypes<EntityPrototype>())
{
if (prototype.Abstract)
@@ -205,6 +210,11 @@ public sealed class EntitySpawningUIController : UIController
continue;
}
if (filter is not null && !prototype.Categories.Contains(filter))
{
continue;
}
if (searchStr != null && !DoesEntityMatchSearch(prototype, searchStr))
{
continue;

View File

@@ -27,6 +27,7 @@ public sealed class TileSpawningUIController : UIController
private readonly List<ITileDefinition> _shownTiles = new();
private bool _clearingTileSelections;
private bool _eraseTile;
public override void Initialize()
{
@@ -35,6 +36,37 @@ public sealed class TileSpawningUIController : UIController
_placement.PlacementChanged += ClearTileSelection;
}
private void StartTilePlacement(int tileType)
{
var newObjInfo = new PlacementInformation
{
PlacementOption = "AlignTileAny",
TileType = tileType,
Range = 400,
IsTile = true
};
_placement.BeginPlacing(newObjInfo);
}
private void OnTileEraseToggled(ButtonToggledEventArgs args)
{
if (_window == null || _window.Disposed)
return;
_placement.Clear();
if (args.Pressed)
{
_eraseTile = true;
StartTilePlacement(0);
}
else
_eraseTile = false;
args.Button.Pressed = args.Pressed;
}
public void ToggleWindow()
{
EnsureWindow();
@@ -60,6 +92,8 @@ public sealed class TileSpawningUIController : UIController
_window.SearchBar.OnTextChanged += OnTileSearchChanged;
_window.TileList.OnItemSelected += OnTileItemSelected;
_window.TileList.OnItemDeselected += OnTileItemDeselected;
_window.EraseButton.Pressed = _eraseTile;
_window.EraseButton.OnToggled += OnTileEraseToggled;
BuildTileList();
}
@@ -76,6 +110,7 @@ public sealed class TileSpawningUIController : UIController
_clearingTileSelections = true;
_window.TileList.ClearSelected();
_clearingTileSelections = false;
_window.EraseButton.Pressed = false;
}
private void OnTileClearPressed(ButtonEventArgs args)
@@ -102,16 +137,7 @@ public sealed class TileSpawningUIController : UIController
private void OnTileItemSelected(ItemList.ItemListSelectedEventArgs args)
{
var definition = _shownTiles[args.ItemIndex];
var newObjInfo = new PlacementInformation
{
PlacementOption = "AlignTileAny",
TileType = definition.TileId,
Range = 400,
IsTile = true
};
_placement.BeginPlacing(newObjInfo);
StartTilePlacement(definition.TileId);
}
private void OnTileItemDeselected(ItemList.ItemListDeselectedEventArgs args)

View File

@@ -26,6 +26,7 @@ namespace Robust.Client.UserInterface.Controls
private bool _enableAllKeybinds;
private ButtonGroup? _group;
private bool _toggleMode;
private bool _muteSounds;
/// <summary>
/// Specifies the group this button belongs to.
@@ -135,7 +136,8 @@ namespace Robust.Client.UserInterface.Controls
if (Pressed != value)
return;
UserInterfaceManager.ClickSound();
if (!MuteSounds)
UserInterfaceManager.ClickSound();
}
/// <summary>
@@ -199,6 +201,16 @@ namespace Robust.Client.UserInterface.Controls
}
}
/// <summary>
/// If <c>true</c>, this button will not emit sounds when the mouse is pressed or hovered over.
/// </summary>
[ViewVariables]
public bool MuteSounds
{
get => _muteSounds;
set => _muteSounds = value;
}
/// <summary>
/// Fired when the button is pushed down by the mouse.
/// </summary>
@@ -298,7 +310,8 @@ namespace Robust.Client.UserInterface.Controls
}
else
{
UserInterfaceManager.ClickSound();
if (!MuteSounds)
UserInterfaceManager.ClickSound();
}
OnPressed?.Invoke(buttonEventArgs);
@@ -353,7 +366,7 @@ namespace Robust.Client.UserInterface.Controls
{
base.MouseEntered();
if (!Disabled)
if (!Disabled && !MuteSounds)
{
UserInterfaceManager.HoverSound();
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Robust.Shared.Maths;
@@ -56,7 +57,7 @@ namespace Robust.Client.UserInterface.Controls
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
// Account for separation.
var separation = ActualSeparation * (ChildCount - 1);
var separation = ActualSeparation * (Children.Where(c => c.Visible).Count() - 1);
var desiredSize = Vector2.Zero;
if (Vertical)
{
@@ -136,13 +137,14 @@ namespace Robust.Client.UserInterface.Controls
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var separation = ActualSeparation;
var visibleChildCount = Children.Where(c => c.Visible).Count();
var stretchAvail = Vertical ? finalSize.Y : finalSize.X;
stretchAvail -= separation * (ChildCount - 1);
stretchAvail -= separation * (visibleChildCount - 1);
stretchAvail = Math.Max(0, stretchAvail);
// Step one: figure out the sizes of all our children and whether they want to stretch.
var sizeList = new List<(Control control, float size, bool stretch)>(ChildCount);
var sizeList = new List<(Control control, float size, bool stretch)>(visibleChildCount);
var totalStretchRatio = 0f;
foreach (var child in Children)
{

View File

@@ -53,6 +53,15 @@ namespace Robust.Client.UserInterface.Controls
private TimeSpan? _lastClickTime;
private Vector2? _lastClickPosition;
// Keep track of the frame on which we got focus, so we can implement SelectAllOnFocus properly.
// Otherwise, there's no way to keep track of whether the KeyDown is the one that focused the text box,
// to avoid text selection stomping on the behavior.
// This isn't a great way to do it.
// A better fix would be to annotate all input events with some unique sequence ID,
// and expose the input event that focused the control in KeyboardFocusEntered.
// But that sounds like a refactor I'm not doing today.
private uint _focusedOnFrame;
private bool IsPlaceHolderVisible => !(HidePlaceHolderOnFocus && HasKeyboardFocus()) && string.IsNullOrEmpty(_text) && _placeHolder != null;
public event Action<LineEditEventArgs>? OnTextChanged;
@@ -190,6 +199,11 @@ namespace Robust.Client.UserInterface.Controls
public bool IgnoreNext { get; set; }
/// <summary>
/// If true, all the text in the LineEdit will be automatically selected whenever it is focused.
/// </summary>
public bool SelectAllOnFocus { get; set; }
private (int start, int length)? _imeData;
@@ -709,7 +723,7 @@ namespace Robust.Client.UserInterface.Controls
args.Handle();
}
else
else if (!(SelectAllOnFocus && _focusedOnFrame == _timing.CurFrame))
{
_lastClickTime = _timing.RealTime;
_lastClickPosition = args.PointerLocation.Position;
@@ -868,6 +882,13 @@ namespace Robust.Client.UserInterface.Controls
{
_clyde.TextInputStart();
}
_focusedOnFrame = _timing.CurFrame;
if (SelectAllOnFocus)
{
CursorPosition = _text.Length;
SelectionStart = 0;
}
}
protected internal override void KeyboardFocusExited()

View File

@@ -1,6 +1,5 @@
using System;
using System.ComponentModel;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.IoC;
@@ -33,9 +32,9 @@ namespace Robust.Client.UserInterface.Controls
public IClydeWindow? Owner { get; set; }
/// <summary>
/// Whether the window is currently open.
/// Whether the window is created and currently open.
/// </summary>
public bool IsOpen => ClydeWindow != null;
public bool IsOpen => ClydeWindow?.IsVisible ?? false;
/// <summary>
/// The title of the window.
@@ -97,12 +96,13 @@ namespace Robust.Client.UserInterface.Controls
}
/// <summary>
/// Show the window to the user.
/// Create the window if not already created.
/// This window is not visible by default, call <see cref="Show"/> to display it.
/// </summary>
public void Show()
public IClydeWindow Create()
{
if (IsOpen)
return;
if (ClydeWindow != null)
return ClydeWindow;
var parameters = new WindowCreateParameters();
@@ -127,6 +127,7 @@ namespace Robust.Client.UserInterface.Controls
parameters.Styles = WindowStyles;
parameters.Owner = Owner;
parameters.StartupLocation = StartupLocation;
parameters.Visible = false;
ClydeWindow = _clyde.CreateWindow(parameters);
ClydeWindow.RequestClosed += OnWindowRequestClosed;
@@ -136,6 +137,19 @@ namespace Robust.Client.UserInterface.Controls
_root = UserInterfaceManager.CreateWindowRoot(ClydeWindow);
_root.AddChild(this);
// Resize the window by our UIScale
ClydeWindow.Size = new((int)(ClydeWindow.Size.X * UIScale), (int)(ClydeWindow.Size.Y * UIScale));
return ClydeWindow;
}
/// <summary>
/// Show the window to the user, creating it if necessary
/// </summary>
public void Show()
{
ClydeWindow = Create();
ClydeWindow.IsVisible = true;
Shown();
}
@@ -179,7 +193,7 @@ namespace Robust.Client.UserInterface.Controls
private void OnWindowResized(WindowResizedEventArgs obj)
{
SetSize = obj.NewSize;
SetSize = obj.NewSize / UIScale;
}
private void RealClosed()

View File

@@ -127,6 +127,7 @@ namespace Robust.Client.UserInterface.Controls
var style = _getStyleBox();
var font = _getFont();
var lineSeparation = font.GetLineSeparation(UIScale);
style?.Draw(handle, PixelSizeBox, UIScale);
var contentBox = _getContentBox();
@@ -141,18 +142,26 @@ namespace Robust.Client.UserInterface.Controls
{
if (entryOffset + entry.Height < 0)
{
entryOffset += entry.Height + font.GetLineSeparation(UIScale);
// Controls within the entry are the children of this control, which means they are drawn separately
// after this Draw call, so we have to mark them as invisible to prevent them from being drawn.
//
// An alternative option is to ensure that the control position updating logic in entry.Draw is always
// run, and then setting RectClipContent = true to use scissor box testing to handle the controls
// visibility
entry.HideControls();
entryOffset += entry.Height + lineSeparation;
continue;
}
if (entryOffset > contentBox.Height)
{
break;
entry.HideControls();
continue;
}
entry.Draw(_tagManager, handle, font, contentBox, entryOffset, context, UIScale);
entryOffset += entry.Height + font.GetLineSeparation(UIScale);
entryOffset += entry.Height + lineSeparation;
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.Graphics;

View File

@@ -127,11 +127,11 @@ namespace Robust.Client.UserInterface.Controls
ClearButtons();
foreach (var num in leftButtons)
{
AddLeftButton(num, num.ToString());
AddLeftButton(num, num.ToString("+#;-#;0"));
}
foreach (var num in rightButtons)
{
AddRightButton(num, num.ToString());
AddRightButton(num, num.ToString("+#;-#;0"));
}
}

View File

@@ -268,12 +268,15 @@ namespace Robust.Client.UserInterface.Controls
var first = GetChild(0);
var second = GetChild(1);
firstMinSize ??= (Vertical ? first.DesiredSize.Y : first.DesiredSize.X);
secondMinSize ??= (Vertical ? second.DesiredSize.Y : second.DesiredSize.X);
var size = Vertical ? controlSize.Y : controlSize.X;
if (first.IsMeasureValid && second.IsMeasureValid)
{
firstMinSize ??= (Vertical ? first.DesiredSize.Y : first.DesiredSize.X);
secondMinSize ??= (Vertical ? second.DesiredSize.Y : second.DesiredSize.X);
var size = Vertical ? controlSize.Y : controlSize.X;
_splitStart = MathHelper.Clamp(_splitStart, firstMinSize.Value,
size - (secondMinSize.Value + _splitWidth));
_splitStart = MathHelper.Clamp(_splitStart, firstMinSize.Value,
size - (secondMinSize.Value + _splitWidth));
}
}
}

View File

@@ -245,7 +245,7 @@ namespace Robust.Client.UserInterface.Controls
var offset = SpriteOffset
? Vector2.Zero
: - (-_eyeRotation).RotateVec(sprite.Offset) * new Vector2(1, -1) * EyeManager.PixelsPerMeter;
: - (-_eyeRotation).RotateVec(sprite.Offset * _scale) * new Vector2(1, -1) * EyeManager.PixelsPerMeter;
var position = PixelSize / 2 + offset * stretch * UIScale;
var scale = Scale * UIScale * stretch;

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Input;
@@ -21,6 +22,8 @@ namespace Robust.Client.UserInterface.Controls
private int _currentTab;
private bool _tabsVisible = true;
// The right-most coordinate of each tab header
private List<float> _tabRight = new();
public int CurrentTab
{
@@ -157,11 +160,14 @@ namespace Robust.Client.UserInterface.Controls
var headerOffset = 0f;
_tabRight.Clear();
// Then, draw the tabs.
for (var i = 0; i < ChildCount; i++)
{
if (!GetTabVisible(i))
{
_tabRight.Add(headerOffset);
continue;
}
@@ -214,6 +220,8 @@ namespace Robust.Client.UserInterface.Controls
}
headerOffset += boxAdvance;
// Remember the right-most point of this tab, for testing clicked areas
_tabRight.Add(headerOffset);
}
}
@@ -283,46 +291,17 @@ namespace Robust.Client.UserInterface.Controls
args.Handle();
var relX = args.RelativePixelPosition.X;
var font = _getFont();
var boxActive = _getTabBoxActive();
var boxInactive = _getTabBoxInactive();
var headerOffset = 0f;
float tabLeft = 0;
for (var i = 0; i < ChildCount; i++)
{
if (!GetTabVisible(i))
if (relX > tabLeft && relX <= _tabRight[i])
{
continue;
}
var title = GetActualTabTitle(i);
var titleLength = 0;
// Get string length.
foreach (var rune in title.EnumerateRunes())
{
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
{
continue;
}
titleLength += metrics.Advance;
}
var active = _currentTab == i;
var box = active ? boxActive : boxInactive;
var boxAdvance = titleLength + (box?.MinimumSize.X ?? 0);
if (headerOffset < relX && headerOffset + boxAdvance > relX)
{
// Got em.
CurrentTab = i;
return;
}
headerOffset += boxAdvance;
// Next tab starts here
tabLeft = _tabRight[i];
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using Robust.Shared.IoC;
@@ -235,6 +236,15 @@ namespace Robust.Client.UserInterface.CustomControls
public void OpenToRight() => OpenCenteredAt(new Vector2(1, 0.5f));
public void OpenCenteredRight() => OpenCenteredAt(new Vector2(0.75f, 0.5f));
/// <summary>
/// Opens a window and centers it relative to the screen position.
/// </summary>
public void OpenScreenAt(Vector2 relativePosition, IClyde clyde)
{
var adjusted = relativePosition / clyde.ScreenSize;
OpenCenteredAt(adjusted);
}
/// <summary>
/// Opens a window, attempting to place the center of the window at some relative point on the screen.
/// </summary>

View File

@@ -13,7 +13,7 @@
</ScrollContainer>
<BoxContainer Orientation="Horizontal">
<Button Name="ReplaceButton" Access="Public" ToggleMode="True" Text="{Loc entity-spawn-window-replace-button-text}"/>
<Button Name="EraseButton" Access="Public" ToggleMode="True" Text="{Loc entity-spawn-window-erase-button-text}"/>
<Button Name="EraseButton" Access="Public" ToggleMode="True" Text="{Loc window-erase-button-text}"/>
<OptionButton Name="OverrideMenu" Access="Public" HorizontalExpand="True" ToolTip="{Loc entity-spawn-window-override-menu-tooltip}" />
</BoxContainer>
<Label Name="RotationLabel" Access="Public"/>

View File

@@ -1,10 +0,0 @@
using System;
namespace Robust.Client.UserInterface.CustomControls;
[Obsolete("Use DefaultWindow instead")]
[Virtual]
public class SS14Window : DefaultWindow
{
}

View File

@@ -131,11 +131,6 @@ internal sealed class TextEditRopeViz : OSWindow
throw new ArgumentOutOfRangeException(nameof(node));
}
}
static UIBox2 Around(Vector2 vec, float size)
{
return new UIBox2(vec - new Vector2(size, size), vec + new Vector2(size, size));
}
}
private static Color[] CalcLeafColors()

View File

@@ -1,6 +1,6 @@
<TileSpawnWindow
xmlns="https://spacestation14.io"
Title="Place Tiles"
Title="{Loc tile-spawn-window-title}"
SetSize="300 300"
MinSize="300 200">
<BoxContainer Orientation="Vertical">
@@ -9,5 +9,8 @@
<Button Name="ClearButton" Access="Public" Text="Clear"/>
</BoxContainer>
<ItemList Name="TileList" Access="Public" VerticalExpand="True"/>
<BoxContainer Orientation="Horizontal">
<Button Name="EraseButton" Access="Public" ToggleMode="True" Text="{Loc window-erase-button-text}"/>
</BoxContainer>
</BoxContainer>
</TileSpawnWindow>

View File

@@ -118,9 +118,6 @@ namespace Robust.Client.UserInterface
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
continue;
if (ProcessRune(ref this, new Rune(' '), out breakLine))
continue;
control.Measure(new Vector2(Width, Height));
var desiredSize = control.DesiredPixelSize;
@@ -167,6 +164,16 @@ namespace Robust.Client.UserInterface
}
}
internal readonly void HideControls()
{
if (_tagControls == null)
return;
foreach (var control in _tagControls.Values)
{
control.Visible = false;
}
}
public readonly void Draw(
MarkupTagManager tagManager,
DrawingHandleScreen handle,
@@ -216,8 +223,11 @@ namespace Robust.Client.UserInterface
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
continue;
var invertedScale = 1f / uiScale;
// Controls may have been previously hidden via HideControls due to being "out-of frame".
// If this ever gets replaced with RectClipContents / scissor box testing, this can be removed.
control.Visible = true;
var invertedScale = 1f / uiScale;
control.Position = new Vector2(baseLine.X * invertedScale, (baseLine.Y - defaultFont.GetAscent(uiScale)) * invertedScale);
control.Measure(new Vector2(Width, Height));
var advanceX = control.DesiredPixelSize.X;

View File

@@ -35,6 +35,7 @@ internal partial class UserInterfaceManager
return;
_controlFocused?.ControlFocusExited();
_controlFocused = value;
_needUpdateActiveCursor = true;
}
}
@@ -148,7 +149,7 @@ internal partial class UserInterfaceManager
var newHovered = MouseGetControl(mouseMoveEventArgs.Position);
SetHovered(newHovered);
var target = ControlFocused ?? newHovered;
var target = ControlFocused ?? CurrentlyHovered;
if (target != null)
{
var pos = mouseMoveEventArgs.Position.Position;
@@ -164,7 +165,7 @@ internal partial class UserInterfaceManager
public void UpdateHovered()
{
var ctrl = MouseGetControl(_inputManager.MouseScreenPosition);
var ctrl = MouseGetControl(_inputManager.MouseScreenPosition);
SetHovered(ctrl);
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared;
using Robust.Shared.Map;
using Robust.Shared.Utility;
@@ -28,10 +29,11 @@ internal sealed partial class UserInterfaceManager
{
MouseFilter = Control.MouseFilterMode.Ignore,
HorizontalAlignment = Control.HAlignment.Stretch,
VerticalAlignment = Control.VAlignment.Stretch,
UIScaleSet = window.ContentScale.X
VerticalAlignment = Control.VAlignment.Stretch
};
newRoot.UIScaleSet = CalculateAutoScale(newRoot);
_roots.Add(newRoot);
_windowsToRoot.Add(window.Id, newRoot);

View File

@@ -215,6 +215,9 @@ namespace Robust.Client.UserInterface
{
using (_prof.Group("Update"))
{
// Update hovered. Can't rely upon mouse movement due to New controls potentially coming up.
UpdateHovered();
foreach (var root in _roots)
{
CheckRootUIScaleUpdate(root);

View File

@@ -0,0 +1,20 @@
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// This service locates the SS14 source tree and watches for changes to its xaml files.
/// </summary>
/// <remarks>
/// It then reloads them instantly.
///
/// It depends on <see cref="IXamlProxyManager"/> and is stubbed on non-TOOLS builds.
/// </remarks>
interface IXamlHotReloadManager
{
/// <summary>
/// Initialize the hot reload manager.
/// </summary>
/// <remarks>
/// You can't do anything with this once it's started, including turn it off.
/// </remarks>
void Initialize();
}

View File

@@ -0,0 +1,11 @@
using System;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// Reexport the Populate method of <see cref="IXamlProxyManager"/> and nothing else.
/// </summary>
public interface IXamlProxyHelper
{
bool Populate(Type t, object o);
}

View File

@@ -0,0 +1,76 @@
using System;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// This service provides a proxy for Populate, which is the generated function that
/// initializes the UI objects of a Xaml widget.
/// </summary>
/// <remarks>
/// The proxy can always return false: in that case, a Xaml widget will self-populate
/// as usual. This is the behavior on Release builds.
///
/// However, it can also call into an externally-provided implementation of the Xaml
/// widget.
///
/// No source of externally-provided implementations actually exists, by default --
/// you will need to call SetImplementation with a blob of xaml source code to provide
/// one. <see cref="IXamlHotReloadManager" /> is an example of a service that calls into
/// that functionality.
/// </remarks>
internal interface IXamlProxyManager
{
/// <summary>
/// Initialize creates the <see cref="IXamlProxyManager"/>.
/// </summary>
/// <remarks>
/// If the <see cref="IXamlProxyManager" /> is not a stub, then it will spy on the
/// assembly list (from <see cref="Robust.Shared.Reflection.IReflectionManager" />)
/// and find <see cref="XamlMetadataAttribute" /> entries on the loaded types.
/// </remarks>
void Initialize();
/// <summary>
/// Return true if at least one <see cref="Type"/> in the current project expects its XAML
/// to come from a file with the given name.
/// </summary>
/// <remarks>
/// This method supports code that is trying to figure out what name the build process
/// would have assigned to a resource file. A caller can try a few candidate names and use
/// its "yes" to continue.
///
/// This method is very fast, so it's OK to hammer it!
///
/// Also, on a non-tools build, this always returns false.
/// </remarks>
/// <param name="fileName">the filename</param>
/// <returns>true if expected</returns>
bool CanSetImplementation(string fileName);
/// <summary>
/// Replace the implementation of <paramref name="fileName"/> with <paramref name="fileContent" />,
/// compiling it if needed.
///
/// All types based on <paramref name="fileName" /> will be recompiled.
/// </summary>
/// <remarks>
/// This may fail and the caller won't be notified. (There will usually be logs.)
///
/// On a non-tools build, this fails silently.
/// </remarks>
/// <param name="fileName">the name of the file</param>
/// <param name="fileContent">the new content of the file</param>
void SetImplementation(string fileName, string fileContent);
/// <summary>
/// If we have a JIT version of the XAML code for <paramref name="t" />, then call
/// the new implementation on <paramref name="o" />.
/// </summary>
/// <remarks>
/// <paramref name="o" /> may be a subclass of <paramref name="t" />.
/// </remarks>
/// <param name="t">the static type of the object</param>
/// <param name="o">the object</param>
/// <returns>true if we called a hot reloaded implementation</returns>
bool Populate(Type t, object o);
}

View File

@@ -0,0 +1,198 @@
#if TOOLS
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Robust.Shared.Asynchronous;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// The real implementation of <see cref="IXamlHotReloadManager" />.
/// </summary>
/// <remarks>
/// Its behavior is described there.
/// </remarks>
internal sealed class XamlHotReloadManager : IXamlHotReloadManager
{
private const string MarkerFileName = "SpaceStation14.sln";
[Dependency] ILogManager _logManager = null!;
[Dependency] private readonly IResourceManager _resources = null!;
[Dependency] private readonly ITaskManager _taskManager = null!;
[Dependency] private readonly IXamlProxyManager _xamlProxyManager = null!;
private ISawmill _sawmill = null!;
private FileSystemWatcher? _watcher;
public void Initialize()
{
_sawmill = _logManager.GetSawmill("xamlhotreload");
var codeLocation = InferCodeLocation();
if (codeLocation == null)
{
_sawmill.Warning($"could not find code -- where is {MarkerFileName}?");
return;
}
_sawmill.Info($"code location: {codeLocation}");
// must not be gc'ed or else it will stop reporting
// therefore: keep a reference
_watcher = CreateWatcher(codeLocation);
}
/// <summary>
/// Create a file system watcher that identifies XAML changes in a given
/// location.
/// </summary>
/// <param name="location">the location (a real path on the OS file system)</param>
/// <returns>the new watcher</returns>
/// <exception cref="ArgumentOutOfRangeException">if <see cref="FileSystemWatcher"/> violates its type-related postconditions</exception>
private FileSystemWatcher CreateWatcher(string location)
{
var watcher = new FileSystemWatcher(location)
{
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName,
};
void OnWatcherEvent(object sender, FileSystemEventArgs args)
{
switch (args.ChangeType)
{
case WatcherChangeTypes.Deleted:
return;
case WatcherChangeTypes.Renamed:
case WatcherChangeTypes.Created:
case WatcherChangeTypes.Changed:
case WatcherChangeTypes.All:
break;
default:
throw new ArgumentOutOfRangeException(nameof(args));
}
_taskManager.RunOnMainThread(() =>
{
var resourceFileName =
ResourceFileName(location, args.FullPath, _xamlProxyManager.CanSetImplementation);
if (resourceFileName == null)
{
return;
}
string newText;
try
{
newText = File.ReadAllText(args.FullPath);
}
catch (IOException ie)
{
_sawmill.Warning($"error attempting a hot reload -- skipped: {ie}");
return;
}
_xamlProxyManager.SetImplementation(resourceFileName, newText);
});
}
watcher.Changed += OnWatcherEvent;
watcher.Renamed += OnWatcherEvent;
watcher.EnableRaisingEvents = true;
return watcher;
}
/// <summary>
/// Using the content roots of the project, infer the location of its code.
/// </summary>
/// <remarks>
/// This kind of introspection is almost universally a bad idea, but we don't
/// feasibly have other options, so I've buried it in a private method.
/// </remarks>
/// <returns>the inferred code location or null</returns>
private string? InferCodeLocation()
{
// ascend upwards from each content root until the solution file is found
foreach (var contentRoot in _resources.GetContentRoots())
{
var systemPath = contentRoot.ToRelativeSystemPath();
while (true)
{
var files = Array.Empty<string>();
try
{
files = Directory.GetFiles(systemPath);
}
catch (IOException) { } // this is allowed to fail, and if so we just keep going up
if (files.Any(f => Path.GetFileName(f).Equals(MarkerFileName, StringComparison.InvariantCultureIgnoreCase)))
{
return systemPath;
}
DirectoryInfo? newPath = null;
try
{
newPath = Directory.GetParent(systemPath);
}
catch (IOException) { } // ditto here. if we don't find it, we're in the wrong place
if (newPath == null)
{
break;
}
systemPath = newPath.FullName;
}
}
return null;
}
/// <summary>
/// Infer the name of the resource file associated with the XAML item at the given path.
/// </summary>
/// <param name="codeLocation">the code location</param>
/// <param name="realPath">the real path of the file</param>
/// <param name="isDesired">a function returning true if something expects this file</param>
/// <returns>the name of a desired resource that matches this file, or null</returns>
private string? ResourceFileName(string codeLocation, string realPath, Predicate<string> isDesired)
{
// start with the name of the file and systematically add each super-directory until we reach
// the inferred code location.
//
// for /home/pyrex/ss14/Content.Client/Instruments/UI/InstrumentMenu.xaml, the following names
// will be tried:
//
// - InstrumentMenu.xaml
// - UI.InstrumentMenu.xaml
// - Instruments.UI.InstrumentMenu.xaml
// - Content.Client.Instruments.UI.InstrumentMenu.xaml
var resourceFileName = Path.GetFileName(realPath);
var super = Directory.GetParent(realPath);
var canonicalCodeLocation = Path.GetFullPath(codeLocation);
while (true)
{
// did someone want it: OK, jump out
if (isDesired(resourceFileName))
{
return resourceFileName;
}
if (super == null || Path.GetFullPath(super.FullName) == canonicalCodeLocation)
{
return null;
}
resourceFileName = super.Name + "." + resourceFileName;
super = super.Parent;
}
}
}
#endif

View File

@@ -0,0 +1,15 @@
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// A stub implementation of <see cref="XamlHotReloadManager"/>. Its
/// behavior is to do nothing.
/// </summary>
internal sealed class XamlHotReloadManagerStub : IXamlHotReloadManager
{
/// <summary>
/// Do nothing.
/// </summary>
public void Initialize()
{
}
}

View File

@@ -0,0 +1,221 @@
#if TOOLS
using System;
using System.Collections.Generic;
using System.Reflection;
using Robust.Shared.Log;
using Robust.Xaml;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// This is a utility class that tracks the relationship between resource file names,
/// Xamlx-compatible <see cref="Uri"/>s, <see cref="Type"/>s that are interested in a
/// given file, and implementations of Populate.
/// </summary>
internal sealed class XamlImplementationStorage
{
/// <summary>
/// For each filename, we store its last known <see cref="Uri"/>.
/// </summary>
/// <remarks>
/// When we compile the new implementation, we will use the same <see cref="Uri"/>.
/// </remarks>
private readonly Dictionary<string, Uri> _fileUri = new();
/// <summary>
/// For each filename, we store its last known content.
/// </summary>
/// <remarks>
/// This is known even for AOT-compiled code -- therefore, we can use this table
/// to convert an AOT-compiled Control to a JIT-compiled one.
/// </remarks>
private readonly Dictionary<string, string> _fileContent = new();
/// <summary>
/// For each filename, we store the type interested in this file.
/// </summary>
private readonly Dictionary<string, Type> _fileType = new();
/// <summary>
/// For each type, store the JIT-compiled implementation of Populate.
/// </summary>
/// <remarks>
/// If no such implementation exists, then methods that would normally
/// find and call a JIT'ed implementation will do nothing and return
/// false instead. As an ultimate result, the AOT'ed implementation
/// will be used.
/// </remarks>
private readonly Dictionary<Type, MethodInfo> _populateImplementations = new();
private readonly ISawmill _sawmill;
private readonly XamlJitDelegate _jitDelegate;
/// <summary>
/// Create the storage.
/// </summary>
/// <remarks>
/// It would be weird to call this from any type outside of
/// <see cref="Robust.Client.UserInterface.XAML.Proxy" />.
/// </remarks>
/// <param name="sawmill">the (shared) logger</param>
/// <param name="jitDelegate">
/// a delegate that calls the
/// <see cref="XamlJitCompiler"/>, possibly handling errors
/// </param>
public XamlImplementationStorage(ISawmill sawmill, XamlJitDelegate jitDelegate)
{
_sawmill = sawmill;
_jitDelegate = jitDelegate;
}
/// <summary>
/// Inspect <paramref name="assembly" /> for types that declare a <see cref="XamlMetadataAttribute"/>.
/// </summary>
/// <remarks>
/// We can only do hot reloading if we know this basic information.
///
/// Note that even release-mode content artifacts contain this attribute.
/// </remarks>
/// <param name="assembly">the assembly</param>
/// <returns>an IEnumerable of types with xaml metadata</returns>
private IEnumerable<(Type, XamlMetadataAttribute)> TypesWithXamlMetadata(Assembly assembly)
{
foreach (var type in assembly.GetTypes())
{
if (type.GetCustomAttribute<XamlMetadataAttribute>() is not { } attr)
{
continue;
}
yield return (type, attr);
}
}
/// <summary>
/// Add all Xaml-annotated types from <paramref name="assembly" /> to this storage.
/// </summary>
/// <remarks>
/// We don't JIT these types, but we store enough info that we could JIT
/// them if we wanted to.
/// </remarks>
/// <param name="assembly">an assembly</param>
public void Add(Assembly assembly)
{
foreach (var (type, metadata) in TypesWithXamlMetadata(assembly))
{
// this can fail, but if it does, that means something is _really_ wrong
// with the compiler, or someone tried to write their own Xaml metadata
Uri uri;
try
{
uri = new Uri(metadata.Uri);
}
catch (UriFormatException)
{
throw new InvalidProgramException(
$"XamlImplementationStorage encountered an malformed Uri in the metadata for {type.FullName}: " +
$"{metadata.Uri}. this is a bug in XamlAotCompiler"
);
}
var fileName = metadata.FileName;
var content = metadata.Content;
_fileUri[fileName] = uri;
_fileContent[fileName] = content;
if (!_fileType.TryAdd(fileName, type))
{
throw new InvalidProgramException(
$"XamlImplementationStorage observed that two types were interested in the same Xaml filename: " +
$"{fileName}. ({type.FullName} and {_fileType[fileName].FullName}). this is a bug in XamlAotCompiler"
);
}
}
}
/// <summary>
/// Quietly JIT every type with XAML metadata.
/// </summary>
/// <remarks>
/// This should have no visible effect except that the <see cref="XamlJitDelegate"/>
/// may dump some info messages into the terminal about cases where the
/// hot reload failed.
/// </remarks>
public void ForceReloadAll()
{
foreach (var (fileName, fileContent) in _fileContent)
{
SetImplementation(fileName, fileContent, true);
}
}
/// <summary>
/// Return true if calling <see cref="SetImplementation" /> on <paramref name="fileName" /> would not be a no-op.
/// </summary>
/// <remarks>
/// That is: if some type cares about the contents of <paramref name="fileName" />.
/// </remarks>
/// <param name="fileName">the filename</param>
/// <returns>true if not a no-op</returns>
public bool CanSetImplementation(string fileName)
{
return _fileType.ContainsKey(fileName);
}
/// <summary>
/// Replace the implementation of <paramref name="fileName"/> by JIT-ing
/// <paramref name="fileContent"/>.
/// </summary>
/// <remarks>
/// If nothing cares about the implementation of <paramref name="fileName"/>, then this will do nothing.
/// </remarks>
/// <param name="fileName">the name of the file whose implementation should be replaced</param>
/// <param name="fileContent">the new implementation</param>
/// <param name="quiet">if true, then don't bother to log</param>
public void SetImplementation(string fileName, string fileContent, bool quiet)
{
if (!_fileType.TryGetValue(fileName, out var type))
{
_sawmill.Warning($"SetImplementation called with {fileName}, but no types care about its contents");
return;
}
var uri =
_fileUri.GetValueOrDefault(fileName) ??
throw new InvalidProgramException("file URI missing (this is a bug in ImplementationStorage)");
if (!quiet)
{
_sawmill.Debug($"replacing {fileName} for {type}");
}
var impl = _jitDelegate(type, uri, fileName, fileContent);
if (impl != null)
{
_populateImplementations[type] = impl;
}
_fileContent[fileName] = fileContent;
}
/// <summary>
/// Call the JITed implementation of Populate on a XAML-associated object <paramref name="o"/>.
///
/// If no JITed implementation exists, return false.
/// </summary>
/// <param name="t">the static type of <paramref name="o"/></param>
/// <param name="o">an instance of <paramref name="t"/> (can be a subclass)</param>
/// <returns>true if a JITed implementation existed</returns>
public bool Populate(Type t, object o)
{
if (!_populateImplementations.TryGetValue(t, out var implementation))
{
// pop out if we never JITed anything
return false;
}
implementation.Invoke(null, [null, o]);
return true;
}
}
#endif

View File

@@ -0,0 +1,17 @@
using System;
using System.Reflection;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// This callback has the approximate type of <see cref="Robust.Xaml.XamlJitCompiler.Compile"/>,
/// but it has no error-signaling faculty.
/// </summary>
/// <remarks>
/// Implementors of this delegate should inform the users of errors in their own way.
///
/// Hot reloading failures should not directly take down the process, so implementors
/// should not rethrow exceptions unless they have a strong reason to believe they
/// will be caught.
/// </remarks>
internal delegate MethodInfo? XamlJitDelegate(Type type, Uri uri, string filename, string content);

View File

@@ -0,0 +1,29 @@
using System;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// Metadata to support JIT compilation of XAML resources for a type.
/// </summary>
/// <remarks>
/// We can feed XamlX data from this type, along with new content, to get new XAML
/// resources.
///
/// This type is inert and is generated for release artifacts too, not just debug
/// artifacts. Released content should support hot reloading if loaded in a debug
/// client, but this is untested.
/// </remarks>
[AttributeUsage(validOn: AttributeTargets.Class, Inherited = false)]
public sealed class XamlMetadataAttribute: System.Attribute
{
public readonly string Uri;
public readonly string FileName;
public readonly string Content;
public XamlMetadataAttribute(string uri, string fileName, string content)
{
Uri = uri;
FileName = fileName;
Content = content;
}
}

View File

@@ -0,0 +1,14 @@
using System;
using Robust.Shared.IoC;
namespace Robust.Client.UserInterface.XAML.Proxy;
internal sealed class XamlProxyHelper: IXamlProxyHelper
{
[Dependency] private IXamlProxyManager _xamlProxyManager = default!;
public bool Populate(Type t, object o)
{
return _xamlProxyManager.Populate(t, o);
}
}

View File

@@ -0,0 +1,127 @@
#if TOOLS
using System;
using System.Collections.Generic;
using System.Reflection;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Reflection;
using Robust.Xaml;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// The real implementation of <see cref="IXamlProxyManager"/>.
/// </summary>
public sealed class XamlProxyManager: IXamlProxyManager
{
ISawmill _sawmill = null!;
[Dependency] IReflectionManager _reflectionManager = null!;
[Dependency] ILogManager _logManager = null!;
XamlImplementationStorage _xamlImplementationStorage = null!;
List<Assembly> _knownAssemblies = [];
XamlJitCompiler? _xamlJitCompiler;
/// <summary>
/// Initialize this, subscribing to assembly changes.
/// </summary>
public void Initialize()
{
_sawmill = _logManager.GetSawmill("xamlhotreload");
_xamlImplementationStorage = new XamlImplementationStorage(_sawmill, Compile);
AddAssemblies();
_reflectionManager.OnAssemblyAdded += (_, _) => { AddAssemblies(); };
}
/// <summary>
/// Return true if setting the implementation of <paramref name="fileName" />
/// would not be a no-op.
/// </summary>
/// <param name="fileName">the file name</param>
/// <returns>true or false</returns>
public bool CanSetImplementation(string fileName)
{
return _xamlImplementationStorage.CanSetImplementation(fileName);
}
/// <summary>
/// Replace the implementation of <paramref name="fileName" />, failing
/// silently if the new content does not compile. (but still logging)
/// </summary>
/// <param name="fileName">the file name</param>
/// <param name="fileContent">the new content</param>
public void SetImplementation(string fileName, string fileContent)
{
_xamlImplementationStorage.SetImplementation(fileName, fileContent, false);
}
/// <summary>
/// Add all the types from all known assemblies, then force-JIT everything
/// again.
/// </summary>
private void AddAssemblies()
{
foreach (var a in _reflectionManager.Assemblies)
{
if (!_knownAssemblies.Contains(a))
{
_knownAssemblies.Add(a);
_xamlImplementationStorage.Add(a);
_xamlJitCompiler = null;
}
}
// Always use the JITed versions on debug builds
_xamlImplementationStorage.ForceReloadAll();
}
/// <summary>
/// Populate <paramref name="o" /> using the JIT compiler, if possible.
/// </summary>
/// <param name="t">the static type of <paramref name="o" /></param>
/// <param name="o">a <paramref name="t" /> instance or subclass</param>
/// <returns>true if there was a JITed implementation</returns>
public bool Populate(Type t, object o)
{
return _xamlImplementationStorage.Populate(t, o);
}
/// <summary>
/// Calls <see cref="XamlJitCompiler.Compile"/> using a stored
/// <see cref="XamlJitCompiler"/> instance.
/// </summary>
/// <param name="t">the <see cref="Type"/> that cares about this Xaml</param>
/// <param name="uri">the <see cref="Uri" /> of this xaml (from the type's metadata)</param>
/// <param name="fileName">the filename of this xaml (from the type's metadata)</param>
/// <param name="content">the new content of the xaml file</param>
/// <returns>the MethodInfo for the new JITed implementation</returns>
private MethodInfo? Compile(Type t, Uri uri, string fileName, string content)
{
// initialize XamlJitCompiler lazily because constructing it has
// very high CPU cost
XamlJitCompiler xjit;
lock(this)
{
xjit = _xamlJitCompiler ??= new XamlJitCompiler();
}
var result = xjit.Compile(t, uri, fileName, content);
if (result is XamlJitCompilerResult.Error e)
{
_sawmill.Info($"hot reloading failed: {t.FullName}; {fileName}; {e.Raw.Message} {e.Hint ?? ""}");
return null;
}
if (result is XamlJitCompilerResult.Success s)
{
return s.MethodInfo;
}
throw new InvalidOperationException($"totally unexpected result from compiler operation: {result}");
}
}
#endif

View File

@@ -0,0 +1,50 @@
using System;
namespace Robust.Client.UserInterface.XAML.Proxy;
/// <summary>
/// The stub implementation of <see cref="IXamlProxyManager"/>.
/// </summary>
public sealed class XamlProxyManagerStub: IXamlProxyManager
{
/// <summary>
/// Do nothing.
/// </summary>
public void Initialize()
{
}
/// <summary>
/// Return false. Nothing is ever interested in a Xaml content update when
/// hot reloading is off.
/// </summary>
/// <param name="fileName">the filename</param>
/// <returns>false</returns>
public bool CanSetImplementation(string fileName)
{
return false;
}
/// <summary>
/// Do nothing. A hot reload will always silently fail if hot reloading is off.
/// </summary>
/// <param name="fileName"></param>
/// <param name="fileContent"></param>
public void SetImplementation(string fileName, string fileContent)
{
}
/// <summary>
/// Return false.
/// </summary>
/// <remarks>
/// There will never be a JIT-ed implementation of Populate if hot reloading is off.
/// </remarks>
/// <param name="t">the static type of <paramref name="o" /></param>
/// <param name="o">an instance of <paramref name="t" /> or a subclass</param>
/// <returns>false</returns>
public bool Populate(Type t, object o)
{
return false;
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.IO;
using JetBrains.Annotations;
using Robust.Shared.IoC;
namespace Robust.Client.Utility
{
@@ -9,6 +8,12 @@ namespace Robust.Client.Utility
{
[Pure]
public static string GetUserDataDir(IGameControllerInternal gameController)
{
return Path.Combine(GetRootUserDataDir(gameController), "data");
}
[Pure]
public static string GetRootUserDataDir(IGameControllerInternal gameController)
{
string appDataDir;
@@ -30,8 +35,7 @@ namespace Robust.Client.Utility
appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
#endif
return Path.Combine(appDataDir, gameController.Options.UserDataDirectoryName, "data");
return Path.Combine(appDataDir, gameController.Options.UserDataDirectoryName);
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.ViewVariables.Instances;
@@ -36,7 +37,7 @@ namespace Robust.Client.ViewVariables.Traits
public override async void Refresh()
{
_memberList.DisposeAllChildren();
List<Control> replacementControls = [];
if (Instance.Object != null)
{
@@ -51,7 +52,7 @@ namespace Robust.Client.ViewVariables.Traits
foreach (var control in group)
{
_memberList.AddChild(control);
replacementControls.Add(control);
}
}
}
@@ -82,10 +83,16 @@ namespace Robust.Client.ViewVariables.Traits
selectorChain, o, r);
};
_memberList.AddChild(propertyEdit);
replacementControls.Add(propertyEdit);
}
}
}
_memberList.DisposeAllChildren();
foreach (var item in replacementControls)
{
_memberList.AddChild(item);
}
}
internal static void CreateMemberGroupHeader(ref bool first, string groupName, Control container)

View File

@@ -35,6 +35,7 @@ public static class Diagnostics
public const string IdDataFieldNoVVReadWrite = "RA0029";
public const string IdUseNonGenericVariant = "RA0030";
public const string IdPreferOtherType = "RA0031";
public const string IdDuplicateDependency = "RA0032";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");

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