Compare commits

...

98 Commits

Author SHA1 Message Date
PJB3005
6abe4eb29e Version: 255.0.1 2025-09-14 14:55:55 +02:00
PJB3005
9469e4f5ef 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)
2025-09-14 14:55:54 +02:00
ElectroJr
03f8d4d3e0 Version: 255.0.0 2025-04-25 17:32:45 +12:00
Leon Friedrich
728d541ca5 Remove assert in UserInterfaceManager.KeyBindDown (#5889) 2025-04-25 15:30:26 +10:00
Leon Friedrich
4cbce064b8 Fix grid fixtures using locale dependent ids (#5887) 2025-04-24 12:49:04 +02:00
IProduceWidgets
93bb7b1532 Sandbox whitelist for calendar stuff. (#5888) 2025-04-24 12:46:41 +02:00
Pieter-Jan Briers
3ba91d2ed0 Add cycle detection when setting MIDI renderer masters (#5879)
If these things ever get in a cycle (as they did in some SS14 replays),
it'll likely get the client stuck in an infinite loop. Avoid that.
2025-04-24 18:12:59 +10:00
Tayrtahn
8f9e0f6bab Fix warnings in PhysicsMap_Test (#5884) 2025-04-24 18:10:13 +10:00
Tayrtahn
3ed408de5d Fix warnings in Joints_Test (#5885) 2025-04-24 18:09:27 +10:00
DrSmugleaf
2f85408f8f Fix SharedPhysicsSystem.CollideContacts looping contacts that are in nullspace (#5881)
* Fix SharedJointSystem.CreateDistanceJoint taking in an int for minimumDistance instead of a float

* Fix SharedPhysicsSystem.CollideContacts looping contacts that are in nullspace
2025-04-22 22:50:56 +10:00
Leon Friedrich
f49b01b1b7 PredictedDeleteEntity fixes (#5873)
* PredictedDeleteEntity fixes

* release notes
2025-04-20 23:38:43 +10:00
Leon Friedrich
3ce764311d Make RobustIntegrationTest pool by default (#5872) 2025-04-20 17:53:55 +10:00
B_Kirill
adf0b6ae78 Fix CA2264 in Resource Manager (#5870) 2025-04-20 05:02:16 +02:00
metalgearsloth
04406311dc Use EntityQuery for map / grid calls (#5869)
Avoids the type lookup etc etc.
2025-04-20 12:59:06 +10:00
Kyle Tyo
ee45a608b9 Replace IsMap and IsGrid calls with HasComp (#5866) 2025-04-20 12:21:19 +10:00
metalgearsloth
077c91a54b Add autocomplete to scale command (#5864)
* Add autocomplete to scale command

* Also the scale

* this
2025-04-19 22:50:09 +10:00
metalgearsloth
2ac17009ee Deep-copy fixtures on client (#5863)
Fixes a LOTTA bugs
2025-04-19 19:31:36 +10:00
metalgearsloth
a73d1b6666 Update container log warning for pred spawns (#5860)
This was on the full branch I just forgot to pull it out. This is intended that they can go into containers.
2025-04-19 18:08:19 +10:00
metalgearsloth
e5d6f194be Add nullable PredictedDel overloads (#5859)
Forgor when I updated it to Entity<T>
2025-04-19 17:47:25 +10:00
Pieter-Jan Briers
72d893dec5 Add "obsolete inheritance" analyzer (#5858)
This allows us to make it obsolete to *inherit* from a class, and only that.

Intended so people stop inheriting UI controls for no good reason.

Fixes #5856
2025-04-19 17:29:17 +10:00
metalgearsloth
191d7ab81c Version: 254.1.0 2025-04-19 16:53:53 +10:00
metalgearsloth
65d2f2dd2f Entity spawn prediction v1 (#5841)
* Entity spawn prediction v1

Client can't properly interact with this but that requires additional work on top so we can cleanly split this.

* This

* delete fix

* cats
2025-04-19 16:50:41 +10:00
Dae
02b451db2a Add no derivatives licenses to rga validation (#5749) 2025-04-19 12:23:21 +10:00
Ed
d5d4584e11 Update Clyde.GridRendering.cs (#5852) 2025-04-19 00:01:16 +02:00
metalgearsloth
b146b1b82c Version: 254.0.0 2025-04-18 19:06:45 +10:00
Leon Friedrich
2f8f4f2f7a Make MappingDataNode use string keys (#5783)
* string keys

* obsoletions

* Fix ValueTupleSerializer

* Release notes

* fix release note conflict

* cleanup

* Fix yaml validator & tests

* cleanup release notes

* a

* enumerator allocations

* Also sequence enumerator alloc
2025-04-18 19:01:34 +10:00
Tayrtahn
7365a59bd9 Cleanup warnings in CollisionWake_Test (#5847) 2025-04-18 12:05:48 +10:00
Tayrtahn
37560f663b Add GetContainingContainers method to SharedContainerSystem (#5803)
* Add SharedContainerSystem.EnumerateContainingContainers

* More obvious name
2025-04-17 21:51:23 +10:00
TemporalOroboros
bd489e9218 A compilation of simple one-line fixes (#5661)
* Fix warnings in SharedJointSystem

* Fix reference to EC TransformComponent method
Replaces call to TransformComponent.GetMapUid with SharedTransformSystem.GetMap

* Fix obsolete calls in SharedPhysicsSystem.Contacts.cs
Fixes a couple calls to obsolete varients of SetAwake and an obsolete call to RegenerateContacts by converting them to their Entity<T> varients

* Fix obsolete call in SharedPhysicsSystem.Components.cs
Adds one set of parenthesis to convert a 'uid, comp, comp, comp' call to an 'Entity<T, T, T> call.

* Removes unused local var
Removes an unused list of broadphases that was being allocated in TryCollideRect

* One-line fixes in SharedPhysicsSystem.Islands.cs
Fixes all of the easy warnings regarding physics island processing, the rest require more complicated changes than a simple argument rearrangement

* Fix obsolete method call in SharedMapSystem

* Fix a few obsolete ToMap calls in EntityLookup.Queries

* Fix calls to obsolete EntityCoordinate methods in SharedMapSystem.Grids

* Fix calls to obsolete EntityCoordinate methods in SharedLookupSystem.ComponentQueries

* Fix a few obsolete method calls in entity spawning

* Fix obsolete method calls in MapLoaderSystem

* Fix obsolete method call in GridFixtureSystem

* Fix obsolete IsMapInitialized call in SaveMap command

* Fix obsolete MapPosition reference in Client.EyeSystem

* Fix obsolete EntitySystem.Get<TSystem> references in DebugLightTreeSystem

* Fix obsolete EntitySystem.Get<TSystem> reference in DebugEntityLookup command

* Fix obsolete method calls in SpriteBoundsOverlay
Slightly more complicated than the rest, but it's really just changing an unused dependency over to use SharedTransformSystem

* Remove unused IClyde references from controls
LineEdit and TextEdit never use their IClyde dependencies and it generates a warning so yeet

* Remove use of EntitySystem.Get from lightbb command

* Fix DebugDrawingSystem
Removes a bunch of unused private IEntityManager vars
Also removes an obsolete use of TransformComponent.GetWorldPositionRotation

* Removes duplicate position set when splitting grids
There's nothing saying why this is this way and the blame looks like it was an oversight when replacing a bit where they set position and then rotation
Please, oh Chesterton's Fence, spare me your wrath

* Fix obsolete method use in PlacementMode

* Fix obsolete method use in Placement Modes

* Removes unused local var in gamestate management

* Fix unreachable code warnings in gamestate management
Use #else sections to make sure they don't complain about being on the wrong side of a throw

* Fix obsolete ToMap use in EyeManager

* Make InputManager use a sawmill to log

* Fix obsolete ContainerManagerComponent method calls in ContainerSystem

* Make ClientPrototypeManager use a sawmill for logging

* poke tests

* Use LocalizedEntityCommands for SpriteBoundsOverlay toggle

* Use LocalizedEntityCommands for system toggles

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2025-04-17 11:57:03 +10:00
metalgearsloth
33166e8866 Component lifecycle generics (#5844)
Non-breaking minor.
2025-04-16 21:46:14 +10:00
Leon Friedrich
ed16032280 Disable tile edges (#5842) 2025-04-16 21:25:31 +10:00
metalgearsloth
cf785c886b Mark GetCollidingEntities as obsolete (#5831)
Bad methods, entitylookup is better (it does everything for area queries) and anyone who wants contacts should be querying those directly.
2025-04-14 18:50:01 +10:00
metalgearsloth
6f28c396cf Version: 253.0.0 2025-04-14 14:12:32 +10:00
metalgearsloth
b631f408f2 ItemList optimisation (#5796)
- VV AddComponent window no longer takes 300ms every time you press a key.
- Significantly optimise ItemList internally.
2025-04-14 13:55:48 +10:00
Leon Friedrich
34f4cf9452 More map validator fixes (#5820) 2025-04-14 13:55:02 +10:00
B_Kirill
6a4e4cf3b4 Cleanup warnings: Commands (#5825) 2025-04-14 13:54:42 +10:00
metalgearsloth
8aefa5c53e Make TestPoint use generics (#5828) 2025-04-14 01:49:07 +10:00
DrSmugleaf
2cfc981aa3 Fix popup text overflowing the sides of the screen (#5788)
* Fix popup text overflowing the sides of the screen

* Fix not escaping text
2025-04-13 12:59:23 +02:00
Leon Friedrich
4987c324d9 Fix bad debug assert (#5826) 2025-04-13 20:50:46 +10:00
DrSmugleaf
5450ddd0ba Fix SharedJointSystem.CreateDistanceJoint taking in an int for minimumDistance instead of a float (#5824) 2025-04-13 16:22:22 +10:00
Tayrtahn
378a10678c Improve location reporting for non-writeable DataFields (#5715)
* Better location reporting for readonly DataField errors

* Better location reporting for readonly DataField property errors

* Use SyntaxKind instead of string for TryGetModifierLocation
2025-04-13 16:22:05 +10:00
Leon Friedrich
2e0735b92f Fix NRE in screen-space overlays (#5823)
* Fix pre-init screen-space overlays

* Debug asserts
2025-04-13 16:21:02 +10:00
Leon Friedrich
5756d15333 Make BoundUserInterfaceMessageAttempt broadcast again (#5821) 2025-04-12 18:22:13 +10:00
Leon Friedrich
b6f74b8dea Fix logMissing in EntitySystem.Resolve() (#5822) 2025-04-12 17:40:08 +10:00
Leon Friedrich
3800c5707e Add new SerializationManager.PushComposition overload (#5785) 2025-04-12 12:58:40 +10:00
Leon Friedrich
8f49785b4e Fix RemCompDeferred not always setting lifestage (#5786)
* Fix RemCompDeferred not setting lifestage

* spaelling
2025-04-12 12:57:11 +10:00
metalgearsloth
f274de0f10 Version: 252.0.0 2025-04-12 01:43:29 +10:00
Milon
e128338f9d hi (#5811) 2025-04-12 01:29:41 +10:00
metalgearsloth
588c46273e Version: 251.0.0 2025-04-10 20:53:09 +10:00
Whatstone
919de8ce0e SharedPhysicsSystem.Island: set position after velocity (#5801) 2025-04-09 01:10:17 +10:00
Errant
af27d2d872 equatable FormattedMessage, take 2 (#5780)
* improved Equals and getHashCode

* less jank
2025-04-08 16:50:08 +02:00
Tayrtahn
45bb8740a0 Add ForbidLiteralAttribute and analyzer (#5808)
* Add ForbidLiteral attribute, analyzer, and test

* Removed unused code

* Switch order of methods. It's better this way.
2025-04-08 16:39:38 +02:00
Tobias Berger
4a24539629 Don't implement GetMassData twice (#5816)
The removed body didn't calculate mass for circles
2025-04-09 00:14:45 +10:00
PJB3005
7536c4ec68 Log late MsgEntity again
Yay :)
2025-04-07 15:50:40 +02:00
metalgearsloth
9268c8629d Refactor TileEdgeOverlay (#5295)
* Refactor TileEdgeOverlay

* weh

* Updates

* exweh

* Stupid weh noises

* I am le stupid

* Add logging about tile atlas build time

Seems fine, but wanted to check.

* Don't over-allocate edge tile region array

* Add DirectionExtensions.AllDirections

* Clean up iteration in ClydeTileDefinitionManager

* Don't stackalloc large chunk edge buffers, other code cleanup.

* More release notes

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-04-03 07:52:28 +02:00
PJB3005
0bc0cafe64 Show entity name in "physics shapeinfo" output 2025-04-03 02:59:09 +02:00
PJB3005
8891f3fa0a Make EntitySystem.Subscriptions.SubscribeLocalEvent not require EntityEventArgs
This means it can be used with struct events.
2025-04-03 02:58:20 +02:00
Tornado Tech
4f96c2d233 Added separate localization & clean up (#5227)
* Added separate localization & clean up

* Added new methods docs

* Added GetFoundCultures method

* Clean up code

* Removed some formating shit

* Do better CultureInfo comparison

* Oops

* Review

* Command fixes

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-04-02 16:41:02 +02:00
PJB3005
ab55d5b2f2 Revert "Add Loc property to LocalizedCommands"
This reverts commit 806c5b694b.
2025-04-02 05:23:43 +02:00
PJB3005
806c5b694b Add Loc property to LocalizedCommands
Avoids some ~300 usages of static Loc in SS14 and RT
2025-04-02 05:21:43 +02:00
metalgearsloth
6898053dbd Add autocomplete to tp command (#5795) 2025-04-02 03:48:16 +02:00
metalgearsloth
ae625ebad8 Inline manifold points (#5794)
* Inline manifold points

Max is 2 so no reason to use an array over a fixedarray.

* this
2025-04-02 00:04:51 +11:00
metalgearsloth
3c754a4f49 Don't disable contacting collisionwake ents (#5798)
Good for some content stuff I don't think it caused issues.
2025-04-01 15:04:05 +11:00
Tayrtahn
d84cb6327c Fix SharedTransformSystem methods erroring on failed Resolves (#5787)
* Don't error when GetGrid fails

* Fix other Resolves in SharedTransformSystem
2025-04-01 04:59:47 +11:00
Ciarán Walsh
4bfd92dbc5 Add button to jump to live chat when scrolled up (#5750)
* Add button to jump to live chat when scrolled up

* Expose scroll button class name as a public constant

* Add localisation string to engine

* Make enabling the OutputPanel scroll button opt-in

* Enable scroll button for the debug console

* De-duplicate visibility logic

* Update scroll button visibility when the enabling property is changed
2025-03-30 03:07:54 +02:00
metalgearsloth
c7d228c223 savemap / savegrid autocomplete (#5784)
How mappers coping without this.
2025-03-27 18:54:13 +11:00
Tayrtahn
f244c94905 Switch from checking comp.Owner == ent to GetComp(ent) == comp (#5776) 2025-03-27 15:21:05 +11:00
Tayrtahn
01cac6465b Cleanup warnings in EntityCoordinates_Tests (#5771)
* Fix warnings

* Use transform refs to simplify WithEntityId
2025-03-27 15:20:20 +11:00
metalgearsloth
5a5f238d9a Version: 250.0.0 2025-03-27 15:12:01 +11:00
Tayrtahn
089224cd44 Cleanup warnings in EntityLookup_Test (#5775)
* Replace MapManager.DeleteMap calls with MapSystem.DeleteMap

* Remove unused resolves
2025-03-27 15:07:05 +11:00
Tayrtahn
9f807f1ad2 Cleanup warnings in ClientGameStateManager (#5774)
* Fix unreachable code

* Remove unused variable
2025-03-27 15:06:37 +11:00
metalgearsloth
4be95ea375 Add OtherBody API to contacts (#5779)
* Add OtherBody API to contacts

Thought I had a pr for this.

* Update Robust.Shared/Physics/Dynamics/Contacts/Contact.cs

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2025-03-27 15:06:04 +11:00
Jerry
03010bf4be Fix DirectoryNotFoundException when saving map or grid on Unix systems (#5773)
* fix(maploader): Fix DirectoryNotFoundException

Someone forgor to create directory before saving map or grid, this caused
an exception on trying to save smth anywhere else than /

* update release notes
2025-03-27 13:56:26 +11:00
Tayrtahn
dacaa974d4 Replace MapManager.DeleteMap with SharedMapSystem.DeleteMap in misc tests (#5777)
* Replace MapManager.DeleteMap with SharedMapSystem.DeleteMap in various tests

* Poke tests

* I guess this is was technically a breaking change?
2025-03-27 13:50:56 +11:00
DrSmugleaf
9f73e0398a Make MappingDataNode.Equals take 2952 times less time to run when loading maps (#5781) 2025-03-27 12:52:29 +11:00
Tayrtahn
ccc383b1bf Cleanup: Remove redundant Prototype names (#5721)
* Cleanup: Remove redundant Prototype names

* Actually this one probably should stay

* Suppress warning

* Remove warning suppression on AudioMetadataPrototype

* But wait, there's more!
2025-03-26 01:39:10 +01:00
PJB3005
ceb59402a1 Make status and info APIs have CORS allow-origin: *
Allows it to be queried from browser JS. No harm in not allowing this.

Added helper function StatusExt.AddAllowOriginAny to make this easy to add.
2025-03-26 01:16:23 +01:00
Errant
5a6b29fcd2 equatable FormattedMessage (#5772) 2025-03-25 15:32:47 +01:00
PJB3005
6b87cd1e1c Deprecate HCY color space functions
These functions use the wrong color primaries (Rec 601 instead of 709) to calculate the luminance, so I'm just gonna throw them out.

Also, I don't actually trust anybody to know to do sRGB conversion before using them.

Discovered this as a result of reviewing #5360
2025-03-24 04:47:34 +01:00
Leon Friedrich
cf2d6a1dbf Make unshaded sprite layers not require shader/texture changes. (#5248)
* Make unshaded sprite layers not require shader/texture changes.

* Always sample texture

* revert some variable name changes

* Use color SIMD
2025-03-24 02:37:04 +01:00
chromiumboy
f8c838f425 Pass AnimationPlayerComponent in AnimationCompletionEvent (#5755)
* Added a field for the animation player component to the animation completion event

* Addressed review comment

* Updated
2025-03-24 02:22:46 +01:00
Tayrtahn
7405904041 Add entity description as tooltip on entity spawn panel (#5761) 2025-03-23 14:07:22 +01:00
metalgearsloth
2eeebab275 Add pure to some angle methods (#5763)
Saw some of these floating around in mover code.
2025-03-22 13:43:18 +01:00
PJB3005
2856bb3626 Shut up RS1038 warnings
We aren't going to fix these until #5610 is figured out, and these aren't even an indicator of an issue itself.

They indicate that we have code fixes in the same assembly as analyzers, meaning the analyzers COULD fail if we relied on some code fix libs - something we don't do.
2025-03-22 06:20:43 +01:00
PJB3005
be0189748b Fix serialization source gen with partial types
This fixes RMC compilation
2025-03-22 06:09:48 +01:00
Tayrtahn
4529a7569a Replace uses of ProtoId<EntityPrototype> with ProtoId<EntityCategoryPrototype> (#5762) 2025-03-21 04:08:12 +01:00
metalgearsloth
2b16e4db96 Version: 249.0.0 2025-03-21 00:52:45 +11:00
metalgearsloth
64f2245194 Add UpdateVisibilityMask method (#5745)
* Add UpdateVisibilityMask method

We tipped over to the point of systems stepping on each other's toes. Now we do the normal thing and just use the eventbus and it makes content a whole lot cleaner.

* Update resolve

* Update name in line with normal.

* Unserialize this

* weh
2025-03-21 00:47:40 +11:00
Milon
1029047e2f fix (#5752) 2025-03-20 22:30:59 +11:00
metalgearsloth
45dc9ad80e Inline polygon vertices (#5758)
* FastPoly

* Inline polygon vertices

No more pooling, pooling bad. No more arrays in engine, only span.

I made a slim version that's only the 4 verts so no padding on it compared to Polygon. Slightly more clamplicated code but entitylookup + mapmanager are both hotpaths and it's easy to do. Memory usage will likely go up for now but heap allocations should drop significantly due to removing the pooling.

* Unhide these

* Fixes

* More fixes

* More fixes

* Avoid potential bomb
2025-03-20 21:26:05 +11:00
metalgearsloth
54ad808eea Add GetWorldManifold overload (#5756)
* Add GetWorldManifold overload

* revert
2025-03-20 21:10:04 +11:00
metalgearsloth
37c75df6a2 Fix showvelocities (#5759)
* Fix showvelocities

Can't use StateRoot anymore so just pretend it's a window.

* Also file-scoped
2025-03-20 21:05:58 +11:00
slarticodefast
e93c1fae61 Add velocity and angular velocity debug overlays (#5693)
* add velocity and angular velocity debug overlays

* minor improvement

* add descriptions

* fix

* review

* Update RELEASE-NOTES.md

* Update RELEASE-NOTES.md

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-03-20 13:10:16 +11:00
metalgearsloth
cd0a35f542 Fix light Aabb query (#5744)
Forgot when this got dropped but we need it for grid-tree query as lights spill over grids.
2025-03-13 19:18:23 +11:00
PJB3005
80f2dc6dd3 Fix BoxContainer stretching causing controls to be made too small
In which I fix a bug by just deleting a ton of code and doing nothing else.

(also I added unit tests)
2025-03-13 01:01:18 +01:00
metalgearsloth
139b6f796c Version: 248.0.2 2025-03-12 20:26:50 +11:00
metalgearsloth
2ee7c35fd3 Don't throw on invalid MapUids for overlays (#5740) 2025-03-12 20:16:13 +11:00
metalgearsloth
56eae5ad08 Reduce EntityManager.IsDefault allocations (#5741)
* Reduce EntityManager.IsDefault allocations

We don't actually need the MappingDataNode so we can avoid allocating the entire class per entity.

* Adjust this order
2025-03-12 19:42:08 +11:00
246 changed files with 4592 additions and 1659 deletions

View File

@@ -57,7 +57,7 @@
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<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="TerraFX.Interop.Windows" Version="10.0.26100.1" />

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

@@ -1,4 +1,4 @@
# Release notes for RobustToolbox.
# Release notes for RobustToolbox.
<!--
NOTE: automatically updated sometimes by version.py.
@@ -54,8 +54,205 @@ END TEMPLATE-->
*None yet*
## 255.0.1
## 255.0.0
### Breaking changes
* `RobustIntegrationTest` now pools server/client instances by default. If a custom settings class is provided, it will still disable pooling unless explicitly enabled.
* Server/Client instances that are returned to the pool should be disconnected. This might require you to update some tests.
* Pooled instances also require you to use `RobustIntegrationTest` methods like `WaitPost()` to ensure the correct thread is used.
### Bugfixes
* Fix `EntityDeserializer` improperly setting entity lifestages when loading a post-mapinit map.
* Fix `EntityManager.PredictedDeleteEntity()` not deleting pure client-side entities.
* Fix grid fixtures using a locale dependent id. This could cause some clients to crash/freeze when connected to a server with a different locale.
### Other
* Add logic to block cycles in master MIDI renderers, which could otherwise cause client freezes.
## 254.1.0
### New features
* Add CC ND licences to the RGA validator.
* Add entity spawn prediction and entity deletion prediction. This is currently limited as you are unable to predict interactions with these entities. These are done via the new methods prefixed with "Predicted". You can also manually flag an entity as a predicted spawn with the `FlagPredicted` method which will clean it up when prediction is reset.
### Bugfixes
* Fix tile edge rendering for neighbor tiles being the same priority.
### Other
* Fix SpawnAttachedTo's system proxy method not the rotation arg like EntityManager.
## 254.0.0
### Breaking changes
* Yaml mappings/dictionaries now only support string keys instead of generic nodes
* Several MappingDataNode method arguments or return values now use strings instead of a DataNode object
* The MappingDataNode class has various helper methods that still accept a ValueDataNode, but these methods are marked as obsolete and may be removed in the future.
* yaml validators should use `MappingDataNode.GetKeyNode()` when validating mapping keys, so that errors can print node start & end information
* ValueTuple yaml serialization has changed
* Previously they would get serialized into a single mapping with one entry (i.e., `{foo : bar }`)
* Now they serialize into a sequence (i.e., `[foo, bar]`)
* The ValueTuple serializer will still try to read mappings, but due to the MappingDataNode this may fail if the previously serialized "key" can't be read as a simple string
### New features
* Add cvar to disable tile edges.
* Add GetContainingContainers method to ContainerSystem to recursively get containers upwards on an entity.
### Internal
* Make component lifecycle methods use generics.
## 253.0.0
### New features
* Add a new `SerializationManager.PushComposition()` overload that takes in a single parent instead of an array of parents.
* `BoundUserInterfaceMessageAttempt` once again gets raised as a broadcast event, in addition to being directed.
* This effectively reverts the breaking part of the changes made in v252.0.0
* Fix CreateDistanceJoint using an int instead of a float for minimum distance.
### Bugfixes
* Fix deferred component removal not setting the component's life stage to `ComponentLifeStage.Stopped` if the component has not yet been initialised.
* Fix some `EntitySystem.Resolve()` overloads not respecting the optional `logMissing` argument.
* Fix screen-space overlays not being useable without first initializing/starting entity manager & systems
* ItemList is now significantly optimized. VV's `AddComponent` window in particular should be much faster.
* Fix some more MapValidator fields.
* Fix popup text overflowing the sides of the screen.
* Improve location reporting for non-writeable datafields via analyzer.
### Other
* TestPoint now uses generics rather than IPhysShape directly.
## 252.0.0
### Breaking changes
* BoundUserInterfaceMessageAttempt is raised directed against entities and no longer broadcast.
## 251.0.0
### Breaking changes
* Localization is now separate between client and server and is handled via cvar.
* Contacting entities no longer can be disabled for CollisionWake to avoid destroying the contacts unnecessarily.
### New features
* Added `DirectionExtensions.AllDirections`, which contains a list of all `Direction`s for easy enumeration.
* Add ForbidLiteralAttribute.
* Log late MsgEntity again.
* Show entity name in `physics shapeinfo` output.
* Make SubscribeLocalEvent not require EntityEventArgs.
* Add autocomplete to `tp` command.
* Add button to jump to live chat when scrolled up.
* Add autocomplete to `savemap` and `savegrid`.
### Bugfixes
* Fix velocity not re-applying correctly on re-parenting.
* Fix Equatable on FormattedMessage.
* Fix SharedTransformSystem methods logging errors on resolves.
### Other
* Significantly optimized tile edge rendering.
### Internal
* Remove duplicate GetMassData method.
* Inline manifold points for physics.
## 250.0.0
### Breaking changes
* The default shader now interprets negative color modulation as a flag that indicates that the light map should be ignored.
* This can be used to avoid having to change the light map texture, thus reducing draw batches.
* Sprite layers that are set to use the "unshaded" shader prototype now use this.
* Any fragment shaders that previously the `VtxModulate` colour modulation variable should instead use the new `MODULATE` variable, as the former may now contain negative values.
### New features
* Add OtherBody API to contacts.
* Make FormattedMessages Equatable.
* AnimationCompletionEvent now has the AnimationPlayerComponent.
* Add entity description as a tooltip on the entity spawn panel.
### Bugfixes
* Fix serialization source generator breaking if a class has two partial locations.
* Fix map saving throwing a `DirectoryNotFoundException` when given a path with a non-existent directory. Now it once again creates any missing directories.
* Fix map loading taking a significant time due to MappingDataNode.Equals calls being slow.
### Other
* Add Pure to some Angle methods.
### Internal
* Cleanup some warnings in classes.
## 249.0.0
### Breaking changes
* Layer is now read-only on VisibilityComponent and isn't serialized.
### New features
* Added a debug overlay for the linear and angular velocity of all entities on the screen. Use the `showvel` and `showangvel` commands to toggle it.
* Add a GetWorldManifold overload that doesn't require a span of points.
* Added a GetVisMaskEvent. Calling `RefreshVisibilityMask` will raise it and subscribers can update the vismask via the event rather than subscribers having to each manually try and handle the vismask directly.
### Bugfixes
* `BoxContainer` no longer causes stretching children to go below their minimum size.
* Fix lights on other grids getting clipped due to ignoring the light range cvar.
* Fix the `showvelocities` command.
* Fix the DirtyFields overload not being sandbox safe for content.
### Internal
* Polygon vertices are now inlined with FixedArray8 and a separate SlimPolygon using FixedArray4 for hot paths rather than using pooled arrays.
## 248.0.2
### Bugfixes
* Don't throw in overlay rendering if MapUid not found.
### Internal
* Reduce EntityManager.IsDefault allocations.
## 248.0.1
### Bugfixes
* Bump ImageSharp version.
* Fix instances of NaN gain for audio where a negative-infinity value is being used for volume.
## 248.0.0

View File

@@ -1,5 +1,7 @@
### Localization for engine console commands
cmd-hint-float = [float]
## generic command errors
cmd-invalid-arg-number-error = Invalid number of arguments.
@@ -11,6 +13,7 @@ cmd-parse-failure-uid = {$arg} is not a valid entity UID.
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
cmd-parse-failure-enum = {$arg} is not a {$enum} Enum.
cmd-parse-failure-grid = {$arg} is not a valid grid.
cmd-parse-failure-cultureinfo = "{$arg}" is not valid CultureInfo.
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
cmd-parse-failure-session = There is no session with username: {$username}
@@ -428,11 +431,20 @@ cmd-entfo-help = Usage: entfo <entityuid>
The entity UID can be prefixed with 'c' to convert it to a client entity UID.
cmd-fuck-desc = Throws an exception
cmd-fuck-help = Throws an exception
cmd-fuck-help = Usage: fuck
cmd-showpos-desc = Enables debug drawing over all entity positions in the game.
cmd-showpos-desc = Show the position of all entities on the screen.
cmd-showpos-help = Usage: showpos
cmd-showrot-desc = Show the rotation of all entities on the screen.
cmd-showrot-help = Usage: showrot
cmd-showvel-desc = Show the local velocity of all entites on the screen.
cmd-showvel-help = Usage: showvel
cmd-showangvel-desc = Show the angular velocity of all entities on the screen.
cmd-showangvel-help = Usage: showangvel
cmd-sggcell-desc = Lists entities on a snap grid cell.
cmd-sggcell-help = Usage: sggcell <gridID> <vector2i>\nThat vector2i param is in the form x<int>,y<int>.
@@ -563,3 +575,8 @@ cmd-pvs-override-info-desc = Prints information about any PVS overrides associat
cmd-pvs-override-info-empty = Entity {$nuid} has no PVS overrides.
cmd-pvs-override-info-global = Entity {$nuid} has a global override.
cmd-pvs-override-info-clients = Entity {$nuid} has a session override for {$clients}.
cmd-localization_set_culture-desc = Set DefaultCulture for the client LocalizationManager
cmd-localization_set_culture-help = Usage: localization_set_culture <cultureName>
cmd-localization_set_culture-culture-name = <cultureName>
cmd-localization_set_culture-changed = Localization changed to { $code } ({ $nativeName } / { $englishName })

View File

@@ -14,6 +14,10 @@ tile-spawn-window-title = Place Tiles
console-line-edit-placeholder = Command Here
## OutputPanel
output-panel-scroll-down-button-text = Scroll Down
## Common Used
window-erase-button-text = Erase Mode

View File

@@ -87,4 +87,66 @@ public sealed class DataDefinitionAnalyzerTest
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldNoVVReadWriteRule).WithSpan(35, 17, 35, 50).WithArguments("Bad", "Foo")
);
}
[Test]
public async Task ReadOnlyFieldTest()
{
const string code = """
using System;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.Serialization.Manager.Attributes
{
public class DataFieldBaseAttribute : Attribute;
public class DataFieldAttribute : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
}
[DataDefinition]
public sealed partial class Foo
{
[DataField]
public readonly int Bad;
[DataField]
public int Good;
}
""";
await Verifier(code,
// /0/Test0.cs(15,12): error RA0019: Data field Bad in data definition Foo is readonly
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldWritableRule).WithSpan(15, 12, 15, 20).WithArguments("Bad", "Foo")
);
}
[Test]
public async Task ReadOnlyPropertyTest()
{
const string code = """
using System;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.Serialization.Manager.Attributes
{
public class DataFieldBaseAttribute : Attribute;
public class DataFieldAttribute : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
}
[DataDefinition]
public sealed partial class Foo
{
[DataField]
public int Bad { get; }
[DataField]
public int Good { get; private set; }
}
""";
await Verifier(code,
// /0/Test0.cs(15,20): error RA0020: Data field property Bad in data definition Foo does not have a setter
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldPropertyWritableRule).WithSpan(15, 20, 15, 28).WithArguments("Bad", "Foo")
);
}
}

View File

@@ -0,0 +1,189 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ForbidLiteralAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
public sealed class ForbidLiteralAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<ForbidLiteralAnalyzer, DefaultVerifier>()
{
TestState =
{
Sources = { code },
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Analyzers.ForbidLiteralAttribute.cs"
);
test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
private const string TestTypeDefs = """
using System.Collections.Generic;
using Robust.Shared.Analyzers;
public sealed class TestClass
{
public static void OneParameterForbidden([ForbidLiteral] string value) { }
public static void TwoParametersFirstForbidden([ForbidLiteral] string first, string second) { }
public static void TwoParametersBothForbidden([ForbidLiteral] string first, [ForbidLiteral] string second) { }
public static void ListParameterForbidden([ForbidLiteral] List<string> values) { }
public static void ParamsListParameterForbidden([ForbidLiteral] params List<string> values) { }
}
public record struct StringWrapper(string value)
{
private readonly string _value = value;
public static implicit operator string(StringWrapper wrapper)
{
return wrapper._value;
}
}
""";
[Test]
public async Task TestOneParameter()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
private static readonly StringWrapper WrappedValue = new("biz");
public void Test()
{
TestClass.OneParameterForbidden(_constValue);
TestClass.OneParameterForbidden(StaticValue);
TestClass.OneParameterForbidden(WrappedValue);
TestClass.OneParameterForbidden("baz");
}
}
""";
await Verifier(code,
// /0/Test0.cs(12,41): error RA0033: The "value" parameter of OneParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(12, 41, 12, 46).WithArguments("value", "OneParameterForbidden")
);
}
[Test]
public async Task TestTwoParametersFirstForbidden()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
public void Test()
{
TestClass.TwoParametersFirstForbidden(_constValue, "whatever");
TestClass.TwoParametersFirstForbidden(_constValue, _constValue);
TestClass.TwoParametersFirstForbidden("foo", "whatever");
}
}
""";
await Verifier(code,
// /0/Test0.cs(9,47): error RA0033: The "first" parameter of TwoParametersFirstForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(9, 47, 9, 52).WithArguments("first", "TwoParametersFirstForbidden")
);
}
[Test]
public async Task TestTwoParametersBothForbidden()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
public void Test()
{
TestClass.TwoParametersBothForbidden(_constValue, _constValue);
TestClass.TwoParametersBothForbidden(_constValue, StaticValue);
TestClass.TwoParametersBothForbidden(_constValue, "whatever");
TestClass.TwoParametersBothForbidden("whatever", _constValue);
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,59): error RA0033: The "second" parameter of TwoParametersBothForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 59, 10, 69).WithArguments("second", "TwoParametersBothForbidden"),
// /0/Test0.cs(11,46): error RA0033: The "first" parameter of TwoParametersBothForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(11, 46, 11, 56).WithArguments("first", "TwoParametersBothForbidden")
);
}
[Test]
public async Task TestListParameter()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
private static readonly StringWrapper WrappedValue = new("biz");
public void Test()
{
TestClass.ListParameterForbidden([_constValue, StaticValue, WrappedValue]);
TestClass.ListParameterForbidden(["foo", _constValue, "bar"]);
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,43): warning RA0033: The "values" parameter of ListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 43, 10, 48).WithArguments("values", "ListParameterForbidden"),
// /0/Test0.cs(10,63): warning RA0033: The "values" parameter of ListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 63, 10, 68).WithArguments("values", "ListParameterForbidden")
);
}
[Test]
public async Task TestParamsListParameter()
{
const string code = """
public sealed class Tester
{
private const string _constValue = "foo";
private static readonly string StaticValue = "bar";
private static readonly StringWrapper WrappedValue = new("biz");
public void Test()
{
TestClass.ParamsListParameterForbidden(_constValue, StaticValue, WrappedValue);
TestClass.ParamsListParameterForbidden("foo", _constValue, "bar");
}
}
""";
await Verifier(code,
// /0/Test0.cs(10,48): warning RA0033: The "values" parameter of ParamsListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 48, 10, 53).WithArguments("values", "ParamsListParameterForbidden"),
// /0/Test0.cs(10,68): warning RA0033: The "values" parameter of ParamsListParameterForbidden forbids literal values
VerifyCS.Diagnostic().WithSpan(10, 68, 10, 73).WithArguments("values", "ParamsListParameterForbidden")
);
}
}

View File

@@ -0,0 +1,86 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ObsoleteInheritanceAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
/// <summary>
/// Analyzer that implements <c>[ObsoleteInheritance]</c> checking, to give obsoletion warnings for inheriting types
/// that should never have been virtual.
/// </summary>
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
public sealed class ObsoleteInheritanceAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new CSharpAnalyzerTest<ObsoleteInheritanceAnalyzer, DefaultVerifier>()
{
TestState =
{
Sources = { code },
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs"
);
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task TestBasic()
{
const string code = """
using Robust.Shared.Analyzers;
[ObsoleteInheritance]
public class Base;
public class NotAllowed : Base;
""";
await Verifier(code,
// /0/Test0.cs(6,14): warning RA0034: Type 'NotAllowed' inherits from 'Base', which has obsoleted inheriting from itself
VerifyCS.Diagnostic(ObsoleteInheritanceAnalyzer.Rule).WithSpan(6, 14, 6, 24).WithArguments("NotAllowed", "Base")
);
}
[Test]
public async Task TestMessage()
{
const string code = """
using Robust.Shared.Analyzers;
[ObsoleteInheritance("Sus")]
public class Base;
public class NotAllowed : Base;
""";
await Verifier(code,
// /0/Test0.cs(6,14): warning RA0034: Type 'NotAllowed' inherits from 'Base', which has obsoleted inheriting from itself: "Sus"
VerifyCS.Diagnostic(ObsoleteInheritanceAnalyzer.RuleWithMessage).WithSpan(6, 14, 6, 24).WithArguments("NotAllowed", "Base", "Sus")
);
}
[Test]
public async Task TestNormal()
{
const string code = """
public class Base;
public class AllowedAllowed : Base;
""";
await Verifier(code);
}
}

View File

@@ -13,6 +13,8 @@
<EmbeddedResource Include="..\Robust.Shared\Analyzers\MustCallBaseAttribute.cs" LogicalName="Robust.Shared.IoC.MustCallBaseAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferNonGenericVariantForAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferOtherTypeAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferOtherTypeAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ForbidLiteralAttribute.cs" LogicalName="Robust.Shared.Analyzers.ForbidLiteralAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ObsoleteInheritanceAttribute.cs" LogicalName="Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\GameObjects\EventBusAttributes.cs" LogicalName="Robust.Shared.GameObjects.EventBusAttributes.cs" LinkBase="Implementations" />
</ItemGroup>

View File

@@ -41,7 +41,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to mark any type containing a nested data definition as partial."
);
private static readonly DiagnosticDescriptor DataFieldWritableRule = new(
public static readonly DiagnosticDescriptor DataFieldWritableRule = new(
Diagnostics.IdDataFieldWritable,
"Data field must not be readonly",
"Data field {0} in data definition {1} is readonly",
@@ -51,7 +51,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
"Make sure to remove the readonly modifier."
);
private static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
public static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
Diagnostics.IdDataFieldPropertyWritable,
"Data field property must have a setter",
"Data field property {0} in data definition {1} does not have a setter",
@@ -149,7 +149,8 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (IsReadOnlyDataField(type, fieldSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, context.Node.GetLocation(), fieldSymbol.Name, type.Name));
TryGetModifierLocation(field, SyntaxKind.ReadOnlyKeyword, out var location);
context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, location, fieldSymbol.Name, type.Name));
}
if (HasRedundantTag(fieldSymbol))
@@ -185,7 +186,8 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
if (IsReadOnlyDataField(type, propertySymbol))
{
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, context.Node.GetLocation(), propertySymbol.Name, type.Name));
var location = property.AccessorList != null ? property.AccessorList.GetLocation() : property.GetLocation();
context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, location, propertySymbol.Name, type.Name));
}
if (HasRedundantTag(propertySymbol))
@@ -285,6 +287,20 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
return false;
}
private static bool TryGetModifierLocation(MemberDeclarationSyntax syntax, SyntaxKind modifierKind, out Location location)
{
foreach (var modifier in syntax.Modifiers)
{
if (modifier.IsKind(modifierKind))
{
location = modifier.GetLocation();
return true;
}
}
location = syntax.GetLocation();
return false;
}
private static bool IsReadOnlyMember(ITypeSymbol type, ISymbol member)
{
if (member is IFieldSymbol field)

View File

@@ -0,0 +1,101 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ForbidLiteralAnalyzer : DiagnosticAnalyzer
{
private const string ForbidLiteralType = "Robust.Shared.Analyzers.ForbidLiteralAttribute";
public static DiagnosticDescriptor ForbidLiteralRule = new(
Diagnostics.IdForbidLiteral,
"Parameter forbids literal values",
"The {0} parameter of {1} forbids literal values",
"Usage",
DiagnosticSeverity.Warning,
true,
"Pass in a validated wrapper type like ProtoId, or a const or static value."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [ForbidLiteralRule];
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterOperationAction(AnalyzeOperation, OperationKind.Invocation);
}
private void AnalyzeOperation(OperationAnalysisContext context)
{
if (context.Operation is not IInvocationOperation invocationOperation)
return;
// Check each parameter of the method invocation
foreach (var argumentOperation in invocationOperation.Arguments)
{
// Check for our attribute on the parameter
if (!AttributeHelper.HasAttribute(argumentOperation.Parameter, ForbidLiteralType, out _))
continue;
// Handle parameters using the params keyword
if (argumentOperation.Syntax is InvocationExpressionSyntax subExpressionSyntax)
{
// Check each param value
foreach (var subArgument in subExpressionSyntax.ArgumentList.Arguments)
{
CheckArgumentSyntax(context, argumentOperation, subArgument);
}
continue;
}
// Not params, so just check the single parameter
if (argumentOperation.Syntax is not ArgumentSyntax argumentSyntax)
continue;
CheckArgumentSyntax(context, argumentOperation, argumentSyntax);
}
}
private void CheckArgumentSyntax(OperationAnalysisContext context, IArgumentOperation operation, ArgumentSyntax argumentSyntax)
{
// Handle collection types
if (argumentSyntax.Expression is CollectionExpressionSyntax collectionExpressionSyntax)
{
// Check each value of the collection
foreach (var elementSyntax in collectionExpressionSyntax.Elements)
{
if (elementSyntax is not ExpressionElementSyntax expressionSyntax)
continue;
// Check if a literal was passed in
if (expressionSyntax.Expression is not LiteralExpressionSyntax)
continue;
context.ReportDiagnostic(Diagnostic.Create(ForbidLiteralRule,
expressionSyntax.GetLocation(),
operation.Parameter.Name,
(context.Operation as IInvocationOperation).TargetMethod.Name
));
}
return;
}
// Not a collection, just a single value to check
// Check if it's a literal
if (argumentSyntax.Expression is not LiteralExpressionSyntax)
return;
context.ReportDiagnostic(Diagnostic.Create(ForbidLiteralRule,
argumentSyntax.GetLocation(),
operation.Parameter.Name,
(context.Operation as IInvocationOperation).TargetMethod.Name
));
}
}

View File

@@ -0,0 +1,75 @@
#nullable enable
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ObsoleteInheritanceAnalyzer : DiagnosticAnalyzer
{
private const string Attribute = "Robust.Shared.Analyzers.ObsoleteInheritanceAttribute";
public static readonly DiagnosticDescriptor Rule = new(
Diagnostics.IdObsoleteInheritance,
"Parent type has obsoleted inheritance",
"Type '{0}' inherits from '{1}', which has obsoleted inheriting from itself",
"Usage",
DiagnosticSeverity.Warning,
true);
public static readonly DiagnosticDescriptor RuleWithMessage = new(
Diagnostics.IdObsoleteInheritanceWithMessage,
"Parent type has obsoleted inheritance",
"Type '{0}' inherits from '{1}', which has obsoleted inheriting from itself: \"{2}\"",
"Usage",
DiagnosticSeverity.Warning,
true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule, RuleWithMessage];
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolAction(CheckClass, SymbolKind.NamedType);
}
private static void CheckClass(SymbolAnalysisContext context)
{
if (context.Symbol is not INamedTypeSymbol typeSymbol)
return;
if (typeSymbol.IsValueType || typeSymbol.BaseType is not { } baseType)
return;
if (!AttributeHelper.HasAttribute(baseType, Attribute, out var data))
return;
var location = context.Symbol.Locations[0];
if (GetMessageFromAttributeData(data) is { } message)
{
context.ReportDiagnostic(Diagnostic.Create(
RuleWithMessage,
location,
[typeSymbol.Name, baseType.Name, message]));
}
else
{
context.ReportDiagnostic(Diagnostic.Create(
Rule,
location,
[typeSymbol.Name, baseType.Name]));
}
}
private static string? GetMessageFromAttributeData(AttributeData data)
{
if (data.ConstructorArguments is not [var message, ..])
return null;
return message.Value as string;
}
}

View File

@@ -42,8 +42,8 @@ public class RecursiveMoveBenchmark : RobustIntegrationTest
public void GlobalSetup()
{
ProgramShared.PathOffset = "../../../../";
var server = StartServer();
var client = StartClient();
var server = StartServer(new() {Pool = false});
var client = StartClient(new() {Pool = false});
Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()).Wait();

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

@@ -226,6 +226,9 @@ internal sealed class MidiRenderer : IMidiRenderer
if (value == _master)
return;
if (CheckMasterCycle(value))
throw new InvalidOperationException("Tried to set master to a child of this renderer!");
if (_master is { Disposed: false })
{
try
@@ -729,4 +732,22 @@ internal sealed class MidiRenderer : IMidiRenderer
_synth?.Dispose();
_player?.Dispose();
}
/// <summary>
/// Check that a given renderer is not already a child of this renderer, i.e. it would introduce a cycle if set as master of this renderer.
/// </summary>
private bool CheckMasterCycle(IMidiRenderer? otherRenderer)
{
// Doesn't inside drift, cringe.
while (otherRenderer != null)
{
if (otherRenderer == this)
return true;
otherRenderer = otherRenderer.Master;
}
return false;
}
}

View File

@@ -173,29 +173,51 @@ namespace Robust.Client.Console.Commands
}
}
internal sealed class ShowPositionsCommand : LocalizedCommands
internal sealed class ShowPositionsCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showpos";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mgr = _entitySystems.GetEntitySystem<DebugDrawingSystem>();
mgr.DebugPositions = !mgr.DebugPositions;
_debugDrawing.DebugPositions = !_debugDrawing.DebugPositions;
}
}
internal sealed class ShowRotationsCommand : LocalizedCommands
internal sealed class ShowRotationsCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showrot";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mgr = _entitySystems.GetEntitySystem<DebugDrawingSystem>();
mgr.DebugRotations = !mgr.DebugRotations;
_debugDrawing.DebugRotations = !_debugDrawing.DebugRotations;
}
}
internal sealed class ShowVelocitiesCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showvel";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_debugDrawing.DebugVelocities = !_debugDrawing.DebugVelocities;
}
}
internal sealed class ShowAngularVelocitiesCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showangvel";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_debugDrawing.DebugAngularVelocities = !_debugDrawing.DebugAngularVelocities;
}
}

View File

@@ -1,17 +1,19 @@
#if DEBUG
using Robust.Client.Debugging;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
public sealed class DebugAnchoredCommand : LocalizedCommands
public sealed class DebugAnchoredCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugAnchoringSystem _system = default!;
public override string Command => "showanchored";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntitySystem.Get<DebugAnchoringSystem>().Enabled ^= true;
_system.Enabled ^= true;
}
}
}

View File

@@ -1,17 +1,19 @@
#if DEBUG
using Robust.Client.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
internal sealed class LightDebugCommand : LocalizedCommands
internal sealed class LightDebugCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugLightTreeSystem _system = default!;
public override string Command => "lightbb";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntitySystem.Get<DebugLightTreeSystem>().Enabled ^= true;
_system.Enabled ^= true;
}
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.Console;
using Robust.Shared.Localization;
namespace Robust.Client.Console.Commands;
[UsedImplicitly]
internal sealed class LocalizationSetCulture : LocalizedCommands
{
private const string Name = "localization_set_culture";
private const int ArgumentCount = 1;
public override string Command => Name;
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != ArgumentCount)
{
shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error"));
return;
}
CultureInfo culture;
try
{
culture = CultureInfo.GetCultureInfo(args[0], predefinedOnly: false);
}
catch (CultureNotFoundException)
{
shell.WriteError(Loc.GetString("cmd-parse-failure-cultureinfo", ("arg", args[0])));
return;
}
LocalizationManager.SetCulture(culture);
shell.WriteLine(LocalizationManager.GetString("cmd-localization_set_culture-changed",
("code", culture.Name),
("nativeName", culture.NativeName),
("englishName", culture.EnglishName)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 => CompletionResult.FromHintOptions(GetCultureNames(),
LocalizationManager.GetString("cmd-localization_set_culture-culture-name")),
_ => CompletionResult.Empty
};
}
private static HashSet<string> GetCultureNames()
{
var cultureInfos = CultureInfo.GetCultures(CultureTypes.AllCultures)
.Where(x => !string.IsNullOrEmpty(x.Name))
.ToArray();
var allNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
allNames.UnionWith(cultureInfos.Select(x => x.TwoLetterISOLanguageName));
allNames.UnionWith(cultureInfos.Select(x => x.Name));
return allNames;
}
}

View File

@@ -0,0 +1,18 @@
using Robust.Client.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
public sealed class ShowPlayerVelocityCommand : LocalizedEntityCommands
{
[Dependency] private readonly ShowPlayerVelocityDebugSystem _system = default!;
public override string Command => "showplayervelocity";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_system.Enabled ^= true;
}
}
}

View File

@@ -1,19 +0,0 @@
using Robust.Client.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
public sealed class VelocitiesCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
public override string Command => "showvelocities";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_entitySystems.GetEntitySystem<VelocityDebugSystem>().Enabled ^= true;
}
}
}

View File

@@ -1,140 +1,221 @@
using System.Numerics;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using System.Numerics;
namespace Robust.Client.Debugging
namespace Robust.Client.Debugging;
/// <summary>
/// A collection of visual debug overlays for the client game.
/// </summary>
public sealed class DebugDrawingSystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private bool _debugPositions;
private bool _debugRotations;
private bool _debugVelocities;
private bool _debugAngularVelocities;
/// <summary>
/// A collection of visual debug overlays for the client game.
/// Toggles the visual overlay of the local origin for each entity on screen.
/// </summary>
public sealed class DebugDrawingSystem : EntitySystem
public bool DebugPositions
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly TransformSystem _transform = default!;
private bool _debugPositions;
private bool _debugRotations;
/// <summary>
/// Toggles the visual overlay of the local origin for each entity on screen.
/// </summary>
public bool DebugPositions
get => _debugPositions;
set
{
get => _debugPositions;
set
if (value == DebugPositions)
{
if (value == DebugPositions)
{
return;
}
return;
}
_debugPositions = value;
_debugPositions = value;
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
{
_overlayManager.AddOverlay(new EntityPositionOverlay(_lookup, EntityManager, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
}
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
{
_overlayManager.AddOverlay(new EntityPositionOverlay(_lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
}
}
}
/// <summary>
/// Toggles the visual overlay of the local rotation.
/// </summary>
public bool DebugRotations
/// <summary>
/// Toggles the visual overlay of the rotation for each entity on screen.
/// </summary>
public bool DebugRotations
{
get => _debugRotations;
set
{
get => _debugRotations;
set
if (value == DebugRotations)
{
if (value == DebugRotations)
{
return;
}
return;
}
_debugRotations = value;
_debugRotations = value;
if (value && !_overlayManager.HasOverlay<EntityRotationOverlay>())
{
_overlayManager.AddOverlay(new EntityRotationOverlay(_lookup, EntityManager));
}
else
{
_overlayManager.RemoveOverlay<EntityRotationOverlay>();
}
if (value && !_overlayManager.HasOverlay<EntityRotationOverlay>())
{
_overlayManager.AddOverlay(new EntityRotationOverlay(_lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityRotationOverlay>();
}
}
}
private sealed class EntityPositionOverlay : Overlay
/// <summary>
/// Toggles the visual overlay of the local velocity for each entity on screen.
/// </summary>
public bool DebugVelocities
{
get => _debugVelocities;
set
{
private readonly EntityLookupSystem _lookup;
private readonly IEntityManager _entityManager;
private readonly SharedTransformSystem _transform;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityPositionOverlay(EntityLookupSystem lookup, IEntityManager entityManager, SharedTransformSystem transform)
if (value == DebugVelocities)
{
_lookup = lookup;
_entityManager = entityManager;
_transform = transform;
return;
}
protected internal override void Draw(in OverlayDrawArgs args)
_debugVelocities = value;
if (value && !_overlayManager.HasOverlay<EntityVelocityOverlay>())
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
foreach (var entity in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = _transform.GetWorldPositionRotation(entity);
var xLine = worldRotation.RotateVec(Vector2.UnitX);
var yLine = worldRotation.RotateVec(Vector2.UnitY);
worldHandle.DrawLine(center, center + xLine * stubLength, Color.Red);
worldHandle.DrawLine(center, center + yLine * stubLength, Color.Green);
}
_overlayManager.AddOverlay(new EntityVelocityOverlay(EntityManager, _lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityVelocityOverlay>();
}
}
}
private sealed class EntityRotationOverlay : Overlay
/// <summary>
/// Toggles the visual overlay of the angular velocity for each entity on screen.
/// </summary>
public bool DebugAngularVelocities
{
get => _debugAngularVelocities;
set
{
private readonly EntityLookupSystem _lookup;
private readonly IEntityManager _entityManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityRotationOverlay(EntityLookupSystem lookup, IEntityManager entityManager)
if (value == DebugAngularVelocities)
{
_lookup = lookup;
_entityManager = entityManager;
return;
}
protected internal override void Draw(in OverlayDrawArgs args)
_debugAngularVelocities = value;
if (value && !_overlayManager.HasOverlay<EntityAngularVelocityOverlay>())
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
_overlayManager.AddOverlay(new EntityAngularVelocityOverlay(EntityManager, _lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityAngularVelocityOverlay>();
}
}
}
private sealed class EntityPositionOverlay(EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
foreach (var entity in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = xformQuery.GetComponent(entity).GetWorldPositionRotation();
protected internal override void Draw(in OverlayDrawArgs args)
{
const float stubLength = 0.25f;
var drawLine = worldRotation.RotateVec(-Vector2.UnitY);
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
worldHandle.DrawLine(center, center + drawLine * stubLength, Color.Red);
}
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = _transform.GetWorldPositionRotation(uid);
var xLine = worldRotation.RotateVec(Vector2.UnitX);
var yLine = worldRotation.RotateVec(Vector2.UnitY);
worldHandle.DrawLine(center, center + xLine * stubLength, Color.Red);
worldHandle.DrawLine(center, center + yLine * stubLength, Color.Green);
}
}
}
private sealed class EntityRotationOverlay(EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
protected internal override void Draw(in OverlayDrawArgs args)
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = _transform.GetWorldPositionRotation(uid);
var drawLine = worldRotation.RotateVec(-Vector2.UnitY);
worldHandle.DrawLine(center, center + drawLine * stubLength, Color.Red);
}
}
}
private sealed class EntityVelocityOverlay(IEntityManager _entityManager, EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
protected internal override void Draw(in OverlayDrawArgs args)
{
const float multiplier = 0.2f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
if(!physicsQuery.TryGetComponent(uid, out var physicsComp))
continue;
var center = _transform.GetWorldPosition(uid);
var localVelocity = physicsComp.LinearVelocity;
if (localVelocity != Vector2.Zero)
worldHandle.DrawLine(center, center + localVelocity * multiplier, Color.Yellow);
}
}
}
private sealed class EntityAngularVelocityOverlay(IEntityManager _entityManager, EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
protected internal override void Draw(in OverlayDrawArgs args)
{
const float multiplier = (float)(0.2 / (2 * System.Math.PI));
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
if(!physicsQuery.TryGetComponent(uid, out var physicsComp))
continue;
var center = _transform.GetWorldPosition(uid);
var angularVelocity = physicsComp.AngularVelocity;
if (angularVelocity != 0.0f)
worldHandle.DrawCircle(center, angularVelocity * multiplier, angularVelocity > 0 ? Color.Magenta : Color.Blue, false);
}
}
}
}

View File

@@ -413,8 +413,9 @@ namespace Robust.Client.Debugging
}
var body = bodyEnt.Comp;
var meta = _entityManager.GetComponent<MetaDataComponent>(bodyEnt);
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner}");
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner} ({meta.EntityName})");
row++;
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
row++;

View File

@@ -384,7 +384,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)

View File

@@ -0,0 +1,123 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
public sealed partial class ClientEntityManager
{
public override EntityUid PredictedSpawnAttachedTo(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
{
var ent = SpawnAttachedTo(protoName, coordinates, overrides, rotation);
FlagPredicted(ent);
return ent;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override EntityUid PredictedSpawn(string? protoName = null, ComponentRegistry? overrides = null, bool doMapInit = true)
{
var ent = Spawn(protoName, overrides, doMapInit);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawn(string? protoName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!)
{
var ent = Spawn(protoName, coordinates, overrides, rotation);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null)
{
var ent = SpawnAtPosition(protoName, coordinates, overrides);
FlagPredicted(ent);
return ent;
}
public override bool PredictedTrySpawnNextTo(
string? protoName,
EntityUid target,
[NotNullWhen(true)] out EntityUid? uid,
TransformComponent? xform = null,
ComponentRegistry? overrides = null)
{
if (!TrySpawnNextTo(protoName, target, out uid, xform, overrides))
return false;
FlagPredicted(uid.Value);
return true;
}
public override bool PredictedTrySpawnInContainer(
string? protoName,
EntityUid containerUid,
string containerId,
[NotNullWhen(true)] out EntityUid? uid,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
if (!TrySpawnInContainer(protoName, containerUid, containerId, out uid, containerComp, overrides))
return false;
FlagPredicted(uid.Value);
return true;
}
public override EntityUid PredictedSpawnNextToOrDrop(string? protoName, EntityUid target, TransformComponent? xform = null, ComponentRegistry? overrides = null)
{
var ent = SpawnNextToOrDrop(protoName, target, xform, overrides);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
var ent = SpawnInContainerOrDrop(protoName, containerUid, containerId, xform, containerComp, overrides);
FlagPredicted(ent);
return ent;
}
public override EntityUid PredictedSpawnInContainerOrDrop(
string? protoName,
EntityUid containerUid,
string containerId,
out bool inserted,
TransformComponent? xform = null,
ContainerManagerComponent? containerComp = null,
ComponentRegistry? overrides = null)
{
var ent = SpawnInContainerOrDrop(protoName,
containerUid,
containerId,
out inserted,
xform,
containerComp,
overrides);
FlagPredicted(ent);
return ent;
}
public override void FlagPredicted(Entity<MetaDataComponent?> ent)
{
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp))
return;
DebugTools.Assert(IsClientSide(ent.Owner, ent.Comp));
EnsureComponent<PredictedSpawnComponent>(ent.Owner);
// TODO: Need to map call site or something, needs to be consistent between client and server.
}
}

View File

@@ -117,7 +117,7 @@ namespace Robust.Client.GameObjects
base.DirtyField(uid, comp, fieldName, metadata);
}
public override void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params ReadOnlySpan<string> fields)
public override void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params string[] fields)
{
// TODO Prediction
// does the client actually need to dirty the field?
@@ -291,5 +291,54 @@ namespace Robust.Client.GameObjects
}
}
#endregion
/// <inheritdoc />
public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityDeleted
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;
}
// So there's 3 scenarios:
// 1. Networked entity we just move to nullspace and rely on state handling.
// 2. Clientside predicted entity we delete and rely on state handling.
// 3. Clientside only entity that actually needs deleting here.
if (ent.Comp1.NetEntity.IsClientSide())
{
DeleteEntity(ent, ent.Comp1, ent.Comp2);
}
else
{
_xforms.DetachEntity(ent, ent.Comp2);
}
}
/// <inheritdoc />
public override void PredictedQueueDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
if (IsQueuedForDeletion(ent.Owner)
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityDeleted
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;
}
if (ent.Comp1.NetEntity.IsClientSide())
{
// client-side QueueDeleteEntity re-fetches MetadataComp and checks IsClientSide().
// base call to skip that.
// TODO create override that takes in metadata comp
base.QueueDeleteEntity(ent);
}
else
{
_xforms.DetachEntity(ent.Owner, ent.Comp2);
}
}
}
}

View File

@@ -10,19 +10,21 @@ using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
public sealed class ShowSpriteBBCommand : LocalizedCommands
public sealed class ShowSpriteBBCommand : LocalizedEntityCommands
{
[Dependency] private readonly SpriteBoundsSystem _system = default!;
public override string Command => "showspritebb";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntitySystem.Get<SpriteBoundsSystem>().Enabled ^= true;
_system.Enabled ^= true;
}
}
public sealed class SpriteBoundsSystem : EntitySystem
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly SpriteTreeSystem _spriteTree = default!;
@@ -40,7 +42,7 @@ namespace Robust.Client.GameObjects
if (_enabled)
{
DebugTools.AssertNull(_overlay);
_overlay = new SpriteBoundsOverlay(_spriteTree, _entityManager);
_overlay = new SpriteBoundsOverlay(_spriteTree, _xformSystem);
_overlayManager.AddOverlay(_overlay);
}
else
@@ -59,13 +61,13 @@ namespace Robust.Client.GameObjects
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly IEntityManager _entityManager;
private readonly SharedTransformSystem _xformSystem;
private SpriteTreeSystem _renderTree;
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, IEntityManager entityManager)
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, SharedTransformSystem xformSystem)
{
_renderTree = renderTree;
_entityManager = entityManager;
_xformSystem = xformSystem;
}
protected internal override void Draw(in OverlayDrawArgs args)
@@ -76,7 +78,7 @@ namespace Robust.Client.GameObjects
foreach (var (sprite, xform) in _renderTree.QueryAabb(currentMap, viewport))
{
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
// Get scaled down bounds used to indicate the "south" of a sprite.

View File

@@ -30,6 +30,7 @@ using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
using static Robust.Shared.Serialization.TypeSerializers.Implementations.SpriteSpecifierSerializer;
using Direction = Robust.Shared.Maths.Direction;
using Vector4 = Robust.Shared.Maths.Vector4;
using SysVec4 = System.Numerics.Vector4;
namespace Robust.Client.GameObjects
{
@@ -753,12 +754,20 @@ namespace Robust.Client.GameObjects
if (layerDatum.Shader == string.Empty)
{
layer.ShaderPrototype = null;
layer.UnShaded = false;
layer.Shader = null;
}
else if (layerDatum.Shader == SpriteSystem.UnshadedId.Id)
{
layer.ShaderPrototype = SpriteSystem.UnshadedId;
layer.UnShaded = true;
layer.Shader = null;
}
else if (prototypes.TryIndex<ShaderPrototype>(layerDatum.Shader, out var prototype))
{
layer.ShaderPrototype = layerDatum.Shader;
layer.Shader = prototype.Instance();
layer.UnShaded = false;
}
else
{
@@ -835,11 +844,28 @@ namespace Robust.Client.GameObjects
if (!TryGetLayer(layer, out var theLayer, true))
return;
if (shader == null)
{
theLayer.UnShaded = false;
theLayer.Shader = null;
theLayer.ShaderPrototype = null;
return;
}
if (prototype == SpriteSystem.UnshadedId.Id)
{
theLayer.UnShaded = true;
theLayer.ShaderPrototype = SpriteSystem.UnshadedId;
theLayer.Shader = null;
return;
}
theLayer.UnShaded = false;
theLayer.Shader = shader;
theLayer.ShaderPrototype = prototype;
}
public void LayerSetShader(object layerKey, ShaderInstance shader, string? prototype = null)
public void LayerSetShader(object layerKey, ShaderInstance? shader, string? prototype = null)
{
if (!LayerMapTryGet(layerKey, out var layer, true))
return;
@@ -1493,10 +1519,18 @@ namespace Robust.Client.GameObjects
{
[ViewVariables] private readonly SpriteComponent _parent;
[ViewVariables] public string? ShaderPrototype;
[ViewVariables] public ProtoId<ShaderPrototype>? ShaderPrototype;
[ViewVariables] public ShaderInstance? Shader;
[ViewVariables] public Texture? Texture;
/// <summary>
/// If true, then this layer is drawn without lighting applied.
/// Unshaded layers are given special treatment and don't just use the unshaded-shader to avoid having to
/// unnecessarily swap out the light texture. This helps the number of batches that need to be sent to the
/// GPU while drawing sprites.
/// </summary>
[ViewVariables] internal bool UnShaded;
private RSI? _rsi;
[ViewVariables] public RSI? RSI
{
@@ -1663,6 +1697,7 @@ namespace Robust.Client.GameObjects
if (toClone.Shader != null)
{
Shader = toClone.Shader.Mutable ? toClone.Shader.Duplicate() : toClone.Shader;
UnShaded = toClone.UnShaded;
ShaderPrototype = toClone.ShaderPrototype;
}
Texture = toClone.Texture;
@@ -2078,6 +2113,20 @@ namespace Robust.Client.GameObjects
drawingHandle.UseShader(Shader);
var layerColor = _parent.color * Color;
DebugTools.Assert(layerColor is {R: >= 0, G: >= 0, B: >= 0, A: >= 0}, "Negative colour modulation");
if (UnShaded)
{
DebugTools.AssertNull(Shader);
// Negative modulation values are used to disable light shading in the default shader.
// Specifically we set colour = -1 - colour
// This ensures that non-negative values become negative & is trivially invertible.
// Alternatively we could just clamp the colour to [0,1] and subtract a constant.
layerColor = new(new SysVec4(-1) - layerColor.RGBA);
}
var textureSize = texture.Size / (float)EyeManager.PixelsPerMeter;
var quad = Box2.FromDimensions(textureSize/-2, textureSize);

View File

@@ -76,7 +76,7 @@ namespace Robust.Client.GameObjects
foreach (var key in remie)
{
component.PlayingAnimations.Remove(key);
var completedEvent = new AnimationCompletedEvent {Uid = uid, Key = key, Finished = true};
var completedEvent = new AnimationCompletedEvent(uid, component, key, true);
EntityManager.EventBus.RaiseLocalEvent(uid, completedEvent, true);
}
@@ -187,7 +187,7 @@ namespace Robust.Client.GameObjects
return;
}
var completedEvent = new AnimationCompletedEvent {Uid = entity.Owner, Key = key, Finished = false};
var completedEvent = new AnimationCompletedEvent(entity.Owner, entity.Comp, key, false);
EntityManager.EventBus.RaiseLocalEvent(entity.Owner, completedEvent, true);
}
@@ -202,13 +202,33 @@ namespace Robust.Client.GameObjects
/// </summary>
public sealed class AnimationCompletedEvent : EntityEventArgs
{
/// <summary>
/// The entity associated with the event.
/// </summary>
public EntityUid Uid { get; init; }
/// <summary>
/// The animation player component associated with the entity this event was raised on.
/// </summary>
public AnimationPlayerComponent AnimationPlayer { get; init; }
/// <summary>
/// The key associated with the animation that was completed.
/// </summary>
public string Key { get; init; } = string.Empty;
/// <summary>
/// If true, the animation finished by getting to its natural end.
/// If false, it was removed prematurely via <see cref="AnimationPlayerSystem.Stop(Robust.Client.GameObjects.AnimationPlayerComponent,string)"/> or similar overloads.
/// If false, it was removed prematurely via <see cref="AnimationPlayerSystem.Stop(EntityUid,AnimationPlayerComponent,string)"/> or similar overloads.
/// </summary>
public bool Finished { get; init; }
public AnimationCompletedEvent(EntityUid uid, AnimationPlayerComponent animationPlayer, string key, bool finished = true)
{
Uid = uid;
AnimationPlayer = animationPlayer;
Key = key;
Finished = finished;
}
}
}

View File

@@ -303,7 +303,7 @@ namespace Robust.Client.GameObjects
while (parent.IsValid() && (!spriteOccluded || !lightOccluded))
{
var parentXform = TransformQuery.GetComponent(parent);
if (TryComp<ContainerManagerComponent>(parent, out var manager) && manager.TryGetContainer(child, out var container))
if (TryComp<ContainerManagerComponent>(parent, out var manager) && TryGetContainingContainer(parent, child, out var container, manager))
{
spriteOccluded = spriteOccluded || !container.ShowContents;
lightOccluded = lightOccluded || container.OccludesLight;
@@ -344,7 +344,7 @@ namespace Robust.Client.GameObjects
var childLightOccluded = lightOccluded;
// We already know either sprite or light is not occluding so need to check container.
if (manager.TryGetContainer(child, out var container))
if (TryGetContainingContainer(entity, child, out var container, manager))
{
childSpriteOccluded = childSpriteOccluded || !container.ShowContents;
childLightOccluded = childLightOccluded || container.OccludesLight;

View File

@@ -2,24 +2,24 @@ using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Console;
using Robust.Shared.Containers;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Utility;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.GameObjects;
public sealed class DebugEntityLookupCommand : LocalizedCommands
public sealed class DebugEntityLookupCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugEntityLookupSystem _system = default!;
public override string Command => "togglelookup";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
EntitySystem.Get<DebugEntityLookupSystem>().Enabled ^= true;
_system.Enabled ^= true;
}
}

View File

@@ -26,10 +26,10 @@ namespace Robust.Client.GameObjects
if (_enabled)
{
_lightOverlay = new DebugLightOverlay(
EntitySystem.Get<EntityLookupSystem>(),
EntityManager.System<EntityLookupSystem>(),
IoCManager.Resolve<IEyeManager>(),
IoCManager.Resolve<IMapManager>(),
Get<LightTreeSystem>());
EntityManager.System<LightTreeSystem>());
overlayManager.AddOverlay(_lightOverlay);
}

View File

@@ -62,7 +62,7 @@ public sealed class EyeSystem : SharedEyeSystem
eyeComponent.Target = null;
}
eyeComponent.Eye.Position = xform.MapPosition;
eyeComponent.Eye.Position = TransformSystem.GetMapCoordinates(xform);
}
}
}

View File

@@ -9,10 +9,6 @@ namespace Robust.Client.GameObjects;
public sealed class MapSystem : SharedMapSystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
protected override MapId GetNextMapId()
{
// Client-side map entities use negative map Ids to avoid conflict with server-side maps.
@@ -23,16 +19,4 @@ public sealed class MapSystem : SharedMapSystem
}
return id;
}
public override void Initialize()
{
base.Initialize();
_overlayManager.AddOverlay(new TileEdgeOverlay(EntityManager, _resource, _tileDefinitionManager));
}
public override void Shutdown()
{
base.Shutdown();
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
}
}

View File

@@ -0,0 +1,66 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics.Components;
namespace Robust.Client.GameObjects;
public sealed class ShowPlayerVelocityDebugSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
internal bool Enabled
{
get => _label.Parent != null;
set
{
if (value)
{
_uiManager.WindowRoot.AddChild(_label);
}
else
{
_label.Orphan();
}
}
}
private Label _label = default!;
public override void Initialize()
{
base.Initialize();
_label = new Label();
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
if (!Enabled)
{
_label.Visible = false;
return;
}
var player = _playerManager.LocalEntity;
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
{
_label.Visible = false;
return;
}
var screenPos = _eyeManager.WorldToScreen(_transform.GetWorldPosition(Transform(player.Value)));
LayoutContainer.SetPosition(_label, screenPos + new Vector2(0, 50));
_label.Visible = true;
_label.Text = $"Speed: {body.LinearVelocity.Length():0.00}\nLinear: {body.LinearVelocity.X:0.00}, {body.LinearVelocity.Y:0.00}\nAngular: {body.AngularVelocity}";
}
}

View File

@@ -37,6 +37,7 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly SharedTransformSystem _xforms = default!;
public static readonly ProtoId<ShaderPrototype> UnshadedId = "unshaded";
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
/// <summary>

View File

@@ -1,54 +0,0 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
namespace Robust.Client.GameObjects
{
public sealed class VelocityDebugSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly TransformSystem _transform = default!;
internal bool Enabled { get; set; }
private Label _label = default!;
public override void Initialize()
{
base.Initialize();
_label = new Label();
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(_label);
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
if (!Enabled)
{
_label.Visible = false;
return;
}
var player = _playerManager.LocalEntity;
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
{
_label.Visible = false;
return;
}
var screenPos = _eyeManager.WorldToScreen(_transform.GetWorldPosition(Transform(player.Value)));
LayoutContainer.SetPosition(_label, screenPos + new Vector2(0, 50));
_label.Visible = true;
_label.Text = $"Speed: {body.LinearVelocity.Length():0.00}\nLinear: {body.LinearVelocity.X:0.00}, {body.LinearVelocity.Y:0.00}\nAngular: {body.AngularVelocity}";
}
}
}

View File

@@ -13,6 +13,7 @@ using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Client.Timing;
using Robust.Shared;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Containers;
@@ -384,7 +385,6 @@ namespace Robust.Client.GameStates
_processor.UpdateFullRep(curState);
}
IEnumerable<NetEntity> createdEntities;
using (_prof.Group("ApplyGameState"))
{
if (_timing.LastProcessedTick < targetProcessedTick && nextState != null)
@@ -564,6 +564,21 @@ namespace Robust.Client.GameStates
var metaQuery = _entities.GetEntityQuery<MetaDataComponent>();
RemQueue<IComponent> toRemove = new();
// Handle predicted entity spawns.
var predicted = new ValueList<EntityUid>();
var predictedQuery = _entities.AllEntityQueryEnumerator<PredictedSpawnComponent>();
while (predictedQuery.MoveNext(out var uid, out var _))
{
predicted.Add(uid);
}
// Entity will get re-created as part of the tick.
foreach (var ent in predicted)
{
_entities.DeleteEntity(ent);
}
foreach (var entity in system.DirtyEntities)
{
DebugTools.Assert(toRemove.Count == 0);
@@ -699,8 +714,9 @@ namespace Robust.Client.GameStates
#if !EXCEPTION_TOLERANCE
throw new KeyNotFoundException();
#endif
#else
continue;
#endif
}
var compData = _compDataPool.Get();
@@ -961,8 +977,9 @@ namespace Robust.Client.GameStates
RequestFullState();
#if !EXCEPTION_TOLERANCE
throw;
#endif
#else
return;
#endif
}
if (data.Created)
@@ -980,8 +997,9 @@ namespace Robust.Client.GameStates
RequestFullState();
#if !EXCEPTION_TOLERANCE
throw;
#endif
#else
return;
#endif
}
}

View File

@@ -123,7 +123,8 @@ namespace Robust.Client.Graphics
/// <inheritdoc />
public ScreenCoordinates CoordinatesToScreen(EntityCoordinates point)
{
return MapToScreen(point.ToMap(_entityManager, _entityManager.System<SharedTransformSystem>()));
var transformSystem = _entityManager.System<SharedTransformSystem>();
return MapToScreen(transformSystem.ToMapCoordinates(point));
}
public ScreenCoordinates MapToScreen(MapCoordinates point)

View File

@@ -21,7 +21,7 @@ namespace Robust.Client.Graphics.Clyde
private const string UniModelMatrix = "modelMatrix";
private const string UniTexturePixelSize = "TEXTURE_PIXEL_SIZE";
private const string UniMainTexture = "TEXTURE";
private const string UniLightTexture = "lightMap";
private const string UniLightTexture = "lightMap"; // TODO CLYDE consistent shader variable naming
private const string UniProjViewMatrices = "projectionViewMatrices";
private const string UniUniformConstants = "uniformConstants";

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
@@ -22,10 +23,30 @@ namespace Robust.Client.Graphics.Clyde
/// </summary>
private HashSet<Type> _erroredGridOverlays = new();
private Vertex2D[]? _chunkMeshBuilderVertexBuffer;
private ushort[]? _chunkMeshBuilderIndexBuffer;
private int _verticesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * 4;
private int _indicesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * GetQuadBatchIndexCount();
private List<Entity<MapGridComponent>> _grids = new();
private bool _drawTileEdges;
private void RenderTileEdgesChanges(bool value)
{
_drawTileEdges = value;
if (!value)
return;
// Dirty all Edges
foreach (var gridData in _mapChunkData.Values)
{
foreach (var chunk in gridData.Values)
{
chunk.EdgeDirty = true;
}
}
}
private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBounds, IEye eye)
{
@@ -63,29 +84,79 @@ namespace Robust.Client.Graphics.Clyde
gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1));
}
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid);
gridProgram.SetUniform(UniIModelMatrix, _transformSystem.GetWorldMatrix(transform));
gridProgram.SetUniform(UniIModelMatrix, _transformSystem.GetWorldMatrix(mapGrid));
var enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
// Handle base texture updates.
while (enumerator.MoveNext(out var chunk))
{
DebugTools.Assert(chunk.FilledTiles > 0);
if (!data.TryGetValue(chunk.Indices, out MapChunkData? datum))
data[chunk.Indices] = datum = _initChunkBuffers(mapGrid, chunk);
var datum = EnsureChunkInitialized(data, chunk, mapGrid);
if (datum.Dirty)
_updateChunkMesh(mapGrid, chunk, datum);
DebugTools.Assert(datum.TileCount > 0);
if (datum.TileCount == 0)
if (!datum.Dirty)
continue;
BindVertexArray(datum.VAO);
CheckGlError();
_updateChunkMesh(mapGrid, chunk, datum);
_debugStats.LastGLDrawCalls += 1;
GL.DrawElements(GetQuadGLPrimitiveType(), datum.TileCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
CheckGlError();
if (!_drawTileEdges)
continue;
// Dirty edge tiles for next step.
datum.EdgeDirty = true;
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
var neighbor = chunk.Indices + new Vector2i(x, y);
if (!mapGrid.Comp.Chunks.TryGetValue(neighbor, out var neighborChunk))
continue;
var neighborDatum = EnsureChunkInitialized(data, neighborChunk, mapGrid);
neighborDatum.EdgeDirty = true;
}
}
}
// Handle edge sprites.
if (_drawTileEdges)
{
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
while (enumerator.MoveNext(out var chunk))
{
var datum = data[chunk.Indices];
if (datum.EdgeDirty)
_updateChunkEdges(mapGrid, chunk, datum);
}
}
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
// Draw chunks
while (enumerator.MoveNext(out var chunk))
{
var datum = data[chunk.Indices];
DebugTools.Assert(datum.TileCount > 0);
if (datum.TileCount > 0)
{
BindVertexArray(datum.VAO);
CheckGlError();
_debugStats.LastGLDrawCalls += 1;
GL.DrawElements(GetQuadGLPrimitiveType(), datum.TileCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
CheckGlError();
}
if (_drawTileEdges && datum.EdgeCount > 0)
{
BindVertexArray(datum.EdgeVAO);
CheckGlError();
_debugStats.LastGLDrawCalls += 1;
GL.DrawElements(GetQuadGLPrimitiveType(), datum.EdgeCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
CheckGlError();
}
}
requiresFlush = false;
@@ -117,6 +188,17 @@ namespace Robust.Client.Graphics.Clyde
CullEmptyChunks();
}
private MapChunkData EnsureChunkInitialized(Dictionary<Vector2i, MapChunkData> data, MapChunk chunk, Entity<MapGridComponent> mapGrid)
{
if (!data.TryGetValue(chunk.Indices, out var datum))
{
data[chunk.Indices] = datum = new MapChunkData();
_initChunkBuffers(mapGrid, chunk, datum);
}
return datum;
}
private void CullEmptyChunks()
{
foreach (var (grid, chunks) in _mapChunkData)
@@ -138,66 +220,143 @@ namespace Robust.Client.Graphics.Clyde
private void _updateChunkMesh(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
Span<ushort> indexBuffer = stackalloc ushort[_indicesPerChunk(chunk)];
Span<Vertex2D> vertexBuffer = stackalloc Vertex2D[_verticesPerChunk(chunk)];
Span<ushort> indexBuffer = EnsureSize(ref _chunkMeshBuilderIndexBuffer, _indicesPerChunk(chunk));
Span<Vertex2D> vertexBuffer = EnsureSize(ref _chunkMeshBuilderVertexBuffer, _verticesPerChunk(chunk));
var i = 0;
var cSz = grid.Comp.ChunkSize;
var cScaled = chunk.Indices * cSz;
for (ushort x = 0; x < cSz; x++)
var chunkSize = grid.Comp.ChunkSize;
var chunkOriginScaled = chunk.Indices * chunkSize;
for (ushort x = 0; x < chunkSize; x++)
{
for (ushort y = 0; y < cSz; y++)
for (ushort y = 0; y < chunkSize; y++)
{
var gridX = x + chunkOriginScaled.X;
var gridY = y + chunkOriginScaled.Y;
var tile = chunk.GetTile(x, y);
if (tile.IsEmpty)
continue;
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile);
Box2 region;
if (regionMaybe == null || regionMaybe.Length <= tile.Variant)
// Tile render
if (x != chunkSize && y != chunkSize)
{
region = _tileDefinitionManager.ErrorTileRegion;
}
else
{
region = regionMaybe[tile.Variant];
}
// ReSharper disable once IntVariableOverflowInUncheckedContext
if (tile.IsEmpty)
continue;
var gx = x + cScaled.X;
var gy = y + cScaled.Y;
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile);
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(gx, gy, region.Left, region.Bottom, Color.White);
vertexBuffer[vIdx + 1] = new Vertex2D(gx + 1, gy, region.Right, region.Bottom, Color.White);
vertexBuffer[vIdx + 2] = new Vertex2D(gx + 1, gy + 1, region.Right, region.Top, Color.White);
vertexBuffer[vIdx + 3] = new Vertex2D(gx, gy + 1, region.Left, region.Top, Color.White);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort)(i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
i += 1;
Box2 region;
if (regionMaybe == null || regionMaybe.Length <= tile.Variant)
{
region = _tileDefinitionManager.ErrorTileRegion;
}
else
{
region = regionMaybe[tile.Variant];
}
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
i += 1;
}
}
}
var indexSlice = indexBuffer[..(i * GetQuadBatchIndexCount())];
var vertSlice = vertexBuffer[..(i * 4)];
GL.BindVertexArray(datum.VAO);
CheckGlError();
datum.EBO.Use();
datum.VBO.Use();
datum.EBO.Reallocate(indexBuffer[..(i * GetQuadBatchIndexCount())]);
datum.VBO.Reallocate(vertexBuffer[..(i * 4)]);
datum.Dirty = false;
datum.EBO.Reallocate(indexSlice);
datum.VBO.Reallocate(vertSlice);
datum.TileCount = i;
datum.Dirty = false;
}
private unsafe MapChunkData _initChunkBuffers(Entity<MapGridComponent> grid, MapChunk chunk)
private void _updateChunkEdges(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
// Need a buffer that can potentially store all neighbor tiles
Span<ushort> indexBuffer = EnsureSize(ref _chunkMeshBuilderIndexBuffer, _indicesPerChunk(chunk) * 8);
Span<Vertex2D> vertexBuffer = EnsureSize(ref _chunkMeshBuilderVertexBuffer, _verticesPerChunk(chunk) * 8);
var i = 0;
var chunkSize = grid.Comp.ChunkSize;
var chunkOriginScaled = chunk.Indices * chunkSize;
var maps = _entityManager.System<SharedMapSystem>();
for (ushort x = 0; x < chunkSize; x++)
{
for (ushort y = 0; y < chunkSize; y++)
{
var gridX = x + chunkOriginScaled.X;
var gridY = y + chunkOriginScaled.Y;
var tile = chunk.GetTile(x, y);
if (!_tileDefinitionManager.TryGetDefinition(tile.TypeId, out var tileDef))
continue;
// Edge render
for (var nx = -1; nx <= 1; nx++)
{
for (var ny = -1; ny <= 1; ny++)
{
if (nx == 0 && ny == 0)
continue;
var neighborIndices = new Vector2i(gridX + nx, gridY + ny);
if (!maps.TryGetTile(grid.Comp, neighborIndices, out var neighborTile))
continue;
if (!_tileDefinitionManager.TryGetDefinition(neighborTile.TypeId, out var neighborDef))
continue;
// If it's the same tile then no edge to be drawn.
if (tile.TypeId == neighborTile.TypeId || neighborDef.EdgeSprites.Count == 0)
continue;
// If neighbor is a lower or same priority then us then don't draw on our tile.
if (neighborDef.EdgeSpritePriority <= tileDef.EdgeSpritePriority)
continue;
var direction = new Vector2i(nx, ny).AsDirection().GetOpposite();
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(neighborTile.TypeId, direction);
if (regionMaybe == null)
continue;
var region = regionMaybe[0];
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
i += 1;
}
}
}
}
// We don't save the edge buffers back because we might need to re-use it if a neighbor chunk updates.
var indexSlice = indexBuffer[..(i * GetQuadBatchIndexCount())];
var vertSlice = vertexBuffer[..(i * 4)];
GL.BindVertexArray(datum.EdgeVAO);
CheckGlError();
datum.EdgeEBO.Use();
datum.EdgeVBO.Use();
datum.EdgeEBO.Reallocate(indexSlice);
datum.EdgeVBO.Reallocate(vertSlice);
datum.EdgeCount = i;
datum.EdgeDirty = false;
}
private unsafe void _initChunkBuffers(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
var vboSize = _verticesPerChunk(chunk) * sizeof(Vertex2D);
var eboSize = _indicesPerChunk(chunk) * sizeof(ushort);
// Base VAO
var vao = GenVertexArray();
BindVertexArray(vao);
CheckGlError();
var vboSize = _verticesPerChunk(chunk) * sizeof(Vertex2D);
var eboSize = _indicesPerChunk(chunk) * sizeof(ushort);
var vbo = new GLBuffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw,
vboSize, $"Grid {grid.Owner} chunk {chunk.Indices} VBO");
var ebo = new GLBuffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw,
@@ -212,12 +371,30 @@ namespace Robust.Client.Graphics.Clyde
vbo.Use();
ebo.Use();
var datum = new MapChunkData(vao, vbo, ebo)
{
Dirty = true
};
datum.EBO = ebo;
datum.VBO = vbo;
datum.VAO = vao;
return datum;
// EdgeVAO
var edgeVao = GenVertexArray();
BindVertexArray(edgeVao);
CheckGlError();
var edgeVbo = new GLBuffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw,
vboSize * 8, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeVBO");
var edgeEbo = new GLBuffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw,
eboSize * 8, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeEBO");
ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, vao, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeVAO");
SetupVAOLayout();
CheckGlError();
edgeVbo.Use();
edgeEbo.Use();
datum.EdgeEBO = edgeEbo;
datum.EdgeVBO = edgeVbo;
datum.EdgeVAO = edgeVao;
}
private void DeleteChunk(MapChunkData data)
@@ -254,19 +431,49 @@ namespace Robust.Client.Graphics.Clyde
_mapChunkData.Remove(gridId);
}
private static T[] EnsureSize<T>(ref T[]? field, int size)
{
if (field == null || field.Length < size)
field = new T[size];
return field;
}
private void WriteTileToBuffers(
int i,
int gridX,
int gridY,
Span<Vertex2D> vertexBuffer,
Span<ushort> indexBuffer,
Box2 region)
{
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(gridX, gridY, region.Left, region.Bottom, Color.White);
vertexBuffer[vIdx + 1] = new Vertex2D(gridX + 1, gridY, region.Right, region.Bottom, Color.White);
vertexBuffer[vIdx + 2] = new Vertex2D(gridX + 1, gridY + 1, region.Right, region.Top, Color.White);
vertexBuffer[vIdx + 3] = new Vertex2D(gridX, gridY + 1, region.Left, region.Top, Color.White);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort)(i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
}
private sealed class MapChunkData
{
public bool Dirty;
public readonly uint VAO;
public readonly GLBuffer VBO;
public readonly GLBuffer EBO;
public bool EdgeDirty = true;
public bool Dirty = true;
public uint VAO;
public GLBuffer VBO = default!;
public GLBuffer EBO = default!;
public int TileCount;
public MapChunkData(uint vao, GLBuffer vbo, GLBuffer ebo)
public uint EdgeVAO;
public GLBuffer EdgeVBO = default!;
public GLBuffer EdgeEBO = default!;
public int EdgeCount;
public MapChunkData()
{
VAO = vao;
VBO = vbo;
EBO = ebo;
}
}
}

