Compare commits

..

106 Commits

Author SHA1 Message Date
PJB3005
f0a4446fb6 Version: 262.0.2 2025-09-19 09:17:26 +02:00
Skye
64307228cc Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:26 +02:00
PJB3005
702c616dfe Version: 262.0.1 2025-09-14 14:55:50 +02:00
PJB3005
7d493f3f7e Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

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

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

    Move CEF cache out of data directory

    Don't want content messing with this...

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

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

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

    Update SpaceWizards.NFluidSynth to 0.2.2

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

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
2025-09-14 14:55:50 +02:00
metalgearsloth
18849be0b4 Version: 262.0.0 2025-06-09 23:56:58 +10:00
Leon Friedrich
c6a1d82bb1 Validate that Toolshed command arguments have parsers (#6014)
* Add Nullable<T> support to ToolshedManager.TryParse

* Check that command arguments are parseable

* release notes

* a

* A is for Array

* Fix test

* Fix indentation
2025-06-09 23:47:29 +10:00
Leon Friedrich
d89e1a43c6 Add PvsResetTest (#6015) 2025-06-09 23:39:06 +10:00
Leon Friedrich
d894ef70ef Misc SpriteSystem fixes (#6001)
* Move GetPrototypeTextures to SpriteSystem

* Fix tests

* Fix #6002

* release notes

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-06-09 18:59:37 +10:00
DrSmugleaf
c7ea2793ca Fix TransformComponent state handling changing the coordinates of detached entities (#6006)
* Fix TransformComponent state handling changing the coordinates of detached entities

* Make ResetPredictedEntities not handle state for detached entities
2025-06-09 17:44:36 +10:00
Perry Fraser
0c61ff2bee fix: default audio params for PlayStatic (#6011) 2025-06-09 00:28:12 +02:00
Tayrtahn
343a34eac7 Fix warning CS0168 in EntityDeserializer (#6013)
* Fix warning CS0168 in EntityDeserializer

* Actually, why use try-catch just to rethrow anyway?
2025-06-09 00:04:03 +02:00
metalgearsloth
7be41f4890 Add ignoredcomps to IsDefault (#5998) 2025-06-05 23:54:47 +10:00
metalgearsloth
293470a5fe Fix incorrect saved window positions (#5927)
* Fix incorrect saved window positions

As of however many UI PRs ago windows store their last position on the client and it re-opens windows at that position.

The issue is that the code to avoid windows being able to go off-screen was immediately bulldozing this value, at least if the x <= 0. Now we just don't run it until we have a valid measure (probably the frame after) and avoid unnecessarily having an incorrect position applied.

* Explainer
2025-06-05 23:35:28 +10:00
metalgearsloth
2b8057acf0 Version: 261.2.0 2025-06-05 22:55:09 +10:00
Tayrtahn
bec3caa5da Fix error when using tpto on a grid (#5991)
* Fix error when using tpto on a grid

* Calculate map coords outside of loop
2025-06-05 22:45:30 +10:00
metalgearsloth
ea6126563b Add NearestChunkEnumerator (#5972)
Not super fast but want it for biome loading to prio chunks nearby first.
2025-06-05 22:36:22 +10:00
slarticodefast
00494ad9eb fix TryQueueDelete (#5996) 2025-06-05 22:34:50 +10:00
Tayrtahn
6672b7b1bd Correct misleading error message in ShareMapSystem.OnParentChange (#5992) 2025-06-05 22:29:47 +10:00
ruddygreat
8dc55e8748 fix the lifestage checks on predicted entity deletion (#5993)
Co-authored-by: Ruddygreat <ruddygreat1@gmail.com>
2025-06-05 22:29:22 +10:00
Tayrtahn
44ea2cd396 Implement IEquatable for ResolvedPathSpecifier and ResolvedCollectionSpecifier (#5980) 2025-06-01 18:10:55 +10:00
metalgearsloth
2c5604432b Update some GetComponentName usages (#5942)
Rider tells me to use generic and generic one seems better.
2025-06-01 17:54:43 +10:00
Tayrtahn
c696466522 Remove ITileDefinition.ID (#5982) 2025-06-01 17:51:59 +10:00
slarticodefast
01bb98e400 fix static grid center of mass (#5985) 2025-05-29 23:05:54 +10:00
metalgearsloth
af08e747de Defer grid state handling TileChangedEvent (#5981)
Rather than doing the old raise-event-per-tile we just raise it at the end.
2025-05-29 09:21:45 +10:00
metalgearsloth
8c35c2c380 Version: 261.1.0 2025-05-29 00:15:26 +10:00
Tayrtahn
6d46d3f4a5 Cleanup most warnings in unit tests (#5946)
* 2 warnings in JointDeletion_Test

* 1 warning in Collision_Test

* 2 warnings in Color_Test (deleted test of deprecated HCY color space)

* 1 warning in MapVelocity_Test

* 2 warnings in MapManager_Tests

* 2 warnings in MapPauseTests

* 1 warning in NetDisconnectMessageTest

* 1 warning in ContainerTests

* Suppress 1 warning in EntityEventBusTests.ComponentEvent

* 4 warnings in MapGridMap_Tests

* 1 warning in GridDeletion_Test

* Remove TryGetContainingContainer foolishness

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-05-29 00:12:58 +10:00
Tayrtahn
50e06e43fa Automatic Sawmill generation for UIControllers (#5967) 2025-05-29 00:06:55 +10:00
metalgearsloth
986b0f979d Fix physics forces not autoclearing (#5978)
* Fix physics forces not autoclearing

* Changes
2025-05-29 00:04:55 +10:00
metalgearsloth
a51d786dee Version: 261.0.0 2025-05-28 19:34:47 +10:00
metalgearsloth
5f5fed5d6c Update contact xform usage (#5977)
Forgot this one as well.
2025-05-28 19:30:50 +10:00
Tayrtahn
e475cc7898 Cleanup warning in SpriteBoundsOverlay (#5944)
* Cleanup warning in SpriteBoundsOverlay

* Make better use of the primary constructor
2025-05-28 19:28:53 +10:00
metalgearsloth
ee8ea4ec3b Purge PhysicsMapComponent (#5766)
* Replace PhysicsMapComponent

- Dumb idea
- Lots of book-keeping and perf overhead.
- Much saner this way.

* stuff

* More work

* Purge

* Fixes

* Eh?

* Fixes

* Also this

* weh

* Fixes

* ice-cream

* Fix

* Fix stacking / gravity

* Gravity query

* MoveBuffer optimisations

* Fixes for test

* World gravity

* Fix build

* Avoid some transform resolves for contactless ents

* Less getcomps

* Fix contact caching

* Possibly less copies

* reh

* bulldoze

* Test "fix"

* seikrets

* a

* I saw this but now I decideded against it

* true
2025-05-28 19:18:36 +10:00
slarticodefast
7482451ec4 optimize ToMapCoordinates (#5953) 2025-05-28 12:07:23 +10:00
metalgearsloth
dddf5cd2fb Add entities to SpawnEntitiesAttachedTo (#5971)
* Add entities to SpawnEntitiesAttachedTo

Need it for biome stuff.

* factorio
2025-05-27 19:45:48 +10:00
metalgearsloth
01979c451d Make RaiseMoveEvent internal (#5918)
I don't think content should really be calling this tbh.
2025-05-27 19:45:04 +10:00
slarticodefast
181a5ef0b4 fix GetMapLinearVelocity (#5950)
* fix GetMapLinearVelocity

* resolve and adjust other methods
2025-05-27 19:41:43 +10:00
Princess Cheeseballs
e7c7011cc0 Init Commit (#5909) 2025-05-27 19:34:32 +10:00
metalgearsloth
dc97615fd4 Add some Box2i methods (#5969)
Equivalent to Box2.
2025-05-27 19:26:50 +10:00
metalgearsloth
3b4944376b Fix FastNoiseLite fractal bounding (#5970)
This shouldn't be datafielded because it gets set by other datafields.
2025-05-27 19:14:49 +10:00
Tayrtahn
fa6bd8f7ba Cleanup TypeSerializer Logger warnings (#5966) 2025-05-24 20:23:28 +02:00
Whatstone
2398cbcf26 GameController: init LocMgr before init broadcast (#5965) 2025-05-24 19:02:26 +02:00
Tayrtahn
38ce48a83f Cleanup 3 warnings in SharedContainerSystem (#5949)
* Cleanup 3 warnings in SharedContainerSystem

* Don't call Transform twice
2025-05-24 19:00:57 +02:00
PJB3005
4e7de2f272 File dialog fixes and improvements
File dialog requests can now specify the share and access mode they want out of the opened file. This means read-only access is now possible.

While doing this I noticed that the SDL3 backend had a memory leak *and* didn't match the behavior of the other backends. Cleaned up the code to avoid that.

In-engine commands that *can* specify read-only access on file open now do.
2025-05-24 16:38:01 +02:00
Cami
b61075c660 Stopped recursive updates for controls that are not visible, as it vi… (#5960)
* Stopped recursive updates for controls that are not visible, as it violates framerate in large menus.

* Update Robust.Client/UserInterface/Control.cs

---------

Co-authored-by: Cam <Nop>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-05-23 17:42:47 +02:00
Tayrtahn
7b571dc80e Fix 2 instances of warning CS0162 (#5951) 2025-05-23 17:37:08 +02:00
TemporalOroboros
f1c76ca899 Remove unused obsolete TryGetContainingContainer override (#5660)
* Remove unused obsolete TryGetContainingContainer override

* poke tests

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-05-23 17:36:38 +02:00
metalgearsloth
84dcd658aa Version: 260.2.0 2025-05-21 23:30:58 +10:00
metalgearsloth
a634d6bd04 Add WorldNormal to StartCollideEvent (#5954)
We already have the value just a matter of adding it to the event.
2025-05-21 20:41:57 +10:00
DrSmugleaf
36f9df3079 Add System.Text.StringBuilder Insert(int, string) to sandbox.yml (#5955) 2025-05-21 11:20:10 +02:00
keronshb
824c018a69 Version: 260.1.0 2025-05-19 13:11:32 -04:00
Tayrtahn
4b6b688c72 Cleanup warnings in PlacementManager (#5939) 2025-05-18 19:14:16 +10:00
Tayrtahn
71df25b251 Cleanup warning in Clyde.Sprite (#5940) 2025-05-18 18:51:27 +10:00
metalgearsloth
be14a3c249 Expose CompFactory to systems (#5941) 2025-05-18 00:56:09 -04:00
metalgearsloth
3c2a4d5c79 Version: 260.0.0 2025-05-18 03:07:24 +10:00
metalgearsloth
44180b3ee0 Fix / remove startcollidevent worldpoint (#5936)
Now it's worldpoints because it may not necessarily be 1 pointr and internally we fix the actual points themselves.
2025-05-18 03:03:12 +10:00
metalgearsloth
bb0e77e937 Add some EntProtoId overloads (#5938)
Need it for some content stuff didn't feel like doing the rest yet.
2025-05-17 18:28:12 +10:00
ArtisticRoomba
684b9bc852 Add new Vertical property to progress bars (#5932) 2025-05-17 18:27:44 +10:00
Tayrtahn
9f3db6693e Add SpriteSystem dependency to VisualizerSystem (#5935)
* Add protected SpriteSystem reference to VisualizerSystem

* Capital S
2025-05-17 13:26:40 +10:00
metalgearsloth
40d869948d Version: 259.0.0 2025-05-15 20:26:10 +10:00
Tayrtahn
5c97b15849 Mark Entity methods as readonly (#5919)
* Mark Entity methods as readonly

* Add to GenericEntityPrint

* No but really
2025-05-15 20:23:29 +10:00
Tayrtahn
3d8a9a41fa Combine TileChangedEvents in SetTiles (#5912)
* Combine TileChangedEvents in SetTiles

* Raise event after regenerating collision

* continue, not return

* No need for GetComponent

* Swap TileRef for Tile + Vector2i

* Estimate size of tileChanges
2025-05-15 20:22:05 +10:00
metalgearsloth
92fc8722da Version: 258.0.1 2025-05-15 19:28:25 +10:00
metalgearsloth
73f6555624 Fix static ent collision spawn (#5933)
* Fix static ent collision spawn

* Fix test

* cool
2025-05-15 19:11:20 +10:00
metalgearsloth
2ac7bc3ce4 Version: 258.0.0 2025-05-15 00:51:12 +10:00
Leon Friedrich
05cb4bb1c9 Make SpriteSystem.LayerMapReserve not throw (#5930)
* Make SpriteSystem.LayerMapReserve not throw

* fix SpriteComponent.Visible

* remove region
2025-05-14 23:23:51 +10:00
Leon Friedrich
a393efc87a Modify markup tag interfaces and fix some bugs (#5442)
* Modify markup tag interfaces

* Why are nullable structs like this.

* AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

* Avoid breaking changes

* Replace IMarkupTag with IMarkupTagHandler in engine

* Its a breaking change now I guess

* cleanup
2025-05-12 14:09:18 +10:00
Leon Friedrich
4d47cfa1a6 Minor respath improvements (#5876)
* Minor respath improvements

* Add helpers

* tweak helper

* Throw on more than 1 char

* comments

* No emoji separators
2025-05-12 13:04:40 +10:00
DrSmugleaf
2b1d755d9f Fix Container state handling not forcing inserts (#5916) 2025-05-11 22:45:31 +10:00
ElectroJr
db7de0a99f Version: 257.0.2 2025-05-11 23:47:14 +12:00
Leon Friedrich
47f18703af Fix unshaded sprite layers (#5924)
* Fix unshaded sprite layers

* update comment
2025-05-11 21:42:40 +10:00
Leon Friedrich
97c1548301 Add SpriteBoundsTest (#5922) 2025-05-11 15:30:14 +10:00
ElectroJr
cd97f1583f Version: 257.0.1 2025-05-11 13:56:27 +12:00
Leon Friedrich
5fbe25ec9d Fix sprite layer bounds (#5920) 2025-05-11 11:52:20 +10:00
metalgearsloth
516ee47b51 Version: 257.0.0 2025-05-10 22:12:35 +10:00
metalgearsloth
89be682e24 Don't raise wake events for terminating contacts (#5757) 2025-05-10 22:02:22 +10:00
metalgearsloth
6086076559 Avoid checking grid traversal for rotation events (#5778)
* Avoid checking grid traversal for rotation events

* Also this one
2025-05-10 22:01:43 +10:00
beck-thompson
5bd90c908a Optimization RSI preloading / atlas creation (#5817)
* First commit

* Fix multiatlas bug

* Use ValueList instead

* Add FFDH sorting for atlases

* Minor cleanup and value lists
2025-05-10 22:00:03 +10:00
Leon Friedrich
a3d0921cc9 Pause entities that leave PVS range (#5878)
* Pause entities that leave PVS range

* Fix merge

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-05-10 21:57:04 +10:00
Leon Friedrich
15d5b9aa02 Move parts of SpriteComponent to SpriteSystem (#5602)
* Partial sprite component ECS

* release notes

* tests

* Why

* SetSnapCardinals

* NoRotation

* DirectionOverride

* This is why I love distinct overrides that take in object

* LayerSetData

* ISerializationHooks continue to haunt me

* Relocate SetShader

* LayerSetSprite

* LayerSetTexture

* yipeeeee

* LayerSetRsi

* Remove GetFallbackState

* LayerSet Scale,Rotation,Color,Visible

* Fix LayerSetRsi

* LayerSetOffset

* LayerSetDirOffset

* Add overrides that take in a Layer

* LayerSetAnimationTime

* LayerSetRenderingStrategy

* Reduce Resolves, Add Layer.Index

* Access

* Try fix NREs

* Asserts

* LayerGetState

* Cleanup

* Merge helper partial classes

* partial rendering

* GetLayerDirectionCount

* Cache local bounds

* RenderLayer

* RefreshCachedState

* RoundToCardinalAngle

* Fix the pr

* Fix debug assert

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-05-10 21:56:53 +10:00
Leon Friedrich
d24854d94f Improve yaml validation errors for ignored prototypes (#5886)
* Improve yaml validation errors for ignored prototypes

* release notes

* Comments
2025-05-10 21:37:42 +10:00
Tayrtahn
b3cf427013 Catch NotYamlSerializable DataFields with analyzer (#5704)
* Catch NotYamlSerializable DataFields with analyzer

* Extract common defs into shared source
2025-05-10 21:36:33 +10:00
Leon Friedrich
c458abdc69 Move TestPair & PoolManager to engine (#5877)
* Engine pool manager

* Move documentation

* Move namespace

* Move TestMapData to engine

* Option to prevent loading test assembly

* release notes

* Rename to avoid conflicts
2025-05-10 21:35:28 +10:00
metalgearsloth
c76444a33f Version: 256.0.0 2025-05-10 13:40:38 +10:00
B_Kirill
4754661467 Cleanup warnings: CS0649 (#5891)
* Clean up

* Remove "struct UpdateTreesJob"

* Use #pragma

* Use #if DEBUG

* More #if DEBUG
2025-05-10 12:40:15 +10:00
B_Kirill
2a8b776ee9 Cleanup warnings: CS0414 (#5892)
* Clean up

* Use #pragma
2025-05-10 12:39:46 +10:00
SpaceManiac
7d8e5a5841 Fix linear lookup on a dictionary in PlacementManager (#5911) 2025-05-06 16:05:58 +02:00
Centronias
8e416e4519 Makes ItemList not run deselection callback on all list items (#5861)
* Makes ItemList not run deselection callback on all list items

even when they weren't selected

* I cannot be expected to do things intelligently at 2a

* Optimize local usage.

* I'm pretty sure the test failure isn't from ItemList

* switch to avoiding doing anything in the `Selected` setter if the value-to-set-to is the same as the current value.
2025-05-06 14:05:05 +02:00
SlamBamActionman
65f74943d3 Add support for rotated/mirrored tiles (#5652)
* Initial commit

* Add tile rotation/mirror perms

* Nicer UI for the rotation

* Review fixes (also seemed to have missed applying the serialization reading oops)

* One less byte, one less struct size!

* Pretty sure it goes here too

* Fix error
2025-05-05 23:13:31 +10:00
Errant
eb5ed12270 Serialize TimeSpan from text (#5865)
* timespanserializer cleanup

* string reading

* high speed, low drag

* unit tests

* Update Robust.Shared/Serialization/TypeSerializers/Implementations/TimespanSerializer.cs

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-05-05 00:57:01 +02:00
PJB3005
c43b7b16c0 Add CancellationTokenRegistration to sandbox 2025-05-05 00:45:05 +02:00
slarticodefast
aee03f0805 fix yaml hotreloading (#5907) 2025-05-04 04:33:37 +02:00
dffdff2423
cfd2b03248 Check audio file signatures instead of extensions (#5894)
* Check audio file signatures instead of extensions

Fixes #5789

Test audio files based on their magic bytes.

* Test for a seekable stream

* Remove Take and Skip linq
2025-05-04 04:33:01 +02:00
dffdff2423
8905a3fe14 Add documentation to the serializer interfaces and remove ITypeReaderWriter (#5897)
* Add documentation to the serializer interfaces

* Remove ITypeReaderWriter and fix the docs

* Fix spelling errors and incorrect docstrings
2025-05-04 04:03:17 +02:00
PJB3005
a878da5b80 Allow texture preload to be skipped for some textures
This is a far cry from a proper resource tracking system, but it's something to avoid a ton of otherwise-unused parallax textures being loaded at game start and consuming VRAM.
2025-05-04 02:24:57 +02:00
Leon Friedrich
806c23e034 Move EntityExt.AsNullable extension methods into the Entity struct (#5899)
* Move `EntityExt.AsNullable` extension methods into the Entity struct

* use constructor

* a
2025-05-04 01:24:23 +02:00
metalgearsloth
e80f5d13a1 Add Vector2i / bitmask conversions (#5901)
Content uses for a couple tile-based flags.
2025-05-04 01:23:04 +02:00
PJB3005
a6905151b6 Move PointLight component states to shared
Necessary so the client can calculate an initial state, which is necessary for prediction and replay seeking to work properly.

Fixes the nuke in SS14 not having its light turn off when going back in a replay.
2025-05-02 01:21:17 +02:00
PJB3005
e742f021fa Dev window tab to show all loaded textures 2025-04-30 15:50:39 +02:00
metalgearsloth
62b4714f1f Version: 255.1.0 2025-04-30 23:38:21 +10:00
Leon Friedrich
1d0404953f Add GridUidChangedEvent and MapUidChangedEvent (#5893)
* Add GridUidChangedEvent and MapUidChangedEvent

* cleanup

* Fix assert

* more fixes

* docs

* record struct

* Use implicit tuple constructor

* stinky review

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-04-30 18:36:53 +10:00
Leon Friedrich
d0da13f895 Fix CompileRobustXamlTask for benchmarks (#5902)
* Fix benchmarks

* I love it when path separators are also escape chars
2025-04-30 13:16:28 +10:00
metalgearsloth
ff23f98b26 Document container events (#5904)
It's hard to discern what it's raised directed on and this makes it much easier.
2025-04-30 13:10:03 +10:00
Leon Friedrich
ccfef2a786 Fix PVS NRE (#5900) 2025-04-28 20:11:18 +10:00
B_Kirill
62ce9724fc Clean up (#5890) 2025-04-26 22:59:02 +10:00
Milon
3bbe0e7f44 ftl hot reloading (#5874)
* sloth is so going to kill me

* the voices in my head told me to do this

* Register ILocalizationManagerInternal on client

* Avoid breaking change

* Cleanup

* Release notes
2025-04-26 22:38:56 +10:00
chromiumboy
addd8b5bdd Initial commit (#5880) 2025-04-25 22:19:11 +10:00
236 changed files with 8022 additions and 3188 deletions

View File

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

View File

@@ -71,6 +71,6 @@
</PropertyGroup>
<Exec
Condition="'$(_RobustUseExternalMSBuild)' == 'true'"
Command="&quot;$(DOTNET_HOST_PATH)&quot; msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
Command="&quot;$(DOTNET_HOST_PATH)&quot; msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileRobustXaml /p:_RobustForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false /p:IntermediateOutputPath=&quot;$(IntermediateOutputPath.TrimEnd('\'))/&quot;"/>
</Target>
</Project>

View File

@@ -54,10 +54,265 @@ END TEMPLATE-->
*None yet*
## 255.0.2
## 262.0.2
## 255.0.1
## 262.0.1
## 262.0.0
### Breaking changes
* Toolshed commands will now validate that each non-generic command argument is parseable (i.e., has a corresponding type parser). This check can be disabled by explicitly marking the argument as unparseable via `CommandArgumentAttribute.Unparseable`.
### New features
* `ToolshedManager.TryParse` now also supports nullable value types.
* Add an ignoredComponents arg to IsDefault.
### Bugfixes
* Fix `SpriteComponent.Layer.Visible` setter not marking a sprite's bounding box as dirty.
* The audio params in the passed SoundSpecifier for PlayStatic(SoundSpecifier, Filter, ...) will now be used as a default like other PlayStatic overrides.
* Fix windows not saving their positions correctly when their x position is <= 0.
* Fix transform state handling overriding PVS detachment.
## 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.
## 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

View File

@@ -9,6 +9,7 @@ entity-spawn-window-override-menu-tooltip = Override placement
## TileSpawnWindow
tile-spawn-window-title = Place Tiles
tile-spawn-window-mirror-button-text = Mirror Tiles
## Console

View 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 }

View File

@@ -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,8 @@ 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")
);
}
@@ -92,16 +98,8 @@ public sealed class DataDefinitionAnalyzerTest
public async Task ReadOnlyFieldTest()
{
const string code = """
using System;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.Serialization.Manager.Attributes
{
public class DataFieldBaseAttribute : Attribute;
public class DataFieldAttribute : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
}
[DataDefinition]
public sealed partial class Foo
{
@@ -114,8 +112,8 @@ public sealed class DataDefinitionAnalyzerTest
""";
await Verifier(code,
// /0/Test0.cs(15,12): error RA0019: Data field Bad in data definition Foo is readonly
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldWritableRule).WithSpan(15, 12, 15, 20).WithArguments("Bad", "Foo")
// /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")
);
}
@@ -123,16 +121,8 @@ public sealed class DataDefinitionAnalyzerTest
public async Task ReadOnlyPropertyTest()
{
const string code = """
using System;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.Serialization.Manager.Attributes
{
public class DataFieldBaseAttribute : Attribute;
public class DataFieldAttribute : DataFieldBaseAttribute;
public sealed class DataDefinitionAttribute : Attribute;
}
[DataDefinition]
public sealed partial class Foo
{
@@ -145,8 +135,40 @@ public sealed class DataDefinitionAnalyzerTest
""";
await Verifier(code,
// /0/Test0.cs(15,20): error RA0020: Data field property Bad in data definition Foo does not have a setter
VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldPropertyWritableRule).WithSpan(15, 20, 15, 28).WithArguments("Bad", "Foo")
// /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")
);
}
}

View File

@@ -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";
@@ -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)
@@ -164,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
));
}
}
}
@@ -201,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)
@@ -383,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))

View File

@@ -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.

View File

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

View File

@@ -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'm pretty sure we do a lot of unnecessary maths.
return _sprite.CalculateBounds((entry.Uid, entry.Component), pos, rot, default).CalcBoundingBox();
}
#endregion
}

View File

@@ -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);

View File

@@ -296,7 +296,7 @@ namespace Robust.Client.GameObjects
public override void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
{
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityDeleted
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;
@@ -322,7 +322,7 @@ namespace Robust.Client.GameObjects
{
if (IsQueuedForDeletion(ent.Owner)
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|| ent.Comp1.EntityDeleted
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
{
return;

View File

@@ -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; }

View File

@@ -1,6 +1,5 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Graphics;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.Maths;

View File

@@ -24,9 +24,7 @@ namespace Robust.Client.GameObjects
public sealed class SpriteBoundsSystem : EntitySystem
{
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly SpriteTreeSystem _spriteTree = default!;
private SpriteBoundsOverlay? _overlay;
@@ -42,7 +40,7 @@ namespace Robust.Client.GameObjects
if (_enabled)
{
DebugTools.AssertNull(_overlay);
_overlay = new SpriteBoundsOverlay(_spriteTree, _xformSystem);
_overlay = new SpriteBoundsOverlay(EntityManager);
_overlayManager.AddOverlay(_overlay);
}
else
@@ -57,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 SharedTransformSystem _xformSystem;
private SpriteTreeSystem _renderTree;
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, SharedTransformSystem xformSystem)
{
_renderTree = renderTree;
_xformSystem = xformSystem;
}
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)
{
@@ -76,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 (sprite, xform) = entry;
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform);
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
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;

View File

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

View File

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

View File

@@ -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);
}

View 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;
}
}
}

View File

@@ -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));
}

View File

@@ -1,11 +1,208 @@
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)
{
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();
}
return GetPrototypeIcon(entityPrototype);
}
/// <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)
{
// This method may spawn & delete an entity to get an accruate RSI state, hence we cache the results
if (_cachedPrototypeIcons.TryGetValue(prototype.ID, out var cachedResult))
return cachedResult;
return _cachedPrototypeIcons[prototype.ID] = GetPrototypeIconInternal(prototype);
}
private IRsiStateLike GetPrototypeIconInternal(EntityPrototype prototype)
{
// IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal.
if (prototype.TryGetComponent(out IconComponent? icon, _factory))
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;
}
public IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype proto) =>
GetPrototypeTextures(proto, out _);
public IEnumerable<IDirectionalTextureProvider> GetPrototypeTextures(EntityPrototype proto, out bool noRot)
{
var results = new List<IDirectionalTextureProvider>();
noRot = false;
if (proto.TryGetComponent(out IconComponent? icon, _factory))
{
results.Add(GetIcon(icon));
return results;
}
if (!proto.Components.ContainsKey("Sprite"))
{
results.Add(_resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
var dummy = Spawn(proto.ID, MapCoordinates.Nullspace);
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
// TODO SPRITE is this needed?
// And if it is, shouldn't GetPrototypeIconInternal also use this?
_appearance.OnChangeData(dummy, spriteComponent);
foreach (var layer in spriteComponent.AllLayers)
{
if (!layer.Visible)
continue;
if (layer.Texture != null)
{
results.Add(layer.Texture);
continue;
}
if (!layer.RsiState.IsValid)
continue;
var rsi = layer.Rsi ?? spriteComponent.BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.RsiState, out var state))
continue;
results.Add(state);
}
noRot = spriteComponent.NoRotation;
Del(dummy);
if (results.Count == 0)
results.Add(_resourceCache.GetFallback<TextureResource>().Texture);
return results;
}
[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>

View 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
}

View File

@@ -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
}

View 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);
}
}

View File

@@ -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;
}
}

View 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);
}
}

View 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!);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
@@ -35,25 +34,25 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
// Note that any new system dependencies have to be added to RobustUnitTest.BaseSetup()
[Dependency] private readonly SharedTransformSystem _xforms = default!;
[Dependency] private readonly SpriteTreeSystem _tree = default!;
[Dependency] private readonly AppearanceSystem _appearance = 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()
{
@@ -62,11 +61,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)
@@ -85,18 +84,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;

View File

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

View File

@@ -631,7 +631,7 @@ namespace Robust.Client.GameStates
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
if (compState != null)
if ((meta.Flags & MetaDataFlags.Detached) == 0 && compState != null)
{
var handleState = new ComponentHandleState(compState, null);
_entities.EventBus.RaiseComponentEvent(entity, comp, ref handleState);
@@ -1306,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);
@@ -1326,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);
}

View File

@@ -254,7 +254,11 @@ namespace Robust.Client.Graphics.Clyde
region = regionMaybe[tile.Variant];
}
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
var rotationMirroring = _tileDefinitionManager[tile.TypeId].AllowRotationMirror
? tile.RotationMirroring
: 0;
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, rotationMirroring);
i += 1;
}
}
@@ -325,7 +329,7 @@ namespace Robust.Client.Graphics.Clyde
continue;
var region = regionMaybe[0];
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region, 0);
i += 1;
}
}
@@ -408,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)
@@ -445,13 +452,57 @@ namespace Robust.Client.Graphics.Clyde
int gridY,
Span<Vertex2D> vertexBuffer,
Span<ushort> indexBuffer,
Box2 region)
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, region.Left, region.Bottom, Color.White);
vertexBuffer[vIdx + 1] = new Vertex2D(gridX + 1, gridY, region.Right, region.Bottom, Color.White);
vertexBuffer[vIdx + 2] = new Vertex2D(gridX + 1, gridY + 1, region.Right, region.Top, Color.White);
vertexBuffer[vIdx + 3] = new Vertex2D(gridX, gridY + 1, region.Left, region.Top, Color.White);
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);

View File

@@ -318,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);
@@ -369,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)
{

View File

@@ -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);

View File

@@ -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);

View File

@@ -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!;
}

View File

@@ -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);
}
}
}
}

View File

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

View File

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

View File

@@ -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;

View File

@@ -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);
}

View File

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

View File

@@ -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;

View File

@@ -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; }
}
}

View 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();
}
}

View File

@@ -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()

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -126,7 +126,7 @@ namespace Robust.Client.Placement
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);
}
}

View File

@@ -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);
}

View File

@@ -13,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!;

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}

View File

@@ -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;

View File

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

View File

@@ -994,6 +994,9 @@ namespace Robust.Client.UserInterface
internal int DoFrameUpdateRecursive(FrameEventArgs args)
{
if (!Visible)
return 0;
var total = 1;
FrameUpdate(args);

View File

@@ -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;

View File

@@ -1,6 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Robust.Client.UserInterface.Controllers;
@@ -11,11 +14,14 @@ namespace Robust.Client.UserInterface.Controllers;
/// and <see cref="UISystemDependencyAttribute"/> to depend on <see cref="EntitySystem"/>s, which will be automatically
/// injected once they are created.
/// </summary>
public abstract partial class UIController
public abstract partial class UIController : IPostInjectInit
{
[Dependency] protected readonly IUserInterfaceManager UIManager = default!;
[Dependency] protected readonly IEntitySystemManager EntitySystemManager = default!;
[Dependency] protected readonly IEntityManager EntityManager = default!;
[Dependency] protected readonly ILogManager LogManager = default!;
public ISawmill Log { get; protected set; } = default!;
public virtual void Initialize()
{
@@ -24,4 +30,39 @@ public abstract partial class UIController
public virtual void FrameUpdate(FrameEventArgs args)
{
}
protected virtual string SawmillName
{
get
{
var name = GetType().Name;
// Strip trailing "UIController"
if (name.EndsWith("UIController"))
name = name.Substring(0, name.Length - "UIController".Length);
// Convert CamelCase to snake_case
// Ignore if all uppercase, assume acronym (e.g. NPC or HTN)
if (name.All(char.IsUpper))
{
name = name.ToLower(CultureInfo.InvariantCulture);
}
else
{
name = string.Concat(name.Select(x => char.IsUpper(x) ? $"_{char.ToLower(x)}" : x.ToString()));
name = name.Trim('_');
}
return $"ui.{name}";
}
}
public void PostInject()
{
Log = LogManager.GetSawmill(SawmillName);
#if !DEBUG
Log.Level = LogLevel.Info;
#endif
}
}

View File

@@ -741,7 +741,7 @@ namespace Robust.Client.UserInterface.Controls
get => _selected;
set
{
if (!Selectable) return;
if (!Selectable || _selected == value) return;
_selected = value;
if(_selected) OnSelected?.Invoke(this);
else OnDeselected?.Invoke(this);

View File

@@ -95,6 +95,12 @@ namespace Robust.Client.UserInterface.Controls
public void Clear()
{
_firstLine = true;
foreach (var entry in _entries)
{
entry.RemoveControls();
}
_entries.Clear();
_totalContentHeight = 0;
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
@@ -104,6 +110,7 @@ namespace Robust.Client.UserInterface.Controls
public void RemoveEntry(Index index)
{
var entry = _entries[index];
entry.RemoveControls();
_entries.RemoveAt(index.GetOffset(_entries.Count));
var font = _getFont();
@@ -189,6 +196,9 @@ namespace Robust.Client.UserInterface.Controls
if (entryOffset > contentBox.Height)
{
entry.HideControls();
// We know that every subsequent entry will also fail the test, but we also need to
// hide all the controls, so we cannot simply break out of the loop
continue;
}

View File

@@ -14,6 +14,27 @@ namespace Robust.Client.UserInterface.Controls
private StyleBox? _backgroundStyleBoxOverride;
private StyleBox? _foregroundStyleBoxOverride;
private bool _vertical;
/// <summary>
/// Whether the progress bar is oriented vertically.
/// </summary>
/// <remarks>
/// A vertical progress bar fills from bottom to top.
/// </remarks>
public bool Vertical
{
get => _vertical;
set
{
if (_vertical != value)
{
_vertical = value;
InvalidateMeasure();
}
}
}
public StyleBox? BackgroundStyleBoxOverride
{
get => _backgroundStyleBoxOverride;
@@ -70,11 +91,23 @@ namespace Robust.Client.UserInterface.Controls
{
return;
}
var minSize = fg.MinimumSize;
var size = PixelWidth * GetAsRatio() - minSize.X;
if (size > 0)
if (_vertical)
{
fg.Draw(handle, UIBox2.FromDimensions(0, 0, minSize.X + size, PixelHeight), UIScale);
var size = PixelHeight * GetAsRatio();
if (size > 0)
{
fg.Draw(handle, UIBox2.FromDimensions(0, PixelHeight - size, PixelWidth, size), UIScale);
}
}
else
{
var minSize = fg.MinimumSize;
var size = PixelWidth * GetAsRatio() - minSize.X;
if (size > 0)
{
fg.Draw(handle, UIBox2.FromDimensions(0, 0, minSize.X + size, PixelHeight), UIScale);
}
}
}

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.Graphics;
@@ -15,8 +17,7 @@ namespace Robust.Client.UserInterface.Controls
{
[Dependency] private readonly MarkupTagManager _tagManager = default!;
private FormattedMessage? _message;
private RichTextEntry _entry;
private RichTextEntry? _entry;
private float _lineHeightScale = 1;
private bool _lineHeightOverride;
@@ -40,19 +41,26 @@ namespace Robust.Client.UserInterface.Controls
public string? Text
{
get => _message?.ToMarkup();
get => _entry?.Message.ToMarkup();
set
{
if (value == null)
{
_message?.Clear();
return;
}
SetMessage(FormattedMessage.FromMarkupPermissive(value));
Clear();
else
SetMessage(FormattedMessage.FromMarkupPermissive(value));
}
}
public void Clear()
{
_entry?.RemoveControls();
_entry = null;
InvalidateMeasure();
}
public IEnumerable<Control> Controls => _entry?.Controls?.Values ?? Enumerable.Empty<Control>();
public IReadOnlyList<MarkupNode> Nodes => _entry?.Message.Nodes ?? Array.Empty<MarkupNode>();
public RichTextLabel()
{
IoCManager.InjectDependencies(this);
@@ -61,8 +69,8 @@ namespace Robust.Client.UserInterface.Controls
public void SetMessage(FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
_message = message;
_entry = new RichTextEntry(_message, this, _tagManager, tagsAllowed, defaultColor);
_entry?.RemoveControls();
_entry = new RichTextEntry(message, this, _tagManager, tagsAllowed, defaultColor);
InvalidateMeasure();
}
@@ -73,31 +81,31 @@ namespace Robust.Client.UserInterface.Controls
SetMessage(msg, tagsAllowed, defaultColor);
}
public string? GetMessage() => _message?.ToMarkup();
public string? GetMessage() => _entry?.Message.ToMarkup();
/// <summary>
/// Returns a copy of the currently used formatted message.
/// </summary>
public FormattedMessage? GetFormattedMessage() => _entry == null ? null : new FormattedMessage(_entry.Value.Message);
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
if (_message == null)
{
if (_entry == null)
return Vector2.Zero;
}
var font = _getFont();
_entry.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
return new Vector2(_entry.Width / UIScale, _entry.Height / UIScale);
// _entry is nullable struct.
// cannot just call _entry.Value.Update() as that doesn't actually update _entry.
_entry = _entry.Value.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
return new Vector2(_entry.Value.Width / UIScale, _entry.Value.Height / UIScale);
}
protected internal override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (_message == null)
{
return;
}
_entry.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
_entry?.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
}
[Pure]

View File

@@ -145,6 +145,11 @@ namespace Robust.Client.UserInterface.CustomControls
protected override void FrameUpdate(FrameEventArgs args)
{
// This is to avoid unnecessarily setting a position where our size isn't yet fully updated.
// This most commonly happens with saved window positions if your window position is <= 0.
if (!IsMeasureValid)
return;
var (spaceX, spaceY) = Parent!.Size;
var maxX = spaceX - ((AllowOffScreen & DirectionFlag.West) == 0 ? Size.X : WindowEdgeSeparation);

View File

@@ -10,7 +10,9 @@
</BoxContainer>
<ItemList Name="TileList" Access="Public" VerticalExpand="True"/>
<BoxContainer Orientation="Horizontal">
<Button Name="MirroredButton" Access="Public" ToggleMode="True" Text="{Loc tile-spawn-window-mirror-button-text}"/>
<Button Name="EraseButton" Access="Public" ToggleMode="True" Text="{Loc window-erase-button-text}"/>
</BoxContainer>
<Label Name="RotationLabel" Access="Public"/>
</BoxContainer>
</TileSpawnWindow>

View File

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

View File

@@ -8,6 +8,7 @@ using Robust.Client.UserInterface.Stylesheets;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Robust.Client.UserInterface
{
@@ -26,6 +27,7 @@ namespace Robust.Client.UserInterface
TabContainer.SetTabTitle(DebugConsole, "Debug Console");
TabContainer.SetTabTitle(UI, "User Interface");
TabContainer.SetTabTitle(Perf, "Profiling");
TabContainer.SetTabTitle(Textures, Loc.GetString("dev-window-tab-textures-title"));
Stylesheet =
new DefaultStylesheet(IoCManager.Resolve<IResourceCache>(), IoCManager.Resolve<IUserInterfaceManager>()).Stylesheet;

View File

@@ -0,0 +1,26 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Robust.Client.UserInterface.DevWindowTabTextures">
<SplitContainer Orientation="Horizontal">
<!-- Left pane: list of textures -->
<BoxContainer Orientation="Vertical" MinWidth="200">
<Button Name="ReloadButton" Text="{Loc 'dev-window-tab-textures-reload'}" />
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'dev-window-tab-textures-filter'}" />
<ScrollContainer HScrollEnabled="False" VerticalExpand="True">
<BoxContainer Name="TextureList" Orientation="Vertical" />
</ScrollContainer>
<Label Name="SummaryLabel" Margin="4" />
</BoxContainer>
<!-- Right pane: show the selected texture info -->
<Control MinWidth="400">
<BoxContainer Orientation="Vertical">
<TextureRect Name="SelectedTextureDisplay" VerticalExpand="True" CanShrink="True"
Stretch="KeepAspectCentered" />
<Label Name="SelectedTextureInfo" />
</BoxContainer>
</Control>
</SplitContainer>
</Control>

View File

@@ -0,0 +1,136 @@
using System;
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Clyde;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface;
/// <summary>
/// Shows all loaded textures in the game.
/// </summary>
[GenerateTypedNameReferences]
internal sealed partial class DevWindowTabTextures : Control
{
[Dependency] private readonly IClydeInternal _clyde = null!;
[Dependency] private readonly ILocalizationManager _loc = null!;
public DevWindowTabTextures()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
ReloadButton.OnPressed += _ => Reload();
SearchBar.OnTextChanged += _ => Reload();
}
protected override void VisibilityChanged(bool newVisible)
{
if (newVisible)
{
Reload();
}
else
{
// Clear to release memory when tab not visible.
Clear();
}
}
private void Clear()
{
TextureList.RemoveAllChildren();
}
private void Reload()
{
Clear();
var total = 0L;
foreach (var (clydeTexture, loadedTexture) in _clyde.GetLoadedTextures()
.OrderByDescending(tup => tup.Item2.MemoryPressure))
{
if (!string.IsNullOrWhiteSpace(SearchBar.Text))
{
if (loadedTexture.Name is not { } name)
continue;
if (!name.Contains(SearchBar.Text, StringComparison.CurrentCultureIgnoreCase))
continue;
}
if (loadedTexture.MemoryPressure == 0)
{
// Bad hack to avoid showing render targets lol.
continue;
}
var button = new ContainerButton
{
Children = { new TextureEntry(loadedTexture, clydeTexture) }
};
button.OnPressed += _ => SelectTexture(loadedTexture, clydeTexture);
TextureList.AddChild(button);
total += loadedTexture.MemoryPressure;
}
SummaryLabel.Text =
_loc.GetString("dev-window-tab-textures-summary", ("bytes", ByteHelpers.FormatBytes(total)));
}
private void SelectTexture(Clyde.LoadedTexture loaded, Clyde.ClydeTexture texture)
{
SelectedTextureDisplay.Texture = texture;
SelectedTextureInfo.Text = _loc.GetString("dev-window-tab-textures-info",
("width", loaded.Width),
("height", loaded.Height),
("pixelType", loaded.TexturePixelType),
("srgb", loaded.IsSrgb),
("name", loaded.Name ?? ""),
("bytes", ByteHelpers.FormatBytes(loaded.MemoryPressure)));
}
private sealed class TextureEntry : Control
{
public TextureEntry(Clyde.LoadedTexture loaded, Clyde.ClydeTexture texture)
{
SetHeight = 64;
var bytes = ByteHelpers.FormatBytes(loaded.MemoryPressure);
var label = loaded.Name == null
? $"{texture.TextureId} ({bytes})"
: $"{loaded.Name}\n{texture.TextureId} ({bytes})";
AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
new TextureRect
{
SetWidth = 64,
Texture = texture,
CanShrink = true,
RectClipContent = true,
Stretch = TextureRect.StretchMode.Scale
},
new Label
{
Text = label,
ClipText = true,
HorizontalExpand = true
}
}
});
}
}
}

View File

@@ -8,12 +8,19 @@ namespace Robust.Client.UserInterface
/// </summary>
internal sealed class DummyFileDialogManager : IFileDialogManager
{
public Task<Stream?> OpenFile(FileDialogFilters? filters = null)
public Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null)
{
return Task.FromResult<Stream?>(null);
}
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true)
public Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(
FileDialogFilters? filters = null,
bool truncate = true,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None)
{
return Task.FromResult<(Stream fileStream, bool alreadyExisted)?>(null);
}

View File

@@ -7,9 +7,7 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Robust.Client.Graphics;
using Robust.Shared;
using Robust.Shared.Console;
using Robust.Shared.Asynchronous;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
@@ -30,18 +28,28 @@ namespace Robust.Client.UserInterface
private bool _kDialogAvailable;
private bool _checkedKDialogAvailable;
public async Task<Stream?> OpenFile(FileDialogFilters? filters = null)
public async Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null)
{
if ((access & FileAccess.ReadWrite) != access)
throw new ArgumentException("Invalid file access specified");
var realShare = share ?? (access == FileAccess.Read ? FileShare.Read : FileShare.None);
if ((realShare & (FileShare.ReadWrite | FileShare.Delete)) != realShare)
throw new ArgumentException("Invalid file share specified");
string? name;
if (_clyde.FileDialogImpl is { } clydeImpl)
return await clydeImpl.OpenFile(filters);
name = await clydeImpl.OpenFile(filters);
else
name = await GetOpenFileName(filters);
var name = await GetOpenFileName(filters);
if (name == null)
{
return null;
}
return File.Open(name, FileMode.Open);
return File.Open(name, FileMode.Open, access, realShare);
}
private async Task<string?> GetOpenFileName(FileDialogFilters? filters)
@@ -54,24 +62,34 @@ namespace Robust.Client.UserInterface
return await OpenFileNfd(filters);
}
public async Task<(Stream, bool)?> SaveFile(FileDialogFilters? filters, bool truncate = true)
public async Task<(Stream, bool)?> SaveFile(
FileDialogFilters? filters,
bool truncate = true,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None)
{
if (_clyde.FileDialogImpl is { } clydeImpl)
return await clydeImpl.SaveFile(filters);
if ((access & FileAccess.ReadWrite) != access)
throw new ArgumentException("Invalid file access specified");
if ((share & (FileShare.ReadWrite | FileShare.Delete)) != share)
throw new ArgumentException("Invalid file share specified");
string? name;
if (_clyde.FileDialogImpl is { } clydeImpl)
name = await clydeImpl.SaveFile(filters);
else
name = await GetSaveFileName(filters);
var name = await GetSaveFileName(filters);
if (name == null)
{
return null;
}
try
{
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open), true);
return (File.Open(name, truncate ? FileMode.Truncate : FileMode.Open, access, share), true);
}
catch (FileNotFoundException)
{
return (File.Open(name, FileMode.Create), false);
return (File.Open(name, FileMode.Create, access, share), false);
}
}

View File

@@ -1,5 +1,6 @@
using System.IO;
using System.Threading.Tasks;
using Robust.Client.Graphics;
namespace Robust.Client.UserInterface
{
@@ -19,7 +20,17 @@ namespace Robust.Client.UserInterface
/// The file stream for the file the user opened.
/// <see langword="null" /> if the user cancelled the action.
/// </returns>
Task<Stream?> OpenFile(FileDialogFilters? filters = null);
/// <param name="filters">Filters for file types that the user can select.</param>
/// <param name="access">What access is desired from the file operation.</param>
/// <param name="share">
/// What sharing mode is desired from the file operation.
/// If null is provided and <paramref name="access"/> is <see cref="FileAccess.Read"/>,
/// <see cref="FileShare.Read"/> is selected, otherwise <see cref="FileShare.None"/>.
/// </param>
Task<Stream?> OpenFile(
FileDialogFilters? filters = null,
FileAccess access = FileAccess.ReadWrite,
FileShare? share = null);
/// <summary>
/// Open a file dialog used for saving a single file.
@@ -29,6 +40,21 @@ namespace Robust.Client.UserInterface
/// Null if the user cancelled the action.
/// </returns>
/// <param name="truncate">Should we truncate an existing file to 0-size then write or append.</param>
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(FileDialogFilters? filters = null, bool truncate = true);
/// <param name="access">What access is desired from the file operation.</param>
/// <param name="share">Sharing mode for the opened file.</param>
Task<(Stream fileStream, bool alreadyExisted)?> SaveFile(
FileDialogFilters? filters = null,
bool truncate = true,
FileAccess access = FileAccess.ReadWrite,
FileShare share = FileShare.None);
}
/// <summary>
/// Internal implementation interface used to connect <see cref="FileDialogManager"/> and <see cref="IClydeInternal"/>.
/// </summary>
internal interface IFileDialogManagerImplementation
{
Task<string?> OpenFile(FileDialogFilters? filters);
Task<string?> SaveFile(FileDialogFilters? filters);
}
}

View File

@@ -5,7 +5,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class BoldItalicTag : IMarkupTag
public sealed class BoldItalicTag : IMarkupTagHandler
{
public const string BoldItalicFont = "DefaultBoldItalic";

View File

@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class BoldTag : IMarkupTag
public sealed class BoldTag : IMarkupTagHandler
{
public const string BoldFont = "DefaultBold";

View File

@@ -2,7 +2,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class BulletTag : IMarkupTag
public sealed class BulletTag : IMarkupTagHandler
{
public string Name => "bullet";

View File

@@ -6,7 +6,7 @@ namespace Robust.Client.UserInterface.RichText;
/// <summary>
/// Colors the text inside its opening and closing nodes
/// </summary>
public sealed class ColorTag : IMarkupTag
public sealed class ColorTag : IMarkupTagHandler
{
public static readonly Color DefaultColor = new(200, 200, 200);

View File

@@ -8,14 +8,14 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class CommandLinkTag : IMarkupTag
public sealed class CommandLinkTag : IMarkupTagHandler
{
[Dependency] private readonly IClientConsoleHost _clientConsoleHost = default!;
public string Name => "cmdlink";
/// <inheritdoc/>
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
if (!node.Value.TryGetString(out var text)
|| !node.Attributes.TryGetValue("command", out var commandParameter)

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.UserInterface.RichText;
/// Applies the font provided as the tags parameter to the markup drawing context.
/// Definitely not save for user supplied markup
/// </summary>
public sealed class FontTag : IMarkupTag
public sealed class FontTag : IMarkupTagHandler
{
public const string DefaultFont = "Default";
public const int DefaultSize = 12;

View File

@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class HeadingTag : IMarkupTag
public sealed class HeadingTag : IMarkupTagHandler
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;

View File

@@ -1,9 +1,16 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public interface IMarkupTag
/// <summary>
/// Classes that implement this interface will be instantiated by <see cref="MarkupTagManager"/> and used to handle
/// the parsing and behaviour of markup tags. Note that each class is only ever instantiated once by the tag manager,
/// and wil be used to handle all tags of that kind, and thus should not contain state information relevant to a
/// specific tag.
/// </summary>
public interface IMarkupTagHandler
{
/// <summary>
/// The string used as the tags name when writing rich text
@@ -54,17 +61,32 @@ public interface IMarkupTag
}
/// <summary>
/// Called inside the constructor of <see cref="RichTextEntry"/> to
/// supply a control that gets rendered inline before this tags children<br/>
/// Text continues to the right of the control until the next line and then continues bellow it
/// Called inside the constructor of <see cref="RichTextEntry"/> to supply a control that gets rendered inline
/// before this tags children. The returned control must be new instance to avoid issues with shallow cloning
/// <see cref="FormattedMessage"/> nodes. Text continues to the right of the control until the next line and
/// then continues bellow it.
/// </summary>
/// <param name="node">The markup node containing the parameter and attributes</param>
/// <param name="control">A UI control for placing in line with this tags children</param>
/// <returns>true if this tag supplies a control</returns>
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
control = null;
return false;
}
}
[Obsolete("Use IMarkupTagHandler")]
public interface IMarkupTag : IMarkupTagHandler
{
bool IMarkupTagHandler.TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
return TryGetControl(node, out control);
}
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
control = null;
return false;
}
}

View File

@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.UserInterface.RichText;
public sealed class ItalicTag : IMarkupTag
public sealed class ItalicTag : IMarkupTagHandler
{
public const string ItalicFont = "DefaultItalic";

View File

@@ -16,7 +16,7 @@ public sealed class MarkupTagManager
/// <summary>
/// Tags defined in engine need to be instantiated here because of sandboxing
/// </summary>
private readonly Dictionary<string, IMarkupTag> _markupTagTypes = new IMarkupTag[] {
private readonly Dictionary<string, IMarkupTagHandler> _markupTagTypes = new IMarkupTagHandler[] {
new BoldItalicTag(),
new BoldTag(),
new BulletTag(),
@@ -44,13 +44,13 @@ public sealed class MarkupTagManager
public void Initialize()
{
foreach (var type in _reflectionManager.GetAllChildren<IMarkupTag>())
foreach (var type in _reflectionManager.GetAllChildren<IMarkupTagHandler>())
{
//Prevent tags defined inside engine from being instantiated
if (_engineTypes.Contains(type))
continue;
var instance = (IMarkupTag)_sandboxHelper.CreateInstance(type);
var instance = (IMarkupTagHandler)_sandboxHelper.CreateInstance(type);
_markupTagTypes[instance.Name.ToLower()] = instance;
}
@@ -60,22 +60,48 @@ public sealed class MarkupTagManager
}
}
[Obsolete("Use GetMarkupTagHandler")]
public IMarkupTag? GetMarkupTag(string name)
{
return _markupTagTypes.GetValueOrDefault(name) as IMarkupTag;
}
public IMarkupTagHandler? GetMarkupTagHandler(string name)
{
return _markupTagTypes.GetValueOrDefault(name);
}
public bool TryGetMarkupTag(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTag? tag)
/// <summary>
/// Attempt to get the tag handler with the corresponding name.
/// </summary>
/// <param name="name">The name of the tag, as specified by <see cref="IMarkupTag.Name"/></param>
/// <param name="tagsAllowed">List of allowed tag types. If null, all types are allowed.</param>
/// <param name="handler">The instance responsible for handling tags of this type.</param>
/// <returns></returns>
public bool TryGetMarkupTagHandler(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTagHandler? handler)
{
if (_markupTagTypes.TryGetValue(name, out var markupTag)
// Using a whitelist prevents new tags from sneaking in.
&& (tagsAllowed == null || Array.IndexOf(tagsAllowed, markupTag.GetType()) != -1))
{
tag = markupTag;
handler = markupTag;
return true;
}
tag = null;
handler = null;
return false;
}
[Obsolete("Use TryGetMarkupTagHandler")]
public bool TryGetMarkupTag(string name, Type[]? tagsAllowed, [NotNullWhen(true)] out IMarkupTag? tag)
{
if (!TryGetMarkupTagHandler(name, tagsAllowed, out var handler) || handler is not IMarkupTag cast)
{
tag = null;
return false;
}
tag = cast;
return true;
}
}

View File

@@ -13,6 +13,9 @@ namespace Robust.Client.UserInterface
{
/// <summary>
/// Used by <see cref="OutputPanel"/> and <see cref="RichTextLabel"/> to handle rich text layout.
/// Note that if this text is ever removed or modified without removing the owning control,
/// then <see cref="RemoveControls"/> should be called to ensure that any controls that were added by this
/// entry are also removed.
/// </summary>
internal struct RichTextEntry
{
@@ -36,7 +39,7 @@ namespace Robust.Client.UserInterface
/// </summary>
public ValueList<int> LineBreaks;
private readonly Dictionary<int, Control>? _tagControls;
public readonly Dictionary<int, Control>? Controls;
public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
@@ -56,15 +59,35 @@ namespace Robust.Client.UserInterface
if (node.Name == null)
continue;
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var handler) || !handler.TryCreateControl(node, out var control))
continue;
// Markup tag handler instances are shared across controls. We need to ensure that the hanlder doesn't
// store state information and return the same control for each rich text entry.
DebugTools.Assert(handler.TryCreateControl(node, out var other) && other != control);
parent.Children.Add(control);
tagControls ??= new Dictionary<int, Control>();
tagControls.Add(nodeIndex, control);
}
_tagControls = tagControls;
Controls = tagControls;
}
// TODO RICH TEXT
// Somehow ensure that this **has** to be called when removing rich text from some control.
/// <summary>
/// Remove all owned controls from their parents.
/// </summary>
public readonly void RemoveControls()
{
if (Controls == null)
return;
foreach (var ctrl in Controls.Values)
{
ctrl.Orphan();
}
}
/// <summary>
@@ -74,7 +97,7 @@ namespace Robust.Client.UserInterface
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
/// <param name="uiScale"></param>
/// <param name="lineHeightScale"></param>
public void Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
public RichTextEntry Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
{
// This method is gonna suck due to complexity.
// Bear with me here.
@@ -112,10 +135,10 @@ namespace Robust.Client.UserInterface
continue;
if (ProcessMetric(ref this, metrics, out breakLine))
return;
return this;
}
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
continue;
control.Measure(new Vector2(Width, Height));
@@ -128,12 +151,14 @@ namespace Robust.Client.UserInterface
desiredSize.Y);
if (ProcessMetric(ref this, controlMetrics, out breakLine))
return;
return this;
}
Width = wordWrap.FinalizeText(out breakLine);
CheckLineBreak(ref this, breakLine);
return this;
bool ProcessRune(ref RichTextEntry src, Rune rune, out int? outBreakLine)
{
wordWrap.NextRune(rune, out breakLine, out var breakNewLine, out var skip);
@@ -166,9 +191,10 @@ namespace Robust.Client.UserInterface
internal readonly void HideControls()
{
if (_tagControls == null)
if (Controls == null)
return;
foreach (var control in _tagControls.Values)
foreach (var control in Controls.Values)
{
control.Visible = false;
}
@@ -220,7 +246,7 @@ namespace Robust.Client.UserInterface
globalBreakCounter += 1;
}
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
if (Controls == null || !Controls.TryGetValue(nodeIndex, out var control))
continue;
// Controls may have been previously hidden via HideControls due to being "out-of frame".
@@ -243,7 +269,7 @@ namespace Robust.Client.UserInterface
return node.Value.StringValue ?? "";
//Skip the node if there is no markup tag for it.
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
if (!tagManager.TryGetMarkupTagHandler(node.Name, _tagsAllowed, out var tag))
return "";
if (!node.Closing)

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Robust.Client.Graphics;
using Robust.Shared;
@@ -21,12 +20,14 @@ internal sealed class ReloadManager : IReloadManager
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILogManager _logMan = default!;
[Dependency] private readonly IResourceManager _res = default!;
#pragma warning disable CS0414
[Dependency] private readonly ITaskManager _tasks = default!;
#pragma warning restore CS0414
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
private CancellationTokenSource _reloadToken = new();
private readonly HashSet<ResPath> _reloadQueue = new();
private List<FileSystemWatcher> _watchers = new();
private List<FileSystemWatcher> _watchers = new(); // this list is never used but needed to prevent them from being garbage collected
public event Action<ResPath>? OnChanged;
@@ -69,6 +70,11 @@ internal sealed class ReloadManager : IReloadManager
_reloadQueue.Clear();
}
public void Register(ResPath directory, string filter)
{
Register(directory.ToString(), filter);
}
public void Register(string directory, string filter)
{
if (!_cfg.GetCVar(CVars.ResPrototypeReloadWatch))
@@ -90,7 +96,7 @@ internal sealed class ReloadManager : IReloadManager
NotifyFilter = NotifyFilters.LastWrite
};
_watchers.Add(watcher);
_watchers.Add(watcher); // prevent garbage collection
watcher.Changed += OnWatch;
@@ -100,7 +106,7 @@ internal sealed class ReloadManager : IReloadManager
}
catch (IOException ex)
{
Logger.Error($"Watching resources in path {path} threw an exception:\n{ex}");
_sawmill.Error($"Watching resources in path {path} threw an exception:\n{ex}");
}
}
@@ -136,6 +142,6 @@ internal sealed class ReloadManager : IReloadManager
}
});
}
#endif
#endif
}
}

