mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2cf359c7a | ||
|
|
30b07b7060 | ||
|
|
977bb7385c | ||
|
|
8de8032904 | ||
|
|
f274de0f10 | ||
|
|
e128338f9d | ||
|
|
588c46273e | ||
|
|
919de8ce0e | ||
|
|
af27d2d872 | ||
|
|
45bb8740a0 | ||
|
|
4a24539629 | ||
|
|
7536c4ec68 | ||
|
|
9268c8629d | ||
|
|
0bc0cafe64 | ||
|
|
8891f3fa0a | ||
|
|
4f96c2d233 | ||
|
|
ab55d5b2f2 | ||
|
|
806c5b694b | ||
|
|
6898053dbd | ||
|
|
ae625ebad8 | ||
|
|
3c754a4f49 | ||
|
|
d84cb6327c | ||
|
|
4bfd92dbc5 | ||
|
|
c7d228c223 | ||
|
|
f244c94905 | ||
|
|
01cac6465b | ||
|
|
5a5f238d9a | ||
|
|
089224cd44 | ||
|
|
9f807f1ad2 | ||
|
|
4be95ea375 | ||
|
|
03010bf4be | ||
|
|
dacaa974d4 | ||
|
|
9f73e0398a | ||
|
|
ccc383b1bf | ||
|
|
ceb59402a1 | ||
|
|
5a6b29fcd2 | ||
|
|
6b87cd1e1c | ||
|
|
cf2d6a1dbf | ||
|
|
f8c838f425 | ||
|
|
7405904041 | ||
|
|
2eeebab275 | ||
|
|
2856bb3626 | ||
|
|
be0189748b | ||
|
|
4529a7569a | ||
|
|
2b16e4db96 | ||
|
|
64f2245194 | ||
|
|
1029047e2f | ||
|
|
45dc9ad80e | ||
|
|
54ad808eea | ||
|
|
37c75df6a2 | ||
|
|
e93c1fae61 | ||
|
|
cd0a35f542 | ||
|
|
80f2dc6dd3 | ||
|
|
139b6f796c | ||
|
|
2ee7c35fd3 | ||
|
|
56eae5ad08 | ||
|
|
0662cae224 | ||
|
|
0e2b00edd0 | ||
|
|
d48f7ecb5b | ||
|
|
e064b7a4f9 | ||
|
|
654480862e | ||
|
|
353c044b52 | ||
|
|
3dda8d9e93 | ||
|
|
41ea10083d | ||
|
|
6290bb7af1 | ||
|
|
348ab70a8d | ||
|
|
47e11e988c | ||
|
|
c459b55052 | ||
|
|
f10e96a6d1 | ||
|
|
e8bac558c6 | ||
|
|
ffa3bb7202 | ||
|
|
9bcdc95651 | ||
|
|
543088ea1f | ||
|
|
56daa63783 | ||
|
|
9fe9730d4a | ||
|
|
a1a7ea92d9 | ||
|
|
0dec6a425f | ||
|
|
56ced913b7 | ||
|
|
76b46479b6 | ||
|
|
de9a8d286a | ||
|
|
a1df0fb4af | ||
|
|
e6bc5a1057 | ||
|
|
11b24579a2 | ||
|
|
685d002bb7 | ||
|
|
2e0d18aeaf | ||
|
|
06dbff0429 | ||
|
|
15958a9447 | ||
|
|
fd5a4d9b8a | ||
|
|
6d958847cb | ||
|
|
8a04a4f3a5 | ||
|
|
7104a4f459 | ||
|
|
f29949a32c | ||
|
|
3bbbabf238 | ||
|
|
d95aca3d9e | ||
|
|
e14537074e | ||
|
|
af2d01981f | ||
|
|
7df23e047c | ||
|
|
5c7ab43049 | ||
|
|
8f75560ec4 | ||
|
|
b323c8bd1e | ||
|
|
faef44daaa | ||
|
|
fbc706f37b |
@@ -55,9 +55,9 @@
|
||||
<PackageVersion Include="Serilog" Version="4.2.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
|
||||
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
|
||||
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
|
||||
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
|
||||
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
246
RELEASE-NOTES.md
246
RELEASE-NOTES.md
@@ -54,6 +54,252 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 252.0.2
|
||||
|
||||
|
||||
## 252.0.1
|
||||
|
||||
|
||||
## 252.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* BoundUserInterfaceMessageAttempt is raised directed against entities and no longer broadcast.
|
||||
|
||||
|
||||
## 251.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Localization is now separate between client and server and is handled via cvar.
|
||||
* Contacting entities no longer can be disabled for CollisionWake to avoid destroying the contacts unnecessarily.
|
||||
|
||||
### New features
|
||||
|
||||
* Added `DirectionExtensions.AllDirections`, which contains a list of all `Direction`s for easy enumeration.
|
||||
* Add ForbidLiteralAttribute.
|
||||
* Log late MsgEntity again.
|
||||
* Show entity name in `physics shapeinfo` output.
|
||||
* Make SubscribeLocalEvent not require EntityEventArgs.
|
||||
* Add autocomplete to `tp` command.
|
||||
* Add button to jump to live chat when scrolled up.
|
||||
* Add autocomplete to `savemap` and `savegrid`.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix velocity not re-applying correctly on re-parenting.
|
||||
* Fix Equatable on FormattedMessage.
|
||||
* Fix SharedTransformSystem methods logging errors on resolves.
|
||||
|
||||
### Other
|
||||
|
||||
* Significantly optimized tile edge rendering.
|
||||
|
||||
### Internal
|
||||
|
||||
* Remove duplicate GetMassData method.
|
||||
* Inline manifold points for physics.
|
||||
|
||||
|
||||
## 250.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* The default shader now interprets negative color modulation as a flag that indicates that the light map should be ignored.
|
||||
* This can be used to avoid having to change the light map texture, thus reducing draw batches.
|
||||
* Sprite layers that are set to use the "unshaded" shader prototype now use this.
|
||||
* Any fragment shaders that previously the `VtxModulate` colour modulation variable should instead use the new `MODULATE` variable, as the former may now contain negative values.
|
||||
|
||||
### New features
|
||||
|
||||
* Add OtherBody API to contacts.
|
||||
* Make FormattedMessages Equatable.
|
||||
* AnimationCompletionEvent now has the AnimationPlayerComponent.
|
||||
* Add entity description as a tooltip on the entity spawn panel.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix serialization source generator breaking if a class has two partial locations.
|
||||
* Fix map saving throwing a `DirectoryNotFoundException` when given a path with a non-existent directory. Now it once again creates any missing directories.
|
||||
* Fix map loading taking a significant time due to MappingDataNode.Equals calls being slow.
|
||||
|
||||
### Other
|
||||
|
||||
* Add Pure to some Angle methods.
|
||||
|
||||
### Internal
|
||||
|
||||
* Cleanup some warnings in classes.
|
||||
|
||||
|
||||
## 249.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Layer is now read-only on VisibilityComponent and isn't serialized.
|
||||
|
||||
### New features
|
||||
|
||||
* Added a debug overlay for the linear and angular velocity of all entities on the screen. Use the `showvel` and `showangvel` commands to toggle it.
|
||||
* Add a GetWorldManifold overload that doesn't require a span of points.
|
||||
* Added a GetVisMaskEvent. Calling `RefreshVisibilityMask` will raise it and subscribers can update the vismask via the event rather than subscribers having to each manually try and handle the vismask directly.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* `BoxContainer` no longer causes stretching children to go below their minimum size.
|
||||
* Fix lights on other grids getting clipped due to ignoring the light range cvar.
|
||||
* Fix the `showvelocities` command.
|
||||
* Fix the DirtyFields overload not being sandbox safe for content.
|
||||
|
||||
### Internal
|
||||
|
||||
* Polygon vertices are now inlined with FixedArray8 and a separate SlimPolygon using FixedArray4 for hot paths rather than using pooled arrays.
|
||||
|
||||
|
||||
## 248.0.2
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Don't throw in overlay rendering if MapUid not found.
|
||||
|
||||
### Internal
|
||||
|
||||
* Reduce EntityManager.IsDefault allocations.
|
||||
|
||||
|
||||
## 248.0.1
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Bump ImageSharp version.
|
||||
* Fix instances of NaN gain for audio where a negative-infinity value is being used for volume.
|
||||
|
||||
|
||||
## 248.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Use `Entity<MapGridComponent>` for TileChangedEvent instead of EntityUid.
|
||||
* Audio files are no longer tempo perfect when being played if the offset is small. At some point in the future an AudioParams bool is likely to be added to enforce this.
|
||||
* MoveProxy method args got changed in the B2DynamicTree update.
|
||||
* ResPath will now assert in debug if you pass in an invalid path containing the non-standardized directory separator.
|
||||
|
||||
### New features
|
||||
|
||||
* Added a new `MapLoaderSystem.TryLoadGrid()` override that loads a grid onto a newly created map.
|
||||
* Added a CVar for the endbuffer for audio. If an audio file will play below this length (for PVS reasons) it will be ignored.
|
||||
* Added Regex.Count + StringBuilder.Chars setter to the sandbox.
|
||||
* Added a public API for PhysicsHull.
|
||||
* Made MapLoader log more helpful.
|
||||
* Add TryLoadGrid override that also creates a map at the same time.
|
||||
* Updated B2Dynamictree to the latest Box2D V3 version.
|
||||
* Added SetItems to ItemList control to set items without removing the existing ones.
|
||||
* Shaders, textures, and audio will now hot reload automatically to varying degrees. Also added IReloadManager to handle watching for file-system changes and relaying events.
|
||||
* Wrap BUI disposes in a try-catch in case of exceptions.
|
||||
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix some instances of invalid PlaybackPositions being set.
|
||||
* Play audio from the start of a file if it's only just come into PVS range / had its state handled.
|
||||
* Fix TryCopyComponents.
|
||||
* Use shell.WriteError if TryLoad fails for mapping commands.
|
||||
* Fix UI control position saving causing exceptions where the entity is cleaned-up alongside a state change.
|
||||
* Fix Map NetId completions.
|
||||
* Fix some ResPath calls using the wrong paths.
|
||||
|
||||
### Internal
|
||||
|
||||
* Remove some unused local variables and the associated warnings.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
@@ -11,6 +11,7 @@ cmd-parse-failure-uid = {$arg} is not a valid entity UID.
|
||||
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
|
||||
cmd-parse-failure-enum = {$arg} is not a {$enum} Enum.
|
||||
cmd-parse-failure-grid = {$arg} is not a valid grid.
|
||||
cmd-parse-failure-cultureinfo = "{$arg}" is not valid CultureInfo.
|
||||
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
|
||||
cmd-parse-failure-session = There is no session with username: {$username}
|
||||
|
||||
@@ -156,6 +157,7 @@ cmd-savemap-not-exist = Target map does not exist.
|
||||
cmd-savemap-init-warning = Attempted to save a post-init map without forcing the save.
|
||||
cmd-savemap-attempt = Attempting to save map {$mapId} to {$path}.
|
||||
cmd-savemap-success = Map successfully saved.
|
||||
cmd-savemap-error = Could not save map! See server log for details.
|
||||
cmd-hint-savemap-id = <MapID>
|
||||
cmd-hint-savemap-path = <Path>
|
||||
cmd-hint-savemap-force = [bool]
|
||||
@@ -293,7 +295,7 @@ cmd-lsgrid-desc = Lists grids.
|
||||
cmd-lsgrid-help = lsgrid
|
||||
|
||||
cmd-addmap-desc = Adds a new empty map to the round. If the mapID already exists, this command does nothing.
|
||||
cmd-addmap-help = addmap <mapID> [initialize]
|
||||
cmd-addmap-help = addmap <mapID> [pre-init]
|
||||
|
||||
cmd-rmmap-desc = Removes a map from the world. You cannot remove nullspace.
|
||||
cmd-rmmap-help = rmmap <mapId>
|
||||
@@ -427,11 +429,20 @@ cmd-entfo-help = Usage: entfo <entityuid>
|
||||
The entity UID can be prefixed with 'c' to convert it to a client entity UID.
|
||||
|
||||
cmd-fuck-desc = Throws an exception
|
||||
cmd-fuck-help = Throws an exception
|
||||
cmd-fuck-help = Usage: fuck
|
||||
|
||||
cmd-showpos-desc = Enables debug drawing over all entity positions in the game.
|
||||
cmd-showpos-desc = Show the position of all entities on the screen.
|
||||
cmd-showpos-help = Usage: showpos
|
||||
|
||||
cmd-showrot-desc = Show the rotation of all entities on the screen.
|
||||
cmd-showrot-help = Usage: showrot
|
||||
|
||||
cmd-showvel-desc = Show the local velocity of all entites on the screen.
|
||||
cmd-showvel-help = Usage: showvel
|
||||
|
||||
cmd-showangvel-desc = Show the angular velocity of all entities on the screen.
|
||||
cmd-showangvel-help = Usage: showangvel
|
||||
|
||||
cmd-sggcell-desc = Lists entities on a snap grid cell.
|
||||
cmd-sggcell-help = Usage: sggcell <gridID> <vector2i>\nThat vector2i param is in the form x<int>,y<int>.
|
||||
|
||||
@@ -562,3 +573,8 @@ cmd-pvs-override-info-desc = Prints information about any PVS overrides associat
|
||||
cmd-pvs-override-info-empty = Entity {$nuid} has no PVS overrides.
|
||||
cmd-pvs-override-info-global = Entity {$nuid} has a global override.
|
||||
cmd-pvs-override-info-clients = Entity {$nuid} has a session override for {$clients}.
|
||||
|
||||
cmd-localization_set_culture-desc = Set DefaultCulture for the client LocalizationManager
|
||||
cmd-localization_set_culture-help = Usage: localization_set_culture <cultureName>
|
||||
cmd-localization_set_culture-culture-name = <cultureName>
|
||||
cmd-localization_set_culture-changed = Localization changed to { $code } ({ $nativeName } / { $englishName })
|
||||
|
||||
@@ -14,6 +14,10 @@ tile-spawn-window-title = Place Tiles
|
||||
|
||||
console-line-edit-placeholder = Command Here
|
||||
|
||||
## OutputPanel
|
||||
|
||||
output-panel-scroll-down-button-text = Scroll Down
|
||||
|
||||
## Common Used
|
||||
|
||||
window-erase-button-text = Erase Mode
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -136,6 +136,7 @@ cmd-savemap-not-exist = O mapa de destino não existe.
|
||||
cmd-savemap-init-warning = Tentativa de salvar um mapa pós-inicialização sem forçar o salvamento.
|
||||
cmd-savemap-attempt = Tentando salvar o mapa {$mapId} em {$path}.
|
||||
cmd-savemap-success = Mapa salvo com sucesso.
|
||||
cmd-savemap-error = Não foi possível salvar o mapa! Consulte o log do servidor para obter detalhes.
|
||||
cmd-hint-savemap-id = <MapID>
|
||||
cmd-hint-savemap-path = <Path>
|
||||
cmd-hint-savemap-force = [bool]
|
||||
|
||||
189
Robust.Analyzers.Tests/ForbidLiteralAnalyzerTest.cs
Normal file
189
Robust.Analyzers.Tests/ForbidLiteralAnalyzerTest.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
using VerifyCS =
|
||||
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.ForbidLiteralAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
|
||||
|
||||
|
||||
namespace Robust.Analyzers.Tests;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture]
|
||||
public sealed class ForbidLiteralAnalyzerTest
|
||||
{
|
||||
private static Task Verifier(string code, params DiagnosticResult[] expected)
|
||||
{
|
||||
var test = new CSharpAnalyzerTest<ForbidLiteralAnalyzer, DefaultVerifier>()
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { code },
|
||||
},
|
||||
};
|
||||
|
||||
TestHelper.AddEmbeddedSources(
|
||||
test.TestState,
|
||||
"Robust.Shared.Analyzers.ForbidLiteralAttribute.cs"
|
||||
);
|
||||
|
||||
test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
|
||||
|
||||
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
|
||||
test.TestState.ExpectedDiagnostics.AddRange(expected);
|
||||
|
||||
return test.RunAsync();
|
||||
}
|
||||
|
||||
private const string TestTypeDefs = """
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
public sealed class TestClass
|
||||
{
|
||||
public static void OneParameterForbidden([ForbidLiteral] string value) { }
|
||||
public static void TwoParametersFirstForbidden([ForbidLiteral] string first, string second) { }
|
||||
public static void TwoParametersBothForbidden([ForbidLiteral] string first, [ForbidLiteral] string second) { }
|
||||
public static void ListParameterForbidden([ForbidLiteral] List<string> values) { }
|
||||
public static void ParamsListParameterForbidden([ForbidLiteral] params List<string> values) { }
|
||||
}
|
||||
|
||||
public record struct StringWrapper(string value)
|
||||
{
|
||||
private readonly string _value = value;
|
||||
|
||||
public static implicit operator string(StringWrapper wrapper)
|
||||
{
|
||||
return wrapper._value;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
[Test]
|
||||
public async Task TestOneParameter()
|
||||
{
|
||||
const string code = """
|
||||
public sealed class Tester
|
||||
{
|
||||
private const string _constValue = "foo";
|
||||
private static readonly string StaticValue = "bar";
|
||||
private static readonly StringWrapper WrappedValue = new("biz");
|
||||
|
||||
public void Test()
|
||||
{
|
||||
TestClass.OneParameterForbidden(_constValue);
|
||||
TestClass.OneParameterForbidden(StaticValue);
|
||||
TestClass.OneParameterForbidden(WrappedValue);
|
||||
TestClass.OneParameterForbidden("baz");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(12,41): error RA0033: The "value" parameter of OneParameterForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(12, 41, 12, 46).WithArguments("value", "OneParameterForbidden")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestTwoParametersFirstForbidden()
|
||||
{
|
||||
const string code = """
|
||||
public sealed class Tester
|
||||
{
|
||||
private const string _constValue = "foo";
|
||||
|
||||
public void Test()
|
||||
{
|
||||
TestClass.TwoParametersFirstForbidden(_constValue, "whatever");
|
||||
TestClass.TwoParametersFirstForbidden(_constValue, _constValue);
|
||||
TestClass.TwoParametersFirstForbidden("foo", "whatever");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(9,47): error RA0033: The "first" parameter of TwoParametersFirstForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(9, 47, 9, 52).WithArguments("first", "TwoParametersFirstForbidden")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestTwoParametersBothForbidden()
|
||||
{
|
||||
const string code = """
|
||||
public sealed class Tester
|
||||
{
|
||||
private const string _constValue = "foo";
|
||||
private static readonly string StaticValue = "bar";
|
||||
|
||||
public void Test()
|
||||
{
|
||||
TestClass.TwoParametersBothForbidden(_constValue, _constValue);
|
||||
TestClass.TwoParametersBothForbidden(_constValue, StaticValue);
|
||||
TestClass.TwoParametersBothForbidden(_constValue, "whatever");
|
||||
TestClass.TwoParametersBothForbidden("whatever", _constValue);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(10,59): error RA0033: The "second" parameter of TwoParametersBothForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(10, 59, 10, 69).WithArguments("second", "TwoParametersBothForbidden"),
|
||||
// /0/Test0.cs(11,46): error RA0033: The "first" parameter of TwoParametersBothForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(11, 46, 11, 56).WithArguments("first", "TwoParametersBothForbidden")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestListParameter()
|
||||
{
|
||||
const string code = """
|
||||
public sealed class Tester
|
||||
{
|
||||
private const string _constValue = "foo";
|
||||
private static readonly string StaticValue = "bar";
|
||||
private static readonly StringWrapper WrappedValue = new("biz");
|
||||
|
||||
public void Test()
|
||||
{
|
||||
TestClass.ListParameterForbidden([_constValue, StaticValue, WrappedValue]);
|
||||
TestClass.ListParameterForbidden(["foo", _constValue, "bar"]);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(10,43): warning RA0033: The "values" parameter of ListParameterForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(10, 43, 10, 48).WithArguments("values", "ListParameterForbidden"),
|
||||
// /0/Test0.cs(10,63): warning RA0033: The "values" parameter of ListParameterForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(10, 63, 10, 68).WithArguments("values", "ListParameterForbidden")
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestParamsListParameter()
|
||||
{
|
||||
const string code = """
|
||||
public sealed class Tester
|
||||
{
|
||||
private const string _constValue = "foo";
|
||||
private static readonly string StaticValue = "bar";
|
||||
private static readonly StringWrapper WrappedValue = new("biz");
|
||||
|
||||
public void Test()
|
||||
{
|
||||
TestClass.ParamsListParameterForbidden(_constValue, StaticValue, WrappedValue);
|
||||
TestClass.ParamsListParameterForbidden("foo", _constValue, "bar");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
await Verifier(code,
|
||||
// /0/Test0.cs(10,48): warning RA0033: The "values" parameter of ParamsListParameterForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(10, 48, 10, 53).WithArguments("values", "ParamsListParameterForbidden"),
|
||||
// /0/Test0.cs(10,68): warning RA0033: The "values" parameter of ParamsListParameterForbidden forbids literal values
|
||||
VerifyCS.Diagnostic().WithSpan(10, 68, 10, 73).WithArguments("values", "ParamsListParameterForbidden")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\MustCallBaseAttribute.cs" LogicalName="Robust.Shared.IoC.MustCallBaseAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferNonGenericVariantForAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferNonGenericVariantForAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\PreferOtherTypeAttribute.cs" LogicalName="Robust.Shared.Analyzers.PreferOtherTypeAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ForbidLiteralAttribute.cs" LogicalName="Robust.Shared.Analyzers.ForbidLiteralAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
|
||||
<EmbeddedResource Include="..\Robust.Shared\GameObjects\EventBusAttributes.cs" LogicalName="Robust.Shared.GameObjects.EventBusAttributes.cs" LinkBase="Implementations" />
|
||||
</ItemGroup>
|
||||
|
||||
101
Robust.Analyzers/ForbidLiteralAnalyzer.cs
Normal file
101
Robust.Analyzers/ForbidLiteralAnalyzer.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
using Robust.Roslyn.Shared;
|
||||
|
||||
namespace Robust.Analyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public sealed class ForbidLiteralAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string ForbidLiteralType = "Robust.Shared.Analyzers.ForbidLiteralAttribute";
|
||||
|
||||
public static DiagnosticDescriptor ForbidLiteralRule = new(
|
||||
Diagnostics.IdForbidLiteral,
|
||||
"Parameter forbids literal values",
|
||||
"The {0} parameter of {1} forbids literal values",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
true,
|
||||
"Pass in a validated wrapper type like ProtoId, or a const or static value."
|
||||
);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [ForbidLiteralRule];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterOperationAction(AnalyzeOperation, OperationKind.Invocation);
|
||||
}
|
||||
|
||||
private void AnalyzeOperation(OperationAnalysisContext context)
|
||||
{
|
||||
if (context.Operation is not IInvocationOperation invocationOperation)
|
||||
return;
|
||||
|
||||
// Check each parameter of the method invocation
|
||||
foreach (var argumentOperation in invocationOperation.Arguments)
|
||||
{
|
||||
// Check for our attribute on the parameter
|
||||
if (!AttributeHelper.HasAttribute(argumentOperation.Parameter, ForbidLiteralType, out _))
|
||||
continue;
|
||||
|
||||
// Handle parameters using the params keyword
|
||||
if (argumentOperation.Syntax is InvocationExpressionSyntax subExpressionSyntax)
|
||||
{
|
||||
// Check each param value
|
||||
foreach (var subArgument in subExpressionSyntax.ArgumentList.Arguments)
|
||||
{
|
||||
CheckArgumentSyntax(context, argumentOperation, subArgument);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not params, so just check the single parameter
|
||||
if (argumentOperation.Syntax is not ArgumentSyntax argumentSyntax)
|
||||
continue;
|
||||
|
||||
CheckArgumentSyntax(context, argumentOperation, argumentSyntax);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckArgumentSyntax(OperationAnalysisContext context, IArgumentOperation operation, ArgumentSyntax argumentSyntax)
|
||||
{
|
||||
// Handle collection types
|
||||
if (argumentSyntax.Expression is CollectionExpressionSyntax collectionExpressionSyntax)
|
||||
{
|
||||
// Check each value of the collection
|
||||
foreach (var elementSyntax in collectionExpressionSyntax.Elements)
|
||||
{
|
||||
if (elementSyntax is not ExpressionElementSyntax expressionSyntax)
|
||||
continue;
|
||||
|
||||
// Check if a literal was passed in
|
||||
if (expressionSyntax.Expression is not LiteralExpressionSyntax)
|
||||
continue;
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(ForbidLiteralRule,
|
||||
expressionSyntax.GetLocation(),
|
||||
operation.Parameter.Name,
|
||||
(context.Operation as IInvocationOperation).TargetMethod.Name
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Not a collection, just a single value to check
|
||||
// Check if it's a literal
|
||||
if (argumentSyntax.Expression is not LiteralExpressionSyntax)
|
||||
return;
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(ForbidLiteralRule,
|
||||
argumentSyntax.GetLocation(),
|
||||
operation.Parameter.Name,
|
||||
(context.Operation as IInvocationOperation).TargetMethod.Name
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,10 @@ public class PhysicsTumblerBenchmark
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
var physics = entManager.System<SharedPhysicsSystem>();
|
||||
var fixtures = entManager.System<FixtureSystem>();
|
||||
entManager.System<SharedMapSystem>().CreateMap(out var mapId);
|
||||
var mapUid = entManager.System<SharedMapSystem>().CreateMap(out var mapId);
|
||||
SetupTumbler(entManager, mapId);
|
||||
|
||||
for (var i = 0; i < 800; i++)
|
||||
for (var i = 0; i < 300; i++)
|
||||
{
|
||||
entManager.TickUpdate(0.016f, false);
|
||||
var boxUid = entManager.SpawnEntity(null, new MapCoordinates(0f, 10f, mapId));
|
||||
@@ -42,6 +42,9 @@ public class PhysicsTumblerBenchmark
|
||||
physics.WakeBody(boxUid, body: box);
|
||||
physics.SetSleepingAllowed(boxUid, box, false);
|
||||
}
|
||||
|
||||
if (entManager.TryGetComponent(mapUid, out BroadphaseComponent? mapBroadphase))
|
||||
entManager.System<SharedBroadphaseSystem>().RebuildBottomUp(mapBroadphase);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
@@ -49,7 +52,7 @@ public class PhysicsTumblerBenchmark
|
||||
{
|
||||
var entManager = _sim.Resolve<IEntityManager>();
|
||||
|
||||
for (var i = 0; i < 5000; i++)
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
entManager.TickUpdate(0.016f, false);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -84,6 +84,19 @@ internal partial class AudioManager
|
||||
AL.Listener(ALListenerfv.Orientation, ref at, ref up);
|
||||
}
|
||||
|
||||
void IAudioInternal.Remove(AudioStream stream)
|
||||
{
|
||||
if (stream.ClydeHandle == null)
|
||||
return;
|
||||
|
||||
if (!_audioSampleBuffers.Remove(stream.BufferId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AL.DeleteBuffer(stream.BufferId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public AudioStream LoadAudioOggVorbis(Stream stream, string? name = null)
|
||||
{
|
||||
@@ -120,9 +133,9 @@ internal partial class AudioManager
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(vorbis.TotalSamples / (double) vorbis.SampleRate);
|
||||
return new AudioStream(handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
|
||||
return new AudioStream(this, buffer, handle, length, (int) vorbis.Channels, name, vorbis.Title, vorbis.Artist);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -179,9 +192,9 @@ internal partial class AudioManager
|
||||
_checkAlError();
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
|
||||
var length = TimeSpan.FromSeconds(wav.Data.Length / (double) wav.BlockAlign / wav.SampleRate);
|
||||
return new AudioStream(handle, length, wav.NumChannels, name);
|
||||
return new AudioStream(this, buffer, handle, length, wav.NumChannels, name);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -210,8 +223,8 @@ internal partial class AudioManager
|
||||
|
||||
var handle = new ClydeHandle(_audioSampleBuffers.Count);
|
||||
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
|
||||
_audioSampleBuffers.Add(new LoadedAudioSample(buffer));
|
||||
return new AudioStream(handle, length, channels, name);
|
||||
_audioSampleBuffers.Add(buffer, new LoadedAudioSample(buffer));
|
||||
return new AudioStream(this, buffer, handle, length, channels, name);
|
||||
}
|
||||
|
||||
public void SetMasterGain(float newGain)
|
||||
@@ -293,7 +306,7 @@ internal partial class AudioManager
|
||||
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
// TODO: This really shouldn't be indexing based on the ClydeHandle...
|
||||
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[(int) stream.ClydeHandle!.Value].BufferHandle);
|
||||
AL.Source(source, ALSourcei.Buffer, _audioSampleBuffers[stream.BufferId].BufferHandle);
|
||||
|
||||
var audioSource = new AudioSource(this, source, stream);
|
||||
_audioSources.Add(source, new WeakReference<BaseAudioSource>(audioSource));
|
||||
@@ -370,5 +383,12 @@ internal partial class AudioManager
|
||||
}
|
||||
|
||||
_bufferedAudioSources.Clear();
|
||||
|
||||
foreach (var buffer in _audioSampleBuffers.Values)
|
||||
{
|
||||
DeleteAudioBufferOnMainThread(buffer.BufferHandle);
|
||||
}
|
||||
|
||||
_audioSampleBuffers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Threading;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
|
||||
using Robust.Client.Audio.Sources;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -17,13 +18,15 @@ internal sealed partial class AudioManager : IAudioInternal
|
||||
{
|
||||
[Shared.IoC.Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Shared.IoC.Dependency] private readonly ILogManager _logMan = default!;
|
||||
[Shared.IoC.Dependency] private readonly IReloadManager _reload = default!;
|
||||
[Shared.IoC.Dependency] private readonly IResourceCache _cache = default!;
|
||||
|
||||
private Thread? _gameThread;
|
||||
|
||||
private ALDevice _openALDevice;
|
||||
private ALContext _openALContext;
|
||||
|
||||
private readonly List<LoadedAudioSample> _audioSampleBuffers = new();
|
||||
private readonly Dictionary<int, LoadedAudioSample> _audioSampleBuffers = new();
|
||||
|
||||
private readonly Dictionary<int, WeakReference<BaseAudioSource>> _audioSources =
|
||||
new();
|
||||
@@ -116,6 +119,22 @@ internal sealed partial class AudioManager : IAudioInternal
|
||||
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
|
||||
|
||||
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterGain, true);
|
||||
|
||||
_reload.Register("/Audio", "*.ogg");
|
||||
_reload.Register("/Audio", "*.wav");
|
||||
|
||||
_reload.OnChanged += OnReload;
|
||||
}
|
||||
|
||||
private void OnReload(ResPath args)
|
||||
{
|
||||
if (args.Extension != "ogg" &&
|
||||
args.Extension != "wav")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cache.ReloadResource<AudioResource>(args);
|
||||
}
|
||||
|
||||
internal bool IsMainThread()
|
||||
@@ -140,6 +159,11 @@ internal sealed partial class AudioManager : IAudioInternal
|
||||
}
|
||||
}
|
||||
|
||||
internal void LogError(string message)
|
||||
{
|
||||
OpenALSawmill.Error(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Like _checkAlError but allows custom data to be passed in as relevant.
|
||||
/// </summary>
|
||||
|
||||
@@ -6,8 +6,15 @@ namespace Robust.Client.Audio;
|
||||
/// <summary>
|
||||
/// Has the metadata for a particular audio stream as well as the relevant internal handle to it.
|
||||
/// </summary>
|
||||
public sealed class AudioStream
|
||||
public sealed class AudioStream : IDisposable
|
||||
{
|
||||
private IAudioInternal _audio;
|
||||
|
||||
/// <summary>
|
||||
/// Buffer ID for this audio in AL.
|
||||
/// </summary>
|
||||
internal int BufferId { get; }
|
||||
|
||||
public TimeSpan Length { get; }
|
||||
internal IClydeHandle? ClydeHandle { get; }
|
||||
public string? Name { get; }
|
||||
@@ -15,8 +22,10 @@ public sealed class AudioStream
|
||||
public string? Artist { get; }
|
||||
public int ChannelCount { get; }
|
||||
|
||||
internal AudioStream(IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
|
||||
internal AudioStream(IAudioInternal internalAudio, int bufferId, IClydeHandle? handle, TimeSpan length, int channelCount, string? name = null, string? title = null, string? artist = null)
|
||||
{
|
||||
_audio = internalAudio;
|
||||
BufferId = bufferId;
|
||||
ClydeHandle = handle;
|
||||
Length = length;
|
||||
ChannelCount = channelCount;
|
||||
@@ -24,4 +33,9 @@ public sealed class AudioStream
|
||||
Title = title;
|
||||
Artist = artist;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_audio.Remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using OpenTK.Audio.OpenAL;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
@@ -56,6 +57,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
|
||||
private float _maxRayLength;
|
||||
private float _zOffset;
|
||||
private float _audioEndBuffer;
|
||||
|
||||
public override float ZOffset
|
||||
{
|
||||
@@ -79,8 +82,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
}
|
||||
|
||||
private float _zOffset;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -108,20 +109,31 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
SubscribeNetworkEvent<PlayAudioEntityMessage>(OnEntityAudio);
|
||||
SubscribeNetworkEvent<PlayAudioPositionalMessage>(OnEntityCoordinates);
|
||||
|
||||
Subs.CVar(CfgManager, CVars.AudioEndBuffer, OnAudioBuffer, true);
|
||||
Subs.CVar(CfgManager, CVars.AudioAttenuation, OnAudioAttenuation, true);
|
||||
Subs.CVar(CfgManager, CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
|
||||
Subs.CVar(CfgManager, CVars.AudioTickRate, OnAudioTickRate, true);
|
||||
InitializeLimit();
|
||||
}
|
||||
|
||||
private void OnAudioBuffer(float value)
|
||||
{
|
||||
_audioEndBuffer = value;
|
||||
}
|
||||
|
||||
private void OnAudioTickRate(int obj)
|
||||
{
|
||||
_audioFrameTime = 1f / obj;
|
||||
_audioFrameTimeRemaining = MathF.Min(_audioFrameTimeRemaining, _audioFrameTime);
|
||||
}
|
||||
|
||||
private void OnAudioState(EntityUid uid, AudioComponent component, ref AfterAutoHandleStateEvent args)
|
||||
private void OnAudioState(Entity<AudioComponent> entity, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
var component = entity.Comp;
|
||||
|
||||
if (component.LifeStage < ComponentLifeStage.Initialized)
|
||||
return;
|
||||
|
||||
ApplyAudioParams(component.Params, component);
|
||||
component.Source.Global = component.Global;
|
||||
|
||||
@@ -145,21 +157,29 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
case AudioState.Stopped:
|
||||
component.StopPlaying();
|
||||
component.PlaybackPosition = 0f;
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
// If playback position changed then update it.
|
||||
if (!string.IsNullOrEmpty(component.FileName))
|
||||
{
|
||||
var position = (float) ((component.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
|
||||
var currentPosition = component.Source.PlaybackPosition;
|
||||
var diff = Math.Abs(position - currentPosition);
|
||||
var position = (float) ((entity.Comp.PauseTime ?? Timing.CurTime) - entity.Comp.AudioStart).TotalSeconds;
|
||||
var currentPosition = entity.Comp.Source.PlaybackPosition;
|
||||
var diff = Math.Abs(position - currentPosition);
|
||||
|
||||
if (diff > 0.1f)
|
||||
// Don't try to set the audio too far ahead.
|
||||
if (!string.IsNullOrEmpty(entity.Comp.FileName))
|
||||
{
|
||||
if (position > GetAudioLengthImpl(entity.Comp.FileName).TotalSeconds - _audioEndBuffer)
|
||||
{
|
||||
component.PlaybackPosition = position;
|
||||
entity.Comp.StopPlaying();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If the difference is minor then we'll just keep playing it.
|
||||
if (diff > 0.1f)
|
||||
{
|
||||
entity.Comp.PlaybackPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -207,6 +227,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
private void SetupSource(Entity<AudioComponent> entity, AudioResource audioResource, TimeSpan? length = null)
|
||||
{
|
||||
var component = entity.Comp;
|
||||
length ??= GetAudioLength(component.FileName);
|
||||
|
||||
// If audio came into range then start playback at the correct position.
|
||||
var offset = ((entity.Comp.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
|
||||
|
||||
if (TryAudioLimit(component.FileName))
|
||||
{
|
||||
@@ -230,10 +254,17 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
// Don't play until first frame so occlusion etc. are correct.
|
||||
component.Gain = 0f;
|
||||
|
||||
length ??= GetAudioLength(component.FileName);
|
||||
|
||||
// If audio came into range then start playback at the correct position.
|
||||
var offset = (Timing.CurTime - component.AudioStart).TotalSeconds % length.Value.TotalSeconds;
|
||||
// If the offset < buffer than just play it from the start.
|
||||
if (offset < AudioDespawnBuffer)
|
||||
{
|
||||
offset = 0;
|
||||
}
|
||||
// Not enough audio to play
|
||||
else if (offset > length.Value.TotalSeconds - _audioEndBuffer)
|
||||
{
|
||||
component.StopPlaying();
|
||||
return;
|
||||
}
|
||||
|
||||
if (offset > 0)
|
||||
{
|
||||
@@ -415,6 +446,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 +474,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 +518,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 +540,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 +554,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 +578,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 +586,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 +598,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 +622,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 +630,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 +670,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 +735,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)
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace Robust.Client.Audio;
|
||||
/// </summary>
|
||||
internal sealed class HeadlessAudioManager : IAudioInternal
|
||||
{
|
||||
private int _audioBuffer;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void InitializePostWindowing()
|
||||
{
|
||||
@@ -65,6 +67,11 @@ internal sealed class HeadlessAudioManager : IAudioInternal
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Remove(AudioStream stream)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StopAllAudio()
|
||||
{
|
||||
@@ -101,11 +108,11 @@ internal sealed class HeadlessAudioManager : IAudioInternal
|
||||
public AudioStream LoadAudioRaw(ReadOnlySpan<short> samples, int channels, int sampleRate, string? name = null)
|
||||
{
|
||||
var length = TimeSpan.FromSeconds((double) samples.Length / channels / sampleRate);
|
||||
return new AudioStream(null, length, channels, name);
|
||||
return new AudioStream(this, _audioBuffer++, null, length, channels, name);
|
||||
}
|
||||
|
||||
private static AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
|
||||
private AudioStream AudioStreamFromMetadata(AudioMetadata metadata, string? name)
|
||||
{
|
||||
return new AudioStream(null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
|
||||
return new AudioStream(this, _audioBuffer++, null, metadata.Length, metadata.ChannelCount, name, metadata.Title, metadata.Artist);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ internal interface IAudioInternal : IAudioManager
|
||||
|
||||
void SetAttenuation(Attenuation attenuation);
|
||||
|
||||
void Remove(AudioStream stream);
|
||||
|
||||
/// <summary>
|
||||
/// Stops all audio from playing.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Robust.Client.Audio.Sources;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
|
||||
namespace Robust.Client.Audio;
|
||||
@@ -11,7 +10,7 @@ namespace Robust.Client.Audio;
|
||||
public interface IAudioManager
|
||||
{
|
||||
IAudioSource? CreateAudioSource(AudioStream stream);
|
||||
|
||||
|
||||
AudioStream LoadAudioOggVorbis(Stream stream, string? name = null);
|
||||
|
||||
AudioStream LoadAudioWav(Stream stream, string? name = null);
|
||||
|
||||
@@ -13,7 +13,7 @@ internal sealed class AudioSource : BaseAudioSource
|
||||
/// <summary>
|
||||
/// Underlying stream to the audio.
|
||||
/// </summary>
|
||||
private readonly AudioStream _sourceStream;
|
||||
internal readonly AudioStream SourceStream;
|
||||
|
||||
#if DEBUG
|
||||
private bool _didPositionWarning;
|
||||
@@ -21,7 +21,7 @@ internal sealed class AudioSource : BaseAudioSource
|
||||
|
||||
public AudioSource(AudioManager master, int sourceHandle, AudioStream sourceStream) : base(master, sourceHandle)
|
||||
{
|
||||
_sourceStream = sourceStream;
|
||||
SourceStream = sourceStream;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -47,13 +47,13 @@ internal sealed class AudioSource : BaseAudioSource
|
||||
#if DEBUG
|
||||
// OpenAL doesn't seem to want to play stereo positionally.
|
||||
// Log a warning if people try to.
|
||||
if (_sourceStream.ChannelCount > 1 && !_didPositionWarning)
|
||||
if (SourceStream.ChannelCount > 1 && !_didPositionWarning)
|
||||
{
|
||||
_didPositionWarning = true;
|
||||
Master.OpenALSawmill.Warning("Attempting to set position on audio source with multiple audio channels! Stream: '{0}'. Make sure the audio is MONO, not stereo.",
|
||||
_sourceStream.Name);
|
||||
SourceStream.Name);
|
||||
// warning isn't enough, people just ignore it :(
|
||||
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{_sourceStream.Name}'. Make sure the audio is MONO, not stereo.");
|
||||
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{SourceStream.Name}'. Make sure the audio is MONO, not stereo.");
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -208,6 +208,12 @@ public abstract class BaseAudioSource : IAudioSource
|
||||
}
|
||||
set
|
||||
{
|
||||
if (float.IsNaN(value))
|
||||
{
|
||||
Master.LogError($"Tried to set NaN gain, setting audio source to 0f: {Environment.StackTrace}");
|
||||
value = 0f;
|
||||
}
|
||||
|
||||
_checkDisposed();
|
||||
var priorOcclusion = 1f;
|
||||
if (!IsEfxSupported)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ using Robust.Shared.Replays;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Upload;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client
|
||||
@@ -102,6 +103,7 @@ namespace Robust.Client
|
||||
deps.Register<ProfViewManager>();
|
||||
deps.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
|
||||
deps.Register<NetworkResourceManager>();
|
||||
deps.Register<IReloadManager, ReloadManager>();
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
|
||||
@@ -173,29 +173,51 @@ namespace Robust.Client.Console.Commands
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ShowPositionsCommand : LocalizedCommands
|
||||
internal sealed class ShowPositionsCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
|
||||
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
|
||||
|
||||
public override string Command => "showpos";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var mgr = _entitySystems.GetEntitySystem<DebugDrawingSystem>();
|
||||
mgr.DebugPositions = !mgr.DebugPositions;
|
||||
_debugDrawing.DebugPositions = !_debugDrawing.DebugPositions;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ShowRotationsCommand : LocalizedCommands
|
||||
internal sealed class ShowRotationsCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
|
||||
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
|
||||
|
||||
public override string Command => "showrot";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var mgr = _entitySystems.GetEntitySystem<DebugDrawingSystem>();
|
||||
mgr.DebugRotations = !mgr.DebugRotations;
|
||||
_debugDrawing.DebugRotations = !_debugDrawing.DebugRotations;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ShowVelocitiesCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
|
||||
|
||||
public override string Command => "showvel";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
_debugDrawing.DebugVelocities = !_debugDrawing.DebugVelocities;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ShowAngularVelocitiesCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
|
||||
|
||||
public override string Command => "showangvel";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
_debugDrawing.DebugAngularVelocities = !_debugDrawing.DebugAngularVelocities;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
67
Robust.Client/Console/Commands/LocalizationCommands.cs
Normal file
67
Robust.Client/Console/Commands/LocalizationCommands.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Robust.Client.Console.Commands;
|
||||
|
||||
[UsedImplicitly]
|
||||
internal sealed class LocalizationSetCulture : LocalizedCommands
|
||||
{
|
||||
private const string Name = "localization_set_culture";
|
||||
private const int ArgumentCount = 1;
|
||||
|
||||
public override string Command => Name;
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != ArgumentCount)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error"));
|
||||
return;
|
||||
}
|
||||
|
||||
CultureInfo culture;
|
||||
try
|
||||
{
|
||||
culture = CultureInfo.GetCultureInfo(args[0], predefinedOnly: false);
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-cultureinfo", ("arg", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
LocalizationManager.SetCulture(culture);
|
||||
shell.WriteLine(LocalizationManager.GetString("cmd-localization_set_culture-changed",
|
||||
("code", culture.Name),
|
||||
("nativeName", culture.NativeName),
|
||||
("englishName", culture.EnglishName)));
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
return args.Length switch
|
||||
{
|
||||
1 => CompletionResult.FromHintOptions(GetCultureNames(),
|
||||
LocalizationManager.GetString("cmd-localization_set_culture-culture-name")),
|
||||
_ => CompletionResult.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private static HashSet<string> GetCultureNames()
|
||||
{
|
||||
var cultureInfos = CultureInfo.GetCultures(CultureTypes.AllCultures)
|
||||
.Where(x => !string.IsNullOrEmpty(x.Name))
|
||||
.ToArray();
|
||||
|
||||
var allNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
allNames.UnionWith(cultureInfos.Select(x => x.TwoLetterISOLanguageName));
|
||||
allNames.UnionWith(cultureInfos.Select(x => x.Name));
|
||||
|
||||
return allNames;
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,15 @@ using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
public sealed class VelocitiesCommand : LocalizedCommands
|
||||
public sealed class ShowPlayerVelocityCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
|
||||
|
||||
public override string Command => "showvelocities";
|
||||
public override string Command => "showplayervelocity";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
_entitySystems.GetEntitySystem<VelocityDebugSystem>().Enabled ^= true;
|
||||
_entitySystems.GetEntitySystem<ShowPlayerVelocityDebugSystem>().Enabled ^= true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,140 +1,221 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Robust.Client.Debugging
|
||||
namespace Robust.Client.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// A collection of visual debug overlays for the client game.
|
||||
/// </summary>
|
||||
public sealed class DebugDrawingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
private bool _debugPositions;
|
||||
private bool _debugRotations;
|
||||
private bool _debugVelocities;
|
||||
private bool _debugAngularVelocities;
|
||||
|
||||
/// <summary>
|
||||
/// A collection of visual debug overlays for the client game.
|
||||
/// Toggles the visual overlay of the local origin for each entity on screen.
|
||||
/// </summary>
|
||||
public sealed class DebugDrawingSystem : EntitySystem
|
||||
public bool DebugPositions
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
|
||||
|
||||
private bool _debugPositions;
|
||||
private bool _debugRotations;
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the visual overlay of the local origin for each entity on screen.
|
||||
/// </summary>
|
||||
public bool DebugPositions
|
||||
get => _debugPositions;
|
||||
set
|
||||
{
|
||||
get => _debugPositions;
|
||||
set
|
||||
if (value == DebugPositions)
|
||||
{
|
||||
if (value == DebugPositions)
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_debugPositions = value;
|
||||
_debugPositions = value;
|
||||
|
||||
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
|
||||
{
|
||||
_overlayManager.AddOverlay(new EntityPositionOverlay(_lookup, EntityManager, _transform));
|
||||
}
|
||||
else
|
||||
{
|
||||
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
|
||||
}
|
||||
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
|
||||
{
|
||||
_overlayManager.AddOverlay(new EntityPositionOverlay(_lookup, _transform));
|
||||
}
|
||||
else
|
||||
{
|
||||
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the visual overlay of the local rotation.
|
||||
/// </summary>
|
||||
public bool DebugRotations
|
||||
/// <summary>
|
||||
/// Toggles the visual overlay of the rotation for each entity on screen.
|
||||
/// </summary>
|
||||
public bool DebugRotations
|
||||
{
|
||||
get => _debugRotations;
|
||||
set
|
||||
{
|
||||
get => _debugRotations;
|
||||
set
|
||||
if (value == DebugRotations)
|
||||
{
|
||||
if (value == DebugRotations)
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_debugRotations = value;
|
||||
_debugRotations = value;
|
||||
|
||||
if (value && !_overlayManager.HasOverlay<EntityRotationOverlay>())
|
||||
{
|
||||
_overlayManager.AddOverlay(new EntityRotationOverlay(_lookup, EntityManager));
|
||||
}
|
||||
else
|
||||
{
|
||||
_overlayManager.RemoveOverlay<EntityRotationOverlay>();
|
||||
}
|
||||
if (value && !_overlayManager.HasOverlay<EntityRotationOverlay>())
|
||||
{
|
||||
_overlayManager.AddOverlay(new EntityRotationOverlay(_lookup, _transform));
|
||||
}
|
||||
else
|
||||
{
|
||||
_overlayManager.RemoveOverlay<EntityRotationOverlay>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EntityPositionOverlay : Overlay
|
||||
/// <summary>
|
||||
/// Toggles the visual overlay of the local velocity for each entity on screen.
|
||||
/// </summary>
|
||||
public bool DebugVelocities
|
||||
{
|
||||
get => _debugVelocities;
|
||||
set
|
||||
{
|
||||
private readonly EntityLookupSystem _lookup;
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly SharedTransformSystem _transform;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
public EntityPositionOverlay(EntityLookupSystem lookup, IEntityManager entityManager, SharedTransformSystem transform)
|
||||
if (value == DebugVelocities)
|
||||
{
|
||||
_lookup = lookup;
|
||||
_entityManager = entityManager;
|
||||
_transform = transform;
|
||||
return;
|
||||
}
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
_debugVelocities = value;
|
||||
|
||||
if (value && !_overlayManager.HasOverlay<EntityVelocityOverlay>())
|
||||
{
|
||||
const float stubLength = 0.25f;
|
||||
|
||||
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
|
||||
|
||||
foreach (var entity in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
|
||||
{
|
||||
var (center, worldRotation) = _transform.GetWorldPositionRotation(entity);
|
||||
|
||||
var xLine = worldRotation.RotateVec(Vector2.UnitX);
|
||||
var yLine = worldRotation.RotateVec(Vector2.UnitY);
|
||||
|
||||
worldHandle.DrawLine(center, center + xLine * stubLength, Color.Red);
|
||||
worldHandle.DrawLine(center, center + yLine * stubLength, Color.Green);
|
||||
}
|
||||
_overlayManager.AddOverlay(new EntityVelocityOverlay(EntityManager, _lookup, _transform));
|
||||
}
|
||||
else
|
||||
{
|
||||
_overlayManager.RemoveOverlay<EntityVelocityOverlay>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EntityRotationOverlay : Overlay
|
||||
/// <summary>
|
||||
/// Toggles the visual overlay of the angular velocity for each entity on screen.
|
||||
/// </summary>
|
||||
public bool DebugAngularVelocities
|
||||
{
|
||||
get => _debugAngularVelocities;
|
||||
set
|
||||
{
|
||||
private readonly EntityLookupSystem _lookup;
|
||||
private readonly IEntityManager _entityManager;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
public EntityRotationOverlay(EntityLookupSystem lookup, IEntityManager entityManager)
|
||||
if (value == DebugAngularVelocities)
|
||||
{
|
||||
_lookup = lookup;
|
||||
_entityManager = entityManager;
|
||||
return;
|
||||
}
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
_debugAngularVelocities = value;
|
||||
|
||||
if (value && !_overlayManager.HasOverlay<EntityAngularVelocityOverlay>())
|
||||
{
|
||||
const float stubLength = 0.25f;
|
||||
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
|
||||
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
|
||||
_overlayManager.AddOverlay(new EntityAngularVelocityOverlay(EntityManager, _lookup, _transform));
|
||||
}
|
||||
else
|
||||
{
|
||||
_overlayManager.RemoveOverlay<EntityAngularVelocityOverlay>();
|
||||
}
|
||||
}
|
||||
}
|
||||
private sealed class EntityPositionOverlay(EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
foreach (var entity in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
|
||||
{
|
||||
var (center, worldRotation) = xformQuery.GetComponent(entity).GetWorldPositionRotation();
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
const float stubLength = 0.25f;
|
||||
|
||||
var drawLine = worldRotation.RotateVec(-Vector2.UnitY);
|
||||
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
|
||||
|
||||
worldHandle.DrawLine(center, center + drawLine * stubLength, Color.Red);
|
||||
}
|
||||
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
|
||||
{
|
||||
var (center, worldRotation) = _transform.GetWorldPositionRotation(uid);
|
||||
|
||||
var xLine = worldRotation.RotateVec(Vector2.UnitX);
|
||||
var yLine = worldRotation.RotateVec(Vector2.UnitY);
|
||||
|
||||
worldHandle.DrawLine(center, center + xLine * stubLength, Color.Red);
|
||||
worldHandle.DrawLine(center, center + yLine * stubLength, Color.Green);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EntityRotationOverlay(EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
const float stubLength = 0.25f;
|
||||
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
|
||||
|
||||
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
|
||||
{
|
||||
var (center, worldRotation) = _transform.GetWorldPositionRotation(uid);
|
||||
|
||||
var drawLine = worldRotation.RotateVec(-Vector2.UnitY);
|
||||
|
||||
worldHandle.DrawLine(center, center + drawLine * stubLength, Color.Red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EntityVelocityOverlay(IEntityManager _entityManager, EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
const float multiplier = 0.2f;
|
||||
|
||||
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
|
||||
|
||||
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
|
||||
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
|
||||
{
|
||||
if(!physicsQuery.TryGetComponent(uid, out var physicsComp))
|
||||
continue;
|
||||
|
||||
var center = _transform.GetWorldPosition(uid);
|
||||
var localVelocity = physicsComp.LinearVelocity;
|
||||
|
||||
if (localVelocity != Vector2.Zero)
|
||||
worldHandle.DrawLine(center, center + localVelocity * multiplier, Color.Yellow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EntityAngularVelocityOverlay(IEntityManager _entityManager, EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
const float multiplier = (float)(0.2 / (2 * System.Math.PI));
|
||||
|
||||
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
|
||||
|
||||
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
|
||||
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
|
||||
{
|
||||
if(!physicsQuery.TryGetComponent(uid, out var physicsComp))
|
||||
continue;
|
||||
|
||||
var center = _transform.GetWorldPosition(uid);
|
||||
var angularVelocity = physicsComp.AngularVelocity;
|
||||
|
||||
if (angularVelocity != 0.0f)
|
||||
worldHandle.DrawCircle(center, angularVelocity * multiplier, angularVelocity > 0 ? Color.Magenta : Color.Blue, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -413,8 +413,9 @@ namespace Robust.Client.Debugging
|
||||
}
|
||||
|
||||
var body = bodyEnt.Comp;
|
||||
var meta = _entityManager.GetComponent<MetaDataComponent>(bodyEnt);
|
||||
|
||||
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner}");
|
||||
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Ent: {bodyEnt.Owner} ({meta.EntityName})");
|
||||
row++;
|
||||
screenHandle.DrawString(_font, drawPos + new Vector2(0, row * lineHeight), $"Layer: {Convert.ToString(body.CollisionLayer, 2)}");
|
||||
row++;
|
||||
|
||||
@@ -93,6 +93,7 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IReplayPlaybackManager _replayPlayback = default!;
|
||||
[Dependency] private readonly IReplayRecordingManagerInternal _replayRecording = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
|
||||
private IWebViewManagerHook? _webViewHook;
|
||||
|
||||
@@ -185,6 +186,7 @@ namespace Robust.Client
|
||||
// before prototype load.
|
||||
ProgramShared.FinishCheckBadFileExtensions(checkBadExtensions);
|
||||
|
||||
_reload.Initialize();
|
||||
_reflectionManager.Initialize();
|
||||
_prototypeManager.Initialize();
|
||||
_prototypeManager.LoadDefaultPrototypes();
|
||||
@@ -382,7 +384,7 @@ namespace Robust.Client
|
||||
|
||||
_prof.Initialize();
|
||||
|
||||
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
|
||||
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
|
||||
|
||||
var mountOptions = _commandLineArgs != null
|
||||
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
|
||||
|
||||
@@ -117,7 +117,7 @@ namespace Robust.Client.GameObjects
|
||||
base.DirtyField(uid, comp, fieldName, metadata);
|
||||
}
|
||||
|
||||
public override void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params ReadOnlySpan<string> fields)
|
||||
public override void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params string[] fields)
|
||||
{
|
||||
// TODO Prediction
|
||||
// does the client actually need to dirty the field?
|
||||
|
||||
@@ -30,6 +30,7 @@ using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
|
||||
using static Robust.Shared.Serialization.TypeSerializers.Implementations.SpriteSpecifierSerializer;
|
||||
using Direction = Robust.Shared.Maths.Direction;
|
||||
using Vector4 = Robust.Shared.Maths.Vector4;
|
||||
using SysVec4 = System.Numerics.Vector4;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
@@ -753,12 +754,20 @@ namespace Robust.Client.GameObjects
|
||||
if (layerDatum.Shader == string.Empty)
|
||||
{
|
||||
layer.ShaderPrototype = null;
|
||||
layer.UnShaded = false;
|
||||
layer.Shader = null;
|
||||
}
|
||||
else if (layerDatum.Shader == SpriteSystem.UnshadedId.Id)
|
||||
{
|
||||
layer.ShaderPrototype = SpriteSystem.UnshadedId;
|
||||
layer.UnShaded = true;
|
||||
layer.Shader = null;
|
||||
}
|
||||
else if (prototypes.TryIndex<ShaderPrototype>(layerDatum.Shader, out var prototype))
|
||||
{
|
||||
layer.ShaderPrototype = layerDatum.Shader;
|
||||
layer.Shader = prototype.Instance();
|
||||
layer.UnShaded = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -835,11 +844,28 @@ namespace Robust.Client.GameObjects
|
||||
if (!TryGetLayer(layer, out var theLayer, true))
|
||||
return;
|
||||
|
||||
if (shader == null)
|
||||
{
|
||||
theLayer.UnShaded = false;
|
||||
theLayer.Shader = null;
|
||||
theLayer.ShaderPrototype = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (prototype == SpriteSystem.UnshadedId.Id)
|
||||
{
|
||||
theLayer.UnShaded = true;
|
||||
theLayer.ShaderPrototype = SpriteSystem.UnshadedId;
|
||||
theLayer.Shader = null;
|
||||
return;
|
||||
}
|
||||
|
||||
theLayer.UnShaded = false;
|
||||
theLayer.Shader = shader;
|
||||
theLayer.ShaderPrototype = prototype;
|
||||
}
|
||||
|
||||
public void LayerSetShader(object layerKey, ShaderInstance shader, string? prototype = null)
|
||||
public void LayerSetShader(object layerKey, ShaderInstance? shader, string? prototype = null)
|
||||
{
|
||||
if (!LayerMapTryGet(layerKey, out var layer, true))
|
||||
return;
|
||||
@@ -1493,10 +1519,18 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
[ViewVariables] private readonly SpriteComponent _parent;
|
||||
|
||||
[ViewVariables] public string? ShaderPrototype;
|
||||
[ViewVariables] public ProtoId<ShaderPrototype>? ShaderPrototype;
|
||||
[ViewVariables] public ShaderInstance? Shader;
|
||||
[ViewVariables] public Texture? Texture;
|
||||
|
||||
/// <summary>
|
||||
/// If true, then this layer is drawn without lighting applied.
|
||||
/// Unshaded layers are given special treatment and don't just use the unshaded-shader to avoid having to
|
||||
/// unnecessarily swap out the light texture. This helps the number of batches that need to be sent to the
|
||||
/// GPU while drawing sprites.
|
||||
/// </summary>
|
||||
[ViewVariables] internal bool UnShaded;
|
||||
|
||||
private RSI? _rsi;
|
||||
[ViewVariables] public RSI? RSI
|
||||
{
|
||||
@@ -1663,6 +1697,7 @@ namespace Robust.Client.GameObjects
|
||||
if (toClone.Shader != null)
|
||||
{
|
||||
Shader = toClone.Shader.Mutable ? toClone.Shader.Duplicate() : toClone.Shader;
|
||||
UnShaded = toClone.UnShaded;
|
||||
ShaderPrototype = toClone.ShaderPrototype;
|
||||
}
|
||||
Texture = toClone.Texture;
|
||||
@@ -2078,6 +2113,20 @@ namespace Robust.Client.GameObjects
|
||||
drawingHandle.UseShader(Shader);
|
||||
|
||||
var layerColor = _parent.color * Color;
|
||||
|
||||
DebugTools.Assert(layerColor is {R: >= 0, G: >= 0, B: >= 0, A: >= 0}, "Negative colour modulation");
|
||||
|
||||
if (UnShaded)
|
||||
{
|
||||
DebugTools.AssertNull(Shader);
|
||||
|
||||
// Negative modulation values are used to disable light shading in the default shader.
|
||||
// Specifically we set colour = -1 - colour
|
||||
// This ensures that non-negative values become negative & is trivially invertible.
|
||||
// Alternatively we could just clamp the colour to [0,1] and subtract a constant.
|
||||
layerColor = new(new SysVec4(-1) - layerColor.RGBA);
|
||||
}
|
||||
|
||||
var textureSize = texture.Size / (float)EyeManager.PixelsPerMeter;
|
||||
var quad = Box2.FromDimensions(textureSize/-2, textureSize);
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace Robust.Client.GameObjects
|
||||
foreach (var key in remie)
|
||||
{
|
||||
component.PlayingAnimations.Remove(key);
|
||||
var completedEvent = new AnimationCompletedEvent {Uid = uid, Key = key, Finished = true};
|
||||
var completedEvent = new AnimationCompletedEvent(uid, component, key, true);
|
||||
EntityManager.EventBus.RaiseLocalEvent(uid, completedEvent, true);
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ namespace Robust.Client.GameObjects
|
||||
return;
|
||||
}
|
||||
|
||||
var completedEvent = new AnimationCompletedEvent {Uid = entity.Owner, Key = key, Finished = false};
|
||||
var completedEvent = new AnimationCompletedEvent(entity.Owner, entity.Comp, key, false);
|
||||
EntityManager.EventBus.RaiseLocalEvent(entity.Owner, completedEvent, true);
|
||||
}
|
||||
|
||||
@@ -202,13 +202,33 @@ namespace Robust.Client.GameObjects
|
||||
/// </summary>
|
||||
public sealed class AnimationCompletedEvent : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity associated with the event.
|
||||
/// </summary>
|
||||
public EntityUid Uid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The animation player component associated with the entity this event was raised on.
|
||||
/// </summary>
|
||||
public AnimationPlayerComponent AnimationPlayer { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The key associated with the animation that was completed.
|
||||
/// </summary>
|
||||
public string Key { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// If true, the animation finished by getting to its natural end.
|
||||
/// If false, it was removed prematurely via <see cref="AnimationPlayerSystem.Stop(Robust.Client.GameObjects.AnimationPlayerComponent,string)"/> or similar overloads.
|
||||
/// If false, it was removed prematurely via <see cref="AnimationPlayerSystem.Stop(EntityUid,AnimationPlayerComponent,string)"/> or similar overloads.
|
||||
/// </summary>
|
||||
public bool Finished { get; init; }
|
||||
|
||||
public AnimationCompletedEvent(EntityUid uid, AnimationPlayerComponent animationPlayer, string key, bool finished = true)
|
||||
{
|
||||
Uid = uid;
|
||||
AnimationPlayer = animationPlayer;
|
||||
Key = key;
|
||||
Finished = finished;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,30 +9,14 @@ namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class MapSystem : SharedMapSystem
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resource = default!;
|
||||
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
|
||||
|
||||
protected override MapId GetNextMapId()
|
||||
{
|
||||
// Client-side map entities use negative map Ids to avoid conflict with server-side maps.
|
||||
var id = new MapId(--LastMapId);
|
||||
while (MapManager.MapExists(id))
|
||||
while (MapExists(id) || UsedIds.Contains(id))
|
||||
{
|
||||
id = new MapId(--LastMapId);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_overlayManager.AddOverlay(new TileEdgeOverlay(EntityManager, _resource, _tileDefinitionManager));
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class ShowPlayerVelocityDebugSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
|
||||
internal bool Enabled
|
||||
{
|
||||
get => _label.Parent != null;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
_uiManager.WindowRoot.AddChild(_label);
|
||||
}
|
||||
else
|
||||
{
|
||||
_label.Orphan();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Label _label = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_label = new Label();
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
if (!Enabled)
|
||||
{
|
||||
_label.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var player = _playerManager.LocalEntity;
|
||||
|
||||
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
|
||||
{
|
||||
_label.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var screenPos = _eyeManager.WorldToScreen(_transform.GetWorldPosition(Transform(player.Value)));
|
||||
LayoutContainer.SetPosition(_label, screenPos + new Vector2(0, 50));
|
||||
_label.Visible = true;
|
||||
|
||||
_label.Text = $"Speed: {body.LinearVelocity.Length():0.00}\nLinear: {body.LinearVelocity.X:0.00}, {body.LinearVelocity.Y:0.00}\nAngular: {body.AngularVelocity}";
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ namespace Robust.Client.GameObjects
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xforms = default!;
|
||||
|
||||
public static readonly ProtoId<ShaderPrototype> UnshadedId = "unshaded";
|
||||
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,12 +25,6 @@ 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)
|
||||
{
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
public sealed class VelocityDebugSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
|
||||
internal bool Enabled { get; set; }
|
||||
|
||||
private Label _label = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_label = new Label();
|
||||
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(_label);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
if (!Enabled)
|
||||
{
|
||||
_label.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var player = _playerManager.LocalEntity;
|
||||
|
||||
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
|
||||
{
|
||||
_label.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var screenPos = _eyeManager.WorldToScreen(_transform.GetWorldPosition(Transform(player.Value)));
|
||||
LayoutContainer.SetPosition(_label, screenPos + new Vector2(0, 50));
|
||||
_label.Visible = true;
|
||||
|
||||
_label.Text = $"Speed: {body.LinearVelocity.Length():0.00}\nLinear: {body.LinearVelocity.X:0.00}, {body.LinearVelocity.Y:0.00}\nAngular: {body.AngularVelocity}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class VisibilitySystem : SharedVisibilitySystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -384,7 +384,6 @@ namespace Robust.Client.GameStates
|
||||
_processor.UpdateFullRep(curState);
|
||||
}
|
||||
|
||||
IEnumerable<NetEntity> createdEntities;
|
||||
using (_prof.Group("ApplyGameState"))
|
||||
{
|
||||
if (_timing.LastProcessedTick < targetProcessedTick && nextState != null)
|
||||
@@ -699,8 +698,9 @@ namespace Robust.Client.GameStates
|
||||
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw new KeyNotFoundException();
|
||||
#endif
|
||||
#else
|
||||
continue;
|
||||
#endif
|
||||
}
|
||||
|
||||
var compData = _compDataPool.Get();
|
||||
@@ -961,8 +961,9 @@ namespace Robust.Client.GameStates
|
||||
RequestFullState();
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
#else
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (data.Created)
|
||||
@@ -980,8 +981,9 @@ namespace Robust.Client.GameStates
|
||||
RequestFullState();
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
#else
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private const string UniModelMatrix = "modelMatrix";
|
||||
private const string UniTexturePixelSize = "TEXTURE_PIXEL_SIZE";
|
||||
private const string UniMainTexture = "TEXTURE";
|
||||
private const string UniLightTexture = "lightMap";
|
||||
private const string UniLightTexture = "lightMap"; // TODO CLYDE consistent shader variable naming
|
||||
private const string UniProjViewMatrices = "projectionViewMatrices";
|
||||
private const string UniUniformConstants = "uniformConstants";
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenToolkit.Graphics.OpenGL4;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Graphics;
|
||||
@@ -22,6 +23,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
/// </summary>
|
||||
private HashSet<Type> _erroredGridOverlays = new();
|
||||
|
||||
private Vertex2D[]? _chunkMeshBuilderVertexBuffer;
|
||||
private ushort[]? _chunkMeshBuilderIndexBuffer;
|
||||
|
||||
private int _verticesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * 4;
|
||||
private int _indicesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * GetQuadBatchIndexCount();
|
||||
|
||||
@@ -63,29 +67,78 @@ namespace Robust.Client.Graphics.Clyde
|
||||
gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1));
|
||||
}
|
||||
|
||||
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid);
|
||||
gridProgram.SetUniform(UniIModelMatrix, _transformSystem.GetWorldMatrix(transform));
|
||||
gridProgram.SetUniform(UniIModelMatrix, _transformSystem.GetWorldMatrix(mapGrid));
|
||||
var enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
|
||||
|
||||
// Handle base texture updates.
|
||||
while (enumerator.MoveNext(out var chunk))
|
||||
{
|
||||
DebugTools.Assert(chunk.FilledTiles > 0);
|
||||
if (!data.TryGetValue(chunk.Indices, out MapChunkData? datum))
|
||||
data[chunk.Indices] = datum = _initChunkBuffers(mapGrid, chunk);
|
||||
var datum = EnsureChunkInitialized(data, chunk, mapGrid);
|
||||
|
||||
if (datum.Dirty)
|
||||
_updateChunkMesh(mapGrid, chunk, datum);
|
||||
|
||||
DebugTools.Assert(datum.TileCount > 0);
|
||||
if (datum.TileCount == 0)
|
||||
if (!datum.Dirty)
|
||||
continue;
|
||||
|
||||
BindVertexArray(datum.VAO);
|
||||
CheckGlError();
|
||||
_updateChunkMesh(mapGrid, chunk, datum);
|
||||
|
||||
// Dirty edge tiles for next step.
|
||||
datum.EdgeDirty = true;
|
||||
|
||||
for (var x = -1; x <= 1; x++)
|
||||
{
|
||||
for (var y = -1; y <= 1; y++)
|
||||
{
|
||||
var neighbor = chunk.Indices + new Vector2i(x, y);
|
||||
|
||||
if (!mapGrid.Comp.Chunks.TryGetValue(neighbor, out var neighborChunk))
|
||||
continue;
|
||||
|
||||
var neighborDatum = EnsureChunkInitialized(data, neighborChunk, mapGrid);
|
||||
neighborDatum.EdgeDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
|
||||
|
||||
// Handle edge sprites.
|
||||
while (enumerator.MoveNext(out var chunk))
|
||||
{
|
||||
var datum = data[chunk.Indices];
|
||||
|
||||
if (!datum.EdgeDirty)
|
||||
continue;
|
||||
|
||||
_updateChunkEdges(mapGrid, chunk, datum);
|
||||
}
|
||||
|
||||
enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds);
|
||||
|
||||
// Draw chunks
|
||||
while (enumerator.MoveNext(out var chunk))
|
||||
{
|
||||
var datum = data[chunk.Indices];
|
||||
DebugTools.Assert(datum.TileCount > 0);
|
||||
if (datum.TileCount > 0)
|
||||
{
|
||||
BindVertexArray(datum.VAO);
|
||||
CheckGlError();
|
||||
|
||||
_debugStats.LastGLDrawCalls += 1;
|
||||
GL.DrawElements(GetQuadGLPrimitiveType(), datum.TileCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
if (datum.EdgeCount > 0)
|
||||
{
|
||||
BindVertexArray(datum.EdgeVAO);
|
||||
CheckGlError();
|
||||
|
||||
_debugStats.LastGLDrawCalls += 1;
|
||||
GL.DrawElements(GetQuadGLPrimitiveType(), datum.EdgeCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
_debugStats.LastGLDrawCalls += 1;
|
||||
GL.DrawElements(GetQuadGLPrimitiveType(), datum.TileCount * GetQuadBatchIndexCount(), DrawElementsType.UnsignedShort, 0);
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
requiresFlush = false;
|
||||
@@ -117,6 +170,17 @@ namespace Robust.Client.Graphics.Clyde
|
||||
CullEmptyChunks();
|
||||
}
|
||||
|
||||
private MapChunkData EnsureChunkInitialized(Dictionary<Vector2i, MapChunkData> data, MapChunk chunk, Entity<MapGridComponent> mapGrid)
|
||||
{
|
||||
if (!data.TryGetValue(chunk.Indices, out var datum))
|
||||
{
|
||||
data[chunk.Indices] = datum = new MapChunkData();
|
||||
_initChunkBuffers(mapGrid, chunk, datum);
|
||||
}
|
||||
|
||||
return datum;
|
||||
}
|
||||
|
||||
private void CullEmptyChunks()
|
||||
{
|
||||
foreach (var (grid, chunks) in _mapChunkData)
|
||||
@@ -138,66 +202,141 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void _updateChunkMesh(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
|
||||
{
|
||||
Span<ushort> indexBuffer = stackalloc ushort[_indicesPerChunk(chunk)];
|
||||
Span<Vertex2D> vertexBuffer = stackalloc Vertex2D[_verticesPerChunk(chunk)];
|
||||
Span<ushort> indexBuffer = EnsureSize(ref _chunkMeshBuilderIndexBuffer, _indicesPerChunk(chunk));
|
||||
Span<Vertex2D> vertexBuffer = EnsureSize(ref _chunkMeshBuilderVertexBuffer, _verticesPerChunk(chunk));
|
||||
|
||||
var i = 0;
|
||||
var cSz = grid.Comp.ChunkSize;
|
||||
var cScaled = chunk.Indices * cSz;
|
||||
for (ushort x = 0; x < cSz; x++)
|
||||
var chunkSize = grid.Comp.ChunkSize;
|
||||
var chunkOriginScaled = chunk.Indices * chunkSize;
|
||||
|
||||
for (ushort x = 0; x < chunkSize; x++)
|
||||
{
|
||||
for (ushort y = 0; y < cSz; y++)
|
||||
for (ushort y = 0; y < chunkSize; y++)
|
||||
{
|
||||
var gridX = x + chunkOriginScaled.X;
|
||||
var gridY = y + chunkOriginScaled.Y;
|
||||
var tile = chunk.GetTile(x, y);
|
||||
if (tile.IsEmpty)
|
||||
continue;
|
||||
|
||||
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile);
|
||||
|
||||
Box2 region;
|
||||
if (regionMaybe == null || regionMaybe.Length <= tile.Variant)
|
||||
// Tile render
|
||||
if (x != chunkSize && y != chunkSize)
|
||||
{
|
||||
region = _tileDefinitionManager.ErrorTileRegion;
|
||||
}
|
||||
else
|
||||
{
|
||||
region = regionMaybe[tile.Variant];
|
||||
}
|
||||
// ReSharper disable once IntVariableOverflowInUncheckedContext
|
||||
if (tile.IsEmpty)
|
||||
continue;
|
||||
|
||||
var gx = x + cScaled.X;
|
||||
var gy = y + cScaled.Y;
|
||||
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile);
|
||||
|
||||
var vIdx = i * 4;
|
||||
vertexBuffer[vIdx + 0] = new Vertex2D(gx, gy, region.Left, region.Bottom, Color.White);
|
||||
vertexBuffer[vIdx + 1] = new Vertex2D(gx + 1, gy, region.Right, region.Bottom, Color.White);
|
||||
vertexBuffer[vIdx + 2] = new Vertex2D(gx + 1, gy + 1, region.Right, region.Top, Color.White);
|
||||
vertexBuffer[vIdx + 3] = new Vertex2D(gx, gy + 1, region.Left, region.Top, Color.White);
|
||||
var nIdx = i * GetQuadBatchIndexCount();
|
||||
var tIdx = (ushort)(i * 4);
|
||||
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
|
||||
i += 1;
|
||||
Box2 region;
|
||||
if (regionMaybe == null || regionMaybe.Length <= tile.Variant)
|
||||
{
|
||||
region = _tileDefinitionManager.ErrorTileRegion;
|
||||
}
|
||||
else
|
||||
{
|
||||
region = regionMaybe[tile.Variant];
|
||||
}
|
||||
|
||||
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var indexSlice = indexBuffer[..(i * GetQuadBatchIndexCount())];
|
||||
var vertSlice = vertexBuffer[..(i * 4)];
|
||||
|
||||
GL.BindVertexArray(datum.VAO);
|
||||
CheckGlError();
|
||||
datum.EBO.Use();
|
||||
datum.VBO.Use();
|
||||
datum.EBO.Reallocate(indexBuffer[..(i * GetQuadBatchIndexCount())]);
|
||||
datum.VBO.Reallocate(vertexBuffer[..(i * 4)]);
|
||||
datum.Dirty = false;
|
||||
datum.EBO.Reallocate(indexSlice);
|
||||
datum.VBO.Reallocate(vertSlice);
|
||||
|
||||
datum.TileCount = i;
|
||||
datum.Dirty = false;
|
||||
}
|
||||
|
||||
private unsafe MapChunkData _initChunkBuffers(Entity<MapGridComponent> grid, MapChunk chunk)
|
||||
private void _updateChunkEdges(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
|
||||
{
|
||||
// Need a buffer that can potentially store all neighbor tiles
|
||||
Span<ushort> indexBuffer = EnsureSize(ref _chunkMeshBuilderIndexBuffer, _indicesPerChunk(chunk) * 8);
|
||||
Span<Vertex2D> vertexBuffer = EnsureSize(ref _chunkMeshBuilderVertexBuffer, _verticesPerChunk(chunk) * 8);
|
||||
|
||||
var i = 0;
|
||||
var chunkSize = grid.Comp.ChunkSize;
|
||||
var chunkOriginScaled = chunk.Indices * chunkSize;
|
||||
var maps = _entityManager.System<SharedMapSystem>();
|
||||
|
||||
for (ushort x = 0; x < chunkSize; x++)
|
||||
{
|
||||
for (ushort y = 0; y < chunkSize; y++)
|
||||
{
|
||||
var gridX = x + chunkOriginScaled.X;
|
||||
var gridY = y + chunkOriginScaled.Y;
|
||||
var tile = chunk.GetTile(x, y);
|
||||
var tileDef = _tileDefinitionManager[tile.TypeId];
|
||||
|
||||
// Edge render
|
||||
for (var nx = -1; nx <= 1; nx++)
|
||||
{
|
||||
for (var ny = -1; ny <= 1; ny++)
|
||||
{
|
||||
if (nx == 0 && ny == 0)
|
||||
continue;
|
||||
|
||||
var neighborIndices = new Vector2i(gridX + nx, gridY + ny);
|
||||
if (!maps.TryGetTile(grid.Comp, neighborIndices, out var neighborTile))
|
||||
continue;
|
||||
|
||||
var neighborDef = _tileDefinitionManager[neighborTile.TypeId];
|
||||
|
||||
// If it's the same tile then no edge to be drawn.
|
||||
if (tile.TypeId == neighborTile.TypeId || neighborDef.EdgeSprites.Count == 0)
|
||||
continue;
|
||||
|
||||
// If neighbor is a lower priority then us then don't draw on our tile.
|
||||
if (neighborDef.EdgeSpritePriority < tileDef.EdgeSpritePriority)
|
||||
continue;
|
||||
|
||||
var direction = new Vector2i(nx, ny).AsDirection().GetOpposite();
|
||||
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(neighborTile.TypeId, direction);
|
||||
|
||||
if (regionMaybe == null)
|
||||
continue;
|
||||
|
||||
var region = regionMaybe[0];
|
||||
WriteTileToBuffers(i, gridX, gridY, vertexBuffer, indexBuffer, region);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We don't save the edge buffers back because we might need to re-use it if a neighbor chunk updates.
|
||||
var indexSlice = indexBuffer[..(i * GetQuadBatchIndexCount())];
|
||||
var vertSlice = vertexBuffer[..(i * 4)];
|
||||
|
||||
GL.BindVertexArray(datum.EdgeVAO);
|
||||
CheckGlError();
|
||||
datum.EdgeEBO.Use();
|
||||
datum.EdgeVBO.Use();
|
||||
datum.EdgeEBO.Reallocate(indexSlice);
|
||||
datum.EdgeVBO.Reallocate(vertSlice);
|
||||
|
||||
datum.EdgeCount = i;
|
||||
datum.EdgeDirty = false;
|
||||
}
|
||||
|
||||
private unsafe void _initChunkBuffers(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
|
||||
{
|
||||
var vboSize = _verticesPerChunk(chunk) * sizeof(Vertex2D);
|
||||
var eboSize = _indicesPerChunk(chunk) * sizeof(ushort);
|
||||
|
||||
// Base VAO
|
||||
var vao = GenVertexArray();
|
||||
BindVertexArray(vao);
|
||||
CheckGlError();
|
||||
|
||||
var vboSize = _verticesPerChunk(chunk) * sizeof(Vertex2D);
|
||||
var eboSize = _indicesPerChunk(chunk) * sizeof(ushort);
|
||||
|
||||
var vbo = new GLBuffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw,
|
||||
vboSize, $"Grid {grid.Owner} chunk {chunk.Indices} VBO");
|
||||
var ebo = new GLBuffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw,
|
||||
@@ -212,12 +351,30 @@ namespace Robust.Client.Graphics.Clyde
|
||||
vbo.Use();
|
||||
ebo.Use();
|
||||
|
||||
var datum = new MapChunkData(vao, vbo, ebo)
|
||||
{
|
||||
Dirty = true
|
||||
};
|
||||
datum.EBO = ebo;
|
||||
datum.VBO = vbo;
|
||||
datum.VAO = vao;
|
||||
|
||||
return datum;
|
||||
// EdgeVAO
|
||||
var edgeVao = GenVertexArray();
|
||||
BindVertexArray(edgeVao);
|
||||
CheckGlError();
|
||||
|
||||
var edgeVbo = new GLBuffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw,
|
||||
vboSize * 8, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeVBO");
|
||||
var edgeEbo = new GLBuffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw,
|
||||
eboSize * 8, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeEBO");
|
||||
|
||||
ObjectLabelMaybe(ObjectLabelIdentifier.VertexArray, vao, $"Grid {grid.Owner} chunk {chunk.Indices} EdgeVAO");
|
||||
SetupVAOLayout();
|
||||
CheckGlError();
|
||||
|
||||
edgeVbo.Use();
|
||||
edgeEbo.Use();
|
||||
|
||||
datum.EdgeEBO = edgeEbo;
|
||||
datum.EdgeVBO = edgeVbo;
|
||||
datum.EdgeVAO = edgeVao;
|
||||
}
|
||||
|
||||
private void DeleteChunk(MapChunkData data)
|
||||
@@ -254,19 +411,49 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_mapChunkData.Remove(gridId);
|
||||
}
|
||||
|
||||
private static T[] EnsureSize<T>(ref T[]? field, int size)
|
||||
{
|
||||
if (field == null || field.Length < size)
|
||||
field = new T[size];
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
private void WriteTileToBuffers(
|
||||
int i,
|
||||
int gridX,
|
||||
int gridY,
|
||||
Span<Vertex2D> vertexBuffer,
|
||||
Span<ushort> indexBuffer,
|
||||
Box2 region)
|
||||
{
|
||||
var vIdx = i * 4;
|
||||
vertexBuffer[vIdx + 0] = new Vertex2D(gridX, gridY, region.Left, region.Bottom, Color.White);
|
||||
vertexBuffer[vIdx + 1] = new Vertex2D(gridX + 1, gridY, region.Right, region.Bottom, Color.White);
|
||||
vertexBuffer[vIdx + 2] = new Vertex2D(gridX + 1, gridY + 1, region.Right, region.Top, Color.White);
|
||||
vertexBuffer[vIdx + 3] = new Vertex2D(gridX, gridY + 1, region.Left, region.Top, Color.White);
|
||||
var nIdx = i * GetQuadBatchIndexCount();
|
||||
var tIdx = (ushort)(i * 4);
|
||||
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
|
||||
}
|
||||
|
||||
private sealed class MapChunkData
|
||||
{
|
||||
public bool Dirty;
|
||||
public readonly uint VAO;
|
||||
public readonly GLBuffer VBO;
|
||||
public readonly GLBuffer EBO;
|
||||
public bool EdgeDirty = true;
|
||||
public bool Dirty = true;
|
||||
|
||||
public uint VAO;
|
||||
public GLBuffer VBO = default!;
|
||||
public GLBuffer EBO = default!;
|
||||
public int TileCount;
|
||||
|
||||
public MapChunkData(uint vao, GLBuffer vbo, GLBuffer ebo)
|
||||
public uint EdgeVAO;
|
||||
public GLBuffer EdgeVBO = default!;
|
||||
public GLBuffer EdgeEBO = default!;
|
||||
public int EdgeCount;
|
||||
|
||||
public MapChunkData()
|
||||
{
|
||||
VAO = vao;
|
||||
VBO = vbo;
|
||||
EBO = ebo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
DebugTools.Assert(space != OverlaySpace.ScreenSpaceBelowWorld && space != OverlaySpace.ScreenSpace);
|
||||
|
||||
var mapId = vp.Eye!.Position.MapId;
|
||||
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), _mapManager.GetMapEntityIdOrThrow(mapId), mapId, worldBox, worldBounds);
|
||||
var args = new OverlayDrawArgs(space, null, vp, _renderHandle, new UIBox2i((0, 0), vp.Size), _mapSystem.GetMapOrInvalid(mapId), mapId, worldBox, worldBounds);
|
||||
|
||||
if (!overlay.BeforeDraw(args))
|
||||
return;
|
||||
@@ -178,7 +178,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var worldAABB = worldBounds.CalcBoundingBox();
|
||||
var mapId = vp.Eye!.Position.MapId;
|
||||
|
||||
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, _mapManager.GetMapEntityIdOrThrow(mapId), mapId, worldAABB, worldBounds);
|
||||
var args = new OverlayDrawArgs(space, vpControl, vp, handle, bounds, _mapSystem.GetMapOrInvalid(mapId), mapId, worldAABB, worldBounds);
|
||||
|
||||
foreach (var overlay in list)
|
||||
{
|
||||
|
||||
@@ -98,9 +98,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private LightCapacityComparer _lightCap = new();
|
||||
private ShadowCapacityComparer _shadowCap = new ShadowCapacityComparer();
|
||||
|
||||
private float _maxLightRadius;
|
||||
|
||||
private unsafe void InitLighting()
|
||||
{
|
||||
|
||||
_cfg.OnValueChanged(CVars.MaxLightRadius, val => { _maxLightRadius = val;}, true);
|
||||
|
||||
// Other...
|
||||
LoadLightingShaders();
|
||||
@@ -617,8 +619,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// Use worldbounds for this one as we only care if the light intersects our actual bounds
|
||||
var xforms = _entityManager.GetEntityQuery<TransformComponent>();
|
||||
var state = (this, count: 0, shadowCastingCount: 0, xforms, worldAABB);
|
||||
var lightAabb = worldAABB.Enlarged(_maxLightRadius);
|
||||
|
||||
foreach (var (uid, comp) in _lightTreeSystem.GetIntersectingTrees(map, worldAABB))
|
||||
foreach (var (uid, comp) in _lightTreeSystem.GetIntersectingTrees(map, lightAabb))
|
||||
{
|
||||
var bounds = _transformSystem.GetInvWorldMatrix(uid, xforms).TransformBox(worldBounds);
|
||||
comp.Tree.QueryAabb(ref state, LightQuery, bounds);
|
||||
|
||||
@@ -523,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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Client.Input;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
@@ -23,6 +24,7 @@ using Robust.Shared.Maths;
|
||||
using Robust.Shared.Profiling;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using TextureWrapMode = Robust.Shared.Graphics.TextureWrapMode;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
@@ -48,6 +50,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly ClientEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IReloadManager _reloads = default!;
|
||||
|
||||
private GLUniformBuffer<ProjViewMatrices> ProjViewUBO = default!;
|
||||
private GLUniformBuffer<UniformConstants> UniformConstantsUBO = default!;
|
||||
@@ -99,6 +103,16 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_sawmillOgl = _logManager.GetSawmill("clyde.ogl");
|
||||
_sawmillWin = _logManager.GetSawmill("clyde.win");
|
||||
|
||||
_reloads.Register("/Shaders", "*.swsl");
|
||||
_reloads.Register("/Textures/Shaders", "*.swsl");
|
||||
_reloads.Register("/Textures", "*.jpg");
|
||||
_reloads.Register("/Textures", "*.jpeg");
|
||||
_reloads.Register("/Textures", "*.png");
|
||||
_reloads.Register("/Textures", "*.webp");
|
||||
|
||||
_reloads.OnChanged += OnChange;
|
||||
_proto.PrototypesReloaded += OnProtoReload;
|
||||
|
||||
_cfg.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
|
||||
_cfg.OnValueChanged(CVars.DisplayVSync, VSyncChanged, true);
|
||||
_cfg.OnValueChanged(CVars.DisplayWindowMode, WindowModeChanged, true);
|
||||
@@ -122,6 +136,38 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return InitWindowing();
|
||||
}
|
||||
|
||||
private void OnProtoReload(PrototypesReloadedEventArgs obj)
|
||||
{
|
||||
if (!obj.WasModified<ShaderPrototype>())
|
||||
return;
|
||||
|
||||
foreach (var shader in obj.ByType[typeof(ShaderPrototype)].Modified.Keys)
|
||||
{
|
||||
_resourceCache.ReloadResource<ShaderSourceResource>(shader);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChange(ResPath obj)
|
||||
{
|
||||
if ((obj.TryRelativeTo(new ResPath("/Shaders"), out _) || obj.TryRelativeTo(new ResPath("/Textures/Shaders"), out _)) && obj.Extension == "swsl")
|
||||
{
|
||||
_resourceCache.ReloadResource<ShaderSourceResource>(obj);
|
||||
}
|
||||
|
||||
if (obj.TryRelativeTo(new ResPath("/Textures"), out _) && !obj.TryRelativeTo(new ResPath("/Textures/Tiles"), out _))
|
||||
{
|
||||
if (obj.Extension == "jpg" || obj.Extension == "jpeg" || obj.Extension == "webp")
|
||||
{
|
||||
_resourceCache.ReloadResource<TextureResource>(obj);
|
||||
}
|
||||
|
||||
if (obj.Extension == "png")
|
||||
{
|
||||
_resourceCache.ReloadResource<TextureResource>(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool InitializePostWindowing()
|
||||
{
|
||||
_gameThread = Thread.CurrentThread;
|
||||
|
||||
@@ -369,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)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
// UV coordinates in texture-space. I.e., (0,0) is the corner of the texture currently being used to draw.
|
||||
// When drawing a sprite from a texture atlas, (0,0) is the corner of the atlas, not the specific sprite being drawn.
|
||||
varying highp vec2 UV;
|
||||
|
||||
// UV coordinates in quad-space. I.e., when drawing a sprite from a texture atlas (0,0) is the corner of the sprite
|
||||
// currently being drawn.
|
||||
varying highp vec2 UV2;
|
||||
|
||||
// TBH I'm not sure what this is for. I think it is scree UV coordiantes, i.e., FRAGCOORD.xy * SCREEN_PIXEL_SIZE ?
|
||||
// TODO CLYDE Is this still needed?
|
||||
varying highp vec2 Pos;
|
||||
|
||||
// Vertex colour modulation. Note that negative values imply that the LIGHTMAP should be ignored. This is used to avoid
|
||||
// having to set the texture to a white/blank texture for sprites that have no light shading applied.
|
||||
varying highp vec4 VtxModulate;
|
||||
|
||||
// The current light map. Unless disabled, this is automatically sampled to create the LIGHT vector, which is then used
|
||||
// to modulate the output colour.
|
||||
// TODO CLYDE consistent shader variable naming
|
||||
uniform sampler2D lightMap;
|
||||
|
||||
// [SHADER_HEADER_CODE]
|
||||
@@ -11,11 +25,37 @@ void main()
|
||||
{
|
||||
highp vec4 FRAGCOORD = gl_FragCoord;
|
||||
|
||||
// The output colour. This should get set by the shader code block.
|
||||
// This will get modified by the LIGHT and MODULATE vectors.
|
||||
lowp vec4 COLOR;
|
||||
|
||||
lowp vec3 lightSample = texture2D(lightMap, Pos).rgb;
|
||||
// The light colour, usually sampled from the LIGHTMAP
|
||||
lowp vec4 LIGHT;
|
||||
|
||||
// Colour modulation vector.
|
||||
highp vec4 MODULATE;
|
||||
|
||||
// Sample the texture outside of the branch / with uniform control flow.
|
||||
LIGHT = texture2D(lightMap, Pos);
|
||||
|
||||
if (VtxModulate.x < 0.0)
|
||||
{
|
||||
// Negative VtxModulate implies unshaded/no lighting.
|
||||
MODULATE = -1.0 - VtxModulate;
|
||||
LIGHT = vec4(1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
MODULATE = VtxModulate;
|
||||
}
|
||||
|
||||
// TODO CLYDE consistent shader variable naming
|
||||
// Requires breaking changes.
|
||||
lowp vec3 lightSample = LIGHT.xyz;
|
||||
|
||||
// [SHADER_CODE]
|
||||
|
||||
gl_FragColor = zAdjustResult(COLOR * VtxModulate * vec4(lightSample, 1.0));
|
||||
LIGHT.xyz = lightSample;
|
||||
|
||||
gl_FragColor = zAdjustResult(COLOR * MODULATE * LIGHT);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ uniform mat3 modelMatrix;
|
||||
// Allows us to do texture atlassing with texture coordinates 0->1
|
||||
// Input texture coordinates get mapped to this range.
|
||||
uniform vec4 modifyUV;
|
||||
// TODO CLYDE Is this still needed?
|
||||
|
||||
// [SHADER_HEADER_CODE]
|
||||
|
||||
@@ -39,5 +40,15 @@ void main()
|
||||
Pos = (VERTEX + 1.0) / 2.0;
|
||||
UV = mix(modifyUV.xy, modifyUV.zw, tCoord);
|
||||
UV2 = tCoord2;
|
||||
VtxModulate = zFromSrgb(modulate);
|
||||
|
||||
// Negative modulation is being used as a hacky way to squeeze in lighting data.
|
||||
// I.e., negative modulation implies we ignore the lighting.
|
||||
if (modulate.x < 0.0)
|
||||
{
|
||||
VtxModulate = -1.0 - zFromSrgb(-1.0 - modulate);
|
||||
}
|
||||
else
|
||||
{
|
||||
VtxModulate = zFromSrgb(modulate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
varying highp vec2 UV;
|
||||
varying highp vec2 UV2;
|
||||
|
||||
// TODO CLYDE consistent shader variable naming
|
||||
uniform sampler2D lightMap;
|
||||
|
||||
// [SHADER_HEADER_CODE]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -16,7 +16,7 @@ using Vector4 = Robust.Shared.Maths.Vector4;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
[Prototype("shader")]
|
||||
[Prototype]
|
||||
public sealed partial class ShaderPrototype : IPrototype, ISerializationHooks
|
||||
{
|
||||
[ViewVariables]
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace Robust.Client.Map
|
||||
{
|
||||
internal sealed class ClydeTileDefinitionManager : TileDefinitionManager, IClydeTileDefinitionManager, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
[Dependency] private readonly IResourceManager _manager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Robust.Client.Map
|
||||
|
||||
public Texture TileTextureAtlas => _tileTextureAtlas ?? Texture.Transparent;
|
||||
|
||||
private readonly Dictionary<int, Box2[]> _tileRegions = new();
|
||||
private FrozenDictionary<(int Id, Direction Direction), Box2[]> _tileRegions = FrozenDictionary<(int Id, Direction Direction), Box2[]>.Empty;
|
||||
|
||||
public Box2 ErrorTileRegion { get; private set; }
|
||||
|
||||
@@ -45,7 +45,14 @@ namespace Robust.Client.Map
|
||||
/// <inheritdoc />
|
||||
public Box2[]? TileAtlasRegion(int tileType)
|
||||
{
|
||||
if (_tileRegions.TryGetValue(tileType, out var region))
|
||||
return TileAtlasRegion(tileType, Direction.Invalid);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Box2[]? TileAtlasRegion(int tileType, Direction direction)
|
||||
{
|
||||
// ReSharper disable once CanSimplifyDictionaryTryGetValueWithGetValueOrDefault
|
||||
if (_tileRegions.TryGetValue((tileType, direction), out var region))
|
||||
{
|
||||
return region;
|
||||
}
|
||||
@@ -57,12 +64,37 @@ namespace Robust.Client.Map
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_protoManager.PrototypesReloaded += OnProtoReload;
|
||||
|
||||
_reload.Register("/Textures/Tiles", "*.png");
|
||||
_reload.OnChanged += OnReload;
|
||||
|
||||
_genTextureAtlas();
|
||||
}
|
||||
|
||||
private void OnProtoReload(PrototypesReloadedEventArgs obj)
|
||||
{
|
||||
if (!obj.WasModified<ITileDefinition>())
|
||||
return;
|
||||
|
||||
_genTextureAtlas();
|
||||
}
|
||||
|
||||
private void OnReload(ResPath obj)
|
||||
{
|
||||
if (obj.Extension != "png")
|
||||
return;
|
||||
|
||||
if (!obj.TryRelativeTo(new ResPath("/Textures/Tiles"), out _))
|
||||
return;
|
||||
|
||||
_genTextureAtlas();
|
||||
}
|
||||
|
||||
internal void _genTextureAtlas()
|
||||
{
|
||||
_tileRegions.Clear();
|
||||
var sw = RStopwatch.StartNew();
|
||||
var tileRegs = new Dictionary<(int Id, Direction Direction), Box2[]>();
|
||||
_tileTextureAtlas = null;
|
||||
|
||||
var defList = TileDefs.Where(t => t.Sprite != null).ToList();
|
||||
@@ -73,7 +105,7 @@ namespace Robust.Client.Map
|
||||
|
||||
const int tileSize = EyeManager.PixelsPerMeter;
|
||||
|
||||
var tileCount = defList.Select(x => (int)x.Variants).Sum() + 1;
|
||||
var tileCount = defList.Select(x => x.Variants + x.EdgeSprites.Count).Sum() + 1;
|
||||
|
||||
var dimensionX = (int) Math.Ceiling(Math.Sqrt(tileCount));
|
||||
var dimensionY = (int) Math.Ceiling((float) tileCount / dimensionX);
|
||||
@@ -81,11 +113,11 @@ namespace Robust.Client.Map
|
||||
var imgWidth = dimensionX * tileSize;
|
||||
var imgHeight = dimensionY * tileSize;
|
||||
var sheet = new Image<Rgba32>(imgWidth, imgHeight);
|
||||
var w = (float) sheet.Width;
|
||||
var h = (float) sheet.Height;
|
||||
|
||||
// Add in the missing tile texture sprite as tile texture 0.
|
||||
{
|
||||
var w = (float) sheet.Width;
|
||||
var h = (float) sheet.Height;
|
||||
ErrorTileRegion = Box2.FromDimensions(
|
||||
0, (h - EyeManager.PixelsPerMeter) / h,
|
||||
tileSize / w, tileSize / h);
|
||||
@@ -133,25 +165,98 @@ namespace Robust.Client.Map
|
||||
var box = new UIBox2i(0, 0, tileSize, tileSize).Translated(new Vector2i(j * tileSize, 0));
|
||||
image.Blit(box, sheet, point);
|
||||
|
||||
var w = (float) sheet.Width;
|
||||
var h = (float) sheet.Height;
|
||||
|
||||
regionList[j] = Box2.FromDimensions(
|
||||
point.X / w, (h - point.Y - EyeManager.PixelsPerMeter) / h,
|
||||
tileSize / w, tileSize / h);
|
||||
column++;
|
||||
|
||||
if (column >= dimensionX)
|
||||
{
|
||||
column = 0;
|
||||
row++;
|
||||
}
|
||||
BumpColumn(ref row, ref column, dimensionX);
|
||||
}
|
||||
|
||||
_tileRegions.Add(def.TileId, regionList);
|
||||
tileRegs.Add((def.TileId, Direction.Invalid), regionList);
|
||||
|
||||
// Edges
|
||||
if (def.EdgeSprites.Count <= 0)
|
||||
continue;
|
||||
|
||||
foreach (var direction in DirectionExtensions.AllDirections)
|
||||
{
|
||||
if (!def.EdgeSprites.TryGetValue(direction, out var edge))
|
||||
continue;
|
||||
|
||||
using (var stream = _manager.ContentFileRead(edge))
|
||||
{
|
||||
image = Image.Load<Rgba32>(stream);
|
||||
}
|
||||
|
||||
if (image.Width != tileSize || image.Height != tileSize)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"Unable to load {path}, due to being unable to use tile textures with a dimension other than {tileSize}x{tileSize}.");
|
||||
}
|
||||
|
||||
Angle angle = Angle.Zero;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
// Corner sprites
|
||||
case Direction.SouthEast:
|
||||
break;
|
||||
case Direction.NorthEast:
|
||||
angle = new Angle(MathF.PI / 2f);
|
||||
break;
|
||||
case Direction.NorthWest:
|
||||
angle = new Angle(MathF.PI);
|
||||
break;
|
||||
case Direction.SouthWest:
|
||||
angle = new Angle(MathF.PI * 1.5f);
|
||||
break;
|
||||
// Edge sprites
|
||||
case Direction.South:
|
||||
break;
|
||||
case Direction.East:
|
||||
angle = new Angle(MathF.PI / 2f);
|
||||
break;
|
||||
case Direction.North:
|
||||
angle = new Angle(MathF.PI);
|
||||
break;
|
||||
case Direction.West:
|
||||
angle = new Angle(MathF.PI * 1.5f);
|
||||
break;
|
||||
}
|
||||
|
||||
if (angle != Angle.Zero)
|
||||
{
|
||||
image.Mutate(o => o.Rotate((float)-angle.Degrees));
|
||||
}
|
||||
|
||||
var point = new Vector2i(column * tileSize, row * tileSize);
|
||||
var box = new UIBox2i(0, 0, tileSize, tileSize);
|
||||
image.Blit(box, sheet, point);
|
||||
|
||||
// If you ever need edge variants then you could just bump this.
|
||||
var edgeList = new Box2[1];
|
||||
edgeList[0] = Box2.FromDimensions(
|
||||
point.X / w, (h - point.Y - EyeManager.PixelsPerMeter) / h,
|
||||
tileSize / w, tileSize / h);
|
||||
|
||||
tileRegs.Add((def.TileId, direction), edgeList);
|
||||
BumpColumn(ref row, ref column, dimensionX);
|
||||
}
|
||||
}
|
||||
|
||||
_tileRegions = tileRegs.ToFrozenDictionary();
|
||||
_tileTextureAtlas = Texture.LoadFromImage(sheet, "Tile Atlas");
|
||||
_sawmill.Debug($"Tile atlas took {sw.Elapsed} to build");
|
||||
}
|
||||
|
||||
private void BumpColumn(ref int row, ref int column, int dimensionX)
|
||||
{
|
||||
column++;
|
||||
|
||||
if (column >= dimensionX)
|
||||
{
|
||||
column = 0;
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
|
||||
@@ -29,5 +29,12 @@ namespace Robust.Client.Map
|
||||
/// </summary>
|
||||
/// <returns>If null, do not draw the tile at all.</returns>
|
||||
Box2[]? TileAtlasRegion(int tileType);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the region inside the texture atlas to use to draw a tile type.
|
||||
/// Also handles edge sprites.
|
||||
/// </summary>
|
||||
/// <returns>If null, do not draw the tile at all.</returns>
|
||||
public Box2[]? TileAtlasRegion(int tileType, Direction direction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Map;
|
||||
|
||||
/// <summary>
|
||||
/// Draws border sprites for tiles that support them.
|
||||
/// </summary>
|
||||
public sealed class TileEdgeOverlay : GridOverlay
|
||||
{
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly IResourceCache _resource;
|
||||
private readonly ITileDefinitionManager _tileDefManager;
|
||||
|
||||
public TileEdgeOverlay(IEntityManager entManager, IResourceCache resource, ITileDefinitionManager tileDefManager)
|
||||
{
|
||||
_entManager = entManager;
|
||||
_resource = resource;
|
||||
_tileDefManager = tileDefManager;
|
||||
ZIndex = -1;
|
||||
}
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (args.MapId == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
var mapSystem = _entManager.System<SharedMapSystem>();
|
||||
var xformSystem = _entManager.System<SharedTransformSystem>();
|
||||
|
||||
var tileSize = Grid.Comp.TileSize;
|
||||
var tileDimensions = new Vector2(tileSize, tileSize);
|
||||
var (_, _, worldMatrix, invMatrix) = xformSystem.GetWorldPositionRotationMatrixWithInv(Grid.Owner);
|
||||
args.WorldHandle.SetTransform(worldMatrix);
|
||||
var bounds = args.WorldBounds;
|
||||
bounds = new Box2Rotated(bounds.Box.Enlarged(1), bounds.Rotation, bounds.Origin);
|
||||
var localAABB = invMatrix.TransformBox(bounds);
|
||||
|
||||
var enumerator = mapSystem.GetLocalTilesEnumerator(Grid.Owner, Grid, localAABB, false);
|
||||
|
||||
while (enumerator.MoveNext(out var tileRef))
|
||||
{
|
||||
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
|
||||
|
||||
if (tileDef.EdgeSprites.Count == 0)
|
||||
continue;
|
||||
|
||||
// Get what tiles border us to determine what sprites we need to draw.
|
||||
for (var x = -1; x <= 1; x++)
|
||||
{
|
||||
for (var y = -1; y <= 1; y++)
|
||||
{
|
||||
if (x == 0 && y == 0)
|
||||
continue;
|
||||
|
||||
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
|
||||
var neighborTile = mapSystem.GetTileRef(Grid.Owner, Grid, neighborIndices);
|
||||
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
|
||||
|
||||
// If it's the same tile then no edge to be drawn.
|
||||
if (tileRef.Tile.TypeId == neighborTile.Tile.TypeId)
|
||||
continue;
|
||||
|
||||
// Don't draw if the the neighbor tile edges should draw over us (or if we have the same priority)
|
||||
if (neighborDef.EdgeSprites.Count != 0 && neighborDef.EdgeSpritePriority >= tileDef.EdgeSpritePriority)
|
||||
continue;
|
||||
|
||||
var direction = new Vector2i(x, y).AsDirection();
|
||||
|
||||
// No edge tile
|
||||
if (!tileDef.EdgeSprites.TryGetValue(direction, out var edgePath))
|
||||
continue;
|
||||
|
||||
var texture = _resource.GetResource<TextureResource>(edgePath);
|
||||
var box = Box2.FromDimensions(neighborIndices, tileDimensions);
|
||||
|
||||
var angle = Angle.Zero;
|
||||
|
||||
// If we ever need one for both cardinals and corners then update this.
|
||||
switch (direction)
|
||||
{
|
||||
// Corner sprites
|
||||
case Direction.SouthEast:
|
||||
break;
|
||||
case Direction.NorthEast:
|
||||
angle = new Angle(MathF.PI / 2f);
|
||||
break;
|
||||
case Direction.NorthWest:
|
||||
angle = new Angle(MathF.PI);
|
||||
break;
|
||||
case Direction.SouthWest:
|
||||
angle = new Angle(MathF.PI * 1.5f);
|
||||
break;
|
||||
// Edge sprites
|
||||
case Direction.South:
|
||||
break;
|
||||
case Direction.East:
|
||||
angle = new Angle(MathF.PI / 2f);
|
||||
break;
|
||||
case Direction.North:
|
||||
angle = new Angle(MathF.PI);
|
||||
break;
|
||||
case Direction.West:
|
||||
angle = new Angle(MathF.PI * 1.5f);
|
||||
break;
|
||||
}
|
||||
|
||||
if (angle == Angle.Zero)
|
||||
args.WorldHandle.DrawTextureRect(texture.Texture, box);
|
||||
else
|
||||
args.WorldHandle.DrawTextureRect(texture.Texture, new Box2Rotated(box, angle, box.Center));
|
||||
|
||||
RequiresFlush = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args.WorldHandle.SetTransform(Matrix3x2.Identity);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
namespace Robust.Client.Prototypes
|
||||
{
|
||||
public sealed class ClientPrototypeManager : PrototypeManager
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] private readonly IClientGameTiming _timing = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameControllerInternal _controller = default!;
|
||||
|
||||
private readonly List<FileSystemWatcher> _watchers = new();
|
||||
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
|
||||
private CancellationTokenSource _reloadToken = new();
|
||||
private readonly HashSet<ResPath> _reloadQueue = new();
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -37,9 +25,8 @@ namespace Robust.Client.Prototypes
|
||||
|
||||
_netManager.RegisterNetMessage<MsgReloadPrototypes>(accept: NetMessageAccept.Server);
|
||||
|
||||
_clyde.OnWindowFocused += WindowFocusedChanged;
|
||||
|
||||
WatchResources();
|
||||
_reload.Register("/Prototypes", "*.yml");
|
||||
_reload.OnChanged += ReloadPrototypeQueue;
|
||||
}
|
||||
|
||||
public override void LoadDefaultPrototypes(Dictionary<Type, HashSet<string>>? changed = null)
|
||||
@@ -49,99 +36,27 @@ namespace Robust.Client.Prototypes
|
||||
ResolveResults();
|
||||
}
|
||||
|
||||
private void WindowFocusedChanged(WindowFocusedEventArgs args)
|
||||
private void ReloadPrototypeQueue(ResPath file)
|
||||
{
|
||||
#if TOOLS
|
||||
if (args.Focused && _reloadQueue.Count > 0)
|
||||
{
|
||||
Timer.Spawn(_reloadDelay, ReloadPrototypeQueue, _reloadToken.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
_reloadToken.Cancel();
|
||||
_reloadToken = new CancellationTokenSource();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (file.Extension != "yml")
|
||||
return;
|
||||
|
||||
private void ReloadPrototypeQueue()
|
||||
{
|
||||
#if TOOLS
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
var msg = new MsgReloadPrototypes();
|
||||
msg.Paths = _reloadQueue.ToArray();
|
||||
var msg = new MsgReloadPrototypes
|
||||
{
|
||||
Paths = [file]
|
||||
};
|
||||
_netManager.ClientSendMessage(msg);
|
||||
|
||||
// Reloading prototypes modifies entities. This currently causes some state management debug asserts to
|
||||
// fail. To avoid this, we set `IGameTiming.ApplyingState` to true, even though this isn't really applying a
|
||||
// server state.
|
||||
using var _ = _timing.StartStateApplicationArea();
|
||||
ReloadPrototypes(_reloadQueue);
|
||||
|
||||
_reloadQueue.Clear();
|
||||
ReloadPrototypes([file]);
|
||||
|
||||
Logger.Info($"Reloaded prototypes in {sw.ElapsedMilliseconds} ms");
|
||||
#endif
|
||||
}
|
||||
|
||||
private void WatchResources()
|
||||
{
|
||||
if (!_cfg.GetCVar(CVars.ResPrototypeReloadWatch))
|
||||
return;
|
||||
|
||||
#if TOOLS
|
||||
foreach (var path in Resources.GetContentRoots().Select(r => r.ToString())
|
||||
.Where(r => Directory.Exists(r + "/Prototypes")).Select(p => p + "/Prototypes"))
|
||||
{
|
||||
var watcher = new FileSystemWatcher(path, "*.yml")
|
||||
{
|
||||
IncludeSubdirectories = true,
|
||||
NotifyFilter = NotifyFilters.LastWrite
|
||||
};
|
||||
|
||||
watcher.Changed += (_, args) =>
|
||||
{
|
||||
switch (args.ChangeType)
|
||||
{
|
||||
case WatcherChangeTypes.Renamed:
|
||||
case WatcherChangeTypes.Deleted:
|
||||
return;
|
||||
case WatcherChangeTypes.Created:
|
||||
// case WatcherChangeTypes.Deleted:
|
||||
case WatcherChangeTypes.Changed:
|
||||
case WatcherChangeTypes.All:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
TaskManager.RunOnMainThread(() =>
|
||||
{
|
||||
var file = new ResPath(args.FullPath);
|
||||
|
||||
foreach (var root in Resources.GetContentRoots())
|
||||
{
|
||||
if (!file.TryRelativeTo(root, out var relative))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_reloadQueue.Add(relative.Value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
watcher.EnableRaisingEvents = true;
|
||||
_watchers.Add(watcher);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.Error($"Watching resources in path {path} threw an exception:\n{ex}");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.ContentPack;
|
||||
@@ -26,22 +27,26 @@ public sealed class AudioResource : BaseResource
|
||||
throw new FileNotFoundException("Content file does not exist for audio sample.");
|
||||
}
|
||||
|
||||
using (var fileStream = cache.ContentFileRead(path))
|
||||
using var fileStream = cache.ContentFileRead(path);
|
||||
var audioManager = dependencies.Resolve<IAudioInternal>();
|
||||
if (path.Extension == "ogg")
|
||||
{
|
||||
var audioManager = dependencies.Resolve<IAudioInternal>();
|
||||
if (path.Extension == "ogg")
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
|
||||
}
|
||||
else if (path.Extension == "wav")
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Unable to load audio files outside of ogg Vorbis or PCM wav");
|
||||
}
|
||||
AudioStream = audioManager.LoadAudioOggVorbis(fileStream, path.ToString());
|
||||
}
|
||||
else if (path.Extension == "wav")
|
||||
{
|
||||
AudioStream = audioManager.LoadAudioWav(fileStream, path.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Unable to load audio files outside of ogg Vorbis or PCM wav");
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default)
|
||||
{
|
||||
dependencies.Resolve<IAudioInternal>().Remove(AudioStream);
|
||||
Load(dependencies, path);
|
||||
}
|
||||
|
||||
public AudioResource(AudioStream stream) : base()
|
||||
|
||||
@@ -71,21 +71,11 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
|
||||
// First, we measure non-stretching children.
|
||||
var stretching = new List<Control>();
|
||||
float totalStretchRatio = 0;
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.Visible)
|
||||
continue;
|
||||
|
||||
var stretch = Vertical ? child.VerticalExpand : child.HorizontalExpand;
|
||||
if (stretch)
|
||||
{
|
||||
totalStretchRatio += child.SizeFlagsStretchRatio;
|
||||
stretching.Add(child);
|
||||
continue;
|
||||
}
|
||||
|
||||
child.Measure(availableSize);
|
||||
|
||||
if (Vertical)
|
||||
@@ -102,35 +92,6 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
}
|
||||
|
||||
if (stretching.Count == 0)
|
||||
return desiredSize;
|
||||
|
||||
// Then we measure stretching children
|
||||
foreach (var child in stretching)
|
||||
{
|
||||
var size = availableSize;
|
||||
if (Vertical)
|
||||
{
|
||||
size.Y *= child.SizeFlagsStretchRatio / totalStretchRatio;
|
||||
child.Measure(size);
|
||||
desiredSize.Y += child.DesiredSize.Y;
|
||||
desiredSize.X = Math.Max(desiredSize.X, child.DesiredSize.X);
|
||||
}
|
||||
else
|
||||
{
|
||||
size.X *= child.SizeFlagsStretchRatio / totalStretchRatio;
|
||||
child.Measure(size);
|
||||
desiredSize.X += child.DesiredSize.X;
|
||||
desiredSize.Y = Math.Max(desiredSize.Y, child.DesiredSize.Y);
|
||||
}
|
||||
|
||||
// TODO Maybe make BoxContainer.MeasureOverride more rigorous.
|
||||
// This should check if size < desired size. If it is, treat child as non-stretching (see the code in
|
||||
// ArrangeOverride). This requires remeasuring all stretching controls + the control that just became
|
||||
// non-stretching. But the re-measured controls might then become smaller (e.g. rich text wrapping),
|
||||
// leading to a recursion problem.
|
||||
}
|
||||
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
@@ -176,6 +176,71 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_scrollBar.MoveToEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace the current list of items with the items in newItems.
|
||||
/// newItems should be in the order which they should appear in the list,
|
||||
/// and items are considered equal if the Item text is equal in each item.
|
||||
///
|
||||
/// Provided the existing items have not been re-ordered relative to each
|
||||
/// other, any items which already exist in the list are not destroyed,
|
||||
/// which maintains consistency of scrollbars, selected items, etc.
|
||||
/// </summary>
|
||||
/// <param name="newItems">The list of items to update this list to</param>
|
||||
public void SetItems(List<Item> newItems)
|
||||
{
|
||||
SetItems(newItems, (a,b) => string.Compare(a.Text, b.Text));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// This variant allows for a custom equality operator to compare items, when
|
||||
/// comparing the Item text is not desired.
|
||||
/// </summary>
|
||||
/// <param name="itemCmp">Comparison function to compare existing to new items.</param>
|
||||
public void SetItems(List<Item> newItems, Comparison<Item> itemCmp)
|
||||
{
|
||||
// Walk through the existing items in this list and in newItems
|
||||
// in parallel to synchronize our items with those in newItems.
|
||||
int i = this.Count - 1;
|
||||
int j = newItems.Count - 1;
|
||||
while(i >= 0 && j >= 0)
|
||||
{
|
||||
var cmpResult = itemCmp(this[i], newItems[j]);
|
||||
if (cmpResult == 0)
|
||||
{
|
||||
// This item exists in both our list and `newItems`. Nothing to do.
|
||||
i--;
|
||||
j--;
|
||||
}
|
||||
else if (cmpResult > 0)
|
||||
{
|
||||
// Item exists in our list, but not in `newItems`. Remove it.
|
||||
RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
else if (cmpResult < 0)
|
||||
{
|
||||
// A new entry which doesn't exist in our list. Insert it.
|
||||
Insert(i + 1, newItems[j]);
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining items in our list don't exist in `newItems` so remove them
|
||||
while (i >= 0)
|
||||
{
|
||||
RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
|
||||
// And finally, any remaining items in `newItems` don't exist in our list. Create them.
|
||||
while (j >= 0)
|
||||
{
|
||||
Insert(0, newItems[j]);
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
// Without this attribute, this would compile into a property called "Item", causing problems with the Item class.
|
||||
[System.Runtime.CompilerServices.IndexerName("IndexItem")]
|
||||
public Item this[int index]
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace Robust.Client.UserInterface.Controls
|
||||
[Virtual]
|
||||
public class LineEdit : Control
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -15,10 +16,23 @@ namespace Robust.Client.UserInterface.Controls
|
||||
[Virtual]
|
||||
public class OutputPanel : Control
|
||||
{
|
||||
public const string StyleClassOutputPanelScrollDownButton = "outputPanelScrollDownButton";
|
||||
|
||||
[Dependency] private readonly MarkupTagManager _tagManager = default!;
|
||||
|
||||
public const string StylePropertyStyleBox = "stylebox";
|
||||
|
||||
public bool ShowScrollDownButton
|
||||
{
|
||||
get => _showScrollDownButton;
|
||||
set
|
||||
{
|
||||
_showScrollDownButton = value;
|
||||
_updateScrollButtonVisibility();
|
||||
}
|
||||
}
|
||||
private bool _showScrollDownButton;
|
||||
|
||||
private readonly RingBufferList<RichTextEntry> _entries = new();
|
||||
private bool _isAtBottom = true;
|
||||
|
||||
@@ -26,6 +40,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
private bool _firstLine = true;
|
||||
private StyleBox? _styleBoxOverride;
|
||||
private VScrollBar _scrollBar;
|
||||
private Button _scrollDownButton;
|
||||
|
||||
public bool ScrollFollowing { get; set; } = true;
|
||||
|
||||
@@ -43,7 +58,25 @@ namespace Robust.Client.UserInterface.Controls
|
||||
HorizontalAlignment = HAlignment.Right
|
||||
};
|
||||
AddChild(_scrollBar);
|
||||
_scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
|
||||
|
||||
AddChild(_scrollDownButton = new Button()
|
||||
{
|
||||
Name = "scrollLiveBtn",
|
||||
StyleClasses = { StyleClassOutputPanelScrollDownButton },
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
Text = String.Format("⬇ {0} ⬇", Loc.GetString("output-panel-scroll-down-button-text")),
|
||||
MaxWidth = 300,
|
||||
Visible = false,
|
||||
});
|
||||
|
||||
_scrollDownButton.OnPressed += _ => ScrollToBottom();
|
||||
|
||||
_scrollBar.OnValueChanged += _ =>
|
||||
{
|
||||
_isAtBottom = _scrollBar.IsAtEnd;
|
||||
_updateScrollButtonVisibility();
|
||||
};
|
||||
}
|
||||
|
||||
public int EntryCount => _entries.Count;
|
||||
@@ -184,6 +217,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
var styleBoxSize = _getStyleBox()?.MinimumSize.Y ?? 0;
|
||||
|
||||
_scrollBar.Page = UIScale * (Height - styleBoxSize);
|
||||
_updateScrollButtonVisibility();
|
||||
_invalidateEntries();
|
||||
}
|
||||
|
||||
@@ -284,5 +318,10 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_invalidOnVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void _updateScrollButtonVisibility()
|
||||
{
|
||||
_scrollDownButton.Visible = ShowScrollDownButton && !_isAtBottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -38,7 +38,6 @@ namespace Robust.Client.UserInterface.Controls;
|
||||
public sealed class TextEdit : Control
|
||||
{
|
||||
[Dependency] private readonly IClipboardManager _clipboard = null!;
|
||||
[Dependency] private readonly IClyde _clyde = null!;
|
||||
|
||||
// @formatter:off
|
||||
public const string StylePropertyCursorColor = "cursor-color";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<OutputPanel Name="Output" VerticalExpand="True" StyleClasses="monospace">
|
||||
<OutputPanel Name="Output" VerticalExpand="True" StyleClasses="monospace" ShowScrollDownButton="True">
|
||||
<OutputPanel.StyleBoxOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#25252add"
|
||||
ContentMarginLeftOverride="3" ContentMarginRightOverride="3"
|
||||
|
||||
@@ -59,6 +59,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
}
|
||||
|
||||
button.EntityLabel.Text = entityLabelText;
|
||||
button.ActualButton.ToolTip = prototype.Description;
|
||||
|
||||
if (prototype == SelectedPrototype)
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
[Prototype("font")]
|
||||
[Prototype]
|
||||
public sealed partial class FontPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
|
||||
141
Robust.Client/Utility/ReloadManager.cs
Normal file
141
Robust.Client/Utility/ReloadManager.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
namespace Robust.Client.Utility;
|
||||
|
||||
internal sealed class ReloadManager : IReloadManager
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
[Dependency] private readonly IResourceManager _res = default!;
|
||||
[Dependency] private readonly ITaskManager _tasks = default!;
|
||||
|
||||
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
|
||||
private CancellationTokenSource _reloadToken = new();
|
||||
private readonly HashSet<ResPath> _reloadQueue = new();
|
||||
private List<FileSystemWatcher> _watchers = new();
|
||||
|
||||
public event Action<ResPath>? OnChanged;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = _logMan.GetSawmill("reload");
|
||||
_clyde.OnWindowFocused += WindowFocusedChanged;
|
||||
}
|
||||
|
||||
private void WindowFocusedChanged(WindowFocusedEventArgs args)
|
||||
{
|
||||
#if TOOLS
|
||||
if (args.Focused && _reloadQueue.Count > 0)
|
||||
{
|
||||
Timer.Spawn(_reloadDelay, ReloadFiles, _reloadToken.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
_reloadToken.Cancel();
|
||||
_reloadToken = new CancellationTokenSource();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void ReloadFiles()
|
||||
{
|
||||
foreach (var file in _reloadQueue)
|
||||
{
|
||||
var rootedFile = file.ToRootedPath();
|
||||
|
||||
if (!_res.ContentFileExists(rootedFile))
|
||||
continue;
|
||||
|
||||
_sawmill.Info($"Reloading {rootedFile}");
|
||||
OnChanged?.Invoke(rootedFile);
|
||||
}
|
||||
|
||||
_reloadQueue.Clear();
|
||||
}
|
||||
|
||||
public void Register(string directory, string filter)
|
||||
{
|
||||
if (!_cfg.GetCVar(CVars.ResPrototypeReloadWatch))
|
||||
return;
|
||||
|
||||
#if TOOLS
|
||||
foreach (var root in _res.GetContentRoots())
|
||||
{
|
||||
var path = root + directory;
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var watcher = new FileSystemWatcher(path, filter)
|
||||
{
|
||||
IncludeSubdirectories = true,
|
||||
NotifyFilter = NotifyFilters.LastWrite
|
||||
};
|
||||
|
||||
_watchers.Add(watcher);
|
||||
|
||||
watcher.Changed += OnWatch;
|
||||
|
||||
try
|
||||
{
|
||||
watcher.EnableRaisingEvents = true;
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.Error($"Watching resources in path {path} threw an exception:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
void OnWatch(object sender, FileSystemEventArgs args)
|
||||
{
|
||||
switch (args.ChangeType)
|
||||
{
|
||||
case WatcherChangeTypes.Renamed:
|
||||
case WatcherChangeTypes.Deleted:
|
||||
return;
|
||||
case WatcherChangeTypes.Created:
|
||||
// case WatcherChangeTypes.Deleted:
|
||||
case WatcherChangeTypes.Changed:
|
||||
case WatcherChangeTypes.All:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
_tasks.RunOnMainThread(() =>
|
||||
{
|
||||
var fullPath = args.FullPath.Replace(Path.DirectorySeparatorChar, '/');
|
||||
var file = new ResPath(fullPath);
|
||||
|
||||
foreach (var rootIter in _res.GetContentRoots())
|
||||
{
|
||||
if (!file.TryRelativeTo(rootIter, out var relative))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_reloadQueue.Add(relative.Value);
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public static class Diagnostics
|
||||
public const string IdUseNonGenericVariant = "RA0030";
|
||||
public const string IdPreferOtherType = "RA0031";
|
||||
public const string IdDuplicateDependency = "RA0032";
|
||||
public const string IdForbidLiteral = "RA0033";
|
||||
|
||||
public static SuppressionDescriptor MeansImplicitAssignment =>
|
||||
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PolySharpIncludeGeneratedTypes>System.Index;System.Diagnostics.CodeAnalysis.NotNullWhenAttribute;System.Runtime.CompilerServices.IsExternalInit;System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute</PolySharpIncludeGeneratedTypes>
|
||||
<NoWarn>RS2008</NoWarn>
|
||||
<NoWarn>RS2008;RS1038</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -35,13 +35,20 @@ public class Generator : IIncrementalGenerator
|
||||
.Where(static type => type != null);
|
||||
|
||||
initContext.RegisterSourceOutput(
|
||||
dataDefinitions,
|
||||
static (sourceContext, source) =>
|
||||
dataDefinitions.Collect(),
|
||||
static (sourceContext, sources) =>
|
||||
{
|
||||
// TODO: deduplicate based on name?
|
||||
var (name, code) = source!.Value;
|
||||
var done = new HashSet<string>();
|
||||
|
||||
sourceContext.AddSource(name, code);
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var (name, code) = source!.Value;
|
||||
|
||||
if (!done.Add(name))
|
||||
continue;
|
||||
|
||||
sourceContext.AddSource(name, code);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
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.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Server.Console.Commands
|
||||
{
|
||||
@@ -42,8 +44,15 @@ namespace Robust.Server.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
_ent.System<MapLoaderSystem>().Save(uid, args[1]);
|
||||
shell.WriteLine("Save successful. Look in the user data directory.");
|
||||
bool saveSuccess = _ent.System<MapLoaderSystem>().TrySaveGrid(uid, new ResPath(args[1]));
|
||||
if(saveSuccess)
|
||||
{
|
||||
shell.WriteLine("Save successful. Look in the user data directory.");
|
||||
}
|
||||
else
|
||||
{
|
||||
shell.WriteError("Save unsuccessful!");
|
||||
}
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
@@ -51,7 +60,7 @@ namespace Robust.Server.Console.Commands
|
||||
switch (args.Length)
|
||||
{
|
||||
case 1:
|
||||
return CompletionResult.FromHint(Loc.GetString("cmd-hint-savebp-id"));
|
||||
return CompletionResult.FromHintOptions(CompletionHelper.Components<MapGridComponent>(args[0], _ent), Loc.GetString("cmd-hint-savebp-id"));
|
||||
case 2:
|
||||
var opts = CompletionHelper.UserFilePath(args[1], _resource.UserData);
|
||||
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-savemap-path"));
|
||||
@@ -63,7 +72,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 +99,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 +121,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 +133,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 +145,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)
|
||||
@@ -148,8 +160,8 @@ namespace Robust.Server.Console.Commands
|
||||
|
||||
public sealed class SaveMap : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _system = default!;
|
||||
[Dependency] private readonly IMapManager _map = default!;
|
||||
[Dependency] private readonly IResourceManager _resource = default!;
|
||||
|
||||
public override string Command => "savemap";
|
||||
@@ -159,7 +171,7 @@ namespace Robust.Server.Console.Commands
|
||||
switch (args.Length)
|
||||
{
|
||||
case 1:
|
||||
return CompletionResult.FromHint(Loc.GetString("cmd-hint-savemap-id"));
|
||||
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(_entManager), Loc.GetString("cmd-hint-savemap-id"));
|
||||
case 2:
|
||||
var opts = CompletionHelper.UserFilePath(args[1], _resource.UserData);
|
||||
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-savemap-path"));
|
||||
@@ -189,13 +201,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,15 +216,21 @@ namespace Robust.Server.Console.Commands
|
||||
}
|
||||
|
||||
shell.WriteLine(Loc.GetString("cmd-savemap-attempt", ("mapId", mapId), ("path", args[1])));
|
||||
_system.GetEntitySystem<MapLoaderSystem>().SaveMap(mapId, args[1]);
|
||||
shell.WriteLine(Loc.GetString("cmd-savemap-success"));
|
||||
bool saveSuccess = _system.GetEntitySystem<MapLoaderSystem>().TrySaveMap(mapId, new ResPath(args[1]));
|
||||
if(saveSuccess)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-savemap-success"));
|
||||
}
|
||||
else
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-savemap-error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +286,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])));
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls PVS visibility of entities. THIS COMPONENT CONTROLS WHETHER ENTITIES ARE NETWORKED TO PLAYERS
|
||||
/// AND SHOULD NOT BE USED AS THE SOLE WAY TO HIDE AN ENTITY FROM A PLAYER.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(VisibilitySystem))]
|
||||
public sealed partial class VisibilityComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The visibility layer for the entity.
|
||||
/// Players whose visibility masks don't match this won't get state updates for it.
|
||||
/// </summary>
|
||||
[DataField("layer")]
|
||||
public ushort Layer = 1;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -230,6 +204,23 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
private void HandleEntityNetworkMessage(MsgEntity message)
|
||||
{
|
||||
if (_logLateMsgs)
|
||||
{
|
||||
var msgT = message.SourceTick;
|
||||
var cT = _gameTiming.CurTick;
|
||||
|
||||
if (msgT < cT)
|
||||
{
|
||||
_netEntSawmill.Warning(
|
||||
"Got late MsgEntity! Diff: {0}, msgT: {2}, cT: {3}, player: {1}, msg: {4}",
|
||||
(int) msgT.Value - (int) cT.Value,
|
||||
message.MsgChannel.UserName,
|
||||
msgT,
|
||||
cT,
|
||||
message.SystemMessage);
|
||||
}
|
||||
}
|
||||
|
||||
_queue.Add(message);
|
||||
}
|
||||
|
||||
|
||||
@@ -184,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
|
||||
|
||||
@@ -29,7 +29,8 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
base.Initialize();
|
||||
EntityManager.EntityDeleted += OnDeleted;
|
||||
_player.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
SubscribeLocalEvent<MapChangedEvent>(OnMapChanged);
|
||||
SubscribeLocalEvent<MapRemovedEvent>(OnMapRemoved);
|
||||
SubscribeLocalEvent<MapCreatedEvent>(OnMapCreated);
|
||||
SubscribeLocalEvent<GridInitializeEvent>(OnGridCreated);
|
||||
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
|
||||
|
||||
@@ -133,7 +134,7 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
|
||||
/// <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 override void AddGlobalOverride(EntityUid uid)
|
||||
{
|
||||
@@ -159,8 +160,9 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
/// 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)
|
||||
{
|
||||
@@ -176,11 +178,12 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
@@ -206,7 +209,7 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
|
||||
/// <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 override void AddSessionOverride(EntityUid uid, ICommonSession session)
|
||||
{
|
||||
@@ -235,15 +238,17 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
|
||||
/// <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 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,14 +275,6 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
|
||||
#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);
|
||||
@@ -290,12 +287,12 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
|
||||
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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -465,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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
@@ -29,12 +29,12 @@ namespace Robust.Server
|
||||
/// <summary>
|
||||
/// Directory to load all assemblies from.
|
||||
/// </summary>
|
||||
public ResPath AssemblyDirectory { get; init; } = new(@"/Assemblies");
|
||||
public ResPath AssemblyDirectory { get; init; } = new(@"/Assemblies/");
|
||||
|
||||
/// <summary>
|
||||
/// Directory to load all prototypes from.
|
||||
/// </summary>
|
||||
public ResPath PrototypeDirectory { get; init; } = new(@"/Prototypes");
|
||||
public ResPath PrototypeDirectory { get; init; } = new(@"/Prototypes/");
|
||||
|
||||
/// <summary>
|
||||
/// Whether to disable mounting the "Resources/" folder on FULL_RELEASE.
|
||||
|
||||
15
Robust.Server/ServerStatus/StatusExt.cs
Normal file
15
Robust.Server/ServerStatus/StatusExt.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Robust.Server.ServerStatus;
|
||||
|
||||
/// <summary>
|
||||
/// Helper functions for working with <see cref="IStatusHandlerContext"/>.
|
||||
/// </summary>
|
||||
public static class StatusExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Add <c>Access-Control-Allow-Origin: *</c> to the response headers for this request.
|
||||
/// </summary>
|
||||
public static void AddAllowOriginAny(this IStatusHandlerContext context)
|
||||
{
|
||||
context.ResponseHeaders.Add("Access-Control-Allow-Origin", "*");
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ namespace Robust.Server.ServerStatus
|
||||
|
||||
OnStatusRequest?.Invoke(jObject);
|
||||
|
||||
context.AddAllowOriginAny();
|
||||
await context.RespondJsonAsync(jObject);
|
||||
|
||||
return true;
|
||||
@@ -121,6 +122,7 @@ namespace Robust.Server.ServerStatus
|
||||
|
||||
OnInfoRequest?.Invoke(jObject);
|
||||
|
||||
context.AddAllowOriginAny();
|
||||
await context.RespondJsonAsync(jObject);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -93,6 +93,7 @@ namespace Robust.Shared.Maths
|
||||
private const double CardinalSegment = 2 * Math.PI / 4.0; // Cut the circle into 4 pieces
|
||||
private const double CardinalOffset = CardinalSegment / 2.0; // offset the pieces by 1/2 their size
|
||||
|
||||
[Pure]
|
||||
public readonly Direction GetCardinalDir()
|
||||
{
|
||||
var ang = Theta % (2 * Math.PI);
|
||||
@@ -167,6 +168,7 @@ namespace Robust.Shared.Maths
|
||||
/// <summary>
|
||||
/// Removes revolutions from a positive or negative angle to make it as small as possible.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public readonly Angle Reduced()
|
||||
{
|
||||
return new(Reduce(Theta));
|
||||
@@ -213,11 +215,13 @@ namespace Robust.Shared.Maths
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public readonly Angle Opposite()
|
||||
{
|
||||
return new Angle(FlipPositive(Theta-Math.PI));
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public readonly Angle FlipPositive()
|
||||
{
|
||||
return new(FlipPositive(Theta));
|
||||
|
||||
@@ -212,6 +212,48 @@ namespace Robust.Shared.Maths
|
||||
return surfaceIntersect / (Area(this) + Area(other) - surfaceIntersect);
|
||||
}
|
||||
|
||||
public readonly bool IsValid()
|
||||
{
|
||||
var d = Vector2.Subtract(TopRight, BottomLeft);
|
||||
bool valid = d.X >= 0.0f && d.Y >= 0.0f;
|
||||
valid = valid && BottomLeft.IsValid() && TopRight.IsValid();
|
||||
return valid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enlarges this box to contain another box.
|
||||
/// </summary>
|
||||
public bool EnlargeAabb(Box2 other)
|
||||
{
|
||||
var changed = false;
|
||||
|
||||
if (other.Left < Left)
|
||||
{
|
||||
Left = other.Left;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (other.Bottom < Bottom)
|
||||
{
|
||||
Bottom = other.Bottom;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (Right < other.Right)
|
||||
{
|
||||
Right = other.Right;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (other.Top < Top)
|
||||
{
|
||||
Top = other.Top;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the smallest rectangle that contains both of the rectangles.
|
||||
/// </summary>
|
||||
@@ -401,6 +443,15 @@ namespace Robust.Shared.Maths
|
||||
public static float Perimeter(in Box2 box)
|
||||
=> (box.Width + box.Height) * 2;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public static Box2 Union(Box2 a, Box2 b)
|
||||
{
|
||||
return new Box2(
|
||||
Vector2.Min(a.BottomLeft, b.BottomLeft),
|
||||
Vector2.Max(a.TopRight, b.TopRight));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public static Box2 Union(in Vector2 a, in Vector2 b)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user