View File

@@ -123,10 +123,14 @@ namespace Robust.Client.Graphics.Clyde
private void RenderSingleWorldOverlay(Overlay overlay, Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
{
// Check that entity manager has started.
// This is required for us to be able to use MapSystem.
DebugTools.Assert(_entityManager.Started, "Entity manager should be started/initialized before rendering world-space overlays");
DebugTools.Assert(space != OverlaySpace.ScreenSpaceBelowWorld && space != OverlaySpace.ScreenSpace);
var mapId = vp.Eye!.Position.MapId;
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), _mapManager.GetMapEntityIdOrThrow(mapId), mapId, worldBox, worldBounds);
var mapId = vp.Eye?.Position.MapId ?? MapId.Nullspace;
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), _mapSystem.GetMapOrInvalid(mapId), mapId, worldBox, worldBounds);
if (!overlay.BeforeDraw(args))
return;
@@ -152,6 +156,7 @@ namespace Robust.Client.Graphics.Clyde
private void RenderOverlays(Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
{
DebugTools.Assert(space != OverlaySpace.ScreenSpaceBelowWorld && space != OverlaySpace.ScreenSpace);
using (DebugGroup($"Overlays: {space}"))
{
foreach (var overlay in GetOverlaysForSpace(space))
@@ -176,9 +181,18 @@ namespace Robust.Client.Graphics.Clyde
var worldBounds = CalcWorldBounds(vp);
var worldAABB = worldBounds.CalcBoundingBox();
var mapId = vp.Eye!.Position.MapId;
var mapId = vp.Eye?.Position.MapId ?? MapId.Nullspace;
var mapUid = EntityUid.Invalid;
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, _mapManager.GetMapEntityIdOrThrow(mapId), mapId, worldAABB, worldBounds);
// Screen space overlays may be getting used before entity manager & entity systems have been initialized.
// This might mean that _mapSystem is currently null.
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (_entityManager.Started && _mapSystem != null)
mapUid = _mapSystem.GetMapOrInvalid(mapId);
DebugTools.Assert(_mapSystem != null || !_entityManager.Started);
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, mapUid, mapId, worldAABB, worldBounds);
foreach (var overlay in list)
{

View File

@@ -98,9 +98,11 @@ namespace Robust.Client.Graphics.Clyde
private LightCapacityComparer _lightCap = new();
private ShadowCapacityComparer _shadowCap = new ShadowCapacityComparer();
private float _maxLightRadius;
private unsafe void InitLighting()
{
_cfg.OnValueChanged(CVars.MaxLightRadius, val => { _maxLightRadius = val;}, true);
// Other...
LoadLightingShaders();
@@ -617,8 +619,9 @@ namespace Robust.Client.Graphics.Clyde
// Use worldbounds for this one as we only care if the light intersects our actual bounds
var xforms = _entityManager.GetEntityQuery<TransformComponent>();
var state = (this, count: 0, shadowCastingCount: 0, xforms, worldAABB);
var lightAabb = worldAABB.Enlarged(_maxLightRadius);
foreach (var (uid, comp) in _lightTreeSystem.GetIntersectingTrees(map, worldAABB))
foreach (var (uid, comp) in _lightTreeSystem.GetIntersectingTrees(map, lightAabb))
{
var bounds = _transformSystem.GetInvWorldMatrix(uid, xforms).TransformBox(worldBounds);
comp.Tree.QueryAabb(ref state, LightQuery, bounds);

View File

@@ -121,6 +121,7 @@ namespace Robust.Client.Graphics.Clyde
_cfg.OnValueChanged(CVars.LightSoftShadows, SoftShadowsChanged, true);
_cfg.OnValueChanged(CVars.MaxLightCount, MaxLightsChanged, true);
_cfg.OnValueChanged(CVars.MaxOccluderCount, MaxOccludersChanged, true);
_cfg.OnValueChanged(CVars.RenderTileEdges, RenderTileEdgesChanges, true);
// I can't be bothered to tear down and set these threads up in a cvar change handler.
// Windows and Linux can be trusted to not explode with threaded windowing,

View File

@@ -1,8 +1,22 @@
// UV coordinates in texture-space. I.e., (0,0) is the corner of the texture currently being used to draw.
// When drawing a sprite from a texture atlas, (0,0) is the corner of the atlas, not the specific sprite being drawn.
varying highp vec2 UV;
// UV coordinates in quad-space. I.e., when drawing a sprite from a texture atlas (0,0) is the corner of the sprite
// currently being drawn.
varying highp vec2 UV2;
// TBH I'm not sure what this is for. I think it is scree UV coordiantes, i.e., FRAGCOORD.xy * SCREEN_PIXEL_SIZE ?
// TODO CLYDE Is this still needed?
varying highp vec2 Pos;
// Vertex colour modulation. Note that negative values imply that the LIGHTMAP should be ignored. This is used to avoid
// having to set the texture to a white/blank texture for sprites that have no light shading applied.
varying highp vec4 VtxModulate;
// The current light map. Unless disabled, this is automatically sampled to create the LIGHT vector, which is then used
// to modulate the output colour.
// TODO CLYDE consistent shader variable naming
uniform sampler2D lightMap;
// [SHADER_HEADER_CODE]
@@ -11,11 +25,37 @@ void main()
{
highp vec4 FRAGCOORD = gl_FragCoord;
// The output colour. This should get set by the shader code block.
// This will get modified by the LIGHT and MODULATE vectors.
lowp vec4 COLOR;
lowp vec3 lightSample = texture2D(lightMap, Pos).rgb;
// The light colour, usually sampled from the LIGHTMAP
lowp vec4 LIGHT;
// Colour modulation vector.
highp vec4 MODULATE;
// Sample the texture outside of the branch / with uniform control flow.
LIGHT = texture2D(lightMap, Pos);
if (VtxModulate.x < 0.0)
{
// Negative VtxModulate implies unshaded/no lighting.
MODULATE = -1.0 - VtxModulate;
LIGHT = vec4(1.0);
}
else
{
MODULATE = VtxModulate;
}
// TODO CLYDE consistent shader variable naming
// Requires breaking changes.
lowp vec3 lightSample = LIGHT.xyz;
// [SHADER_CODE]
gl_FragColor = zAdjustResult(COLOR * VtxModulate * vec4(lightSample, 1.0));
LIGHT.xyz = lightSample;
gl_FragColor = zAdjustResult(COLOR * MODULATE * LIGHT);
}

View File

@@ -18,6 +18,7 @@ uniform mat3 modelMatrix;
// Allows us to do texture atlassing with texture coordinates 0->1
// Input texture coordinates get mapped to this range.
uniform vec4 modifyUV;
// TODO CLYDE Is this still needed?
// [SHADER_HEADER_CODE]
@@ -39,5 +40,15 @@ void main()
Pos = (VERTEX + 1.0) / 2.0;
UV = mix(modifyUV.xy, modifyUV.zw, tCoord);
UV2 = tCoord2;
VtxModulate = zFromSrgb(modulate);
// Negative modulation is being used as a hacky way to squeeze in lighting data.
// I.e., negative modulation implies we ignore the lighting.
if (modulate.x < 0.0)
{
VtxModulate = -1.0 - zFromSrgb(-1.0 - modulate);
}
else
{
VtxModulate = zFromSrgb(modulate);
}
}

View File

@@ -1,6 +1,7 @@
varying highp vec2 UV;
varying highp vec2 UV2;
// TODO CLYDE consistent shader variable naming
uniform sampler2D lightMap;
// [SHADER_HEADER_CODE]

View File

@@ -16,7 +16,7 @@ using Vector4 = Robust.Shared.Maths.Vector4;
namespace Robust.Client.Graphics
{
[Prototype("shader")]
[Prototype]
public sealed partial class ShaderPrototype : IPrototype, ISerializationHooks
{
[ViewVariables]

View File

@@ -48,6 +48,7 @@ namespace Robust.Client.Input
[Dependency] private readonly IUserInterfaceManagerInternal _uiMgr = default!;
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
private ISawmill _logger = default!;
private bool _currentlyFindingViewport;
@@ -114,6 +115,8 @@ namespace Robust.Client.Input
/// <inheritdoc />
public void Initialize()
{
_logger = Logger.GetSawmill("input");
NetworkBindMap = new BoundKeyMap(_reflectionManager);
NetworkBindMap.PopulateKeyFunctionsMap();
@@ -130,7 +133,7 @@ namespace Robust.Client.Input
}
catch (Exception e)
{
Logger.ErrorS("input", "Failed to load user keybindings: " + e);
_logger.Error("Failed to load user keybindings: " + e);
}
}
@@ -531,8 +534,7 @@ namespace Robust.Client.Input
if (reg.Type != KeyBindingType.Command && !NetworkBindMap.FunctionExists(reg.Function.FunctionName))
{
Logger.DebugS("input", "Key function in {0} does not exist: '{1}'.", file,
reg.Function);
_logger.Debug("Key function in {0} does not exist: '{1}'.", file, reg.Function);
invalid = true;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.Graphics;
@@ -10,9 +11,11 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace Robust.Client.Map
{
@@ -29,7 +32,7 @@ namespace Robust.Client.Map
public Texture TileTextureAtlas => _tileTextureAtlas ?? Texture.Transparent;
private readonly Dictionary<int, Box2[]> _tileRegions = new();
private FrozenDictionary<(int Id, Direction Direction), Box2[]> _tileRegions = FrozenDictionary<(int Id, Direction Direction), Box2[]>.Empty;
public Box2 ErrorTileRegion { get; private set; }
@@ -42,7 +45,14 @@ namespace Robust.Client.Map
/// <inheritdoc />
public Box2[]? TileAtlasRegion(int tileType)
{
if (_tileRegions.TryGetValue(tileType, out var region))
return TileAtlasRegion(tileType, Direction.Invalid);
}
/// <inheritdoc />
public Box2[]? TileAtlasRegion(int tileType, Direction direction)
{
// ReSharper disable once CanSimplifyDictionaryTryGetValueWithGetValueOrDefault
if (_tileRegions.TryGetValue((tileType, direction), out var region))
{
return region;
}
@@ -83,7 +93,8 @@ namespace Robust.Client.Map
internal void _genTextureAtlas()
{
_tileRegions.Clear();
var sw = RStopwatch.StartNew();
var tileRegs = new Dictionary<(int Id, Direction Direction), Box2[]>();
_tileTextureAtlas = null;
var defList = TileDefs.Where(t => t.Sprite != null).ToList();
@@ -94,7 +105,7 @@ namespace Robust.Client.Map
const int tileSize = EyeManager.PixelsPerMeter;
var tileCount = defList.Select(x => (int)x.Variants).Sum() + 1;
var tileCount = defList.Select(x => x.Variants + x.EdgeSprites.Count).Sum() + 1;
var dimensionX = (int) Math.Ceiling(Math.Sqrt(tileCount));
var dimensionY = (int) Math.Ceiling((float) tileCount / dimensionX);
@@ -102,11 +113,11 @@ namespace Robust.Client.Map
var imgWidth = dimensionX * tileSize;
var imgHeight = dimensionY * tileSize;
var sheet = new Image<Rgba32>(imgWidth, imgHeight);
var w = (float) sheet.Width;
var h = (float) sheet.Height;
// Add in the missing tile texture sprite as tile texture 0.
{
var w = (float) sheet.Width;
var h = (float) sheet.Height;
ErrorTileRegion = Box2.FromDimensions(
0, (h - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h);
@@ -154,25 +165,98 @@ namespace Robust.Client.Map
var box = new UIBox2i(0, 0, tileSize, tileSize).Translated(new Vector2i(j * tileSize, 0));
image.Blit(box, sheet, point);
var w = (float) sheet.Width;
var h = (float) sheet.Height;
regionList[j] = Box2.FromDimensions(
point.X / w, (h - point.Y - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h);
column++;
if (column >= dimensionX)
{
column = 0;
row++;
}
BumpColumn(ref row, ref column, dimensionX);
}
_tileRegions.Add(def.TileId, regionList);
tileRegs.Add((def.TileId, Direction.Invalid), regionList);
// Edges
if (def.EdgeSprites.Count <= 0)
continue;
foreach (var direction in DirectionExtensions.AllDirections)
{
if (!def.EdgeSprites.TryGetValue(direction, out var edge))
continue;
using (var stream = _manager.ContentFileRead(edge))
{
image = Image.Load<Rgba32>(stream);
}
if (image.Width != tileSize || image.Height != tileSize)
{
throw new NotSupportedException(
$"Unable to load {path}, due to being unable to use tile textures with a dimension other than {tileSize}x{tileSize}.");
}
Angle angle = Angle.Zero;
switch (direction)
{
// Corner sprites
case Direction.SouthEast:
break;
case Direction.NorthEast:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.NorthWest:
angle = new Angle(MathF.PI);
break;
case Direction.SouthWest:
angle = new Angle(MathF.PI * 1.5f);
break;
// Edge sprites
case Direction.South:
break;
case Direction.East:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.North:
angle = new Angle(MathF.PI);
break;
case Direction.West:
angle = new Angle(MathF.PI * 1.5f);
break;
}
if (angle != Angle.Zero)
{
image.Mutate(o => o.Rotate((float)-angle.Degrees));
}
var point = new Vector2i(column * tileSize, row * tileSize);
var box = new UIBox2i(0, 0, tileSize, tileSize);
image.Blit(box, sheet, point);
// If you ever need edge variants then you could just bump this.
var edgeList = new Box2[1];
edgeList[0] = Box2.FromDimensions(
point.X / w, (h - point.Y - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h);
tileRegs.Add((def.TileId, direction), edgeList);
BumpColumn(ref row, ref column, dimensionX);
}
}
_tileRegions = tileRegs.ToFrozenDictionary();
_tileTextureAtlas = Texture.LoadFromImage(sheet, "Tile Atlas");
_sawmill.Debug($"Tile atlas took {sw.Elapsed} to build");
}
private void BumpColumn(ref int row, ref int column, int dimensionX)
{
column++;
if (column >= dimensionX)
{
column = 0;
row++;
}
}
void IPostInjectInit.PostInject()

View File

@@ -29,5 +29,12 @@ namespace Robust.Client.Map
/// </summary>
/// <returns>If null, do not draw the tile at all.</returns>
Box2[]? TileAtlasRegion(int tileType);
/// <summary>
/// Gets the region inside the texture atlas to use to draw a tile type.
/// Also handles edge sprites.
/// </summary>
/// <returns>If null, do not draw the tile at all.</returns>
public Box2[]? TileAtlasRegion(int tileType, Direction direction);
}
}

View File

@@ -1,125 +0,0 @@
using System;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Map;
/// <summary>
/// Draws border sprites for tiles that support them.
/// </summary>
public sealed class TileEdgeOverlay : GridOverlay
{
private readonly IEntityManager _entManager;
private readonly IResourceCache _resource;
private readonly ITileDefinitionManager _tileDefManager;
public TileEdgeOverlay(IEntityManager entManager, IResourceCache resource, ITileDefinitionManager tileDefManager)
{
_entManager = entManager;
_resource = resource;
_tileDefManager = tileDefManager;
ZIndex = -1;
}
protected internal override void Draw(in OverlayDrawArgs args)
{
if (args.MapId == MapId.Nullspace)
return;
var mapSystem = _entManager.System<SharedMapSystem>();
var xformSystem = _entManager.System<SharedTransformSystem>();
var tileSize = Grid.Comp.TileSize;
var tileDimensions = new Vector2(tileSize, tileSize);
var (_, _, worldMatrix, invMatrix) = xformSystem.GetWorldPositionRotationMatrixWithInv(Grid.Owner);
args.WorldHandle.SetTransform(worldMatrix);
var bounds = args.WorldBounds;
bounds = new Box2Rotated(bounds.Box.Enlarged(1), bounds.Rotation, bounds.Origin);
var localAABB = invMatrix.TransformBox(bounds);
var enumerator = mapSystem.GetLocalTilesEnumerator(Grid.Owner, Grid, localAABB, false);
while (enumerator.MoveNext(out var tileRef))
{
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
if (tileDef.EdgeSprites.Count == 0)
continue;
// Get what tiles border us to determine what sprites we need to draw.
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x == 0 && y == 0)
continue;
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
var neighborTile = mapSystem.GetTileRef(Grid.Owner, Grid, neighborIndices);
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
// If it's the same tile then no edge to be drawn.
if (tileRef.Tile.TypeId == neighborTile.Tile.TypeId)
continue;
// Don't draw if the the neighbor tile edges should draw over us (or if we have the same priority)
if (neighborDef.EdgeSprites.Count != 0 && neighborDef.EdgeSpritePriority >= tileDef.EdgeSpritePriority)
continue;
var direction = new Vector2i(x, y).AsDirection();
// No edge tile
if (!tileDef.EdgeSprites.TryGetValue(direction, out var edgePath))
continue;
var texture = _resource.GetResource<TextureResource>(edgePath);
var box = Box2.FromDimensions(neighborIndices, tileDimensions);
var angle = Angle.Zero;
// If we ever need one for both cardinals and corners then update this.
switch (direction)
{
// Corner sprites
case Direction.SouthEast:
break;
case Direction.NorthEast:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.NorthWest:
angle = new Angle(MathF.PI);
break;
case Direction.SouthWest:
angle = new Angle(MathF.PI * 1.5f);
break;
// Edge sprites
case Direction.South:
break;
case Direction.East:
angle = new Angle(MathF.PI / 2f);
break;
case Direction.North:
angle = new Angle(MathF.PI);
break;
case Direction.West:
angle = new Angle(MathF.PI * 1.5f);
break;
}
if (angle == Angle.Zero)
args.WorldHandle.DrawTextureRect(texture.Texture, box);
else
args.WorldHandle.DrawTextureRect(texture.Texture, new Box2Rotated(box, angle, box.Center));
RequiresFlush = true;
}
}
}
args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
}

View File

@@ -30,11 +30,13 @@ namespace Robust.Client.Placement.Modes
return;
}
var mapId = MouseCoords.GetMapId(pManager.EntityManager);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
var mapId = transformSys.GetMapId(MouseCoords);
var snapToEntities = EntitySystem.Get<EntityLookupSystem>().GetEntitiesInRange(MouseCoords, SnapToRange)
var snapToEntities = pManager.EntityManager.System<EntityLookupSystem>()
.GetEntitiesInRange(MouseCoords, SnapToRange)
.Where(entity => pManager.EntityManager.GetComponent<MetaDataComponent>(entity).EntityPrototype == pManager.CurrentPrototype && pManager.EntityManager.GetComponent<TransformComponent>(entity).MapID == mapId)
.OrderBy(entity => (pManager.EntityManager.GetComponent<TransformComponent>(entity).WorldPosition - MouseCoords.ToMapPos(pManager.EntityManager, pManager.EntityManager.System<SharedTransformSystem>())).LengthSquared())
.OrderBy(entity => (transformSys.GetWorldPosition(entity) - transformSys.ToMapCoordinates(MouseCoords).Position).LengthSquared())
.ToList();
if (snapToEntities.Count == 0)
@@ -51,10 +53,11 @@ namespace Robust.Client.Placement.Modes
var closestBounds = component.BaseRSI.Size;
var closestPos = transformSys.GetWorldPosition(closestTransform);
var closestRect =
Box2.FromDimensions(
closestTransform.WorldPosition.X - closestBounds.X / 2f,
closestTransform.WorldPosition.Y - closestBounds.Y / 2f,
closestPos.X - closestBounds.X / 2f,
closestPos.Y - closestBounds.Y / 2f,
closestBounds.X, closestBounds.Y);
var sides = new[]

View File

@@ -21,7 +21,8 @@ namespace Robust.Client.Placement.Modes
{
MouseCoords = ScreenToCursorGrid(mouseScreen);
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
var gridIdOpt = transformSys.GetGrid(MouseCoords);
SnapSize = 1f;
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{

View File

@@ -16,8 +16,10 @@ namespace Robust.Client.Placement.Modes
{
MouseCoords = ScreenToCursorGrid(mouseScreen);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
var tileSize = 1f;
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
var gridIdOpt = transformSys.GetGrid(MouseCoords);
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{

View File

@@ -16,9 +16,11 @@ namespace Robust.Client.Placement.Modes
{
MouseCoords = ScreenToCursorGrid(mouseScreen);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
var tileSize = 1f;
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
var gridIdOpt = transformSys.GetGrid(MouseCoords);
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
var mapGrid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);

View File

@@ -121,8 +121,8 @@ namespace Robust.Client.Placement
{
if (!coordinate.IsValid(pManager.EntityManager))
return; // Just some paranoia just in case
var worldPos = coordinate.ToMapPos(pManager.EntityManager, transformSys);
var worldRot = pManager.EntityManager.GetComponent<TransformComponent>(coordinate.EntityId).WorldRotation + dirAng;
var worldPos = transformSys.ToMapCoordinates(coordinate).Position;
var worldRot = transformSys.GetWorldRotation(coordinate.EntityId) + dirAng;
sprite.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
var rot = args.Viewport.Eye?.Rotation ?? default;
@@ -230,7 +230,7 @@ namespace Robust.Client.Placement
var range = pManager.CurrentPermission!.Range;
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
if (range > 0 && !pManager.EntityManager.GetComponent<TransformComponent>(controlled).Coordinates.InRange(pManager.EntityManager, transformSys, coordinates, range))
if (range > 0 && !transformSys.InRange(pManager.EntityManager.GetComponent<TransformComponent>(controlled).Coordinates, coordinates, range))
return false;
return true;
}
@@ -239,7 +239,7 @@ namespace Robust.Client.Placement
{
var bounds = pManager.ColliderAABB;
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
var mapCoords = coordinates.ToMap(pManager.EntityManager, transformSys);
var mapCoords = transformSys.ToMapCoordinates(coordinates);
var (x, y) = mapCoords.Position;
var collisionBox = Box2.FromDimensions(
@@ -248,7 +248,9 @@ namespace Robust.Client.Placement
bounds.Width,
bounds.Height);
return EntitySystem.Get<SharedPhysicsSystem>().TryCollideRect(collisionBox, mapCoords.MapId);
return pManager.EntityManager
.System<SharedPhysicsSystem>()
.TryCollideRect(collisionBox, mapCoords.MapId);
}
protected Vector2 ScreenToWorld(Vector2 point)
@@ -265,10 +267,8 @@ namespace Robust.Client.Placement
{
var mapCoords = pManager.EyeManager.PixelToMap(coords.Position);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid))
if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out _))
{
return transformSys.ToCoordinates(mapCoords);
}

View File

@@ -2,9 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using Robust.Client.Timing;
using Robust.Client.Utility;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Prototypes;
@@ -56,7 +54,7 @@ namespace Robust.Client.Prototypes
using var _ = _timing.StartStateApplicationArea();
ReloadPrototypes([file]);
Logger.Info($"Reloaded prototypes in {sw.ElapsedMilliseconds} ms");
Sawmill.Info($"Reloaded prototypes in {sw.ElapsedMilliseconds} ms");
#endif
}
}

View File

@@ -71,21 +71,11 @@ namespace Robust.Client.UserInterface.Controls
}
// First, we measure non-stretching children.
var stretching = new List<Control>();
float totalStretchRatio = 0;
foreach (var child in Children)
{
if (!child.Visible)
continue;
var stretch = Vertical ? child.VerticalExpand : child.HorizontalExpand;
if (stretch)
{
totalStretchRatio += child.SizeFlagsStretchRatio;
stretching.Add(child);
continue;
}
child.Measure(availableSize);
if (Vertical)
@@ -102,35 +92,6 @@ namespace Robust.Client.UserInterface.Controls
}
}
if (stretching.Count == 0)
return desiredSize;
// Then we measure stretching children
foreach (var child in stretching)
{
var size = availableSize;
if (Vertical)
{
size.Y *= child.SizeFlagsStretchRatio / totalStretchRatio;
child.Measure(size);
desiredSize.Y += child.DesiredSize.Y;
desiredSize.X = Math.Max(desiredSize.X, child.DesiredSize.X);
}
else
{
size.X *= child.SizeFlagsStretchRatio / totalStretchRatio;
child.Measure(size);
desiredSize.X += child.DesiredSize.X;
desiredSize.Y = Math.Max(desiredSize.Y, child.DesiredSize.Y);
}
// TODO Maybe make BoxContainer.MeasureOverride more rigorous.
// This should check if size < desired size. If it is, treat child as non-stretching (see the code in
// ArrangeOverride). This requires remeasuring all stretching controls + the control that just became
// non-stretching. But the re-measured controls might then become smaller (e.g. rich text wrapping),
// leading to a recursion problem.
}
return desiredSize;
}

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Collections;
using Robust.Shared.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Maths;
@@ -83,9 +84,23 @@ namespace Robust.Client.UserInterface.Controls
_updateScrollbarVisibility();
}
public void Add(IEnumerable<Item> items)
{
foreach (var item in items)
{
if(item.Owner != this) throw new ArgumentException("Item is owned by another ItemList!");
_itemList.Add(item);
item.OnSelected += Select;
item.OnDeselected += Deselect;
}
Recalculate();
}
public void Add(Item item)
{
if (item == null) return;
if(item.Owner != this) throw new ArgumentException("Item is owned by another ItemList!");
_itemList.Add(item);
@@ -93,9 +108,19 @@ namespace Robust.Client.UserInterface.Controls
item.OnSelected += Select;
item.OnDeselected += Deselect;
RecalculateContentHeight();
if (_isAtBottom && ScrollFollowing)
_scrollBar.MoveToEnd();
Recalculate();
}
public void AddItems(IEnumerable<string> texts, Texture? icon = null, bool selectable = true, object? metadata = null)
{
var items = new ValueList<Item>();
foreach (var text in texts)
{
items.Add(new Item(this) {Text = text, Icon = icon, Selectable = selectable, Metadata = metadata});
}
Add(items);
}
public Item AddItem(string text, Texture? icon = null, bool selectable = true, object? metadata = null)
@@ -107,11 +132,15 @@ namespace Robust.Client.UserInterface.Controls
public void Clear()
{
foreach (var item in _itemList.ToArray())
// Handle this manually so we can just clear all at once.
foreach (var item in _itemList)
{
Remove(item);
item.OnSelected -= Select;
item.OnDeselected -= Deselect;
}
_itemList.Clear();
Recalculate();
_totalContentHeight = 0;
}
@@ -125,25 +154,35 @@ namespace Robust.Client.UserInterface.Controls
_itemList.CopyTo(array, arrayIndex);
}
private void InternalRemoveAt(int index)
{
if (_itemList.Count <= index)
return;
// If you modify this then also make sure to update Clear!
var item = _itemList[index];
_itemList.RemoveAt(index);
item.OnSelected -= Select;
item.OnDeselected -= Deselect;
}
public bool Remove(Item item)
{
if (item == null) return false;
var value = _itemList.Remove(item);
item.OnSelected -= Select;
item.OnDeselected -= Deselect;
RecalculateContentHeight();
if (_isAtBottom && ScrollFollowing)
_scrollBar.MoveToEnd();
Recalculate();
return value;
}
public void RemoveAt(int index)
{
Remove(this[index]);
InternalRemoveAt(index);
Recalculate();
}
public IEnumerator<Item> GetEnumerator()
@@ -161,16 +200,24 @@ namespace Robust.Client.UserInterface.Controls
return _itemList.IndexOf(item);
}
public void Insert(int index, Item item)
private void InternalInsert(int index, Item item)
{
if (item == null) return;
if(item.Owner != this) throw new ArgumentException("Item is owned by another ItemList!");
_itemList.Insert(index, item);
item.OnSelected += Select;
item.OnDeselected += Deselect;
}
public void Insert(int index, Item item)
{
InternalInsert(index, item);
Recalculate();
}
private void Recalculate()
{
RecalculateContentHeight();
if (_isAtBottom && ScrollFollowing)
_scrollBar.MoveToEnd();
@@ -191,7 +238,6 @@ namespace Robust.Client.UserInterface.Controls
SetItems(newItems, (a,b) => string.Compare(a.Text, b.Text));
}
/// <inheritdoc />
/// <summary>
/// This variant allows for a custom equality operator to compare items, when
/// comparing the Item text is not desired.
@@ -215,13 +261,13 @@ namespace Robust.Client.UserInterface.Controls
else if (cmpResult > 0)
{
// Item exists in our list, but not in `newItems`. Remove it.
RemoveAt(i);
InternalRemoveAt(i);
i--;
}
else if (cmpResult < 0)
{
// A new entry which doesn't exist in our list. Insert it.
Insert(i + 1, newItems[j]);
InternalInsert(i + 1, newItems[j]);
j--;
}
}
@@ -229,16 +275,18 @@ namespace Robust.Client.UserInterface.Controls
// Any remaining items in our list don't exist in `newItems` so remove them
while (i >= 0)
{
RemoveAt(i);
InternalRemoveAt(i);
i--;
}
// And finally, any remaining items in `newItems` don't exist in our list. Create them.
while (j >= 0)
{
Insert(0, newItems[j]);
InternalInsert(0, newItems[j]);
j--;
}
Recalculate();
}
// Without this attribute, this would compile into a property called "Item", causing problems with the Item class.
@@ -290,9 +338,12 @@ namespace Robust.Client.UserInterface.Controls
public void ClearSelected(int? except = null)
{
foreach (var item in GetSelected())
for (var i = 0; i < _itemList.Count; i++)
{
if(IndexOf(item) == except) continue;
if (i == except)
continue;
var item = _itemList[i];
item.Selected = false;
}
}

