Compare commits

...

96 Commits

Author SHA1 Message Date
PJB3005
7212d6d6b2 Version: 247.2.3 2025-09-26 13:40:50 +02:00
PJB3005
2d5e6c2b77 Validate that content assemblies have a limited list of names.
Also, only read assemblies once from disk

(cherry picked from commit 443a8dfca65be7d60c4bd46181b4c749b4756114)
2025-09-26 13:40:50 +02:00
PJB3005
19307875cf Version: 247.2.2 2025-09-19 09:17:35 +02:00
Skye
a76c57f841 Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:35 +02:00
PJB3005
0a604e4a04 Version: 247.2.1 2025-09-14 14:58:24 +02:00
PJB3005
6218ef6e3f Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

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

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

    Move CEF cache out of data directory

    Don't want content messing with this...

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

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

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

    Update SpaceWizards.NFluidSynth to 0.2.2

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

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
(cherry picked from commit 4413695c77fb705054c2f81fa18ec0a189b685dd)
2025-09-14 14:58:24 +02:00
PJB3005
76b46479b6 Version: 247.2.0 2025-02-23 01:44:44 +01:00
PJB3005
de9a8d286a Release notes 2025-02-23 01:43:58 +01:00
Milon
a1df0fb4af fix some issues with ClientDisconnect (#5625)
* fix

* actually fix for real this time

* just use HappyEyeballsHttp :godo:

* review
2025-02-23 01:33:58 +01:00
pathetic meowmeow
e6bc5a1057 Proxy scrollbar values and value targets in ScrollContainer (#5697) 2025-02-22 22:10:32 +01:00
beck-thompson
11b24579a2 Fix MultiRootInheritanceGraph not detecting circular inheritance (#5672)
* Fix

* This is better!

* Fixes
2025-02-22 22:08:31 +01:00
metalgearsloth
685d002bb7 Move VisibilitySystem to shared (#5694)
* Move VisibilitySystem to shared

* this

* Remove redundant qualifiers.

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-02-22 21:36:49 +01:00
Tobias Berger
2e0d18aeaf Fix wrong parameters for Regex.Escape in Sandbox whitelist (#5688) 2025-02-22 18:08:28 +01:00
Kyle Tyo
06dbff0429 believe that should be all of em. (#5691) 2025-02-22 18:01:07 +01:00
Southbridge
15958a9447 Fix issue regarding Tilemaps not saving when modified (#5696)
* One line C# fix

* fixed typo

* after spending way too long trying to figure out the problem, turns out all we needed was a simple one line change
2025-02-22 17:33:33 +01:00
pathetic meowmeow
fd5a4d9b8a Refactor audio system to send collection IDs over the network (#5540)
This is important groundwork for future features such as captioning,
as a caption and other data can be associated with the collection
prototype instead of passing extra data everywhere with the sound.
2025-02-22 17:29:47 +01:00
DrSmugleaf
6d958847cb Fix prototype hot reloading crashing when adding a component that an existing entity already has (#5695) 2025-02-22 16:30:05 +01:00
Milon
8a04a4f3a5 Add a method for copying components (#5654)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-02-21 09:46:13 +11:00
ElectroJr
7104a4f459 Version: 247.1.0 2025-02-20 16:26:04 +13:00
Leon Friedrich
f29949a32c Revert "Add ICloneable support to ComponentNetworkGenerator (#5656)" (#5687)
This reverts commit e14537074e.
2025-02-20 14:22:36 +11:00
Leon Friedrich
3bbbabf238 Update map format validator (#5686)
* Update map format validator

* string -> str
2025-02-20 12:11:12 +11:00
metalgearsloth
d95aca3d9e Fix DirtyFields proxy method (#5684) 2025-02-20 00:15:05 +11:00
Tayrtahn
e14537074e Add ICloneable support to ComponentNetworkGenerator (#5656) 2025-02-18 23:21:40 +11:00
DrSmugleaf
af2d01981f Add optional minimumDistance parameter to SharedJointSystem.CreateDistanceJoint (#5682) 2025-02-18 14:18:15 +11:00
Fildrance
7df23e047c feat: shaders now can accept array of Color as parameter (#5679)
* feat: shaders now can accept array of Color as parameter

* fix: Clyde.SetUniformDirect for Color[] doesn't mutate original array, removed invalid 'in' keyword on SetUniform

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
2025-02-16 14:13:04 +01:00
ElectroJr
5c7ab43049 Fix typo in RELEASE-NOTES.md 2025-02-17 00:15:27 +13:00
ElectroJr
8f75560ec4 Version: 247.0.0 2025-02-17 00:14:18 +13:00
Leon Friedrich
b323c8bd1e Add support for optional and params T[] toolshed command arguments. (#5573)
* Include argument name in completion suggestions

* Support optional args

* It (not so shrimply) works

* Add tests

* Add TestGenericPipeInference

* Fix tests

* Release notes

* Overzealous YAMLLinter

* Improve help signatures, fix map command

* Improve NoImplementationError

* Better type argument help signatures

* better pipe syntax

* fix NRE

* Add test

* a

* Fix silent toolshed failure

* Fix GetConcreteMethodInternal

* Improve vars command

* EntProtoId IAsType

* More GetConcreteMethodInternal fixes

* I hate this so much

* update tp command description

The command arguments call the the "other" entity the "target"

* Support localized argument hints/signatures
2025-02-16 21:55:05 +11:00
Leon Friedrich
faef44daaa Make PVS overrides respect vismasks (#5598)
* Make overrides respect vismasks

* Thread safety

* Release notes

* Use ExpandPvsEvent.Mask for other overrides

* check if already queued
2025-02-16 21:32:23 +11:00
Leon Friedrich
fbc706f37b Refactor map loading & saving (#5572)
* Refactor map loading & saving

* test fixes

* ISerializationManager tweaks

* Fix component composition

* Try fix entity deserialization component composition

* comments

* CL

* error preinit

* a

* cleanup

* error if version is too new

* Add AlwaysPushSerializationTest

* Add auto-inclusion test

* Better categorization

* Combine test components

* Save -> TrySave

Also better handling for saving multiple entities individually

* Create new partial class for map loading

* Add OrphanSerializationTest

* Include MapIds in BeforeSerializationEvent

* Addd LifetimeSerializationTest

* Add TestMixedLifetimeSerialization

* Add CategorizationTest

* explicitly serialize list of nullspace entities

* Add backwards compatibility test

* Version comments

also fixes wrong v4 format

* add MapMergeTest

* Add NetEntity support

* Optimize EntityDeserializer

Avoid unnecessary component deserialization

* fix assert & other bugs

* fucking containers strike again

* Fix deletion of pre-init entities

* fix release note merge conflict

* Update Robust.Shared/Map/MapManager.GridCollection.cs

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* VV

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-02-16 21:25:07 +11:00
metalgearsloth
9d1b15ab4b Version: 246.0.0 2025-02-16 19:32:42 +11:00
metalgearsloth
ea1cc5e446 Planet lighting pre-reqs (#5490)
* Add another lookup overload

* Fix RenderInRenderTarget

See the linked issue for what happens.

* Also this one

* stuff

* Fix stencilling

* fixes

* mix blend

* fix

* blur fixes

* Tile flag

* Minor tweak

* Fixes

* Render state fixes

* Fixes

* Fix stupidity

* More state render bug fixes

* MapUid on overlay draw

* Remove blur comment

* Fixes

* Fixes

* Remove

* Engine vibe
2025-02-16 19:29:32 +11:00
metalgearsloth
bcb5c2d35d Version: 245.1.0 2025-02-16 14:56:39 +11:00
metalgearsloth
c011eff80e Increase audio despawn buffer (#5665)
Apparently it can clip and the buffer is really just there so we despawn 'at some point' and rather than hunching over my debugger for potentially an hour this is easier and almost no impact.

I've also considered flagging some audio as "play the full thing" if someone misses the start of it but need to thonk on that one a bit in future.
2025-02-16 14:30:18 +11:00
Fildrance
e163c496c3 fix: fixed EntityPrototypeView not reacting on SetPrototype when EnteredTree already was called with _currentPrototype empty (#5649)
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
2025-02-16 14:29:59 +11:00
Tayrtahn
fec81bc2a1 Add more info to AnchorEntity debug assert (#5668) 2025-02-16 03:16:23 +01:00
Leon Friedrich
7016facb9a Tweak UserInterfaceComponent shutdown to prevent bugs (#5678) 2025-02-14 18:16:24 +11:00
Simon
0c41a041e3 Move ParseObject method into a public class for content to use (#5674) 2025-02-14 14:21:22 +11:00
ElectroJr
55571ef5b1 Version: 245.0.0 2025-02-14 16:11:50 +13:00
Leon Friedrich
afaef645b0 Fix MappingDataNode.TryAddCopy() (#5677) 2025-02-14 14:10:13 +11:00
Milon
d442d90d60 no more (#5676) 2025-02-13 01:41:42 -05:00
metalgearsloth
fea592e1d5 Version: 244.0.0 2025-02-12 13:31:03 +11:00
metalgearsloth
bb9517fd19 Check equatable on client-predicted BUI states (#5671) 2025-02-12 13:29:19 +11:00
metalgearsloth
a734bc50fa Avoid unnecessary UpdateState calls for BUIs (#5670) 2025-02-12 12:50:33 +11:00
metalgearsloth
9e9ac56c95 Bump physics speed cap (#5667) 2025-02-11 20:54:21 +11:00
metalgearsloth
6979a63b1e Add CreateWindowCenteredRight method (#5666) 2025-02-11 15:02:31 +11:00
metalgearsloth
ae7725aafe Add compreg methods to entitymanager (#5655) 2025-02-10 21:39:41 +11:00
metalgearsloth
1a7e490e4b Version: 243.0.1 2025-02-08 19:06:50 +11:00
metalgearsloth
51971d0994 Revert basewindow change (#5664) 2025-02-08 17:38:40 +11:00
Mohamed Dwidar
d2aa8ecb5a Fixing guidebook not resizable from left and right (#5618)
* Fixing guidebook not resizable from left and right

issue 34504 in space-wizards/space-station-14 needs this fix

* update fix guideBood not resizable

a safer and more error resistent solution to https://github.com/space-wizards/space-station-14/issues/34504

* Mask DragMode.Move

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2025-02-08 17:14:08 +11:00
ScarKy0
c4a5752c2a Shared portion of PvsOverrideSystem to allow for usage in shared (#5651) 2025-02-04 12:23:42 -05:00
metalgearsloth
6a336d236b Version: 243.0.0 2025-02-04 00:25:15 +11:00
metalgearsloth
fc55c8e0d3 Add saved BUI positions (#5650) 2025-02-04 00:19:22 +11:00
metalgearsloth
2719b9f0c8 Tweak grid state slightly (#5644) 2025-02-04 00:15:36 +11:00
metalgearsloth
bd69d51d36 Version: 242.0.1 2025-02-02 12:29:24 +11:00
metalgearsloth
1bf0687671 Fix poly pooling (#5645) 2025-02-01 15:19:34 +11:00
Leon Friedrich
bdef9e3401 Fix reloading prototypes with AlwaysPushInheritance (#5612) 2025-01-31 15:38:13 +11:00
metalgearsloth
43648201ce Drop debug allocs a lot (#5638)
IDK why the lifestage one in particular ballooned my dotmemory one up in particular but it did; I would've thought if it's boxing the other ones would've shown up. Doesn't matter for release just QOL to drop allocs by more than half.
2025-01-30 05:07:57 +01:00
slarticodefast
2b2d08ba47 don't normalize direction vector when calling atan2 (#5641) 2025-01-30 12:07:34 +11:00
metalgearsloth
d7f6a9ba43 Version: 242.0.0 2025-01-29 23:45:59 +11:00
metalgearsloth
15f81751f7 Reduce lookup allocs significantly (#5639) 2025-01-29 23:43:20 +11:00
metalgearsloth
033c52751a MergeImplicitData fix (#5640) 2025-01-29 23:36:14 +11:00
Leon Friedrich
51edceae4d Ensure parents are always initialized & started before children (#5595)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-01-29 23:16:14 +11:00
ShadowCommander
5d7720755a Fix keyboard not focusing on search bar of the tile spawn window on open (#5630) 2025-01-29 22:48:59 +11:00
Leon Friedrich
acc7bf7595 Add support for overlays to draw controls (#5223)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-01-29 22:48:11 +11:00
Fildrance
de55d1bc52 fix: EntityPrototypeView now only creates entities when the UI is open
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-01-28 00:25:52 +11:00
metalgearsloth
d5e6e91b58 Make collisionwake resolve false (#5637) 2025-01-28 00:05:29 +11:00
metalgearsloth
da2bfdaa10 Version: 241.0.0 2025-01-27 21:31:31 +11:00
Leon Friedrich
af6cac14d6 Add CollisionPredictionTest (#5493)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2025-01-27 21:23:23 +11:00
Leon Friedrich
3f37846731 Avoid unnecessary DirtyField() calls (#5620) 2025-01-27 21:21:53 +11:00
metalgearsloth
d9bf1d1afb BUI deferral tweaks (#5503) 2025-01-27 21:04:27 +11:00
metalgearsloth
b9b80192e7 Minor contact QOL (#5560) 2025-01-27 16:20:04 +11:00
DrSmugleaf
e03aec47ef Fix component network generator not working for dicts with entityuid values but not keys 2025-01-21 19:09:22 -08:00
DrSmugleaf
ee906af16e Version: 240.1.2 2025-01-21 18:43:28 -08:00
wixoa
e205ae3627 Use the color arg when drawing a font character in world space (#5626) 2025-01-21 22:39:07 +01:00
Jerry
d818c5aa0c fix StatusHost RespondJsonAsync method (#5622) 2025-01-21 16:58:37 +01:00
ElectroJr
aa8fe8ac92 Version: 240.1.1 2025-01-20 14:16:39 +13:00
Leon Friedrich
4ba6687b9d Fix OverlayManager.RemoveOverlay (#5623) 2025-01-20 12:14:58 +11:00
ElectroJr
8f2817aa4e Version: 240.1.0 2025-01-20 13:04:19 +13:00
Leon Friedrich
89c7839fe2 Fix exception in DestroyContacts (#5619) 2025-01-19 16:25:58 +11:00
DrSmugleaf
9799132001 Make GamePrototypeLoadManager send a single message with all uploaded prototypes in a single message (#5616) 2025-01-19 11:49:27 +11:00
Milon
34ffa56c57 add AsNullable() extension method to Entity<T> (#5617)
* just works first try

* use a constructor instead
2025-01-18 20:52:18 +01:00
ElectroJr
f8410a4674 Version: 240.0.1 2025-01-18 16:54:03 +13:00
Leon Friedrich
43b991c690 Fix SharedBroadphaseSystem.GetBroadphases (#5615) 2025-01-18 14:37:54 +11:00
PJB3005
c463fc5e78 Make source generators emit EditorBrowsable(Never)
This hides the generated types from intellisense if you have the relevant option in Rider enabled.

Good because honestly these just bloat IntelliSense, and if you want to show them there's an IDE setting for it.
2025-01-18 01:23:35 +01:00
PJB3005
e21b3e069a Version: 240.0.0 2025-01-17 17:57:31 +01:00
PJB3005
c8f94ab40d Update release notes 2025-01-17 17:57:14 +01:00
Tornado Tech
2a882b5555 Moved Overlay sorting to OverlayManager (#5614)
* Moved Overlay sorting to OverlayManager

* Changed Add to AddRange
2025-01-17 17:47:13 +01:00
Leon Friedrich
32d8a1cba9 Misc grid state changes (#5597) 2025-01-17 17:07:09 +01:00
Leon Friedrich
5d84be9c78 Make ComponentRegistry not implement ISerializationContext (#5613) 2025-01-17 15:01:25 +01:00
PJB3005
eaaa70437a Reduce DynamicMethod use in Serv3
Data definitions created individual read/write methods for every single field that can be serialized. This was extremely inefficient and likely caused lots of overhead.

These methods are necessary because, in some cases, we can't directly use expression trees to write fields. But... we can still do it most of the time! So now for most data fields we can avoid a DynamicMethod entirely.
2025-01-16 15:10:35 +01:00
Leon Friedrich
448e8b0c2c Validate static EntProtoId<T> fields (#5593) 2025-01-16 10:36:47 +01:00
Leon Friedrich
42948d8f8e Fix autocompletion hint for toolshed strings (#5584)
* Fix autocompletion hint for toolshed strings

* Split functionality

* Remove combined flag
2025-01-16 10:30:42 +01:00
c4llv07e
039468f4b6 Add function to check if localization culture was already loaded. (#5603) 2025-01-16 10:28:15 +01:00
Leon Friedrich
6a3f88b1c6 Fix replay playback bugs (#5604)
* Sort replay entities

* Fix nameof(TState)

* I forgot to generate implicit states

* release notes
2025-01-16 10:26:53 +01:00
PJB3005
a314c5f797 Tickrate is now a ushort
Fixes #5592

This allows net.tickrate to be set to a max of 65535 instead of 255.

I didn't raise it fully to a uint because there are many places it's cast to an int, so uint would cause various compiler errors and compat issues I don't wanna deal with.
2025-01-16 01:13:50 +01:00
276 changed files with 12622 additions and 4009 deletions

View File

@@ -57,7 +57,7 @@
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />

View File

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

View File

@@ -54,6 +54,306 @@ END TEMPLATE-->
*None yet*
## 247.2.3
## 247.2.2
## 247.2.1
## 247.2.0
### New features
* Added functions for copying components to `IEntityManager` and `EntitySystem`.
* Sound played from sound collections is now sent as "collection ID + index" over the network instead of the final filename.
* This enables integration of future accessibility systems.
* Added a new `ResolvedSoundSpecifier` to represent played sounds. Methods that previously took a filename now take a `ResolvedSoundSpecifier`, with an implicit cast from string being interpreted as a raw filename.
* `VisibilitySystem` has been made accessible to shared as `SharedVisibilitySystem`.
* `ScrollContainer` now has properties exposing `Value` and `ValueTarget` on its internal scroll bars.
### Bugfixes
* Fix prototype hot reload crashing when adding a new component already exists on an entity.
* Fix maps failing to save in some cases related to tilemap IDs.
* Fix `Regex.Escape(string)` not being available in sandbox.
* Prototypes that parent themselves directly won't cause the game to hang on an infinite loop anymore.
* Fixed disconnecting during a connection attempt leaving the client stuck in a phantom state.
### Internal
* More warning cleanup.
## 247.1.0
### New features
* Added support for `Color[]` shader uniforms
* Added optional minimumDistance parameter to `SharedJointSystem.CreateDistanceJoint()`
### Bugfixes
* Fixed `EntitySystem.DirtyFields()` not actually marking fields as dirty.
### Other
* Updated the Yamale map file format validator to support v7 map/grid files.
## 247.0.0
### Breaking changes
* `ITileDefinitionManager.AssignAlias` and general tile alias functionality has been removed. `TileAliasPrototype` still exist, but are only used during entity deserialization.
* `IMapManager.AddUninitializedMap` has been removed. Use the map-init options on `CreateMap()` instead.
* Re-using a MapId will now log a warning. This may cause some integration tests to fail if they are configured to fail
when warnings are logged.
* The minimum supported map format / version has been increased from 2 to 3.
* The server-side `MapLoaderSystem` and associated classes & structs has been moved to `Robust.Shared`, and has been significantly modified.
* The `TryLoad` and `Save` methods have been replaced with grid, map, generic entity variants. I.e, `SaveGrid`, `SaveMap`, and `SaveEntities`.
* Most of the serialization logic and methods have been moved out of `MapLoaderSystem` and into new `EntitySerializer`
and `EntityDeserializer` classes, which also replace the old `MapSerializationContext`.
* The `MapLoadOptions` class has been split into `MapLoadOptions`, `SerializationOptions`, and `DeserializationOptions`
structs.
* The interaction between PVS overrides and visibility masks / layers have changed:
* Any forced entities (i.e., `PvsOverrideSystem.AddForceSend()`) now ignore visibility masks.
* Any global & session overrides (`PvsOverrideSystem.AddGlobalOverride()` & `PvsOverrideSystem.AddSessionOverride()`) now respect visibility masks.
* Entities added via the `ExpandPvsEvent` respect visibility masks.
* The mask used for any global/session overrides can be modified via `ExpandPvsEvent.Mask`.
* Toolshed Changes:
* The signature of Toolshed type parsers have changed. Instead of taking in an optional command argument name string, they now take in a `CommandArgument` struct.
* Toolshed commands can no longer contain a '|', as this symbol is now used for explicitly piping the output of one command to another. command pipes. The existing `|` and '|~' commands have been renamed to `bitor` and `bitnotor`.
* Semicolon terminated command blocks in toolshed commands no longer return anything. I.e., `i { i 2 ; }` is no longer a valid command, as the block has no return value.
### New features
* The current map format/version has increased from 6 to 7 and now contains more information to try support serialization of maps with null-space entities and full game saves.
* `IEntitySystemManager` now provides access to the system `IDependencyCollection`.
* Toolshed commands now support optional and `params T[]` arguments. optional / variable length commands can be terminated using ';' or '|'.
### Bugfixes
* Fixed entity deserialization for components with a data fields that have a AlwaysPushInheritance Attribute
* Audio entities attached to invisible / masked entities should no longer be able to temporarily make those entities visible to all players.
* The map-like Toolshed commands now work when a collection is piped in.
* Fixed a bug in toolshed that could cause it to preferentially use the incorrect command implementation.
* E.g., passing a concrete enumerable type would previously use the command implementation that takes in an unconstrained generic parameter `T` instead of a dedicated `IEnumeerable<T>` implementation.
### Other
* `MapChangedEvent` has been marked as obsolete, and should be replaced with `MapCreatedEvent` and `MapRemovedEvent.
* The default auto-completion hint for Toolshed commands have been changed and somewhat standardized. Most parsers should now generate a hint of the form:
* `<name (Type)>` for mandatory arguments
* `[name (Type)]` for optional arguments
* `[name (Type)]...` for variable length arguments (i.e., for `params T[]`)
## 246.0.0
### Breaking changes
* The fixes to renderer state may have inadvertantly broken some rendering code that relied upon the old behavior.
* TileRenderFlag has been removed and now it's just a byte flag on the tile for content usage.
### New features
* Add BeforeLighting overlay draw space for overlays that need to draw directly to lighting and want to do it immediately beforehand.
* Change BlurLights to BlurRenderTarget and make it public for content usage.
* Add ContentFlag to tiles for content-flag usage.
* Add a basic mix shader for doing canvas blends.
* Add GetClearColorEvent for content to override the clear color behavior.
### Bugfixes
* Fix pushing renderer state not restoring stencil status, blend status, queued shader instance scissor state.
## 245.1.0
### New features
* Add more info to the AnchorEntity debug message.
* Make ParseObject public where it will parse a supplied Type and string into the specified object.
### Bugfixes
* Fix EntityPrototypeView not always updating the entity correctly.
* Tweak BUI shutdown to potentially avoid skipping closing.
### Other
* Increase Audio entity despawn buffer to avoid clipping.
## 245.0.0
### Breaking changes
* `BoundUserInterface.Open()` now has the `MustCallBase` attribute
### Bugfixes
* Fixed an error in `MappingDataNode.TryAddCopy()`, which was causing yaml inheritance/deserialization bugs.
## 244.0.0
### Breaking changes
* Increase physics speedcap default from 35m/s to 400m/s in-line with box2d v3.
### New features
* Add EntityManager overloads for ComponentRegistration that's faster than the generic methods.
* Add CreateWindowCenteredRight for BUIs.
### Bugfixes
* Avoid calling UpdateState before opening a BUI.
## 243.0.1
### Bugfixes
* Fixed `BaseWindow` sometimes not properly updating the mouse cursor shape.
* Revert `BaseWindow` OnClose ordering due to prior reliance upon the ordering.
## 243.0.0
### Breaking changes
* RemoveChild is called after OnClose for BaseWindow.
### New features
* BUIs now have their positions saved when closed and re-used when opened when using the `CreateWindow<T>` helper or via manually registering it via RegisterControl.
### Other
* Ensure grid fixtures get updated in client state handling even if exceptions occur.
## 242.0.1
### Bugfixes
* Fixed prototype reloading/hotloading not properly handling data-fields with the `AlwaysPushInheritanceAttribute`
* Fix the pooled polygons using incorrect vertices for EntityLookup and MapManager.
### Internal
* Avoid normalizing angles constructed from vectors.
## 242.0.0
### Breaking changes
* The order in which the client initialises networked entities has changed. It will now always apply component states, initialise, and start an entity's parent before processing any children. This might break anything that was relying on the old behaviour where all component states were applied before any entities were initialised & started.
* `IClydeViewport` overlay rendering methods now take in an `IRenderHandle` instead of a world/screen handle.
* The `OverlayDrawArgs` struct now has an internal constructor.
### New features
* Controls can now be manually restyled via `Control.InvalidateStyleSheet()` and `Control.DoStyleUpdate()`
* Added `IUserInterfaceManager.RenderControl()` for manually drawing controls.
* `OverlayDrawArgs` struct now has an `IRenderHandle` field such that overlays can use the new `RenderControl()` methods.
* TileSpawnWindow will now take focus when opened.
### Bugfixes
* Fixed a client-side bug where `TransformComponent.GridUid` does not get set properly when an existing entity is attached to a new entity outside of the player's PVS range.
* EntityPrototypeView will only create entities when it's on the UI tree and not when the prototype is set.
* Make CollisionWake not log errors if it can't resolve.
### Other
* Replace IPhysShape API with generics on IMapManager and EntityLookupSystem.
### Internal
* Significantly reduce allocations for Box2 / Box2Rotated queries.
## 241.0.0
### Breaking changes
* Remove DeferredClose from BUIs.
### New features
* Added `EntityManager.DirtyFields()`, which allows components with delta states to simultaneously mark several fields as dirty at the same time.
* Add `CloserUserUIs<T>` to close keys of a specific key.
### Bugfixes
* Fixed `RaisePredictiveEvent()` not properly re-raising events during prediction for event handlers that did not take an `EntitySessionEventArgs` argument.
* BUI openings are now deferred to avoid having slight desync between deferred closes and opens occurring in the same tick.
## 240.1.2
## 240.1.1
### Bugfixes
* Fixed one of the `IOverlayManager.RemoveOverlay` overrides not fully removing the overlay.
## 240.1.0
### New features
* Added an `AsNullable` extension method for converting an `Entity<T>` into an `Entity<T?>`
### Bugfixes
* Fixed an exception in `PhysicsSystem.DestroyContacts()` that could result in entities getting stuck with broken physics.
### Other
* `GamePrototypeLoadManager` will now send all uploaded prototypes to connecting players in a single `GamePrototypeLoadMessage`, as opposed to one message per upload.
## 240.0.1
### Bugfixes
* Fixed `SharedBroadphaseSystem.GetBroadphases()` not returning the map itself, which was causing physics to not work properly off-grid.
## 240.0.0
### Breaking changes
* `ComponentRegistry` no longer implements `ISerializationContext`
* Tickrate values are now `ushort`, allowing them to go up to 65535.
### New features
* Console completion options now have new flags for preventing suggestions from being escaped or quoted.
* Added `ILocalizationManager.HasCulture()`.
* Static `EntProtoId<T>` fields are now validated to exist.
### Bugfixes
* Fixed a state handling bug in replays, which was causing exceptions to be thrown when applying delta states.
### Other
* Reduced amount of `DynamicMethod`s used by serialization system. This should improve performance somewhat.
### Internal
* Avoided sorting overlays every render frame.
* Various clean up to grid fixture code/adding asserts.
## 239.0.1
### Bugfixes

View File

@@ -4,6 +4,12 @@
kind: canvas
light_mode: unshaded
# Simple mix blend
- type: shader
id: Mix
kind: canvas
blend_mode: Mix
- type: shader
id: shaded
kind: canvas

View File

@@ -42,8 +42,7 @@ command-description-as =
command-description-count =
Counts the amount of entries in it's input, returning an integer.
command-description-map =
Maps the input over the given block, with the provided expected return type.
This command may be modified to not need an explicit return type in the future.
Maps the input over the given block.
command-description-select =
Selects N objects or N% of objects from the input.
One can additionally invert this command with not to make it select everything except N objects instead.
@@ -149,7 +148,7 @@ command-description-max =
Returns the maximum of two values.
command-description-BitAndCommand =
Performs bitwise AND.
command-description-BitOrCommand =
command-description-bitor =
Performs bitwise OR.
command-description-BitXorCommand =
Performs bitwise XOR.
@@ -203,11 +202,11 @@ command-description-mappos =
command-description-pos =
Returns an entity's coordinates.
command-description-tp-coords =
Teleports the target to the given coordinates.
Teleports the given entities to the target coordinates.
command-description-tp-to =
Teleports the target to the given other entity.
Teleports the given entities to the target entity.
command-description-tp-into =
Teleports the target "into" the given other entity, attaching it at (0 0) relative to it.
Teleports the given entities "into" the target entity, attaching it at (0 0) relative to it.
command-description-comp-get =
Gets the given component from the given entity.
command-description-comp-add =
@@ -277,7 +276,7 @@ command-description-ModVecCommand =
Performs the modulus operation over the input with the given constant right-hand value.
command-description-BitAndNotCommand =
Performs bitwise AND-NOT over the input.
command-description-BitOrNotCommand =
command-description-bitornot =
Performs bitwise OR-NOT over the input.
command-description-BitXnorCommand =
Performs bitwise XNOR over the input.

View File

@@ -66,6 +66,7 @@ public sealed class ComponentPauseGeneratorTest
public partial class FooComponent
{
[RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()
@@ -106,6 +107,7 @@ public sealed class ComponentPauseGeneratorTest
public partial class FooComponent
{
[RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()
@@ -147,6 +149,7 @@ public sealed class ComponentPauseGeneratorTest
public partial class FooComponent
{
[RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()
@@ -188,6 +191,7 @@ public sealed class ComponentPauseGeneratorTest
public partial class FooComponent
{
[RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class FooComponent_AutoPauseSystem : EntitySystem
{
public override void Initialize()

View File

@@ -0,0 +1,96 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using JetBrains.Annotations;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.UnitTesting.Server;
namespace Robust.Benchmarks.EntityManager;
[Virtual]
public partial class HasComponentBenchmark
{
private static readonly Consumer Consumer = new();
private ISimulation _simulation = default!;
private IEntityManager _entityManager = default!;
private ComponentRegistration _compReg = default!;
private A _dummyA = new();
[UsedImplicitly]
[Params(1, 10, 100, 1000)]
public int N;
[GlobalSetup]
public void GlobalSetup()
{
_simulation = RobustServerSimulation
.NewSimulation()
.RegisterComponents(f => f.RegisterClass<A>())
.InitializeInstance();
_entityManager = _simulation.Resolve<IEntityManager>();
var map = _simulation.CreateMap().Uid;
var coords = new EntityCoordinates(map, default);
_compReg = _entityManager.ComponentFactory.GetRegistration(typeof(A));
for (var i = 0; i < N; i++)
{
var uid = _entityManager.SpawnEntity(null, coords);
_entityManager.AddComponent<A>(uid);
}
}
[Benchmark]
public void HasComponentGeneric()
{
for (var i = 2; i <= N+1; i++)
{
var uid = new EntityUid(i);
var result = _entityManager.HasComponent<A>(uid);
Consumer.Consume(result);
}
}
[Benchmark]
public void HasComponentCompReg()
{
for (var i = 2; i <= N+1; i++)
{
var uid = new EntityUid(i);
var result = _entityManager.HasComponent(uid, _compReg);
Consumer.Consume(result);
}
}
[Benchmark]
public void HasComponentType()
{
for (var i = 2; i <= N+1; i++)
{
var uid = new EntityUid(i);
var result = _entityManager.HasComponent(uid, typeof(A));
Consumer.Consume(result);
}
}
[Benchmark]
public void HasComponentGetType()
{
for (var i = 2; i <= N+1; i++)
{
var uid = new EntityUid(i);
var type = _dummyA.GetType();
var result = _entityManager.HasComponent(uid, type);
Consumer.Consume(result);
}
}
[ComponentProtoName("A")]
public sealed partial class A : Component
{
}
}

View File

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

View File

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

View File

@@ -40,11 +40,7 @@ namespace Robust.Client.Animations
var keyFrame = KeyFrames[keyFrameIndex];
var audioParams = keyFrame.AudioParamsFunc.Invoke();
var audio = new SoundPathSpecifier(keyFrame.Resource)
{
Params = audioParams
};
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(audio, Filter.Local(), entity, true);
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayEntity(keyFrame.Specifier, Filter.Local(), entity, true, audioParams);
}
return (keyFrameIndex, playingTime);
@@ -55,7 +51,7 @@ namespace Robust.Client.Animations
/// <summary>
/// The RSI state to play when this keyframe gets triggered.
/// </summary>
public readonly string Resource;
public readonly ResolvedSoundSpecifier Specifier;
/// <summary>
/// A function that returns the audio parameter to be used.
@@ -69,9 +65,9 @@ namespace Robust.Client.Animations
/// </summary>
public readonly float KeyTime;
public KeyFrame(string resource, float keyTime, Func<AudioParams>? audioParams = null)
public KeyFrame(ResolvedSoundSpecifier specifier, float keyTime, Func<AudioParams>? audioParams = null)
{
Resource = resource;
Specifier = specifier;
KeyTime = keyTime;
AudioParamsFunc = audioParams ?? (() => AudioParams.Default);
}

View File

@@ -415,6 +415,16 @@ public sealed partial class AudioSystem : SharedAudioSystem
return occlusion;
}
private bool TryGetAudio(ResolvedSoundSpecifier specifier, [NotNullWhen(true)] out AudioResource? audio)
{
var filename = GetAudioPath(specifier);
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
return true;
Log.Error($"Server tried to play audio file {filename} which does not exist.");
return false;
}
private bool TryGetAudio(string filename, [NotNullWhen(true)] out AudioResource? audio)
{
if (_resourceCache.TryGetResource(new ResPath(filename), out audio))
@@ -433,15 +443,15 @@ public sealed partial class AudioSystem : SharedAudioSystem
return false;
}
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityCoordinates coordinates,
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
return PlayStatic(filename, Filter.Local(), coordinates, true, audioParams);
return PlayStatic(specifier, Filter.Local(), coordinates, true, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, Filter.Local(), uid, true, audioParams);
return PlayEntity(specifier, Filter.Local(), uid, true, audioParams);
}
/// <inheritdoc />
@@ -477,21 +487,21 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, AudioParams? audioParams = null, bool recordReplay = true)
private (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioGlobalMessage
{
FileName = filename,
Specifier = specifier,
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayGlobal(audio, audioParams) : default;
return TryGetAudio(specifier, out var audio) ? PlayGlobal(audio, specifier, audioParams) : default;
}
/// <summary>
@@ -499,9 +509,9 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
/// <param name="stream">The audio stream to play.</param>
/// <param name="audioParams"></param>
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, AudioParams? audioParams = null)
public (EntityUid Entity, AudioComponent Component)? PlayGlobal(AudioStream stream, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
{
var (entity, component) = CreateAndStartPlayingStream(audioParams, stream);
var (entity, component) = CreateAndStartPlayingStream(audioParams, specifier, stream);
component.Global = true;
component.Source.Global = true;
DirtyField(entity, component, nameof(AudioComponent.Global));
@@ -513,22 +523,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
private (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
private (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, EntityUid entity, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioEntityMessage
{
FileName = filename,
Specifier = specifier,
NetEntity = GetNetEntity(entity),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayEntity(audio, entity, audioParams) : default;
return TryGetAudio(specifier, out var audio) ? PlayEntity(audio, entity, specifier, audioParams) : default;
}
/// <summary>
@@ -537,7 +547,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="stream">The audio stream to play.</param>
/// <param name="entity">The entity "emitting" the audio.</param>
/// <param name="audioParams"></param>
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, AudioParams? audioParams = null)
public (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(entity))
{
@@ -545,7 +555,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
return playing;
@@ -557,22 +567,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
private (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates, AudioParams? audioParams = null, bool recordReplay = true)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (recordReplay && _replayRecording.IsRecording)
{
_replayRecording.RecordReplayMessage(new PlayAudioPositionalMessage
{
FileName = filename,
Specifier = specifier,
Coordinates = GetNetCoordinates(coordinates),
AudioParams = audioParams ?? AudioParams.Default
});
}
return TryGetAudio(filename, out var audio) ? PlayStatic(audio, coordinates, audioParams) : default;
return TryGetAudio(specifier, out var audio) ? PlayStatic(audio, coordinates, specifier, audioParams) : default;
}
/// <summary>
@@ -581,7 +591,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="stream">The audio stream to play.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
/// <param name="audioParams"></param>
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, AudioParams? audioParams = null)
public (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, ResolvedSoundSpecifier? specifier, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
@@ -589,33 +599,33 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
_xformSys.SetCoordinates(playing.Entity, coordinates);
return playing;
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
return PlayGlobal(specifier, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null)
{
return PlayEntity(filename, entity, audioParams);
return PlayEntity(specifier, entity, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
return PlayStatic(specifier, coordinates, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
return PlayGlobal(specifier, audioParams);
}
public override void LoadStream<T>(Entity<AudioComponent> entity, T stream)
@@ -629,39 +639,39 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, EntityUid recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, audioParams);
return PlayGlobal(specifier, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
return PlayEntity(specifier, uid, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, uid, audioParams);
return PlayEntity(specifier, uid, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
return PlayStatic(specifier, coordinates, audioParams);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, coordinates, audioParams);
return PlayStatic(specifier, coordinates, audioParams);
}
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, AudioStream stream)
private (EntityUid Entity, AudioComponent Component) CreateAndStartPlayingStream(AudioParams? audioParams, ResolvedSoundSpecifier? specifier, AudioStream stream)
{
var audioP = audioParams ?? AudioParams.Default;
var entity = SetupAudio(null, audioP, initialize: false, length: stream.Length);
var entity = SetupAudio(specifier, audioP, initialize: false, length: stream.Length);
LoadStream(entity, stream);
EntityManager.InitializeAndStartEntity(entity);
var comp = entity.Comp;
@@ -694,17 +704,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
private void OnEntityCoordinates(PlayAudioPositionalMessage ev)
{
PlayStatic(ev.FileName, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
PlayStatic(ev.Specifier, GetCoordinates(ev.Coordinates), ev.AudioParams, false);
}
private void OnEntityAudio(PlayAudioEntityMessage ev)
{
PlayEntity(ev.FileName, GetEntity(ev.NetEntity), ev.AudioParams, false);
PlayEntity(ev.Specifier, GetEntity(ev.NetEntity), ev.AudioParams, false);
}
private void OnGlobalAudio(PlayAudioGlobalMessage ev)
{
PlayGlobal(ev.FileName, ev.AudioParams, false);
PlayGlobal(ev.Specifier, ev.AudioParams, false);
}
protected override TimeSpan GetAudioLengthImpl(string filename)

View File

@@ -88,10 +88,10 @@ namespace Robust.Client
{
if (GameInfo != null)
{
GameInfo.TickRate = (byte) tickrate;
GameInfo.TickRate = (ushort) tickrate;
}
_timing.SetTickRateAt((byte) tickrate, info.TickChanged);
_timing.SetTickRateAt((ushort) tickrate, info.TickChanged);
_logger.Info($"Tickrate changed to: {tickrate} on tick {_timing.CurTick}");
}
@@ -115,10 +115,6 @@ namespace Robust.Client
/// <inheritdoc />
public void DisconnectFromServer(string reason)
{
DebugTools.Assert(RunLevel > ClientRunLevel.Initialize);
DebugTools.Assert(_net.IsConnected);
// run level changed in OnNetDisconnect()
// are both of these *really* needed?
_net.ClientDisconnect(reason);
}
@@ -395,6 +391,6 @@ namespace Robust.Client
/// </summary>
public int ServerMaxPlayers { get; set; }
public byte TickRate { get; internal set; }
public uint TickRate { get; internal set; }
}
}

View File

@@ -382,7 +382,7 @@ namespace Robust.Client
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)

View File

@@ -101,11 +101,33 @@ namespace Robust.Client.GameObjects
/// <inheritdoc />
public override void Dirty<T>(Entity<T> ent, MetaDataComponent? meta = null)
{
// Client only dirties during prediction
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.Dirty(ent, meta);
}
public override void DirtyField<T>(EntityUid uid, T comp, string fieldName, MetaDataComponent? metadata = null)
{
// TODO Prediction
// does the client actually need to dirty the field?
// I.e., can't it just dirty the whole component to trigger a reset?
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.DirtyField(uid, comp, fieldName, metadata);
}
public override void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params ReadOnlySpan<string> fields)
{
// TODO Prediction
// does the client actually need to dirty the field?
// I.e., can't it just dirty the whole component to trigger a reset?
// Client only dirties during prediction
if (_gameTiming.InPrediction)
base.DirtyFields(uid, comp, meta, fields);
}
/// <inheritdoc />
public override void Dirty<T1, T2>(Entity<T1, T2> ent, MetaDataComponent? meta = null)
{

View File

@@ -26,6 +26,8 @@ namespace Robust.Client.GameObjects
protected override void OnAppearanceGetState(EntityUid uid, AppearanceComponent component, ref ComponentGetState args)
{
// TODO Game State
// Force the client to serialize & de-serialize implicitly generated component states.
var clone = CloneAppearanceData(component.AppearanceData);
args.State = new AppearanceComponentState(clone);
}

View File

@@ -17,7 +17,7 @@ public sealed class MapSystem : SharedMapSystem
{
// Client-side map entities use negative map Ids to avoid conflict with server-side maps.
var id = new MapId(--LastMapId);
while (MapManager.MapExists(id))
while (MapExists(id) || UsedIds.Contains(id))
{
id = new MapId(--LastMapId);
}

View File

@@ -1,10 +1,18 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.UserInterface;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
{
private Dictionary<EntityUid, Dictionary<Enum, Vector2>> _savedPositions = new();
private Dictionary<BoundUserInterface, Control> _registeredControls = new();
public override void Initialize()
{
base.Initialize();
@@ -17,6 +25,59 @@ public sealed class UserInterfaceSystem : SharedUserInterfaceSystem
ProtoManager.PrototypesReloaded -= OnProtoReload;
}
protected override void OnUserInterfaceShutdown(Entity<UserInterfaceComponent> ent, ref ComponentShutdown args)
{
base.OnUserInterfaceShutdown(ent, ref args);
_savedPositions.Remove(ent.Owner);
}
/// <inheritdoc />
public override void OpenUi(Entity<UserInterfaceComponent?> entity, Enum key, bool predicted = false)
{
var player = Player.LocalEntity;
if (player == null)
return;
OpenUi(entity, key, player.Value, predicted);
}
protected override void SavePosition(BoundUserInterface bui)
{
if (!_registeredControls.Remove(bui, out var control))
return;
var keyed = _savedPositions[bui.Owner];
keyed[bui.UiKey] = control.Position;
}
/// <summary>
/// Registers a control so it will later have its position stored by <see cref="SavePosition"/> when the BUI is closed.
/// </summary>
public void RegisterControl(BoundUserInterface bui, Control control)
{
DebugTools.Assert(!_registeredControls.ContainsKey(bui));
_registeredControls[bui] = control;
_savedPositions.GetOrNew(bui.Owner);
}
public override bool TryGetPosition(Entity<UserInterfaceComponent?> entity, Enum key, out Vector2 position)
{
position = default;
if (!_savedPositions.TryGetValue(entity.Owner, out var keyed))
{
return false;
}
if (!keyed.TryGetValue(key, out position))
{
return false;
}
return true;
}
private void OnProtoReload(PrototypesReloadedEventArgs obj)
{
var player = Player.LocalEntity;

View File

@@ -0,0 +1,8 @@
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects;
public sealed class VisibilitySystem : SharedVisibilitySystem
{
}

View File

@@ -23,7 +23,6 @@ using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Profiling;
@@ -42,13 +41,13 @@ namespace Robust.Client.GameStates
private uint _nextInputCmdSeq = 1;
private readonly Queue<FullInputCmdMessage> _pendingInputs = new();
private readonly Queue<(uint sequence, GameTick sourceTick, EntityEventArgs msg, object sessionMsg)>
private readonly Queue<(uint sequence, GameTick sourceTick, object msg, object sessionMsg)>
_pendingSystemMessages
= new();
// Game state dictionaries that get used every tick.
private readonly Dictionary<EntityUid, (NetEntity NetEntity, MetaDataComponent Meta, bool EnteringPvs, GameTick LastApplied, EntityState? curState, EntityState? nextState)> _toApply = new();
private readonly Dictionary<NetEntity, EntityState> _toCreate = new();
private readonly Dictionary<EntityUid, StateData> _toApply = new();
private StateData[] _toApplySorted = default!;
private readonly Dictionary<ushort, (IComponent Component, IComponentState? curState, IComponentState? nextState)> _compStateWork = new();
private readonly Dictionary<EntityUid, HashSet<Type>> _pendingReapplyNetStates = new();
private readonly HashSet<NetEntity> _stateEnts = new();
@@ -56,15 +55,29 @@ namespace Robust.Client.GameStates
private readonly List<IComponent> _toRemove = new();
private readonly Dictionary<NetEntity, Dictionary<ushort, IComponentState?>> _outputData = new();
private readonly List<(EntityUid, TransformComponent)> _queuedBroadphaseUpdates = new();
private readonly HashSet<EntityUid> _sorted = new();
private readonly List<NetEntity> _created = new();
private readonly List<NetEntity> _detached = new();
private readonly record struct StateData(
EntityUid Uid,
NetEntity NetEntity,
MetaDataComponent Meta,
bool Created,
bool EnteringPvs,
GameTick LastApplied,
EntityState? CurState,
EntityState? NextState,
HashSet<Type>? PendingReapply);
private readonly ObjectPool<Dictionary<ushort, IComponentState?>> _compDataPool =
new DefaultObjectPool<Dictionary<ushort, IComponentState?>>(new DictPolicy<ushort, IComponentState?>(), 256);
private uint _metaCompNetId;
private uint _xformCompNetId;
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
[Dependency] private readonly IComponentFactory _compFactory = default!;
[Dependency] private readonly IClientEntityManagerInternal _entities = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IClientNetManager _network = default!;
[Dependency] private readonly IBaseClient _client = default!;
@@ -72,7 +85,7 @@ namespace Robust.Client.GameStates
[Dependency] private readonly INetConfigurationManager _config = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly ClientEntityManager _entityManager = default!;
[Dependency] private readonly ClientEntityManager _entities = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly ProfManager _prof = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
@@ -126,7 +139,6 @@ namespace Robust.Client.GameStates
private bool _resettingPredictedEntities;
private readonly List<EntityUid> _brokenEnts = new();
private readonly List<(EntityUid, NetEntity)> _toStart = new();
/// <inheritdoc />
public void Initialize()
@@ -172,6 +184,12 @@ namespace Robust.Client.GameStates
throw new InvalidOperationException("MetaDataComponent does not have a NetId.");
_metaCompNetId = metaId.Value;
var xformId = _compFactory.GetRegistration(typeof(TransformComponent)).NetID;
if (!xformId.HasValue)
throw new InvalidOperationException("TransformComponent does not have a NetId.");
_xformCompNetId = xformId.Value;
}
private void OnComponentAdded(AddedComponentEventArgs args)
@@ -183,11 +201,11 @@ namespace Robust.Client.GameStates
if (comp.NetID == null)
return;
if (_entityManager.IsClientSide(args.BaseArgs.Owner))
if (_entities.IsClientSide(args.BaseArgs.Owner))
return;
_sawmill.Error($"""
Added component {comp.Name} to entity {_entityManager.ToPrettyString(args.BaseArgs.Owner)} while resetting predicted entities.
Added component {comp.Name} to entity {_entities.ToPrettyString(args.BaseArgs.Owner)} while resetting predicted entities.
Stack trace:
{Environment.StackTrace}
""");
@@ -385,7 +403,7 @@ namespace Robust.Client.GameStates
try
{
#endif
createdEntities = ApplyGameState(curState, nextState);
ApplyGameState(curState, nextState);
#if EXCEPTION_TOLERANCE
}
catch (MissingMetadataException e)
@@ -399,7 +417,7 @@ namespace Robust.Client.GameStates
using (_prof.Group("MergeImplicitData"))
{
MergeImplicitData(createdEntities);
MergeImplicitData();
}
if (_lastProcessedInput < curState.LastProcessedInput)
@@ -456,7 +474,7 @@ namespace Robust.Client.GameStates
using (_prof.Group("Tick"))
{
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: !IsPredictionEnabled);
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: !IsPredictionEnabled, histogram: null);
}
}
@@ -504,9 +522,7 @@ namespace Robust.Client.GameStates
while (hasPendingMessage && pendingMessagesEnumerator.Current.sourceTick <= _timing.CurTick)
{
var msg = pendingMessagesEnumerator.Current.msg;
_entities.EventBus.RaiseEvent(EventSource.Local, msg);
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.msg);
_entities.EventBus.RaiseEvent(EventSource.Local, pendingMessagesEnumerator.Current.sessionMsg);
hasPendingMessage = pendingMessagesEnumerator.MoveNext();
}
@@ -545,7 +561,7 @@ namespace Robust.Client.GameStates
PredictionNeedsResetting = false;
var countReset = 0;
var system = _entitySystemManager.GetEntitySystem<ClientDirtySystem>();
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
var metaQuery = _entities.GetEntityQuery<MetaDataComponent>();
RemQueue<IComponent> toRemove = new();
foreach (var entity in system.DirtyEntities)
@@ -632,7 +648,7 @@ namespace Robust.Client.GameStates
if (!last.TryGetValue(netId, out var state))
continue;
var comp = _entityManager.AddComponent(entity, netId, meta);
var comp = _entities.AddComponent(entity, netId, meta);
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was removed: {comp.GetType()}");
@@ -652,7 +668,7 @@ namespace Robust.Client.GameStates
meta.EntityLastModifiedTick = _timing.LastRealTick;
}
_entityManager.System<PhysicsSystem>().ResetContacts();
_entities.System<PhysicsSystem>().ResetContacts();
// TODO maybe reset more of physics?
// E.g., warm impulses for warm starting?
@@ -671,21 +687,21 @@ namespace Robust.Client.GameStates
/// initial server state for any newly created entity. It does this by simply using the standard <see
/// cref="IEntityManager.GetComponentState"/>.
/// </remarks>
private void MergeImplicitData(IEnumerable<NetEntity> createdEntities)
public void MergeImplicitData()
{
var bus = _entityManager.EventBus;
var bus = _entities.EventBus;
foreach (var netEntity in createdEntities)
foreach (var netEntity in _created)
{
#if EXCEPTION_TOLERANCE
if (!_entityManager.TryGetEntityData(netEntity, out _, out var meta))
if (!_entities.TryGetEntityData(netEntity, out _, out var meta))
{
_sawmill.Error($"Encountered deleted entity while merging implicit data! NetEntity: {netEntity}");
#if !EXCEPTION_TOLERANCE
throw new KeyNotFoundException();
#endif
continue;
}
#else
var (_, meta) = _entityManager.GetEntityData(netEntity);
#endif
var compData = _compDataPool.Get();
_outputData.Add(netEntity, compData);
@@ -694,12 +710,13 @@ namespace Robust.Client.GameStates
{
DebugTools.Assert(component.NetSyncEnabled);
var state = _entityManager.GetComponentState(bus, component, null, GameTick.Zero);
var state = _entities.GetComponentState(bus, component, null, GameTick.Zero);
DebugTools.Assert(state is not IComponentDeltaState);
compData.Add(netId, state);
}
}
_created.Clear();
_processor.MergeImplicitData(_outputData);
foreach (var data in _outputData.Values)
@@ -735,10 +752,9 @@ namespace Robust.Client.GameStates
_config.TickProcessMessages();
}
(IEnumerable<NetEntity> Created, List<NetEntity> Detached) output;
using (_prof.Group("Entity"))
{
output = ApplyEntityStates(curState, nextState);
ApplyEntityStates(curState, nextState);
}
using (_prof.Group("Player"))
@@ -748,13 +764,13 @@ namespace Robust.Client.GameStates
using (_prof.Group("Callback"))
{
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState, output.Detached));
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState, _detached));
}
return output.Created;
return _created;
}
private (IEnumerable<NetEntity> Created, List<NetEntity> Detached) ApplyEntityStates(GameState curState, GameState? nextState)
private void ApplyEntityStates(GameState curState, GameState? nextState)
{
var metas = _entities.GetEntityQuery<MetaDataComponent>();
var xforms = _entities.GetEntityQuery<TransformComponent>();
@@ -762,90 +778,74 @@ namespace Robust.Client.GameStates
var enteringPvs = 0;
_toApply.Clear();
_toCreate.Clear();
_created.Clear();
_pendingReapplyNetStates.Clear();
var curSpan = curState.EntityStates.Span;
// Create new entities
// This is done BEFORE state application to ensure any new parents exist before existing children have their states applied, otherwise, we may have issues with entity transforms!
{
using var _ = _prof.Group("Create uninitialized entities");
var count = 0;
using (_prof.Group("Create uninitialized entities"))
{
var created = 0;
foreach (var es in curSpan)
{
if (_entityManager.TryGetEntity(es.NetEntity, out var nUid))
if (_entities.TryGetEntity(es.NetEntity, out var nUid))
{
DebugTools.Assert(_entityManager.EntityExists(nUid));
DebugTools.Assert(_entities.EntityExists(nUid));
continue;
}
count++;
var metaState = (MetaDataComponentState?)es.ComponentChanges.Value?.FirstOrDefault(c => c.NetID == _metaCompNetId).State;
if (metaState == null)
throw new MissingMetadataException(es.NetEntity);
var uid = _entities.CreateEntity(metaState.PrototypeId, out var newMeta);
_toCreate.Add(es.NetEntity, es);
_toApply.Add(uid, (es.NetEntity, newMeta, false, GameTick.Zero, es, null));
// Client creates a client-side net entity for the newly created entity.
// We need to clear this mapping before assigning the real net id.
// TODO NetEntity Jank: prevent the client from creating this in the first place.
_entityManager.ClearNetEntity(newMeta.NetEntity);
_entityManager.SetNetEntity(uid, es.NetEntity, newMeta);
newMeta.LastStateApplied = curState.ToSequence;
// Check if there's any component states awaiting this entity.
if (_entityManager.PendingNetEntityStates.Remove(es.NetEntity, out var value))
{
foreach (var (type, owner) in value)
{
var pending = _pendingReapplyNetStates.GetOrNew(owner);
pending.Add(type);
}
}
created++;
CreateNewEntity(es, curState.ToSequence);
}
_prof.WriteValue("Count", ProfData.Int32(count));
_prof.WriteValue("Count", ProfData.Int32(created));
}
// Add entity entities that aren't new to _toCreate.
// In the process, we also check if these entities are re-entering PVS range.
foreach (var es in curSpan)
{
if (_toCreate.ContainsKey(es.NetEntity))
if (!_entities.TryGetEntityData(es.NetEntity, out var uid, out var meta))
continue;
if (!_entityManager.TryGetEntityData(es.NetEntity, out var uid, out var meta))
continue;
bool isEnteringPvs = (meta.Flags & MetaDataFlags.Detached) != 0;
var isEnteringPvs = (meta.Flags & MetaDataFlags.Detached) != 0;
if (isEnteringPvs)
{
// _toApply already contains newly created entities, but these should never be "entering PVS"
DebugTools.Assert(!_toApply.ContainsKey(uid.Value));
meta.Flags &= ~MetaDataFlags.Detached;
enteringPvs++;
}
else if (meta.LastStateApplied >= es.EntityLastModified && meta.LastStateApplied != GameTick.Zero)
{
// _toApply already contains newly created entities, but for those this set should have no effect
DebugTools.Assert(!_toApply.ContainsKey(uid.Value) || meta.LastStateApplied == curState.ToSequence);
meta.LastStateApplied = curState.ToSequence;
continue;
}
_toApply.Add(uid.Value, (es.NetEntity, meta, isEnteringPvs, meta.LastStateApplied, es, null));
// Any newly created entities already added to _toApply should've already been caught by the previous continue
DebugTools.Assert(!_toApply.ContainsKey(uid.Value));
_toApply.Add(uid.Value, new(uid.Value, es.NetEntity, meta, false, isEnteringPvs, meta.LastStateApplied, es, null, null));
meta.LastStateApplied = curState.ToSequence;
}
// Detach entities to null space
var containerSys = _entitySystemManager.GetEntitySystem<ContainerSystem>();
var lookupSys = _entitySystemManager.GetEntitySystem<EntityLookupSystem>();
var detached = ProcessPvsDeparture(curState.ToSequence, metas, xforms, xformSys, containerSys, lookupSys);
ProcessPvsDeparture(curState.ToSequence, metas, xforms, xformSys, containerSys, lookupSys);
// Check next state (AFTER having created new entities introduced in curstate)
if (nextState != null)
{
foreach (var es in nextState.EntityStates.Span)
{
if (!_entityManager.TryGetEntityData(es.NetEntity, out var uid, out var meta))
if (!_entities.TryGetEntityData(es.NetEntity, out var uid, out var meta))
continue;
// Does the next state actually have any future information about this entity that could be used for interpolation?
@@ -854,15 +854,14 @@ namespace Robust.Client.GameStates
ref var state = ref CollectionsMarshal.GetValueRefOrAddDefault(_toApply, uid.Value, out var exists);
if (exists)
state = (es.NetEntity, meta, state.EnteringPvs, state.LastApplied, state.curState, es);
else
state = (es.NetEntity, meta, false, GameTick.Zero, null, es);
state = exists
? state with {NextState = es}
: new(uid.Value, es.NetEntity, meta, false, false, GameTick.Zero, null, es, null);
}
}
// Check pending states and see if we need to force any entities to re-run component states.
foreach (var uid in _pendingReapplyNetStates.Keys)
foreach (var (uid, pending) in _pendingReapplyNetStates)
{
// Original entity referencing the NetEntity may have been deleted.
if (!metas.TryGetComponent(uid, out var meta))
@@ -879,51 +878,30 @@ namespace Robust.Client.GameStates
DebugTools.Assert(!curState.EntityDeletions.Value.Contains(meta.NetEntity));
// State already being re-applied so don't bulldoze it.
ref var state = ref CollectionsMarshal.GetValueRefOrAddDefault(_toApply, uid, out var exists);
if (exists)
continue;
state = (meta.NetEntity, meta, false, GameTick.Zero, null, null);
state = exists
? state with {PendingReapply = pending}
: new(uid, meta.NetEntity, meta, false, false, GameTick.Zero, null, null, pending);
}
_queuedBroadphaseUpdates.Clear();
using (_prof.Group("Sort States"))
{
SortStates(_toApply);
}
// Apply entity states.
using (_prof.Group("Apply States"))
{
foreach (var (entity, data) in _toApply)
var span = _toApplySorted.AsSpan(0, _toApply.Count);
foreach (ref var data in span)
{
#if EXCEPTION_TOLERANCE
try
{
#endif
HandleEntityState(entity, data.NetEntity, data.Meta, _entities.EventBus, data.curState,
data.nextState, data.LastApplied, curState.ToSequence, data.EnteringPvs);
#if EXCEPTION_TOLERANCE
}
catch (Exception e)
{
_sawmill.Error($"Caught exception while applying entity state. Entity: {_entities.ToPrettyString(entity)}. Exception: {e}");
_entityManager.DeleteEntity(entity);
RequestFullState();
continue;
}
#endif
if (!data.EnteringPvs)
continue;
// Now that things like collision data, fixtures, and positions have been updated, we queue a
// broadphase update. However, if this entity is parented to some other entity also re-entering PVS,
// we only need to update it's parent (as it recursively updates children anyways).
var xform = xforms.GetComponent(entity);
DebugTools.Assert(xform.Broadphase == BroadphaseData.Invalid);
xform.Broadphase = null;
if (!_toApply.TryGetValue(xform.ParentUid, out var parent) || !parent.EnteringPvs)
_queuedBroadphaseUpdates.Add((entity, xform));
ApplyEntState(data, curState.ToSequence);
}
Array.Clear(_toApplySorted, 0, _toApply.Count);
_prof.WriteValue("Count", ProfData.Int32(_toApply.Count));
}
@@ -958,14 +936,166 @@ namespace Robust.Client.GameStates
}
}
// Initialize and start the newly created entities.
if (_toCreate.Count > 0)
InitializeAndStart(_toCreate, metas, xforms);
// Delete any entities that failed to properly initialize/start
foreach (var entity in _brokenEnts)
{
_entities.DeleteEntity(entity);
}
_brokenEnts.Clear();
_prof.WriteValue("State Size", ProfData.Int32(curSpan.Length));
_prof.WriteValue("Entered PVS", ProfData.Int32(enteringPvs));
}
return (_toCreate.Keys, detached);
private void ApplyEntState(in StateData data, GameTick toTick)
{
try
{
HandleEntityState(data, _entities.EventBus, toTick);
}
catch (Exception e)
{
_sawmill.Error($"Caught exception while applying entity state. Entity: {_entities.ToPrettyString(data.Uid)}. Exception: {e}");
_brokenEnts.Add(data.Uid);
RequestFullState();
#if !EXCEPTION_TOLERANCE
throw;
#endif
return;
}
if (data.Created)
{
try
{
_entities.InitializeEntity(data.Uid, data.Meta);
_entities.StartEntity(data.Uid);
}
catch (Exception e)
{
_sawmill.Error(
$"Caught exception while initializing or starting entity: {_entities.ToPrettyString(data.Uid)}. Exception: {e}");
_brokenEnts.Add(data.Uid);
RequestFullState();
#if !EXCEPTION_TOLERANCE
throw;
#endif
return;
}
}
if (!data.EnteringPvs)
return;
// Now that things like collision data, fixtures, and positions have been updated, we queue a
// broadphase update. However, if this entity is parented to some other entity also re-entering PVS,
// we only need to update it's parent (as it recursively updates children anyways).
var xform = _entities.TransformQuery.Comp(data.Uid);
DebugTools.Assert(xform.Broadphase == BroadphaseData.Invalid);
xform.Broadphase = null;
if (!_toApply.TryGetValue(xform.ParentUid, out var parent) || !parent.EnteringPvs)
_queuedBroadphaseUpdates.Add((data.Uid, xform));
}
private void CreateNewEntity(EntityState state, GameTick toTick)
{
// TODO GAME STATE
// store MetaData & Transform information separately.
var metaState =
(MetaDataComponentState?) state.ComponentChanges.Value?.FirstOrDefault(c => c.NetID == _metaCompNetId)
.State;
if (metaState == null)
throw new MissingMetadataException(state.NetEntity);
var uid = _entities.CreateEntity(metaState.PrototypeId, out var newMeta);
_toApply.Add(uid, new(uid, state.NetEntity, newMeta, true, false, GameTick.Zero, state, null, null));
_created.Add(state.NetEntity);
// Client creates a client-side net entity for the newly created entity.
// We need to clear this mapping before assigning the real net id.
// TODO NetEntity Jank: prevent the client from creating this in the first place.
_entities.ClearNetEntity(newMeta.NetEntity);
_entities.SetNetEntity(uid, state.NetEntity, newMeta);
newMeta.LastStateApplied = toTick;
// Check if there's any component states awaiting this entity.
if (!_entities.PendingNetEntityStates.Remove(state.NetEntity, out var value))
return;
foreach (var (type, owner) in value)
{
var pending = _pendingReapplyNetStates.GetOrNew(owner);
pending.Add(type);
}
}
/// <summary>
/// Sort states to ensure that we always apply states, initialize, and start parent entities before any of their
/// children.
/// </summary>
private void SortStates(Dictionary<EntityUid, StateData> toApply)
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (_toApplySorted == null || _toApplySorted.Length < toApply.Count)
Array.Resize(ref _toApplySorted, toApply.Count);
_sorted.Clear();
var i = 0;
foreach (var (ent, data) in toApply)
{
AddToSorted(ent, data, ref i);
}
DebugTools.AssertEqual(i, toApply.Count);
}
private void AddToSorted(EntityUid ent, in StateData data, ref int i)
{
if (!_sorted.Add(ent))
return;
EnsureParentsSorted(ent, data, ref i);
_toApplySorted[i++] = data;
}
private void EnsureParentsSorted(EntityUid ent, in StateData data, ref int i)
{
var parent = GetStateParent(ent, data);
while (parent != EntityUid.Invalid)
{
if (_toApply.TryGetValue(parent, out var parentData))
{
AddToSorted(parent, parentData, ref i);
// The above method will handle the rest of the transform hierarchy, so we can just return early.
return;
}
parent = _entities.TransformQuery.GetComponent(parent).ParentUid;
}
}
/// <summary>
/// Get the entity's parent in the game state that is being applies. I.e., if the state contains a new
/// transform state, get the parent from that. Otherwise, return the entity's current parent.
/// </summary>
private EntityUid GetStateParent(EntityUid uid, in StateData data)
{
// TODO GAME STATE
// store MetaData & Transform information separately.
if (data.CurState != null
&& data.CurState.ComponentChanges.Value
.TryFirstOrNull(c => c.NetID == _xformCompNetId, out var found))
{
var state = (TransformComponentState) found.Value.State!;
return _entities.GetEntity(state.ParentID);
}
return _entities.TransformQuery.GetComponent(uid).ParentUid;
}
/// <inheritdoc />
@@ -1000,7 +1130,7 @@ namespace Robust.Client.GameStates
_toDelete.Clear();
// Client side entities won't need the transform, but that should always be a tiny minority of entities
var metaQuery = _entityManager.AllEntityQueryEnumerator<MetaDataComponent, TransformComponent>();
var metaQuery = _entities.AllEntityQueryEnumerator<MetaDataComponent, TransformComponent>();
while (metaQuery.MoveNext(out var ent, out var metadata, out var xform))
{
@@ -1067,9 +1197,9 @@ namespace Robust.Client.GameStates
foreach (var netEntity in delSpan)
{
// Don't worry about this for later.
_entityManager.PendingNetEntityStates.Remove(netEntity);
_entities.PendingNetEntityStates.Remove(netEntity);
if (!_entityManager.TryGetEntity(netEntity, out var id))
if (!_entities.TryGetEntity(netEntity, out var id))
continue;
if (!xforms.TryGetComponent(id, out var xform))
@@ -1099,9 +1229,10 @@ namespace Robust.Client.GameStates
var containerSys = _entitySystemManager.GetEntitySystem<ContainerSystem>();
var lookupSys = _entitySystemManager.GetEntitySystem<EntityLookupSystem>();
Detach(GameTick.MaxValue, null, entities, metas, xforms, xformSys, containerSys, lookupSys);
_detached.Clear();
}
private List<NetEntity> ProcessPvsDeparture(
private void ProcessPvsDeparture(
GameTick toTick,
EntityQuery<MetaDataComponent> metas,
EntityQuery<TransformComponent> xforms,
@@ -1110,25 +1241,23 @@ namespace Robust.Client.GameStates
EntityLookupSystem lookupSys)
{
var toDetach = _processor.GetEntitiesToDetach(toTick, _pvsDetachBudget);
var detached = new List<NetEntity>();
if (toDetach.Count == 0)
return detached;
return;
// TODO optimize
// If an entity is leaving PVS, so are all of its children. If we can preserve the hierarchy we can avoid
// things like container insertion and ejection.
using var _ = _prof.Group("Leave PVS");
detached.EnsureCapacity(toDetach.Count);
_detached.Clear();
foreach (var (tick, ents) in toDetach)
{
Detach(tick, toTick, ents, metas, xforms, xformSys, containerSys, lookupSys, detached);
Detach(tick, toTick, ents, metas, xforms, xformSys, containerSys, lookupSys);
}
_prof.WriteValue("Count", ProfData.Int32(detached.Count));
return detached;
_prof.WriteValue("Count", ProfData.Int32(_detached.Count));
}
private void Detach(GameTick maxTick,
@@ -1138,12 +1267,11 @@ namespace Robust.Client.GameStates
EntityQuery<TransformComponent> xforms,
SharedTransformSystem xformSys,
ContainerSystem containerSys,
EntityLookupSystem lookupSys,
List<NetEntity>? detached = null)
EntityLookupSystem lookupSys)
{
foreach (var netEntity in entities)
{
if (!_entityManager.TryGetEntityData(netEntity, out var ent, out var meta))
if (!_entities.TryGetEntityData(netEntity, out var ent, out var meta))
continue;
if (meta.LastStateApplied > maxTick)
@@ -1184,159 +1312,75 @@ namespace Robust.Client.GameStates
containerSys.AddExpectedEntity(netEntity, container);
}
detached?.Add(netEntity);
_detached.Add(netEntity);
}
}
private void InitializeAndStart(
Dictionary<NetEntity, EntityState> toCreate,
EntityQuery<MetaDataComponent> metas,
EntityQuery<TransformComponent> xforms)
{
_toStart.Clear();
using (_prof.Group("Initialize Entity"))
{
EntityUid entity = default;
foreach (var netEntity in toCreate.Keys)
{
(entity, var meta) = _entityManager.GetEntityData(netEntity);
InitializeRecursive(entity, meta, metas, xforms);
}
}
using (_prof.Group("Start Entity"))
{
foreach (var (entity, netEntity) in _toStart)
{
try
{
_entities.StartEntity(entity);
}
catch (Exception e)
{
_sawmill.Error($"Server entity threw in Start: nent={netEntity}, ent={_entityManager.ToPrettyString(entity)}");
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
_toCreate.Remove(netEntity);
_brokenEnts.Add(entity);
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
}
}
foreach (var entity in _brokenEnts)
{
_entityManager.DeleteEntity(entity);
}
_brokenEnts.Clear();
}
private void InitializeRecursive(
EntityUid entity,
MetaDataComponent meta,
EntityQuery<MetaDataComponent> metas,
EntityQuery<TransformComponent> xforms)
{
var xform = xforms.GetComponent(entity);
if (xform.ParentUid is {Valid: true} parent)
{
var parentMeta = metas.GetComponent(parent);
if (parentMeta.EntityLifeStage < EntityLifeStage.Initialized)
InitializeRecursive(parent, parentMeta, metas, xforms);
}
if (meta.EntityLifeStage >= EntityLifeStage.Initialized)
{
// Was probably already initialized because one of its children appeared earlier in the list.
DebugTools.AssertEqual(_toStart.Count(x => x.Item1 == entity), 1);
return;
}
try
{
_entities.InitializeEntity(entity, meta);
_toStart.Add((entity, meta.NetEntity));
}
catch (Exception e)
{
_sawmill.Error($"Server entity threw in Init: nent={meta.NetEntity}, ent={_entities.ToPrettyString(entity)}");
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(InitializeAndStart)}");
_toCreate.Remove(meta.NetEntity);
_brokenEnts.Add(entity);
#if !EXCEPTION_TOLERANCE
throw;
#endif
}
}
private void HandleEntityState(EntityUid uid, NetEntity netEntity, MetaDataComponent meta, IEventBus bus, EntityState? curState,
EntityState? nextState, GameTick lastApplied, GameTick toTick, bool enteringPvs)
private void HandleEntityState(in StateData data, IEventBus bus, GameTick toTick)
{
_compStateWork.Clear();
// First remove any deleted components
if (curState?.NetComponents != null)
if (data.CurState?.NetComponents is {} netComps)
{
_toRemove.Clear();
foreach (var (id, comp) in meta.NetComponents)
foreach (var (id, comp) in data.Meta.NetComponents)
{
DebugTools.Assert(comp.NetSyncEnabled);
if (!curState.NetComponents.Contains(id))
if (!netComps.Contains(id))
_toRemove.Add(comp);
}
foreach (var comp in _toRemove)
{
_entities.RemoveComponent(uid, comp, meta);
_entities.RemoveComponent(data.Uid, comp, data.Meta);
}
}
if (enteringPvs)
if (data.EnteringPvs)
{
// last-server state has already been updated with new information from curState
// --> simply reset to the most recent server state.
//
// as to why we need to reset: because in the process of detaching to null-space, we will have dirtied
// the entity. most notably, all entities will have been ejected from their containers.
foreach (var (id, state) in _processor.GetLastServerStates(netEntity))
foreach (var (id, state) in _processor.GetLastServerStates(data.NetEntity))
{
if (!meta.NetComponents.TryGetValue(id, out var comp))
if (!data.Meta.NetComponents.TryGetValue(id, out var comp))
{
comp = _compFactory.GetComponent(id);
_entityManager.AddComponent(uid, comp, true, metadata: meta);
_entities.AddComponent(data.Uid, comp, true, metadata: data.Meta);
}
_compStateWork[id] = (comp, state, null);
}
}
else if (curState != null)
else if (data.CurState != null)
{
foreach (var compChange in curState.ComponentChanges.Span)
foreach (var compChange in data.CurState.ComponentChanges.Span)
{
if (!meta.NetComponents.TryGetValue(compChange.NetID, out var comp))
if (!data.Meta.NetComponents.TryGetValue(compChange.NetID, out var comp))
{
comp = _compFactory.GetComponent(compChange.NetID);
_entityManager.AddComponent(uid, comp, true, metadata:meta);
_entities.AddComponent(data.Uid, comp, true, metadata: data.Meta);
}
else if (compChange.LastModifiedTick <= lastApplied && lastApplied != GameTick.Zero)
else if (compChange.LastModifiedTick <= data.LastApplied && data.LastApplied != GameTick.Zero)
continue;
_compStateWork[compChange.NetID] = (comp, compChange.State, null);
}
}
if (nextState != null)
if (data.NextState != null)
{
foreach (var compState in nextState.ComponentChanges.Span)
foreach (var compState in data.NextState.ComponentChanges.Span)
{
if (compState.LastModifiedTick != toTick + 1)
continue;
if (!meta.NetComponents.TryGetValue(compState.NetID, out var comp))
if (!data.Meta.NetComponents.TryGetValue(compState.NetID, out var comp))
{
// The component can be null here due to interp, because the NEXT state will have a new
// component, but the component does not yet exist.
@@ -1354,9 +1398,10 @@ namespace Robust.Client.GameStates
}
// If we have a NetEntity we reference come in then apply their state.
if (_pendingReapplyNetStates.TryGetValue(uid, out var reapplyTypes))
DebugTools.Assert(_pendingReapplyNetStates.ContainsKey(data.Uid) == (data.PendingReapply != null));
if (data.PendingReapply is {} reapplyTypes)
{
var lastState = _processor.GetLastServerStates(netEntity);
var lastState = _processor.GetLastServerStates(data.NetEntity);
foreach (var type in reapplyTypes)
{
@@ -1366,7 +1411,7 @@ namespace Robust.Client.GameStates
if (netId == null)
continue;
if (!meta.NetComponents.TryGetValue(netId.Value, out var comp) ||
if (!data.Meta.NetComponents.TryGetValue(netId.Value, out var comp) ||
!lastState.TryGetValue(netId.Value, out var lastCompState))
{
continue;
@@ -1388,7 +1433,7 @@ namespace Robust.Client.GameStates
continue;
var handleState = new ComponentHandleState(cur, next);
bus.RaiseComponentEvent(uid, comp, ref handleState);
bus.RaiseComponentEvent(data.Uid, comp, ref handleState);
}
}
@@ -1510,7 +1555,7 @@ namespace Robust.Client.GameStates
{
using var _ = _timing.StartStateApplicationArea();
var query = _entityManager.AllEntityQueryEnumerator<MetaDataComponent>();
var query = _entities.AllEntityQueryEnumerator<MetaDataComponent>();
while (query.MoveNext(out var uid, out var meta))
{
@@ -1536,14 +1581,14 @@ namespace Robust.Client.GameStates
if (!meta.NetComponents.TryGetValue(id, out var comp))
{
comp = _compFactory.GetComponent(id);
_entityManager.AddComponent(uid, comp, true, meta);
_entities.AddComponent(uid, comp, true, meta);
}
if (state == null)
continue;
var handleState = new ComponentHandleState(state, null);
_entityManager.EventBus.RaiseComponentEvent(uid, comp, ref handleState);
_entities.EventBus.RaiseComponentEvent(uid, comp, ref handleState);
}
// ensure we don't have any extra components

View File

@@ -82,6 +82,8 @@ namespace Robust.Client.GameStates
/// </summary>
IEnumerable<NetEntity> ApplyGameState(GameState curState, GameState? nextState);
void MergeImplicitData();
/// <summary>
/// Resets any entities that have changed while predicting future ticks.
/// </summary>

View File

@@ -0,0 +1,5 @@
using Robust.Shared.GameStates;
namespace Robust.Client.GameStates;
public sealed partial class PvsOverrideSystem : SharedPvsOverrideSystem;

View File

@@ -125,7 +125,8 @@ namespace Robust.Client.Graphics.Clyde
{
DebugTools.Assert(space != OverlaySpace.ScreenSpaceBelowWorld && space != OverlaySpace.ScreenSpace);
var args = new OverlayDrawArgs(space, null, vp, _renderHandle.DrawingHandleWorld, new UIBox2i((0, 0), vp.Size), vp.Eye!.Position.MapId, worldBox, worldBounds);
var mapId = vp.Eye!.Position.MapId;
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), _mapManager.GetMapEntityIdOrThrow(mapId), mapId, worldBox, worldBounds);
if (!overlay.BeforeDraw(args))
return;
@@ -165,7 +166,7 @@ namespace Robust.Client.Graphics.Clyde
private void RenderOverlaysDirect(
Viewport vp,
IViewportControl vpControl,
DrawingHandleBase handle,
IRenderHandle handle,
OverlaySpace space,
in UIBox2i bounds)
{
@@ -175,8 +176,9 @@ namespace Robust.Client.Graphics.Clyde
var worldBounds = CalcWorldBounds(vp);
var worldAABB = worldBounds.CalcBoundingBox();
var mapId = vp.Eye!.Position.MapId;
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, vp.Eye!.Position.MapId, worldAABB, worldBounds);
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, _mapManager.GetMapEntityIdOrThrow(mapId), mapId, worldAABB, worldBounds);
foreach (var overlay in list)
{
@@ -215,8 +217,6 @@ namespace Robust.Client.Graphics.Clyde
}
}
_overlays.Sort(OverlayComparer.Instance);
return _overlays;
}
@@ -423,12 +423,19 @@ namespace Robust.Client.Graphics.Clyde
var oldTransform = _currentMatrixModel;
var oldScissor = _currentScissorState;
var oldMatrixProj = _currentMatrixProj;
var oldMatrixView = _currentMatrixView;
var oldBoundTarget = _currentBoundRenderTarget;
var oldRenderTarget = _currentRenderTarget;
var oldShader = _queuedShaderInstance;
var oldCaps = _glCaps;
// Need to get state before flushing render queue in case they modify the original state.
var state = PushRenderStateFull();
// Have to flush the render queue so that all commands finish rendering to the previous framebuffer.
FlushRenderQueue();
var state = PushRenderStateFull();
{
BindRenderTargetFull(RtToLoaded(rt));
if (clearColor is not null)
@@ -450,8 +457,16 @@ namespace Robust.Client.Graphics.Clyde
PopRenderStateFull(state);
_updateUniformConstants(_currentRenderTarget.Size);
SetScissorFull(oldScissor);
_currentMatrixModel = oldTransform;
DebugTools.Assert(oldCaps.Equals(_glCaps));
DebugTools.Assert(_currentMatrixModel.Equals(oldTransform));
DebugTools.Assert(_currentScissorState.Equals(oldScissor));
DebugTools.Assert(_currentMatrixProj.Equals(oldMatrixProj));
DebugTools.Assert(oldMatrixView.Equals(_currentMatrixView));
DebugTools.Assert(oldRenderTarget.Equals(_currentRenderTarget));
DebugTools.Assert(oldBoundTarget.Equals(_currentBoundRenderTarget));
DebugTools.Assert(oldShader.Equals(_queuedShaderInstance));
}
private void RenderViewport(Viewport viewport)
@@ -574,17 +589,5 @@ namespace Robust.Client.Graphics.Clyde
return new Box2Rotated(aabb, rotation, aabb.Center);
}
private sealed class OverlayComparer : IComparer<Overlay>
{
public static readonly OverlayComparer Instance = new();
public int Compare(Overlay? x, Overlay? y)
{
var zX = x?.ZIndex ?? 0;
var zY = y?.ZIndex ?? 0;
return zX.CompareTo(zY);
}
}
}
}

View File

@@ -1,21 +1,19 @@
using System;
using System.Collections.Generic;
using System.Buffers;
using System.Diagnostics.Contracts;
using System.Numerics;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
using Robust.Shared.Physics;
using Robust.Client.ComponentTrees;
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using static Robust.Shared.GameObjects.OccluderComponent;
using Robust.Shared.Utility;
@@ -279,8 +277,7 @@ namespace Robust.Client.Graphics.Clyde
{
const float arbitraryDistanceMax = 1234;
GL.Disable(EnableCap.Blend);
CheckGlError();
IsBlending = false;
GL.Enable(EnableCap.DepthTest);
CheckGlError();
@@ -329,8 +326,7 @@ namespace Robust.Client.Graphics.Clyde
GL.Disable(EnableCap.DepthTest);
CheckGlError();
GL.Enable(EnableCap.Blend);
CheckGlError();
IsBlending = true;
}
private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 worldAABB, IEye eye)
@@ -394,21 +390,43 @@ namespace Robust.Client.Graphics.Clyde
FinalizeDepthDraw();
}
GL.Enable(EnableCap.StencilTest);
_isStencilling = true;
IsStencilling = true;
var (lightW, lightH) = GetLightMapSize(viewport.Size);
GL.Viewport(0, 0, lightW, lightH);
CheckGlError();
BindRenderTargetImmediate(RtToLoaded(viewport.LightRenderTarget));
DebugTools.Assert(_currentBoundRenderTarget.TextureHandle.Equals(viewport.LightRenderTarget.Texture.TextureId));
CheckGlError();
GLClearColor(_entityManager.GetComponentOrNull<MapLightComponent>(mapUid)?.AmbientLightColor ?? MapLightComponent.DefaultColor);
var clearEv = new GetClearColorEvent();
_entityManager.EventBus.RaiseEvent(EventSource.Local, ref clearEv);
var clearColor = clearEv.Color ?? GetClearColor(mapUid);
GLClearColor(clearColor);
GL.ClearStencil(0xFF);
GL.StencilMask(0xFF);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
CheckGlError();
var oldTarget = _currentRenderTarget;
var oldProj = _currentMatrixProj;
var oldShader = _queuedShaderInstance;
var oldModel = _currentMatrixModel;
var oldScissor = _currentScissorState;
var state = PushRenderStateFull();
RenderOverlays(viewport, OverlaySpace.BeforeLighting, worldAABB, worldBounds);
PopRenderStateFull(state);
DebugTools.Assert(oldScissor.Equals(_currentScissorState));
DebugTools.Assert(oldModel.Equals(_currentMatrixModel));
DebugTools.Assert(oldShader.Equals(_queuedShaderInstance));
DebugTools.Assert(oldProj.Equals(_currentMatrixProj));
DebugTools.Assert(oldTarget.Equals(_currentRenderTarget));
DebugTools.Assert(_currentBoundRenderTarget.TextureHandle.Equals(viewport.LightRenderTarget.Texture.TextureId));
ApplyLightingFovToBuffer(viewport, eye);
var lightShader = _loadedShaders[_enableSoftShadows ? _lightSoftShaderHandle : _lightHardShaderHandle]
@@ -509,13 +527,12 @@ namespace Robust.Client.Graphics.Clyde
}
ResetBlendFunc();
GL.Disable(EnableCap.StencilTest);
_isStencilling = false;
IsStencilling = false;
CheckGlError();
if (_cfg.GetCVar(CVars.LightBlur))
BlurLights(viewport, eye);
BlurRenderTarget(viewport, viewport.LightRenderTarget, viewport.LightBlurTarget, eye, 14f);
using (_prof.Group("BlurOntoWalls"))
{
@@ -531,9 +548,8 @@ namespace Robust.Client.Graphics.Clyde
GL.Viewport(0, 0, viewport.Size.X, viewport.Size.Y);
CheckGlError();
Array.Clear(_lightsToRenderList, 0, count);
_lightingReady = true;
Array.Clear(_lightsToRenderList, 0, count);
}
private static bool LightQuery(ref (
@@ -643,21 +659,33 @@ namespace Robust.Client.Graphics.Clyde
return (state.count, expandedBounds);
}
private void BlurLights(Viewport viewport, IEye eye)
/// <inheritdoc/>
[Pure]
public Color GetClearColor(EntityUid mapUid)
{
using var _ = DebugGroup(nameof(BlurLights));
return _entityManager.GetComponentOrNull<MapLightComponent>(mapUid)?.AmbientLightColor ??
MapLightComponent.DefaultColor;
}
GL.Disable(EnableCap.Blend);
CheckGlError();
/// <inheritdoc/>
public void BlurRenderTarget(IClydeViewport viewport, IRenderTarget target, IRenderTarget blurBuffer, IEye eye, float multiplier)
{
if (target is not RenderTexture rTexture || blurBuffer is not RenderTexture blurTexture)
return;
using var _ = DebugGroup(nameof(BlurRenderTarget));
var state = PushRenderStateFull();
IsBlending = false;
CalcScreenMatrices(viewport.Size, out var proj, out var view);
SetProjViewBuffer(proj, view);
var shader = _loadedShaders[_lightBlurShaderHandle].Program;
shader.Use();
SetupGlobalUniformsImmediate(shader, viewport.LightRenderTarget.Texture);
SetupGlobalUniformsImmediate(shader, rTexture.Texture);
var size = viewport.LightRenderTarget.Size;
var size = target.Size;
shader.SetUniformMaybe("size", (Vector2)size);
shader.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
@@ -667,14 +695,13 @@ namespace Robust.Client.Graphics.Clyde
// Initially we're pulling from the light render target.
// So we set it out of the loop so
// _wallBleedIntermediateRenderTarget2 gets bound at the end of the loop body.
SetTexture(TextureUnit.Texture0, viewport.LightRenderTarget.Texture);
SetTexture(TextureUnit.Texture0, rTexture.Texture);
// Have to scale the blurring radius based on viewport size and camera zoom.
const float refCameraHeight = 14;
var facBase = _cfg.GetCVar(CVars.LightBlurFactor);
var cameraSize = eye.Zoom.Y * viewport.Size.Y * (1 / viewport.RenderScale.Y) / EyeManager.PixelsPerMeter;
// 7e-3f is just a magic factor that makes it look ok.
var factor = facBase * (refCameraHeight / cameraSize);
var factor = facBase * (multiplier / cameraSize);
// Multi-iteration gaussian blur.
for (var i = 3; i > 0; i--)
@@ -683,35 +710,31 @@ namespace Robust.Client.Graphics.Clyde
// Set factor.
shader.SetUniformMaybe("radius", scale);
BindRenderTargetFull(viewport.LightBlurTarget);
BindRenderTargetImmediate(RtToLoaded(blurBuffer));
// Blur horizontally to _wallBleedIntermediateRenderTarget1.
shader.SetUniformMaybe("direction", Vector2.UnitX);
_drawQuad(Vector2.Zero, viewport.Size, Matrix3x2.Identity, shader);
SetTexture(TextureUnit.Texture0, viewport.LightBlurTarget.Texture);
SetTexture(TextureUnit.Texture0, blurTexture.Texture);
BindRenderTargetFull(viewport.LightRenderTarget);
BindRenderTargetImmediate(RtToLoaded(rTexture));
// Blur vertically to _wallBleedIntermediateRenderTarget2.
shader.SetUniformMaybe("direction", Vector2.UnitY);
_drawQuad(Vector2.Zero, viewport.Size, Matrix3x2.Identity, shader);
SetTexture(TextureUnit.Texture0, viewport.LightRenderTarget.Texture);
SetTexture(TextureUnit.Texture0, rTexture.Texture);
}
GL.Enable(EnableCap.Blend);
CheckGlError();
// We didn't trample over the old _currentMatrices so just roll it back.
SetProjViewBuffer(_currentMatrixProj, _currentMatrixView);
PopRenderStateFull(state);
}
private void BlurOntoWalls(Viewport viewport, IEye eye)
{
using var _ = DebugGroup(nameof(BlurOntoWalls));
GL.Disable(EnableCap.Blend);
CheckGlError();
IsBlending = false;
CalcScreenMatrices(viewport.Size, out var proj, out var view);
SetProjViewBuffer(proj, view);
@@ -761,8 +784,7 @@ namespace Robust.Client.Graphics.Clyde
SetTexture(TextureUnit.Texture0, viewport.WallBleedIntermediateRenderTarget2.Texture);
}
GL.Enable(EnableCap.Blend);
CheckGlError();
IsBlending = true;
// We didn't trample over the old _currentMatrices so just roll it back.
SetProjViewBuffer(_currentMatrixProj, _currentMatrixView);
}
@@ -775,8 +797,7 @@ namespace Robust.Client.Graphics.Clyde
GL.Viewport(0, 0, viewport.LightRenderTarget.Size.X, viewport.LightRenderTarget.Size.Y);
CheckGlError();
GL.Disable(EnableCap.Blend);
CheckGlError();
IsBlending = false;
var shader = _loadedShaders[_mergeWallLayerShaderHandle].Program;
shader.Use();
@@ -796,8 +817,7 @@ namespace Robust.Client.Graphics.Clyde
IntPtr.Zero);
CheckGlError();
GL.Enable(EnableCap.Blend);
CheckGlError();
IsBlending = true;
}
private void ApplyFovToBuffer(Viewport viewport, IEye eye)
@@ -827,8 +847,7 @@ namespace Robust.Client.Graphics.Clyde
FovSetTransformAndBlit(viewport, eye.Position.Position, fovShader);
GL.StencilMask(0x00);
GL.Disable(EnableCap.StencilTest);
_isStencilling = false;
IsStencilling = false;
}
private void ApplyLightingFovToBuffer(Viewport viewport, IEye eye)
@@ -1135,22 +1154,20 @@ namespace Robust.Client.Graphics.Clyde
var lightMapSize = GetLightMapSize(viewport.Size);
var lightMapSizeQuart = GetLightMapSize(viewport.Size, true);
var lightMapColorFormat = _hasGLFloatFramebuffers
? RenderTargetColorFormat.R11FG11FB10F
: RenderTargetColorFormat.Rgba8;
var lightMapSampleParameters = new TextureSampleParameters { Filter = true };
viewport.LightRenderTarget?.Dispose();
viewport.WallMaskRenderTarget?.Dispose();
viewport.WallBleedIntermediateRenderTarget1?.Dispose();
viewport.WallBleedIntermediateRenderTarget2?.Dispose();
var lightMapColorFormat = _hasGLFloatFramebuffers
? RenderTargetColorFormat.R11FG11FB10F
: RenderTargetColorFormat.Rgba8;
var lightMapSampleParameters = new TextureSampleParameters { Filter = true };
viewport.WallMaskRenderTarget = CreateRenderTarget(viewport.Size, RenderTargetColorFormat.R8,
name: $"{viewport.Name}-{nameof(viewport.WallMaskRenderTarget)}");
viewport.LightRenderTarget = CreateRenderTarget(lightMapSize,
new RenderTargetFormatParameters(lightMapColorFormat, hasDepthStencil: true),
lightMapSampleParameters,
viewport.LightRenderTarget = (RenderTexture) CreateLightRenderTarget(lightMapSize,
$"{viewport.Name}-{nameof(viewport.LightRenderTarget)}");
viewport.LightBlurTarget = CreateRenderTarget(lightMapSize,
@@ -1158,11 +1175,13 @@ namespace Robust.Client.Graphics.Clyde
lightMapSampleParameters,
$"{viewport.Name}-{nameof(viewport.LightBlurTarget)}");
viewport.WallBleedIntermediateRenderTarget1 = CreateRenderTarget(lightMapSizeQuart, lightMapColorFormat,
viewport.WallBleedIntermediateRenderTarget1 = CreateRenderTarget(lightMapSizeQuart,
new RenderTargetFormatParameters(lightMapColorFormat),
lightMapSampleParameters,
$"{viewport.Name}-{nameof(viewport.WallBleedIntermediateRenderTarget1)}");
viewport.WallBleedIntermediateRenderTarget2 = CreateRenderTarget(lightMapSizeQuart, lightMapColorFormat,
viewport.WallBleedIntermediateRenderTarget2 = CreateRenderTarget(lightMapSizeQuart,
new RenderTargetFormatParameters(lightMapColorFormat),
lightMapSampleParameters,
$"{viewport.Name}-{nameof(viewport.WallBleedIntermediateRenderTarget2)}");
}

View File

@@ -30,6 +30,20 @@ namespace Robust.Client.Graphics.Clyde
// It, like _mainWindowRenderTarget, is initialized in Clyde's constructor
private LoadedRenderTarget _currentBoundRenderTarget;
public IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true)
{
var lightMapColorFormat = _hasGLFloatFramebuffers
? RTCF.R11FG11FB10F
: RTCF.Rgba8;
var lightMapSampleParameters = new TextureSampleParameters { Filter = true };
return CreateRenderTarget(size,
new RenderTargetFormatParameters(lightMapColorFormat, hasDepthStencil: depthStencil),
lightMapSampleParameters,
name: name);
}
IRenderTexture IClyde.CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
TextureSampleParameters? sampleParameters, string? name)
{
@@ -204,7 +218,8 @@ namespace Robust.Client.Graphics.Clyde
Size = size,
TextureHandle = textureObject.TextureId,
MemoryPressure = pressure,
ColorFormat = format.ColorFormat
ColorFormat = format.ColorFormat,
SampleParameters = sampleParameters,
};
//GC.AddMemoryPressure(pressure);
@@ -251,9 +266,15 @@ namespace Robust.Client.Graphics.Clyde
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private LoadedRenderTarget RtToLoaded(RenderTargetBase rt)
private LoadedRenderTarget RtToLoaded(IRenderTarget rt)
{
return _renderTargets[rt.Handle];
switch (rt)
{
case RenderTargetBase based:
return _renderTargets[based.Handle];
default:
throw new NotImplementedException();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -302,6 +323,8 @@ namespace Robust.Client.Graphics.Clyde
// Renderbuffer handle
public GLHandle DepthStencilHandle;
public long MemoryPressure;
public TextureSampleParameters? SampleParameters;
}
private abstract class RenderTargetBase : IRenderTarget

View File

@@ -90,9 +90,61 @@ namespace Robust.Client.Graphics.Clyde
// (queue) and (misc), current state of the scissor test. Null if disabled.
private UIBox2i? _currentScissorState;
// Some simple flags that basically just tracks the current state of glEnable(GL_STENCIL/GL_SCISSOR_TEST)
private bool _isScissoring;
private bool _isStencilling;
/// <summary>
/// Tracks enabled GL capabilities for renderer state.
/// </summary>
private GLCaps _glCaps = GLCaps.None;
private bool IsStencilling
{
get => (_glCaps & GLCaps.Stencilling) == GLCaps.Stencilling;
set
{
if (value == IsStencilling)
return;
if (value)
{
_glCaps |= GLCaps.Stencilling;
GL.Enable(EnableCap.StencilTest);
}
else
{
_glCaps &= ~GLCaps.Stencilling;
GL.Disable(EnableCap.StencilTest);
}
CheckGlError();
}
}
private bool IsBlending
{
get => (_glCaps & GLCaps.Blending) == GLCaps.Blending;
set
{
if (value == IsBlending)
return;
if (value)
{
_glCaps |= GLCaps.Blending;
GL.Enable(EnableCap.Blend);
}
else
{
_glCaps &= ~GLCaps.Blending;
GL.Disable(EnableCap.Blend);
}
CheckGlError();
}
}
private bool IsScissoring
{
get => _currentScissorState != null;
}
private readonly RefList<RenderCommand> _queuedRenderCommands = new RefList<RenderCommand>();
@@ -364,16 +416,17 @@ namespace Robust.Client.Graphics.Clyde
private void SetScissorImmediate(bool enable, in UIBox2i box)
{
var oldIsScissoring = _isScissoring;
_isScissoring = enable;
if (_isScissoring)
if (enable)
{
if (!oldIsScissoring)
{
GL.Enable(EnableCap.ScissorTest);
CheckGlError();
}
GL.Enable(EnableCap.ScissorTest);
}
else
{
GL.Disable(EnableCap.ScissorTest);
}
if (enable)
{
// Don't forget to flip it, these coordinates have bottom left as origin.
// TODO: Broken when rendering to non-screen render targets.
@@ -387,11 +440,6 @@ namespace Robust.Client.Graphics.Clyde
}
CheckGlError();
}
else if (oldIsScissoring)
{
GL.Disable(EnableCap.ScissorTest);
CheckGlError();
}
}
// NOTE: sRGB IS IN LINEAR IF FRAMEBUFFER_SRGB IS ACTIVE.
@@ -420,17 +468,11 @@ namespace Robust.Client.Graphics.Clyde
var program = shader.Program;
program.Use();
IsStencilling = instance.Stencil.Enabled;
// Handle stencil parameters.
if (instance.Stencil.Enabled)
{
if (!_isStencilling)
{
GL.Enable(EnableCap.StencilTest);
CheckGlError();
_isStencilling = true;
}
GL.StencilMask(instance.Stencil.WriteMask);
CheckGlError();
GL.StencilFunc(ToGLStencilFunc(instance.Stencil.Func), instance.Stencil.Ref, instance.Stencil.ReadMask);
@@ -438,12 +480,6 @@ namespace Robust.Client.Graphics.Clyde
GL.StencilOp(TKStencilOp.Keep, TKStencilOp.Keep, ToGLStencilOp(instance.Stencil.Op));
CheckGlError();
}
else if (_isStencilling)
{
GL.Disable(EnableCap.StencilTest);
CheckGlError();
_isStencilling = false;
}
if (instance.Parameters.Count == 0)
return (program, instance);
@@ -487,6 +523,9 @@ namespace Robust.Client.Graphics.Clyde
case Color color:
program.SetUniform(name, color);
break;
case Color[] colorArr:
program.SetUniform(name, colorArr);
break;
case int i:
program.SetUniform(name, i);
break;
@@ -859,17 +898,34 @@ namespace Robust.Client.Graphics.Clyde
private FullStoredRendererState PushRenderStateFull()
{
return new FullStoredRendererState(_currentMatrixProj, _currentMatrixView, _currentRenderTarget);
return new FullStoredRendererState(
_currentMatrixProj,
_currentMatrixView,
_currentBoundRenderTarget,
_currentRenderTarget,
_queuedShaderInstance,
_currentScissorState,
_glCaps);
}
private void PopRenderStateFull(in FullStoredRendererState state)
{
SetProjViewFull(state.ProjMatrix, state.ViewMatrix);
BindRenderTargetFull(state.RenderTarget);
BindRenderTargetImmediate(state.BoundRenderTarget);
var (width, height) = state.RenderTarget.Size;
_queuedShaderInstance = state.QueuedShaderInstance;
_currentRenderTarget = state.RenderTarget;
var (width, height) = state.BoundRenderTarget.Size;
GL.Viewport(0, 0, width, height);
CheckGlError();
IsStencilling = (state.GLCaps & GLCaps.Stencilling) == GLCaps.Stencilling;
IsBlending = (state.GLCaps & GLCaps.Blending) == GLCaps.Blending;
SetScissorFull(state.ScissorState);
GL.ClearStencil(0xFF);
GL.StencilMask(0xFF);
GL.Clear(ClearBufferMask.StencilBufferBit);
}
private void SetViewportImmediate(Box2i box)
@@ -1061,15 +1117,44 @@ namespace Robust.Client.Graphics.Clyde
{
public readonly Matrix3x2 ProjMatrix;
public readonly Matrix3x2 ViewMatrix;
public readonly LoadedRenderTarget BoundRenderTarget;
public readonly LoadedRenderTarget RenderTarget;
public readonly ClydeShaderInstance QueuedShaderInstance;
public FullStoredRendererState(in Matrix3x2 projMatrix, in Matrix3x2 viewMatrix,
LoadedRenderTarget renderTarget)
public readonly UIBox2i? ScissorState;
public readonly GLCaps GLCaps;
public FullStoredRendererState(
in Matrix3x2 projMatrix,
in Matrix3x2 viewMatrix,
LoadedRenderTarget boundRenderTarget,
LoadedRenderTarget renderTarget,
ClydeShaderInstance queuedShaderInstance,
UIBox2i? scissorState,
GLCaps glcaps
)
{
ProjMatrix = projMatrix;
ViewMatrix = viewMatrix;
BoundRenderTarget = boundRenderTarget;
RenderTarget = renderTarget;
QueuedShaderInstance = queuedShaderInstance;
ScissorState = scissorState;
GLCaps = glcaps;
}
}
[Flags]
private enum GLCaps : ushort
{
// If you add flags here make sure to update PopRenderState!
None = 0,
Blending = 1 << 0,
Stencilling = 1 << 2,
}
}
}

View File

@@ -485,6 +485,13 @@ namespace Robust.Client.Graphics.Clyde
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, Color[] value)
{
var data = Parent._shaderInstances[Handle];
data.ParametersDirty = true;
data.Parameters[name] = value;
}
private protected override void SetParameterImpl(string name, int value)
{
var data = Parent._shaderInstances[Handle];

View File

@@ -171,7 +171,7 @@ namespace Robust.Client.Graphics.Clyde
}
public void RenderScreenOverlaysBelow(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds)
{
@@ -179,7 +179,7 @@ namespace Robust.Client.Graphics.Clyde
}
public void RenderScreenOverlaysAbove(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds)
{

View File

@@ -21,6 +21,7 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
@@ -245,7 +246,7 @@ namespace Robust.Client.Graphics.Clyde
overrideVersion != null,
_windowing!.GetDescription());
GL.Enable(EnableCap.Blend);
IsBlending = true;
if (_hasGLSrgb && !_isGLES)
{
GL.Enable(EnableCap.FramebufferSrgb);

View File

@@ -9,6 +9,7 @@ using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -189,6 +190,22 @@ namespace Robust.Client.Graphics.Clyde
return new DummyTexture(size);
}
/// <inheritdoc />
public Color GetClearColor(EntityUid mapUid)
{
return Color.Transparent;
}
public void BlurRenderTarget(IClydeViewport viewport, IRenderTarget target, IRenderTarget blurBuffer, IEye eye, float multiplier)
{
// NOOP
}
public IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true)
{
return CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.R8, hasDepthStencil: depthStencil), null, name: name);
}
public IRenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
TextureSampleParameters? sampleParameters = null, string? name = null)
{
@@ -352,6 +369,10 @@ namespace Robust.Client.Graphics.Clyde
{
}
private protected override void SetParameterImpl(string name, Color[] value)
{
}
private protected override void SetParameterImpl(string name, int value)
{
}
@@ -497,7 +518,7 @@ namespace Robust.Client.Graphics.Clyde
}
public void RenderScreenOverlaysBelow(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds)
{
@@ -505,7 +526,7 @@ namespace Robust.Client.Graphics.Clyde
}
public void RenderScreenOverlaysAbove(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds)
{

View File

@@ -334,7 +334,7 @@ namespace Robust.Client.Graphics.Clyde
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, in Color color, bool convertToLinear=true)
private void SetUniformDirect(int slot, in Color color, bool convertToLinear = true)
{
var converted = color;
if (convertToLinear)
@@ -349,6 +349,39 @@ namespace Robust.Client.Graphics.Clyde
}
}
public void SetUniform(string uniformName, Color[] colors)
{
var uniformId = GetUniform(uniformName);
SetUniformDirect(uniformId, colors);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetUniformDirect(int slot, Color[] colors, bool convertToLinear = true)
{
scoped Span<Color> colorsToPass;
if (convertToLinear)
{
colorsToPass = stackalloc Color[colors.Length];
for (int i = 0; i < colors.Length; i++)
{
colorsToPass[i] = Color.FromSrgb(colors[i]);
}
}
else
{
colorsToPass = colors;
}
unsafe
{
fixed (Color* ptr = &colorsToPass[0])
{
GL.Uniform4(slot, colorsToPass.Length, (float*)ptr);
_clyde.CheckGlError();
}
}
}
public void SetUniform(string uniformName, in Vector3 vector)
{
var uniformId = GetUniform(uniformName);

View File

@@ -133,7 +133,7 @@ namespace Robust.Client.Graphics
baseline += new Vector2(metrics.Value.BearingX, -metrics.Value.BearingY);
if(handle is DrawingHandleWorld worldhandle)
worldhandle.DrawTextureRect(texture, Box2.FromDimensions(baseline, texture.Size));
worldhandle.DrawTextureRect(texture, Box2.FromDimensions(baseline, texture.Size), color);
else
handle.DrawTexture(texture, baseline, color);
return metrics.Value.Advance;

View File

@@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Numerics;
using System.Threading.Tasks;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Robust.Client.Graphics
{
@@ -71,6 +74,24 @@ namespace Robust.Client.Graphics
in TextureLoadParameters? loadParams = null)
where T : unmanaged, IPixel<T>;
/// <summary>
/// Gets the clear color for the specified map viewport.
/// </summary>
[Pure]
Color GetClearColor(EntityUid mapUid);
/// <summary>
/// Applies a blur to the specified render target. Requires a separate buffer with similar properties to draw intermediate steps into.
/// </summary>
/// <param name="viewport">The viewport being used for drawing.</param>
/// <param name="target">The blur target.</param>
/// <param name="blurBuffer">The separate buffer to draw into.</param>
/// <param name="eye">The eye being drawn with.</param>
/// <param name="multiplier">Scale of how much blur to blur by.</param>
void BlurRenderTarget(IClydeViewport viewport, IRenderTarget target, IRenderTarget blurBuffer, IEye eye, float multiplier);
IRenderTexture CreateLightRenderTarget(Vector2i size, string? name = null, bool depthStencil = true);
IRenderTexture CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
TextureSampleParameters? sampleParameters = null, string? name = null);

View File

@@ -18,6 +18,7 @@ namespace Robust.Client.Graphics
/// </summary>
IRenderTexture RenderTarget { get; }
IRenderTexture LightRenderTarget { get; }
IEye? Eye { get; set; }
Vector2i Size { get; }
@@ -66,7 +67,7 @@ namespace Robust.Client.Graphics
/// Not relative to the current transform of <see cref="handle"/>.
/// </param>
public void RenderScreenOverlaysBelow(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds);
@@ -80,7 +81,7 @@ namespace Robust.Client.Graphics
/// Not relative to the current transform of <see cref="handle"/>.
/// </param>
public void RenderScreenOverlaysAbove(
DrawingHandleScreen handle,
IRenderHandle handle,
IViewportControl control,
in UIBox2i viewportBounds);
}

View File

@@ -1,4 +1,6 @@
using System;
using System.Numerics;
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using SixLabors.ImageSharp.PixelFormats;
@@ -15,5 +17,42 @@ namespace Robust.Client.Graphics
Vector2i Size { get; }
void CopyPixelsToMemory<T>(CopyPixelsDelegate<T> callback, UIBox2i? subRegion = null) where T : unmanaged, IPixel<T>;
public Vector2 LocalToWorld(IEye eye, Vector2 point, Vector2 scale)
{
var newPoint = point;
// (inlined version of UiProjMatrix^-1)
newPoint -= Size / 2f;
newPoint *= new Vector2(1, -1) / EyeManager.PixelsPerMeter;
// view matrix
eye.GetViewMatrixInv(out var viewMatrixInv, scale);
newPoint = Vector2.Transform(newPoint, viewMatrixInv);
return newPoint;
}
public Vector2 WorldToLocal(Vector2 point, IEye eye, Vector2 scale)
{
var newPoint = point;
eye.GetViewMatrix(out var viewMatrix, scale);
newPoint = Vector2.Transform(newPoint, viewMatrix);
// (inlined version of UiProjMatrix)
newPoint *= new Vector2(1, -1) * EyeManager.PixelsPerMeter;
newPoint += Size / 2f;
return newPoint;
}
public Matrix3x2 GetWorldToLocalMatrix(IEye eye, Vector2 scale)
{
eye.GetViewMatrix(out var viewMatrix, scale * new Vector2(EyeManager.PixelsPerMeter, -EyeManager.PixelsPerMeter));
viewMatrix.M31 += Size.X / 2f;
viewMatrix.M32 += Size.Y / 2f;
return viewMatrix;
}
}
}

View File

@@ -0,0 +1,13 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics;
/// <summary>
/// Raised by the engine if content wishes to override the default clear color.
/// </summary>
[ByRefEvent]
public record struct GetClearColorEvent
{
public Color? Color;
}

View File

@@ -9,6 +9,5 @@ namespace Robust.Client.Graphics
public bool DrawHardFov { get; set; } = true;
public bool DrawLighting { get; set; } = true;
public bool LockConsoleAccess { get; set; } = false;
public Color AmbientLightColor { get; set; } = Color.FromSrgb(Color.Black);
}
}

View File

@@ -1,6 +1,7 @@
using JetBrains.Annotations;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -39,6 +40,8 @@ namespace Robust.Client.Graphics
/// </summary>
public readonly UIBox2i ViewportBounds;
public readonly EntityUid MapUid;
/// <summary>
/// <see cref="MapId"/> of the viewport's eye.
/// </summary>
@@ -54,24 +57,32 @@ namespace Robust.Client.Graphics
/// </summary>
public readonly Box2Rotated WorldBounds;
public readonly IRenderHandle RenderHandle;
public DrawingHandleScreen ScreenHandle => (DrawingHandleScreen) DrawingHandle;
public DrawingHandleWorld WorldHandle => (DrawingHandleWorld) DrawingHandle;
public OverlayDrawArgs(
internal OverlayDrawArgs(
OverlaySpace space,
IViewportControl? viewportControl,
IClydeViewport viewport,
DrawingHandleBase drawingHandle,
IRenderHandle renderHandle,
in UIBox2i viewportBounds,
in EntityUid mapUid,
in MapId mapId,
in Box2 worldAabb,
in Box2Rotated worldBounds)
{
DrawingHandle = space is OverlaySpace.ScreenSpace or OverlaySpace.ScreenSpaceBelowWorld
? renderHandle.DrawingHandleScreen
: renderHandle.DrawingHandleWorld;
Space = space;
ViewportControl = viewportControl;
Viewport = viewport;
DrawingHandle = drawingHandle;
RenderHandle = renderHandle;
ViewportBounds = viewportBounds;
MapUid = mapUid;
MapId = mapId;
WorldAABB = worldAabb;
WorldBounds = worldBounds;

View File

@@ -13,10 +13,22 @@ internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
[Dependency] private readonly ILogManager _logMan = default!;
[ViewVariables]
private readonly Dictionary<Type, Overlay> _overlays = new Dictionary<Type, Overlay>();
private readonly Dictionary<Type, Overlay> _overlays = new();
/// <summary>
/// A list that duplicates a value from <see cref="_overlays"/>,
/// but already sorted, by invoking <see cref="Sort"/>
/// in <see cref="AddOverlay"/> and <see cref="RemoveOverlay(System.Type)"/>.
/// </summary>
[ViewVariables]
private readonly List<Overlay> _sortedOverlays = [];
private ISawmill _logger = default!;
public IEnumerable<Overlay> AllOverlays => _overlays.Values;
/// <summary>
/// Returns a list of all overlays sorted by <see cref="Overlay.ZIndex"/>
/// </summary>
public IEnumerable<Overlay> AllOverlays => _sortedOverlays;
public void FrameUpdate(FrameEventArgs args)
{
@@ -28,9 +40,10 @@ internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
public bool AddOverlay(Overlay overlay)
{
if (_overlays.ContainsKey(overlay.GetType()))
if (!_overlays.TryAdd(overlay.GetType(), overlay))
return false;
_overlays.Add(overlay.GetType(), overlay);
Sort();
return true;
}
@@ -42,7 +55,9 @@ internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
return false;
}
return _overlays.Remove(overlayClass);
var result = _overlays.Remove(overlayClass);
Sort();
return result;
}
public bool RemoveOverlay<T>() where T : Overlay
@@ -52,7 +67,7 @@ internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
public bool RemoveOverlay(Overlay overlay)
{
return _overlays.Remove(overlay.GetType());
return RemoveOverlay(overlay.GetType());
}
public bool TryGetOverlay(Type overlayClass, [NotNullWhen(true)] out Overlay? overlay)
@@ -104,8 +119,27 @@ internal sealed class OverlayManager : IOverlayManagerInternal, IPostInjectInit
return _overlays.ContainsKey(typeof(T));
}
private void Sort()
{
_sortedOverlays.Clear();
_sortedOverlays.AddRange(_overlays.Values);
_sortedOverlays.Sort(OverlayComparer.Instance);
}
void IPostInjectInit.PostInject()
{
_logger = _logMan.GetSawmill("overlay");
}
private sealed class OverlayComparer : IComparer<Overlay>
{
public static readonly OverlayComparer Instance = new();
public int Compare(Overlay? x, Overlay? y)
{
var zX = x?.ZIndex ?? 0;
var zY = y?.ZIndex ?? 0;
return zX.CompareTo(zY);
}
}
}

View File

@@ -221,11 +221,12 @@ namespace Robust.Client.Graphics
public static bool TypeSupportsArrays(this ShaderDataType type)
{
// TODO: add support for int, and vec3/4 arrays
// TODO: add support for int, and vec3 arrays
return
(type == ShaderDataType.Float) ||
(type == ShaderDataType.Vec2) ||
(type == ShaderDataType.Bool);
(type == ShaderDataType.Bool) ||
(type == ShaderDataType.Vec4);
}
[SuppressMessage("ReSharper", "StringLiteralTypo")]

View File

@@ -113,6 +113,13 @@ namespace Robust.Client.Graphics
SetParameterImpl(name, value);
}
public void SetParameter(string name, Color[] value)
{
EnsureAlive();
EnsureMutable();
SetParameterImpl(name, value);
}
public void SetParameter(string name, Vector4 value)
{
EnsureAlive();
@@ -223,6 +230,7 @@ namespace Robust.Client.Graphics
private protected abstract void SetParameterImpl(string name, Vector3 value);
private protected abstract void SetParameterImpl(string name, Vector4 value);
private protected abstract void SetParameterImpl(string name, Color value);
private protected abstract void SetParameterImpl(string name, Color[] value);
private protected abstract void SetParameterImpl(string name, int value);
private protected abstract void SetParameterImpl(string name, Vector2i value);
private protected abstract void SetParameterImpl(string name, bool value);

View File

@@ -93,7 +93,7 @@ public sealed partial class PhysicsSystem
var maps = new HashSet<EntityUid>();
var enumerator = AllEntityQuery<PredictedPhysicsComponent, PhysicsComponent, TransformComponent>();
while (enumerator.MoveNext(out var _, out var physics, out var xform))
while (enumerator.MoveNext(out _, out var physics, out var xform))
{
DebugTools.Assert(physics.Predict);

View File

@@ -144,7 +144,7 @@ namespace Robust.Client.Placement
if (mousePos.MapId == MapId.Nullspace)
yield break;
var (_, (x, y)) = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
var (_, (x, y)) = transformSys.ToCoordinates(pManager.StartPoint.EntityId, mousePos) - pManager.StartPoint;
float iterations;
Vector2 distance;
if (Math.Abs(x) > Math.Abs(y))
@@ -176,7 +176,7 @@ namespace Robust.Client.Placement
if (mousePos.MapId == MapId.Nullspace)
yield break;
var placementdiff = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint;
var placementdiff = transformSys.ToCoordinates(pManager.StartPoint.EntityId, mousePos) - pManager.StartPoint;
var xSign = Math.Sign(placementdiff.X);
var ySign = Math.Sign(placementdiff.Y);
@@ -264,13 +264,15 @@ namespace Robust.Client.Placement
protected EntityCoordinates ScreenToCursorGrid(ScreenCoordinates coords)
{
var mapCoords = pManager.EyeManager.PixelToMap(coords.Position);
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid))
{
return EntityCoordinates.FromMap(pManager.MapManager, mapCoords);
return transformSys.ToCoordinates(mapCoords);
}
var transformSys = pManager.EntityManager.System<SharedTransformSystem>();
return EntityCoordinates.FromMap(gridUid, mapCoords, transformSys, pManager.EntityManager);
return transformSys.ToCoordinates(gridUid, mapCoords);
}
}
}

View File

@@ -352,6 +352,9 @@ public sealed partial class ReplayLoadManager
// prototype changes when jumping around in time. This also requires reworking how the initial
// implicit state data is generated, because we can't simply cache it anymore.
// Also, does reloading prototypes in release mode modify existing entities?
// Yes, yes it does. See PrototypeReloadSystem.UpdateEntity()
// Its just not supported ATM.
// TBH it'd be easier if overriding existing prototypes in release mode was just forbidden.
var msg = $"Overwriting an existing prototype! Kind: {kind.Name}. Ids: {string.Join(", ", ids)}";
if (_confMan.GetCVar(CVars.ReplayIgnoreErrors))
@@ -361,7 +364,6 @@ public sealed partial class ReplayLoadManager
}
}
_protoMan.ResolveResults();
_protoMan.ReloadPrototypes(changed);
_locMan.ReloadLocalizations();
}

View File

@@ -95,9 +95,21 @@ public sealed partial class ReplayLoadManager
_gameState.ClearDetachQueue();
_gameState.ApplyGameState(checkpoint.State, next);
// Sort entities to ensure that we initialize parents before children
var sorted = new List<EntityUid>(entities.Count);
var added = new HashSet<EntityUid>(entities.Count);
var xformQuery = _entMan.GetEntityQuery<TransformComponent>();
foreach (var uid in entities)
{
AddSorted(uid, sorted, added, xformQuery);
}
DebugTools.AssertEqual(sorted.Count, entities.Count);
DebugTools.AssertEqual(added.Count, entities.Count);
await callback(i, total, LoadingState.Initializing, false);
i = 0;
var query = _entMan.GetEntityQuery<MetaDataComponent>();
foreach (var uid in entities)
foreach (var uid in sorted)
{
_entMan.InitializeEntity(uid, query.GetComponent(uid));
if (i++ % 50 == 0)
@@ -109,7 +121,7 @@ public sealed partial class ReplayLoadManager
i = 0;
await callback(0, total, LoadingState.Starting, true);
foreach (var uid in entities)
foreach (var uid in sorted)
{
_entMan.StartEntity(uid);
if (i++ % 50 == 0)
@@ -132,4 +144,16 @@ public sealed partial class ReplayLoadManager
_replayPlayback.StartReplay(data);
_timing.Paused = false;
}
private void AddSorted(EntityUid uid, List<EntityUid> sortedList, HashSet<EntityUid> added, EntityQuery<TransformComponent> query)
{
if (!added.Add(uid))
return;
var parent = query.Comp(uid).ParentUid;
if (parent != EntityUid.Invalid)
AddSorted(parent, sortedList, added, query);
sortedList.Add(uid);
}
}

View File

@@ -45,6 +45,7 @@ internal sealed partial class ReplayPlaybackManager
var next = Replay.NextState;
BeforeApplyState?.Invoke((state, next));
_gameState.ApplyGameState(state, next);
_gameState.MergeImplicitData();
DebugTools.Assert(Replay.LastApplied >= state.FromSequence);
DebugTools.Assert(Replay.LastApplied + 1 <= state.ToSequence);
Replay.LastApplied = state.ToSequence;

View File

@@ -1,4 +1,5 @@
using System;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects;
@@ -6,14 +7,63 @@ namespace Robust.Client.UserInterface;
public static class BoundUserInterfaceExt
{
private static T GetWindow<T>(BoundUserInterface bui) where T : BaseWindow, new()
{
var window = bui.CreateDisposableControl<T>();
window.OnClose += bui.Close;
var system = bui.EntMan.System<UserInterfaceSystem>();
system.RegisterControl(bui, window);
return window;
}
/// <summary>
/// Helper method to create a window and also handle closing the BUI when it's closed.
/// </summary>
public static T CreateWindow<T>(this BoundUserInterface bui) where T : BaseWindow, new()
{
var window = bui.CreateDisposableControl<T>();
window.OpenCentered();
window.OnClose += bui.Close;
var window = GetWindow<T>(bui);
if (bui.EntMan.System<UserInterfaceSystem>().TryGetPosition(bui.Owner, bui.UiKey, out var position))
{
window.Open(position);
}
else
{
window.OpenCentered();
}
return window;
}
public static T CreateWindowCenteredLeft<T>(this BoundUserInterface bui) where T : BaseWindow, new()
{
var window = GetWindow<T>(bui);
if (bui.EntMan.System<UserInterfaceSystem>().TryGetPosition(bui.Owner, bui.UiKey, out var position))
{
window.Open(position);
}
else
{
window.OpenCenteredLeft();
}
return window;
}
public static T CreateWindowCenteredRight<T>(this BoundUserInterface bui) where T : BaseWindow, new()
{
var window = GetWindow<T>(bui);
if (bui.EntMan.System<UserInterfaceSystem>().TryGetPosition(bui.Owner, bui.UiKey, out var position))
{
window.Open(position);
}
else
{
window.OpenCenteredRight();
}
return window;
}

View File

@@ -128,7 +128,7 @@ namespace Robust.Client.UserInterface
UserInterfaceManagerInternal.QueueStyleUpdate(this);
}
internal void StyleSheetUpdate()
public void InvalidateStyleSheet()
{
_stylesheetUpdateNeeded = true;
@@ -137,7 +137,7 @@ namespace Robust.Client.UserInterface
internal void StylesheetUpdateRecursive()
{
StyleSheetUpdate();
InvalidateStyleSheet();
foreach (var child in Children)
{
@@ -149,7 +149,7 @@ namespace Robust.Client.UserInterface
}
}
internal void DoStyleUpdate()
public void DoStyleUpdate()
{
_styleProperties.Clear();

View File

@@ -549,7 +549,7 @@ namespace Robust.Client.UserInterface
{
}
internal virtual void DrawInternal(IRenderHandle renderHandle)
protected internal virtual void Draw(IRenderHandle renderHandle)
{
Draw(renderHandle.DrawingHandleScreen);
}

View File

@@ -87,7 +87,6 @@ public sealed class TileSpawningUIController : UIController
return;
_window = UIManager.CreateWindow<TileSpawnWindow>();
LayoutContainer.SetAnchorPreset(_window,LayoutContainer.LayoutPreset.CenterLeft);
_window.SearchBar.GrabKeyboardFocus();
_window.ClearButton.OnPressed += OnTileClearPressed;
_window.SearchBar.OnTextChanged += OnTileSearchChanged;
_window.TileList.OnItemSelected += OnTileItemSelected;

View File

@@ -220,7 +220,7 @@ internal partial class UserInterfaceManager
continue;
var typeDict = _assignerRegistry.GetOrNew(fieldInfo.FieldType);
var assigner = (InternalReflectionUtils.AssignField<UIController, object?>)InternalReflectionUtils.EmitFieldAssigner<UIController>(backingField, true);
var assigner = (InternalReflectionUtils.AssignField<UIController, object?>)InternalReflectionUtils.EmitFieldAssigner(typeof(UIController), backingField, true);
typeDict.Add(controllerType, assigner);
}

View File

@@ -9,6 +9,7 @@ public class EntityPrototypeView : SpriteView
{
private string? _currentPrototype;
private EntityUid? _ourEntity;
private bool _isShowing;
public EntityPrototypeView()
{
@@ -22,8 +23,6 @@ public class EntityPrototypeView : SpriteView
public void SetPrototype(EntProtoId? entProto)
{
SpriteSystem ??= EntMan.System<SpriteSystem>();
if (entProto == _currentPrototype
&& EntMan.TryGetComponent(Entity?.Owner, out MetaDataComponent? meta)
&& meta.EntityPrototype?.ID == _currentPrototype)
@@ -32,14 +31,10 @@ public class EntityPrototypeView : SpriteView
}
_currentPrototype = entProto;
SetEntity(null);
EntMan.DeleteEntity(_ourEntity);
if (_currentPrototype != null)
if (_ourEntity != null || _isShowing)
{
_ourEntity = EntMan.Spawn(_currentPrototype);
SpriteSystem.ForceUpdate(_ourEntity.Value);
SetEntity(_ourEntity);
UpdateEntity();
}
}
@@ -48,12 +43,38 @@ public class EntityPrototypeView : SpriteView
base.EnteredTree();
if (_currentPrototype != null)
SetPrototype(_currentPrototype);
{
UpdateEntity();
}
_isShowing = true;
}
protected override void ExitedTree()
{
base.ExitedTree();
EntMan.TryQueueDeleteEntity(_ourEntity);
_ourEntity = null;
_isShowing = false;
}
private void UpdateEntity()
{
SetEntity(null);
EntMan.DeleteEntity(_ourEntity);
if (_currentPrototype != null)
{
SpriteSystem ??= EntMan.System<SpriteSystem>();
_ourEntity = EntMan.Spawn(_currentPrototype);
SpriteSystem.ForceUpdate(_ourEntity.Value);
SetEntity(_ourEntity);
}
else
{
_ourEntity = null;
}
}
}

View File

@@ -28,6 +28,30 @@ namespace Robust.Client.UserInterface.Controls
public int ScrollSpeedX { get; set; } = 50;
public int ScrollSpeedY { get; set; } = 50;
public float VScroll
{
get => _vScrollBar.Value;
set => _vScrollBar.Value = value;
}
public float VScrollTarget
{
get => _vScrollBar.ValueTarget;
set => _vScrollBar.ValueTarget = value;
}
public float HScroll
{
get => _hScrollBar.Value;
set => _hScrollBar.Value = value;
}
public float HScrollTarget
{
get => _hScrollBar.ValueTarget;
set => _hScrollBar.ValueTarget = value;
}
private bool _reserveScrollbarSpace;
public bool ReserveScrollbarSpace
{

View File

@@ -224,7 +224,7 @@ namespace Robust.Client.UserInterface.Controls
_spriteSize = new Vector2(longestRotatedSide, longestRotatedSide);
}
internal override void DrawInternal(IRenderHandle renderHandle)
protected internal override void Draw(IRenderHandle renderHandle)
{
if (!ResolveEntity(out var uid, out var sprite, out var xform))
return;

View File

@@ -5,6 +5,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
@@ -102,6 +103,8 @@ namespace Robust.Client.UserInterface.CustomControls
{
var cursor = CursorShape.Arrow;
var previewDragMode = GetDragModeFor(args.RelativePosition);
previewDragMode &= ~DragMode.Move;
switch (previewDragMode)
{
case DragMode.Top:
@@ -116,9 +119,6 @@ namespace Robust.Client.UserInterface.CustomControls
case DragMode.Bottom | DragMode.Left:
case DragMode.Top | DragMode.Right:
cursor = CursorShape.Crosshair;
break;
case DragMode.Bottom | DragMode.Right:
case DragMode.Top | DragMode.Left:
cursor = CursorShape.Crosshair;
@@ -160,15 +160,6 @@ namespace Robust.Client.UserInterface.CustomControls
var rect = new UIBox2(left, top, right, bottom);
LayoutContainer.SetPosition(this, rect.TopLeft);
SetSize = rect.Size;
/*
var timing = IoCManager.Resolve<IGameTiming>();
var l = GetValue<float>(LayoutContainer.MarginLeftProperty);
var t = GetValue<float>(LayoutContainer.MarginTopProperty);
Logger.Debug($"{timing.CurFrame}: {rect.TopLeft}/({l}, {t}), {rect.Size}/{SetSize}");
*/
}
}
@@ -229,6 +220,16 @@ namespace Robust.Client.UserInterface.CustomControls
OnOpen?.Invoke();
}
/// <summary>
/// Opens the window and places it at the specified position.
/// </summary>
public void Open(Vector2 position)
{
Measure(Vector2Helpers.Infinity);
Open();
LayoutContainer.SetPosition(this, position);
}
public void OpenCentered() => OpenCenteredAt(new Vector2(0.5f, 0.5f));
public void OpenToLeft() => OpenCenteredAt(new Vector2(0, 0.5f));

View File

@@ -276,10 +276,14 @@ public sealed partial class DebugConsole
// This means that letter casing will match the completion suggestion.
CommandBar.CursorPosition = lastRange.end;
CommandBar.SelectionStart = lastRange.start;
var insertValue = CommandParsing.Escape(completion);
var insertValue = (completionFlags & CompletionOptionFlags.NoEscape) == 0
? CommandParsing.Escape(completion)
: completion;
// If the replacement contains a space, we must quote it to treat it as a single argument.
var mustQuote = insertValue.Contains(' ');
var mustQuote = (completionFlags & CompletionOptionFlags.NoQuote) == 0 && insertValue.Contains(' ');
if ((completionFlags & CompletionOptionFlags.PartialCompletion) == 0)
{
if (mustQuote)

View File

@@ -10,4 +10,11 @@ public sealed partial class TileSpawnWindow : DefaultWindow
{
RobustXamlLoader.Load(this);
}
protected override void Opened()
{
base.Opened();
SearchBar.GrabKeyboardFocus();
}
}

View File

@@ -65,13 +65,13 @@ namespace Robust.Client.UserInterface.CustomControls
// -- Handlers: Out --
protected internal override void Draw(DrawingHandleScreen handle)
protected internal override void Draw(IRenderHandle handle)
{
base.Draw(handle);
if (Viewport == null)
{
handle.DrawRect(UIBox2.FromDimensions(new Vector2(0, 0), Size * UIScale), Color.Red);
handle.DrawingHandleScreen.DrawRect(UIBox2.FromDimensions(new Vector2(0, 0), Size * UIScale), Color.Red);
}
else
{
@@ -82,7 +82,7 @@ namespace Robust.Client.UserInterface.CustomControls
Viewport.RenderScreenOverlaysBelow(handle, this, viewportBounds);
Viewport.Render();
handle.DrawTextureRect(Viewport.RenderTarget.Texture,
handle.DrawingHandleScreen.DrawTextureRect(Viewport.RenderTarget.Texture,
UIBox2.FromDimensions(new Vector2(0, 0), (Vector2i) (Viewport.Size / _viewportResolution)));
Viewport.RenderScreenOverlaysAbove(handle, this, viewportBounds);

View File

@@ -7,6 +7,7 @@ using Robust.Client.UserInterface.CustomControls;
using Robust.Shared;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface
{
@@ -146,6 +147,16 @@ namespace Robust.Client.UserInterface
/// but not necessarily a new or existing control is rearranged.
/// </summary>
void UpdateHovered();
/// <summary>
/// Render a control and all of its children.
/// </summary>
void RenderControl(in Control.ControlRenderArguments args, Control control);
/// <summary>
/// Render a control and all of its children.
/// </summary>
void RenderControl(IRenderHandle handle, Control control, Vector2i position);
}
public readonly struct PostDrawUIRootEventArgs

View File

@@ -313,7 +313,10 @@ internal partial class UserInterfaceManager
private void _clearTooltip()
{
if (!_showingTooltip) return;
_resetTooltipTimer();
if (!_showingTooltip)
return;
if (_suppliedTooltip != null)
{
@@ -322,7 +325,6 @@ internal partial class UserInterfaceManager
}
CurrentlyHovered?.PerformHideTooltip();
_resetTooltipTimer();
_showingTooltip = false;
}

View File

@@ -37,7 +37,7 @@ internal sealed partial class UserInterfaceManager
_roots.Add(newRoot);
_windowsToRoot.Add(window.Id, newRoot);
newRoot.StyleSheetUpdate();
newRoot.InvalidateStyleSheet();
newRoot.InvalidateMeasure();
QueueMeasureUpdate(newRoot);
QueueArrangeUpdate(newRoot);

View File

@@ -335,6 +335,30 @@ namespace Robust.Client.UserInterface
}
}
public void RenderControl(in Control.ControlRenderArguments args, Control control)
{
var _ = 0;
RenderControl(args.Handle,
ref _,
control,
args.Position,
args.Modulate,
args.ScissorBox,
args.CoordinateTransform);
}
public void RenderControl(IRenderHandle handle, Control control, Vector2i position)
{
var _ = 0;
RenderControl(handle,
ref _,
control,
position,
Color.White,
null,
Matrix3x2.Identity);
}
public void RenderControl(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate,
UIBox2i? scissorBox, Matrix3x2 coordinateTransform)
{
@@ -393,7 +417,7 @@ namespace Robust.Client.UserInterface
// Handle modulation with care.
var oldMod = handle.Modulate;
handle.Modulate = modulate * control.ActualModulateSelf;
control.DrawInternal(renderHandle);
control.Draw(renderHandle);
handle.Modulate = oldMod;
handle.UseShader(null);
}
@@ -409,10 +433,11 @@ namespace Robust.Client.UserInterface
control.PreRenderChildren(ref args);
foreach (var child in control.Children)
for (var index = 0; index < control.ChildCount; index++)
{
var child = control.GetChild(index);
var pos = position + (Vector2i)Vector2.Transform(child.PixelPosition, coordinateTransform);
control.RenderChildOverride(ref args, child.GetPositionInParent(), pos);
control.RenderChildOverride(ref args, index, pos);
}
control.PostRenderChildren(ref args);

View File

@@ -153,6 +153,7 @@ public sealed class ComponentPauseGenerator : IIncrementalGenerator
builder.AppendLine($$"""
[RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class {{info.PartialTypeInfo.Name}}_AutoPauseSystem : EntitySystem
{
public override void Initialize()

View File

@@ -81,53 +81,58 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? specifier, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
var entity = SetupAudio(filename, audioParams);
var entity = SetupAudio(specifier, audioParams);
AddAudioFilter(entity, entity.Comp, playerFilter);
entity.Comp.Global = true;
return (entity, entity.Comp);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (TerminatingOrDeleted(uid))
return null;
var entity = SetupAudio(filename, audioParams);
var entity = SetupAudio(specifier, audioParams);
// Move it after setting it up
XformSystem.SetCoordinates(entity, new EntityCoordinates(uid, Vector2.Zero));
// TODO AUDIO
// Add methods that allow for custom audio range.
// Some methods try to reduce the audio range, resulting in a custom filter which then unnecessarily has to
// use PVS overrides. PlayEntity with a reduced range shouldn't need PVS overrides at all.
AddAudioFilter(entity, entity.Comp, playerFilter);
return (entity, entity.Comp);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityUid uid, AudioParams? audioParams = null)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (TerminatingOrDeleted(uid))
return null;
var entity = SetupAudio(filename, audioParams);
var entity = SetupAudio(specifier, audioParams);
XformSystem.SetCoordinates(entity, new EntityCoordinates(uid, Vector2.Zero));
return (entity, entity.Comp);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? specifier, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (TerminatingOrDeleted(coordinates.EntityId))
@@ -139,7 +144,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (!coordinates.IsValid(EntityManager))
return null;
var entity = SetupAudio(filename, audioParams);
var entity = SetupAudio(specifier, audioParams);
XformSystem.SetCoordinates(entity, coordinates);
AddAudioFilter(entity, entity.Comp, playerFilter);
@@ -147,10 +152,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string? filename, EntityCoordinates coordinates,
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? specifier, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
if (string.IsNullOrEmpty(filename))
if (specifier is null)
return null;
if (TerminatingOrDeleted(coordinates.EntityId))
@@ -163,7 +168,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
// TODO: Transform TryFindGridAt mess + optimisation required.
var entity = SetupAudio(filename, audioParams);
var entity = SetupAudio(specifier, audioParams);
XformSystem.SetCoordinates(entity, coordinates);
return (entity, entity.Comp);
@@ -186,7 +191,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (sound == null)
return null;
var audio = PlayPvs(GetSound(sound), source, audioParams ?? sound.Params);
var audio = PlayPvs(ResolveSound(sound), source, audioParams ?? sound.Params);
if (audio == null)
return null;
@@ -201,7 +206,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (sound == null)
return null;
var audio = PlayPvs(GetSound(sound), coordinates, audioParams ?? sound.Params);
var audio = PlayPvs(ResolveSound(sound), coordinates, audioParams ?? sound.Params);
if (audio == null)
return null;
@@ -210,12 +215,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
return audio;
}
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? filename, ICommonSession recipient, AudioParams? audioParams = null)
{
return PlayGlobal(filename, Filter.SinglePlayer(recipient), false, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? filename, EntityUid recipient, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayGlobal(filename, actor.PlayerSession, audioParams);
@@ -223,12 +228,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
}
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return PlayEntity(filename, Filter.SinglePlayer(recipient), uid, false, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayEntity(filename, actor.PlayerSession, uid, audioParams);
@@ -236,12 +241,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
return null;
}
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return PlayStatic(filename, Filter.SinglePlayer(recipient), coordinates, false, audioParams);
}
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
if (TryComp(recipient, out ActorComponent? actor))
return PlayStatic(filename, actor.PlayerSession, coordinates, audioParams);

View File

@@ -297,7 +297,7 @@ namespace Robust.Server
: null;
// Set up the VFS
_resources.Initialize(dataDir);
_resources.Initialize(dataDir, hideUserDataDir: false);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;
@@ -586,7 +586,7 @@ namespace Robust.Server
{
_config.OnValueChanged(CVars.NetTickrate, i =>
{
var b = (byte) i;
var b = (ushort) i;
_time.TickRate = b;
_logger.Info($"Tickrate changed to: {b} on tick {_time.CurTick}");
@@ -594,7 +594,7 @@ namespace Robust.Server
var startOffset = TimeSpan.FromSeconds(_config.GetCVar(CVars.NetTimeStartOffset));
_time.TimeBase = (startOffset, GameTick.First);
_time.TickRate = (byte) _config.GetCVar(CVars.NetTickrate);
_time.TickRate = (ushort) _config.GetCVar(CVars.NetTickrate);
_logger.Info($"Name: {ServerName}");
_logger.Info($"TickRate: {_time.TickRate}({_time.TickPeriod.TotalMilliseconds:0.00}ms)");

View File

@@ -1,14 +1,15 @@
using System.Linq;
using System.Numerics;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Server.Console.Commands
{
@@ -42,7 +43,7 @@ namespace Robust.Server.Console.Commands
return;
}
_ent.System<MapLoaderSystem>().Save(uid, args[1]);
_ent.System<MapLoaderSystem>().TrySaveGrid(uid, new ResPath(args[1]));
shell.WriteLine("Save successful. Look in the user data directory.");
}
@@ -63,7 +64,6 @@ namespace Robust.Server.Console.Commands
public sealed class LoadGridCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IResourceManager _resource = default!;
public override string Command => "loadgrid";
@@ -91,13 +91,14 @@ namespace Robust.Server.Console.Commands
return;
}
if (!_map.MapExists(mapId))
var sys = _system.GetEntitySystem<SharedMapSystem>();
if (!sys.MapExists(mapId))
{
shell.WriteError("Target map does not exist.");
return;
}
var loadOptions = new MapLoadOptions();
Vector2 offset = default;
if (args.Length >= 4)
{
if (!float.TryParse(args[2], out var x))
@@ -112,9 +113,10 @@ namespace Robust.Server.Console.Commands
return;
}
loadOptions.Offset = new Vector2(x, y);
offset = new Vector2(x, y);
}
Angle rot = default;
if (args.Length >= 5)
{
if (!float.TryParse(args[4], out var rotation))
@@ -123,9 +125,10 @@ namespace Robust.Server.Console.Commands
return;
}
loadOptions.Rotation = Angle.FromDegrees(rotation);
rot = Angle.FromDegrees(rotation);
}
var opts = DeserializationOptions.Default;
if (args.Length >= 6)
{
if (!bool.TryParse(args[5], out var storeUids))
@@ -134,10 +137,11 @@ namespace Robust.Server.Console.Commands
return;
}
loadOptions.StoreMapUids = storeUids;
opts.StoreYamlUids = storeUids;
}
_system.GetEntitySystem<MapLoaderSystem>().Load(mapId, args[1], loadOptions);
var path = new ResPath(args[1]);
_system.GetEntitySystem<MapLoaderSystem>().TryLoadGrid(mapId, path, out _, opts, offset, rot);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
@@ -149,7 +153,6 @@ namespace Robust.Server.Console.Commands
public sealed class SaveMap : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IResourceManager _resource = default!;
public override string Command => "savemap";
@@ -189,13 +192,14 @@ namespace Robust.Server.Console.Commands
if (mapId == MapId.Nullspace)
return;
if (!_map.MapExists(mapId))
var sys = _system.GetEntitySystem<SharedMapSystem>();
if (!sys.MapExists(mapId))
{
shell.WriteError(Loc.GetString("cmd-savemap-not-exist"));
return;
}
if (_map.IsMapInitialized(mapId) &&
if (sys.IsInitialized(mapId) &&
( args.Length < 3 || !bool.TryParse(args[2], out var force) || !force))
{
shell.WriteError(Loc.GetString("cmd-savemap-init-warning"));
@@ -203,7 +207,7 @@ namespace Robust.Server.Console.Commands
}
shell.WriteLine(Loc.GetString("cmd-savemap-attempt", ("mapId", mapId), ("path", args[1])));
_system.GetEntitySystem<MapLoaderSystem>().SaveMap(mapId, args[1]);
_system.GetEntitySystem<MapLoaderSystem>().TrySaveMap(mapId, new ResPath(args[1]));
shell.WriteLine(Loc.GetString("cmd-savemap-success"));
}
}
@@ -211,7 +215,6 @@ namespace Robust.Server.Console.Commands
public sealed class LoadMap : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IResourceManager _resource = default!;
public override string Command => "loadmap";
@@ -267,61 +270,49 @@ namespace Robust.Server.Console.Commands
return;
}
if (_map.MapExists(mapId))
var sys = _system.GetEntitySystem<SharedMapSystem>();
if (sys.MapExists(mapId))
{
shell.WriteError(Loc.GetString("cmd-loadmap-exists", ("mapId", mapId)));
return;
}
var loadOptions = new MapLoadOptions();
float x = 0, y = 0;
if (args.Length >= 3)
float x = 0;
if (args.Length >= 3 && !float.TryParse(args[2], out x))
{
if (!float.TryParse(args[2], out x))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[2])));
return;
}
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[2])));
return;
}
if (args.Length >= 4)
float y = 0;
if (args.Length >= 4 && !float.TryParse(args[3], out y))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[3])));
return;
}
var offset = new Vector2(x, y);
if (!float.TryParse(args[3], out y))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[3])));
return;
}
float rotation = 0;
if (args.Length >= 5 && !float.TryParse(args[4], out rotation))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[4])));
return;
}
var rot = new Angle(rotation);
bool storeUids = false;
if (args.Length >= 6 && !bool.TryParse(args[5], out storeUids))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-bool", ("arg", args[5])));
return;
}
loadOptions.Offset = new Vector2(x, y);
var opts = new DeserializationOptions {StoreYamlUids = storeUids};
if (args.Length >= 5)
{
if (!float.TryParse(args[4], out var rotation))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[4])));
return;
}
var path = new ResPath(args[1]);
_system.GetEntitySystem<MapLoaderSystem>().TryLoadMapWithId(mapId, path, out _, out _, opts, offset, rot);
loadOptions.Rotation = new Angle(rotation);
}
if (args.Length >= 6)
{
if (!bool.TryParse(args[5], out var storeUids))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-bool", ("arg", args[5])));
return;
}
loadOptions.StoreMapUids = storeUids;
}
_system.GetEntitySystem<MapLoaderSystem>().TryLoad(mapId, args[1], out _, loadOptions);
if (_map.MapExists(mapId))
if (sys.MapExists(mapId))
shell.WriteLine(Loc.GetString("cmd-loadmap-success", ("mapId", mapId), ("path", args[1])));
else
shell.WriteLine(Loc.GetString("cmd-loadmap-error", ("path", args[1])));

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ namespace Robust.Server.GameObjects
protected override MapId GetNextMapId()
{
var id = new MapId(++LastMapId);
while (MapManager.MapExists(id))
while (MapExists(id) || UsedIds.Contains(id))
{
id = new MapId(++LastMapId);
}

View File

@@ -6,7 +6,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Server.GameObjects
{
public sealed class VisibilitySystem : EntitySystem
public sealed class VisibilitySystem : SharedVisibilitySystem
{
[Dependency] private readonly PvsSystem _pvs = default!;
[Dependency] private readonly IViewVariablesManager _vvManager = default!;
@@ -40,7 +40,7 @@ namespace Robust.Server.GameObjects
EntityManager.EntityInitialized -= OnEntityInit;
}
public void AddLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
public override void AddLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp<VisibilityComponent>(ent.Owner);
@@ -53,7 +53,7 @@ namespace Robust.Server.GameObjects
RefreshVisibility(ent);
}
public void RemoveLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
public override void RemoveLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
if (!_visibilityQuery.Resolve(ent.Owner, ref ent.Comp, false))
return;
@@ -67,7 +67,7 @@ namespace Robust.Server.GameObjects
RefreshVisibility(ent);
}
public void SetLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
public override void SetLayer(Entity<VisibilityComponent?> ent, ushort layer, bool refresh = true)
{
ent.Comp ??= _visibilityQuery.CompOrNull(ent.Owner) ?? AddComp<VisibilityComponent>(ent.Owner);
@@ -90,14 +90,14 @@ namespace Robust.Server.GameObjects
RefreshVisibility(ent.Owner, null, ent.Comp);
}
public void RefreshVisibility(EntityUid uid,
public override void RefreshVisibility(EntityUid uid,
VisibilityComponent? visibilityComponent = null,
MetaDataComponent? meta = null)
{
RefreshVisibility((uid, visibilityComponent, meta));
}
public void RefreshVisibility(Entity<VisibilityComponent?, MetaDataComponent?> ent)
public override void RefreshVisibility(Entity<VisibilityComponent?, MetaDataComponent?> ent)
{
if (!_metaQuery.Resolve(ent, ref ent.Comp2, false))
return;

View File

@@ -1,21 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Robust.Server.GameObjects
{
internal interface IServerEntityManagerInternal : IServerEntityManager
{
// These methods are used by the map loader to do multi-stage entity construction during map load.
// I would recommend you refer to the MapLoader for usage.
EntityUid AllocEntity(EntityPrototype? prototype);
void FinishEntityLoad(EntityUid entity, IEntityLoadContext? context = null);
void FinishEntityLoad(EntityUid entity, EntityPrototype? prototype, IEntityLoadContext? context = null);
void FinishEntityInitialization(EntityUid entity, MetaDataComponent? meta = null);
void FinishEntityStartup(EntityUid entity);
}
}

View File

@@ -1,17 +0,0 @@
using Robust.Shared.GameObjects;
namespace Robust.Server.GameObjects
{
/// <summary>
/// Metadata component used to keep consistent UIDs inside map files cross saving.
/// </summary>
/// <remarks>
/// This component stores the previous map UID of entities from map load.
/// This can then be used to re-serialize the entity with the same UID for the merge driver to recognize.
/// </remarks>
[RegisterComponent, UnsavedComponent]
public sealed partial class MapSaveIdComponent : Component
{
public int Uid { get; set; }
}
}

View File

@@ -27,7 +27,7 @@ namespace Robust.Server.GameObjects
/// Manager for entities -- controls things like template loading and instantiation
/// </summary>
[UsedImplicitly] // DI Container
public sealed class ServerEntityManager : EntityManager, IServerEntityManagerInternal
public sealed class ServerEntityManager : EntityManager, IServerEntityManager
{
private static readonly Gauge EntitiesCount = Metrics.CreateGauge(
"robust_entities_count",
@@ -61,32 +61,6 @@ namespace Robust.Server.GameObjects
_pvs = System<PvsSystem>();
}
EntityUid IServerEntityManagerInternal.AllocEntity(EntityPrototype? prototype)
{
return AllocEntity(prototype, out _);
}
void IServerEntityManagerInternal.FinishEntityLoad(EntityUid entity, IEntityLoadContext? context)
{
LoadEntity(entity, context);
}
void IServerEntityManagerInternal.FinishEntityLoad(EntityUid entity, EntityPrototype? prototype, IEntityLoadContext? context)
{
LoadEntity(entity, context, prototype);
}
void IServerEntityManagerInternal.FinishEntityInitialization(EntityUid entity, MetaDataComponent? meta)
{
InitializeEntity(entity, meta);
}
[Obsolete("Use StartEntity")]
void IServerEntityManagerInternal.FinishEntityStartup(EntityUid entity)
{
StartEntity(entity);
}
internal override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
{
if (prototypeName == null)

View File

@@ -20,6 +20,10 @@ namespace Robust.Server.GameStates;
/// </summary>
internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion<PvsData> memoryRegion)
{
#if DEBUG
public HashSet<NetEntity> ToSendSet = new();
#endif
public readonly ICommonSession Session = session;
public readonly ResizableMemoryRegion<PvsData> DataMemory = memoryRegion;
@@ -180,6 +184,9 @@ internal struct PvsMetadata
public NetEntity NetEntity;
public GameTick LastModifiedTick;
// TODO PVS maybe store as int?
// Theres extra space anyways, and the mask checks always need to convert to an int first, so it'd probably be faster too.
public ushort VisMask;
public EntityLifeStage LifeStage;
#if DEBUG
@@ -197,7 +204,7 @@ internal struct PvsMetadata
{
DebugTools.AssertEqual(NetEntity, comp.NetEntity);
DebugTools.AssertEqual(VisMask, comp.VisibilityMask);
DebugTools.AssertEqual(LifeStage, comp.EntityLifeStage);
DebugTools.Assert(LifeStage == comp.EntityLifeStage);
DebugTools.Assert(LastModifiedTick == comp.EntityLastModifiedTick || LastModifiedTick.Value == 0);
}
}

View File

@@ -5,13 +5,14 @@ using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
public sealed class PvsOverrideSystem : EntitySystem
public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IConsoleHost _console = default!;
@@ -28,7 +29,8 @@ public sealed class PvsOverrideSystem : EntitySystem
base.Initialize();
EntityManager.EntityDeleted += OnDeleted;
_player.PlayerStatusChanged += OnPlayerStatusChanged;
SubscribeLocalEvent<MapChangedEvent>(OnMapChanged);
SubscribeLocalEvent<MapRemovedEvent>(OnMapRemoved);
SubscribeLocalEvent<MapCreatedEvent>(OnMapCreated);
SubscribeLocalEvent<GridInitializeEvent>(OnGridCreated);
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
@@ -132,10 +134,12 @@ public sealed class PvsOverrideSystem : EntitySystem
/// <summary>
/// Forces the entity, all of its parents, and all of its children to ignore normal PVS range limitations,
/// causing them to always be sent to all clients.
/// causing them to be sent to all clients. This will still respect visibility masks, it only overrides the range.
/// </summary>
public void AddGlobalOverride(EntityUid uid)
public override void AddGlobalOverride(EntityUid uid)
{
base.AddGlobalOverride(uid);
if (GlobalOverride.Add(uid))
_hasOverride.Add(uid);
}
@@ -143,8 +147,10 @@ public sealed class PvsOverrideSystem : EntitySystem
/// <summary>
/// Removes an entity from the global overrides.
/// </summary>
public void RemoveGlobalOverride(EntityUid uid)
public override void RemoveGlobalOverride(EntityUid uid)
{
base.RemoveGlobalOverride(uid);
GlobalOverride.Remove(uid);
// Not bothering to clear _hasOverride, as we'd have to check all the other collections, and at that point we
// might as well just do that when the entity gets deleted anyways.
@@ -154,8 +160,9 @@ public sealed class PvsOverrideSystem : EntitySystem
/// This causes an entity and all of its parents to always be sent to all players.
/// </summary>
/// <remarks>
/// This differs from <see cref="AddGlobalOverride"/> as it does not send children, and will ignore a players usual
/// PVS budget. You generally shouldn't use this unless an entity absolutely always needs to be sent to all clients.
/// This differs from <see cref="AddGlobalOverride"/> as it does not send children, will ignore a players usual
/// PVS budget, and ignores visibility masks. You generally shouldn't use this unless an entity absolutely always
/// needs to be sent to all clients.
/// </remarks>
public void AddForceSend(EntityUid uid)
{
@@ -171,11 +178,12 @@ public sealed class PvsOverrideSystem : EntitySystem
}
/// <summary>
/// This causes an entity and all of its parents to always be sent to a player..
/// This causes an entity and all of its parents to always be sent to a player.
/// </summary>
/// <remarks>
/// This differs from <see cref="AddSessionOverride"/> as it does not send children, and will ignore a players usual
/// PVS budget. You generally shouldn't use this unless an entity absolutely always needs to be sent to a client.
/// This differs from <see cref="AddSessionOverride"/> as it does not send children, will ignore a players usual
/// PVS budget, and ignores visibility masks. You generally shouldn't use this unless an entity absolutely always
/// needs to be sent to a client.
/// </remarks>
public void AddForceSend(EntityUid uid, ICommonSession session)
{
@@ -201,10 +209,12 @@ public sealed class PvsOverrideSystem : EntitySystem
/// <summary>
/// Forces the entity, all of its parents, and all of its children to ignore normal PVS range limitations for a
/// specific session.
/// specific session. This will still respect visibility masks, it only overrides the range.
/// </summary>
public void AddSessionOverride(EntityUid uid, ICommonSession session)
public override void AddSessionOverride(EntityUid uid, ICommonSession session)
{
base.AddSessionOverride(uid, session);
if (SessionOverrides.GetOrNew(session).Add(uid))
_hasOverride.Add(uid);
}
@@ -212,8 +222,10 @@ public sealed class PvsOverrideSystem : EntitySystem
/// <summary>
/// Removes an entity from a session's overrides.
/// </summary>
public void RemoveSessionOverride(EntityUid uid, ICommonSession session)
public override void RemoveSessionOverride(EntityUid uid, ICommonSession session)
{
base.RemoveSessionOverride(uid, session);
if (!SessionOverrides.TryGetValue(session, out var overrides))
return;
@@ -226,13 +238,17 @@ public sealed class PvsOverrideSystem : EntitySystem
/// <summary>
/// Forces the entity, all of its parents, and all of its children to ignore normal PVS range limitations,
/// causing them to always be sent to all clients.
/// causing them to always be sent to the specified clients. This will still respect visibility masks, it only
/// overrides the range.
/// </summary>
public void AddSessionOverrides(EntityUid uid, Filter filter)
public override void AddSessionOverrides(EntityUid uid, Filter filter)
{
_hasOverride.Add(uid);
base.AddSessionOverrides(uid, filter);
foreach (var session in filter.Recipients)
{
AddSessionOverride(uid, session);
SessionOverrides.GetOrNew(session).Add(uid);
}
}
@@ -259,14 +275,6 @@ public sealed class PvsOverrideSystem : EntitySystem
#region Map/Grid Events
private void OnMapChanged(MapChangedEvent ev)
{
if (ev.Created)
OnMapCreated(ev);
else
OnMapDestroyed(ev);
}
private void OnGridRemoved(GridRemovalEvent ev)
{
RemoveForceSend(ev.EntityUid);
@@ -279,12 +287,12 @@ public sealed class PvsOverrideSystem : EntitySystem
AddForceSend(ev.EntityUid);
}
private void OnMapDestroyed(MapChangedEvent ev)
private void OnMapRemoved(MapRemovedEvent ev)
{
RemoveForceSend(ev.Uid);
}
private void OnMapCreated(MapChangedEvent ev)
private void OnMapCreated(MapCreatedEvent ev)
{
// TODO PVS remove this requirement.
// I think this just required refactoring client game state logic so it doesn't sending maps/grids to nullspace.

View File

@@ -303,11 +303,8 @@ internal sealed partial class PvsSystem
RemoveRoot(ev.EntityUid);
}
private void OnMapChanged(MapChangedEvent ev)
private void OnMapChanged(MapRemovedEvent ev)
{
if (!ev.Destroyed)
return;
RemoveRoot(ev.Uid);
}

View File

@@ -19,14 +19,15 @@ internal sealed partial class PvsSystem
private void AddAllOverrides(PvsSession session)
{
var mask = session.VisMask;
var fromTick = session.FromTick;
RaiseExpandEvent(session, fromTick);
var mask = RaiseExpandEvent(session, fromTick);
foreach (ref var ent in CollectionsMarshal.AsSpan(_cachedGlobalOverride))
{
ref var meta = ref _metadataMemory.GetRef(ent.Ptr.Index);
meta.Validate(ent.Meta);
// PVS overrides still respect visibility masks
if ((mask & meta.VisMask) == meta.VisMask)
AddEntity(session, ref ent, ref meta, fromTick);
}
@@ -36,7 +37,7 @@ internal sealed partial class PvsSystem
foreach (var uid in sessionOverrides)
{
RecursivelyAddOverride(session, uid, fromTick, addChildren: true);
RecursivelyAddOverride(session, uid, fromTick, addChildren: true, mask);
}
}
@@ -45,22 +46,23 @@ internal sealed partial class PvsSystem
/// </summary>
private void AddForcedEntities(PvsSession session)
{
// Forced overrides do not respect visibility masks, so we set all bits.
var mask = -1;
// Ignore PVS budgets
session.Budget = new() {NewLimit = int.MaxValue, EnterLimit = int.MaxValue};
var mask = session.VisMask;
var fromTick = session.FromTick;
foreach (ref var ent in CollectionsMarshal.AsSpan(_cachedForceOverride))
{
ref var meta = ref _metadataMemory.GetRef(ent.Ptr.Index);
meta.Validate(ent.Meta);
if ((mask & meta.VisMask) == meta.VisMask)
AddEntity(session, ref ent, ref meta, fromTick);
AddEntity(session, ref ent, ref meta, fromTick);
}
foreach (var uid in session.Viewers)
{
RecursivelyAddOverride(session, uid, fromTick, addChildren: false);
RecursivelyAddOverride(session, uid, fromTick, addChildren: false, mask);
}
if (!_pvsOverride.SessionForceSend.TryGetValue(session.Session, out var sessionForce))
@@ -68,13 +70,13 @@ internal sealed partial class PvsSystem
foreach (var uid in sessionForce)
{
RecursivelyAddOverride(session, uid, fromTick, addChildren: false);
RecursivelyAddOverride(session, uid, fromTick, addChildren: false, mask);
}
}
private void RaiseExpandEvent(PvsSession session, GameTick fromTick)
private int RaiseExpandEvent(PvsSession session, GameTick fromTick)
{
var expandEvent = new ExpandPvsEvent(session.Session);
var expandEvent = new ExpandPvsEvent(session.Session, session.VisMask);
if (session.Session.AttachedEntity != null)
RaiseLocalEvent(session.Session.AttachedEntity.Value, ref expandEvent, true);
@@ -85,23 +87,25 @@ internal sealed partial class PvsSystem
{
foreach (var uid in expandEvent.Entities)
{
RecursivelyAddOverride(session, uid, fromTick, addChildren: false);
RecursivelyAddOverride(session, uid, fromTick, addChildren: false, expandEvent.VisMask);
}
}
if (expandEvent.RecursiveEntities == null)
return;
return expandEvent.VisMask;
foreach (var uid in expandEvent.RecursiveEntities)
{
RecursivelyAddOverride(session, uid, fromTick, addChildren: true);
RecursivelyAddOverride(session, uid, fromTick, addChildren: true, expandEvent.VisMask);
}
return expandEvent.VisMask;
}
/// <summary>
/// Recursively add an entity and all of its parents to the to-send set. This optionally also adds all children.
/// </summary>
private bool RecursivelyAddOverride(PvsSession session, EntityUid uid, GameTick fromTick, bool addChildren)
private bool RecursivelyAddOverride(PvsSession session, EntityUid uid, GameTick fromTick, bool addChildren, int mask)
{
if (!_xformQuery.TryGetComponent(uid, out var xform))
{
@@ -116,17 +120,20 @@ internal sealed partial class PvsSystem
// to the toSend set, it doesn't guarantee that its parents have been. E.g., if a player ghost just teleported
// to follow a far away entity, the player's own entity is still being sent, but we need to ensure that we also
// send the new parents, which may otherwise be delayed because of the PVS budget.
if (parent.IsValid() && !RecursivelyAddOverride(session, parent, fromTick, false))
if (parent.IsValid() && !RecursivelyAddOverride(session, parent, fromTick, false, mask))
return false;
if (!_metaQuery.TryGetComponent(uid, out var meta))
return false;
if ((mask & meta.VisibilityMask) != meta.VisibilityMask)
return false;
if (!AddEntity(session, (uid, meta), fromTick))
return false;
if (addChildren)
RecursivelyAddChildren(session, xform, fromTick);
RecursivelyAddChildren(session, xform, fromTick, mask);
return true;
}
@@ -134,7 +141,7 @@ internal sealed partial class PvsSystem
/// <summary>
/// Recursively add an entity and all of its children to the to-send set.
/// </summary>
private void RecursivelyAddChildren(PvsSession session, TransformComponent xform, GameTick fromTick)
private void RecursivelyAddChildren(PvsSession session, TransformComponent xform, GameTick fromTick, int mask)
{
foreach (var child in xform._children)
{
@@ -145,10 +152,14 @@ internal sealed partial class PvsSystem
}
var metadata = _metaQuery.GetComponent(child);
if (!AddEntity(session, (child, metadata), fromTick))
return;
RecursivelyAddChildren(session, childXform, fromTick);
if ((mask & metadata.VisibilityMask) != metadata.VisibilityMask)
continue;
if (!AddEntity(session, (child, metadata), fromTick))
return; // Budget was exceeded (or some error occurred) -> return instead of continue.
RecursivelyAddChildren(session, childXform, fromTick, mask);
}
}

View File

@@ -142,18 +142,32 @@ internal sealed partial class PvsSystem
if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
{
var rep = new EntityStringRepresentation(entity);
Log.Error($"Attempted to add a deleted entity to PVS send set: '{rep}'. Deletion queued: {EntityManager.IsQueuedForDeletion(uid)}. Trace:\n{Environment.StackTrace}");
// This can happen if some entity was some removed from it's parent while that parent was being deleted.
// As a result the entity was marked for deletion but was never actually properly deleted.
EntityManager.QueueDeleteEntity(uid);
bool queued;
lock (_toDelete)
{
queued = EntityManager.IsQueuedForDeletion(uid) || _toDelete.Contains(uid);
if (!queued)
_toDelete.Add(uid);
}
var rep = new EntityStringRepresentation(entity);
Log.Error($"Attempted to add a deleted entity to PVS send set: '{rep}'. Deletion queued: {queued}. Trace:\n{Environment.StackTrace}");
return false;
}
data.LastSeen = _gameTiming.CurTick;
session.ToSend!.Add(entity.Comp.PvsData);
// TODO PVS PERFORMANCE
// Investigate whether its better to defer actually creating the entity state & populating session.States here?
// I.e., should be be constructing the to-send list & to-get-states lists, and then separately getting all states
// after we have gotten all entities? If the CPU can focus on only processing data in session.DataMemory without
// having to access miscellaneous component info, maybe it will be faster?
// Though for that to work I guess it also has to avoid accessing the metadata component's lifestage?
if (session.RequestedFull)
{
var state = GetFullEntityState(session.Session, uid, meta);

View File

@@ -94,6 +94,8 @@ internal sealed partial class PvsSystem : EntitySystem
/// </summary>
private readonly List<GameTick> _deletedTick = new();
private readonly HashSet<EntityUid> _toDelete = new();
/// <summary>
/// The sessions that are currently being processed. Note that this is in general used by parallel & async tasks.
/// Hence player disconnection processing is deferred and only run via <see cref="ProcessDisconnections"/>.
@@ -127,7 +129,7 @@ internal sealed partial class PvsSystem : EntitySystem
_metaQuery = GetEntityQuery<MetaDataComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
SubscribeLocalEvent<MapChangedEvent>(OnMapChanged);
SubscribeLocalEvent<MapRemovedEvent>(OnMapChanged);
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
SubscribeLocalEvent<TransformComponent, TransformStartupEvent>(OnTransformStartup);
@@ -195,6 +197,12 @@ internal sealed partial class PvsSystem : EntitySystem
// Construct & serialize the game state for each player (and for the replay).
SerializeStates();
foreach (var uid in _toDelete)
{
EntityManager.QueueDeleteEntity(uid);
}
_toDelete.Clear();
// Compress & send the states.
SendStates();
@@ -302,7 +310,9 @@ internal sealed partial class PvsSystem : EntitySystem
// Process all entities in visible PVS chunks
AddPvsChunks(session);
#if DEBUG
VerifySessionData(session);
#endif
var toSend = session.ToSend!;
session.ToSend = null;
@@ -332,11 +342,12 @@ internal sealed partial class PvsSystem : EntitySystem
session.Overflow = oldEntry.Value;
}
[Conditional("DEBUG")]
#if DEBUG
private void VerifySessionData(PvsSession pvsSession)
{
var toSend = pvsSession.ToSend;
var toSendSet = new HashSet<NetEntity>(toSend!.Count);
var toSend = pvsSession.ToSend!;
var toSendSet = pvsSession.ToSendSet;
toSendSet.Clear();
foreach (var intPtr in toSend)
{
@@ -360,6 +371,7 @@ internal sealed partial class PvsSystem : EntitySystem
|| data.LastSeen == _gameTiming.CurTick - 1);
}
}
#endif
private (Vector2 worldPos, float range, EntityUid? map) CalcViewBounds(Entity<TransformComponent, EyeComponent?> eye)
{
@@ -461,18 +473,27 @@ internal sealed partial class PvsSystem : EntitySystem
}
[ByRefEvent]
public struct ExpandPvsEvent(ICommonSession session)
public struct ExpandPvsEvent(ICommonSession session, int mask)
{
public readonly ICommonSession Session = session;
/// <summary>
/// List of entities that will get added to this session's PVS set.
/// List of entities that will get added to this session's PVS set. This will still respect visibility masks.
/// </summary>
public List<EntityUid>? Entities;
/// <summary>
/// List of entities that will get added to this session's PVS set. Unlike <see cref="Entities"/> this will also
/// recursively add all children of the given entity.
/// recursively add all children of the given entity. This will still respect visibility masks.
/// </summary>
public List<EntityUid>? RecursiveEntities;
/// <summary>
/// Visibility mask to use when adding entities. Defaults to the usual visibility mask for that client.
/// </summary>
/// <remarks>
/// Note that this mask will affect all global & session overrides from <see cref="PvsOverrideSystem"/> for this
/// client, not just the entities in <see cref="Entities"/> and <see cref="RecursiveEntities"/>.
/// </remarks>
public int VisMask = mask;
}

View File

@@ -1,12 +0,0 @@
using Robust.Shared.GameObjects;
namespace Robust.Server.Maps;
/// <summary>
/// Added to Maps that were loaded by MapLoaderSystem. If not present then this map was created externally.
/// </summary>
[RegisterComponent]
public sealed partial class LoadedMapComponent : Component
{
}

View File

@@ -1,59 +0,0 @@
using System.Numerics;
using JetBrains.Annotations;
using Robust.Shared.Maths;
namespace Robust.Server.Maps
{
[PublicAPI]
public sealed class MapLoadOptions
{
/// <summary>
/// If true, UID components will be created for loaded entities
/// to maintain consistency upon subsequent savings.
/// </summary>
public bool StoreMapUids { get; set; }
/// <summary>
/// Offset to apply to the loaded objects.
/// </summary>
public Vector2 Offset
{
get => _offset;
set
{
TransformMatrix = Matrix3Helpers.CreateTransform(value, Rotation);
_offset = value;
}
}
private Vector2 _offset = Vector2.Zero;
/// <summary>
/// Rotation to apply to the loaded objects as a collective, around 0, 0.
/// </summary>
/// <remarks>Setting this overrides </remarks>
public Angle Rotation
{
get => _rotation;
set
{
TransformMatrix = Matrix3Helpers.CreateTransform(Offset, value);
_rotation = value;
}
}
private Angle _rotation = Angle.Zero;
public Matrix3x2 TransformMatrix { get; set; } = Matrix3x2.Identity;
/// <summary>
/// If there is a map entity serialized should we also load it.
/// </summary>
/// <remarks>
/// This should be set to false if you want to load a map file onto an existing map and do not wish to overwrite the existing entity.
/// </remarks>
public bool LoadMap { get; set; } = true;
public bool DoMapInit = false;
}
}

View File

@@ -67,7 +67,6 @@ namespace Robust.Server
deps.Register<IResourceManagerInternal, ResourceManager>();
deps.Register<EntityManager, ServerEntityManager>();
deps.Register<IServerEntityManager, ServerEntityManager>();
deps.Register<IServerEntityManagerInternal, ServerEntityManager>();
deps.Register<IServerGameStateManager, ServerGameStateManager>();
deps.Register<IReplayRecordingManager, ReplayRecordingManager>();
deps.Register<IReplayRecordingManagerInternal, ReplayRecordingManager>();

View File

@@ -336,6 +336,7 @@ namespace Robust.Server.ServerStatus
{
RespondShared();
_context.Response.StatusCode = (int)code;
_context.Response.ContentType = "application/json";
await JsonSerializer.SerializeAsync(_context.Response.OutputStream, jsonData);

View File

@@ -50,14 +50,14 @@ public sealed class GamePrototypeLoadManager : SharedPrototypeLoadManager
internal void SendToNewUser(INetChannel channel)
{
if (LoadedPrototypes.Count == 0)
return;
// Just dump all the prototypes on connect, before them missing could be an issue.
foreach (var prototype in LoadedPrototypes)
var msg = new GamePrototypeLoadMessage
{
var msg = new GamePrototypeLoadMessage
{
PrototypeData = prototype
};
channel.SendMessage(msg);
}
PrototypeData = string.Join("\n\n", LoadedPrototypes)
};
channel.SendMessage(msg);
}
}

View File

@@ -346,8 +346,8 @@ namespace Robust.Shared.CompNetworkGenerator
if (value is GlobalEntityUidName or GlobalNullableEntityUidName)
{
networkedType = $"Dictionary<{key}, {value}>";
value = valueNullable ? GlobalNetEntityNullableName : GlobalNetEntityName;
networkedType = $"Dictionary<{key}, {value}>";
stateFields.Append($@"
public {networkedType} {name} = default!;");
@@ -575,12 +575,14 @@ public partial class {componentName}{deltaInterface}
{deltaCompFields}
[System.Serializable, NetSerializable]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class {stateName} : IComponentState
{{{stateFields}
{cloneMethod}
}}
[RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class {componentName}_AutoNetworkSystem : EntitySystem
{{
public override void Initialize()

View File

@@ -39,7 +39,6 @@ namespace Robust.Shared.Maths
/// <param name="dir"></param>
public Angle(Vector2 dir)
{
dir = dir.Normalized();
Theta = Math.Atan2(dir.Y, dir.X);
}

View File

@@ -6,4 +6,3 @@
[assembly: InternalsVisibleTo("Robust.Client")]
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("Content.Benchmarks")]

View File

@@ -0,0 +1,90 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.Utility;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Prototypes;
namespace Robust.Shared.Audio;
/// <summary>
/// Represents a path to a sound resource, either as a literal path or as a collection ID and index.
/// </summary>
/// <seealso cref="ResolvedPathSpecifier"/>
/// <seealso cref="ResolvedCollectionSpecifier"/>
[Serializable, NetSerializable]
public abstract partial class ResolvedSoundSpecifier {
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
public static implicit operator ResolvedSoundSpecifier(string s) => new ResolvedPathSpecifier(s);
[Obsolete("String literals for sounds are deprecated, use a SoundSpecifier or ResolvedSoundSpecifier as appropriate instead")]
public static implicit operator ResolvedSoundSpecifier(ResPath s) => new ResolvedPathSpecifier(s);
/// <summary>
/// Returns whether <c>s</c> is null, or if it contains an empty path/collection ID.
/// </summary>
public static bool IsNullOrEmpty(ResolvedSoundSpecifier? s) {
return s switch {
null => true,
ResolvedPathSpecifier path => path.Path.ToString() == "",
ResolvedCollectionSpecifier collection => string.IsNullOrEmpty(collection.Collection),
_ => throw new ArgumentOutOfRangeException("s", s, "argument is not a ResolvedPathSpecifier or a ResolvedCollectionSpecifier"),
};
}
}
/// <summary>
/// Represents a path to a sound resource as a literal path.
/// </summary>
/// <seealso cref="ResolvedCollectionSpecifier"/>
[Serializable, NetSerializable]
public sealed partial class ResolvedPathSpecifier : ResolvedSoundSpecifier {
/// <summary>
/// The resource path of the sound.
/// </summary>
public ResPath Path { get; private set; }
override public string ToString() =>
$"ResolvedPathSpecifier({Path})";
[UsedImplicitly]
private ResolvedPathSpecifier()
{
}
public ResolvedPathSpecifier(ResPath path)
{
Path = path;
}
public ResolvedPathSpecifier(string path) : this(new ResPath(path))
{
}
}
/// <summary>
/// Represents a path to a sound resource as a collection ID and index.
/// </summary>
/// <seealso cref="ResolvedPathSpecifier"/>
[Serializable, NetSerializable]
public sealed partial class ResolvedCollectionSpecifier : ResolvedSoundSpecifier {
/// <summary>
/// The ID of the <see cref="SoundCollectionPrototype">sound collection</see> to look up.
/// </summary>
public ProtoId<SoundCollectionPrototype>? Collection { get; private set; }
/// <summary>
/// The index of the file in the associated sound collection to play.
/// </summary>
public int Index { get; private set; }
override public string ToString() =>
$"ResolvedCollectionSpecifier({Collection}, {Index})";
[UsedImplicitly]
private ResolvedCollectionSpecifier()
{
}
public ResolvedCollectionSpecifier(string collection, int index)
{
Collection = collection;
Index = index;
}
}

View File

@@ -27,6 +27,9 @@ public sealed partial class SoundPathSpecifier : SoundSpecifier
[DataField(Node, customTypeSerializer: typeof(ResPathSerializer), required: true)]
public ResPath Path { get; private set; }
override public string ToString() =>
$"SoundPathSpecifier({Path})";
[UsedImplicitly]
private SoundPathSpecifier()
{
@@ -52,6 +55,9 @@ public sealed partial class SoundCollectionSpecifier : SoundSpecifier
[DataField(Node, customTypeSerializer: typeof(PrototypeIdSerializer<SoundCollectionPrototype>), required: true)]
public string? Collection { get; private set; }
override public string ToString() =>
$"SoundCollectionSpecifier({Collection})";
[UsedImplicitly]
public SoundCollectionSpecifier() { }

View File

@@ -37,6 +37,8 @@ public abstract partial class SharedAudioSystem : EntitySystem
[Dependency] protected readonly MetaDataSystem MetadataSys = default!;
[Dependency] protected readonly SharedTransformSystem XformSystem = default!;
private const float AudioDespawnBuffer = 1f;
/// <summary>
/// Default max range at which the sound can be heard.
/// </summary>
@@ -234,7 +236,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
{
var timed = EnsureComp<TimedDespawnComponent>(entity.Value);
var audioLength = GetAudioLength(component.FileName);
timed.Lifetime = (float) audioLength.TotalSeconds + 0.01f;
timed.Lifetime = (float) audioLength.TotalSeconds + AudioDespawnBuffer;
}
break;
}
@@ -281,33 +283,60 @@ public abstract partial class SharedAudioSystem : EntitySystem
}
/// <summary>
/// Resolves the filepath to a sound file.
/// Resolve a sound specifier so it can be consistently played back on all clients.
/// </summary>
public string GetSound(SoundSpecifier specifier)
public ResolvedSoundSpecifier ResolveSound(SoundSpecifier specifier)
{
switch (specifier)
{
case SoundPathSpecifier path:
return path.Path == default ? string.Empty : path.Path.ToString();
return new ResolvedPathSpecifier(path.Path == default ? string.Empty : path.Path.ToString());
case SoundCollectionSpecifier collection:
{
if (collection.Collection == null)
return string.Empty;
return new ResolvedPathSpecifier(string.Empty);
var soundCollection = ProtoMan.Index<SoundCollectionPrototype>(collection.Collection);
return RandMan.Pick(soundCollection.PickFiles).ToString();
var index = RandMan.Next(soundCollection.PickFiles.Count);
return new ResolvedCollectionSpecifier(collection.Collection, index);
}
}
return string.Empty;
return new ResolvedPathSpecifier(string.Empty);
}
/// <summary>
/// Resolves the filepath to a sound file.
/// </summary>
[Obsolete("Use ResolveSound() and pass around resolved sound specifiers instead.")]
public string GetSound(SoundSpecifier specifier)
{
var resolved = ResolveSound(specifier);
return GetAudioPath(resolved);
}
#region AudioParams
protected Entity<AudioComponent> SetupAudio(string? fileName, AudioParams? audioParams, bool initialize = true, TimeSpan? length = null)
[return: NotNullIfNotNull(nameof(specifier))]
public string? GetAudioPath(ResolvedSoundSpecifier? specifier)
{
return specifier switch {
ResolvedPathSpecifier path =>
path.Path.ToString(),
ResolvedCollectionSpecifier collection =>
collection.Collection is null ?
string.Empty :
ProtoMan.Index<SoundCollectionPrototype>(collection.Collection).PickFiles[collection.Index].ToString(),
null => null,
_ => throw new ArgumentOutOfRangeException("specifier", specifier, "argument is not a ResolvedPathSpecifier or a ResolvedCollectionSpecifier"),
};
}
protected Entity<AudioComponent> SetupAudio(ResolvedSoundSpecifier? specifier, AudioParams? audioParams, bool initialize = true, TimeSpan? length = null)
{
var uid = EntityManager.CreateEntityUninitialized("Audio", MapCoordinates.Nullspace);
var fileName = GetAudioPath(specifier);
DebugTools.Assert(!string.IsNullOrEmpty(fileName) || length is not null);
MetadataSys.SetEntityName(uid, $"Audio ({fileName})", raiseEvents: false);
audioParams ??= AudioParams.Default;
@@ -322,7 +351,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
var despawn = AddComp<TimedDespawnComponent>(uid);
// Don't want to clip audio too short due to imprecision.
despawn.Lifetime = (float) length.Value.TotalSeconds + 0.01f;
despawn.Lifetime = (float) length.Value.TotalSeconds + AudioDespawnBuffer;
}
if (comp.Params.Variation != null && comp.Params.Variation.Value != 0f)
@@ -393,8 +422,9 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <summary>
/// Gets the timespan of the specified audio.
/// </summary>
public TimeSpan GetAudioLength(string filename)
public TimeSpan GetAudioLength(ResolvedSoundSpecifier specifier)
{
var filename = GetAudioPath(specifier) ?? string.Empty;
if (!filename.StartsWith("/"))
throw new ArgumentException("Path must be rooted");
@@ -427,7 +457,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file globally, without position.
@@ -436,7 +466,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="playerFilter">The set of players that will hear the sound.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(SoundSpecifier? sound, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null)
{
return sound == null ? null : PlayGlobal(GetSound(sound), playerFilter, recordReplay, audioParams ?? sound.Params);
return sound == null ? null : PlayGlobal(ResolveSound(sound), playerFilter, recordReplay, audioParams ?? sound.Params);
}
/// <summary>
@@ -444,7 +474,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, ICommonSession recipient, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? filename, ICommonSession recipient, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file globally, without position.
@@ -453,7 +483,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="recipient">The player that will hear the sound.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(SoundSpecifier? sound, ICommonSession recipient, AudioParams? audioParams = null)
{
return sound == null ? null : PlayGlobal(GetSound(sound), recipient, audioParams ?? sound.Params);
return sound == null ? null : PlayGlobal(ResolveSound(sound), recipient, audioParams ?? sound.Params);
}
public abstract void LoadStream<T>(Entity<AudioComponent> entity, T stream);
@@ -463,7 +493,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(string? filename, EntityUid recipient, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(ResolvedSoundSpecifier? filename, EntityUid recipient, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file globally, without position.
@@ -472,7 +502,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="recipient">The player that will hear the sound.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayGlobal(SoundSpecifier? sound, EntityUid recipient, AudioParams? audioParams = null)
{
return sound == null ? null : PlayGlobal(GetSound(sound), recipient, audioParams ?? sound.Params);
return sound == null ? null : PlayGlobal(ResolveSound(sound), recipient, audioParams ?? sound.Params);
}
/// <summary>
@@ -481,7 +511,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string? filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file following an entity.
@@ -489,7 +519,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file following an entity.
@@ -497,7 +527,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(string? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(ResolvedSoundSpecifier? filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file following an entity.
@@ -507,7 +537,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(SoundSpecifier? sound, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
{
return sound == null ? null : PlayEntity(GetSound(sound), playerFilter, uid, recordReplay, audioParams ?? sound.Params);
return sound == null ? null : PlayEntity(ResolveSound(sound), playerFilter, uid, recordReplay, audioParams ?? sound.Params);
}
/// <summary>
@@ -518,7 +548,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(SoundSpecifier? sound, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null)
{
return sound == null ? null : PlayEntity(GetSound(sound), recipient, uid, audioParams ?? sound.Params);
return sound == null ? null : PlayEntity(ResolveSound(sound), recipient, uid, audioParams ?? sound.Params);
}
/// <summary>
@@ -529,7 +559,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayEntity(SoundSpecifier? sound, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null)
{
return sound == null ? null : PlayEntity(GetSound(sound), recipient, uid, audioParams ?? sound.Params);
return sound == null ? null : PlayEntity(ResolveSound(sound), recipient, uid, audioParams ?? sound.Params);
}
/// <summary>
@@ -539,7 +569,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(SoundSpecifier? sound, EntityUid uid, AudioParams? audioParams = null)
{
return sound == null ? null : PlayPvs(GetSound(sound), uid, audioParams ?? sound.Params);
return sound == null ? null : PlayPvs(ResolveSound(sound), uid, audioParams ?? sound.Params);
}
/// <summary>
@@ -549,7 +579,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="coordinates">The EntityCoordinates to attach the audio source to.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(SoundSpecifier? sound, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return sound == null ? null : PlayPvs(GetSound(sound), coordinates, audioParams ?? sound.Params);
return sound == null ? null : PlayPvs(ResolveSound(sound), coordinates, audioParams ?? sound.Params);
}
/// <summary>
@@ -557,7 +587,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// </summary>
/// <param name="sound">The sound specifier that points the audio file(s) that should be played.</param>
/// <param name="coordinates">The EntityCoordinates to attach the audio source to.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string? filename,
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? filename,
EntityCoordinates coordinates, AudioParams? audioParams = null);
/// <summary>
@@ -565,7 +595,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// </summary>
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="uid">The UID of the entity "emitting" the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(string? filename, EntityUid uid,
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayPvs(ResolvedSoundSpecifier? filename, EntityUid uid,
AudioParams? audioParams = null);
/// <summary>
@@ -602,7 +632,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="playerFilter">The set of players that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file at a static position.
@@ -610,7 +640,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file at a static position.
@@ -618,7 +648,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="filename">The resource path to the OGG Vorbis file to play.</param>
/// <param name="recipient">The player that will hear the sound.</param>
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(string? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
public abstract (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(ResolvedSoundSpecifier? filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null);
/// <summary>
/// Play an audio file at a static position.
@@ -628,7 +658,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
return sound == null ? null : PlayStatic(GetSound(sound), playerFilter, coordinates, recordReplay, audioParams);
return sound == null ? null : PlayStatic(ResolveSound(sound), playerFilter, coordinates, recordReplay, audioParams);
}
/// <summary>
@@ -639,7 +669,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return sound == null ? null : PlayStatic(GetSound(sound), recipient, coordinates, audioParams ?? sound.Params);
return sound == null ? null : PlayStatic(ResolveSound(sound), recipient, coordinates, audioParams ?? sound.Params);
}
/// <summary>
@@ -650,7 +680,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// <param name="coordinates">The coordinates at which to play the audio.</param>
public (EntityUid Entity, Components.AudioComponent Component)? PlayStatic(SoundSpecifier? sound, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
return sound == null ? null : PlayStatic(GetSound(sound), recipient, coordinates, audioParams ?? sound.Params);
return sound == null ? null : PlayStatic(ResolveSound(sound), recipient, coordinates, audioParams ?? sound.Params);
}
// These are just here for replays now.
@@ -663,7 +693,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
[NetSerializable, Serializable]
protected abstract class AudioMessage : EntityEventArgs
{
public string FileName = string.Empty;
public ResolvedSoundSpecifier Specifier = new ResolvedPathSpecifier(string.Empty);
public AudioParams AudioParams;
}

View File

@@ -1349,10 +1349,10 @@ namespace Robust.Shared
/// MaxLinVelocity is compared to the dot product of linearVelocity * frameTime.
/// </summary>
/// <remarks>
/// Default is 35 m/s. Around half a tile per tick at 60 ticks per second.
/// Default is 400 m/s in-line with Box2c. Box2d used 120m/s.
/// </remarks>
public static readonly CVarDef<float> MaxLinVelocity =
CVarDef.Create("physics.maxlinvelocity", 35f, CVar.SERVER | CVar.REPLICATED);
CVarDef.Create("physics.maxlinvelocity", 400f, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Maximum angular velocity in full rotations per second.
@@ -1364,7 +1364,6 @@ namespace Robust.Shared
public static readonly CVarDef<float> MaxAngVelocity =
CVarDef.Create("physics.maxangvelocity", 15f);
/*
* User interface
*/

View File

@@ -59,7 +59,7 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
UpdatesAfter.Add(typeof(SharedTransformSystem));
UpdatesAfter.Add(typeof(SharedPhysicsSystem));
SubscribeLocalEvent<MapChangedEvent>(MapManagerOnMapCreated);
SubscribeLocalEvent<MapCreatedEvent>(MapManagerOnMapCreated);
SubscribeLocalEvent<GridInitializeEvent>(MapManagerOnGridCreated);
SubscribeLocalEvent<TComp, ComponentStartup>(OnCompStartup);
@@ -143,11 +143,8 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
RemComp(uid, component);
}
private void MapManagerOnMapCreated(MapChangedEvent e)
private void MapManagerOnMapCreated(MapCreatedEvent e)
{
if (e.Destroyed || e.Map == MapId.Nullspace)
return;
EnsureComp<TTreeComp>(e.Uid);
}

View File

@@ -8,6 +8,58 @@ using Robust.Shared.Utility;
namespace Robust.Shared.Configuration
{
public static class CVarCommandUtil
{
/// <summary>
/// Parses a string into an object of the given type.
/// </summary>
/// <exception cref="FormatException">Thrown if the string could not be parsed into the given type.</exception>
/// <exception cref="NotSupportedException">Thrown if the type is not supported.</exception>
public static object ParseObject(Type type, string input)
{
if (type == typeof(bool))
{
if (bool.TryParse(input, out var val))
return val;
if (Parse.TryInt32(input, out var intVal))
{
if (intVal == 0) return false;
if (intVal == 1) return true;
}
throw new FormatException($"Could not parse bool value: {input}");
}
if (type == typeof(string))
{
return input;
}
if (type == typeof(int))
{
return Parse.Int32(input);
}
if (type == typeof(float))
{
return Parse.Float(input);
}
if (type == typeof(long))
{
return long.Parse(input);
}
if (type == typeof(ushort))
{
return ushort.Parse(input);
}
throw new NotSupportedException();
}
}
[SuppressMessage("ReSharper", "StringLiteralTypo")]
internal sealed class CVarCommand : LocalizedCommands
{
@@ -51,7 +103,7 @@ namespace Robust.Shared.Configuration
var type = _cfg.GetCVarType(name);
try
{
var parsed = ParseObject(type, value);
var parsed = CVarCommandUtil.ParseObject(type, value);
_cfg.SetCVar(name, parsed);
}
catch (FormatException)
@@ -95,50 +147,6 @@ namespace Robust.Shared.Configuration
return value;
}
private static object ParseObject(Type type, string input)
{
if (type == typeof(bool))
{
if (bool.TryParse(input, out var val))
return val;
if (Parse.TryInt32(input, out var intVal))
{
if (intVal == 0) return false;
if (intVal == 1) return true;
}
throw new FormatException($"Could not parse bool value: {input}");
}
if (type == typeof(string))
{
return input;
}
if (type == typeof(int))
{
return Parse.Int32(input);
}
if (type == typeof(float))
{
return Parse.Float(input);
}
if (type == typeof(long))
{
return long.Parse(input);
}
if (type == typeof(ushort))
{
return ushort.Parse(input);
}
throw new NotSupportedException();
}
}
internal sealed class CVarSubsCommand : LocalizedCommands

View File

@@ -22,7 +22,7 @@ public sealed class CommandBuffer
_commandBuffer.AddFirst(command);
}
public void Tick(byte tickRate)
public void Tick(ushort tickRate)
{
_tickrate = tickRate;

View File

@@ -138,7 +138,7 @@ internal sealed class ListMapsCommand : LocalizedEntityCommands
{
var msg = new StringBuilder();
foreach (var mapId in _map.GetAllMapIds().OrderBy(id => id.Value))
foreach (var mapId in _mapSystem.GetAllMapIds().OrderBy(id => id.Value))
{
if (!_mapSystem.TryGetMap(mapId, out var mapUid))
continue;

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Utility;
namespace Robust.Shared.Console;
@@ -74,4 +75,14 @@ public enum CompletionOptionFlags
/// (instead of adding a space to go to the next one).
/// </summary>
PartialCompletion = 1 << 0,
/// <summary>
/// Prevents suggestions containing spaces from being automatically wrapped in quotes.
/// </summary>
NoQuote = 1 << 1,
/// <summary>
/// Prevents suggestions from being escaped using <see cref="CommandParsing.Escape"/>.
/// </summary>
NoEscape = 1 << 2,
}

View File

@@ -197,6 +197,10 @@ public abstract partial class SharedContainerSystem
{
if (entity.Comp2 is { } physics)
{
// TODO CONTAINER
// Is this actually needed?
// I.e., shouldn't this just do a if (_timing.ApplyingState) return
// Here we intentionally don't dirty the physics comp. Client-side state handling will apply these same
// changes. This also ensures that the server doesn't have to send the physics comp state to every
// player for any entity inside of a container during init.

View File

@@ -88,6 +88,7 @@ namespace Robust.Shared.ContentPack
public string SystemAssemblyName = default!;
public HashSet<VerifierError> AllowedVerifierErrors = default!;
public List<string> WhitelistedNamespaces = default!;
public List<string> AllowedAssemblyPrefixes = default!;
public Dictionary<string, Dictionary<string, TypeConfig>> Types = default!;
}

View File

@@ -131,6 +131,16 @@ namespace Robust.Shared.ContentPack
return false;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
if (!loadedConfig.AllowedAssemblyPrefixes.Any(allowedNamePrefix => asmName.StartsWith(allowedNamePrefix)))
{
_sawmill.Error($"Assembly name '{asmName}' is not allowed for a content assembly");
return false;
}
if (VerifyIL)
{
if (!DoVerifyIL(asmName, resolver, peReader, reader))
@@ -179,10 +189,6 @@ namespace Robust.Shared.ContentPack
return true;
}
#pragma warning disable RA0004
var loadedConfig = _config.Result;
#pragma warning restore RA0004
var badRefs = new ConcurrentBag<EntityHandle>();
// We still do explicit type reference scanning, even though the actual whitelists work with raw members.

View File

@@ -60,7 +60,7 @@ namespace Robust.Shared.ContentPack
internal string GetPath(ResPath relPath)
{
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()));
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
}
/// <inheritdoc />

View File

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

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