View File

@@ -17,6 +17,7 @@ namespace Robust.Client.Utility
/// </summary>
public static class SpriteSpecifierExt
{
[Obsolete("Use SpriteSystem.GetTexture() instead")]
public static Texture GetTexture(this SpriteSpecifier.Texture texSpecifier, IResourceCache cache)
{
return cache
@@ -24,13 +25,14 @@ namespace Robust.Client.Utility
.Texture;
}
[Obsolete("Use SpriteSystem")]
[Obsolete("Use SpriteSystem.GetState() instead")]
public static RSI.State GetState(this SpriteSpecifier.Rsi rsiSpecifier, IResourceCache cache)
{
if (!cache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / rsiSpecifier.RsiPath, out var theRsi))
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
Logger.Error("SpriteSpecifier failed to load RSI {0}", rsiSpecifier.RsiPath);
return SpriteComponent.GetFallbackState(cache);
return sys.GetFallbackState();
}
if (theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
@@ -39,21 +41,22 @@ namespace Robust.Client.Utility
}
Logger.Error($"SpriteSpecifier has invalid RSI state '{rsiSpecifier.RsiState}' for RSI: {rsiSpecifier.RsiPath}");
return SpriteComponent.GetFallbackState(cache);
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>().GetFallbackState();
}
[Obsolete("Use SpriteSystem")]
[Obsolete("Use SpriteSystem.Frame0() instead")]
public static Texture Frame0(this SpriteSpecifier specifier)
{
return specifier.RsiStateLike().Default;
}
[Obsolete("Use SpriteSystem.RsiStateLike() instead")]
public static IDirectionalTextureProvider DirFrame0(this SpriteSpecifier specifier)
{
return specifier.RsiStateLike();
}
[Obsolete("Use SpriteSystem")]
[Obsolete("Use SpriteSystem.RsiStateLike() instead")]
public static IRsiStateLike RsiStateLike(this SpriteSpecifier specifier)
{
var resC = IoCManager.Resolve<IResourceCache>();
@@ -67,10 +70,11 @@ namespace Robust.Client.Utility
case SpriteSpecifier.EntityPrototype prototypeIcon:
var protMgr = IoCManager.Resolve<IPrototypeManager>();
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
if (!protMgr.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
{
Logger.Error("Failed to load PrototypeIcon {0}", prototypeIcon.EntityPrototypeId);
return SpriteComponent.GetFallbackState(resC);
return sys.GetFallbackState();
}
return SpriteComponent.GetPrototypeIcon(prototype, resC);

View File

@@ -39,6 +39,7 @@ public static class Diagnostics
public const string IdForbidLiteral = "RA0033";
public const string IdObsoleteInheritance = "RA0034";
public const string IdObsoleteInheritanceWithMessage = "RA0035";
public const string IdDataFieldYamlSerializable = "RA0036";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");

View File

@@ -331,6 +331,7 @@ namespace Robust.Server
// TODO: solve this properly.
_serializer.Initialize();
_loc.Initialize();
_loc.AddLoadedToStringSerializer(_stringSerializer);
//IoCManager.Resolve<IMapLoader>().LoadedMapData +=

View File

@@ -14,6 +14,19 @@ public sealed class PointLightSystem : SharedPointLightSystem
base.Initialize();
SubscribeLocalEvent<PointLightComponent, ComponentGetState>(OnLightGetState);
SubscribeLocalEvent<PointLightComponent, ComponentStartup>(OnLightStartup);
SubscribeLocalEvent<PointLightComponent, ComponentShutdown>(OnLightShutdown);
SubscribeLocalEvent<PointLightComponent, MetaFlagRemoveAttemptEvent>(OnFlagRemoveAttempt);
}
private void OnLightShutdown(Entity<PointLightComponent> ent, ref ComponentShutdown args)
{
UpdatePriority(ent.Owner, ent.Comp, MetaData(ent.Owner));
}
private void OnFlagRemoveAttempt(Entity<PointLightComponent> ent, ref MetaFlagRemoveAttemptEvent args)
{
if (IsHighPriority(ent.Comp))
args.ToRemove &= ~MetaDataFlags.PvsPriority;
}
private void OnLightStartup(EntityUid uid, PointLightComponent component, ComponentStartup args)
@@ -21,24 +34,14 @@ public sealed class PointLightSystem : SharedPointLightSystem
UpdatePriority(uid, component, MetaData(uid));
}
protected override void UpdatePriority(EntityUid uid, SharedPointLightComponent comp, MetaDataComponent meta)
private bool IsHighPriority(SharedPointLightComponent comp)
{
var isHighPriority = comp.Enabled && comp.CastShadows && (comp.Radius > 7);
_metadata.SetFlag((uid, meta), MetaDataFlags.PvsPriority, isHighPriority);
return comp is {Enabled: true, CastShadows: true, Radius: > 7, LifeStage: <= ComponentLifeStage.Running};
}
private void OnLightGetState(EntityUid uid, PointLightComponent component, ref ComponentGetState args)
protected override void UpdatePriority(EntityUid uid, SharedPointLightComponent comp, MetaDataComponent meta)
{
args.State = new PointLightComponentState()
{
Color = component.Color,
Enabled = component.Enabled,
Energy = component.Energy,
Offset = component.Offset,
Radius = component.Radius,
Softness = component.Softness,
CastShadows = component.CastShadows,
};
_metadata.SetFlag((uid, meta), MetaDataFlags.PvsPriority, IsHighPriority(comp));
}
public override SharedPointLightComponent EnsureLight(EntityUid uid)