View File

@@ -4,6 +4,7 @@ using Robust.Client.Graphics;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Collections;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -15,10 +16,23 @@ namespace Robust.Client.UserInterface.Controls
[Virtual]
public class OutputPanel : Control
{
public const string StyleClassOutputPanelScrollDownButton = "outputPanelScrollDownButton";
[Dependency] private readonly MarkupTagManager _tagManager = default!;
public const string StylePropertyStyleBox = "stylebox";
public bool ShowScrollDownButton
{
get => _showScrollDownButton;
set
{
_showScrollDownButton = value;
_updateScrollButtonVisibility();
}
}
private bool _showScrollDownButton;
private readonly RingBufferList<RichTextEntry> _entries = new();
private bool _isAtBottom = true;
@@ -26,6 +40,7 @@ namespace Robust.Client.UserInterface.Controls
private bool _firstLine = true;
private StyleBox? _styleBoxOverride;
private VScrollBar _scrollBar;
private Button _scrollDownButton;
public bool ScrollFollowing { get; set; } = true;
@@ -43,7 +58,25 @@ namespace Robust.Client.UserInterface.Controls
HorizontalAlignment = HAlignment.Right
};
AddChild(_scrollBar);
_scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
AddChild(_scrollDownButton = new Button()
{
Name = "scrollLiveBtn",
StyleClasses = { StyleClassOutputPanelScrollDownButton },
VerticalAlignment = VAlignment.Bottom,
HorizontalAlignment = HAlignment.Center,
Text = String.Format("⬇ {0} ⬇", Loc.GetString("output-panel-scroll-down-button-text")),
MaxWidth = 300,
Visible = false,
});
_scrollDownButton.OnPressed += _ => ScrollToBottom();
_scrollBar.OnValueChanged += _ =>
{
_isAtBottom = _scrollBar.IsAtEnd;
_updateScrollButtonVisibility();
};
}
public int EntryCount => _entries.Count;
@@ -184,6 +217,7 @@ namespace Robust.Client.UserInterface.Controls
var styleBoxSize = _getStyleBox()?.MinimumSize.Y ?? 0;
_scrollBar.Page = UIScale * (Height - styleBoxSize);
_updateScrollButtonVisibility();
_invalidateEntries();
}
@@ -284,5 +318,10 @@ namespace Robust.Client.UserInterface.Controls
_invalidOnVisible = false;
}
}
private void _updateScrollButtonVisibility()
{
_scrollDownButton.Visible = ShowScrollDownButton && !_isAtBottom;
}
}
}

