mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
186 Commits
v248.0.3-s
...
v261.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b8057acf0 | ||
|
|
bec3caa5da | ||
|
|
ea6126563b | ||
|
|
00494ad9eb | ||
|
|
6672b7b1bd | ||
|
|
8dc55e8748 | ||
|
|
44ea2cd396 | ||
|
|
2c5604432b | ||
|
|
c696466522 | ||
|
|
01bb98e400 | ||
|
|
af08e747de | ||
|
|
8c35c2c380 | ||
|
|
6d46d3f4a5 | ||
|
|
50e06e43fa | ||
|
|
986b0f979d | ||
|
|
a51d786dee | ||
|
|
5f5fed5d6c | ||
|
|
e475cc7898 | ||
|
|
ee8ea4ec3b | ||
|
|
7482451ec4 | ||
|
|
dddf5cd2fb | ||
|
|
01979c451d | ||
|
|
181a5ef0b4 | ||
|
|
e7c7011cc0 | ||
|
|
dc97615fd4 | ||
|
|
3b4944376b | ||
|
|
fa6bd8f7ba | ||
|
|
2398cbcf26 | ||
|
|
38ce48a83f | ||
|
|
4e7de2f272 | ||
|
|
b61075c660 | ||
|
|
7b571dc80e | ||
|
|
f1c76ca899 | ||
|
|
84dcd658aa | ||
|
|
a634d6bd04 | ||
|
|
36f9df3079 | ||
|
|
824c018a69 | ||
|
|
4b6b688c72 | ||
|
|
71df25b251 | ||
|
|
be14a3c249 | ||
|
|
3c2a4d5c79 | ||
|
|
44180b3ee0 | ||
|
|
bb0e77e937 | ||
|
|
684b9bc852 | ||
|
|
9f3db6693e | ||
|
|
40d869948d | ||
|
|
5c97b15849 | ||
|
|
3d8a9a41fa | ||
|
|
92fc8722da | ||
|
|
73f6555624 | ||
|
|
2ac7bc3ce4 | ||
|
|
05cb4bb1c9 | ||
|
|
a393efc87a | ||
|
|
4d47cfa1a6 | ||
|
|
2b1d755d9f | ||
|
|
db7de0a99f | ||
|
|
47f18703af | ||
|
|
97c1548301 | ||
|
|
cd97f1583f | ||
|
|
5fbe25ec9d | ||
|
|
516ee47b51 | ||
|
|
89be682e24 | ||
|
|
6086076559 | ||
|
|
5bd90c908a | ||
|
|
a3d0921cc9 | ||
|
|
15d5b9aa02 | ||
|
|
d24854d94f | ||
|
|
b3cf427013 | ||
|
|
c458abdc69 | ||
|
|
c76444a33f | ||
|
|
4754661467 | ||
|
|
2a8b776ee9 | ||
|
|
7d8e5a5841 | ||
|
|
8e416e4519 | ||
|
|
65f74943d3 | ||
|
|
eb5ed12270 | ||
|
|
c43b7b16c0 | ||
|
|
aee03f0805 | ||
|
|
cfd2b03248 | ||
|
|
8905a3fe14 | ||
|
|
a878da5b80 | ||
|
|
806c23e034 | ||
|
|
e80f5d13a1 | ||
|
|
a6905151b6 | ||
|
|
e742f021fa | ||
|
|
62b4714f1f | ||
|
|
1d0404953f | ||
|
|
d0da13f895 | ||
|
|
ff23f98b26 | ||
|
|
ccfef2a786 | ||
|
|
62ce9724fc | ||
|
|
3bbe0e7f44 | ||
|
|
addd8b5bdd | ||
|
|
03f8d4d3e0 | ||
|
|
728d541ca5 | ||
|
|
4cbce064b8 | ||
|
|
93bb7b1532 | ||
|
|
3ba91d2ed0 | ||
|
|
8f9e0f6bab | ||
|
|
3ed408de5d | ||
|
|
2f85408f8f | ||
|
|
f49b01b1b7 | ||
|
|
3ce764311d | ||
|
|
adf0b6ae78 | ||
|
|
04406311dc | ||
|
|
ee45a608b9 | ||
|
|
077c91a54b | ||
|
|
2ac17009ee | ||
|
|
a73d1b6666 | ||
|
|
e5d6f194be | ||
|
|
72d893dec5 | ||
|
|
191d7ab81c | ||
|
|
65d2f2dd2f | ||
|
|
02b451db2a | ||
|
|
d5d4584e11 | ||
|
|
b146b1b82c | ||
|
|
2f8f4f2f7a | ||
|
|
7365a59bd9 | ||
|
|
37560f663b | ||
|
|
bd489e9218 | ||
|
|
33166e8866 | ||
|
|
ed16032280 | ||
|
|
cf785c886b | ||
|
|
6f28c396cf | ||
|
|
b631f408f2 | ||
|
|
34f4cf9452 | ||
|
|
6a4e4cf3b4 | ||
|
|
8aefa5c53e | ||
|
|
2cfc981aa3 | ||
|
|
4987c324d9 | ||
|
|
5450ddd0ba | ||
|
|
378a10678c | ||
|
|
2e0735b92f | ||
|
|
5756d15333 | ||
|
|
b6f74b8dea | ||
|
|
3800c5707e | ||
|
|
8f49785b4e | ||
|
|
f274de0f10 | ||
|
|
e128338f9d | ||
|
|
588c46273e | ||
|
|
919de8ce0e | ||
|
|
af27d2d872 | ||
|
|
45bb8740a0 | ||
|
|
4a24539629 | ||
|
|
7536c4ec68 | ||
|
|
9268c8629d | ||
|
|
0bc0cafe64 | ||
|
|
8891f3fa0a | ||
|
|
4f96c2d233 | ||
|
|
ab55d5b2f2 | ||
|
|
806c5b694b | ||
|
|
6898053dbd | ||
|
|
ae625ebad8 | ||
|
|
3c754a4f49 | ||
|
|
d84cb6327c | ||
|
|
4bfd92dbc5 | ||
|
|
c7d228c223 | ||
|
|
f244c94905 | ||
|
|
01cac6465b | ||
|
|
5a5f238d9a | ||
|
|
089224cd44 | ||
|
|
9f807f1ad2 | ||
|
|
4be95ea375 | ||
|
|
03010bf4be | ||
|
|
dacaa974d4 | ||
|
|
9f73e0398a | ||
|
|
ccc383b1bf | ||
|
|
ceb59402a1 | ||
|
|
5a6b29fcd2 | ||
|
|
6b87cd1e1c | ||
|
|
cf2d6a1dbf | ||
|
|
f8c838f425 | ||
|
|
7405904041 | ||
|
|
2eeebab275 | ||
|
|
2856bb3626 | ||
|
|
be0189748b | ||
|
|
4529a7569a | ||
|
|
2b16e4db96 | ||
|
|
64f2245194 | ||
|
|
1029047e2f | ||
|
|
45dc9ad80e | ||
|
|
54ad808eea | ||
|
|
37c75df6a2 | ||
|
|
e93c1fae61 | ||
|
|
cd0a35f542 | ||
|
|
80f2dc6dd3 |
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -71,6 +71,6 @@
|
||||
</PropertyGroup>
|
||||
<Exec
|
||||
Condition="'$(_RobustUseExternalMSBuild)' == 'true'"
|
||||
Command=""$(DOTNET_HOST_PATH)" msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
|
||||
Command=""$(DOTNET_HOST_PATH)" msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false /p:IntermediateOutputPath="$(IntermediateOutputPath.TrimEnd('\'))/""/>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
414
RELEASE-NOTES.md
414
RELEASE-NOTES.md
@@ -1,4 +1,4 @@
|
||||
# Release notes for RobustToolbox.
|
||||
# Release notes for RobustToolbox.
|
||||
|
||||
<!--
|
||||
NOTE: automatically updated sometimes by version.py.
|
||||
@@ -54,10 +54,418 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 248.0.3-source-gen-debug
|
||||
## 261.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Implement IEquatable for ResolvedPathSpecifier & ResolvedCollectionSpecifier.
|
||||
* Add NearestChunkEnumerator.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix static entities not having the center of mass updated.
|
||||
* Fix TryQueueDelete.
|
||||
* Fix tpto potentially parenting grids to non-map entities.
|
||||
|
||||
### Other
|
||||
|
||||
* TileChangedEvent is now raised once in clientside grid state handling rather than per tile.
|
||||
* Removed ITileDefinition.ID as it was redundant.
|
||||
* Change the lifestage checks on predicted entity deletion to check for terminating.
|
||||
|
||||
### Internal
|
||||
|
||||
* Update some `GetComponentName<T>` uses to generic.
|
||||
|
||||
|
||||
## 248.0.2-source-gen-debug
|
||||
## 261.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Automatically create logger sawmills for `UIController`s similar to `EntitySystem`s.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix physics forces not auto-clearing / respecting the cvar.
|
||||
|
||||
### Internal
|
||||
|
||||
* Cleanup more compiler warnings in unit tests.
|
||||
|
||||
|
||||
## 261.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Remove unused TryGetContainingContainer override.
|
||||
* Stop recursive FrameUpdates for controls that are not visible.
|
||||
* Initialize LocMgr earlier in the callstack for GameController.
|
||||
* Fix FastNoiseLise fractal bounding and remove its DataField property as it should be derived on other properties updating.
|
||||
* Make RaiseMoveEvent internal.
|
||||
* MovedGridsComponent and PhysicsMapComponent are now purged and properties on `SharedPhysicsSystem`. Additionally the TransformComponent for Awake entities is stored alongside the PhysicsComponent for them.
|
||||
* TransformComponent is now stored on physics contacts.
|
||||
* Gravity2DComponent and Gravity2DController were moved to SharedPhysicsSystem.
|
||||
|
||||
### New features
|
||||
|
||||
* `IFileDialogManager` now allows specifying `FileAccess` and `FileShare` modes.
|
||||
* Add Intersects and Enlarged to Box2i in line with Box2.
|
||||
* Make `KeyFrame`s on `AnimationTrackProperty` public settable.
|
||||
* Add the spawned entities to a returned array from `SpawnEntitiesAttachedTo`.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed SDL3 file dialog implementation having a memory leak and not opening files read-write.
|
||||
* Fix GetMapLinearVelocity.
|
||||
|
||||
### Other
|
||||
|
||||
* `uploadfile` and `loadprototype` commands now only open files with read access.
|
||||
* Optimize `ToMapCoordinates`.
|
||||
|
||||
### Internal
|
||||
|
||||
* Cleanup on internals of `IFileDialogManager`, removing duplicate code.
|
||||
* Fix Contacts not correctly being marked as `Touching` while contact is ongoing.
|
||||
|
||||
|
||||
## 260.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Add `StringBuilder.Insert(int, string)` to sandbox.
|
||||
* Add the WorldNormal to the StartCollideEvent.
|
||||
|
||||
|
||||
## 260.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* `ComponentFactory` is now exposed to `EntitySystem` as `Factory`
|
||||
|
||||
### Other
|
||||
|
||||
* Cleanup warnings in PLacementManager
|
||||
* Cleanup warnings in Clide.Sprite
|
||||
|
||||
## 260.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Fix / change `StartCollideEvent.WorldPoint` to return all points for the collision which may be up to 2 instead of 1.
|
||||
|
||||
### New features
|
||||
|
||||
* Add SpriteSystem dependency to VisualizerSystem.
|
||||
* Add Vertical property to progress bars
|
||||
* Add some `EntProtoId` overloads for group entity spawn methods.
|
||||
|
||||
|
||||
## 259.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* TileChangedEvent now has an array of tile changed entries rather than raising an individual event for every single tile changed.
|
||||
|
||||
### Other
|
||||
|
||||
* `Entity<T>` methods were marked as `readonly` as appropriate.
|
||||
|
||||
|
||||
## 258.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix static physics bodies not generating contacts if they spawn onto sleeping bodies.
|
||||
|
||||
|
||||
## 258.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `IMarkupTag` and related methods in `MarkupTagManager` have been obsoleted and should be replaced with the new `IMarkupTagHandler` interface. Various engine tags (e.g., `BoldTag`, `ColorTag`, etc) no longer implement the old interface.
|
||||
|
||||
### New features
|
||||
|
||||
* Add IsValidPath to ResPath and make some minor performance improvements.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* OutputPanel and RichTextLabel now remove controls associated with rich text tags when the text is updated.
|
||||
* Fix `SpriteComponent.Visible` datafield not being read from yaml.
|
||||
* Fix container state handling not forcing inserts.
|
||||
|
||||
### Other
|
||||
|
||||
* `SpriteSystem.LayerMapReserve()` no longer throws an exception if the specified layer already exists. This makes it behave like the obsoleted `SpriteComponent.LayerMapReserveBlank()`.
|
||||
|
||||
|
||||
## 257.0.2
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix unshaded sprite layers not rendering correctly.
|
||||
|
||||
|
||||
## 257.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix sprite layer bounding box calculations. This was causing various sprite rendering & render-tree lookup issues.
|
||||
|
||||
|
||||
## 257.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The client will now automatically pause any entities that leave their PVS range.
|
||||
* Contacts for terminating entities no longer raise wake events.
|
||||
|
||||
### New features
|
||||
|
||||
* Added `IPrototypeManager.IsIgnored()` for checking whether a given prototype kind has been marked as ignored via `RegisterIgnore()`.
|
||||
* Added `PoolManager` & `TestPair` classes to `Robust.UnitTesting`. These classes make it easier to create & use pooled server/client instance pairs in integration tests.
|
||||
* Catch NotYamlSerializable DataFields with an analyzer.
|
||||
* Optimized RSI preloading and texture atlas creation.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix clients unintentionally un-pausing paused entities that re-enter pvs range
|
||||
|
||||
### Other
|
||||
|
||||
* The yaml prototype id serialiser now provides better feedback when trying to validate an id for a prototype kind that has been ignored via `IPrototypeManager.RegisterIgnore()`
|
||||
* Several SpriteComponent methods have been marked as obsolete, and should be replaced with new methods in SpriteSystem.
|
||||
* Rotation events no longer check for grid traversal.
|
||||
|
||||
|
||||
## 256.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* `ITypeReaderWriter<TType, TNode>` has been removed due to being unused. Implement `ITypeSerializer<TType, TNode>` instead
|
||||
* Moved AsNullable extension methods to the Entity struct.
|
||||
|
||||
### New features
|
||||
|
||||
* Add DevWindow tab to show all loaded textures.
|
||||
* Add Vector2i / bitmask converfsion helpers.
|
||||
* Allow texture preload to be skipped for some textures.
|
||||
* Check audio file signatures instead of extensions.
|
||||
* Add CancellationTokenRegistration to sandbox.
|
||||
* Add the ability to serialize TimeSpan from text.
|
||||
* Add support for rotated / mirrored tiles.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix yaml hot reloading.
|
||||
* Fix a linear dictionary lookup in PlacementManager.
|
||||
|
||||
### Other
|
||||
|
||||
* Make ItemList not run deselection callback on all items if they aren't selected.
|
||||
* Cleanup warnings for CS0649 & CS0414.
|
||||
|
||||
### Internal
|
||||
|
||||
* Move PointLight component states to shared.
|
||||
|
||||
|
||||
## 255.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* The client localisation manager now supports hot-reloading ftl files.
|
||||
* TransformSystem can now raise `GridUidChangedEvent` and `MapUidChangedEvent` when a entity's grid or map changes. This event is only raised if the `ExtraTransformEvents` metadata flag is enabled.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed a server crash due to a `NullReferenceException` in PVS system when a player's local entity is also one of their view subscriptions.
|
||||
* Fix CompileRobustXamlTask for benchmarks.
|
||||
* .ftl files will now hot reload.
|
||||
* Fix placementmanager sometimes not clearing.
|
||||
|
||||
### Other
|
||||
|
||||
* Container events are now documented.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -9,11 +9,16 @@ entity-spawn-window-override-menu-tooltip = Override placement
|
||||
## TileSpawnWindow
|
||||
|
||||
tile-spawn-window-title = Place Tiles
|
||||
tile-spawn-window-mirror-button-text = Mirror Tiles
|
||||
|
||||
## Console
|
||||
|
||||
console-line-edit-placeholder = Command Here
|
||||
|
||||
## OutputPanel
|
||||
|
||||
output-panel-scroll-down-button-text = Scroll Down
|
||||
|
||||
## Common Used
|
||||
|
||||
window-erase-button-text = Erase Mode
|
||||
|
||||
10
Resources/Locale/en-US/dev-window.ftl
Normal file
10
Resources/Locale/en-US/dev-window.ftl
Normal file
@@ -0,0 +1,10 @@
|
||||
## "Textures" dev window tab
|
||||
|
||||
dev-window-tab-textures-title = Textures
|
||||
dev-window-tab-textures-reload = Reload
|
||||
dev-window-tab-textures-filter = Filter
|
||||
dev-window-tab-textures-summary = Total (est): { $bytes }
|
||||
dev-window-tab-textures-info = Width: { $width } Height: { $height }
|
||||
PixelType: { $pixelType } sRGB: { $srgb }
|
||||
Name: { $name }
|
||||
Est. memory usage: { $bytes }
|
||||
@@ -21,47 +21,53 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
namespace Robust.Shared.ViewVariables
|
||||
{
|
||||
public sealed class ViewVariablesAttribute : Attribute
|
||||
{
|
||||
public readonly VVAccess Access = VVAccess.ReadOnly;
|
||||
|
||||
public ViewVariablesAttribute() { }
|
||||
|
||||
public ViewVariablesAttribute(VVAccess access)
|
||||
{
|
||||
Access = access;
|
||||
}
|
||||
}
|
||||
public enum VVAccess : byte
|
||||
{
|
||||
ReadOnly = 0,
|
||||
ReadWrite = 1,
|
||||
}
|
||||
}
|
||||
|
||||
namespace Robust.Shared.Serialization.Manager.Attributes
|
||||
{
|
||||
public class DataFieldBaseAttribute : Attribute;
|
||||
public class DataFieldAttribute : DataFieldBaseAttribute;
|
||||
public sealed class DataDefinitionAttribute : Attribute;
|
||||
public sealed class NotYamlSerializableAttribute : Attribute;
|
||||
}
|
||||
""";
|
||||
|
||||
[Test]
|
||||
public async Task Test()
|
||||
public async Task NoVVReadOnlyTest()
|
||||
{
|
||||
const string code = """
|
||||
using System;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Robust.Shared.ViewVariables
|
||||
{
|
||||
public sealed class ViewVariablesAttribute : Attribute
|
||||
{
|
||||
public readonly VVAccess Access = VVAccess.ReadOnly;
|
||||
|
||||
public ViewVariablesAttribute() { }
|
||||
|
||||
public ViewVariablesAttribute(VVAccess access)
|
||||
{
|
||||
Access = access;
|
||||
}
|
||||
}
|
||||
public enum VVAccess : byte
|
||||
{
|
||||
ReadOnly = 0,
|
||||
ReadWrite = 1,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
@@ -83,8 +89,86 @@ public sealed class DataDefinitionAnalyzerTest
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(35,17): info RA0028: Data field Bad in data definition Foo has ViewVariables attribute with ReadWrite access, which is redundant
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldNoVVReadWriteRule).WithSpan(35, 17, 35, 50).WithArguments("Bad", "Foo")
|
||||
// /0/Test0.cs(7,17): info RA0028: Data field Bad in data definition Foo has ViewVariables attribute with ReadWrite access, which is redundant
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldNoVVReadWriteRule).WithSpan(7, 17, 7, 50).WithArguments("Bad", "Foo")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ReadOnlyFieldTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
[DataField]
|
||||
public readonly int Bad;
|
||||
|
||||
[DataField]
|
||||
public int Good;
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(7,12): error RA0019: Data field Bad in data definition Foo is readonly
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldWritableRule).WithSpan(7, 12, 7, 20).WithArguments("Bad", "Foo")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ReadOnlyPropertyTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
[DataField]
|
||||
public int Bad { get; }
|
||||
|
||||
[DataField]
|
||||
public int Good { get; private set; }
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(7,20): error RA0020: Data field property Bad in data definition Foo does not have a setter
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldPropertyWritableRule).WithSpan(7, 20, 7, 28).WithArguments("Bad", "Foo")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NotYamlSerializableTest()
|
||||
{
|
||||
const string code = """
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
[NotYamlSerializable]
|
||||
public sealed class NotSerializableClass { }
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class Foo
|
||||
{
|
||||
[DataField]
|
||||
public NotSerializableClass BadField;
|
||||
|
||||
[DataField]
|
||||
public NotSerializableClass BadProperty { get; set; }
|
||||
|
||||
public NotSerializableClass GoodField; // Not a DataField, not a problem
|
||||
|
||||
public NotSerializableClass GoodProperty { get; set; } // Not a DataField, not a problem
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(10,12): error RA0033: Data field BadField in data definition Foo is type NotSerializableClass, which is not YAML serializable
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(10, 12, 10, 32).WithArguments("BadField", "Foo", "NotSerializableClass"),
|
||||
// /0/Test0.cs(13,12): error RA0033: Data field BadProperty in data definition Foo is type NotSerializableClass, which is not YAML serializable
|
||||
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldYamlSerializableRule).WithSpan(13, 12, 13, 32).WithArguments("BadProperty", "Foo", "NotSerializableClass")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
189
Robust.Analyzers.Tests/ForbidLiteralAnalyzerTest.cs
Normal file
189
Robust.Analyzers.Tests/ForbidLiteralAnalyzerTest.cs
Normal 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")
|
||||
);
|
||||
}
|
||||
}
|
||||
86
Robust.Analyzers.Tests/ObsoleteInheritanceAnalyzerTest.cs
Normal file
86
Robust.Analyzers.Tests/ObsoleteInheritanceAnalyzerTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -18,6 +18,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
private const string ImplicitDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataDefinitionForInheritorsAttribute";
|
||||
private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute";
|
||||
private const string ViewVariablesNamespace = "Robust.Shared.ViewVariables.ViewVariablesAttribute";
|
||||
private const string NotYamlSerializableName = "Robust.Shared.Serialization.Manager.Attributes.NotYamlSerializableAttribute";
|
||||
private const string DataFieldAttributeName = "DataField";
|
||||
private const string ViewVariablesAttributeName = "ViewVariables";
|
||||
|
||||
@@ -41,7 +42,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 +52,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",
|
||||
@@ -81,9 +82,19 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
"Make sure to remove the ViewVariables attribute."
|
||||
);
|
||||
|
||||
public static readonly DiagnosticDescriptor DataFieldYamlSerializableRule = new(
|
||||
Diagnostics.IdDataFieldYamlSerializable,
|
||||
"Data field type is not YAML serializable",
|
||||
"Data field {0} in data definition {1} is type {2}, which is not YAML serializable",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Error,
|
||||
true,
|
||||
"Make sure to use a type that is YAML serializable."
|
||||
);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
||||
DataDefinitionPartialRule, NestedDataDefinitionPartialRule, DataFieldWritableRule, DataFieldPropertyWritableRule,
|
||||
DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule
|
||||
DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule, DataFieldYamlSerializableRule
|
||||
);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
@@ -149,7 +160,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))
|
||||
@@ -163,6 +175,19 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
TryGetAttributeLocation(field, ViewVariablesAttributeName, out var location);
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, fieldSymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (context.SemanticModel.GetSymbolInfo(field.Declaration.Type).Symbol is not ITypeSymbol fieldTypeSymbol)
|
||||
continue;
|
||||
|
||||
if (IsNotYamlSerializable(fieldSymbol, fieldTypeSymbol))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
|
||||
(context.Node as FieldDeclarationSyntax)?.Declaration.Type.GetLocation(),
|
||||
fieldSymbol.Name,
|
||||
type.Name,
|
||||
fieldTypeSymbol.MetadataName
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +210,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))
|
||||
@@ -199,6 +225,19 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
TryGetAttributeLocation(property, ViewVariablesAttributeName, out var location);
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldNoVVReadWriteRule, location, propertySymbol.Name, type.Name));
|
||||
}
|
||||
|
||||
if (context.SemanticModel.GetSymbolInfo(property.Type).Symbol is not ITypeSymbol propertyTypeSymbol)
|
||||
return;
|
||||
|
||||
if (IsNotYamlSerializable(propertySymbol, propertyTypeSymbol))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DataFieldYamlSerializableRule,
|
||||
(context.Node as PropertyDeclarationSyntax)?.Type.GetLocation(),
|
||||
propertySymbol.Name,
|
||||
type.Name,
|
||||
propertyTypeSymbol.Name
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field)
|
||||
@@ -285,6 +324,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)
|
||||
@@ -367,6 +420,14 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
|
||||
return (VVAccess)accessByte == VVAccess.ReadWrite;
|
||||
}
|
||||
|
||||
private static bool IsNotYamlSerializable(ISymbol field, ITypeSymbol type)
|
||||
{
|
||||
if (!IsDataField(field, out _, out _))
|
||||
return false;
|
||||
|
||||
return HasAttribute(type, NotYamlSerializableName);
|
||||
}
|
||||
|
||||
private static bool IsImplicitDataDefinition(ITypeSymbol type)
|
||||
{
|
||||
if (HasAttribute(type, ImplicitDataDefinitionNamespace))
|
||||
|
||||
101
Robust.Analyzers/ForbidLiteralAnalyzer.cs
Normal file
101
Robust.Analyzers/ForbidLiteralAnalyzer.cs
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
75
Robust.Analyzers/ObsoleteInheritanceAnalyzer.cs
Normal file
75
Robust.Analyzers/ObsoleteInheritanceAnalyzer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Robust.Client.Animations
|
||||
/// </summary>
|
||||
public abstract class AnimationTrackProperty : AnimationTrack
|
||||
{
|
||||
public List<KeyFrame> KeyFrames { get; protected set; } = new();
|
||||
public List<KeyFrame> KeyFrames { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// How to interpolate values when between two keyframes.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Clyde;
|
||||
using Robust.Client.HWId;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Localization;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.Player;
|
||||
@@ -36,6 +37,7 @@ using Robust.Shared.Console;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Physics;
|
||||
@@ -104,6 +106,8 @@ namespace Robust.Client
|
||||
deps.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
|
||||
deps.Register<NetworkResourceManager>();
|
||||
deps.Register<IReloadManager, ReloadManager>();
|
||||
deps.Register<ILocalizationManager, ClientLocalizationManager>();
|
||||
deps.Register<ILocalizationManagerInternal, ClientLocalizationManager>();
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Numerics;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.ComponentTrees;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
@@ -9,25 +10,7 @@ namespace Robust.Client.ComponentTrees;
|
||||
|
||||
public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent, SpriteComponent>
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SpriteComponent, QueueSpriteTreeUpdateEvent>(OnQueueUpdate);
|
||||
}
|
||||
|
||||
private void OnQueueUpdate(EntityUid uid, SpriteComponent component, ref QueueSpriteTreeUpdateEvent args)
|
||||
=> QueueTreeUpdate(uid, component, args.Xform);
|
||||
|
||||
// TODO remove this when finally ECSing sprite components
|
||||
[ByRefEvent]
|
||||
internal readonly struct QueueSpriteTreeUpdateEvent
|
||||
{
|
||||
public readonly TransformComponent Xform;
|
||||
public QueueSpriteTreeUpdateEvent(TransformComponent xform)
|
||||
{
|
||||
Xform = xform;
|
||||
}
|
||||
}
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
#region Component Tree Overrides
|
||||
protected override bool DoFrameUpdate => true;
|
||||
@@ -36,6 +19,11 @@ public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent,
|
||||
protected override int InitialCapacity => 1024;
|
||||
|
||||
protected override Box2 ExtractAabb(in ComponentTreeEntry<SpriteComponent> entry, Vector2 pos, Angle rot)
|
||||
=> entry.Component.CalculateRotatedBoundingBox(pos, rot, default).CalcBoundingBox();
|
||||
{
|
||||
// TODO SPRITE optimize this
|
||||
// Because the just take the BB of the rotated BB, I'mt pretty sure we do a lot of unnecessary maths.
|
||||
return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
67
Robust.Client/Console/Commands/LocalizationCommands.cs
Normal file
67
Robust.Client/Console/Commands/LocalizationCommands.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
18
Robust.Client/Console/Commands/ShowPlayerVelocityCommand.cs
Normal file
18
Robust.Client/Console/Commands/ShowPlayerVelocityCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -31,6 +31,7 @@ using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
@@ -94,6 +95,7 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IReplayRecordingManagerInternal _replayRecording = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
private IWebViewManagerHook? _webViewHook;
|
||||
|
||||
@@ -158,6 +160,7 @@ namespace Robust.Client
|
||||
}
|
||||
|
||||
_serializationManager.Initialize();
|
||||
_loc.Initialize();
|
||||
|
||||
// Call Init in game assemblies.
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
|
||||
|
||||
123
Robust.Client/GameObjects/ClientEntityManager.Spawn.cs
Normal file
123
Robust.Client/GameObjects/ClientEntityManager.Spawn.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
@@ -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.EntityLifeStage >= EntityLifeStage.Terminating
|
||||
|| !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.EntityLifeStage >= EntityLifeStage.Terminating
|
||||
|| !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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using System;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
[Obsolete]
|
||||
public partial interface IRenderableComponent : IComponent
|
||||
{
|
||||
int DrawDepth { get; set; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Graphics.RSI;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
|
||||
@@ -10,21 +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 IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly SpriteTreeSystem _spriteTree = default!;
|
||||
|
||||
private SpriteBoundsOverlay? _overlay;
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Robust.Client.GameObjects
|
||||
if (_enabled)
|
||||
{
|
||||
DebugTools.AssertNull(_overlay);
|
||||
_overlay = new SpriteBoundsOverlay(_spriteTree, _entityManager);
|
||||
_overlay = new SpriteBoundsOverlay(EntityManager);
|
||||
_overlayManager.AddOverlay(_overlay);
|
||||
}
|
||||
else
|
||||
@@ -55,18 +55,13 @@ namespace Robust.Client.GameObjects
|
||||
private bool _enabled;
|
||||
}
|
||||
|
||||
public sealed class SpriteBoundsOverlay : Overlay
|
||||
public sealed class SpriteBoundsOverlay(IEntityManager entMan) : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
private readonly IEntityManager _entityManager;
|
||||
private SpriteTreeSystem _renderTree;
|
||||
|
||||
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, IEntityManager entityManager)
|
||||
{
|
||||
_renderTree = renderTree;
|
||||
_entityManager = entityManager;
|
||||
}
|
||||
private readonly SharedTransformSystem _xformSystem = entMan.System<SharedTransformSystem>();
|
||||
private readonly SpriteSystem _spriteSystem = entMan.System<SpriteSystem>();
|
||||
private readonly SpriteTreeSystem _renderTree = entMan.System<SpriteTreeSystem>();
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
@@ -74,10 +69,11 @@ namespace Robust.Client.GameObjects
|
||||
var currentMap = args.MapId;
|
||||
var viewport = args.WorldBounds;
|
||||
|
||||
foreach (var (sprite, xform) in _renderTree.QueryAabb(currentMap, viewport))
|
||||
foreach (var entry in _renderTree.QueryAabb(currentMap, viewport))
|
||||
{
|
||||
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
|
||||
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
|
||||
var (sprite, xform) = entry;
|
||||
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
|
||||
var bounds = _spriteSystem.CalculateBounds((entry.Uid, sprite), worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
|
||||
|
||||
// Get scaled down bounds used to indicate the "south" of a sprite.
|
||||
var localBound = bounds.Box;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,9 @@ namespace Robust.Client.GameObjects
|
||||
private EntityQuery<AnimationPlayerComponent> _playerQuery;
|
||||
private EntityQuery<MetaDataComponent> _metaQuery;
|
||||
|
||||
#pragma warning disable CS0414
|
||||
[Dependency] private readonly IComponentFactory _compFact = default!;
|
||||
#pragma warning restore CS0414
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -76,7 +78,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 +189,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 +204,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Shared.Containers.ContainerManagerComponent;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
@@ -58,7 +57,7 @@ namespace Robust.Client.GameObjects
|
||||
if (!RemoveExpectedEntity(meta.NetEntity, out var container))
|
||||
return;
|
||||
|
||||
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container);
|
||||
Insert((uid, TransformQuery.GetComponent(uid), MetaQuery.GetComponent(uid), null), container, force: true);
|
||||
}
|
||||
|
||||
public override void ShutdownContainer(BaseContainer container)
|
||||
@@ -232,7 +231,7 @@ namespace Robust.Client.GameObjects
|
||||
return;
|
||||
}
|
||||
|
||||
Insert(message.Entity, container);
|
||||
Insert(message.Entity, container, force: true);
|
||||
}
|
||||
|
||||
public void AddExpectedEntity(NetEntity netEntity, BaseContainer container)
|
||||
@@ -303,7 +302,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 +343,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public sealed class EyeSystem : SharedEyeSystem
|
||||
eyeComponent.Target = null;
|
||||
}
|
||||
|
||||
eyeComponent.Eye.Position = xform.MapPosition;
|
||||
eyeComponent.Eye.Position = TransformSystem.GetMapCoordinates(xform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Robust.Client.GameObjects
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentGetState>(OnLightGetState);
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentInit>(HandleInit);
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentHandleState>(OnLightHandleState);
|
||||
}
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
149
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Bounds.cs
Normal file
149
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Bounds.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Graphics.RSI;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.GameObjects.SpriteComponent;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
// This partial class contains code related to updating a sprites bounding boxes and its position in the sprite tree.
|
||||
public sealed partial class SpriteSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a sprite's local bounding box. The returned bounds do factor in the sprite's scale but not the rotation or
|
||||
/// offset.
|
||||
/// </summary>
|
||||
public Box2 GetLocalBounds(Entity<SpriteComponent> sprite)
|
||||
{
|
||||
if (!sprite.Comp.BoundsDirty)
|
||||
{
|
||||
DebugTools.Assert(sprite.Comp.Layers.All(x => !x.BoundsDirty || !x.Drawn));
|
||||
return sprite.Comp._bounds;
|
||||
}
|
||||
|
||||
var bounds = new Box2();
|
||||
foreach (var layer in sprite.Comp.Layers)
|
||||
{
|
||||
if (layer.Drawn)
|
||||
bounds = bounds.Union(GetLocalBounds(layer));
|
||||
}
|
||||
|
||||
sprite.Comp._bounds = bounds;
|
||||
sprite.Comp.BoundsDirty = false;
|
||||
return sprite.Comp._bounds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a layer's local bounding box relative to its owning sprite. Unlike the sprite variant of this method, this
|
||||
/// does account for the layer's rotation and offset.
|
||||
/// </summary>
|
||||
public Box2 GetLocalBounds(Layer layer)
|
||||
{
|
||||
if (!layer.BoundsDirty)
|
||||
{
|
||||
DebugTools.Assert(layer.Bounds.EqualsApprox(CalculateLocalBounds(layer)));
|
||||
return layer.Bounds;
|
||||
}
|
||||
|
||||
layer.Bounds = CalculateLocalBounds(layer);
|
||||
layer.BoundsDirty = false;
|
||||
return layer.Bounds;
|
||||
}
|
||||
|
||||
internal Box2 CalculateLocalBounds(Layer layer)
|
||||
{
|
||||
var textureSize = (Vector2) layer.PixelSize / EyeManager.PixelsPerMeter;
|
||||
var longestSide = MathF.Max(textureSize.X, textureSize.Y);
|
||||
var longestRotatedSide = Math.Max(longestSide, (textureSize.X + textureSize.Y) / MathF.Sqrt(2));
|
||||
|
||||
Vector2 size;
|
||||
var sprite = layer.Owner.Comp;
|
||||
|
||||
// If this layer has any form of arbitrary rotation, return a bounding box big enough to cover
|
||||
// any possible rotation.
|
||||
if (layer._rotation != 0)
|
||||
{
|
||||
size = new Vector2(longestRotatedSide, longestRotatedSide);
|
||||
return Box2.CenteredAround(layer.Offset, size * layer._scale);
|
||||
}
|
||||
|
||||
var snapToCardinals = sprite.SnapCardinals;
|
||||
if (sprite.GranularLayersRendering && layer.RenderingStrategy != LayerRenderingStrategy.UseSpriteStrategy)
|
||||
{
|
||||
snapToCardinals = layer.RenderingStrategy == LayerRenderingStrategy.SnapToCardinals;
|
||||
}
|
||||
|
||||
if (snapToCardinals)
|
||||
{
|
||||
// Snapping to cardinals only makes sense for 1-directional layers/sprites
|
||||
DebugTools.Assert(layer._actualState == null || layer._actualState.RsiDirections == RsiDirectionType.Dir1);
|
||||
|
||||
// We won't know the actual direction it snaps to, so we ahve to assume the box is given by the longest side.
|
||||
size = new Vector2(longestSide, longestSide);
|
||||
return Box2.CenteredAround(layer.Offset, size * layer._scale);
|
||||
}
|
||||
|
||||
// Build the bounding box based on how many directions the sprite has
|
||||
size = (layer._actualState?.RsiDirections) switch
|
||||
{
|
||||
RsiDirectionType.Dir4 => new Vector2(longestSide, longestSide),
|
||||
RsiDirectionType.Dir8 => new Vector2(longestRotatedSide, longestRotatedSide),
|
||||
_ => textureSize
|
||||
};
|
||||
|
||||
return Box2.CenteredAround(layer.Offset, size * layer._scale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a sprite's bounding box in world coordinates.
|
||||
/// </summary>
|
||||
public Box2Rotated CalculateBounds(Entity<SpriteComponent> sprite, Vector2 worldPos, Angle worldRot, Angle eyeRot)
|
||||
{
|
||||
// fast check for invisible sprites
|
||||
if (!sprite.Comp.Visible || sprite.Comp.Layers.Count == 0)
|
||||
return new Box2Rotated(new Box2(worldPos, worldPos), Angle.Zero, worldPos);
|
||||
|
||||
// We need to modify world rotation so that it lies between 0 and 2pi.
|
||||
// This matters for 4 or 8 directional sprites deciding which quadrant (octant?) they lie in.
|
||||
// the 0->2pi convention is set by the sprite-rendering code that selects the layers.
|
||||
// See RenderInternal().
|
||||
|
||||
worldRot = worldRot.Reduced();
|
||||
if (worldRot.Theta < 0)
|
||||
worldRot = new Angle(worldRot.Theta + Math.Tau);
|
||||
|
||||
// Next, what we do is take the box2 and apply the sprite's transform, and then the entity's transform. We
|
||||
// could do this via Matrix3.TransformBox, but that only yields bounding boxes. So instead we manually
|
||||
// transform our box by the combination of these matrices:
|
||||
|
||||
var finalRotation = sprite.Comp.NoRotation
|
||||
? sprite.Comp.Rotation - eyeRot
|
||||
: sprite.Comp.Rotation + worldRot;
|
||||
|
||||
var bounds = GetLocalBounds(sprite);
|
||||
|
||||
// slightly faster path if offset == 0 (true for 99.9% of sprites)
|
||||
if (sprite.Comp.Offset == Vector2.Zero)
|
||||
return new Box2Rotated(bounds.Translated(worldPos), finalRotation, worldPos);
|
||||
|
||||
var adjustedOffset = sprite.Comp.NoRotation
|
||||
? (-eyeRot).RotateVec(sprite.Comp.Offset)
|
||||
: worldRot.RotateVec(sprite.Comp.Offset);
|
||||
|
||||
var position = adjustedOffset + worldPos;
|
||||
return new Box2Rotated(bounds.Translated(position), finalRotation, position);
|
||||
}
|
||||
|
||||
private void DirtyBounds(Entity<SpriteComponent> sprite)
|
||||
{
|
||||
sprite.Comp.BoundsDirty = true;
|
||||
foreach (var layer in sprite.Comp.Layers)
|
||||
{
|
||||
layer.BoundsDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
@@ -41,4 +45,66 @@ public sealed partial class SpriteSystem
|
||||
layer.AnimationTimeLeft = (float) -(time % state.TotalDelay);
|
||||
layer.AnimationFrame = 0;
|
||||
}
|
||||
|
||||
public void CopySprite(Entity<SpriteComponent?> source, Entity<SpriteComponent?> target)
|
||||
{
|
||||
if (!Resolve(source.Owner, ref source.Comp))
|
||||
return;
|
||||
|
||||
if (!Resolve(target.Owner, ref target.Comp))
|
||||
return;
|
||||
|
||||
target.Comp._baseRsi = source.Comp._baseRsi;
|
||||
target.Comp._bounds = source.Comp._bounds;
|
||||
target.Comp._visible = source.Comp._visible;
|
||||
target.Comp.color = source.Comp.color;
|
||||
target.Comp.offset = source.Comp.offset;
|
||||
target.Comp.rotation = source.Comp.rotation;
|
||||
target.Comp.scale = source.Comp.scale;
|
||||
target.Comp.LocalMatrix = Matrix3Helpers.CreateTransform(
|
||||
in target.Comp.offset,
|
||||
in target.Comp.rotation,
|
||||
in target
|
||||
.Comp.scale);
|
||||
|
||||
target.Comp.drawDepth = source.Comp.drawDepth;
|
||||
target.Comp.NoRotation = source.Comp.NoRotation;
|
||||
target.Comp.DirectionOverride = source.Comp.DirectionOverride;
|
||||
target.Comp.EnableDirectionOverride = source.Comp.EnableDirectionOverride;
|
||||
target.Comp.Layers = new List<SpriteComponent.Layer>(source.Comp.Layers.Count);
|
||||
foreach (var otherLayer in source.Comp.Layers)
|
||||
{
|
||||
var layer = new SpriteComponent.Layer(otherLayer, target.Comp);
|
||||
layer.Index = target.Comp.Layers.Count;
|
||||
layer.Owner = target!;
|
||||
target.Comp.Layers.Add(layer);
|
||||
}
|
||||
|
||||
target.Comp.IsInert = source.Comp.IsInert;
|
||||
target.Comp.LayerMap = source.Comp.LayerMap.ShallowClone();
|
||||
target.Comp.PostShader = source.Comp.PostShader is {Mutable: true}
|
||||
? source.Comp.PostShader.Duplicate()
|
||||
: source.Comp.PostShader;
|
||||
|
||||
target.Comp.RenderOrder = source.Comp.RenderOrder;
|
||||
target.Comp.GranularLayersRendering = source.Comp.GranularLayersRendering;
|
||||
|
||||
DirtyBounds(target!);
|
||||
_tree.QueueTreeUpdate(target!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a sprite to a queue that will update <see cref="SpriteComponent.IsInert"/> next frame.
|
||||
/// </summary>
|
||||
public void QueueUpdateIsInert(Entity<SpriteComponent> sprite)
|
||||
{
|
||||
if (sprite.Comp._inertUpdateQueued)
|
||||
return;
|
||||
|
||||
sprite.Comp._inertUpdateQueued = true;
|
||||
_inertUpdateQueue.Enqueue(sprite);
|
||||
}
|
||||
|
||||
[Obsolete("Use QueueUpdateIsInert")]
|
||||
public void QueueUpdateInert(EntityUid uid, SpriteComponent sprite) => QueueUpdateIsInert(new (uid, sprite));
|
||||
}
|
||||
|
||||
@@ -1,11 +1,153 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
// This partial class contains various public helper methods, including methods for extracting textures/icons from
|
||||
// sprite specifiers and entity prototypes.
|
||||
public sealed partial class SpriteSystem
|
||||
{
|
||||
private readonly Dictionary<string, IRsiStateLike> _cachedPrototypeIcons = new();
|
||||
|
||||
public Texture Frame0(EntityPrototype prototype)
|
||||
{
|
||||
return GetPrototypeIcon(prototype).Default;
|
||||
}
|
||||
|
||||
public Texture Frame0(SpriteSpecifier specifier)
|
||||
{
|
||||
return RsiStateLike(specifier).Default;
|
||||
}
|
||||
|
||||
public IRsiStateLike RsiStateLike(SpriteSpecifier specifier)
|
||||
{
|
||||
switch (specifier)
|
||||
{
|
||||
case SpriteSpecifier.Texture tex:
|
||||
return GetTexture(tex);
|
||||
|
||||
case SpriteSpecifier.Rsi rsi:
|
||||
return GetState(rsi);
|
||||
|
||||
case SpriteSpecifier.EntityPrototype prototypeIcon:
|
||||
return GetPrototypeIcon(prototypeIcon.EntityPrototypeId);
|
||||
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public Texture GetIcon(IconComponent icon)
|
||||
{
|
||||
return GetState(icon.Icon).Frame0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
|
||||
/// This method caches the result based on the prototype identifier.
|
||||
/// </summary>
|
||||
public IRsiStateLike GetPrototypeIcon(string prototype)
|
||||
{
|
||||
// Check if this prototype has been cached before, and if so return the result.
|
||||
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
|
||||
return cachedResult;
|
||||
|
||||
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
|
||||
{
|
||||
// The specified prototype doesn't exist, return the fallback "error" sprite.
|
||||
_sawmill.Error("Failed to load PrototypeIcon {0}", prototype);
|
||||
return GetFallbackState();
|
||||
}
|
||||
|
||||
// Generate the icon and cache it in case it's ever needed again.
|
||||
var result = GetPrototypeIcon(entityPrototype);
|
||||
_cachedPrototypeIcons[prototype] = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
|
||||
/// This method does NOT cache the result.
|
||||
/// </summary>
|
||||
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
|
||||
{
|
||||
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
|
||||
if (prototype.Components.TryGetValue("Icon", out var compData)
|
||||
&& compData.Component is IconComponent icon)
|
||||
{
|
||||
return GetIcon(icon);
|
||||
}
|
||||
|
||||
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
|
||||
if (!prototype.Components.ContainsKey("Sprite"))
|
||||
{
|
||||
return GetFallbackState();
|
||||
}
|
||||
|
||||
// Finally, we use spawn a dummy entity to get its icon.
|
||||
var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace);
|
||||
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
|
||||
var result = spriteComponent.Icon ?? GetFallbackState();
|
||||
Del(dummy);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public RSI.State GetFallbackState()
|
||||
{
|
||||
return _resourceCache.GetFallback<RSIResource>().RSI["error"];
|
||||
}
|
||||
|
||||
public Texture GetFallbackTexture()
|
||||
{
|
||||
return _resourceCache.GetFallback<TextureResource>().Texture;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier)
|
||||
{
|
||||
if (_resourceCache.TryGetResource<RSIResource>(
|
||||
TextureRoot / rsiSpecifier.RsiPath,
|
||||
out var theRsi) &&
|
||||
theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
_sawmill.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
|
||||
return GetFallbackState();
|
||||
}
|
||||
|
||||
public Texture GetTexture(SpriteSpecifier.Texture texSpecifier)
|
||||
{
|
||||
return _resourceCache
|
||||
.GetResource<TextureResource>(TextureRoot / texSpecifier.TexturePath)
|
||||
.Texture;
|
||||
}
|
||||
|
||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
|
||||
{
|
||||
if (!args.TryGetModified<EntityPrototype>(out var modified))
|
||||
return;
|
||||
|
||||
// Remove all changed prototypes from the cache, if they're there.
|
||||
foreach (var prototype in modified)
|
||||
{
|
||||
// Let's be lazy and not regenerate them until something needs them again.
|
||||
_cachedPrototypeIcons.Remove(prototype);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an entity's sprite position in world terms.
|
||||
/// </summary>
|
||||
|
||||
257
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Layer.cs
Normal file
257
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Layer.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.GameObjects.SpriteComponent;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
// This partial class contains various public methods for managing a sprite's layers.
|
||||
// This setter methods for modifying a layer's properties are in a separate file.
|
||||
public sealed partial class SpriteSystem
|
||||
{
|
||||
public bool LayerExists(Entity<SpriteComponent?> sprite, int index)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return false;
|
||||
|
||||
return index > 0 && index < sprite.Comp.Layers.Count;
|
||||
}
|
||||
|
||||
public bool TryGetLayer(
|
||||
Entity<SpriteComponent?> sprite,
|
||||
int index,
|
||||
[NotNullWhen(true)] out Layer? layer,
|
||||
bool logMissing)
|
||||
{
|
||||
layer = null;
|
||||
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
|
||||
return false;
|
||||
|
||||
if (index >= 0 && index < sprite.Comp.Layers.Count)
|
||||
{
|
||||
layer = sprite.Comp.Layers[index];
|
||||
DebugTools.AssertEqual(layer.Owner, sprite!);
|
||||
DebugTools.AssertEqual(layer.Index, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (logMissing)
|
||||
Log.Error($"Layer index '{index}' on entity {ToPrettyString(sprite)} does not exist! Trace:\n{Environment.StackTrace}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool RemoveLayer(Entity<SpriteComponent?> sprite, int index, bool logMissing = true)
|
||||
{
|
||||
return RemoveLayer(sprite.Owner, index, out _, logMissing);
|
||||
}
|
||||
|
||||
public bool RemoveLayer(
|
||||
Entity<SpriteComponent?> sprite,
|
||||
int index,
|
||||
[NotNullWhen(true)] out Layer? layer,
|
||||
bool logMissing = true)
|
||||
{
|
||||
layer = null;
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
|
||||
return false;
|
||||
|
||||
if (!TryGetLayer(sprite, index, out layer, logMissing))
|
||||
return false;
|
||||
|
||||
sprite.Comp.Layers.RemoveAt(index);
|
||||
|
||||
foreach (var otherLayer in sprite.Comp.Layers[index..])
|
||||
{
|
||||
otherLayer.Index--;
|
||||
}
|
||||
|
||||
// TODO SPRITE track inverse-mapping?
|
||||
foreach (var (key, value) in sprite.Comp.LayerMap)
|
||||
{
|
||||
if (value == index)
|
||||
sprite.Comp.LayerMap.Remove(key);
|
||||
else if (value > index)
|
||||
{
|
||||
sprite.Comp.LayerMap[key]--;
|
||||
}
|
||||
}
|
||||
|
||||
layer.Owner = default;
|
||||
layer.Index = -1;
|
||||
|
||||
#if DEBUG
|
||||
foreach (var otherLayer in sprite.Comp.Layers)
|
||||
{
|
||||
DebugTools.AssertEqual(otherLayer, sprite.Comp.Layers[otherLayer.Index]);
|
||||
}
|
||||
#endif
|
||||
|
||||
sprite.Comp.BoundsDirty = true;
|
||||
_tree.QueueTreeUpdate(sprite!);
|
||||
QueueUpdateIsInert(sprite!);
|
||||
return true;
|
||||
}
|
||||
|
||||
#region AddLayer
|
||||
|
||||
/// <summary>
|
||||
/// Add the given sprite layer. If an index is specified, this will insert the layer with the given index, resulting
|
||||
/// in all other layers being reshuffled.
|
||||
/// </summary>
|
||||
public int AddLayer(Entity<SpriteComponent?> sprite, Layer layer, int? index = null)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
{
|
||||
layer.Index = -1;
|
||||
layer.Owner = default;
|
||||
return -1;
|
||||
}
|
||||
|
||||
layer.Owner = sprite!;
|
||||
|
||||
if (index is { } i && i != sprite.Comp.Layers.Count)
|
||||
{
|
||||
foreach (var otherLayer in sprite.Comp.Layers[i..])
|
||||
{
|
||||
otherLayer.Index++;
|
||||
}
|
||||
|
||||
// TODO SPRITE track inverse-mapping?
|
||||
sprite.Comp.Layers.Insert(i, layer);
|
||||
layer.Index = i;
|
||||
|
||||
foreach (var (key, value) in sprite.Comp.LayerMap)
|
||||
{
|
||||
if (value >= i)
|
||||
sprite.Comp.LayerMap[key]++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
layer.Index = sprite.Comp.Layers.Count;
|
||||
sprite.Comp.Layers.Add(layer);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
foreach (var otherLayer in sprite.Comp.Layers)
|
||||
{
|
||||
DebugTools.AssertEqual(otherLayer, sprite.Comp.Layers[otherLayer.Index]);
|
||||
}
|
||||
#endif
|
||||
|
||||
layer.BoundsDirty = true;
|
||||
if (!layer.Blank)
|
||||
{
|
||||
sprite.Comp.BoundsDirty = true;
|
||||
_tree.QueueTreeUpdate(sprite!);
|
||||
QueueUpdateIsInert(sprite!);
|
||||
}
|
||||
return layer.Index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a layer corresponding to the given RSI state.
|
||||
/// </summary>
|
||||
/// <param name="sprite">The sprite</param>
|
||||
/// <param name="stateId">The RSI state</param>
|
||||
/// <param name="rsi">The RSI to use. If not specified, it will default to using <see cref="SpriteComponent.BaseRSI"/></param>
|
||||
/// <param name="index">The layer index to use for the new sprite.</param>
|
||||
/// <returns></returns>
|
||||
public int AddRsiLayer(Entity<SpriteComponent?> sprite, RSI.StateId stateId, RSI? rsi = null, int? index = null)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return -1;
|
||||
|
||||
var layer = AddBlankLayer(sprite!, index);
|
||||
|
||||
if (rsi != null)
|
||||
LayerSetRsi(layer, rsi, stateId);
|
||||
else
|
||||
LayerSetRsiState(layer, stateId);
|
||||
|
||||
return layer.Index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a layer corresponding to the given RSI state.
|
||||
/// </summary>
|
||||
/// <param name="sprite">The sprite</param>
|
||||
/// <param name="state">The RSI state</param>
|
||||
/// <param name="path">The path to the RSI.</param>
|
||||
/// <param name="index">The layer index to use for the new sprite.</param>
|
||||
/// <returns></returns>
|
||||
public int AddRsiLayer(Entity<SpriteComponent?> sprite, RSI.StateId state, ResPath path, int? index = null)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return -1;
|
||||
|
||||
if (!_resourceCache.TryGetResource<RSIResource>(TextureRoot / path, out var res))
|
||||
Log.Error($"Unable to load RSI '{path}'. Trace:\n{Environment.StackTrace}");
|
||||
|
||||
if (path.Extension != "rsi")
|
||||
Log.Error($"Expected rsi path but got '{path}'?");
|
||||
|
||||
return AddRsiLayer(sprite, state, res?.RSI, index);
|
||||
}
|
||||
|
||||
public int AddTextureLayer(Entity<SpriteComponent?> sprite, ResPath path, int? index = null)
|
||||
{
|
||||
if (_resourceCache.TryGetResource<TextureResource>(TextureRoot / path, out var texture))
|
||||
return AddTextureLayer(sprite, texture?.Texture, index);
|
||||
|
||||
if (path.Extension == "rsi")
|
||||
Log.Error($"Expected texture but got rsi '{path}', did you mean 'sprite:' instead of 'texture:'?");
|
||||
|
||||
Log.Error($"Unable to load texture '{path}'. Trace:\n{Environment.StackTrace}");
|
||||
return AddTextureLayer(sprite, texture?.Texture, index);
|
||||
}
|
||||
|
||||
public int AddTextureLayer(Entity<SpriteComponent?> sprite, Texture? texture, int? index = null)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return -1;
|
||||
|
||||
var layer = new Layer {Texture = texture};
|
||||
return AddLayer(sprite, layer, index);
|
||||
}
|
||||
|
||||
public int AddLayer(Entity<SpriteComponent?> sprite, SpriteSpecifier specifier, int? newIndex = null)
|
||||
{
|
||||
return specifier switch
|
||||
{
|
||||
SpriteSpecifier.Texture tex => AddTextureLayer(sprite, tex.TexturePath, newIndex),
|
||||
SpriteSpecifier.Rsi rsi => AddRsiLayer(sprite, rsi.RsiState, rsi.RsiPath, newIndex),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new sprite layer and populate it using the provided layer data.
|
||||
/// </summary>
|
||||
public int AddLayer(Entity<SpriteComponent?> sprite, PrototypeLayerData layerDatum, int? index)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return -1;
|
||||
|
||||
var layer = AddBlankLayer(sprite!, index);
|
||||
LayerSetData(layer, layerDatum);
|
||||
return layer.Index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a blank sprite layer.
|
||||
/// </summary>
|
||||
public Layer AddBlankLayer(Entity<SpriteComponent> sprite, int? index = null)
|
||||
{
|
||||
var layer = new Layer();
|
||||
AddLayer(sprite!, layer, index);
|
||||
return layer;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Graphics.RSI;
|
||||
using static Robust.Client.GameObjects.SpriteComponent;
|
||||
using static Robust.Client.Graphics.RSI;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
// This partial class contains various public methods for reading a layer's properties
|
||||
public sealed partial class SpriteSystem
|
||||
{
|
||||
#region RsiState
|
||||
|
||||
/// <summary>
|
||||
/// Get the RSI state being used by the current layer. Note that the return value may be an invalid state. E.g.,
|
||||
/// this might be a texture layer that does not use RSIs.
|
||||
/// </summary>
|
||||
public StateId LayerGetRsiState(Entity<SpriteComponent?> sprite, int index)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
return layer.StateId;
|
||||
|
||||
return StateId.Invalid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the RSI state being used by the current layer. Note that the return value may be an invalid state. E.g.,
|
||||
/// this might be a texture layer that does not use RSIs.
|
||||
/// </summary>
|
||||
public StateId LayerGetRsiState(Entity<SpriteComponent?> sprite, string key, StateId state)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
return layer.StateId;
|
||||
|
||||
return StateId.Invalid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the RSI state being used by the current layer. Note that the return value may be an invalid state. E.g.,
|
||||
/// this might be a texture layer that does not use RSIs.
|
||||
/// </summary>
|
||||
public StateId LayerGetRsiState(Entity<SpriteComponent?> sprite, Enum key, StateId state)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
return layer.StateId;
|
||||
|
||||
return StateId.Invalid;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RsiState
|
||||
|
||||
/// <summary>
|
||||
/// Returns the RSI being used by the layer to resolve it's RSI state. If the layer does not specify an RSI, this
|
||||
/// will just be the base RSI of the owning sprite (<see cref="SpriteComponent.BaseRSI"/>).
|
||||
/// </summary>
|
||||
public RSI? LayerGetEffectiveRsi(Entity<SpriteComponent?> sprite, int index)
|
||||
{
|
||||
TryGetLayer(sprite, index, out var layer, true);
|
||||
return layer?.ActualRsi;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the RSI being used by the layer to resolve it's RSI state. If the layer does not specify an RSI, this
|
||||
/// will just be the base RSI of the owning sprite (<see cref="SpriteComponent.BaseRSI"/>).
|
||||
/// </summary>
|
||||
public RSI? LayerGetEffectiveRsi(Entity<SpriteComponent?> sprite, string key, StateId state)
|
||||
{
|
||||
TryGetLayer(sprite, key, out var layer, true);
|
||||
return layer?.ActualRsi;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the RSI being used by the layer to resolve it's RSI state. If the layer does not specify an RSI, this
|
||||
/// will just be the base RSI of the owning sprite (<see cref="SpriteComponent.BaseRSI"/>).
|
||||
/// </summary>
|
||||
public RSI? LayerGetEffectiveRsi(Entity<SpriteComponent?> sprite, Enum key, StateId state)
|
||||
{
|
||||
TryGetLayer(sprite, key, out var layer, true);
|
||||
return layer?.ActualRsi;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directions
|
||||
|
||||
public RsiDirectionType LayerGetDirections(Entity<SpriteComponent?> sprite, int index)
|
||||
{
|
||||
return TryGetLayer(sprite, index, out var layer, true)
|
||||
? LayerGetDirections(layer)
|
||||
: RsiDirectionType.Dir1;
|
||||
}
|
||||
|
||||
|
||||
public RsiDirectionType LayerGetDirections(Entity<SpriteComponent?> sprite, Enum key)
|
||||
{
|
||||
return TryGetLayer(sprite, key, out var layer, true)
|
||||
? LayerGetDirections(layer)
|
||||
: RsiDirectionType.Dir1;
|
||||
}
|
||||
|
||||
public RsiDirectionType LayerGetDirections(Entity<SpriteComponent?> sprite, string key)
|
||||
{
|
||||
return TryGetLayer(sprite, key, out var layer, true)
|
||||
? LayerGetDirections(layer)
|
||||
: RsiDirectionType.Dir1;
|
||||
}
|
||||
|
||||
public RsiDirectionType LayerGetDirections(Layer layer)
|
||||
{
|
||||
if (!layer.StateId.IsValid)
|
||||
return RsiDirectionType.Dir1;
|
||||
|
||||
// Pull texture from RSI state instead.
|
||||
if (layer.ActualRsi is not {} rsi || !rsi.TryGetState(layer.StateId, out var state))
|
||||
return RsiDirectionType.Dir1;
|
||||
|
||||
return state.RsiDirections;
|
||||
}
|
||||
|
||||
public int LayerGetDirectionCount(Entity<SpriteComponent?> sprite, int index)
|
||||
{
|
||||
return TryGetLayer(sprite, index, out var layer, true) ? LayerGetDirectionCount(layer) : 1;
|
||||
}
|
||||
|
||||
public int LayerGetDirectionCount(Entity<SpriteComponent?> sprite, Enum key)
|
||||
{
|
||||
return TryGetLayer(sprite, key, out var layer, true) ? LayerGetDirectionCount(layer) : 1;
|
||||
}
|
||||
|
||||
public int LayerGetDirectionCount(Entity<SpriteComponent?> sprite, string key)
|
||||
{
|
||||
return TryGetLayer(sprite, key, out var layer, true) ? LayerGetDirectionCount(layer) : 1;
|
||||
}
|
||||
|
||||
public int LayerGetDirectionCount(Layer layer)
|
||||
{
|
||||
return LayerGetDirections(layer) switch
|
||||
{
|
||||
RsiDirectionType.Dir1 => 1,
|
||||
RsiDirectionType.Dir4 => 4,
|
||||
RsiDirectionType.Dir8 => 8,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
299
Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerMap.cs
Normal file
299
Robust.Client/GameObjects/EntitySystems/SpriteSystem.LayerMap.cs
Normal file
@@ -0,0 +1,299 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.GameObjects;
|
||||
using static Robust.Client.GameObjects.SpriteComponent;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
// This partial class contains various public methods for manipulating layer mappings.
|
||||
public sealed partial class SpriteSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Map an enum to a layer index.
|
||||
/// </summary>
|
||||
public void LayerMapSet(Entity<SpriteComponent?> sprite, Enum key, int index)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
if (index < 0 || index >= sprite.Comp.Layers.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
sprite.Comp.LayerMap[key] = index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map string to a layer index. If possible, it is preferred to use an enum key.
|
||||
/// string keys mainly exist to make it easier to define custom layer keys in yaml.
|
||||
/// </summary>
|
||||
public void LayerMapSet(Entity<SpriteComponent?> sprite, string key, int index)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
if (index < 0 || index >= sprite.Comp.Layers.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
sprite.Comp.LayerMap[key] = index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map an enum to a layer index.
|
||||
/// </summary>
|
||||
public void LayerMapAdd(Entity<SpriteComponent?> sprite, Enum key, int index)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
if (index < 0 || index >= sprite.Comp.Layers.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
sprite.Comp.LayerMap.Add(key, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map a string to a layer index. If possible, it is preferred to use an enum key.
|
||||
/// string keys mainly exist to make it easier to define custom layer keys in yaml.
|
||||
/// </summary>
|
||||
public void LayerMapAdd(Entity<SpriteComponent?> sprite, string key, int index)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
if (index < 0 || index >= sprite.Comp.Layers.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
sprite.Comp.LayerMap.Add(key, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an enum mapping.
|
||||
/// </summary>
|
||||
public bool LayerMapRemove(Entity<SpriteComponent?> sprite, Enum key)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return false;
|
||||
|
||||
return sprite.Comp.LayerMap.Remove(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a string mapping.
|
||||
/// </summary>
|
||||
public bool LayerMapRemove(Entity<SpriteComponent?> sprite, string key)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return false;
|
||||
|
||||
return sprite.Comp.LayerMap.Remove(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an enum mapping.
|
||||
/// </summary>
|
||||
public bool LayerMapRemove(Entity<SpriteComponent?> sprite, Enum key, out int index)
|
||||
{
|
||||
if (_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return sprite.Comp.LayerMap.Remove(key, out index);
|
||||
|
||||
index = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a string mapping.
|
||||
/// </summary>
|
||||
public bool LayerMapRemove(Entity<SpriteComponent?> sprite, string key, out int index)
|
||||
{
|
||||
if (_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return sprite.Comp.LayerMap.Remove(key, out index);
|
||||
|
||||
index = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to resolve an enum mapping.
|
||||
/// </summary>
|
||||
public bool LayerMapTryGet(Entity<SpriteComponent?> sprite, Enum key, out int index, bool logMissing)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
|
||||
{
|
||||
index = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sprite.Comp.LayerMap.TryGetValue(key, out index))
|
||||
return true;
|
||||
|
||||
if (logMissing)
|
||||
Log.Error($"Layer with key '{key}' does not exist on entity {ToPrettyString(sprite)}! Trace:\n{Environment.StackTrace}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to resolve a string mapping.
|
||||
/// </summary>
|
||||
public bool LayerMapTryGet(Entity<SpriteComponent?> sprite, string key, out int index, bool logMissing)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
|
||||
{
|
||||
index = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sprite.Comp.LayerMap.TryGetValue(key, out index))
|
||||
return true;
|
||||
|
||||
if (logMissing)
|
||||
Log.Error($"Layer with key '{key}' does not exist on entity {ToPrettyString(sprite)}! Trace:\n{Environment.StackTrace}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to resolve an enum mapping.
|
||||
/// </summary>
|
||||
public bool TryGetLayer(Entity<SpriteComponent?> sprite, Enum key, [NotNullWhen(true)] out Layer? layer, bool logMissing)
|
||||
{
|
||||
layer = null;
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
|
||||
return false;
|
||||
|
||||
return LayerMapTryGet(sprite, key, out var index, logMissing)
|
||||
&& TryGetLayer(sprite, index, out layer, logMissing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to resolve a string mapping.
|
||||
/// </summary>
|
||||
public bool TryGetLayer(Entity<SpriteComponent?> sprite, string key, [NotNullWhen(true)] out Layer? layer, bool logMissing)
|
||||
{
|
||||
layer = null;
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
|
||||
return false;
|
||||
|
||||
return LayerMapTryGet(sprite, key, out var index, logMissing)
|
||||
&& TryGetLayer(sprite, index, out layer, logMissing);
|
||||
}
|
||||
|
||||
public int LayerMapGet(Entity<SpriteComponent?> sprite, Enum key)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return -1;
|
||||
|
||||
return sprite.Comp.LayerMap[key];
|
||||
}
|
||||
|
||||
public int LayerMapGet(Entity<SpriteComponent?> sprite, string key)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return -1;
|
||||
|
||||
return sprite.Comp.LayerMap[key];
|
||||
}
|
||||
|
||||
public bool LayerExists(Entity<SpriteComponent?> sprite, string key)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return false;
|
||||
|
||||
return sprite.Comp.LayerMap.TryGetValue(key, out var index)
|
||||
&& LayerExists(sprite, index);
|
||||
}
|
||||
|
||||
public bool LayerExists(Entity<SpriteComponent?> sprite, Enum key)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return false;
|
||||
|
||||
return sprite.Comp.LayerMap.TryGetValue(key, out var index)
|
||||
&& LayerExists(sprite, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that a layer with the given key exists and return the layer's index.
|
||||
/// If the layer does not yet exist, this will create and add a blank layer.
|
||||
/// </summary>
|
||||
public int LayerMapReserve(Entity<SpriteComponent?> sprite, Enum key)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return -1;
|
||||
|
||||
if (LayerMapTryGet(sprite, key, out var layerIndex, false))
|
||||
return layerIndex;
|
||||
|
||||
var layer = AddBlankLayer(sprite!);
|
||||
LayerMapSet(sprite, key, layer.Index);
|
||||
return layer.Index;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="LayerMapReserve(Entity{SpriteComponent?},System.Enum)"/>
|
||||
public int LayerMapReserve(Entity<SpriteComponent?> sprite, string key)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return -1;
|
||||
|
||||
if (LayerMapTryGet(sprite, key, out var layerIndex, false))
|
||||
return layerIndex;
|
||||
|
||||
var layer = AddBlankLayer(sprite!);
|
||||
LayerMapSet(sprite, key, layer.Index);
|
||||
return layer.Index;
|
||||
}
|
||||
|
||||
public bool RemoveLayer(Entity<SpriteComponent?> sprite, string key, bool logMissing = true)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
|
||||
return false;
|
||||
|
||||
if (!LayerMapTryGet(sprite, key, out var index, logMissing))
|
||||
return false;
|
||||
|
||||
return RemoveLayer(sprite, index, logMissing);
|
||||
}
|
||||
|
||||
public bool RemoveLayer(Entity<SpriteComponent?> sprite, Enum key, bool logMissing = true)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
|
||||
return false;
|
||||
|
||||
if (!LayerMapTryGet(sprite, key, out var index, logMissing))
|
||||
return false;
|
||||
|
||||
return RemoveLayer(sprite, index, logMissing);
|
||||
}
|
||||
|
||||
public bool RemoveLayer(
|
||||
Entity<SpriteComponent?> sprite,
|
||||
string key,
|
||||
[NotNullWhen(true)] out Layer? layer,
|
||||
bool logMissing = true)
|
||||
{
|
||||
layer = null;
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
|
||||
return false;
|
||||
|
||||
if (!LayerMapTryGet(sprite, key, out var index, logMissing))
|
||||
return false;
|
||||
|
||||
return RemoveLayer(sprite, index, out layer, logMissing);
|
||||
}
|
||||
|
||||
public bool RemoveLayer(
|
||||
Entity<SpriteComponent?> sprite,
|
||||
Enum key,
|
||||
[NotNullWhen(true)] out Layer? layer,
|
||||
bool logMissing = true)
|
||||
{
|
||||
layer = null;
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp, logMissing))
|
||||
return false;
|
||||
|
||||
if (!LayerMapTryGet(sprite, key, out var index, logMissing))
|
||||
return false;
|
||||
|
||||
return RemoveLayer(sprite, index, out layer, logMissing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.GameObjects.SpriteComponent;
|
||||
using static Robust.Client.Graphics.RSI;
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
// This partial class contains various public methods for modifying a layer's properties.
|
||||
public sealed partial class SpriteSystem
|
||||
{
|
||||
#region SetData
|
||||
|
||||
public void LayerSetData(Entity<SpriteComponent?> sprite, int index, PrototypeLayerData data)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetData(layer, data);
|
||||
}
|
||||
|
||||
public void LayerSetData(Entity<SpriteComponent?> sprite, string key, PrototypeLayerData data)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetData(layer, data);
|
||||
}
|
||||
|
||||
public void LayerSetData(Entity<SpriteComponent?> sprite, Enum key, PrototypeLayerData data)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetData(layer, data);
|
||||
}
|
||||
|
||||
public void LayerSetData(Layer layer, PrototypeLayerData data)
|
||||
{
|
||||
DebugTools.Assert(layer.Owner != default);
|
||||
DebugTools.AssertNotNull(layer.Owner.Comp);
|
||||
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
|
||||
// TODO SPRITE ECS
|
||||
layer._parent.LayerSetData(layer, data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SpriteSpecifier
|
||||
|
||||
public void LayerSetSprite(Entity<SpriteComponent?> sprite, int index, SpriteSpecifier specifier)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetSprite(layer, specifier);
|
||||
}
|
||||
|
||||
public void LayerSetSprite(Entity<SpriteComponent?> sprite, string key, SpriteSpecifier specifier)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetSprite(layer, specifier);
|
||||
}
|
||||
|
||||
public void LayerSetSprite(Entity<SpriteComponent?> sprite, Enum key, SpriteSpecifier specifier)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetSprite(layer, specifier);
|
||||
}
|
||||
|
||||
public void LayerSetSprite(Layer layer, SpriteSpecifier specifier)
|
||||
{
|
||||
switch (specifier)
|
||||
{
|
||||
case SpriteSpecifier.Texture tex:
|
||||
LayerSetTexture(layer, tex.TexturePath);
|
||||
break;
|
||||
|
||||
case SpriteSpecifier.Rsi rsi:
|
||||
LayerSetRsi(layer, rsi.RsiPath, rsi.RsiState);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Texture
|
||||
|
||||
public void LayerSetTexture(Entity<SpriteComponent?> sprite, int index, Texture? texture)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetTexture(layer, texture);
|
||||
}
|
||||
|
||||
public void LayerSetTexture(Entity<SpriteComponent?> sprite, string key, Texture? texture)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetTexture(layer, texture);
|
||||
}
|
||||
|
||||
public void LayerSetTexture(Entity<SpriteComponent?> sprite, Enum key, Texture? texture)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetTexture(layer, texture);
|
||||
}
|
||||
|
||||
public void LayerSetTexture(Layer layer, Texture? texture)
|
||||
{
|
||||
LayerSetRsiState(layer, StateId.Invalid, refresh: true);
|
||||
layer.Texture = texture;
|
||||
}
|
||||
|
||||
public void LayerSetTexture(Entity<SpriteComponent?> sprite, int index, ResPath path)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetTexture(layer, path);
|
||||
}
|
||||
|
||||
public void LayerSetTexture(Entity<SpriteComponent?> sprite, string key, ResPath path)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetTexture(layer, path);
|
||||
}
|
||||
|
||||
public void LayerSetTexture(Entity<SpriteComponent?> sprite, Enum key, ResPath path)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetTexture(layer, path);
|
||||
}
|
||||
|
||||
private void LayerSetTexture(Layer layer, ResPath path)
|
||||
{
|
||||
if (!_resourceCache.TryGetResource<TextureResource>(TextureRoot / path, out var texture))
|
||||
{
|
||||
if (path.Extension == "rsi")
|
||||
Log.Error($"Expected texture but got rsi '{path}', did you mean 'sprite:' instead of 'texture:'?");
|
||||
Log.Error($"Unable to load texture '{path}'. Trace:\n{Environment.StackTrace}");
|
||||
}
|
||||
|
||||
LayerSetTexture(layer, texture?.Texture);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RsiState
|
||||
|
||||
public void LayerSetRsiState(Entity<SpriteComponent?> sprite, int index, StateId state)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetRsiState(layer, state);
|
||||
}
|
||||
|
||||
public void LayerSetRsiState(Entity<SpriteComponent?> sprite, string key, StateId state)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetRsiState(layer, state);
|
||||
}
|
||||
|
||||
public void LayerSetRsiState(Entity<SpriteComponent?> sprite, Enum key, StateId state)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetRsiState(layer, state);
|
||||
}
|
||||
|
||||
public void LayerSetRsiState(Layer layer, StateId state, bool refresh = false)
|
||||
{
|
||||
DebugTools.Assert(layer.Owner != default);
|
||||
DebugTools.AssertNotNull(layer.Owner.Comp);
|
||||
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
|
||||
|
||||
if (layer.StateId == state && !refresh)
|
||||
return;
|
||||
|
||||
layer.StateId = state;
|
||||
RefreshCachedState(layer, true, null);
|
||||
_tree.QueueTreeUpdate(layer.Owner);
|
||||
QueueUpdateIsInert(layer.Owner);
|
||||
layer.BoundsDirty = true;
|
||||
layer.Owner.Comp.BoundsDirty = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rsi
|
||||
|
||||
public void LayerSetRsi(Entity<SpriteComponent?> sprite, int index, RSI? rsi, StateId? state = null)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetRsi(layer, rsi, state);
|
||||
}
|
||||
|
||||
public void LayerSetRsi(Entity<SpriteComponent?> sprite, string key, RSI? rsi, StateId? state = null)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetRsi(layer, rsi, state);
|
||||
}
|
||||
|
||||
public void LayerSetRsi(Entity<SpriteComponent?> sprite, Enum key, RSI? rsi, StateId? state = null)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetRsi(layer, rsi, state);
|
||||
}
|
||||
|
||||
public void LayerSetRsi(Layer layer, RSI? rsi, StateId? state = null)
|
||||
{
|
||||
layer._rsi = rsi;
|
||||
LayerSetRsiState(layer, state ?? layer.StateId, refresh: true);
|
||||
}
|
||||
|
||||
public void LayerSetRsi(Entity<SpriteComponent?> sprite, int index, ResPath rsi, StateId? state = null)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetRsi(layer, rsi, state);
|
||||
}
|
||||
|
||||
public void LayerSetRsi(Entity<SpriteComponent?> sprite, string key, ResPath rsi, StateId? state = null)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetRsi(layer, rsi, state);
|
||||
}
|
||||
|
||||
public void LayerSetRsi(Entity<SpriteComponent?> sprite, Enum key, ResPath rsi, StateId? state = null)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetRsi(layer, rsi, state);
|
||||
}
|
||||
|
||||
public void LayerSetRsi(Layer layer, ResPath rsi, StateId? state = null)
|
||||
{
|
||||
if (!_resourceCache.TryGetResource<RSIResource>(TextureRoot / rsi, out var res))
|
||||
Log.Error($"Unable to load RSI '{rsi}' for entity {ToPrettyString(layer.Owner)}. Trace:\n{Environment.StackTrace}");
|
||||
|
||||
LayerSetRsi(layer, res?.RSI, state);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scale
|
||||
|
||||
public void LayerSetScale(Entity<SpriteComponent?> sprite, int index, Vector2 value)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetScale(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetScale(Entity<SpriteComponent?> sprite, string key, Vector2 value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetScale(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetScale(Entity<SpriteComponent?> sprite, Enum key, Vector2 value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetScale(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetScale(Layer layer, Vector2 value)
|
||||
{
|
||||
DebugTools.Assert(layer.Owner != default);
|
||||
DebugTools.AssertNotNull(layer.Owner.Comp);
|
||||
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
|
||||
|
||||
if (layer._scale.EqualsApprox(value))
|
||||
return;
|
||||
|
||||
if (!ValidateScale(layer.Owner, value))
|
||||
return;
|
||||
|
||||
layer._scale = value;
|
||||
layer.UpdateLocalMatrix();
|
||||
_tree.QueueTreeUpdate(layer.Owner);
|
||||
layer.BoundsDirty = true;
|
||||
layer.Owner.Comp.BoundsDirty = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rotation
|
||||
|
||||
public void LayerSetRotation(Entity<SpriteComponent?> sprite, int index, Angle value)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetRotation(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetRotation(Entity<SpriteComponent?> sprite, string key, Angle value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetRotation(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetRotation(Entity<SpriteComponent?> sprite, Enum key, Angle value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetRotation(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetRotation(Layer layer, Angle value)
|
||||
{
|
||||
DebugTools.Assert(layer.Owner != default);
|
||||
DebugTools.AssertNotNull(layer.Owner.Comp);
|
||||
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
|
||||
|
||||
if (layer._rotation.EqualsApprox(value))
|
||||
return;
|
||||
|
||||
layer._rotation = value;
|
||||
layer.UpdateLocalMatrix();
|
||||
_tree.QueueTreeUpdate(layer.Owner);
|
||||
layer.BoundsDirty = true;
|
||||
layer.Owner.Comp.BoundsDirty = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Offset
|
||||
|
||||
public void LayerSetOffset(Entity<SpriteComponent?> sprite, int index, Vector2 value)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetOffset(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetOffset(Entity<SpriteComponent?> sprite, string key, Vector2 value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetOffset(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetOffset(Entity<SpriteComponent?> sprite, Enum key, Vector2 value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetOffset(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetOffset(Layer layer, Vector2 value)
|
||||
{
|
||||
DebugTools.Assert(layer.Owner != default);
|
||||
DebugTools.AssertNotNull(layer.Owner.Comp);
|
||||
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
|
||||
|
||||
if (layer._offset.EqualsApprox(value))
|
||||
return;
|
||||
|
||||
layer._offset = value;
|
||||
layer.UpdateLocalMatrix();
|
||||
_tree.QueueTreeUpdate(layer.Owner);
|
||||
layer.BoundsDirty = true;
|
||||
layer.Owner.Comp.BoundsDirty = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Visible
|
||||
|
||||
public void LayerSetVisible(Entity<SpriteComponent?> sprite, int index, bool value)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetVisible(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetVisible(Entity<SpriteComponent?> sprite, string key, bool value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetVisible(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetVisible(Entity<SpriteComponent?> sprite, Enum key, bool value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetVisible(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetVisible(Layer layer, bool value)
|
||||
{
|
||||
DebugTools.Assert(layer.Owner != default);
|
||||
DebugTools.AssertNotNull(layer.Owner.Comp);
|
||||
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
|
||||
|
||||
if (layer._visible == value)
|
||||
return;
|
||||
|
||||
layer._visible = value;
|
||||
QueueUpdateIsInert(layer.Owner);
|
||||
_tree.QueueTreeUpdate(layer.Owner);
|
||||
layer.Owner.Comp.BoundsDirty = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Color
|
||||
|
||||
public void LayerSetColor(Entity<SpriteComponent?> sprite, int index, Color value)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetColor(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetColor(Entity<SpriteComponent?> sprite, string key, Color value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetColor(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetColor(Entity<SpriteComponent?> sprite, Enum key, Color value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetColor(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetColor(Layer layer, Color value)
|
||||
{
|
||||
DebugTools.Assert(layer.Owner != default);
|
||||
DebugTools.AssertNotNull(layer.Owner.Comp);
|
||||
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
|
||||
|
||||
layer.Color = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DirOffset
|
||||
|
||||
public void LayerSetDirOffset(Entity<SpriteComponent?> sprite, int index, DirectionOffset value)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetDirOffset(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetDirOffset(Entity<SpriteComponent?> sprite, string key, DirectionOffset value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetDirOffset(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetDirOffset(Entity<SpriteComponent?> sprite, Enum key, DirectionOffset value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetDirOffset(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetDirOffset(Layer layer, DirectionOffset value)
|
||||
{
|
||||
DebugTools.Assert(layer.Owner != default);
|
||||
DebugTools.AssertNotNull(layer.Owner.Comp);
|
||||
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
|
||||
|
||||
layer.DirOffset = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AnimationTime
|
||||
|
||||
public void LayerSetAnimationTime(Entity<SpriteComponent?> sprite, int index, float value)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetAnimationTime(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetAnimationTime(Entity<SpriteComponent?> sprite, string key, float value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetAnimationTime(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetAnimationTime(Entity<SpriteComponent?> sprite, Enum key, float value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetAnimationTime(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetAnimationTime(Layer layer, float value)
|
||||
{
|
||||
DebugTools.Assert(layer.Owner != default);
|
||||
DebugTools.AssertNotNull(layer.Owner.Comp);
|
||||
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
|
||||
|
||||
if (!layer.StateId.IsValid)
|
||||
return;
|
||||
|
||||
if (layer.ActualRsi is not { } rsi)
|
||||
return;
|
||||
|
||||
var state = rsi[layer.StateId];
|
||||
if (value > layer.AnimationTime)
|
||||
{
|
||||
// Handle advancing differently from going backwards.
|
||||
layer.AnimationTimeLeft -= (value - layer.AnimationTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Going backwards we re-calculate from zero.
|
||||
// Definitely possible to optimize this for going backwards but I'm too lazy to figure that out.
|
||||
layer.AnimationTimeLeft = -value + state.GetDelay(0);
|
||||
layer.AnimationFrame = 0;
|
||||
}
|
||||
|
||||
layer.AnimationTime = value;
|
||||
layer.AdvanceFrameAnimation(state);
|
||||
layer.SetAnimationTime(value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AutoAnimated
|
||||
|
||||
public void LayerSetAutoAnimated(Entity<SpriteComponent?> sprite, int index, bool value)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetAutoAnimated(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetAutoAnimated(Entity<SpriteComponent?> sprite, string key, bool value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetAutoAnimated(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetAutoAnimated(Entity<SpriteComponent?> sprite, Enum key, bool value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetAutoAnimated(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetAutoAnimated(Layer layer, bool value)
|
||||
{
|
||||
DebugTools.Assert(layer.Owner != default);
|
||||
DebugTools.AssertNotNull(layer.Owner.Comp);
|
||||
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
|
||||
|
||||
if (layer._autoAnimated == value)
|
||||
return;
|
||||
|
||||
layer._autoAnimated = value;
|
||||
QueueUpdateIsInert(layer.Owner);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region LayerSetRenderingStrategy
|
||||
|
||||
public void LayerSetRenderingStrategy(Entity<SpriteComponent?> sprite, int index, LayerRenderingStrategy value)
|
||||
{
|
||||
if (TryGetLayer(sprite, index, out var layer, true))
|
||||
LayerSetRenderingStrategy(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetRenderingStrategy(Entity<SpriteComponent?> sprite, string key, LayerRenderingStrategy value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetRenderingStrategy(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetRenderingStrategy(Entity<SpriteComponent?> sprite, Enum key, LayerRenderingStrategy value)
|
||||
{
|
||||
if (TryGetLayer(sprite, key, out var layer, true))
|
||||
LayerSetRenderingStrategy(layer, value);
|
||||
}
|
||||
|
||||
public void LayerSetRenderingStrategy(Layer layer, LayerRenderingStrategy value)
|
||||
{
|
||||
DebugTools.Assert(layer.Owner != default);
|
||||
DebugTools.AssertNotNull(layer.Owner.Comp);
|
||||
DebugTools.AssertEqual(layer.Owner.Comp.Layers[layer.Index], layer);
|
||||
|
||||
layer.RenderingStrategy = value;
|
||||
layer.BoundsDirty = true;
|
||||
layer.Owner.Comp.BoundsDirty = true;
|
||||
_tree.QueueTreeUpdate(layer.Owner);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes an RSI layer's cached RSI state.
|
||||
/// </summary>
|
||||
private void RefreshCachedState(Layer layer, bool logErrors, RSI.State? fallback)
|
||||
{
|
||||
if (!layer.StateId.IsValid)
|
||||
{
|
||||
layer._actualState = null;
|
||||
}
|
||||
else if (layer.ActualRsi is not { } rsi)
|
||||
{
|
||||
layer._actualState = fallback ?? GetFallbackState();
|
||||
if (logErrors)
|
||||
Log.Error(
|
||||
$"{ToPrettyString(layer.Owner)} has no RSI to pull new state from! Trace:\n{Environment.StackTrace}");
|
||||
}
|
||||
else if (!rsi.TryGetState(layer.StateId, out layer._actualState))
|
||||
{
|
||||
layer._actualState = fallback ?? GetFallbackState();
|
||||
if (logErrors)
|
||||
Log.Error(
|
||||
$"{ToPrettyString(layer.Owner)}'s state '{layer.StateId}' does not exist in RSI {rsi.Path}. Trace:\n{Environment.StackTrace}");
|
||||
}
|
||||
|
||||
layer.AnimationFrame = 0;
|
||||
layer.AnimationTime = 0;
|
||||
layer.AnimationTimeLeft = layer._actualState?.GetDelay(0) ?? 0f;
|
||||
}
|
||||
}
|
||||
196
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs
Normal file
196
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Clyde;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Graphics.RSI;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.GameObjects.SpriteComponent;
|
||||
using Vector4 = Robust.Shared.Maths.Vector4;
|
||||
using SysVec4 = System.Numerics.Vector4;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
// This partial class contains code related to actually rendering sprites.
|
||||
public sealed partial class SpriteSystem
|
||||
{
|
||||
public void RenderSprite(
|
||||
Entity<SpriteComponent> sprite,
|
||||
DrawingHandleWorld drawingHandle,
|
||||
Angle eyeRotation,
|
||||
Angle worldRotation,
|
||||
Vector2 worldPosition)
|
||||
{
|
||||
RenderSprite(sprite,
|
||||
drawingHandle,
|
||||
eyeRotation,
|
||||
worldRotation,
|
||||
worldPosition,
|
||||
sprite.Comp.EnableDirectionOverride ? sprite.Comp.DirectionOverride : null);
|
||||
}
|
||||
|
||||
public void RenderSprite(
|
||||
Entity<SpriteComponent> sprite,
|
||||
DrawingHandleWorld drawingHandle,
|
||||
Angle eyeRotation,
|
||||
Angle worldRotation,
|
||||
Vector2 worldPosition,
|
||||
Direction? overrideDirection)
|
||||
{
|
||||
// TODO SPRITE RENDERING
|
||||
// Add fast path for simple sprites.
|
||||
// I.e., when a sprite is modified, check if it is "simple". If it is. cache texture information in a struct
|
||||
// and use a fast path here.
|
||||
// E.g., simple 1-directional, 1-layer sprites can basically become a direct texture draw call. (most in game items).
|
||||
// Similarly, 1-directional multi-layer sprites can become a sequence of direct draw calls (most in game walls).
|
||||
|
||||
if (!sprite.Comp.IsInert)
|
||||
_queuedFrameUpdate.Add(sprite);
|
||||
|
||||
var angle = worldRotation + eyeRotation; // angle on-screen. Used to decide the direction of 4/8 directional RSIs
|
||||
angle = angle.Reduced().FlipPositive(); // Reduce the angles to fix math shenanigans
|
||||
|
||||
var cardinal = Angle.Zero;
|
||||
|
||||
// If we have a 1-directional sprite then snap it to try and always face it south if applicable.
|
||||
if (sprite.Comp is {NoRotation: false, SnapCardinals: true})
|
||||
cardinal = angle.RoundToCardinalAngle();
|
||||
|
||||
// worldRotation + eyeRotation should be the angle of the entity on-screen. If no-rot is enabled this is just set to zero.
|
||||
// However, at some point later the eye-matrix is applied separately, so we subtract -eye rotation for now:
|
||||
var entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, sprite.Comp.NoRotation ? -eyeRotation : worldRotation - cardinal);
|
||||
var spriteMatrix = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix);
|
||||
|
||||
// Fast path for when all sprites use the same transform matrix
|
||||
if (!sprite.Comp.GranularLayersRendering)
|
||||
{
|
||||
foreach (var layer in sprite.Comp.Layers)
|
||||
{
|
||||
RenderLayer(layer, drawingHandle, ref spriteMatrix, angle, overrideDirection);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//Default rendering (NoRotation = false)
|
||||
entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, worldRotation);
|
||||
var transformDefault = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix);
|
||||
|
||||
//Snap to cardinals
|
||||
entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, worldRotation - angle.RoundToCardinalAngle());
|
||||
var transformSnap = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix);
|
||||
|
||||
//No rotation
|
||||
entityMatrix = Matrix3Helpers.CreateTransform(worldPosition, -eyeRotation);
|
||||
var transformNoRot = Matrix3x2.Multiply(sprite.Comp.LocalMatrix, entityMatrix);
|
||||
|
||||
foreach (var layer in sprite.Comp.Layers)
|
||||
{
|
||||
switch (layer.RenderingStrategy)
|
||||
{
|
||||
case LayerRenderingStrategy.UseSpriteStrategy:
|
||||
RenderLayer(layer, drawingHandle, ref spriteMatrix, angle, overrideDirection);
|
||||
break;
|
||||
case LayerRenderingStrategy.Default:
|
||||
RenderLayer(layer, drawingHandle, ref transformDefault, angle, overrideDirection);
|
||||
break;
|
||||
case LayerRenderingStrategy.NoRotation:
|
||||
RenderLayer(layer, drawingHandle, ref transformNoRot, angle, overrideDirection);
|
||||
break;
|
||||
case LayerRenderingStrategy.SnapToCardinals:
|
||||
RenderLayer(layer, drawingHandle, ref transformSnap, angle, overrideDirection);
|
||||
break;
|
||||
default:
|
||||
Log.Error($"Tried to render a layer with unknown rendering stragegy: {layer.RenderingStrategy}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render a layer. This assumes that the input angle is between 0 and 2pi.
|
||||
/// </summary>
|
||||
private void RenderLayer(Layer layer, DrawingHandleWorld drawingHandle, ref Matrix3x2 spriteMatrix, Angle angle, Direction? overrideDirection)
|
||||
{
|
||||
if (!layer.Visible || layer.Blank)
|
||||
return;
|
||||
|
||||
var state = layer._actualState;
|
||||
var dir = state == null ? RsiDirection.South : Layer.GetDirection(state.RsiDirections, angle);
|
||||
|
||||
// Set the drawing transform for this layer
|
||||
layer.GetLayerDrawMatrix(dir, out var layerMatrix, layer.Owner.Comp.NoRotation);
|
||||
|
||||
// The direction used to draw the sprite can differ from the one that the angle would naively suggest,
|
||||
// due to direction overrides or offsets.
|
||||
if (overrideDirection != null && state != null)
|
||||
dir = overrideDirection.Value.Convert(state.RsiDirections);
|
||||
dir = dir.OffsetRsiDir(layer.DirOffset);
|
||||
|
||||
var texture = state?.GetFrame(dir, layer.AnimationFrame) ?? layer.Texture ?? GetFallbackTexture();
|
||||
|
||||
// TODO SPRITE
|
||||
// Refactor shader-param-layers to a separate layer type after layers are split into types & collections.
|
||||
// I.e., separate Layer -> RsiLayer, TextureLayer, LayerCollection, SpriteLayer, and ShaderLayer
|
||||
if (layer.CopyToShaderParameters != null)
|
||||
{
|
||||
HandleShaderLayer(layer, texture, layer.CopyToShaderParameters);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the drawing transform for this layer
|
||||
var transformMatrix = Matrix3x2.Multiply(layerMatrix, spriteMatrix);
|
||||
drawingHandle.SetTransform(in transformMatrix);
|
||||
|
||||
if (layer.Shader != null)
|
||||
drawingHandle.UseShader(layer.Shader);
|
||||
|
||||
var layerColor = layer.Owner.Comp.color * layer.Color;
|
||||
var textureSize = texture.Size / (float) EyeManager.PixelsPerMeter;
|
||||
var quad = Box2.FromDimensions(textureSize / -2, textureSize);
|
||||
|
||||
if (layer.UnShaded)
|
||||
{
|
||||
DebugTools.AssertNull(layer.Shader);
|
||||
DebugTools.Assert(layerColor is {R: >= 0, G: >= 0, B: >= 0, A: >= 0}, "Default shader should not be used with negative color modulation.");
|
||||
|
||||
// Negative color modulation values are by the default shader to disable light shading.
|
||||
// Specifically we set colour = - 1 - colour
|
||||
// This is good enough to ensure that non-negative values become negative & is trivially invertible.
|
||||
layerColor = new(new SysVec4(-1) - layerColor.RGBA);
|
||||
}
|
||||
|
||||
drawingHandle.DrawTextureRectRegion(texture, quad, layerColor);
|
||||
|
||||
if (layer.Shader != null)
|
||||
drawingHandle.UseShader(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a a "fake layer" that just exists to modify the parameters of a shader being used by some other
|
||||
/// layer.
|
||||
/// </summary>
|
||||
private void HandleShaderLayer(Layer layer, Texture texture, CopyToShaderParameters @params)
|
||||
{
|
||||
// Multiple atrocities to god being committed right here.
|
||||
var otherLayerIdx = layer._parent.LayerMap[@params.LayerKey!];
|
||||
var otherLayer = layer._parent.Layers[otherLayerIdx];
|
||||
if (otherLayer.Shader is not { } shader)
|
||||
return;
|
||||
|
||||
if (!shader.Mutable)
|
||||
otherLayer.Shader = shader = shader.Duplicate();
|
||||
|
||||
var clydeTexture = Clyde.RenderHandle.ExtractTexture(texture, null, out var csr);
|
||||
|
||||
if (@params.ParameterTexture is { } paramTexture)
|
||||
shader.SetParameter(paramTexture, clydeTexture);
|
||||
|
||||
if (@params.ParameterUV is not { } paramUV)
|
||||
return;
|
||||
|
||||
var sr = Clyde.RenderHandle.WorldTextureBoundsToUV(clydeTexture, csr);
|
||||
var uv = new Vector4(sr.Left, sr.Bottom, sr.Right, sr.Top);
|
||||
shader.SetParameter(paramUV, uv);
|
||||
}
|
||||
}
|
||||
166
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Setters.cs
Normal file
166
Robust.Client/GameObjects/EntitySystems/SpriteSystem.Setters.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
// This partial class contains various public methods for setting sprite component data.
|
||||
public sealed partial class SpriteSystem
|
||||
{
|
||||
private bool ValidateScale(Entity<SpriteComponent> sprite, Vector2 scale)
|
||||
{
|
||||
if (!(MathF.Abs(scale.X) < 0.005f) && !(MathF.Abs(scale.Y) < 0.005f))
|
||||
return true;
|
||||
|
||||
// Scales of ~0.0025 or lower can lead to singular matrices due to rounding errors.
|
||||
Log.Error(
|
||||
$"Attempted to set layer sprite scale to very small values. Entity: {ToPrettyString(sprite)}. Scale: {scale}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#region Transform
|
||||
public void SetScale(Entity<SpriteComponent?> sprite, Vector2 value)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
if (!ValidateScale(sprite!, value))
|
||||
return;
|
||||
|
||||
sprite.Comp._bounds = sprite.Comp._bounds.Scale(value / sprite.Comp.scale);
|
||||
sprite.Comp.scale = value;
|
||||
sprite.Comp.LocalMatrix = Matrix3Helpers.CreateTransform(
|
||||
in sprite.Comp.offset,
|
||||
in sprite.Comp.rotation,
|
||||
in sprite.Comp.scale);
|
||||
}
|
||||
|
||||
public void SetRotation(Entity<SpriteComponent?> sprite, Angle value)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
sprite.Comp.rotation = value;
|
||||
sprite.Comp.LocalMatrix = Matrix3Helpers.CreateTransform(
|
||||
in sprite.Comp.offset,
|
||||
in sprite.Comp.rotation,
|
||||
in sprite.Comp.scale);
|
||||
}
|
||||
|
||||
public void SetOffset(Entity<SpriteComponent?> sprite, Vector2 value)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
sprite.Comp.offset = value;
|
||||
sprite.Comp.LocalMatrix = Matrix3Helpers.CreateTransform(
|
||||
in sprite.Comp.offset,
|
||||
in sprite.Comp.rotation,
|
||||
in sprite.Comp.scale);
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void SetVisible(Entity<SpriteComponent?> sprite, bool value)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
if (sprite.Comp.Visible == value)
|
||||
return;
|
||||
|
||||
sprite.Comp._visible = value;
|
||||
_tree.QueueTreeUpdate(sprite!);
|
||||
}
|
||||
|
||||
public void SetDrawDepth(Entity<SpriteComponent?> sprite, int value)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
sprite.Comp.drawDepth = value;
|
||||
}
|
||||
|
||||
public void SetColor(Entity<SpriteComponent?> sprite, Color value)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
sprite.Comp.color = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modify a sprites base RSI. This is the RSI that is used by any RSI layers that do not specify their own.
|
||||
/// Note that changing the base RSI may result in existing layers having an invalid state. This will not log errors
|
||||
/// under the assumption that the states of each layers will be updated after the base RSI has changed.
|
||||
/// </summary>
|
||||
public void SetBaseRsi(Entity<SpriteComponent?> sprite, RSI? value)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
if (value == sprite.Comp._baseRsi)
|
||||
return;
|
||||
|
||||
sprite.Comp._baseRsi = value;
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
var fallback = GetFallbackState();
|
||||
for (var i = 0; i < sprite.Comp.Layers.Count; i++)
|
||||
{
|
||||
var layer = sprite.Comp.Layers[i];
|
||||
if (!layer.State.IsValid || layer.RSI != null)
|
||||
continue;
|
||||
|
||||
RefreshCachedState(layer, logErrors: false, fallback);
|
||||
|
||||
if (value.TryGetState(layer.State, out var state))
|
||||
{
|
||||
layer.AnimationTimeLeft = state.GetDelay(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"Layer {i} no longer has state '{layer.State}' due to base RSI change. Trace:\n{Environment.StackTrace}");
|
||||
layer.Texture = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetContainerOccluded(Entity<SpriteComponent?> sprite, bool value)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
sprite.Comp._containerOccluded = value;
|
||||
_tree.QueueTreeUpdate(sprite!);
|
||||
}
|
||||
|
||||
public void SetSnapCardinals(Entity<SpriteComponent?> sprite, bool value)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
if (value == sprite.Comp._snapCardinals)
|
||||
return;
|
||||
|
||||
sprite.Comp._snapCardinals = value;
|
||||
_tree.QueueTreeUpdate(sprite!);
|
||||
DirtyBounds(sprite!);
|
||||
}
|
||||
|
||||
public void SetGranularLayersRendering(Entity<SpriteComponent?> sprite, bool value)
|
||||
{
|
||||
if (!_query.Resolve(sprite.Owner, ref sprite.Comp))
|
||||
return;
|
||||
|
||||
if (value == sprite.Comp.GranularLayersRendering)
|
||||
return;
|
||||
|
||||
sprite.Comp.GranularLayersRendering = value;
|
||||
_tree.QueueTreeUpdate(sprite!);
|
||||
DirtyBounds(sprite!);
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed partial class SpriteSystem
|
||||
{
|
||||
private readonly Dictionary<string, IRsiStateLike> _cachedPrototypeIcons = new();
|
||||
|
||||
public Texture Frame0(EntityPrototype prototype)
|
||||
{
|
||||
return GetPrototypeIcon(prototype).Default;
|
||||
}
|
||||
|
||||
public Texture Frame0(SpriteSpecifier specifier)
|
||||
{
|
||||
return RsiStateLike(specifier).Default;
|
||||
}
|
||||
|
||||
public IRsiStateLike RsiStateLike(SpriteSpecifier specifier)
|
||||
{
|
||||
switch (specifier)
|
||||
{
|
||||
case SpriteSpecifier.Texture tex:
|
||||
return tex.GetTexture(_resourceCache);
|
||||
|
||||
case SpriteSpecifier.Rsi rsi:
|
||||
return GetState(rsi);
|
||||
|
||||
case SpriteSpecifier.EntityPrototype prototypeIcon:
|
||||
return GetPrototypeIcon(prototypeIcon.EntityPrototypeId);
|
||||
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public Texture GetIcon(IconComponent icon)
|
||||
{
|
||||
return GetState(icon.Icon).Frame0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
|
||||
/// This method caches the result based on the prototype identifier.
|
||||
/// </summary>
|
||||
public IRsiStateLike GetPrototypeIcon(string prototype)
|
||||
{
|
||||
// Check if this prototype has been cached before, and if so return the result.
|
||||
if (_cachedPrototypeIcons.TryGetValue(prototype, out var cachedResult))
|
||||
return cachedResult;
|
||||
|
||||
if (!_proto.TryIndex<EntityPrototype>(prototype, out var entityPrototype))
|
||||
{
|
||||
// The specified prototype doesn't exist, return the fallback "error" sprite.
|
||||
_sawmill.Error("Failed to load PrototypeIcon {0}", prototype);
|
||||
return GetFallbackState();
|
||||
}
|
||||
|
||||
// Generate the icon and cache it in case it's ever needed again.
|
||||
var result = GetPrototypeIcon(entityPrototype);
|
||||
_cachedPrototypeIcons[prototype] = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an icon for a given <see cref="EntityPrototype"/> ID, or a fallback in case of an error.
|
||||
/// This method does NOT cache the result.
|
||||
/// </summary>
|
||||
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype)
|
||||
{
|
||||
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
|
||||
if (prototype.Components.TryGetValue("Icon", out var compData)
|
||||
&& compData.Component is IconComponent icon)
|
||||
{
|
||||
return GetIcon(icon);
|
||||
}
|
||||
|
||||
// If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback.
|
||||
if (!prototype.Components.ContainsKey("Sprite"))
|
||||
{
|
||||
return GetFallbackState();
|
||||
}
|
||||
|
||||
// Finally, we use spawn a dummy entity to get its icon.
|
||||
var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace);
|
||||
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
|
||||
var result = spriteComponent.Icon ?? GetFallbackState();
|
||||
Del(dummy);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public RSI.State GetFallbackState()
|
||||
{
|
||||
return _resourceCache.GetFallback<RSIResource>().RSI["error"];
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier)
|
||||
{
|
||||
if (_resourceCache.TryGetResource<RSIResource>(
|
||||
SpriteSpecifierSerializer.TextureRoot / rsiSpecifier.RsiPath,
|
||||
out var theRsi) &&
|
||||
theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
_sawmill.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
|
||||
return GetFallbackState();
|
||||
}
|
||||
|
||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
|
||||
{
|
||||
if (!args.TryGetModified<EntityPrototype>(out var modified))
|
||||
return;
|
||||
|
||||
// Remove all changed prototypes from the cache, if they're there.
|
||||
foreach (var prototype in modified)
|
||||
{
|
||||
// Let's be lazy and not regenerate them until something needs them again.
|
||||
_cachedPrototypeIcons.Remove(prototype);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,9 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Graphics.RSI;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.GameObjects.SpriteComponent;
|
||||
@@ -36,23 +35,20 @@ namespace Robust.Client.GameObjects
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xforms = default!;
|
||||
[Dependency] private readonly SpriteTreeSystem _tree = default!;
|
||||
|
||||
public static readonly ProtoId<ShaderPrototype> UnshadedId = "unshaded";
|
||||
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
|
||||
|
||||
public static readonly ResPath TextureRoot = SpriteSpecifierSerializer.TextureRoot;
|
||||
|
||||
/// <summary>
|
||||
/// Entities that require a sprite frame update.
|
||||
/// </summary>
|
||||
private readonly HashSet<EntityUid> _queuedFrameUpdate = new();
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
internal void Render(EntityUid uid, SpriteComponent sprite, DrawingHandleWorld drawingHandle, Angle eyeRotation, in Angle worldRotation, in Vector2 worldPosition)
|
||||
{
|
||||
if (!sprite.IsInert)
|
||||
_queuedFrameUpdate.Add(uid);
|
||||
|
||||
sprite.RenderInternal(drawingHandle, eyeRotation, worldRotation, worldPosition, sprite.EnableDirectionOverride ? sprite.DirectionOverride : null);
|
||||
}
|
||||
private EntityQuery<SpriteComponent> _query;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -61,11 +57,11 @@ namespace Robust.Client.GameObjects
|
||||
UpdatesAfter.Add(typeof(SpriteTreeSystem));
|
||||
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
|
||||
SubscribeLocalEvent<SpriteComponent, SpriteUpdateInertEvent>(QueueUpdateInert);
|
||||
SubscribeLocalEvent<SpriteComponent, ComponentInit>(OnInit);
|
||||
|
||||
Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
|
||||
_sawmill = _logManager.GetSawmill("sprite");
|
||||
_query = GetEntityQuery<SpriteComponent>();
|
||||
}
|
||||
|
||||
public bool IsVisible(Layer layer)
|
||||
@@ -84,18 +80,6 @@ namespace Robust.Client.GameObjects
|
||||
SpriteComponent.DirectionBias = value;
|
||||
}
|
||||
|
||||
private void QueueUpdateInert(EntityUid uid, SpriteComponent sprite, ref SpriteUpdateInertEvent ev)
|
||||
=> QueueUpdateInert(uid, sprite);
|
||||
|
||||
public void QueueUpdateInert(EntityUid uid, SpriteComponent sprite)
|
||||
{
|
||||
if (sprite._inertUpdateQueued)
|
||||
return;
|
||||
|
||||
sprite._inertUpdateQueued = true;
|
||||
_inertUpdateQueue.Enqueue(sprite);
|
||||
}
|
||||
|
||||
private void DoUpdateIsInert(SpriteComponent component)
|
||||
{
|
||||
component._inertUpdateQueued = false;
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ public abstract class VisualizerSystem<T> : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly AppearanceSystem AppearanceSystem = default!;
|
||||
[Dependency] protected readonly AnimationPlayerSystem AnimationSystem = default!;
|
||||
[Dependency] protected readonly SpriteSystem SpriteSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1288,6 +1306,11 @@ namespace Robust.Client.GameStates
|
||||
meta.LastStateApplied = lastStateApplied.Value;
|
||||
|
||||
var xform = xforms.GetComponent(ent.Value);
|
||||
|
||||
// TODO PVS DETACH
|
||||
// Why is this if block here again? If a null-space entity gets sent to a player via some PVS override,
|
||||
// and then later on it gets removed, you would assume that the client marks it as detached?
|
||||
// I.e., modifying the metadata flag & pausing the entity should probably happen outside of this block.
|
||||
if (xform.ParentUid.IsValid())
|
||||
{
|
||||
lookupSys.RemoveFromEntityTree(ent.Value, xform);
|
||||
@@ -1308,6 +1331,13 @@ namespace Robust.Client.GameStates
|
||||
xformSys.DetachEntity(ent.Value, xform);
|
||||
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0);
|
||||
|
||||
// We mark the entity as paused, without raising a pause-event.
|
||||
// The entity gets un-paused when the metadata's comp-state is reapplied (which also does not raise
|
||||
// an un-pause event). The assumption is that game logic that has to handle the pausing should be
|
||||
// getting networked anyway. And if its some client-side timer on a networked entity, the timer
|
||||
// shouldn't actually be getting paused just because the entity has left the players view.
|
||||
meta.PauseTime = TimeSpan.Zero;
|
||||
|
||||
if (container != null)
|
||||
containerSys.AddExpectedEntity(netEntity, container);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,147 @@ 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];
|
||||
}
|
||||
|
||||
var rotationMirroring = _tileDefinitionManager[tile.TypeId].AllowRotationMirror
|
||||
? tile.RotationMirroring
|
||||
: 0;
|
||||
|
||||
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, rotationMirroring);
|
||||
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, 0);
|
||||
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 +375,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)
|
||||
@@ -231,8 +412,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private void _updateTileMapOnUpdate(ref TileChangedEvent args)
|
||||
{
|
||||
var gridData = _mapChunkData.GetOrNew(args.Entity);
|
||||
if (gridData.TryGetValue(args.ChunkIndex, out var data))
|
||||
data.Dirty = true;
|
||||
foreach (var change in args.Changes)
|
||||
{
|
||||
if (gridData.TryGetValue(change.ChunkIndex, out var data))
|
||||
data.Dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void _updateOnGridCreated(GridStartupEvent ev)
|
||||
@@ -254,19 +438,93 @@ 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,
|
||||
int rotationMirroring)
|
||||
{
|
||||
var rLeftBottom = (region.Left, region.Bottom);
|
||||
var rRightBottom = (region.Right, region.Bottom);
|
||||
var rRightTop = (region.Right, region.Top);
|
||||
var rLeftTop = (region.Left, region.Top);
|
||||
|
||||
// The vertices must be changed if there's any rotation or mirroring to the tile
|
||||
if (rotationMirroring != 0)
|
||||
{
|
||||
// Rotate the tile
|
||||
for (int r = 0; r < rotationMirroring % 4; r++)
|
||||
{
|
||||
(rLeftBottom, rRightBottom, rRightTop, rLeftTop) =
|
||||
(rLeftTop, rLeftBottom, rRightBottom, rRightTop);
|
||||
}
|
||||
|
||||
// Mirror on the x-axis
|
||||
if (rotationMirroring >= 4)
|
||||
{
|
||||
if (rotationMirroring % 2 == 0)
|
||||
{
|
||||
rLeftBottom = (rLeftBottom.Item1.Equals(region.Left) ? region.Right : region.Left,
|
||||
rLeftBottom.Item2);
|
||||
rRightBottom = (rRightBottom.Item1.Equals(region.Left) ? region.Right : region.Left,
|
||||
rRightBottom.Item2);
|
||||
rRightTop = (rRightTop.Item1.Equals(region.Left) ? region.Right : region.Left,
|
||||
rRightTop.Item2);
|
||||
rLeftTop = (rLeftTop.Item1.Equals(region.Left) ? region.Right : region.Left,
|
||||
rLeftTop.Item2);
|
||||
}
|
||||
else
|
||||
{
|
||||
rLeftBottom = (rLeftBottom.Item1,
|
||||
rLeftBottom.Item2.Equals(region.Bottom) ? region.Top : region.Bottom);
|
||||
rRightBottom = (rRightBottom.Item1,
|
||||
rRightBottom.Item2.Equals(region.Bottom) ? region.Top : region.Bottom);
|
||||
rRightTop = (rRightTop.Item1,
|
||||
rRightTop.Item2.Equals(region.Bottom) ? region.Top : region.Bottom);
|
||||
rLeftTop = (rLeftTop.Item1,
|
||||
rLeftTop.Item2.Equals(region.Bottom) ? region.Top : region.Bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var vIdx = i * 4;
|
||||
vertexBuffer[vIdx + 0] = new Vertex2D(gridX, gridY, rLeftBottom.Left, rLeftBottom.Bottom, Color.White);
|
||||
vertexBuffer[vIdx + 1] = new Vertex2D(gridX + 1, gridY, rRightBottom.Right, rRightBottom.Bottom, Color.White);
|
||||
vertexBuffer[vIdx + 2] = new Vertex2D(gridX + 1, gridY + 1, rRightTop.Right, rRightTop.Top, Color.White);
|
||||
vertexBuffer[vIdx + 3] = new Vertex2D(gridX, gridY + 1, rLeftTop.Left, rLeftTop.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,9 +123,13 @@ 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 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))
|
||||
@@ -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, _mapSystem.GetMapOrInvalid(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)
|
||||
{
|
||||
@@ -304,7 +318,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
screenSpriteSize.Y++;
|
||||
|
||||
bool exit = false;
|
||||
if (entry.Sprite.GetScreenTexture)
|
||||
if (entry.Sprite.GetScreenTexture && entry.Sprite.PostShader != null)
|
||||
{
|
||||
FlushRenderQueue();
|
||||
var tex = CopyScreenTexture(viewport.RenderTarget);
|
||||
@@ -355,7 +369,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
spriteSystem.Render(entry.Uid, entry.Sprite, _renderHandle.DrawingHandleWorld, eye.Rotation, in entry.WorldRot, in entry.WorldPos);
|
||||
spriteSystem.RenderSprite(new(entry.Uid, entry.Sprite), _renderHandle.DrawingHandleWorld, eye.Rotation, entry.WorldRot, entry.WorldPos);
|
||||
|
||||
if (entry.Sprite.PostShader != null && entityPostRenderTarget != null)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -613,6 +613,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
EnsureBatchSpaceAvailable(4, GetQuadBatchIndexCount());
|
||||
EnsureBatchState(texture, true, GetQuadBatchPrimitiveType(), _queuedShader);
|
||||
|
||||
// TODO RENDERING
|
||||
// It's probably better to do this on the GPU.
|
||||
bl = Vector2.Transform(bl, _currentMatrixModel);
|
||||
br = Vector2.Transform(br, _currentMatrixModel);
|
||||
tr = Vector2.Transform(tr, _currentMatrixModel);
|
||||
|
||||
@@ -153,7 +153,7 @@ internal partial class Clyde
|
||||
|
||||
// special casing angle = n*pi/2 to avoid box rotation & bounding calculations doesn't seem to give significant speedups.
|
||||
data.SpriteScreenBB = TransformCenteredBox(
|
||||
data.Sprite.Bounds,
|
||||
_spriteSystem.GetLocalBounds((data.Uid, data.Sprite)),
|
||||
finalRotation,
|
||||
pos + batch.PreScaleViewOffset,
|
||||
batch.ViewScale);
|
||||
|
||||
@@ -10,6 +10,7 @@ internal sealed partial class Clyde
|
||||
private MapSystem _mapSystem = default!;
|
||||
private LightTreeSystem _lightTreeSystem = default!;
|
||||
private TransformSystem _transformSystem = default!;
|
||||
private SpriteSystem _spriteSystem = default!;
|
||||
private SpriteTreeSystem _spriteTreeSystem = default!;
|
||||
private ClientOccluderSystem _occluderSystem = default!;
|
||||
|
||||
@@ -24,6 +25,7 @@ internal sealed partial class Clyde
|
||||
_mapSystem = _entitySystemManager.GetEntitySystem<MapSystem>();
|
||||
_lightTreeSystem = _entitySystemManager.GetEntitySystem<LightTreeSystem>();
|
||||
_transformSystem = _entitySystemManager.GetEntitySystem<TransformSystem>();
|
||||
_spriteSystem = _entitySystemManager.GetEntitySystem<SpriteSystem>();
|
||||
_spriteTreeSystem = _entitySystemManager.GetEntitySystem<SpriteTreeSystem>();
|
||||
_occluderSystem = _entitySystemManager.GetEntitySystem<ClientOccluderSystem>();
|
||||
}
|
||||
@@ -33,6 +35,7 @@ internal sealed partial class Clyde
|
||||
_mapSystem = null!;
|
||||
_lightTreeSystem = null!;
|
||||
_transformSystem = null!;
|
||||
_spriteSystem = null!;
|
||||
_spriteTreeSystem = null!;
|
||||
_occluderSystem = null!;
|
||||
}
|
||||
|
||||
@@ -305,8 +305,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
IsSrgb = srgb,
|
||||
Name = name,
|
||||
MemoryPressure = memoryPressure,
|
||||
TexturePixelType = pixType
|
||||
// TextureInstance = new WeakReference<ClydeTexture>(instance)
|
||||
TexturePixelType = pixType,
|
||||
TextureInstance = new WeakReference<ClydeTexture>(instance)
|
||||
};
|
||||
|
||||
_loadedTextures.Add(id, loaded);
|
||||
@@ -466,15 +466,15 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
var white = new Image<Rgba32>(1, 1);
|
||||
white[0, 0] = new Rgba32(255, 255, 255, 255);
|
||||
_stockTextureWhite = (ClydeTexture) Texture.LoadFromImage(white);
|
||||
_stockTextureWhite = (ClydeTexture) Texture.LoadFromImage(white, name: "StockTextureWhite");
|
||||
|
||||
var black = new Image<Rgba32>(1, 1);
|
||||
black[0, 0] = new Rgba32(0, 0, 0, 255);
|
||||
_stockTextureBlack = (ClydeTexture) Texture.LoadFromImage(black);
|
||||
_stockTextureBlack = (ClydeTexture) Texture.LoadFromImage(black, name: "StockTextureBlack");
|
||||
|
||||
var blank = new Image<Rgba32>(1, 1);
|
||||
blank[0, 0] = new Rgba32(0, 0, 0, 0);
|
||||
_stockTextureTransparent = (ClydeTexture) Texture.LoadFromImage(blank);
|
||||
_stockTextureTransparent = (ClydeTexture) Texture.LoadFromImage(blank, name: "StockTextureTransparent");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -571,7 +571,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LoadedTexture
|
||||
internal sealed class LoadedTexture
|
||||
{
|
||||
public GLHandle OpenGLObject;
|
||||
public int Width;
|
||||
@@ -582,10 +582,10 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public TexturePixelType TexturePixelType;
|
||||
|
||||
public Vector2i Size => (Width, Height);
|
||||
// public WeakReference<ClydeTexture> TextureInstance;
|
||||
public required WeakReference<ClydeTexture> TextureInstance;
|
||||
}
|
||||
|
||||
private enum TexturePixelType : byte
|
||||
internal enum TexturePixelType : byte
|
||||
{
|
||||
RenderTarget = 0,
|
||||
Rgba32,
|
||||
@@ -686,5 +686,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_ => throw new ArgumentException(nameof(stockTexture))
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<(ClydeTexture, LoadedTexture)> GetLoadedTextures()
|
||||
{
|
||||
foreach (var loaded in _loadedTextures.Values)
|
||||
{
|
||||
if (!loaded.TextureInstance.TryGetTarget(out var textureInstance))
|
||||
continue;
|
||||
|
||||
yield return (textureInstance, loaded);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_windowing!.RunOnWindowThread(a);
|
||||
}
|
||||
|
||||
public IFileDialogManager? FileDialogImpl => _windowing as IFileDialogManager;
|
||||
public IFileDialogManagerImplementation? FileDialogImpl => _windowing as IFileDialogManagerImplementation;
|
||||
|
||||
private abstract class WindowReg
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -72,6 +72,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return new DummyTexture((1, 1));
|
||||
}
|
||||
|
||||
public IEnumerable<(Clyde.ClydeTexture, Clyde.LoadedTexture)> GetLoadedTextures()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public ClydeDebugLayers DebugLayers { get; set; }
|
||||
|
||||
public string GetKeyName(Keyboard.Key key) => string.Empty;
|
||||
@@ -302,7 +307,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
action();
|
||||
}
|
||||
|
||||
public IFileDialogManager? FileDialogImpl => null;
|
||||
public IFileDialogManagerImplementation? FileDialogImpl => null;
|
||||
|
||||
private sealed class DummyCursor : ICursor
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
/// Basically just a handle around the integer object handles returned by OpenGL.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
private struct GLHandle : IEquatable<GLHandle>
|
||||
internal struct GLHandle : IEquatable<GLHandle>
|
||||
{
|
||||
public readonly uint Handle;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
varying highp vec2 UV;
|
||||
varying highp vec2 UV2;
|
||||
|
||||
// TODO CLYDE consistent shader variable naming
|
||||
uniform sampler2D lightMap;
|
||||
|
||||
// [SHADER_HEADER_CODE]
|
||||
|
||||
@@ -87,11 +87,13 @@ namespace Robust.Client.Graphics.Clyde
|
||||
if (cmd.Cursor != default)
|
||||
ptr = _winThreadCursors[cmd.Cursor].Ptr;
|
||||
|
||||
#if DEBUG
|
||||
if (_win32Experience)
|
||||
{
|
||||
// Based on a true story.
|
||||
Thread.Sleep(15);
|
||||
}
|
||||
#endif
|
||||
|
||||
GLFW.SetCursor(window, ptr);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private readonly ISawmill _sawmillGlfw;
|
||||
|
||||
private bool _glfwInitialized;
|
||||
#if DEBUG
|
||||
private bool _win32Experience;
|
||||
#endif
|
||||
|
||||
public GlfwWindowingImpl(Clyde clyde, IDependencyCollection deps)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -12,31 +11,16 @@ namespace Robust.Client.Graphics.Clyde;
|
||||
|
||||
internal partial class Clyde
|
||||
{
|
||||
private sealed partial class Sdl3WindowingImpl : IFileDialogManager
|
||||
private sealed partial class Sdl3WindowingImpl : IFileDialogManagerImplementation
|
||||
{
|
||||
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
|
||||
public async Task<string?> OpenFile(FileDialogFilters? filters)
|
||||
{
|
||||
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
|
||||
if (fileName == null)
|
||||
return null;
|
||||
|
||||
return File.OpenRead(fileName);
|
||||
return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_OPENFILE, filters);
|
||||
}
|
||||
|
||||
public async Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
|
||||
public async Task<string?> SaveFile(FileDialogFilters? filters)
|
||||
{
|
||||
var fileName = await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
|
||||
if (fileName == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return (File.Open(fileName, truncate ? FileMode.Truncate : FileMode.Open), true);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return (File.Open(fileName, FileMode.Create), false);
|
||||
}
|
||||
return await ShowFileDialogOfType(SDL.SDL_FILEDIALOG_SAVEFILE, filters);
|
||||
}
|
||||
|
||||
private unsafe Task<string?> ShowFileDialogOfType(int type, FileDialogFilters? filters)
|
||||
@@ -74,6 +58,8 @@ internal partial class Clyde
|
||||
NativeMemory.Free(filter.name);
|
||||
NativeMemory.Free(filter.pattern);
|
||||
}
|
||||
|
||||
NativeMemory.Free(filtersAlloc);
|
||||
}
|
||||
|
||||
return task;
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace Robust.Client.Graphics
|
||||
IClydeDebugStats DebugStats { get; }
|
||||
|
||||
Texture GetStockTexture(ClydeStockTexture stockTexture);
|
||||
IEnumerable<(Clyde.Clyde.ClydeTexture, Clyde.Clyde.LoadedTexture)> GetLoadedTextures();
|
||||
|
||||
ClydeDebugLayers DebugLayers { get; set; }
|
||||
|
||||
@@ -70,6 +71,6 @@ namespace Robust.Client.Graphics
|
||||
|
||||
void RunOnWindowThread(Action action);
|
||||
|
||||
IFileDialogManager? FileDialogImpl { get; }
|
||||
IFileDialogManagerImplementation? FileDialogImpl { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
33
Robust.Client/Localization/ClientLocalizationManager.cs
Normal file
33
Robust.Client/Localization/ClientLocalizationManager.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Localization;
|
||||
|
||||
internal sealed class ClientLocalizationManager : LocalizationManager, ILocalizationManagerInternal
|
||||
{
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
|
||||
void ILocalizationManager.Initialize() => Initialize();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_reload.Register(LocaleDirPath, "*.ftl");
|
||||
|
||||
_reload.OnChanged += OnReload;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles Fluent hot reloading via LocalizationManager.ReloadLocalizations()
|
||||
/// </summary>
|
||||
private void OnReload(ResPath args)
|
||||
{
|
||||
if (args.Extension != "ftl")
|
||||
return;
|
||||
|
||||
ReloadLocalizations();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ namespace Robust.Client.Physics
|
||||
* This will draw above every body involved in a particular island solve.
|
||||
*/
|
||||
|
||||
public readonly Queue<(TimeSpan Time, List<PhysicsComponent> Bodies)> IslandSolve = new();
|
||||
public readonly Queue<(TimeSpan Time, List<Entity<PhysicsComponent, TransformComponent>> Bodies)> IslandSolve = new();
|
||||
public const float SolveDuration = 0.1f;
|
||||
|
||||
public override void Initialize()
|
||||
|
||||
@@ -90,9 +90,10 @@ public sealed partial class PhysicsSystem
|
||||
// existing contacts for predicted entities before performing any actual prediction.
|
||||
|
||||
var contacts = new List<Contact>();
|
||||
var maps = new HashSet<EntityUid>();
|
||||
|
||||
var enumerator = AllEntityQuery<PredictedPhysicsComponent, PhysicsComponent, TransformComponent>();
|
||||
_broadphase.FindNewContacts();
|
||||
|
||||
while (enumerator.MoveNext(out _, out var physics, out var xform))
|
||||
{
|
||||
DebugTools.Assert(physics.Predict);
|
||||
@@ -100,10 +101,6 @@ public sealed partial class PhysicsSystem
|
||||
if (xform.MapUid is not { } map)
|
||||
continue;
|
||||
|
||||
if (maps.Add(map) && PhysMapQuery.TryGetComponent(map, out var physMap) &&
|
||||
MapQuery.TryGetComponent(map, out var mapComp))
|
||||
_broadphase.FindNewContacts(physMap, mapComp.MapId);
|
||||
|
||||
contacts.AddRange(physics.Contacts);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Physics;
|
||||
@@ -23,21 +24,23 @@ namespace Robust.Client.Physics
|
||||
SimulateWorld(frameTime, _gameTiming.InPrediction);
|
||||
}
|
||||
|
||||
protected override void Cleanup(PhysicsMapComponent component, float frameTime)
|
||||
protected override void Cleanup(float frameTime)
|
||||
{
|
||||
var toRemove = new List<Entity<PhysicsComponent>>();
|
||||
var toRemove = new ValueList<Entity<PhysicsComponent, TransformComponent>>();
|
||||
|
||||
// Because we're not predicting 99% of bodies its sleep timer never gets incremented so we'll just do it ourselves.
|
||||
// (and serializing it over the network isn't necessary?)
|
||||
// This is a client-only problem.
|
||||
// Also need to suss out having the client build the island anyway and just... not solving it?
|
||||
foreach (var body in component.AwakeBodies)
|
||||
foreach (var ent in AwakeBodies)
|
||||
{
|
||||
var body = ent.Comp1;
|
||||
|
||||
if (!body.SleepingAllowed || body.LinearVelocity.Length() > LinearToleranceSqr / 2f || body.AngularVelocity * body.AngularVelocity > AngularToleranceSqr / 2f) continue;
|
||||
body.SleepTime += frameTime;
|
||||
if (body.SleepTime > TimeToSleep)
|
||||
{
|
||||
toRemove.Add(new Entity<PhysicsComponent>(body.Owner, body));
|
||||
toRemove.Add(ent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,37 +49,38 @@ namespace Robust.Client.Physics
|
||||
SetAwake(body, false);
|
||||
}
|
||||
|
||||
base.Cleanup(component, frameTime);
|
||||
base.Cleanup(frameTime);
|
||||
}
|
||||
|
||||
protected override void UpdateLerpData(PhysicsMapComponent component, List<PhysicsComponent> bodies, EntityQuery<TransformComponent> xformQuery)
|
||||
protected override void UpdateLerpData(List<Entity<PhysicsComponent, TransformComponent>> bodies)
|
||||
{
|
||||
foreach (var body in bodies)
|
||||
foreach (var bodyEnt in bodies)
|
||||
{
|
||||
var body = bodyEnt.Comp1;
|
||||
var xform = bodyEnt.Comp2;
|
||||
|
||||
if (body.BodyType == BodyType.Static ||
|
||||
component.LerpData.TryGetValue(body.Owner, out var lerpData) ||
|
||||
!xformQuery.TryGetComponent(body.Owner, out var xform) ||
|
||||
lerpData.ParentUid == xform.ParentUid)
|
||||
LerpData.TryGetValue(bodyEnt, out var lerpData) ||
|
||||
lerpData == xform.ParentUid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
component.LerpData[xform.Owner] = (xform.ParentUid, xform.LocalPosition, xform.LocalRotation);
|
||||
LerpData[bodyEnt.Owner] = xform.ParentUid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush all of our lerping data.
|
||||
/// </summary>
|
||||
protected override void FinalStep(PhysicsMapComponent component)
|
||||
protected override void FinalStep()
|
||||
{
|
||||
base.FinalStep(component);
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
base.FinalStep();
|
||||
|
||||
foreach (var (uid, (parentUid, position, rotation)) in component.LerpData)
|
||||
foreach (var (uid, parentUid) in LerpData)
|
||||
{
|
||||
if (!xformQuery.TryGetComponent(uid, out var xform) ||
|
||||
!parentUid.IsValid())
|
||||
// Can't just re-use xform from before as movement events may cause event subs to fire.
|
||||
if (!XformQuery.TryGetComponent(uid, out var xform) || !parentUid.IsValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -85,7 +89,7 @@ namespace Robust.Client.Physics
|
||||
_transform.SetLocalPositionRotation(uid, xform.LocalPosition, xform.LocalRotation, xform);
|
||||
}
|
||||
|
||||
component.LerpData.Clear();
|
||||
LerpData.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,20 @@ namespace Robust.Client.Placement
|
||||
Direction Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets called when Direction changed (presently for EntitySpawnWindow UI)
|
||||
/// Whether a tile placement should be mirrored or not.
|
||||
/// </summary>
|
||||
bool Mirrored { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets called when Direction changed (presently for EntitySpawnWindow/TileSpawnWindow UI)
|
||||
/// </summary>
|
||||
event EventHandler DirectionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets called when Mirrored changed (presently for TileSpawnWindow UI)
|
||||
/// </summary>
|
||||
event EventHandler MirroredChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets called when the PlacementManager changed its build/erase mode or when the hijacks changed
|
||||
/// </summary>
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -47,6 +47,7 @@ namespace Robust.Client.Placement
|
||||
|
||||
private SharedMapSystem Maps => EntityManager.System<SharedMapSystem>();
|
||||
private SharedTransformSystem XformSystem => EntityManager.System<SharedTransformSystem>();
|
||||
private SpriteSystem Sprite => EntityManager.System<SpriteSystem>();
|
||||
|
||||
/// <summary>
|
||||
/// How long before a pending tile change is dropped.
|
||||
@@ -174,6 +175,18 @@ namespace Robust.Client.Placement
|
||||
|
||||
private Direction _direction = Direction.South;
|
||||
|
||||
private bool _mirrored;
|
||||
|
||||
public bool Mirrored
|
||||
{
|
||||
get => _mirrored;
|
||||
set
|
||||
{
|
||||
_mirrored = value;
|
||||
MirroredChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Direction Direction
|
||||
{
|
||||
@@ -188,6 +201,9 @@ namespace Robust.Client.Placement
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? DirectionChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? MirroredChanged;
|
||||
|
||||
private PlacementOverlay _drawOverlay = default!;
|
||||
private bool _isActive;
|
||||
|
||||
@@ -344,18 +360,27 @@ namespace Robust.Client.Placement
|
||||
|
||||
private void HandleTileChanged(ref TileChangedEvent args)
|
||||
{
|
||||
var coords = Maps.GridTileToLocal(
|
||||
args.NewTile.GridUid,
|
||||
EntityManager.GetComponent<MapGridComponent>(args.NewTile.GridUid),
|
||||
args.NewTile.GridIndices);
|
||||
foreach (var change in args.Changes)
|
||||
{
|
||||
var coords = Maps.GridTileToLocal(
|
||||
args.Entity,
|
||||
args.Entity.Comp,
|
||||
change.GridIndices);
|
||||
|
||||
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
|
||||
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? PlacementChanged;
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ClearWithoutDeactivation();
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
private void ClearWithoutDeactivation()
|
||||
{
|
||||
PlacementChanged?.Invoke(this, EventArgs.Empty);
|
||||
Hijack = null;
|
||||
@@ -365,7 +390,6 @@ namespace Robust.Client.Placement
|
||||
CurrentMode = null;
|
||||
DeactivateSpecialPlacement();
|
||||
_placenextframe = false;
|
||||
IsActive = false;
|
||||
Eraser = false;
|
||||
EraserRect = null;
|
||||
PlacementOffset = Vector2i.Zero;
|
||||
@@ -480,18 +504,17 @@ namespace Robust.Client.Placement
|
||||
|
||||
public void BeginHijackedPlacing(PlacementInformation info, PlacementHijack? hijack = null)
|
||||
{
|
||||
Clear();
|
||||
ClearWithoutDeactivation();
|
||||
|
||||
CurrentPermission = info;
|
||||
|
||||
if (!_modeDictionary.TryFirstOrNull(pair => pair.Key.Equals(CurrentPermission.PlacementOption), out KeyValuePair<string, Type>? placeMode))
|
||||
if (info.PlacementOption is not { } option || !_modeDictionary.TryGetValue(option, out var placeMode))
|
||||
{
|
||||
_sawmill.Log(LogLevel.Warning, $"Invalid placement mode `{CurrentPermission.PlacementOption}`");
|
||||
_sawmill.Log(LogLevel.Warning, $"Invalid placement mode `{info.PlacementOption}`");
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentMode = (PlacementMode) Activator.CreateInstance(placeMode.Value.Value, this)!;
|
||||
CurrentPermission = info;
|
||||
CurrentMode = (PlacementMode) Activator.CreateInstance(placeMode, this)!;
|
||||
|
||||
if (hijack != null)
|
||||
{
|
||||
@@ -689,11 +712,11 @@ namespace Robust.Client.Placement
|
||||
CurrentPlacementOverlayEntity = null;
|
||||
}
|
||||
|
||||
private SpriteComponent SetupPlacementOverlayEntity()
|
||||
private Entity<SpriteComponent> SetupPlacementOverlayEntity()
|
||||
{
|
||||
EnsureNoPlacementOverlayEntity();
|
||||
CurrentPlacementOverlayEntity = EntityManager.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
return EntityManager.EnsureComponent<SpriteComponent>(CurrentPlacementOverlayEntity.Value);
|
||||
return (CurrentPlacementOverlayEntity.Value, EntityManager.EnsureComponent<SpriteComponent>(CurrentPlacementOverlayEntity.Value));
|
||||
}
|
||||
|
||||
private void PreparePlacement(string templateName)
|
||||
@@ -710,10 +733,16 @@ namespace Robust.Client.Placement
|
||||
EntityManager.GetComponent<MetaDataComponent>(CurrentPlacementOverlayEntity.Value));
|
||||
}
|
||||
|
||||
public void PreparePlacementSprite(SpriteComponent sprite)
|
||||
public void PreparePlacementSprite(Entity<SpriteComponent> sprite)
|
||||
{
|
||||
var sc = SetupPlacementOverlayEntity();
|
||||
sc.CopyFrom(sprite);
|
||||
Sprite.CopySprite(sprite.AsNullable(), sc.AsNullable());
|
||||
}
|
||||
|
||||
[Obsolete("Use the Entity<SpriteComponent> overload.")]
|
||||
public void PreparePlacementSprite(SpriteComponent sprite)
|
||||
{
|
||||
PreparePlacementSprite((sprite.Owner, sprite));
|
||||
}
|
||||
|
||||
public void PreparePlacementTexList(List<IDirectionalTextureProvider>? texs, bool noRot, EntityPrototype? prototype)
|
||||
@@ -724,27 +753,27 @@ namespace Robust.Client.Placement
|
||||
// This one covers most cases (including Construction)
|
||||
foreach (var v in texs)
|
||||
{
|
||||
if (v is RSI.State)
|
||||
if (v is RSI.State st)
|
||||
{
|
||||
var st = (RSI.State) v;
|
||||
sc.AddLayer(st.StateId, st.RSI);
|
||||
Sprite.AddRsiLayer(sc.AsNullable(), st.StateId, st.RSI);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
sc.AddLayer(v.Default);
|
||||
Sprite.AddTextureLayer(sc.AsNullable(), v.Default);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sc.AddLayer(new ResPath("/Textures/Interface/tilebuildoverlay.png"));
|
||||
Sprite.AddTextureLayer(sc.AsNullable(), new ResPath("/Textures/Interface/tilebuildoverlay.png"));
|
||||
}
|
||||
sc.NoRotation = noRot;
|
||||
|
||||
sc.Comp.NoRotation = noRot;
|
||||
|
||||
if (prototype != null && prototype.TryGetComponent<SpriteComponent>("Sprite", out var spriteComp))
|
||||
{
|
||||
sc.Scale = spriteComp.Scale;
|
||||
Sprite.SetScale(sc.AsNullable(), spriteComp.Scale);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -752,7 +781,7 @@ namespace Robust.Client.Placement
|
||||
private void PreparePlacementTile()
|
||||
{
|
||||
var sc = SetupPlacementOverlayEntity();
|
||||
sc.AddLayer(new ResPath("/Textures/Interface/tilebuildoverlay.png"));
|
||||
Sprite.AddTextureLayer(sc.AsNullable(), new ResPath("/Textures/Interface/tilebuildoverlay.png"));
|
||||
|
||||
IsActive = true;
|
||||
}
|
||||
@@ -772,7 +801,9 @@ namespace Robust.Client.Placement
|
||||
var grid = EntityManager.GetComponent<MapGridComponent>(gridId);
|
||||
|
||||
// no point changing the tile to the same thing.
|
||||
if (Maps.GetTileRef(gridId, grid, coordinates).Tile.TypeId == CurrentPermission.TileType)
|
||||
var tileRef = Maps.GetTileRef(gridId, grid, coordinates).Tile;
|
||||
if (tileRef.TypeId == CurrentPermission.TileType &&
|
||||
tileRef.RotationMirroring == Tile.DirectionToByte(Direction) + (Mirrored ? 4 : 0))
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -796,9 +827,14 @@ namespace Robust.Client.Placement
|
||||
};
|
||||
|
||||
if (CurrentPermission.IsTile)
|
||||
{
|
||||
message.TileType = CurrentPermission.TileType;
|
||||
message.Mirrored = Mirrored;
|
||||
}
|
||||
else
|
||||
{
|
||||
message.EntityTemplateName = CurrentPermission.EntityType;
|
||||
}
|
||||
|
||||
// world x and y
|
||||
message.NetCoordinates = EntityManager.GetNetCoordinates(coordinates);
|
||||
|
||||
@@ -121,12 +121,12 @@ 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;
|
||||
spriteSys.Render(uid.Value, sprite, args.WorldHandle, rot, worldRot, worldPos);
|
||||
spriteSys.RenderSprite((uid.Value, sprite), args.WorldHandle, rot, worldRot, worldPos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ namespace Robust.Client.Player
|
||||
{
|
||||
if (_client.RunLevel != ClientRunLevel.SinglePlayerGame)
|
||||
Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues.");
|
||||
var eye = (EyeComponent) Factory.GetComponent(typeof(EyeComponent));
|
||||
var eye = Factory.GetComponent<EyeComponent>();
|
||||
eye.NetSyncEnabled = false;
|
||||
EntManager.AddComponent(uid.Value, eye);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -15,7 +13,9 @@ namespace Robust.Client.Prototypes
|
||||
public sealed class ClientPrototypeManager : PrototypeManager
|
||||
{
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
#pragma warning disable CS0414
|
||||
[Dependency] private readonly IClientGameTiming _timing = default!;
|
||||
#pragma warning restore CS0414
|
||||
[Dependency] private readonly IGameControllerInternal _controller = default!;
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
|
||||
@@ -56,7 +56,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -8,6 +9,7 @@ using Robust.Client.Graphics;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Graphics;
|
||||
@@ -59,7 +61,14 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
try
|
||||
{
|
||||
TextureResource.LoadPreTexture(_manager, data);
|
||||
TextureResource.LoadTextureParameters(_manager, data);
|
||||
if (!data.LoadParameters.Preload)
|
||||
{
|
||||
data.Skip = true;
|
||||
return;
|
||||
}
|
||||
|
||||
TextureResource.LoadPreTextureData(_manager, data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -72,7 +81,7 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
foreach (var data in texList)
|
||||
{
|
||||
if (data.Bad)
|
||||
if (data.Bad || data.Skip)
|
||||
continue;
|
||||
|
||||
try
|
||||
@@ -87,6 +96,7 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
|
||||
var errors = 0;
|
||||
var skipped = 0;
|
||||
foreach (var data in texList)
|
||||
{
|
||||
if (data.Bad)
|
||||
@@ -95,6 +105,12 @@ namespace Robust.Client.ResourceManagement
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data.Skip)
|
||||
{
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var texResource = new TextureResource();
|
||||
@@ -110,9 +126,10 @@ namespace Robust.Client.ResourceManagement
|
||||
}
|
||||
|
||||
sawmill.Debug(
|
||||
"Preloaded {CountLoaded} textures ({CountErrored} errored) in {LoadTime}",
|
||||
texList.Length,
|
||||
"Preloaded {CountLoaded} textures ({CountErrored} errored, {CountSkipped} skipped) in {LoadTime}",
|
||||
texList.Length - skipped - errors,
|
||||
errors,
|
||||
skipped,
|
||||
sw.Elapsed);
|
||||
}
|
||||
|
||||
@@ -176,65 +193,143 @@ namespace Robust.Client.ResourceManagement
|
||||
// TODO allow RSIs to opt out (useful for very big & rare RSIs)
|
||||
// TODO combine with (non-rsi) texture atlas?
|
||||
|
||||
Array.Sort(atlasList, (b, a) => (b.AtlasSheet?.Height ?? 0).CompareTo(a.AtlasSheet?.Height ?? 0));
|
||||
// We now need to insert the RSIs into the atlas. This specific problem is 2BP|O|F - the items are oriented
|
||||
// and cutting is free. The sorting is done by a slightly modified FFDH algorithm. The algorithm is exactly
|
||||
// the same as the standard FFDH algorithm with one main difference: We create new "levels" above placed
|
||||
// blocks. For example if the first block was 10x20, then the second was 10x10 units, we would create a
|
||||
// 10x10 level above the second block that would be treated as a normal level. This increases the packing
|
||||
// efficiency from ~85% to ~95% with very little extra computational effort. The algorithm appears to be
|
||||
// ~97% effective for storing SS14s RSIs.
|
||||
//
|
||||
// Here are some more resources about the strip packing problem!
|
||||
// - https://en.wikipedia.org/w/index.php?title=Strip_packing_problem&oldid=1263496949#First-fit_decreasing-height_(FFDH)
|
||||
// - https://www.csc.liv.ac.uk/~epa/surveyhtml.html
|
||||
// - https://www.dei.unipd.it/~fisch/ricop/tesi/tesi_dottorato_Lodi_1999.pdf
|
||||
|
||||
// The array must be sorted from biggest to smallest first.
|
||||
Array.Sort(atlasList, (b, a) => a.AtlasSheet.Height.CompareTo(b.AtlasSheet.Height));
|
||||
|
||||
// Each RSI sub atlas has a different size.
|
||||
// Even if we iterate through them once to estimate total area, I have NFI how to sanely estimate an optimal square-texture size.
|
||||
// So fuck it, just default to letting it be as large as it needs to and crop it as needed?
|
||||
var maxSize = Math.Min(GL.GetInteger(GetPName.MaxTextureSize), _configurationManager.GetCVar(CVars.ResRSIAtlasSize));
|
||||
var sheet = new Image<Rgba32>(maxSize, maxSize);
|
||||
|
||||
var deltaY = 0;
|
||||
Vector2i offset = default;
|
||||
int finalized = -1;
|
||||
int atlasCount = 0;
|
||||
for (int i = 0; i < atlasList.Length; i++)
|
||||
// THIS IS NOT GUARANTEED TO HAVE ANY PARTICULARLY LOGICAL ORDERING.
|
||||
// E.G you could have atlas 1 RSIs appear *before* you're done seeing atlas 2 RSIs.
|
||||
var levels = new ValueList<Level>();
|
||||
|
||||
// List of all the image atlases.
|
||||
var imageAtlases = new ValueList<Image<Rgba32>>();
|
||||
|
||||
// List of all the actual atlases.
|
||||
var finalAtlases = new ValueList<OwnedTexture>();
|
||||
|
||||
// Number of total pixels in each atlas.
|
||||
var finalPixels = new ValueList<int>();
|
||||
|
||||
// First we just find the location of all the RSIs in the atlas before actually placing them.
|
||||
// This allows us to effectively determine how much space we need to allocate for the images.
|
||||
var currentHeight = 0;
|
||||
var currentAtlasIndex = 0;
|
||||
foreach (var rsi in atlasList)
|
||||
{
|
||||
var rsi = atlasList[i];
|
||||
if (rsi.Bad)
|
||||
var insertHeight = rsi.AtlasSheet.Height;
|
||||
var insertWidth = rsi.AtlasSheet.Width;
|
||||
|
||||
var found = false;
|
||||
for (var i = 0; i < levels.Count && !found; i++)
|
||||
{
|
||||
var levelPosition = levels[i].Position;
|
||||
var levelWidth = levels[i].Width;
|
||||
var levelHeight = levels[i].Height;
|
||||
|
||||
// Check if it can fit in this level.
|
||||
if (levelHeight < insertHeight || levelWidth + insertWidth > levels[i].MaxWidth)
|
||||
continue;
|
||||
|
||||
found = true;
|
||||
|
||||
levels[i].Width += insertWidth;
|
||||
rsi.AtlasOffset = levelPosition + new Vector2i(levelWidth, 0);
|
||||
levels[i].RSIList.Add(rsi);
|
||||
|
||||
// Creating the extra "free" space above blocks that can be used for inserting more items.
|
||||
// This differs from the FFDH spec which just ignores this space.
|
||||
Debug.Assert(levelHeight >= insertHeight); // Must be true because the array needs to be sorted
|
||||
if (levelHeight - insertHeight == 0)
|
||||
continue;
|
||||
|
||||
var freeLevel = new Level
|
||||
{
|
||||
AtlasId = levels[i].AtlasId,
|
||||
Position = levelPosition + new Vector2i(levelWidth, insertHeight),
|
||||
Height = levelHeight - insertHeight,
|
||||
Width = 0,
|
||||
MaxWidth = insertWidth,
|
||||
RSIList = [ ]
|
||||
};
|
||||
|
||||
levels.Add(freeLevel);
|
||||
}
|
||||
|
||||
if (found)
|
||||
continue;
|
||||
|
||||
DebugTools.Assert(rsi.AtlasSheet.Width < sheet.Width);
|
||||
DebugTools.Assert(rsi.AtlasSheet.Height < sheet.Height);
|
||||
|
||||
if (offset.X + rsi.AtlasSheet.Width > sheet.Width)
|
||||
// Ran out of space, we need to move on to the next atlas.
|
||||
// This also isn't in the normal FFDH algorithm (obviously) but its close enough.
|
||||
if (currentHeight + insertHeight > maxSize)
|
||||
{
|
||||
offset.X = 0;
|
||||
offset.Y += deltaY;
|
||||
imageAtlases.Add(new Image<Rgba32>(maxSize, currentHeight));
|
||||
finalPixels.Add(0);
|
||||
currentHeight = 0;
|
||||
currentAtlasIndex++;
|
||||
}
|
||||
|
||||
if (offset.Y + rsi.AtlasSheet.Height > sheet.Height)
|
||||
{
|
||||
FinalizeMetaAtlas(i-1, sheet);
|
||||
sheet = new Image<Rgba32>(maxSize, maxSize);
|
||||
deltaY = 0;
|
||||
offset = default;
|
||||
}
|
||||
rsi.AtlasOffset = new Vector2i(0, currentHeight);
|
||||
|
||||
deltaY = Math.Max(deltaY, rsi.AtlasSheet.Height);
|
||||
var box = new UIBox2i(0, 0, rsi.AtlasSheet.Width, rsi.AtlasSheet.Height);
|
||||
rsi.AtlasSheet.Blit(box, sheet, offset);
|
||||
rsi.AtlasOffset = offset;
|
||||
offset.X += rsi.AtlasSheet.Width;
|
||||
var newLevel = new Level
|
||||
{
|
||||
AtlasId = currentAtlasIndex,
|
||||
Position = new Vector2i(0, currentHeight),
|
||||
Height = insertHeight,
|
||||
Width = insertWidth,
|
||||
MaxWidth = maxSize,
|
||||
RSIList = [ rsi ]
|
||||
};
|
||||
levels.Add(newLevel);
|
||||
|
||||
currentHeight += insertHeight;
|
||||
}
|
||||
|
||||
var height = offset.Y + deltaY;
|
||||
var croppedSheet = new Image<Rgba32>(maxSize, height);
|
||||
sheet.Blit(new UIBox2i(0, 0, maxSize, height), croppedSheet, default);
|
||||
FinalizeMetaAtlas(atlasList.Length - 1, croppedSheet);
|
||||
// This allocation takes a long time.
|
||||
imageAtlases.Add(new Image<Rgba32>(maxSize, currentHeight));
|
||||
finalPixels.Add(0);
|
||||
|
||||
void FinalizeMetaAtlas(int toIndex, Image<Rgba32> sheet)
|
||||
// Put all textures on the atlases
|
||||
foreach (var level in levels)
|
||||
{
|
||||
var fromIndex = finalized + 1;
|
||||
var atlas = Clyde.LoadTextureFromImage(sheet, $"Meta atlas {fromIndex}-{toIndex}");
|
||||
for (int i = fromIndex; i <= toIndex; i++)
|
||||
foreach (var rsi in level.RSIList)
|
||||
{
|
||||
var rsi = atlasList[i];
|
||||
rsi.AtlasTexture = atlas;
|
||||
}
|
||||
var box = new UIBox2i(0, 0, rsi.AtlasSheet.Width, rsi.AtlasSheet.Height);
|
||||
|
||||
finalized = toIndex;
|
||||
atlasCount++;
|
||||
rsi.AtlasSheet.Blit(box, imageAtlases[level.AtlasId], rsi.AtlasOffset);
|
||||
finalPixels[level.AtlasId] += rsi.AtlasSheet.Width * rsi.AtlasSheet.Height;
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize the atlases.
|
||||
for (var i = 0; i < imageAtlases.Count; i++)
|
||||
{
|
||||
var atlasTexture = Clyde.LoadTextureFromImage(imageAtlases[i], $"Meta atlas {i}");
|
||||
finalAtlases.Add(atlasTexture);
|
||||
|
||||
sawmill.Debug($"(Meta atlas {i}) - cropped utilization: {(float)finalPixels[i] / (maxSize * imageAtlases[i].Height):P2}, fill percentage: {(float)imageAtlases[i].Height / maxSize:P2}");
|
||||
}
|
||||
|
||||
// Finally, reference the actual atlas from the RSIs.
|
||||
foreach (var level in levels)
|
||||
{
|
||||
foreach (var rsi in level.RSIList)
|
||||
{
|
||||
rsi.AtlasTexture = finalAtlases[level.AtlasId];
|
||||
}
|
||||
}
|
||||
|
||||
Parallel.ForEach(rsiList, data =>
|
||||
@@ -279,7 +374,7 @@ namespace Robust.Client.ResourceManagement
|
||||
sawmill.Debug(
|
||||
"Preloaded {CountLoaded} RSIs into {CountAtlas} Atlas(es?) ({CountNotAtlas} not atlassed, {CountErrored} errored) in {LoadTime}",
|
||||
rsiList.Length,
|
||||
atlasCount,
|
||||
finalAtlases.Count,
|
||||
nonAtlasList.Length,
|
||||
errors,
|
||||
sw.Elapsed);
|
||||
@@ -290,4 +385,38 @@ namespace Robust.Client.ResourceManagement
|
||||
return rsi.MetaAtlas && rsi.LoadParameters == TextureLoadParameters.Default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A "Level" to place boxes. Similar to FFDH levels, but with more parameters so we can fit in "free" levels
|
||||
/// above placed boxes.
|
||||
/// </summary>
|
||||
internal sealed class Level
|
||||
{
|
||||
/// <summary>
|
||||
/// Index of the atlas this is located.
|
||||
/// </summary>
|
||||
public required int AtlasId;
|
||||
/// <summary>
|
||||
/// Bottom left of the location for the RSIs.
|
||||
/// </summary>
|
||||
public required Vector2i Position;
|
||||
/// <summary>
|
||||
/// The current width of the level.
|
||||
/// </summary>
|
||||
/// <remarks>This can (and will) be 0. Will change.</remarks>
|
||||
public required int Width;
|
||||
/// <summary>
|
||||
/// The current height of the level.
|
||||
/// </summary>
|
||||
/// <remarks>This value should never change.</remarks>
|
||||
public required int Height;
|
||||
/// <summary>
|
||||
/// Maximum width of the level.
|
||||
/// </summary>
|
||||
public required int MaxWidth;
|
||||
/// <summary>
|
||||
/// List of all the RSIs stored in this level. RSIs are ordered from tallest to smallest per level.
|
||||
/// </summary>
|
||||
public required List<RSIResource.LoadStepData> RSIList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -11,6 +11,13 @@ namespace Robust.Client.ResourceManagement;
|
||||
|
||||
public sealed class AudioResource : BaseResource
|
||||
{
|
||||
// from: https://en.wikipedia.org/wiki/List_of_file_signatures
|
||||
private static readonly byte[] OggSignature = "OggS"u8.ToArray();
|
||||
private static readonly byte[] RiffSignature = "RIFF"u8.ToArray();
|
||||
private const int WavSignatureStart = 8; // RIFF????
|
||||
private static readonly byte[] WavSignature = "WAVE"u8.ToArray();
|
||||
private const int MaxSignatureLength = 12; // RIFF????WAVE
|
||||
|
||||
public AudioStream AudioStream { get; private set; } = default!;
|
||||
|
||||
public void Load(AudioStream stream)
|
||||
@@ -28,14 +35,19 @@ public sealed class AudioResource : BaseResource
|
||||
}
|
||||
|
||||
using var fileStream = cache.ContentFileRead(path);
|
||||
var seekableStream = fileStream.CanSeek ? fileStream : fileStream.CopyToMemoryStream();
|
||||
byte[] signature = seekableStream.ReadExact(MaxSignatureLength);
|
||||
seekableStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
var audioManager = dependencies.Resolve<IAudioInternal>();
|
||||
if (path.Extension == "ogg")
|
||||
if (signature[..OggSignature.Length].SequenceEqual(OggSignature))
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
|
||||
AudioStream = audioManager.LoadAudioOggVorbis(seekableStream, path.ToString());
|
||||
}
|
||||
else if (path.Extension == "wav")
|
||||
else if (signature[..RiffSignature.Length].SequenceEqual(RiffSignature)
|
||||
&& signature[WavSignatureStart..MaxSignatureLength].SequenceEqual(WavSignature))
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
|
||||
AudioStream = audioManager.LoadAudioWav(seekableStream, path.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -32,18 +32,22 @@ namespace Robust.Client.ResourceManagement
|
||||
|
||||
var data = new LoadStepData {Path = path};
|
||||
|
||||
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadTextureParameters(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadPreTextureData(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadTexture(dependencies.Resolve<IClyde>(), data);
|
||||
LoadFinish(dependencies.Resolve<IResourceCache>(), data);
|
||||
}
|
||||
|
||||
internal static void LoadPreTexture(IResourceManager cache, LoadStepData data)
|
||||
internal static void LoadPreTextureData(IResourceManager cache, LoadStepData data)
|
||||
{
|
||||
using (var stream = cache.ContentFileRead(data.Path))
|
||||
{
|
||||
data.Image = Image.Load<Rgba32>(stream);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void LoadTextureParameters(IResourceManager cache, LoadStepData data)
|
||||
{
|
||||
data.LoadParameters = TryLoadTextureParameters(cache, data.Path) ?? TextureLoadParameters.Default;
|
||||
}
|
||||
|
||||
@@ -95,7 +99,8 @@ namespace Robust.Client.ResourceManagement
|
||||
{
|
||||
var data = new LoadStepData {Path = path};
|
||||
|
||||
LoadPreTexture(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadTextureParameters(dependencies.Resolve<IResourceManager>(), data);
|
||||
LoadPreTextureData(dependencies.Resolve<IResourceManager>(), data);
|
||||
|
||||
if (data.Image.Width == Texture.Width && data.Image.Height == Texture.Height)
|
||||
{
|
||||
@@ -119,6 +124,7 @@ namespace Robust.Client.ResourceManagement
|
||||
public Image<Rgba32> Image = default!;
|
||||
public TextureLoadParameters LoadParameters;
|
||||
public OwnedTexture Texture = default!;
|
||||
public bool Skip;
|
||||
public bool Bad;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class LoadPrototypeCommand : IConsoleCommand
|
||||
var dialogManager = IoCManager.Resolve<IFileDialogManager>();
|
||||
var loadManager = IoCManager.Resolve<IGamePrototypeLoadManager>();
|
||||
|
||||
var stream = await dialogManager.OpenFile();
|
||||
var stream = await dialogManager.OpenFile(access: FileAccess.Read);
|
||||
if (stream is null)
|
||||
return;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.IO;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -36,7 +37,7 @@ public sealed class UploadFileCommand : IConsoleCommand
|
||||
var path = new ResPath(args[0]).ToRelativePath();
|
||||
|
||||
var filters = new FileDialogFilters(new FileDialogFilters.Group(path.Extension));
|
||||
await using var file = await _dialog.OpenFile(filters);
|
||||
await using var file = await _dialog.OpenFile(filters, FileAccess.Read);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
|
||||
@@ -994,6 +994,9 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
internal int DoFrameUpdateRecursive(FrameEventArgs args)
|
||||
{
|
||||
if (!Visible)
|
||||
return 0;
|
||||
|
||||
var total = 1;
|
||||
FrameUpdate(args);
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.Placement.Modes;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
@@ -28,19 +30,23 @@ public sealed class TileSpawningUIController : UIController
|
||||
private readonly List<ITileDefinition> _shownTiles = new();
|
||||
private bool _clearingTileSelections;
|
||||
private bool _eraseTile;
|
||||
private bool _mirrorableTile; // Tracks if the chosen tile even can be mirrored.
|
||||
private bool _mirroredTile;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
DebugTools.Assert(_init == false);
|
||||
_init = true;
|
||||
_placement.PlacementChanged += ClearTileSelection;
|
||||
_placement.DirectionChanged += OnDirectionChanged;
|
||||
_placement.MirroredChanged += OnMirroredChanged;
|
||||
}
|
||||
|
||||
private void StartTilePlacement(int tileType)
|
||||
{
|
||||
var newObjInfo = new PlacementInformation
|
||||
{
|
||||
PlacementOption = "AlignTileAny",
|
||||
PlacementOption = nameof(AlignTileAny),
|
||||
TileType = tileType,
|
||||
Range = 400,
|
||||
IsTile = true
|
||||
@@ -67,6 +73,17 @@ public sealed class TileSpawningUIController : UIController
|
||||
args.Button.Pressed = args.Pressed;
|
||||
}
|
||||
|
||||
private void OnTileMirroredToggled(ButtonToggledEventArgs args)
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
_placement.Mirrored = args.Pressed;
|
||||
_mirroredTile = _placement.Mirrored;
|
||||
|
||||
args.Button.Pressed = args.Pressed;
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
EnsureWindow();
|
||||
@@ -78,6 +95,9 @@ public sealed class TileSpawningUIController : UIController
|
||||
else
|
||||
{
|
||||
_window.Open();
|
||||
UpdateEntityDirectionLabel();
|
||||
UpdateMirroredButton();
|
||||
_window.SearchBar.GrabKeyboardFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +113,10 @@ public sealed class TileSpawningUIController : UIController
|
||||
_window.TileList.OnItemDeselected += OnTileItemDeselected;
|
||||
_window.EraseButton.Pressed = _eraseTile;
|
||||
_window.EraseButton.OnToggled += OnTileEraseToggled;
|
||||
_window.MirroredButton.Disabled = !_mirrorableTile;
|
||||
_window.RotationLabel.FontColorOverride = _mirrorableTile ? Color.White : Color.Gray;
|
||||
_window.MirroredButton.Pressed = _mirroredTile;
|
||||
_window.MirroredButton.OnToggled += OnTileMirroredToggled;
|
||||
BuildTileList();
|
||||
}
|
||||
|
||||
@@ -110,6 +134,7 @@ public sealed class TileSpawningUIController : UIController
|
||||
_window.TileList.ClearSelected();
|
||||
_clearingTileSelections = false;
|
||||
_window.EraseButton.Pressed = false;
|
||||
_window.MirroredButton.Pressed = _placement.Mirrored;
|
||||
}
|
||||
|
||||
private void OnTileClearPressed(ButtonEventArgs args)
|
||||
@@ -137,6 +162,7 @@ public sealed class TileSpawningUIController : UIController
|
||||
{
|
||||
var definition = _shownTiles[args.ItemIndex];
|
||||
StartTilePlacement(definition.TileId);
|
||||
UpdateMirroredButton();
|
||||
}
|
||||
|
||||
private void OnTileItemDeselected(ItemList.ItemListDeselectedEventArgs args)
|
||||
@@ -149,6 +175,41 @@ public sealed class TileSpawningUIController : UIController
|
||||
_placement.Clear();
|
||||
}
|
||||
|
||||
private void OnDirectionChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateEntityDirectionLabel();
|
||||
}
|
||||
|
||||
private void UpdateEntityDirectionLabel()
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
_window.RotationLabel.Text = _placement.Direction.ToString();
|
||||
}
|
||||
|
||||
private void OnMirroredChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateMirroredButton();
|
||||
}
|
||||
|
||||
private void UpdateMirroredButton()
|
||||
{
|
||||
if (_window == null || _window.Disposed)
|
||||
return;
|
||||
|
||||
if (_placement.CurrentPermission != null && _placement.CurrentPermission.IsTile)
|
||||
{
|
||||
var allowed = _tiles[_placement.CurrentPermission.TileType].AllowRotationMirror;
|
||||
_mirrorableTile = allowed;
|
||||
_window.MirroredButton.Disabled = !_mirrorableTile;
|
||||
_window.RotationLabel.FontColorOverride = _mirrorableTile ? Color.White : Color.Gray;
|
||||
}
|
||||
|
||||
_mirroredTile = _placement.Mirrored;
|
||||
_window.MirroredButton.Pressed = _mirroredTile;
|
||||
}
|
||||
|
||||
private void BuildTileList(string? searchStr = null)
|
||||
{
|
||||
if (_window == null || _window.Disposed) return;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user