View File

@@ -9,12 +9,30 @@ public sealed class ServerOccluderSystem : OccluderSystem
{
[Dependency] private readonly MetaDataSystem _metadata = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<OccluderComponent, MetaFlagRemoveAttemptEvent>(OnFlagRemoveAttempt);
}
private void OnFlagRemoveAttempt(Entity<OccluderComponent> ent, ref MetaFlagRemoveAttemptEvent args)
{
if (ent.Comp is {Enabled: true, LifeStage: <= ComponentLifeStage.Running})
args.ToRemove &= ~MetaDataFlags.PvsPriority;
}
protected override void OnCompStartup(EntityUid uid, OccluderComponent component, ComponentStartup args)
{
base.OnCompStartup(uid, component, args);
_metadata.SetFlag(uid, MetaDataFlags.PvsPriority, component.Enabled);
}
protected override void OnCompRemoved(EntityUid uid, OccluderComponent component, ComponentRemove args)
{
base.OnCompRemoved(uid, component, args);
_metadata.SetFlag(uid, MetaDataFlags.PvsPriority, false);
}
public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null, MetaDataComponent? meta = null)
{
if (!Resolve(uid, ref comp, false))

View File

@@ -166,11 +166,11 @@ internal sealed partial class PvsSystem
var session = pvsSession.Session;
if (session.Status != SessionStatus.InGame)
{
pvsSession.Viewers = Array.Empty<Entity<TransformComponent, EyeComponent?>>();
pvsSession.Viewers = Array.Empty<Entity<TransformComponent, EyeComponent?>>();
return;
}
// Fast path
// The majority of players will have no view subscriptions
if (session.ViewSubscriptions.Count == 0)
{
if (session.AttachedEntity is not {} attached)
@@ -184,15 +184,21 @@ internal sealed partial class PvsSystem
return;
}
var count = session.ViewSubscriptions.Count;
var i = 0;
if (session.AttachedEntity is { } local)
{
Array.Resize(ref pvsSession.Viewers, session.ViewSubscriptions.Count + 1);
if (!session.ViewSubscriptions.Contains(local))
count += 1;
Array.Resize(ref pvsSession.Viewers, count);
// Attached entity is always the first viewer, to prioritize it and help reduce pop-in for the "main" eye.
pvsSession.Viewers[i++] = (local, Transform(local), _eyeQuery.CompOrNull(local));
}
else
{
Array.Resize(ref pvsSession.Viewers, session.ViewSubscriptions.Count);
Array.Resize(ref pvsSession.Viewers, count);
}
foreach (var ent in session.ViewSubscriptions)
@@ -200,6 +206,8 @@ internal sealed partial class PvsSystem
if (ent != session.AttachedEntity)
pvsSession.Viewers[i++] = (ent, Transform(ent), _eyeQuery.CompOrNull(ent));
}
DebugTools.AssertEqual(i, pvsSession.Viewers.Length);
}
private void ProcessVisibleChunks()