View File

@@ -1,7 +1,7 @@
<Control xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics">
<BoxContainer Orientation="Vertical">
<OutputPanel Name="Output" VerticalExpand="True" StyleClasses="monospace">
<OutputPanel Name="Output" VerticalExpand="True" StyleClasses="monospace" ShowScrollDownButton="True">
<OutputPanel.StyleBoxOverride>
<gfx:StyleBoxFlat BackgroundColor="#25252add"
ContentMarginLeftOverride="3" ContentMarginRightOverride="3"

View File

@@ -59,6 +59,7 @@ namespace Robust.Client.UserInterface.CustomControls
}
button.EntityLabel.Text = entityLabelText;
button.ActualButton.ToolTip = prototype.Description;
if (prototype == SelectedPrototype)
{

View File

@@ -4,7 +4,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
[Prototype("font")]
[Prototype]
public sealed partial class FontPrototype : IPrototype
{
[IdDataField]

View File

@@ -0,0 +1,7 @@
namespace Robust.Client.UserInterface;
internal static class UIConstants
{
public const string ObsoleteInheritanceMessage =
"Do not inherit from standard UI controls, compose via nesting instead";
}

View File

@@ -103,7 +103,6 @@ internal partial class UserInterfaceManager
return;
}
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
args.PointerLocation.Position - control.GlobalPixelPosition);
@@ -115,10 +114,6 @@ internal partial class UserInterfaceManager
args.Handle();
}
// Attempt to ensure that keybind-up events get raised after a keybind-down.
DebugTools.Assert(!_focusedControls.TryGetValue(args.Function, out var existing)
|| !existing.VisibleInTree
|| args.IsRepeat && existing == control);
_focusedControls[args.Function] = control;
OnKeyBindDown?.Invoke(control);