View File

@@ -0,0 +1,8 @@
using Robust.Shared.Localization;
namespace Robust.Server.Localization;
internal sealed class ServerLocalizationManager : LocalizationManager, ILocalizationManager
{
void ILocalizationManager.Initialize() => Initialize();
}

View File

@@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Controllers;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
namespace Robust.Server.Physics.Commands;
public sealed class BoxStackCommand : IConsoleCommand
{
[Dependency] private readonly IEntityManager _entManager = default!;
public string Command => "boxstack";
public string Description => string.Empty;
public string Help => "boxstack [mapid] [columns] [rows]";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 3 ||
!int.TryParse(args[0], out var mapInt) ||
!int.TryParse(args[1], out var columns) ||
!int.TryParse(args[2], out var rows))
{
return;
}
var mapId = new MapId(mapInt);
var fixtureSystem = _entManager.System<FixtureSystem>();
var physSystem = _entManager.System<SharedPhysicsSystem>();
physSystem.SetGravity(new Vector2(0f, -9.8f));
var groundUid = _entManager.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var ground = _entManager.AddComponent<PhysicsComponent>(groundUid);
var groundManager = _entManager.EnsureComponent<FixturesComponent>(groundUid);
var horizontal = new EdgeShape(new Vector2(-40, 0), new Vector2(40, 0));
fixtureSystem.CreateFixture(groundUid, "fix1", new Fixture(horizontal, 1, 1, true), manager: groundManager, body: ground);
var vertical = new EdgeShape(new Vector2(10, 0), new Vector2(10, 10));
fixtureSystem.CreateFixture(groundUid, "fix2", new Fixture(vertical, 1, 1, true), manager: groundManager, body: ground);
physSystem.WakeBody(groundUid, manager: groundManager, body: ground);
var xs = new[]
{
0.0f, -10.0f, -5.0f, 5.0f, 10.0f
};
for (var j = 0; j < columns; j++)
{
for (var i = 0; i < rows; i++)
{
var x = 0.0f;
var boxUid = _entManager.SpawnEntity(null,
new MapCoordinates(new Vector2(xs[j] + x, 0.55f + 2.1f * i), mapId));
var box = _entManager.AddComponent<PhysicsComponent>(boxUid);
var manager = _entManager.EnsureComponent<FixturesComponent>(boxUid);
physSystem.SetBodyType(boxUid, BodyType.Dynamic, manager: manager, body: box);
var poly = new PolygonShape(0.001f);
poly.Set(new List<Vector2>()
{
new(0.5f, -0.5f),
new(0.5f, 0.5f),
new(-0.5f, 0.5f),
new(-0.5f, -0.5f),
});
fixtureSystem.CreateFixture(boxUid, "fix1", new Fixture(poly, 1, 1, true), manager: manager, body: box);
physSystem.WakeBody(boxUid, manager: manager, body: box);
}
}
}
}

View File

@@ -140,7 +140,7 @@ namespace Robust.Server.Placement
{
// Replace existing entities if relevant.
if (msg.Replacement && _prototype.Index<EntityPrototype>(entityTemplateName).Components.TryGetValue(
_factory.GetComponentName(typeof(PlacementReplacementComponent)), out var compRegistry))
_factory.GetComponentName<PlacementReplacementComponent>(), out var compRegistry))
{
var key = ((PlacementReplacementComponent)compRegistry.Component).Key;
var gridUid = _xformSystem.GetGrid(coordinates);
@@ -179,11 +179,14 @@ namespace Robust.Server.Placement
}
else
{
PlaceNewTile(tileType, coordinates, msg.MsgChannel.UserId);
if (_tileDefinitionManager[tileType].AllowRotationMirror)
PlaceNewTile(tileType, coordinates, msg.MsgChannel.UserId, Tile.DirectionToByte(dirRcv), msg.Mirrored);
else
PlaceNewTile(tileType, coordinates, msg.MsgChannel.UserId, Tile.DirectionToByte(Direction.South), false);
}
}
private void PlaceNewTile(int tileType, EntityCoordinates coordinates, NetUserId placingUserId)
private void PlaceNewTile(int tileType, EntityCoordinates coordinates, NetUserId placingUserId, byte direction, bool mirrored)
{
if (!coordinates.IsValid(_entityManager)) return;
@@ -193,7 +196,7 @@ namespace Robust.Server.Placement
if (_entityManager.TryGetComponent(coordinates.EntityId, out grid)
|| _mapManager.TryFindGridAt(_xformSystem.ToMapCoordinates(coordinates), out gridId, out grid))
{
_maps.SetTile(gridId, grid, coordinates, new Tile(tileType));
_maps.SetTile(gridId, grid, coordinates, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0))));
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
@@ -207,7 +210,7 @@ namespace Robust.Server.Placement
_xformSystem.SetWorldPosition(newGridXform, coordinates.Position - newGrid.Comp.TileSizeHalfVector); // assume bottom left tile origin
var tilePos = _maps.WorldToTile(newGrid.Owner, newGrid.Comp, coordinates.Position);
_maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType));
_maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0))));
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);