View File

@@ -5,9 +5,9 @@ using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface;
internal sealed partial class UserInterfaceManager
@@ -61,7 +61,7 @@ internal sealed partial class UserInterfaceManager
Title = string.IsNullOrEmpty(title) ? Loc.GetString("popup-title") : title,
};
var label = new Label { Text = contents };
var label = new RichTextLabel { Text = $"[color=white]{FormattedMessage.EscapeText(contents)}[/color]" };
var vBox = new BoxContainer
{

View File

@@ -2,11 +2,15 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Collections;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Robust.Client.ViewVariables
{
@@ -51,15 +55,19 @@ namespace Robust.Client.ViewVariables
_lastSearch = search;
EntryItemList.ClearSelected();
EntryItemList.Clear();
AddButton.Disabled = true;
var items = new ValueList<string>();
foreach (var component in _entries)
{
if(!string.IsNullOrEmpty(search) && !component.Contains(search, StringComparison.InvariantCultureIgnoreCase))
continue;
EntryItemList.AddItem(component);
items.Add(component);
}
EntryItemList.AddItems(items);
}
private void OnSearchTextChanged(LineEdit.LineEditEventArgs obj)

View File

@@ -36,6 +36,9 @@ public static class Diagnostics
public const string IdUseNonGenericVariant = "RA0030";
public const string IdPreferOtherType = "RA0031";
public const string IdDuplicateDependency = "RA0032";
public const string IdForbidLiteral = "RA0033";
public const string IdObsoleteInheritance = "RA0034";
public const string IdObsoleteInheritanceWithMessage = "RA0035";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");

View File

@@ -14,7 +14,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>enable</ImplicitUsings>
<PolySharpIncludeGeneratedTypes>System.Index;System.Diagnostics.CodeAnalysis.NotNullWhenAttribute;System.Runtime.CompilerServices.IsExternalInit;System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute</PolySharpIncludeGeneratedTypes>
<NoWarn>RS2008</NoWarn>
<NoWarn>RS2008;RS1038</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@@ -35,13 +35,20 @@ public class Generator : IIncrementalGenerator
.Where(static type => type != null);
initContext.RegisterSourceOutput(
dataDefinitions,
static (sourceContext, source) =>
dataDefinitions.Collect(),
static (sourceContext, sources) =>
{
// TODO: deduplicate based on name?
var (name, code) = source!.Value;
var done = new HashSet<string>();
sourceContext.AddSource(name, code);
foreach (var source in sources)
{
var (name, code) = source!.Value;
if (!done.Add(name))
continue;
sourceContext.AddSource(name, code);
}
}
);
}

View File

@@ -297,7 +297,7 @@ namespace Robust.Server
: null;
// Set up the VFS
_resources.Initialize(dataDir);
_resources.Initialize(dataDir, hideUserDataDir: false);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;

View File

@@ -8,6 +8,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -46,11 +47,11 @@ namespace Robust.Server.Console.Commands
bool saveSuccess = _ent.System<MapLoaderSystem>().TrySaveGrid(uid, new ResPath(args[1]));
if(saveSuccess)
{
shell.WriteLine("Save successful. Look in the user data directory.");
shell.WriteLine("Save successful. Look in the user data directory.");
}
else
{
shell.WriteError("Save unsuccessful!");
shell.WriteError("Save unsuccessful!");
}
}
@@ -59,7 +60,7 @@ namespace Robust.Server.Console.Commands
switch (args.Length)
{
case 1:
return CompletionResult.FromHint(Loc.GetString("cmd-hint-savebp-id"));
return CompletionResult.FromHintOptions(CompletionHelper.Components<MapGridComponent>(args[0], _ent), Loc.GetString("cmd-hint-savebp-id"));
case 2:
var opts = CompletionHelper.UserFilePath(args[1], _resource.UserData);
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-savemap-path"));
@@ -159,6 +160,7 @@ namespace Robust.Server.Console.Commands
public sealed class SaveMap : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly IResourceManager _resource = default!;
@@ -169,7 +171,7 @@ namespace Robust.Server.Console.Commands
switch (args.Length)
{
case 1:
return CompletionResult.FromHint(Loc.GetString("cmd-hint-savemap-id"));
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(_entManager), Loc.GetString("cmd-hint-savemap-id"));
case 2:
var opts = CompletionHelper.UserFilePath(args[1], _resource.UserData);
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-savemap-path"));

View File

@@ -4,6 +4,7 @@ using Robust.Server.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
@@ -16,6 +17,20 @@ public sealed class ScaleCommand : LocalizedCommands
[Dependency] private readonly IEntityManager _entityManager = default!;
public override string Command => "scale";
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
switch (args.Length)
{
case 1:
return CompletionResult.FromOptions(CompletionHelper.NetEntities(args[0], entManager: _entityManager));
case 2:
return CompletionResult.FromHint(Loc.GetString("cmd-hint-float"));
default:
return CompletionResult.Empty;
}
}
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)