View File

@@ -13,8 +13,10 @@ namespace Robust.Server.Prototypes
{
public sealed class ServerPrototypeManager : PrototypeManager
{
#pragma warning disable CS0414
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IConGroupController _conGroups = default!;
#pragma warning restore CS0414
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IBaseServerInternal _server = default!;

View File

@@ -4,6 +4,7 @@ using Robust.Server.Console;
using Robust.Server.DataMetrics;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Server.Localization;
using Robust.Server.Placement;
using Robust.Server.Player;
using Robust.Server.Prototypes;
@@ -21,6 +22,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.Player;
@@ -97,7 +99,9 @@ namespace Robust.Server
deps.Register<NetworkResourceManager>();
deps.Register<IHttpClientHolder, HttpClientHolder>();
deps.Register<UploadedContentManager>();
deps.Register<IHWId, DummyHWId>();
deps.Register<IHWId, DummyHWId>();
deps.Register<ILocalizationManager, ServerLocalizationManager>();
deps.Register<ILocalizationManagerInternal, ServerLocalizationManager>();
}
}
}

View File

@@ -104,6 +104,16 @@ namespace Robust.Shared.Maths
return (Direction) (Math.Floor((ang + CardinalOffset) / CardinalSegment) * 2 % 8);
}
/// <summary>
/// Rounds the angle to the nearest cardinal direction. This behaves similarly to a combination of
/// <see cref="GetCardinalDir"/> and Direction.ToAngle(), however this may return an angle outside of the range
/// returned by those methods (-pi to pi).
/// </summary>
public Angle RoundToCardinalAngle()
{
return new Angle(CardinalSegment * Math.Floor((Theta + CardinalOffset) / CardinalSegment));
}
/// <summary>
/// Rotates the vector counter-clockwise around its origin by the value of Theta.
/// </summary>

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.Contracts;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -101,6 +102,7 @@ namespace Robust.Shared.Maths
/// <summary>
/// Returns the smallest rectangle that contains both of the rectangles.
/// </summary>
[Pure]
public readonly Box2i Union(in Box2i other)
{
var botLeft = Vector2i.ComponentMin(BottomLeft, other.BottomLeft);
@@ -207,6 +209,7 @@ namespace Robust.Shared.Maths
/// <summary>
/// Multiplies each side of the box by the scalar.
/// </summary>
[Pure]
public Box2i Scale(int scalar)
{
return new Box2i(
@@ -215,6 +218,21 @@ namespace Robust.Shared.Maths
Right * scalar,
Top * scalar);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool Intersects(in Box2i other)
{
return other.Bottom <= this.Top && other.Top >= this.Bottom && other.Right >= this.Left &&
other.Left <= this.Right;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public readonly Box2i Enlarged(int size)
{
return new(Left - size, Bottom - size, Right + size, Top + size);
}
}
/// <summary>

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