View File

@@ -204,6 +204,23 @@ namespace Robust.Server.GameObjects
private void HandleEntityNetworkMessage(MsgEntity message)
{
if (_logLateMsgs)
{
var msgT = message.SourceTick;
var cT = _gameTiming.CurTick;
if (msgT < cT)
{
_netEntSawmill.Warning(
"Got late MsgEntity! Diff: {0}, msgT: {2}, cT: {3}, player: {1}, msg: {4}",
(int) msgT.Value - (int) cT.Value,
message.MsgChannel.UserName,
msgT,
cT,
message.SystemMessage);
}
}
_queue.Add(message);
}

View File

@@ -204,7 +204,7 @@ internal struct PvsMetadata
{
DebugTools.AssertEqual(NetEntity, comp.NetEntity);
DebugTools.AssertEqual(VisMask, comp.VisibilityMask);
DebugTools.Assert(LifeStage == comp.EntityLifeStage);
DebugTools.AssertEqual(LifeStage, comp.EntityLifeStage);
DebugTools.Assert(LastModifiedTick == comp.EntityLastModifiedTick || LastModifiedTick.Value == 0);
}
}

View File

@@ -268,7 +268,6 @@ namespace Robust.Server.Physics
newGrids[i] = newGridUid;
// Keep same origin / velocity etc; this makes updating a lot faster and easier.
_xformSystem.SetWorldPosition(newGridXform, gridPos);
_xformSystem.SetWorldPositionRotation(newGridUid, gridPos, gridRot, newGridXform);
var splitBody = _bodyQuery.GetComponent(newGridUid);
_physics.SetLinearVelocity(newGridUid, mapBody.LinearVelocity, body: splitBody);
@@ -290,7 +289,7 @@ namespace Robust.Server.Physics
}
_maps.SetTiles(newGrid.Owner, newGrid.Comp, tileData);
DebugTools.Assert(_mapManager.IsGrid(newGridUid), "A split grid had no tiles?");
DebugTools.Assert(_gridQuery.HasComp(newGridUid), "A split grid had no tiles?");
// Set tiles on new grid + update anchored entities
foreach (var node in group)

View File

@@ -0,0 +1,15 @@
namespace Robust.Server.ServerStatus;
/// <summary>
/// Helper functions for working with <see cref="IStatusHandlerContext"/>.
/// </summary>
public static class StatusExt
{
/// <summary>
/// Add <c>Access-Control-Allow-Origin: *</c> to the response headers for this request.
/// </summary>
public static void AddAllowOriginAny(this IStatusHandlerContext context)
{
context.ResponseHeaders.Add("Access-Control-Allow-Origin", "*");
}
}

View File

@@ -61,6 +61,7 @@ namespace Robust.Server.ServerStatus
OnStatusRequest?.Invoke(jObject);
context.AddAllowOriginAny();
await context.RespondJsonAsync(jObject);
return true;
@@ -121,6 +122,7 @@ namespace Robust.Server.ServerStatus
OnInfoRequest?.Invoke(jObject);
context.AddAllowOriginAny();
await context.RespondJsonAsync(jObject);
return true;

View File

@@ -93,6 +93,7 @@ namespace Robust.Shared.Maths
private const double CardinalSegment = 2 * Math.PI / 4.0; // Cut the circle into 4 pieces
private const double CardinalOffset = CardinalSegment / 2.0; // offset the pieces by 1/2 their size
[Pure]
public readonly Direction GetCardinalDir()
{
var ang = Theta % (2 * Math.PI);
@@ -167,6 +168,7 @@ namespace Robust.Shared.Maths
/// <summary>
/// Removes revolutions from a positive or negative angle to make it as small as possible.
/// </summary>
[Pure]
public readonly Angle Reduced()
{
return new(Reduce(Theta));
@@ -213,11 +215,13 @@ namespace Robust.Shared.Maths
return !(a == b);
}
[Pure]
public readonly Angle Opposite()
{
return new Angle(FlipPositive(Theta-Math.PI));
}
[Pure]
public readonly Angle FlipPositive()
{
return new(FlipPositive(Theta));

View File

@@ -682,6 +682,7 @@ namespace Robust.Shared.Maths
/// (which is copied to the output's Alpha value).
/// Each has a range of 0.0 to 1.0.
/// </param>
[Obsolete("The HCY color space is mathematically incorrect and these functions are broken, use something else")]
public static Color FromHcy(Vector4 hcy)
{
var hue = hcy.X * 360.0f;
@@ -750,6 +751,7 @@ namespace Robust.Shared.Maths
/// </returns>
/// <param name="rgb">Color value to convert.</param>
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
[Obsolete("The HCY color space is mathematically incorrect and these functions are broken, use something else")]
public static Vector4 ToHcy(Color rgb)
{
var max = MathF.Max(rgb.R, MathF.Max(rgb.G, rgb.B));

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Immutable;
using System.Diagnostics.Contracts;
using System.Numerics;
namespace Robust.Shared.Maths
@@ -36,6 +38,21 @@ namespace Robust.Shared.Maths
/// </summary>
public static class DirectionExtensions
{
/// <summary>
/// A list of all cardinal and diagonal <see cref="Direction"/>s.
/// </summary>
public static readonly ImmutableArray<Direction> AllDirections =
[
Direction.South,
Direction.SouthEast,
Direction.East,
Direction.NorthEast,
Direction.North,
Direction.NorthWest,
Direction.West,
Direction.SouthWest
];
private const double Segment = 2 * Math.PI / 8.0; // Cut the circle into 8 pieces
public static Direction AsDir(this DirectionFlag directionFlag)
@@ -238,6 +255,7 @@ namespace Robust.Shared.Maths
/// </summary>
/// <param name="dir"></param>
/// <returns></returns>
[Pure]
public static Angle ToAngle(this Direction dir)
{
var ang = Segment * (int) dir;

View File

@@ -0,0 +1,11 @@
using System;
namespace Robust.Shared.Analyzers;
/// <summary>
/// Marks that values used for this parameter should not be literal values.
/// This helps prevent magic numbers/strings/etc, by indicating that values
/// should either be wrapped (for validation) or defined as constants or readonly statics.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class ForbidLiteralAttribute : Attribute;

View File

@@ -0,0 +1,28 @@
using System;
namespace Robust.Shared.Analyzers;
/// <summary>
/// Indicates that the ability to <i>inherit</i> this type is obsolete, and attempting to do so should give a warning.
/// </summary>
/// <remarks>
/// This is useful to gracefully deal with types that should never have had <see cref="VirtualAttribute"/>.
/// </remarks>
/// <seealso cref="VirtualAttribute"/>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class ObsoleteInheritanceAttribute : Attribute
{
/// <summary>
/// An optional message provided alongside this obsoletion.
/// </summary>
public string? Message { get; }
public ObsoleteInheritanceAttribute()
{
}
public ObsoleteInheritanceAttribute(string message)
{
Message = message;
}
}

View File

@@ -9,6 +9,7 @@ namespace Robust.Shared.Analyzers;
/// Robust uses analyzers to prevent accidental usage of non-sealed classes:
/// a class must be either marked [Virtual], abstract, or sealed.
/// </remarks>
/// <seealso cref="ObsoleteInheritanceAttribute"/>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class VirtualAttribute : Attribute
{

View File

@@ -8,7 +8,7 @@ namespace Robust.Shared.Audio;
/// Contains audio defaults to set for sounds.
/// This can be used by <see cref="Content.Shared.Audio.SharedContentAudioSystem"/> to apply an audio preset.
/// </summary>
[Prototype("audioPreset")]
[Prototype]
public sealed partial class AudioPresetPrototype : IPrototype
{
[IdDataField]

View File

@@ -6,7 +6,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Shared.Audio;
[Prototype("soundCollection")]
[Prototype]
public sealed partial class SoundCollectionPrototype : IPrototype
{
[ViewVariables]

View File

@@ -968,6 +968,13 @@ namespace Robust.Shared
public static readonly CVarDef<string> RenderFOVColor =
CVarDef.Create("render.fov_color", Color.Black.ToHex(), CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Whether to render tile edges, which is where some tiles can partially overlap other adjacent tiles on a grid.
/// E.g., snow tiles partly extending beyond their own tile to blend together with different adjacent tiles types.
/// </summary>
public static readonly CVarDef<bool> RenderTileEdges =
CVarDef.Create("render.tile_edges", true, CVar.CLIENTONLY);
/*
* CONTROLS
*/
@@ -1874,5 +1881,12 @@ namespace Robust.Shared
/// </summary>
public static readonly CVarDef<int> ToolshedNearbyEntitiesLimit =
CVarDef.Create("toolshed.nearby_entities_limit", 5, CVar.SERVER | CVar.REPLICATED);
/*
* Localization
*/
public static readonly CVarDef<string> LocCultureName =
CVarDef.Create("loc.culture_name", "en-US", CVar.ARCHIVE);
}
}

View File

@@ -1,6 +1,7 @@
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Robust.Shared.ComponentTrees;
@@ -13,20 +14,23 @@ namespace Robust.Shared.ComponentTrees;
/// </remarks>
internal sealed class RecursiveMoveSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public delegate void TreeRecursiveMoveEventHandler(EntityUid uid, TransformComponent xform);
public event TreeRecursiveMoveEventHandler? OnTreeRecursiveMove;
private EntityQuery<MapComponent> _mapQuery;
private EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<TransformComponent> _xformQuery;
bool _subscribed = false;
private bool _subscribed = false;
public override void Initialize()
{
base.Initialize();
_gridQuery = GetEntityQuery<MapGridComponent>();
_mapQuery = GetEntityQuery<MapComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
}
@@ -52,8 +56,8 @@ internal sealed class RecursiveMoveSystem : EntitySystem
if (args.Component.MapUid == args.Sender || args.Component.GridUid == args.Sender)
return;
DebugTools.Assert(!_mapManager.IsMap(args.Sender));
DebugTools.Assert(!_mapManager.IsGrid(args.Sender));
DebugTools.Assert(!_mapQuery.HasComp(args.Sender));
DebugTools.Assert(!_gridQuery.HasComp(args.Sender));
AnythingMovedSubHandler(args.Sender, args.Component);
}

View File

@@ -37,7 +37,7 @@ sealed class AddMapCommand : LocalizedEntityCommands
sealed class RemoveMapCommand : LocalizedCommands
{
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IEntitySystemManager _systems = default!;
public override string Command => "rmmap";
public override bool RequireServerOrSingleplayer => true;
@@ -51,14 +51,15 @@ sealed class RemoveMapCommand : LocalizedCommands
}
var mapId = new MapId(int.Parse(args[0]));
var mapSystem = _systems.GetEntitySystem<SharedMapSystem>();
if (!_map.MapExists(mapId))
if (!mapSystem.MapExists(mapId))
{
shell.WriteError($"Map {mapId.Value} does not exist.");
return;
}
_map.DeleteMap(mapId);
mapSystem.DeleteMap(mapId);
shell.WriteLine($"Map {mapId.Value} was removed.");
}
}

View File

@@ -20,6 +20,7 @@ internal sealed class TeleportCommand : LocalizedEntityCommands
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
public override string Command => "tp";
public override bool RequireServerOrSingleplayer => true;
@@ -46,7 +47,7 @@ internal sealed class TeleportCommand : LocalizedEntityCommands
else
mapId = transform.MapID;
if (!_map.MapExists(mapId))
if (!_mapSystem.MapExists(mapId))
{
shell.WriteError($"Map {mapId} doesn't exist!");
return;
@@ -60,13 +61,26 @@ internal sealed class TeleportCommand : LocalizedEntityCommands
}
else
{
var mapEnt = _map.GetMapEntityIdOrThrow(mapId);
_transform.SetWorldPosition((entity, transform), position);
_transform.SetParent(entity, transform, mapEnt);
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
{
_transform.SetWorldPosition((entity, transform), position);
_transform.SetParent(entity, transform, mapEnt.Value);
}
}
shell.WriteLine($"Teleported {shell.Player} to {mapId}:{posX},{posY}.");
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 => CompletionResult.FromHint("<x>"),
2 => CompletionResult.FromHint("<y>"),
3 => CompletionResult.FromHintOptions(CompletionHelper.MapIds(_entityManager), "[MapId]"),
_ => CompletionResult.Empty
};
}
}
public sealed class TeleportToCommand : LocalizedEntityCommands

View File

@@ -71,7 +71,7 @@ namespace Robust.Shared.Containers
#if DEBUG
// TODO make this a proper debug assert when gun code no longer fudges client-side spawn prediction.
if (entMan.IsClientSide(toInsert) && !entMan.IsClientSide(Owner) && Manager.NetSyncEnabled)
if (entMan.IsClientSide(toInsert) && !entMan.IsClientSide(Owner) && Manager.NetSyncEnabled && !entMan.HasComponent<PredictedSpawnComponent>(toInsert))
Logger.Warning("Inserting a client-side entity into a networked container slot. This will block the container slot and may cause issues.");
#endif
ContainedEntity = toInsert;

View File

@@ -530,6 +530,39 @@ namespace Robust.Shared.Containers
return false;
}
/// <summary>
/// Returns the full chain of containers containing the entity passed in, from innermost to outermost.
/// </summary>
/// <remarks>
/// The resulting collection includes the container directly containing the entity (if any),
/// the container containing that container, and so on until reaching the outermost container.
/// </remarks>
public IEnumerable<BaseContainer> GetContainingContainers(Entity<TransformComponent?> ent)
{
if (!ent.Owner.IsValid())
yield break;
if (!Resolve(ent, ref ent.Comp))
yield break;
var child = ent.Owner;
var parent = ent.Comp.ParentUid;
while (parent.IsValid())
{
if (((MetaQuery.GetComponent(child).Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) &&
_managerQuery.TryGetComponent(parent, out var conManager) &&
TryGetContainingContainer(parent, child, out var parentContainer, conManager))
{
yield return parentContainer;
}
var parentXform = TransformQuery.GetComponent(parent);
child = parent;
parent = parentXform.ParentUid;
}
}
/// <summary>
/// Gets the top-most container in the hierarchy for this entity, if it exists.
/// </summary>

View File

@@ -60,9 +60,7 @@ namespace Robust.Shared.ContentPack
internal string GetPath(ResPath relPath)
{
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()))
// Sanitise platform-specific path and standardize it for engine use.
.Replace(Path.DirectorySeparatorChar, '/');
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
}
/// <inheritdoc />

View File

@@ -14,7 +14,11 @@ namespace Robust.Shared.ContentPack
/// The directory to use for user data.
/// If null, a virtual temporary file system is used instead.
/// </param>
void Initialize(string? userData);
/// <param name="hideUserDataDir">
/// If true, <see cref="IWritableDirProvider.RootDir"/> will be hidden on
/// <see cref="IResourceManager.UserData"/>.
/// </param>
void Initialize(string? userData, bool hideUserDataDir);
/// <summary>
/// Mounts a single stream as a content file. Useful for unit testing.

View File

@@ -13,7 +13,7 @@ namespace Robust.Shared.ContentPack
{
/// <summary>
/// The root path of this provider.
/// Can be null if it's a virtual provider.
/// Can be null if it's a virtual provider or the path is protected (e.g. on the client).
/// </summary>
string? RootDir { get; }

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
{
@@ -63,5 +64,27 @@ namespace Robust.Shared.ContentPack
!OperatingSystem.IsWindows()
&& !OperatingSystem.IsMacOS();
internal static string SafeGetResourcePath(string baseDir, ResPath path)
{
var relSysPath = path.ToRelativeSystemPath();
if (relSysPath.Contains("\\..") || relSysPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
var retPath = Path.GetFullPath(Path.Join(baseDir, relSysPath));
// better safe than sorry check
if (!retPath.StartsWith(baseDir))
{
// Allow path to match if it's just missing the directory separator at the end.
if (retPath != baseDir.TrimEnd('\\'))
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return retPath;
}
}
}

View File

@@ -41,13 +41,13 @@ namespace Robust.Shared.ContentPack
public IWritableDirProvider UserData { get; private set; } = default!;
/// <inheritdoc />
public virtual void Initialize(string? userData)
public virtual void Initialize(string? userData, bool hideRootDir)
{
Sawmill = _logManager.GetSawmill("res");
if (userData != null)
{
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
}
else
{
@@ -273,8 +273,6 @@ namespace Robust.Shared.ContentPack
public IEnumerable<string> ContentGetDirectoryEntries(ResPath path)
{
ArgumentNullException.ThrowIfNull(path, nameof(path));
if (!path.IsRooted)
throw new ArgumentException("Path is not rooted", nameof(path));
@@ -381,6 +379,10 @@ namespace Robust.Shared.ContentPack
{
var rootDir = loader.GetPath(new ResPath(@"/"));
// TODO: GET RID OF THIS.
// This code shouldn't be passing OS disk paths through ResPath.
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
yield return new ResPath(rootDir);
}
}

View File

@@ -497,6 +497,23 @@ Types:
DebuggerVisualizerAttribute: { All: True }
Stopwatch: { All: True }
System.Globalization:
# all the calendars are here, screw making our own.
Calendar: { All: True }
ChineseLunisolarCalendar: { All: True }
GregorianCalendar: { All: True }
HebrewCalendar: { All: True }
HijriCalendar: { All: True }
JapaneseCalendar: { All: True }
JapaneseLunisolarCalendar: { All: True }
JulianCalendar: { All: True }
KoreanCalendar: { All: True }
KoreanLunisolarCalendar: { All: True }
PersianCalendar: { All: True }
TaiwanCalendar: { All: True }
TaiwanLunisolarCalendar: { All: True }
ThaiBuddhistCalendar: { All: True }
UmAlQuraCalendar: { All: True }
#end calendars
CompareOptions: { }
CultureInfo: { All: True }
DateTimeStyles: { All: True } # Enum
@@ -1071,6 +1088,7 @@ Types:
DateTime: { All: True }
DateTimeKind: { } # Enum
DateTimeOffset: { All: True }
DayOfWeek: { } # Enum
Decimal: { All: True }
Delegate:
Methods:

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