Compare commits

..

79 Commits

Author SHA1 Message Date
metalgearsloth
b6cadfedd5 Update arch (#4605) 2023-11-25 14:56:56 +11:00
metalgearsloth
9f57b705d7 Version: 182.0.0 2023-11-24 00:21:00 +11:00
metalgearsloth
68be9712ad Add entity gen to hashcode (#4601) 2023-11-24 00:19:58 +11:00
metalgearsloth
aaa446254c Version: 181.0.2 2023-11-23 23:43:47 +11:00
metalgearsloth
5e2d2ab317 Fix too many pointlights causing blackscreen (#4599) 2023-11-23 23:39:51 +11:00
metalgearsloth
20ae63fbbd Replace tile intersecting with enumerator (#4595) 2023-11-23 22:36:40 +11:00
metalgearsloth
a92c0cbef4 Fix nullable comps being raised for client gamestates (#4596) 2023-11-23 22:07:27 +11:00
metalgearsloth
95649a2dd0 Version: 181.0.1 2023-11-23 16:53:03 +11:00
metalgearsloth
861807f8b4 Fix HasComp(uid, Type) (#4594) 2023-11-23 16:52:31 +11:00
metalgearsloth
bd73f1c05a Version: 181.0.0 2023-11-23 15:28:35 +11:00
metalgearsloth
7dce51e2cf Arch PR two electric boogaloo (#4388)
Co-authored-by: DrSmugleaf <drsmugleaf@gmail.com>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2023-11-23 14:29:37 +11:00
DrSmugleaf
d9b0f3a227 Version: 180.2.1 2023-11-22 17:02:13 -08:00
Vasilis
05766a2eaa Fix not using dotnet 7 for actions in engine (#4591) 2023-11-23 00:43:15 +01:00
metalgearsloth
a761fbc09e Version: 180.2.0 2023-11-22 22:00:05 +11:00
metalgearsloth
f69440b3f2 Minor PVS stuff (#4573)
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2023-11-22 21:54:07 +11:00
Leon Friedrich
b459d2ce21 Add more map system helper methods. (#4589) 2023-11-22 21:51:46 +11:00
Leon Friedrich
202182e3d4 Add new EnsureEntity variants (#4586) 2023-11-20 17:44:36 +11:00
metalgearsloth
96cb52e5d2 Version: 180.1.0 2023-11-20 16:31:41 +11:00
metalgearsloth
82e0c0baeb Add cvar for lidgren pool size (#4585) 2023-11-20 16:28:13 +11:00
Leon Friedrich
54d6552164 Fix shape lookups for non-hard fixtures (#4583) 2023-11-20 16:15:27 +11:00
Jordan Dominion
c21b6c993c Fix potential error when writing runtime log (#4575) 2023-11-19 15:47:53 +01:00
metalgearsloth
2fe4a8b859 Add map name to lsmap (#4576) 2023-11-19 15:47:19 +01:00
metalgearsloth
8325966dbb Fix contact constraints allocs (#4581) 2023-11-19 15:47:07 +01:00
metalgearsloth
2459a9d688 Version: 180.0.0 2023-11-19 15:09:59 +11:00
Leon Friedrich
2cd2d1edd6 Add misc helpful methods (#4577) 2023-11-19 15:05:26 +11:00
metalgearsloth
b982350851 Use NetEntities for F3 panel (#4571) 2023-11-16 20:53:23 +11:00
DrSmugleaf
4a50bc2154 Add AddEntitiesIntersecting for phys shapes, change float range overload to use circles, remove obsolete methods (#4572) 2023-11-16 20:44:21 +11:00
metalgearsloth
4c85e205b9 Add chain support to TryGetNearest (#4567) 2023-11-16 20:40:23 +11:00
ElectroJr
0b447d9d82 Version: 179.0.0 2023-11-12 13:35:17 -05:00
Leon Friedrich
ceb205ad52 Allow per-eye lighting toggling. (#4569) 2023-11-13 05:30:00 +11:00
Leon Friedrich
a48ff3dbf1 Fix PlacementManager bug (#4568) 2023-11-13 05:29:18 +11:00
DrSmugleaf
2b85fa88c1 Print stack trace when adding a component while iterating net comps in ResetPredictedEntities (#4541) 2023-11-13 05:26:13 +11:00
Leon Friedrich
19564a421b Fix deserialization of empty grid chunks (#4565) 2023-11-13 05:22:20 +11:00
Leon Friedrich
b3f0e467ee Improve UnknownPrototypeException error message (#4566) 2023-11-13 05:22:05 +11:00
Leon Friedrich
216292c849 Make EyeComponent.Eye not nullable (#4564) 2023-11-13 04:20:09 +11:00
Jerry
68753d15e0 Fix stack overflow error on planet station (#4563) 2023-11-12 13:28:35 +11:00
ElectroJr
2a357051ae Version: 178.0.0 2023-11-10 20:58:35 -05:00
Leon Friedrich
58e0b62145 Merge ActorSystem and IPlayerManager (#4530) 2023-11-11 12:50:21 +11:00
Leon Friedrich
14cc273997 Add NetListAsArray<T>.Value to sandbox whitelist (#4537) 2023-11-11 11:57:58 +11:00
DrSmugleaf
93f4428635 Version: 177.0.0 2023-11-08 00:21:12 -08:00
DrSmugleaf
164bf68aca Move TryGetUi/TryToggleUi/ToggleUi/TryOpen/OpenUi/TryClose/CloseUi to SharedUserInterfaceSystem (#4562) 2023-11-08 16:52:38 +11:00
Leon Friedrich
773b87672b Fix terminating entity reparenting bug (#4549) 2023-11-08 15:39:08 +11:00
metalgearsloth
eecf834039 Fix PlacementManager warnings (#4557) 2023-11-08 15:34:54 +11:00
Leon Friedrich
325fe46aa3 Add More Entity<T> query methods (#4550) 2023-11-07 20:24:42 -08:00
metalgearsloth
2f6c29ab43 Add GetMapCoordinates to TransformSystem (#4556) 2023-11-07 20:23:05 -08:00
metalgearsloth
aab1a2dba9 Fix transform test warnings (#4558) 2023-11-07 20:22:07 -08:00
DrSmugleaf
f36fbd9c83 Fix inverted GetAllMapGrids mapid check (#4561) 2023-11-07 14:26:01 -08:00
metalgearsloth
126c863f45 Hotfix containersystem.remove (#4560) 2023-11-07 16:18:08 +11:00
Leon Friedrich
618a8491bf Add BeforeApplyState event to replay playback (#4536) 2023-11-07 15:07:26 +11:00
Leon Friedrich
2743b64a2b Mark container methods as obsolete (#4551) 2023-11-07 15:05:32 +11:00
Leon Friedrich
28cc91934c Change PVS error log into warning (#4548) 2023-11-07 15:02:13 +11:00
metalgearsloth
eadfcd4c09 Specify RichTextLabel VAlignment as Center (#4520) 2023-11-07 10:27:49 +11:00
metalgearsloth
7871b0010e Version: 176.0.0 2023-11-07 09:51:32 +11:00
metalgearsloth
3da04ed17e Robust.Packaging updates (#4547) 2023-11-07 09:36:33 +11:00
metalgearsloth
170d192791 Revert audio rework (#4554) 2023-11-07 09:34:09 +11:00
Leon Friedrich
dcd9939554 Fix PVS initial list capacity bug (#4546) 2023-11-06 04:41:56 +11:00
Leon Friedrich
98ef58eca6 Add max game state buffer size cvar (#4543) 2023-11-05 02:58:48 +11:00
metalgearsloth
ab1e99a0df Add GetEntitiesInRange that takes in a set (#4544) 2023-11-04 15:02:20 +11:00
Leon Friedrich
499c236798 Fix replay lerp error spam (#4534) 2023-10-30 04:29:47 +11:00
metalgearsloth
8dc2345ceb Fix audio position on first tick (#4533) 2023-10-29 15:30:59 +11:00
metalgearsloth
9b04270178 Version: 175.0.0 2023-10-29 15:03:09 +11:00
metalgearsloth
d75dbc901f Audio rework (#4421) 2023-10-29 14:58:19 +11:00
Leon Friedrich
19a3e82848 Cache prototype data for IEntityManager.IsDefault() (#4531) 2023-10-29 12:54:52 +11:00
Leon Friedrich
911abf2693 Remove empty planet-map chunks (#4529) 2023-10-29 12:52:03 +11:00
ElectroJr
f5874ea402 Version: 174.0.0 2023-10-28 13:26:49 -04:00
metalgearsloth
b486ef885c Add NextAngle for System.Random (#4522) 2023-10-29 04:22:32 +11:00
metalgearsloth
9d55d77e48 Sprite GetFrame (#4528) 2023-10-29 04:21:52 +11:00
Leon Friedrich
5af3cb969c Move ActorComponent to shared (#4527) 2023-10-29 04:21:09 +11:00
metalgearsloth
429bc806dc Version: 173.1.0 2023-10-28 15:36:26 +11:00
metalgearsloth
81484699a8 Add chain shapes (#4523)
* Add chain shapes

* rar only

* that too

* weh

* a

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

Co-authored-by: Moony <moony@hellomouse.net>

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

Co-authored-by: Moony <moony@hellomouse.net>

---------

Co-authored-by: Moony <moony@hellomouse.net>
2023-10-28 15:29:30 +11:00
metalgearsloth
7cad8d5ba3 Version: 173.0.0 2023-10-28 14:02:06 +11:00
Leon Friedrich
3aa04a3c86 Fix grid chunk bugs (#4525)
* Fix grid rendering

* Use TileChangedEvent

* Other empty chunk fixes

* Remove assert

Good ol integration tests at it again, adding invalid components
2023-10-28 13:57:54 +11:00
metalgearsloth
9750b113c8 Version: 172.0.0 2023-10-24 20:22:31 +11:00
Leon Friedrich
5a6c4220fc IPlayerManager refactor (#4518) 2023-10-24 20:18:58 +11:00
Leon Friedrich
b2d389f184 Remove TryLifestage() helpers (#4519) 2023-10-24 18:46:46 +11:00
Leon Friedrich
ad0cb05dd6 Add EnsureComponent(ref Entity<T?>) (#4516) 2023-10-24 17:19:38 +11:00
Leon Friedrich
ad134d9e4e Fix game state logging spam (#4517) 2023-10-24 14:09:55 +11:00
Leon Friedrich
be33bc2219 Re-add force ack threshold (#4423) and fix bugs. (#4438)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2023-10-22 23:27:15 +11:00
metalgearsloth
aa2fd2107d Add mgs to physics codeowners (#4510)
Please ping me for this is creates more work if you do not ping me.
2023-10-22 05:03:47 -07:00
260 changed files with 6074 additions and 4261 deletions

3
.github/CODEOWNERS vendored
View File

@@ -7,3 +7,6 @@
**/Toolshed/** @moonheart08
*Command.cs @moonheart08
*Commands.cs @moonheart08
# Physics
**/Robust.Shared/Physics/** @metalgearsloth

3
.gitmodules vendored
View File

@@ -13,3 +13,6 @@
[submodule "cefglue"]
path = cefglue
url = https://github.com/space-wizards/cefglue.git
[submodule "Arch/Arch"]
path = Arch/Arch
url = https://github.com/space-wizards/Arch.git

1
Arch/Arch Submodule

Submodule Arch/Arch added at c76d18feb7

94
Arch/Arch.csproj Normal file
View File

@@ -0,0 +1,94 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<LangVersion>latest</LangVersion>
<TargetFramework>net7.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<Nullable>enable</Nullable>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
<PackageId>Arch</PackageId>
<Title>Arch</Title>
<Version>1.2.7.1-alpha</Version>
<Authors>genaray</Authors>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<Description>A high performance c# net.6 and net.7 archetype based ECS ( Entity component system ).</Description>
<PackageReleaseNotes>Updated LowLevel which fixes bugs. </PackageReleaseNotes>
<PackageTags>c#;.net;.net6;.net7;ecs;game;entity;gamedev; game-development; game-engine; entity-component-system;stride;unity;godot;</PackageTags>
<PackageProjectUrl>https://github.com/genaray/Arch</PackageProjectUrl>
<RepositoryUrl>https://github.com/genaray/Arch.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<IsPackable>true</IsPackable>
<LangVersion>11</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Copyright>Apache2.0</Copyright>
<NoWarn>1701;1702;1591</NoWarn>
<Configurations>Debug;Debug-PureECS;Debug-Events;Release;Release-PureECS;Release-Events;</Configurations>
<AssemblyName>Arch</AssemblyName>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DefaultItemExcludes>src/Arch/**/*</DefaultItemExcludes>
<DefineConstants>$(DefineConstants);PURE_ECS;CONTRACTS_FULL</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DefineConstants>$(DefineConstants);PURE_ECS;CONTRACTS_FULL;TRACE;</DefineConstants>
<Optimize>false</Optimize>
<PlatformTarget>AnyCPU</PlatformTarget>
</PropertyGroup>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Arch.Benchmarks" />
<InternalsVisibleTo Include="Arch.Tests" />
</ItemGroup>
<ItemGroup>
<Using Include="System" />
<Using Include="System.Collections" />
<Using Include="System.Collections.Generic" />
<Using Include="System.Diagnostics" />
<Using Include="System.Diagnostics.CodeAnalysis" />
<Using Include="System.IO" />
<Using Include="System.Linq" />
<Using Include="System.Runtime.CompilerServices" />
<Using Include="System.Runtime.InteropServices" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include=".\Arch\src\Arch.SourceGen\Arch.SourceGen.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Arch.LowLevel" Version="1.1.0" />
<PackageReference Include="Collections.Pooled" Version="2.0.0-preview.27" />
<PackageReference Include="CommunityToolkit.HighPerformance" Version="7.1.2" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
<PackageReference Include="ZeroAllocJobScheduler" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<Compile Include="Arch\src\Arch\**\*.cs">
<Link>Arch\%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>
<Compile Remove="Arch\src\Arch\obj\**\*.cs" />
<InternalsVisibleTo Include="Arch.Benchmarks" />
<InternalsVisibleTo Include="Arch.Tests" />
</ItemGroup>
<Import Project="../MSBuild/Robust.Properties.targets" />
</Project>

View File

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

View File

@@ -54,6 +54,240 @@ END TEMPLATE-->
*None yet*
## 182.0.0
### Breaking changes
* Add EntityUid's generation / version to the hashcode.
## 181.0.2
### Bugfixes
* Fix exceptions from having too many lights on screen and causing the game to go black.
* Fix components having events raised in ClientGameStateManager before fully set and causing nullable reference exceptions.
* Replace tile intersection IEnumerables with TileEnumerator internally. Also made it public for external callers that wish to avoid IEnumerable.
## 181.0.1
### Bugfixes
* Fix the non-generic HasComp and add a test for good measure.
## 181.0.0
### Breaking changes
- Arch is merged refactoring how components are stored on engine. There's minimal changes on the API end to facilitate component nullability with much internal refactoring.
## 180.2.1
## 180.2.0
### New features
* Add EnsureEntity variants that take in collections.
* Add more MapSystem helper methods.
### Internal
* Cache some more PVS data to avoid re-allocating every tick.
## 180.1.0
### New features
* Add the map name to lsmap.
* Add net.pool_size to CVars to control the message data pool size in Lidgren and to also toggle pooling.
### Bugfixes
* Fix physics contraints causing enormous heap allocations.
* Fix potential error when writing a runtime log.
* Fix shape lookups for non-hard fixtures in EntityLookupSystem from 180.0.0
## 180.0.0
### Breaking changes
* Removed some obsolete methods from EntityLookupSystem.
### New features
* PhysicsSystem.TryGetNearest now supports chain shapes.
* Add IPhysShape methods to EntityLookupSystem rather than relying on AABB checks.
* Add some more helper methods to SharedTransformSystem.
* Add GetOrNew dictionary extension that also returns a bool on whether the key existed.
* Add a GetAnchoredEntities overload that takes in a list.
### Other
* Use NetEntities for the F3 debug panel to align with command usage.
## 179.0.0
### Breaking changes
* EyeComponent.Eye is no longer nullable
### New features
* Light rendering can now be enabled or disable per eye.
### Bugfixes
* Deserializing old maps with empty grid chunks should now just ignore those chunks.
### Other
* UnknownPrototypeException now also tells you the prototype kind instead of just the unkown ID.
* Adding or removing networked components while resetting predicted entities now results in a more informative exception.
## 178.0.0
### Breaking changes
* Most methods in ActorSystem have been moved to ISharedPlayerManager.
* Several actor/player related components and events have been moved to shared.
### New features
* Added `NetListAsArray<T>.Value` to the sandbox whitelist
## 177.0.0
### Breaking changes
* Removed toInsertXform and added containerXform in SharedContainerSystem.CanInsert.
* Removed EntityQuery parameters from SharedContainerSystem.IsEntityOrParentInContainer.
* Changed the signature of ContainsEntity in SharedTransformSystem to use Entity<T>.
* Removed one obsoleted SharedTransformSystem.AnchorEntity method.
* Changed signature of SharedTransformSystem.SetCoordinates to use Entity<T>.
### New features
* Added more Entity<T> query methods.
* Added BeforeApplyState event to replay playback.
### Bugfixes
* Fixed inverted GetAllMapGrids map id check.
* Fixed transform test warnings.
* Fixed PlacementManager warnings.
* Fixed reparenting bug for entities that are being deleted.
### Other
* Changed VerticalAlignment of RichTextLabel to Center to be consistent with Label.
* Changed PVS error log to be a warning instead.
* Marked insert and remove container methods as obsolete, added container system methods to replace them.
* Marked TransformComponent.MapPosition as obsolete, added GetMapCoordinates system method to replace it.
### Internal
* Moved TryGetUi/TryToggleUi/ToggleUi/TryOpen/OpenUi/TryClose/CloseUi methods from UserInterfaceSystem to SharedUserInterfaceSystem.
## 176.0.0
### Breaking changes
* Reverted audio rework temporarily until packaging is fixed.
* Changes to Robust.Packaging to facilitate Content.Packaging ports from the python packaging scripts.
### New features
* Add a cvar for max game state buffer size.
* Add an overload for GetEntitiesInRange that takes in a set.
### Bugfixes
* Fix PVS initial list capacity always being 0.
* Fix replay lerp error spam.
## 175.0.0
### Breaking changes
* Removed static SoundSystem.Play methods.
* Moved IPlayingAudioStream onto AudioComponent and entities instead of an abstract stream.
* IResourceCache is in shared and IClientResourceCache is the client version to use for textures.
* Default audio attenuation changed from InverseDistanceClamped to LinearDistanceClamped.
* Removed per-source audio attenuation.
### New features
* Add preliminary support for EFX Reverb presets + auxiliary slots; these are also entities.
* Audio on grid entities is now attached to the grid.
### Bugfixes
* If an audio entity comes into PVS range its track will start at the relevant offset and not the beginning.
* Z-Axis offset is considered for ReferenceDistance / MaxDistance for audio.
* Audio will now pause if the attached entity is paused.
### Other
* Changed audio Z-Axis offset from -5m to -1m.
## 174.0.0
### Breaking changes
* ActorComponent has been moved to `Robust.Shared.Player` (namespace changed).
### New features
* Added `SpriteSystem.GetFrame()` method, which takes in an animated RSI and a time and returns a frame/texture.
* Added `IRobustRandom.NextAngle()`
## 173.1.0
### New features
* Add physics chain shapes from Box2D.
## 173.0.0
### Breaking changes
* Remove GridModifiedEvent in favor of TileChangedEvent.
### Bugfixes
* Fix some grid rendering bugs where chunks don't get destroyed correctly.
## 172.0.0
### Breaking changes
* Remove TryLifestage helper methods.
* Refactor IPlayerManager to remove more IPlayerSession, changed PlayerAttachedEvent etc on client to have the Local prefix, and shuffled namespaces around.
### New features
* Add EnsureComponent(ref Entity<\T?>)
### Bugfixes
* Re-add force ask threshold and fix other PVS bugs.
## 171.0.0
### Breaking changes

View File

@@ -2203,3 +2203,207 @@
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- name: Arch
license: |
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 Lars Matthäus/genaray
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,177 @@
using System;
using System.Runtime.CompilerServices;
using Arch.Core;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using Robust.Shared.Analyzers;
using static Robust.Benchmarks.EntityManager.ArchetypeComponentAccessBenchmark;
namespace Robust.Benchmarks.Arch;
[MemoryDiagnoser]
[Virtual]
public class ArchComponentAccessBenchmark
{
private const int N = 10000;
private static readonly Consumer Consumer = new();
private Entity _entity;
private World _world = default!;
private QueryDescription _singleQuery;
private QueryDescription _tenQuery;
[GlobalSetup]
public void GlobalSetup()
{
var _ = new JobScheduler.JobScheduler("ArchBenchmark");
_world = World.Create();
for (var i = 0; i < N; i++)
{
var entity = _world.Create();
// Randomly chosen id
if (entity.Id == 1584)
_entity = entity;
_world.Add(
entity,
new Struct1(),
new Struct2(),
new Struct3(),
new Struct4(),
new Struct5(),
new Struct6(),
new Struct7(),
new Struct8(),
new Struct9(),
new Struct10()
);
}
_singleQuery = new QueryDescription().WithAll<Struct1>();
_tenQuery = new QueryDescription().WithAll<Struct1, Struct2, Struct3, Struct4, Struct5, Struct6, Struct7, Struct8, Struct9, Struct10>();
}
[GlobalCleanup]
public void GlobalCleanup()
{
JobScheduler.JobScheduler.Instance.Dispose();
Environment.Exit(0);
}
[Benchmark]
public Struct1 GetSingle()
{
return _world.Get<Struct1>(_entity);
}
[Benchmark]
public (Struct1, Struct2, Struct3, Struct4, Struct5, Struct6, Struct7, Struct8, Struct9, Struct10)
GetTen()
{
return (
_world.Get<Struct1>(_entity),
_world.Get<Struct2>(_entity),
_world.Get<Struct3>(_entity),
_world.Get<Struct4>(_entity),
_world.Get<Struct5>(_entity),
_world.Get<Struct6>(_entity),
_world.Get<Struct7>(_entity),
_world.Get<Struct8>(_entity),
_world.Get<Struct9>(_entity),
_world.Get<Struct10>(_entity)
);
}
[Benchmark]
public bool HasSingle()
{
return _world.Has<Struct1>(_entity);
}
[Benchmark]
public bool HasTen()
{
return _world.Has<Struct1>(_entity) &&
_world.Has<Struct2>(_entity) &&
_world.Has<Struct3>(_entity) &&
_world.Has<Struct4>(_entity) &&
_world.Has<Struct5>(_entity) &&
_world.Has<Struct6>(_entity) &&
_world.Has<Struct7>(_entity) &&
_world.Has<Struct8>(_entity) &&
_world.Has<Struct9>(_entity) &&
_world.Has<Struct10>(_entity);
}
[Benchmark]
public void IterateSingle()
{
_world.Query(_singleQuery, static (ref Struct1 s) => Consumer.Consume(s));
}
[Benchmark]
public void IterateSingleInline()
{
_world.InlineQuery<QueryConsumer>(_singleQuery);
}
[Benchmark]
public void IterateSingleParallel()
{
_world.ParallelQuery(_singleQuery, static (ref Struct1 s) => Consumer.Consume(s));
}
[Benchmark]
public void IterateSingleInlineParallel()
{
_world.InlineParallelQuery<QueryConsumer>(_singleQuery);
}
[Benchmark]
public void IterateTen()
{
_world.Query(_tenQuery,
static (
ref Struct1 s1, ref Struct2 s2, ref Struct3 s3, ref Struct4 s4,
ref Struct5 s5, ref Struct6 s6, ref Struct7 s7, ref Struct8 s8,
ref Struct9 s9, ref Struct10 s10) =>
Consumer.Consume((s1, s2, s3, s4, s5, s6, s7, s8, s9, s10)));
}
[Benchmark]
public void IterateTenInline()
{
_world.InlineQuery<QueryConsumer>(_tenQuery);
}
[Benchmark]
public void IterateTenParallel()
{
_world.ParallelQuery(_tenQuery,
static (
ref Struct1 s1, ref Struct2 s2, ref Struct3 s3, ref Struct4 s4,
ref Struct5 s5, ref Struct6 s6, ref Struct7 s7, ref Struct8 s8,
ref Struct9 s9, ref Struct10 s10) =>
Consumer.Consume((s1, s2, s3, s4, s5, s6, s7, s8, s9, s10)));
}
[Benchmark]
public void IterateTenInlineParallel()
{
_world.InlineParallelQuery<QueryConsumer>(_tenQuery);
}
private struct QueryConsumer : IForEach
{
private static readonly Consumer Consumer = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Update(Entity entity)
{
Consumer.Consume(entity);
}
}
}

View File

@@ -41,7 +41,7 @@ public partial class AddRemoveComponentBenchmark
{
for (var i = 2; i <= N+1; i++)
{
var uid = new EntityUid(i);
var uid = new EntityUid(i, -1);
_entityManager.AddComponent<A>(uid);
_entityManager.RemoveComponent<A>(uid);
}

View File

@@ -57,7 +57,7 @@ public class ComponentIndexBenchmark
private static class CompArrayIndex<T>
{
// ReSharper disable once StaticMemberInGenericType
public static readonly CompIdx Idx = new(Interlocked.Increment(ref _compIndexMaster));
public static readonly CompIdx Idx = new(Interlocked.Increment(ref _compIndexMaster), typeof(T));
}
private static CompIdx GetCompIdIndex(Type type)

View File

@@ -46,7 +46,7 @@ public partial class GetComponentBenchmark
{
for (var i = 2; i <= N+1; i++)
{
Comps[i] = _entityManager.GetComponent<A>(new EntityUid(i));
Comps[i] = _entityManager.GetComponent<A>(new EntityUid(i, -1));
}
// Return something so the JIT doesn't optimize out all the GetComponent calls.

View File

@@ -8,6 +8,7 @@
<NoWarn>RA0003</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Arch\Arch.csproj" />
<ProjectReference Include="..\Robust.Server\Robust.Server.csproj" />
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
<ProjectReference Include="..\Robust.UnitTesting\Robust.UnitTesting.csproj" />

View File

@@ -1,8 +1,6 @@
using System;
using System.Linq;
using System.Net;
using Robust.Client.Configuration;
using Robust.Client.Debugging;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Player;
@@ -10,13 +8,12 @@ using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -65,12 +62,12 @@ namespace Robust.Client
_configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged, invokeImmediately: true);
_playMan.Initialize();
_playMan.Initialize(0);
_playMan.PlayerListUpdated += OnPlayerListUpdated;
Reset();
}
private void OnPlayerListUpdated(object? sender, EventArgs e)
private void OnPlayerListUpdated()
{
var serverPlayers = _playMan.PlayerCount;
if (_net.ServerChannel != null && GameInfo != null && _net.IsConnected)
@@ -130,9 +127,10 @@ namespace Robust.Client
{
DebugTools.Assert(RunLevel < ClientRunLevel.Connecting);
DebugTools.Assert(!_net.IsConnected);
_playMan.Startup();
_playMan.LocalPlayer!.Name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName);
var name = PlayerNameOverride ?? _configManager.GetCVar(CVars.PlayerName);
_playMan.SetupSinglePlayer(name);
OnRunLevelChanged(ClientRunLevel.SinglePlayerGame);
_playMan.JoinGame(_playMan.LocalSession!);
GameStartedSetup();
}
@@ -173,22 +171,14 @@ namespace Robust.Client
info.ServerName = serverName;
}
var maxPlayers = _configManager.GetCVar<int>("game.maxplayers");
info.ServerMaxPlayers = maxPlayers;
var userName = _net.ServerChannel!.UserName;
var userId = _net.ServerChannel.UserId;
var channel = _net.ServerChannel!;
// start up player management
_playMan.Startup();
_playMan.LocalPlayer!.UserId = userId;
_playMan.LocalPlayer.Name = userName;
_playMan.LocalPlayer.StatusChanged += OnLocalStatusChanged;
_playMan.SetupMultiplayer(channel);
_playMan.PlayerStatusChanged += OnStatusChanged;
var serverPlayers = _playMan.PlayerCount;
_discord.Update(info.ServerName, userName, info.ServerMaxPlayers.ToString(), serverPlayers.ToString());
_discord.Update(info.ServerName, channel.UserName, info.ServerMaxPlayers.ToString(), serverPlayers.ToString());
}
@@ -221,6 +211,8 @@ namespace Robust.Client
private void Reset()
{
_configManager.ReceivedInitialNwVars -= OnReceivedClientData;
_playMan.PlayerStatusChanged -= OnStatusChanged;
_configManager.ClearReceivedInitialNwVars();
OnRunLevelChanged(ClientRunLevel.Initialize);
}
@@ -263,19 +255,17 @@ namespace Robust.Client
Reset();
}
private void OnLocalStatusChanged(object? obj, StatusEventArgs eventArgs)
private void OnStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.Session != _playMan.LocalSession)
return;
// player finished fully connecting to the server.
// OldStatus is used here because it can go from connecting-> connected or connecting-> ingame
if (eventArgs.OldStatus == SessionStatus.Connecting)
{
OnPlayerJoinedServer(_playMan.LocalPlayer!.Session);
}
if (eventArgs.NewStatus == SessionStatus.InGame)
{
OnPlayerJoinedGame(_playMan.LocalPlayer!.Session);
}
if (e.OldStatus == SessionStatus.Connecting)
OnPlayerJoinedServer(e.Session);
else if (e.NewStatus == SessionStatus.InGame)
OnPlayerJoinedGame(e.Session);
}
private void OnRunLevelChanged(ClientRunLevel newRunLevel)

View File

@@ -37,7 +37,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Replays;

View File

@@ -13,7 +13,7 @@ using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;

View File

@@ -256,7 +256,7 @@ namespace Robust.Client.Console.Commands
return;
}
var uid = EntityUid.Parse(args[0]);
var uid = EntityUid.Parse(args[0], "-1");
var entmgr = _entityManager;
if (!entmgr.EntityExists(uid))
{

View File

@@ -43,6 +43,7 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
GC.Collect();
Span<EntityUid> ents = stackalloc EntityUid[amount];
var stopwatch = new Stopwatch();
stopwatch.Start();
@@ -50,12 +51,17 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
for (var i = 0; i < amount; i++)
{
_entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
ents[i] = _entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
}
MeasureProfiler.SaveData();
shell.WriteLine($"Client: Profiled spawning {amount} entities in {stopwatch.Elapsed.TotalMilliseconds:N3} ms");
foreach (var ent in ents)
{
_entities.DeleteEntity(ent);
}
}
}
#endif

View File

@@ -366,6 +366,9 @@ namespace Robust.Client.Debugging
_debugPhysicsSystem.PointCount = 0;
}
worldHandle.UseShader(null);
worldHandle.SetTransform(Matrix3.Identity);
}
private void DrawScreen(DrawingHandleScreen screenHandle, OverlayDrawArgs args)
@@ -438,6 +441,9 @@ namespace Robust.Client.Debugging
}
}
}
screenHandle.UseShader(null);
screenHandle.SetTransform(Matrix3.Identity);
}
protected internal override void Draw(in OverlayDrawArgs args)
@@ -459,11 +465,26 @@ namespace Robust.Client.Debugging
{
switch (fixture.Shape)
{
case ChainShape cShape:
{
var count = cShape.Count;
var vertices = cShape.Vertices;
var v1 = Transform.Mul(xform, vertices[0]);
for (var i = 1; i < count; ++i)
{
var v2 = Transform.Mul(xform, vertices[i]);
worldHandle.DrawLine(v1, v2, color);
v1 = v2;
}
}
break;
case PhysShapeCircle circle:
var center = Transform.Mul(xform, circle.Position);
worldHandle.DrawCircle(center, circle.Radius, color);
break;
case EdgeShape edge:
{
var v1 = Transform.Mul(xform, edge.Vertex1);
var v2 = Transform.Mul(xform, edge.Vertex2);
worldHandle.DrawLine(v1, v2, color);
@@ -473,6 +494,7 @@ namespace Robust.Client.Debugging
worldHandle.DrawCircle(v1, 0.1f, color);
worldHandle.DrawCircle(v2, 0.1f, color);
}
}
break;
case PolygonShape poly:

View File

@@ -632,7 +632,6 @@ namespace Robust.Client
logManager.GetSawmill("ogl.debug.other").Level = LogLevel.Warning;
logManager.GetSawmill("gdparse").Level = LogLevel.Error;
logManager.GetSawmill("discord").Level = LogLevel.Warning;
logManager.GetSawmill("net.predict").Level = LogLevel.Info;
logManager.GetSawmill("szr").Level = LogLevel.Info;
logManager.GetSawmill("loc").Level = LogLevel.Warning;

View File

@@ -44,7 +44,7 @@ namespace Robust.Client.GameObjects
EntityUid IClientEntityManagerInternal.CreateEntity(string? prototypeName, out MetaDataComponent metadata)
{
return base.CreateEntity(prototypeName, out metadata);
return base.CreateEntity(prototypeName, out metadata, out _);
}
void IClientEntityManagerInternal.InitializeEntity(EntityUid entity, MetaDataComponent? meta)
@@ -170,7 +170,7 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc />
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel channel)
public void SendSystemNetworkMessage(EntityEventArgs message, INetChannel? channel)
{
throw new NotSupportedException();
}

View File

@@ -3,21 +3,17 @@ using System.Collections.Generic;
using Robust.Client.Animations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
{
public sealed class AnimationPlayerSystem : EntitySystem, IPostInjectInit
public sealed class AnimationPlayerSystem : EntitySystem
{
private readonly List<Entity<AnimationPlayerComponent>> _activeAnimations = new();
private EntityQuery<MetaDataComponent> _metaQuery;
[Dependency] private readonly IComponentFactory _compFact = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
public override void Initialize()
{
@@ -123,13 +119,13 @@ namespace Robust.Client.GameObjects
if (compTrack.ComponentType == null)
{
_sawmill.Error("Attempted to play a component animation without any component specified.");
Log.Error("Attempted to play a component animation without any component specified.");
return;
}
if (!EntityManager.TryGetComponent(ent, compTrack.ComponentType, out var animatedComp))
{
_sawmill.Error(
Log.Error(
$"Attempted to play a component animation, but the entity {ToPrettyString(ent)} does not have the component to be animated: {compTrack.ComponentType}.");
return;
}
@@ -147,7 +143,7 @@ namespace Robust.Client.GameObjects
if (animatedComp.GetType().GetProperty(compTrack.Property) is { } property &&
property.HasCustomAttribute<AutoNetworkedFieldAttribute>())
{
_sawmill.Warning($"Playing a component animation on a networked component {reg.Name} belonging to {ToPrettyString(ent)}");
Log.Warning($"Playing a component animation on a networked component {reg.Name} belonging to {ToPrettyString(ent)}");
}
}
}
@@ -182,19 +178,18 @@ namespace Robust.Client.GameObjects
public void Stop(EntityUid uid, string key)
{
if (!TryComp<AnimationPlayerComponent>(uid, out var player)) return;
if (!TryComp<AnimationPlayerComponent>(uid, out var player))
return;
player.PlayingAnimations.Remove(key);
}
public void Stop(EntityUid uid, AnimationPlayerComponent? component, string key)
{
if (!Resolve(uid, ref component, false)) return;
component.PlayingAnimations.Remove(key);
}
if (!Resolve(uid, ref component, false))
return;
void IPostInjectInit.PostInject()
{
_sawmill = _logManager.GetSawmill("anim");
component.PlayingAnimations.Remove(key);
}
}

View File

@@ -17,7 +17,6 @@ using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Random;
using Robust.Shared.Replays;
using Robust.Shared.Threading;

View File

@@ -1,9 +1,8 @@
using Robust.Client.Graphics;
using Robust.Client.Physics;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Player;
namespace Robust.Client.GameObjects;
@@ -15,8 +14,8 @@ public sealed class EyeSystem : SharedEyeSystem
{
base.Initialize();
SubscribeLocalEvent<EyeComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<EyeComponent, PlayerDetachedEvent>(OnEyeDetached);
SubscribeLocalEvent<EyeComponent, PlayerAttachedEvent>(OnEyeAttached);
SubscribeLocalEvent<EyeComponent, LocalPlayerDetachedEvent>(OnEyeDetached);
SubscribeLocalEvent<EyeComponent, LocalPlayerAttachedEvent>(OnEyeAttached);
SubscribeLocalEvent<EyeComponent, AfterAutoHandleStateEvent>(OnEyeAutoState);
// Make sure this runs *after* entities have been moved by interpolation and movement.
@@ -26,35 +25,25 @@ public sealed class EyeSystem : SharedEyeSystem
private void OnEyeAutoState(EntityUid uid, EyeComponent component, ref AfterAutoHandleStateEvent args)
{
UpdateEye(component);
UpdateEye((uid, component));
}
private void OnEyeAttached(EntityUid uid, EyeComponent component, PlayerAttachedEvent args)
private void OnEyeAttached(EntityUid uid, EyeComponent component, LocalPlayerAttachedEvent args)
{
// TODO: This probably shouldn't be nullable bruv.
if (component._eye != null)
{
_eyeManager.CurrentEye = component._eye;
}
UpdateEye((uid, component));
_eyeManager.CurrentEye = component.Eye;
var ev = new EyeAttachedEvent(uid, component);
RaiseLocalEvent(uid, ref ev, true);
}
private void OnEyeDetached(EntityUid uid, EyeComponent component, PlayerDetachedEvent args)
private void OnEyeDetached(EntityUid uid, EyeComponent component, LocalPlayerDetachedEvent args)
{
_eyeManager.ClearCurrentEye();
}
private void OnInit(EntityUid uid, EyeComponent component, ComponentInit args)
{
component._eye = new Eye
{
Position = Transform(uid).MapPosition,
Zoom = component.Zoom,
DrawFov = component.DrawFov,
Rotation = component.Rotation,
};
UpdateEye((uid, component));
}
/// <inheritdoc />
@@ -64,7 +53,7 @@ public sealed class EyeSystem : SharedEyeSystem
while (query.MoveNext(out var uid, out var eyeComponent))
{
if (eyeComponent._eye == null)
if (eyeComponent.Eye == null)
continue;
if (!TryComp<TransformComponent>(eyeComponent.Target, out var xform))
@@ -73,7 +62,7 @@ public sealed class EyeSystem : SharedEyeSystem
eyeComponent.Target = null;
}
eyeComponent._eye.Position = xform.MapPosition;
eyeComponent.Eye.Position = xform.MapPosition;
}
}
}

View File

@@ -3,16 +3,13 @@ using System.Numerics;
using Robust.Client.GameStates;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -131,7 +128,7 @@ namespace Robust.Client.GameObjects
public override void Initialize()
{
SubscribeLocalEvent<PlayerAttachSysMessage>(OnAttachedEntityChanged);
SubscribeLocalEvent<LocalPlayerAttachedEvent>(OnAttachedEntityChanged);
_conHost.RegisterCommand("incmd",
"Inserts an input command into the simulation",
@@ -171,11 +168,11 @@ namespace Robust.Client.GameObjects
HandleInputCommand(localPlayer.Session, keyFunction, message);
}
private void OnAttachedEntityChanged(PlayerAttachSysMessage message)
private void OnAttachedEntityChanged(LocalPlayerAttachedEvent message)
{
if (message.AttachedEntity != default) // attach
if (message.Entity != default) // attach
{
SetEntityContextActive(_inputManager, message.AttachedEntity);
SetEntityContextActive(_inputManager, message.Entity);
}
else // detach
{
@@ -227,44 +224,4 @@ namespace Robust.Client.GameObjects
_sawmillInputContext = _logManager.GetSawmill("input.context");
}
}
/// <summary>
/// Entity system message that is raised when the player changes attached entities.
/// </summary>
public sealed class PlayerAttachSysMessage : EntityEventArgs
{
/// <summary>
/// New entity the player is attached to.
/// </summary>
public EntityUid AttachedEntity { get; }
/// <summary>
/// Creates a new instance of <see cref="PlayerAttachSysMessage"/>.
/// </summary>
/// <param name="attachedEntity">New entity the player is attached to.</param>
public PlayerAttachSysMessage(EntityUid attachedEntity)
{
AttachedEntity = attachedEntity;
}
}
public sealed class PlayerAttachedEvent : EntityEventArgs
{
public PlayerAttachedEvent(EntityUid entity)
{
Entity = entity;
}
public EntityUid Entity { get; }
}
public sealed class PlayerDetachedEvent : EntityEventArgs
{
public PlayerDetachedEvent(EntityUid entity)
{
Entity = entity;
}
public EntityUid Entity { get; }
}
}

View File

@@ -1,12 +1,9 @@
using Robust.Client.Graphics;
using Robust.Client.Map;
using Robust.Client.Physics;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Dynamics;
namespace Robust.Client.GameObjects
{
@@ -28,10 +25,5 @@ namespace Robust.Client.GameObjects
base.Shutdown();
_overlayManager.RemoveOverlay<TileEdgeOverlay>();
}
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
{
EnsureComp<PhysicsMapComponent>(uid);
}
}
}

View File

@@ -1,13 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.ComponentTrees;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics.RSI;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
@@ -15,6 +18,7 @@ using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Robust.Client.GameObjects
@@ -183,6 +187,48 @@ namespace Robust.Client.GameObjects
{
_queuedFrameUpdate.Add(uid);
}
/// <summary>
/// Gets the specified frame for this sprite at the specified time.
/// </summary>
public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime)
{
Texture? sprite = null;
switch (spriteSpec)
{
case SpriteSpecifier.Rsi rsi:
var rsiActual = _resourceCache.GetResource<RSIResource>(rsi.RsiPath).RSI;
rsiActual.TryGetState(rsi.RsiState, out var state);
var frames = state!.GetFrames(RsiDirection.South);
var delays = state.GetDelays();
var totalDelay = delays.Sum();
var time = curTime.TotalSeconds % totalDelay;
var delaySum = 0f;
for (var i = 0; i < delays.Length; i++)
{
var delay = delays[i];
delaySum += delay;
if (time > delaySum)
continue;
sprite = frames[i];
break;
}
sprite ??= Frame0(spriteSpec);
break;
case SpriteSpecifier.Texture texture:
sprite = texture.GetTexture(_resourceCache);
break;
default:
throw new NotImplementedException();
}
return sprite;
}
}
/// <summary>

View File

@@ -54,9 +54,10 @@ namespace Robust.Client.GameObjects
// should show the entity lerping.
// - If the client predicts an entity will move while already lerping due to a state-application, it should
// clear the state's lerp, under the assumption that the client predicted the state and already rendered
// the entity in the final position.
// the entity in the state's final position.
// - If the client predicts that an entity moves, then we only lerp if this is the first time that the tick
// was predicted. I.e., we assume the entity was already rendered in it's final of that lerp.
// was predicted. I.e., we assume the entity was already rendered in the final position that was
// previously predicted.
// - If the client predicts that an entity should lerp twice in the same tick, then we need to combine them.
// I.e. moving from a->b then b->c, the client should lerp from a->c.

View File

@@ -39,7 +39,7 @@ public sealed class ClientDirtySystem : EntitySystem
private void OnTerminate(ref EntityTerminatingEvent ev)
{
if (!_timing.InPrediction || IsClientSide(ev.Entity))
if (!_timing.InPrediction || IsClientSide(ev.Entity, ev.Metadata))
return;
// Client-side entity deletion is not supported and will cause errors.

View File

@@ -5,6 +5,8 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using Arch.Core;
using Collections.Pooled;
using JetBrains.Annotations;
using Microsoft.Extensions.ObjectPool;
using Robust.Client.GameObjects;
@@ -26,18 +28,18 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Profiling;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
using Robust.Shared.Toolshed.TypeParsers;
using Robust.Shared.Utility;
using ComponentType = Arch.Core.Utils.ComponentType;
namespace Robust.Client.GameStates
{
/// <inheritdoc />
[UsedImplicitly]
public sealed class ClientGameStateManager : IClientGameStateManager, IPostInjectInit
public sealed class ClientGameStateManager : IClientGameStateManager
{
private GameStateProcessor _processor = default!;
@@ -54,11 +56,12 @@ namespace Robust.Client.GameStates
private readonly Dictionary<ushort, (IComponent Component, ComponentState? curState, ComponentState? nextState)> _compStateWork = new();
private readonly Dictionary<EntityUid, HashSet<Type>> _pendingReapplyNetStates = new();
private readonly HashSet<NetEntity> _stateEnts = new();
private readonly List<EntityUid> _toDelete = new();
private readonly List<IComponent> _toRemove = new();
private readonly Dictionary<NetEntity, Dictionary<ushort, ComponentState>> _outputData = new();
private readonly List<(EntityUid, TransformComponent)> _queuedBroadphaseUpdates = new();
private readonly List<NetEntity> _created = new();
private readonly List<NetEntity> _detached = new();
private readonly ObjectPool<Dictionary<ushort, ComponentState>> _compDataPool =
new DefaultObjectPool<Dictionary<ushort, ComponentState>>(new DictPolicy<ushort, ComponentState>(), 256);
@@ -82,6 +85,13 @@ namespace Robust.Client.GameStates
private ISawmill _sawmill = default!;
/// <summary>
/// If we are waiting for a full game state from the server, we will automatically re-send full state requests
/// if they do not arrive in time. Ideally this should never happen, this here just in case a client gets
/// stuck waiting for a full state that the server doesn't know the client even wants.
/// </summary>
public static readonly TimeSpan FullStateTimeout = TimeSpan.FromSeconds(10);
/// <inheritdoc />
public int MinBufferSize => _processor.MinBufferSize;
@@ -89,7 +99,8 @@ namespace Robust.Client.GameStates
public int TargetBufferSize => _processor.TargetBufferSize;
/// <inheritdoc />
public int CurrentBufferSize => _processor.CalculateBufferSize(_timing.LastRealTick);
public int GetApplicableStateCount() => _processor.GetApplicableStateCount();
public int StateCount => _processor.StateCount;
public bool IsPredictionEnabled { get; private set; }
public bool PredictionNeedsResetting { get; private set; }
@@ -118,10 +129,15 @@ namespace Robust.Client.GameStates
public bool DropStates;
#endif
private bool _resettingPredictedEntities;
/// <inheritdoc />
public void Initialize()
{
_processor = new GameStateProcessor(_timing);
_sawmill = _logMan.GetSawmill("state");
_sawmill.Level = LogLevel.Info;
_processor = new GameStateProcessor(this, _timing, _sawmill);
_network.RegisterNetMessage<MsgState>(HandleStateMessage);
_network.RegisterNetMessage<MsgStateLeavePvs>(HandlePvsLeaveMessage);
@@ -137,6 +153,7 @@ namespace Robust.Client.GameStates
_config.OnValueChanged(CVars.NetPredictLagBias, i => PredictLagBias = i, true);
_config.OnValueChanged(CVars.NetStateBufMergeThreshold, i => StateBufferMergeThreshold = i, true);
_config.OnValueChanged(CVars.NetPVSEntityExitBudget, i => _pvsDetachBudget = i, true);
_config.OnValueChanged(CVars.NetMaxBufferSize, i => _processor.MaxBufferSize = i, true);
_processor.Interpolation = _config.GetCVar(CVars.NetInterp);
_processor.BufferSize = _config.GetCVar(CVars.NetBufferSize);
@@ -151,6 +168,8 @@ namespace Robust.Client.GameStates
_conHost.RegisterCommand("localdelete", Loc.GetString("cmd-local-delete-desc"), Loc.GetString("cmd-local-delete-help"), LocalDeleteEntCommand);
_conHost.RegisterCommand("fullstatereset", Loc.GetString("cmd-full-state-reset-desc"), Loc.GetString("cmd-full-state-reset-help"), (_,_,_) => RequestFullState());
_entities.ComponentAdded += OnComponentAdded;
var metaId = _compFactory.GetRegistration(typeof(MetaDataComponent)).NetID;
if (!metaId.HasValue)
throw new InvalidOperationException("MetaDataComponent does not have a NetId.");
@@ -158,6 +177,23 @@ namespace Robust.Client.GameStates
_metaCompNetId = metaId.Value;
}
private void OnComponentAdded(AddedComponentEventArgs args)
{
if (_resettingPredictedEntities)
{
var comp = args.ComponentType;
if (comp.NetID == null)
return;
_sawmill.Error($"""
Added component {comp.Name} with net id {comp.NetID} while resetting predicted entities.
Stack trace:
{Environment.StackTrace}
""");
}
}
/// <inheritdoc />
public void Reset()
{
@@ -245,9 +281,19 @@ namespace Robust.Client.GameStates
/// <inheritdoc />
public void ApplyGameState()
{
// If we have been waiting for a full state for a long time, re-request a full state.
if (_processor.WaitingForFull
&& _processor.LastFullStateRequested is {} last
&& DateTime.UtcNow - last.Time > FullStateTimeout)
{
// Re-request a full state.
// We use the previous from-tick, just in case the full state is already on the way,
RequestFullState(null, last.Tick);
}
// Calculate how many states we need to apply this tick.
// Always at least one, but can be more based on StateBufferMergeThreshold.
var curBufSize = CurrentBufferSize;
var curBufSize = GetApplicableStateCount();
var targetBufSize = TargetBufferSize;
var bufferOverflow = curBufSize - targetBufSize - StateBufferMergeThreshold;
@@ -300,9 +346,9 @@ namespace Robust.Client.GameStates
}
// If we were waiting for a new state, we are now applying it.
if (_processor.LastFullStateRequested.HasValue)
if (_processor.WaitingForFull)
{
_processor.LastFullStateRequested = null;
_processor.OnFullStateReceived();
_timing.LastProcessedTick = curState.ToSequence;
DebugTools.Assert(curState.FromSequence == GameTick.Zero);
PartialStateReset(curState, true);
@@ -367,7 +413,7 @@ namespace Robust.Client.GameStates
if (_processor.WaitingForFull)
_timing.TickTimingAdjustment = 0f;
else
_timing.TickTimingAdjustment = (CurrentBufferSize - (float)TargetBufferSize) * 0.10f;
_timing.TickTimingAdjustment = (GetApplicableStateCount() - (float)TargetBufferSize) * 0.10f;
// If we are about to process an another tick in the same frame, lets not bother unnecessarily running prediction ticks
// Really the main-loop ticking just needs to be more specialized for clients.
@@ -412,11 +458,11 @@ namespace Robust.Client.GameStates
}
}
public void RequestFullState(NetEntity? missingEntity = null)
public void RequestFullState(NetEntity? missingEntity = null, GameTick? tick = null)
{
_sawmill.Info("Requesting full server state");
_network.ClientSendMessage(new MsgStateRequestFull { Tick = _timing.LastRealTick , MissingEntity = missingEntity ?? NetEntity.Invalid });
_processor.RequestFullState();
_processor.OnFullStateRequested(tick ?? _timing.LastRealTick);
}
public void PredictTicks(GameTick predictionTarget)
@@ -499,7 +545,9 @@ namespace Robust.Client.GameStates
var countReset = 0;
var system = _entitySystemManager.GetEntitySystem<ClientDirtySystem>();
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
RemQueue<IComponent> toRemove = new();
using var toRemove = new PooledList<IComponent>();
using var toAdd = new PooledList<ushort>();
using var toAddStates = new PooledList<ComponentState>();
foreach (var entity in system.DirtyEntities)
{
@@ -517,48 +565,63 @@ namespace Robust.Client.GameStates
countReset += 1;
foreach (var (netId, comp) in meta.NetComponents)
try
{
if (!comp.NetSyncEnabled)
continue;
_resettingPredictedEntities = true;
// Was this component added during prediction?
if (comp.CreationTick > _timing.LastRealTick)
foreach (var (netId, comp) in meta.NetComponents)
{
if (last.ContainsKey(netId))
if (!comp.NetSyncEnabled)
continue;
// Was this component added during prediction?
if (comp.CreationTick > _timing.LastRealTick)
{
// Component was probably removed and then re-addedd during a single prediction run
// Just reset state as normal.
comp.ClearCreationTick();
if (last.ContainsKey(netId))
{
// Component was probably removed and then re-addedd during a single prediction run
// Just reset state as normal.
comp.ClearCreationTick();
}
else
{
toRemove.Add(comp);
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A new component was added: {comp.GetType()}");
continue;
}
}
else
if (comp.LastModifiedTick <= _timing.LastRealTick ||
!last.TryGetValue(netId, out var compState))
{
toRemove.Add(comp);
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A new component was added: {comp.GetType()}");
continue;
}
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
var handleState = new ComponentHandleState(compState, null);
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
comp.LastModifiedTick = _timing.LastRealTick;
}
if (comp.LastModifiedTick <= _timing.LastRealTick || !last.TryGetValue(netId, out var compState))
{
continue;
}
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was dirtied: {comp.GetType()}");
var handleState = new ComponentHandleState(compState, null);
_entities.EventBus.RaiseComponentEvent(comp, ref handleState);
comp.LastModifiedTick = _timing.LastRealTick;
}
// Remove predicted component additions
foreach (var comp in toRemove)
finally
{
_entities.RemoveComponent(entity, comp);
_resettingPredictedEntities = false;
}
if (toRemove.Count > 0)
{
// Remove predicted component additions
// TODO: 1 archetype change.
foreach (var comp in toRemove)
{
_entities.RemoveComponent(entity, comp, meta);
}
toRemove.Clear();
}
toRemove.Clear();
// Re-add predicted removals
if (system.RemovedComponents.TryGetValue(entity, out var netIds))
@@ -571,15 +634,29 @@ namespace Robust.Client.GameStates
if (!last.TryGetValue(netId, out var state))
continue;
var comp = _entityManager.AddComponent(entity, netId, meta);
toAdd.Add(netId);
toAddStates.Add(state);
}
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was removed: {comp.GetType()}");
if (toAdd.Count > 0)
{
for (var i = 0; i < toAdd.Count; i++)
{
var netId = toAdd[i];
var state = toAddStates[i];
var comp = _entityManager.AddComponent(entity, netId, meta);
var stateEv = new ComponentHandleState(state, null);
_entities.EventBus.RaiseComponentEvent(comp, ref stateEv);
comp.ClearCreationTick(); // don't undo the re-adding.
comp.LastModifiedTick = _timing.LastRealTick;
if (_sawmill.Level <= LogLevel.Debug)
_sawmill.Debug($" A component was removed: {comp.GetType()}");
var stateEv = new ComponentHandleState(state, null);
_entities.EventBus.RaiseComponentEvent(comp, ref stateEv);
comp.ClearCreationTick(); // don't undo the re-adding.
comp.LastModifiedTick = _timing.LastRealTick;
}
toAdd.Clear();
toAddStates.Clear();
}
}
@@ -662,26 +739,25 @@ namespace Robust.Client.GameStates
_config.TickProcessMessages();
}
(IEnumerable<NetEntity> Created, List<NetEntity> Detached) output;
using (_prof.Group("Entity"))
{
output = ApplyEntityStates(curState, nextState);
ApplyEntityStates(curState, nextState);
}
using (_prof.Group("Player"))
{
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<PlayerState>());
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<SessionState>());
}
using (_prof.Group("Callback"))
{
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState, output.Detached));
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState, _detached));
}
return output.Created;
return _created;
}
private (IEnumerable<NetEntity> Created, List<NetEntity> Detached) ApplyEntityStates(GameState curState, GameState? nextState)
private void ApplyEntityStates(GameState curState, GameState? nextState)
{
var metas = _entities.GetEntityQuery<MetaDataComponent>();
var xforms = _entities.GetEntityQuery<TransformComponent>();
@@ -690,6 +766,8 @@ namespace Robust.Client.GameStates
var enteringPvs = 0;
_toApply.Clear();
_toCreate.Clear();
_detached.Clear();
_created.Clear();
_pendingReapplyNetStates.Clear();
var curSpan = curState.EntityStates.Span;
@@ -714,6 +792,7 @@ namespace Robust.Client.GameStates
var uid = _entities.CreateEntity(metaState.PrototypeId, out var newMeta);
_toCreate.Add(es.NetEntity, es);
_created.Add(es.NetEntity);
_toApply.Add(uid, (es.NetEntity, newMeta, false, GameTick.Zero, es, null));
// Client creates a client-side net entity for the newly created entity.
@@ -767,7 +846,7 @@ namespace Robust.Client.GameStates
// Detach entities to null space
var containerSys = _entitySystemManager.GetEntitySystem<ContainerSystem>();
var lookupSys = _entitySystemManager.GetEntitySystem<EntityLookupSystem>();
var detached = ProcessPvsDeparture(curState.ToSequence, metas, xforms, xformSys, containerSys, lookupSys);
ProcessPvsDeparture(_detached, curState.ToSequence, metas, xforms, xformSys, containerSys, lookupSys);
// Check next state (AFTER having created new entities introduced in curstate)
if (nextState != null)
@@ -869,8 +948,6 @@ namespace Robust.Client.GameStates
_prof.WriteValue("State Size", ProfData.Int32(curSpan.Length));
_prof.WriteValue("Entered PVS", ProfData.Int32(enteringPvs));
return (_toCreate.Keys, detached);
}
/// <inheritdoc />
@@ -901,7 +978,7 @@ namespace Robust.Client.GameStates
var xforms = _entities.GetEntityQuery<TransformComponent>();
var xformSys = _entitySystemManager.GetEntitySystem<SharedTransformSystem>();
_toDelete.Clear();
using var toDelete = new PooledList<EntityUid>();
// Client side entities won't need the transform, but that should always be a tiny minority of entities
var metaQuery = _entityManager.AllEntityQueryEnumerator<MetaDataComponent, TransformComponent>();
@@ -912,7 +989,7 @@ namespace Robust.Client.GameStates
if (metadata.NetEntity.IsClientSide())
{
if (deleteClientEntities)
_toDelete.Add(ent);
toDelete.Add(ent);
continue;
}
@@ -939,14 +1016,14 @@ namespace Robust.Client.GameStates
&& !deleteClientEntities // don't add duplicates
&& _entities.IsClientSide(child.Value))
{
_toDelete.Add(child.Value);
toDelete.Add(child.Value);
}
}
_toDelete.Add(ent);
toDelete.Add(ent);
}
foreach (var ent in _toDelete)
foreach (var ent in toDelete)
{
_entities.DeleteEntity(ent);
}
@@ -1006,7 +1083,8 @@ namespace Robust.Client.GameStates
Detach(GameTick.MaxValue, null, entities, metas, xforms, xformSys, containerSys, lookupSys);
}
private List<NetEntity> ProcessPvsDeparture(
private void ProcessPvsDeparture(
IList<NetEntity> detached,
GameTick toTick,
EntityQuery<MetaDataComponent> metas,
EntityQuery<TransformComponent> xforms,
@@ -1014,18 +1092,17 @@ namespace Robust.Client.GameStates
ContainerSystem containerSys,
EntityLookupSystem lookupSys)
{
var toDetach = _processor.GetEntitiesToDetach(toTick, _pvsDetachBudget);
var detached = new List<NetEntity>();
using var toDetach = new PooledList<(GameTick Tick, List<NetEntity> Entities)>();
_processor.GetEntitiesToDetach(toDetach, toTick, _pvsDetachBudget);
if (toDetach.Count == 0)
return detached;
return;
// TODO optimize
// If an entity is leaving PVS, so are all of its children. If we can preserve the hierarchy we can avoid
// things like container insertion and ejection.
using var _ = _prof.Group("Leave PVS");
detached.EnsureCapacity(toDetach.Count);
foreach (var (tick, ents) in toDetach)
{
@@ -1033,7 +1110,6 @@ namespace Robust.Client.GameStates
}
_prof.WriteValue("Count", ProfData.Int32(detached.Count));
return detached;
}
private void Detach(GameTick maxTick,
@@ -1044,7 +1120,7 @@ namespace Robust.Client.GameStates
SharedTransformSystem xformSys,
ContainerSystem containerSys,
EntityLookupSystem lookupSys,
List<NetEntity>? detached = null)
IList<NetEntity>? detached = null)
{
foreach (var netEntity in entities)
{
@@ -1098,7 +1174,7 @@ namespace Robust.Client.GameStates
var metaQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
#if EXCEPTION_TOLERANCE
var brokenEnts = new List<EntityUid>();
using var brokenEnts = new PooledList<EntityUid>();
#endif
using (_prof.Group("Initialize Entity"))
{
@@ -1162,18 +1238,28 @@ namespace Robust.Client.GameStates
// First remove any deleted components
if (curState?.NetComponents != null)
{
_toRemove.Clear();
using var toRemove = new PooledList<IComponent>();
using var compTypes = new PooledList<ComponentType>();
foreach (var (id, comp) in meta.NetComponents)
{
if (comp.NetSyncEnabled && !curState.NetComponents.Contains(id))
_toRemove.Add(comp);
{
toRemove.Add(comp);
compTypes.Add(comp.GetType());
}
}
foreach (var comp in _toRemove)
if (toRemove.Count > 0)
{
_entities.RemoveComponent(uid, comp, meta);
foreach (var comp in toRemove)
{
_entityManager.RemoveComponentInternal(uid, comp, terminating: false, archetypeChange: false, meta);
}
}
if (compTypes.Count > 0)
_entityManager.RemoveComponentRange(uid, compTypes);
}
if (enteringPvs)
@@ -1188,7 +1274,7 @@ namespace Robust.Client.GameStates
if (!meta.NetComponents.TryGetValue(id, out var comp))
{
comp = _compFactory.GetComponent(id);
_entityManager.AddComponent(uid, comp, true, metadata: meta);
_entityManager.AddComponent(uid, comp, metadata: meta);
}
_compStateWork[id] = (comp, state, null);
@@ -1196,18 +1282,49 @@ namespace Robust.Client.GameStates
}
else if (curState != null)
{
using var addedComps = new PooledList<IComponent>();
using var addedCompTypes = new PooledList<ComponentType>();
using var addedRegistrations = new PooledList<ComponentRegistration>();
foreach (var compChange in curState.ComponentChanges.Span)
{
if (!meta.NetComponents.TryGetValue(compChange.NetID, out var comp))
{
comp = _compFactory.GetComponent(compChange.NetID);
_entityManager.AddComponent(uid, comp, true, metadata:meta);
var registration = _compFactory.GetRegistration(compChange.NetID);
addedRegistrations.Add(registration);
comp = _compFactory.GetComponent(registration);
comp.Owner = uid;
addedComps.Add(comp);
addedCompTypes.Add(comp.GetType());
}
else if (compChange.LastModifiedTick <= lastApplied && lastApplied != GameTick.Zero)
continue;
_compStateWork[compChange.NetID] = (comp, compChange.State, null);
}
// To avoid shuffling the archetype we'll set the component range up-front.
if (addedComps.Count > 0)
{
// TODO: This fucking sucks but
// - Frequent archetype changes PER COMPONENT sucks
// - the components will be null in event handlers until it's done.
_entityManager.AddComponentRange(uid, addedCompTypes);
for (var i = 0; i < addedComps.Count; i++)
{
var component = addedComps[i];
var reg = addedRegistrations[i];
_entityManager.AddComponentInternalOnly(uid, component, reg, meta);
}
for (var i = 0; i < addedComps.Count; i++)
{
var component = addedComps[i];
var reg = addedRegistrations[i];
_entityManager.AddComponentEvents(uid, component, reg, false, meta);
}
}
}
if (nextState != null)
@@ -1297,7 +1414,7 @@ namespace Robust.Client.GameStates
return false;
}
if (!EntityUid.TryParse(args[0], out uid))
if (!EntityUid.TryParse(args[0], "-1", out uid))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-uid", ("arg", args[0])));
meta = null;
@@ -1429,23 +1546,22 @@ namespace Robust.Client.GameStates
if (!meta.NetComponents.TryGetValue(id, out var comp))
{
comp = _compFactory.GetComponent(id);
_entityManager.AddComponent(uid, comp, true, meta);
_entityManager.AddComponent(uid, comp, meta);
}
var handleState = new ComponentHandleState(state, null);
_entityManager.EventBus.RaiseComponentEvent(comp, ref handleState);
}
// ensure we don't have any extra components
_toRemove.Clear();
using var toRemove = new PooledList<IComponent>();
foreach (var (id, comp) in meta.NetComponents)
{
if (comp.NetSyncEnabled && !lastState.ContainsKey(id))
_toRemove.Add(comp);
toRemove.Add(comp);
}
foreach (var comp in _toRemove)
foreach (var comp in toRemove)
{
_entities.RemoveComponent(uid, comp);
}
@@ -1454,19 +1570,14 @@ namespace Robust.Client.GameStates
public bool IsQueuedForDetach(NetEntity entity)
=> _processor.IsQueuedForDetach(entity);
void IPostInjectInit.PostInject()
{
_sawmill = _logMan.GetSawmill(CVars.NetPredict.Name);
}
}
public sealed class GameStateAppliedArgs : EventArgs
{
public GameState AppliedState { get; }
public readonly List<NetEntity> Detached;
public readonly GameState AppliedState;
public readonly IReadOnlyList<NetEntity> Detached;
public GameStateAppliedArgs(GameState appliedState, List<NetEntity> detached)
public GameStateAppliedArgs(GameState appliedState, IReadOnlyList<NetEntity> detached)
{
AppliedState = appliedState;
Detached = detached;

View File

@@ -1,46 +1,33 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Robust.Client.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameStates
{
/// <inheritdoc />
internal sealed class GameStateProcessor : IGameStateProcessor, IPostInjectInit
internal sealed class GameStateProcessor : IGameStateProcessor
{
[Dependency] private ILogManager _logMan = default!;
private readonly IClientGameTiming _timing;
private readonly IClientGameStateManager _state;
private readonly ISawmill _logger;
private readonly List<GameState> _stateBuffer = new();
private readonly Dictionary<GameTick, List<NetEntity>> _pvsDetachMessages = new();
private ISawmill _logger = default!;
private ISawmill _stateLogger = default!;
public GameState? LastFullState { get; private set; }
public bool WaitingForFull => LastFullStateRequested.HasValue;
public GameTick? LastFullStateRequested
{
get => _lastFullStateRequested;
set
{
_lastFullStateRequested = value;
LastFullState = null;
}
}
public GameTick? _lastFullStateRequested = GameTick.Zero;
public (GameTick Tick, DateTime Time)? LastFullStateRequested { get; private set; } = (GameTick.Zero, DateTime.MaxValue);
private int _bufferSize;
private int _maxBufferSize = 512;
public const int MinimumMaxBufferSize = 256;
/// <summary>
/// This dictionary stores the full most recently received server state of any entity. This is used whenever predicted entities get reset.
@@ -61,7 +48,14 @@ namespace Robust.Client.GameStates
public int BufferSize
{
get => _bufferSize;
set => _bufferSize = value < 0 ? 0 : value;
set => _bufferSize = Math.Max(value, 0);
}
public int MaxBufferSize
{
get => _maxBufferSize;
// We place a lower bound on the maximum size to avoid spamming servers with full game state requests.
set => _maxBufferSize = Math.Max(value, MinimumMaxBufferSize);
}
/// <inheritdoc />
@@ -71,9 +65,12 @@ namespace Robust.Client.GameStates
/// Constructs a new instance of <see cref="GameStateProcessor"/>.
/// </summary>
/// <param name="timing">Timing information of the current state.</param>
public GameStateProcessor(IClientGameTiming timing)
/// <param name="clientGameStateManager"></param>
public GameStateProcessor(IClientGameStateManager state, IClientGameTiming timing, ISawmill logger)
{
_timing = timing;
_state = state;
_logger = logger;
}
/// <inheritdoc />
@@ -83,7 +80,7 @@ namespace Robust.Client.GameStates
if (state.ToSequence <= _timing.LastRealTick)
{
if (Logging)
_stateLogger.Debug($"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
_logger.Debug($"Received Old GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return false;
}
@@ -95,7 +92,7 @@ namespace Robust.Client.GameStates
continue;
if (Logging)
_stateLogger.Debug($"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
_logger.Debug($"Received Dupe GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return false;
}
@@ -104,34 +101,68 @@ namespace Robust.Client.GameStates
if (!WaitingForFull)
{
// This is a good state that we will be using.
_stateBuffer.Add(state);
TryAdd(state);
if (Logging)
_stateLogger.Debug($"Received New GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
_logger.Debug($"Received New GameState: lastRealTick={_timing.LastRealTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}");
return true;
}
if (LastFullState == null && state.FromSequence == GameTick.Zero && state.ToSequence >= LastFullStateRequested!.Value)
if (LastFullState == null && state.FromSequence == GameTick.Zero)
{
LastFullState = state;
if (Logging)
if (state.ToSequence >= LastFullStateRequested!.Value.Tick)
{
LastFullState = state;
_logger.Info($"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}");
return true;
}
return true;
_logger.Info($"Received a late full game state. Received: {state.ToSequence}. Requested: {LastFullStateRequested.Value.Tick}");
}
if (LastFullState != null && state.ToSequence <= LastFullState.ToSequence)
{
if (Logging)
_logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
_logger.Info($"While waiting for full, received late GameState with lower to={state.ToSequence} than the last full state={LastFullState.ToSequence}");
return false;
}
_stateBuffer.Add(state);
TryAdd(state);
return true;
}
public void TryAdd(GameState state)
{
if (_stateBuffer.Count <= MaxBufferSize)
{
_stateBuffer.Add(state);
return;
}
// This can happen if a required state gets dropped somehow and the client keeps receiving future
// game states that they can't apply. I.e., GetApplicableStateCount() is zero, even though there are many
// states in the list.
//
// This can seemingly happen when the server sends ""reliable"" game states while the client is paused?
// For example, when debugging the client, while the server is running:
// - The client stops sending acks for states that the server sends out.
// - Thus the client will exceed the net.force_ack_threshold cvar
// - The server starts sending some packets ""reliably"" and just force updates the clients last ack.
//
// What should happen is that when the client resumes, it receives the reliably sent states and can just
// resume. However, even though the packets are sent ""reliably"", they just seem to get dropped.
// I don't quite understand how/why yet, but this ensures the client doesn't get stuck.
#if FULL_RELEASE
_logger.Warning(@$"Exceeded maximum state buffer size!
Tick: {_timing.CurTick}/{_timing.LastProcessedTick}/{_timing.LastRealTick}
Size: {_stateBuffer.Count}
Applicable states: {GetApplicableStateCount()}
Was waiting for full: {WaitingForFull} {LastFullStateRequested}
Had full state: {LastFullState != null}"
);
#endif
_state.RequestFullState();
}
/// <summary>
/// Attempts to get the current and next states to apply.
/// </summary>
@@ -152,7 +183,7 @@ namespace Robust.Client.GameStates
"Tried to apply a non-extrapolated state that has too high of a FromSequence!");
if (Logging)
_stateLogger.Debug($"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
_logger.Debug($"Applying State: cTick={_timing.LastProcessedTick}, fSeq={curState.FromSequence}, tSeq={curState.ToSequence}, buf={_stateBuffer.Count}");
}
return applyNextState;
@@ -273,9 +304,8 @@ namespace Robust.Client.GameStates
public void ClearDetachQueue() => _pvsDetachMessages.Clear();
public List<(GameTick Tick, List<NetEntity> Entities)> GetEntitiesToDetach(GameTick toTick, int budget)
public void GetEntitiesToDetach(IList<(GameTick Tick, List<NetEntity> Entities)> result, GameTick toTick, int budget)
{
var result = new List<(GameTick Tick, List<NetEntity> Entities)>();
foreach (var (tick, entities) in _pvsDetachMessages)
{
if (tick > toTick)
@@ -294,7 +324,6 @@ namespace Robust.Client.GameStates
entities.RemoveRange(index, budget);
break;
}
return result;
}
private bool TryGetDeltaState(out GameState? curState, out GameState? nextState)
@@ -344,14 +373,20 @@ namespace Robust.Client.GameStates
{
_stateBuffer.Clear();
LastFullState = null;
LastFullStateRequested = GameTick.Zero;
LastFullStateRequested = (GameTick.Zero, DateTime.MaxValue);
}
public void RequestFullState()
public void OnFullStateRequested(GameTick tick)
{
_stateBuffer.Clear();
LastFullState = null;
LastFullStateRequested = _timing.LastRealTick;
LastFullStateRequested = (tick, DateTime.UtcNow);
}
public void OnFullStateReceived()
{
LastFullState = null;
LastFullStateRequested = null;
}
public void MergeImplicitData(Dictionary<NetEntity, Dictionary<ushort, ComponentState>> implicitData)
@@ -416,10 +451,11 @@ namespace Robust.Client.GameStates
return false;
}
public int CalculateBufferSize(GameTick fromTick)
public int GetApplicableStateCount(GameTick? fromTick = null)
{
fromTick ??= _timing.LastRealTick;
bool foundState;
var nextTick = fromTick;
var nextTick = fromTick.Value;
do
{
@@ -437,13 +473,9 @@ namespace Robust.Client.GameStates
}
while (foundState);
return (int) (nextTick.Value - fromTick.Value);
return (int) (nextTick.Value - fromTick.Value.Value);
}
void IPostInjectInit.PostInject()
{
_logger = _logMan.GetSawmill("net");
_stateLogger = _logMan.GetSawmill("net.state");
}
public int StateCount => _stateBuffer.Count;
}
}

View File

@@ -32,7 +32,15 @@ namespace Robust.Client.GameStates
/// <summary>
/// Number of applicable game states currently in the state buffer.
/// </summary>
int CurrentBufferSize { get; }
int GetApplicableStateCount();
[Obsolete("use GetApplicableStateCount()")]
int CurrentBufferSize => GetApplicableStateCount();
/// <summary>
/// Total number of game states currently in the state buffer.
/// </summary>
int StateCount { get; }
/// <summary>
/// If the buffer size is this many states larger than the target buffer size,
@@ -91,7 +99,7 @@ namespace Robust.Client.GameStates
/// <summary>
/// Requests a full state from the server. This should override even implicit entity data.
/// </summary>
void RequestFullState(NetEntity? missingEntity = null);
void RequestFullState(NetEntity? missingEntity = null, GameTick? tick = null);
uint SystemMessageDispatched<T>(T message) where T : EntityEventArgs;

View File

@@ -96,7 +96,7 @@ namespace Robust.Client.GameStates
/// This includes only applicable states. If there is a gap, future buffers are not included.
/// </summary>
/// <param name="fromTick">The tick to calculate from.</param>
int CalculateBufferSize(GameTick fromTick);
int GetApplicableStateCount(GameTick? fromTick);
bool TryGetLastServerStates(NetEntity entity,
[NotNullWhen(true)] out Dictionary<ushort, ComponentState>? dictionary);

View File

@@ -68,7 +68,7 @@ namespace Robust.Client.GameStates
var lag = _netManager.ServerChannel!.Ping;
// calc interp info
var buffer = _gameStateManager.CurrentBufferSize;
var buffer = _gameStateManager.GetApplicableStateCount();
_totalHistoryPayload += sz;
_history.Add((toSeq, sz, lag, buffer));
@@ -268,7 +268,7 @@ namespace Robust.Client.GameStates
handle.DrawString(_font, new Vector2(LeftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms");
// buffer text
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states");
handle.DrawString(_font, new Vector2(LeftMargin, height + LowerGraphOffset), $"{_gameStateManager.GetApplicableStateCount().ToString()} states");
}
protected override void DisposeBehavior()

View File

@@ -50,14 +50,18 @@ namespace Robust.Client.Graphics.Clyde
var transform = _entityManager.GetComponent<TransformComponent>(mapGrid);
gridProgram.SetUniform(UniIModelMatrix, transform.WorldMatrix);
var enumerator = mapGrid.Comp.GetMapChunks(worldBounds);
var data = _mapChunkData[mapGrid];
while (enumerator.MoveNext(out var chunk))
{
if (_isChunkDirty(mapGrid, chunk))
_updateChunkMesh(mapGrid, chunk);
DebugTools.Assert(chunk.FilledTiles > 0);
if (!data.TryGetValue(chunk.Indices, out MapChunkData? datum))
data[chunk.Indices] = datum = _initChunkBuffers(mapGrid, chunk);
var datum = _mapChunkData[mapGrid][chunk.Indices];
if (datum.Dirty)
_updateChunkMesh(mapGrid, chunk, datum);
DebugTools.Assert(datum.TileCount > 0);
if (datum.TileCount == 0)
continue;
@@ -69,17 +73,31 @@ namespace Robust.Client.Graphics.Clyde
CheckGlError();
}
}
CullEmptyChunks();
}
private void _updateChunkMesh(Entity<MapGridComponent> grid, MapChunk chunk)
private void CullEmptyChunks()
{
var data = _mapChunkData[grid];
if (!data.TryGetValue(chunk.Indices, out var datum))
foreach (var (grid, chunks) in _mapChunkData)
{
datum = _initChunkBuffers(grid, chunk);
}
var gridComp = _mapManager.GetGridComp(grid);
foreach (var (index, chunk) in chunks)
{
if (!chunk.Dirty || gridComp.Chunks.ContainsKey(index))
{
DebugTools.Assert(gridComp.Chunks[index].FilledTiles > 0);
continue;
}
DeleteChunk(chunk);
chunks.Remove(index);
}
}
}
private void _updateChunkMesh(Entity<MapGridComponent> grid, MapChunk chunk, MapChunkData datum)
{
Span<ushort> indexBuffer = stackalloc ushort[_indicesPerChunk(chunk)];
Span<Vertex2D> vertexBuffer = stackalloc Vertex2D[_verticesPerChunk(chunk)];
@@ -159,41 +177,22 @@ namespace Robust.Client.Graphics.Clyde
Dirty = true
};
_mapChunkData[grid].Add(chunk.Indices, datum);
return datum;
}
private bool _isChunkDirty(Entity<MapGridComponent> grid, MapChunk chunk)
private void DeleteChunk(MapChunkData data)
{
var data = _mapChunkData[grid];
return !data.TryGetValue(chunk.Indices, out var datum) || datum.Dirty;
}
public void _setChunkDirty(Entity<MapGridComponent> grid, Vector2i chunk)
{
var data = _mapChunkData.GetOrNew(grid);
if (data.TryGetValue(chunk, out var datum))
{
datum.Dirty = true;
}
// Don't need to set it if we don't have an entry since lack of an entry is treated as dirty.
}
private void _updateOnGridModified(GridModifiedEvent args)
{
foreach (var (pos, _) in args.Modified)
{
var grid = args.Grid;
var chunk = grid.GridTileToChunkIndices(pos);
_setChunkDirty((args.GridEnt, grid), chunk);
}
DeleteVertexArray(data.VAO);
CheckGlError();
data.VBO.Delete();
data.EBO.Delete();
}
private void _updateTileMapOnUpdate(ref TileChangedEvent args)
{
var grid = _mapManager.GetGrid(args.NewTile.GridUid);
var chunk = grid.GridTileToChunkIndices(new Vector2i(args.NewTile.X, args.NewTile.Y));
_setChunkDirty((args.NewTile.GridUid, grid), chunk);
var gridData = _mapChunkData.GetOrNew(args.Entity);
if (gridData.TryGetValue(args.ChunkIndex, out var data))
data.Dirty = true;
}
private void _updateOnGridCreated(GridStartupEvent ev)
@@ -209,10 +208,7 @@ namespace Robust.Client.Graphics.Clyde
var data = _mapChunkData[gridId];
foreach (var chunkDatum in data.Values)
{
DeleteVertexArray(chunkDatum.VAO);
CheckGlError();
chunkDatum.VBO.Delete();
chunkDatum.EBO.Delete();
DeleteChunk(chunkDatum);
}
_mapChunkData.Remove(gridId);

View File

@@ -512,7 +512,7 @@ namespace Robust.Client.Graphics.Clyde
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowFOV, worldAABB, worldBounds);
}
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawFov)
if (_lightManager.Enabled && _lightManager.DrawHardFov && eye.DrawLight && eye.DrawFov)
{
ApplyFovToBuffer(viewport, eye);
}

View File

@@ -97,6 +97,9 @@ namespace Robust.Client.Graphics.Clyde
private (PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot)[] _lightsToRenderList = default!;
private LightCapacityComparer _lightCap = new();
private ShadowCapacityComparer _shadowCap = new ShadowCapacityComparer();
private unsafe void InitLighting()
{
@@ -332,7 +335,7 @@ namespace Robust.Client.Graphics.Clyde
private void DrawLightsAndFov(Viewport viewport, Box2Rotated worldBounds, Box2 worldAABB, IEye eye)
{
if (!_lightManager.Enabled)
if (!_lightManager.Enabled || !eye.DrawLight)
{
return;
}
@@ -570,6 +573,28 @@ namespace Robust.Client.Graphics.Clyde
return true;
}
private sealed class LightCapacityComparer : IComparer<(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot)>
{
public int Compare(
(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot) x,
(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot) y)
{
if (x.light.CastShadows && !y.light.CastShadows) return 1;
if (!x.light.CastShadows && y.light.CastShadows) return -1;
return 0;
}
}
private sealed class ShadowCapacityComparer : IComparer<(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot)>
{
public int Compare(
(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot) x,
(PointLightComponent light, Vector2 pos, float distanceSquared, Angle rot) y)
{
return x.distanceSquared.CompareTo(y.distanceSquared);
}
}
private (int count, Box2 expandedBounds) GetLightsToRender(
MapId map,
in Box2Rotated worldBounds,
@@ -595,20 +620,10 @@ namespace Robust.Client.Graphics.Clyde
// First, partition the array based on whether the lights are shadow casting or not
// (non shadow casting lights should be the first partition, shadow casting lights the second)
Array.Sort(_lightsToRenderList, 0, state.count,
Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
{
if (x.light.CastShadows && !y.light.CastShadows) return 1;
else if (!x.light.CastShadows && y.light.CastShadows) return -1;
else return 0;
}));
Array.Sort(_lightsToRenderList, 0, state.count, _lightCap);
// Next, sort just the shadow casting lights by distance.
Array.Sort(_lightsToRenderList, state.count - state.shadowCastingCount, state.shadowCastingCount,
Comparer<(PointLightComponent light, Vector2 pos, float distanceSquared)>.Create((x, y) =>
{
return x.distanceSquared.CompareTo(y.distanceSquared);
}));
Array.Sort(_lightsToRenderList, state.count - state.shadowCastingCount, state.shadowCastingCount, _shadowCap);
// Then effectively delete the furthest lights, by setting the end of the array to exclude N
// number of shadow casting lights (where N is the number above the max number per scene.)

View File

@@ -173,7 +173,6 @@ namespace Robust.Client.Graphics.Clyde
_entityManager.EventBus.SubscribeEvent<TileChangedEvent>(EventSource.Local, this, _updateTileMapOnUpdate);
_entityManager.EventBus.SubscribeEvent<GridStartupEvent>(EventSource.Local, this, _updateOnGridCreated);
_entityManager.EventBus.SubscribeEvent<GridRemovalEvent>(EventSource.Local, this, _updateOnGridRemoved);
_entityManager.EventBus.SubscribeEvent<GridModifiedEvent>(EventSource.Local, this, _updateOnGridModified);
}
public void ShutdownGridEcsEvents()
@@ -181,7 +180,6 @@ namespace Robust.Client.Graphics.Clyde
_entityManager.EventBus.UnsubscribeEvent<TileChangedEvent>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<GridStartupEvent>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<GridRemovalEvent>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<GridModifiedEvent>(EventSource.Local, this);
}
private void GLInitBindings(bool gles)

View File

@@ -42,15 +42,20 @@ public sealed class TileEdgeOverlay : Overlay
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var mapSystem = _entManager.System<SharedMapSystem>();
var xformSystem = _entManager.System<SharedTransformSystem>();
foreach (var grid in _grids)
{
var tileSize = grid.Comp.TileSize;
var tileDimensions = new Vector2(tileSize, tileSize);
var xform = xformQuery.GetComponent(grid);
args.WorldHandle.SetTransform(xform.WorldMatrix);
var (_, _, worldMatrix, invMatrix) = xformSystem.GetWorldPositionRotationMatrixWithInv(grid.Owner);
args.WorldHandle.SetTransform(worldMatrix);
var localAABB = invMatrix.TransformBox(args.WorldBounds);
foreach (var tileRef in grid.Comp.GetTilesIntersecting(args.WorldBounds, false))
var enumerator = mapSystem.GetLocalTilesEnumerator(grid.Owner, grid.Comp, localAABB, false);
while (enumerator.MoveNext(out var tileRef))
{
var tileDef = _tileDefManager[tileRef.Tile.TypeId];
@@ -66,7 +71,7 @@ public sealed class TileEdgeOverlay : Overlay
continue;
var neighborIndices = new Vector2i(tileRef.GridIndices.X + x, tileRef.GridIndices.Y + y);
var neighborTile = grid.Comp.GetTileRef(neighborIndices);
var neighborTile = mapSystem.GetTileRef(grid.Owner, grid.Comp, neighborIndices);
var neighborDef = _tileDefManager[neighborTile.Tile.TypeId];
// If it's the same tile then no edge to be drawn.
@@ -118,9 +123,9 @@ public sealed class TileEdgeOverlay : Overlay
}
if (angle == Angle.Zero)
args.WorldHandle.DrawTextureRect(texture, box);
args.WorldHandle.DrawTextureRect(texture.Texture, box);
else
args.WorldHandle.DrawTextureRect(texture, new Box2Rotated(box, angle, box.Center));
args.WorldHandle.DrawTextureRect(texture.Texture, new Box2Rotated(box, angle, box.Center));
}
}
}

View File

@@ -1,6 +1,5 @@
using System.Buffers;
using System.Collections.Generic;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
@@ -8,6 +7,7 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Dynamics.Contacts;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Client.Physics;
@@ -20,8 +20,8 @@ public sealed partial class PhysicsSystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlayerAttachedEvent>(OnAttach);
SubscribeLocalEvent<PlayerDetachedEvent>(OnDetach);
SubscribeLocalEvent<LocalPlayerAttachedEvent>(OnAttach);
SubscribeLocalEvent<LocalPlayerDetachedEvent>(OnDetach);
SubscribeLocalEvent<PhysicsComponent, JointAddedEvent>(OnJointAdded);
SubscribeLocalEvent<PhysicsComponent, JointRemovedEvent>(OnJointRemoved);
}
@@ -63,12 +63,12 @@ public sealed partial class PhysicsSystem
UpdateIsPredicted(args.Joint.BodyBUid);
}
private void OnAttach(PlayerAttachedEvent ev)
private void OnAttach(LocalPlayerAttachedEvent ev)
{
UpdateIsPredicted(ev.Entity);
}
private void OnDetach(PlayerDetachedEvent ev)
private void OnDetach(LocalPlayerDetachedEvent ev)
{
UpdateIsPredicted(ev.Entity);
}

View File

@@ -44,7 +44,7 @@ namespace Robust.Client.Placement.Modes
var closestEntity = snapToEntities[0];
var closestTransform = pManager.EntityManager.GetComponent<TransformComponent>(closestEntity);
if (!pManager.EntityManager.TryGetComponent<SpriteComponent?>(closestEntity, out var component) || component.BaseRSI == null)
if (!pManager.EntityManager.TryGetComponent(closestEntity, out SpriteComponent? component) || component.BaseRSI == null)
{
return;
}

View File

@@ -8,7 +8,6 @@ using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
@@ -287,23 +286,17 @@ namespace Robust.Client.Placement
}, outsidePrediction: true))
.Register<PlacementManager>();
var localPlayer = PlayerManager.LocalPlayer;
localPlayer!.EntityAttached += OnEntityAttached;
PlayerManager.LocalPlayerDetached += OnDetached;
}
private void TearDownInput()
{
CommandBinds.Unregister<PlacementManager>();
if (PlayerManager.LocalPlayer != null)
{
PlayerManager.LocalPlayer.EntityAttached -= OnEntityAttached;
}
PlayerManager.LocalPlayerDetached -= OnDetached;
}
private void OnEntityAttached(EntityAttachedEventArgs eventArgs)
private void OnDetached(EntityUid obj)
{
// player attached to a new entity, basically disable the editor
Clear();
}

View File

@@ -1,44 +1,52 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
using Robust.Shared.Player;
namespace Robust.Client.Player
namespace Robust.Client.Player;
public interface IPlayerManager : ISharedPlayerManager
{
public interface IPlayerManager : ISharedPlayerManager
{
new IEnumerable<ICommonSession> Sessions { get; }
/// <summary>
/// Invoked when the list of sessions/players gets updated.
/// </summary>
event Action? PlayerListUpdated;
[ViewVariables]
IReadOnlyDictionary<NetUserId, ICommonSession> SessionsDict { get; }
/// <summary>
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets attached to a new entity, or when the local
/// session gets updated. See also <see cref="LocalPlayerAttachedEvent"/>
/// </summary>
event Action<EntityUid>? LocalPlayerAttached;
[ViewVariables]
LocalPlayer? LocalPlayer { get; }
/// <summary>
/// Invoked when <see cref="ISharedPlayerManager.LocalSession"/> gets detached from an entity, or when the local
/// session gets updated. See also <see cref="LocalPlayerDetachedEvent"/>
/// </summary>
event Action<EntityUid>? LocalPlayerDetached;
/// <summary>
/// Invoked after LocalPlayer is changed
/// </summary>
event Action<LocalPlayerChangedEventArgs>? LocalPlayerChanged;
/// <summary>
/// Invoked whenever <see cref="ISharedPlayerManager.LocalSession"/> changes.
/// </summary>
event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged;
event EventHandler PlayerListUpdated;
void ApplyPlayerStates(IReadOnlyCollection<SessionState> list);
void Initialize();
void Startup();
void Shutdown();
/// <summary>
/// Sets up a single player game. This creates a dummy <see cref="ISharedPlayerManager.LocalSession"/> without an
/// <see cref="INetChannel"/>.
/// </summary>
void SetupSinglePlayer(string name);
void ApplyPlayerStates(IReadOnlyCollection<PlayerState> list);
}
/// <summary>
/// Sets up the manager for a multiplayer game. This creates a <see cref="ISharedPlayerManager.LocalSession"/>
/// using the given <see cref="INetChannel"/>.
/// </summary>
void SetupMultiplayer(INetChannel channel);
public sealed class LocalPlayerChangedEventArgs : EventArgs
{
public readonly LocalPlayer? OldPlayer;
public readonly LocalPlayer? NewPlayer;
public LocalPlayerChangedEventArgs(LocalPlayer? oldPlayer, LocalPlayer? newPlayer)
{
OldPlayer = oldPlayer;
NewPlayer = newPlayer;
}
}
void SetLocalSession(ICommonSession session);
[Obsolete("Use LocalSession instead")]
LocalPlayer? LocalPlayer { get;}
}

View File

@@ -1,11 +1,6 @@
using System;
using Robust.Client.GameObjects;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Player
@@ -15,163 +10,30 @@ namespace Robust.Client.Player
/// </summary>
public sealed class LocalPlayer
{
/// <summary>
/// An entity has been attached to the local player.
/// </summary>
public event Action<EntityAttachedEventArgs>? EntityAttached;
/// <summary>
/// An entity has been detached from the local player.
/// </summary>
public event Action<EntityDetachedEventArgs>? EntityDetached;
public LocalPlayer(ICommonSession session)
{
Session = session;
}
/// <summary>
/// Game entity that the local player is controlling. If this is default, the player is not attached to any
/// entity at all.
/// </summary>
[ViewVariables] public EntityUid? ControlledEntity { get; private set; }
[ViewVariables] public NetUserId UserId { get; set; }
/// <summary>
/// Session of the local client.
/// </summary>
[ViewVariables]
public ICommonSession Session => InternalSession;
public EntityUid? ControlledEntity => Session.AttachedEntity;
internal PlayerSession InternalSession { get; set; } = default!;
[ViewVariables]
public NetUserId UserId => Session.UserId;
/// <summary>
/// OOC name of the local player.
/// </summary>
[ViewVariables]
public string Name { get; set; } = default!;
public string Name => Session.Name;
/// <summary>
/// The status of the client's session has changed.
/// Session of the local client.
/// </summary>
public event EventHandler<StatusEventArgs>? StatusChanged;
/// <summary>
/// Attaches a client to an entity.
/// </summary>
/// <param name="entity">Entity to attach the client to.</param>
public void AttachEntity(EntityUid entity, IEntityManager entMan, IBaseClient client)
{
if (ControlledEntity == entity)
return;
// Detach and cleanup first
DetachEntity();
if (!entMan.EntityExists(entity))
{
Logger.Error($"Attempting to attach player to non-existent entity {entity}!");
return;
}
ControlledEntity = entity;
InternalSession.AttachedEntity = entity;
if (!entMan.TryGetComponent<EyeComponent?>(entity, out var eye))
{
eye = entMan.AddComponent<EyeComponent>(entity);
if (client.RunLevel != ClientRunLevel.SinglePlayerGame)
{
Logger.Warning($"Attaching local player to an entity {entMan.ToPrettyString(entity)} without an eye. This eye will not be netsynced and may cause issues.");
}
eye.NetSyncEnabled = false;
}
EntityAttached?.Invoke(new EntityAttachedEventArgs(entity));
// notify ECS Systems
var eventBus = entMan.EventBus;
eventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(entity));
eventBus.RaiseLocalEvent(entity, new PlayerAttachedEvent(entity), true);
}
/// <summary>
/// Detaches the client from an entity.
/// </summary>
public void DetachEntity()
{
var entMan = IoCManager.Resolve<IEntityManager>();
var previous = ControlledEntity;
ControlledEntity = null;
InternalSession.AttachedEntity = null;
if (previous != null)
{
entMan.EventBus.RaiseEvent(EventSource.Local, new PlayerAttachSysMessage(default));
entMan.EventBus.RaiseLocalEvent(previous.Value, new PlayerDetachedEvent(previous.Value), true);
EntityDetached?.Invoke(new EntityDetachedEventArgs(previous.Value));
}
}
/// <summary>
/// Changes the state of the session.
/// </summary>
public void SwitchState(SessionStatus newStatus)
{
SwitchState(Session.Status, newStatus);
}
/// <summary>
/// Changes the state of the session. This overload allows you to spoof the oldStatus, use with caution.
/// </summary>
public void SwitchState(SessionStatus oldStatus, SessionStatus newStatus)
{
var args = new StatusEventArgs(oldStatus, newStatus);
Session.Status = newStatus;
StatusChanged?.Invoke(this, args);
}
}
/// <summary>
/// Event arguments for when the status of a session changes.
/// </summary>
public sealed class StatusEventArgs : EventArgs
{
/// <summary>
/// Status that the session switched from.
/// </summary>
public SessionStatus OldStatus { get; }
/// <summary>
/// Status that the session switched to.
/// </summary>
public SessionStatus NewStatus { get; }
/// <summary>
/// Constructs a new instance of the class.
/// </summary>
public StatusEventArgs(SessionStatus oldStatus, SessionStatus newStatus)
{
OldStatus = oldStatus;
NewStatus = newStatus;
}
}
public sealed class EntityDetachedEventArgs : EventArgs
{
public EntityDetachedEventArgs(EntityUid oldEntity)
{
OldEntity = oldEntity;
}
public EntityUid OldEntity { get; }
}
public sealed class EntityAttachedEventArgs : EventArgs
{
public EntityAttachedEventArgs(EntityUid newEntity)
{
NewEntity = newEntity;
}
public EntityUid NewEntity { get; }
public ICommonSession Session;
}
}

View File

@@ -2,16 +2,13 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Player
{
@@ -20,154 +17,212 @@ namespace Robust.Client.Player
/// Why not just attach the inputs directly? It's messy! This makes the whole thing nicely encapsulated.
/// This class also communicates with the server to let the server control what entity it is attached to.
/// </summary>
public sealed class PlayerManager : IPlayerManager
internal sealed class PlayerManager : SharedPlayerManager, IPlayerManager
{
[Dependency] private readonly IClientNetManager _network = default!;
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly ILogManager _logMan = default!;
/// <summary>
/// Active sessions of connected clients to the server.
/// Received player states that had an unknown <see cref="NetEntity"/>.
/// </summary>
private readonly Dictionary<NetUserId, ICommonSession> _sessions = new();
private Dictionary<NetUserId, SessionState> _pendingStates = new ();
private List<SessionState> _pending = new();
/// <inheritdoc />
public IEnumerable<ICommonSession> NetworkedSessions
public override ICommonSession[] NetworkedSessions
{
get
{
if (LocalPlayer is not null)
return new[] {LocalPlayer.Session};
return Enumerable.Empty<ICommonSession>();
return LocalSession != null
? new [] { LocalSession }
: Array.Empty<ICommonSession>();
}
}
/// <inheritdoc />
IEnumerable<ICommonSession> ISharedPlayerManager.Sessions => _sessions.Values;
public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1;
public LocalPlayer? LocalPlayer { get; private set; }
public event Action<SessionStatusEventArgs>? LocalStatusChanged;
public event Action? PlayerListUpdated;
public event Action<EntityUid>? LocalPlayerDetached;
public event Action<EntityUid>? LocalPlayerAttached;
public event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged;
/// <inheritdoc />
public int PlayerCount => _sessions.Values.Count;
/// <inheritdoc />
public int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? 0;
public ICommonSession? LocalSession => LocalPlayer?.Session;
/// <inheritdoc />
[ViewVariables]
public LocalPlayer? LocalPlayer
public override void Initialize(int maxPlayers)
{
get => _localPlayer;
private set
{
if (_localPlayer == value) return;
var oldValue = _localPlayer;
_localPlayer = value;
LocalPlayerChanged?.Invoke(new LocalPlayerChangedEventArgs(oldValue, _localPlayer));
}
}
private LocalPlayer? _localPlayer;
private ISawmill _sawmill = default!;
public event Action<LocalPlayerChangedEventArgs>? LocalPlayerChanged;
/// <inheritdoc />
[ViewVariables]
IEnumerable<ICommonSession> IPlayerManager.Sessions => _sessions.Values;
/// <inheritdoc />
public IReadOnlyDictionary<NetUserId, ICommonSession> SessionsDict => _sessions;
/// <inheritdoc />
public event EventHandler? PlayerListUpdated;
/// <inheritdoc />
public void Initialize()
{
_client.RunLevelChanged += OnRunLevelChanged;
_sawmill = _logMan.GetSawmill("player");
base.Initialize(maxPlayers);
_network.RegisterNetMessage<MsgPlayerListReq>();
_network.RegisterNetMessage<MsgPlayerList>(HandlePlayerList);
PlayerStatusChanged += StatusChanged;
}
private void StatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.Session == LocalPlayer?.Session)
LocalStatusChanged?.Invoke(e);
}
public void SetupSinglePlayer(string name)
{
if (LocalSession != null)
throw new InvalidOperationException($"Player manager already running?");
var session = CreateAndAddSession(default, name);
session.ClientSide = true;
SetLocalSession(session);
Startup();
PlayerListUpdated?.Invoke();
}
public void SetupMultiplayer(INetChannel channel)
{
if (LocalSession != null)
throw new InvalidOperationException($"Player manager already running?");
SetLocalSession(CreateAndAddSession(channel));
Startup();
_network.ClientSendMessage(new MsgPlayerListReq());
}
public void SetLocalSession(ICommonSession? session)
{
if (session == LocalSession)
return;
var old = LocalSession;
if (old?.AttachedEntity is {} oldUid)
{
LocalSession = null;
LocalPlayer = null;
Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(oldUid)}.");
EntManager.EventBus.RaiseLocalEvent(oldUid, new LocalPlayerDetachedEvent(oldUid), true);
LocalPlayerDetached?.Invoke(oldUid);
}
LocalSession = session;
LocalPlayer = session == null ? null : new LocalPlayer(session);
Sawmill.Info($"Changing local session from {old?.ToString() ?? "null"} to {session?.ToString() ?? "null"}.");
LocalSessionChanged?.Invoke((old, LocalSession));
if (session?.AttachedEntity is {} newUid)
{
Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(newUid)}.");
EntManager.EventBus.RaiseLocalEvent(newUid, new LocalPlayerAttachedEvent(newUid), true);
LocalPlayerAttached?.Invoke(newUid);
}
}
/// <inheritdoc />
public void Startup()
public override void Shutdown()
{
DebugTools.Assert(LocalPlayer == null);
LocalPlayer = new LocalPlayer();
var msgList = new MsgPlayerListReq();
// message is empty
_network.ClientSendMessage(msgList);
}
/// <inheritdoc />
public void Shutdown()
{
LocalPlayer?.DetachEntity();
SetAttachedEntity(LocalSession, null, out _);
LocalPlayer = null;
_sessions.Clear();
LocalSession = null;
_pendingStates.Clear();
base.Shutdown();
PlayerListUpdated?.Invoke();
}
/// <inheritdoc />
public void ApplyPlayerStates(IReadOnlyCollection<PlayerState> list)
public override bool SetAttachedEntity(ICommonSession? session, EntityUid? uid, out ICommonSession? kicked, bool force = false)
{
kicked = null;
if (session == null)
return false;
if (session.AttachedEntity == uid)
return true;
var old = session.AttachedEntity;
if (!base.SetAttachedEntity(session, uid, out kicked, force))
return false;
if (session != LocalSession)
return true;
if (old.HasValue)
{
Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(old)}.");
EntManager.EventBus.RaiseLocalEvent(old.Value, new LocalPlayerDetachedEvent(old.Value), true);
LocalPlayerDetached?.Invoke(old.Value);
}
if (uid == null)
{
Sawmill.Info($"Local player is no longer attached to any entity.");
return true;
}
if (!EntManager.EntityExists(uid))
{
Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!");
return true;
}
if (!EntManager.EnsureComponent(uid.Value, out EyeComponent eye))
{
if (_client.RunLevel != ClientRunLevel.SinglePlayerGame)
Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues.");
eye.NetSyncEnabled = false;
}
Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(uid)}.");
EntManager.EventBus.RaiseLocalEvent(uid.Value, new LocalPlayerAttachedEvent(uid.Value), true);
LocalPlayerAttached?.Invoke(uid.Value);
return true;
}
public void ApplyPlayerStates(IReadOnlyCollection<SessionState> list)
{
var dirty = ApplyStates(list, true);
if (_pendingStates.Count == 0)
{
// This is somewhat inefficient as it might try to re-apply states that failed just a moment ago.
_pending.Clear();
_pending.AddRange(_pendingStates.Values);
_pendingStates.Clear();
dirty |= ApplyStates(_pending, false);
}
if (dirty)
PlayerListUpdated?.Invoke();
}
private bool ApplyStates(IReadOnlyCollection<SessionState> list, bool fullList)
{
if (list.Count == 0)
{
// This happens when the server says "nothing changed!"
return;
}
return false;
DebugTools.Assert(_network.IsConnected || _client.RunLevel == ClientRunLevel.SinglePlayerGame // replays use state application.
, "Received player state without being connected?");
DebugTools.Assert(LocalPlayer != null, "Call Startup()");
DebugTools.Assert(LocalPlayer!.Session != null, "Received player state before Session finished setup.");
DebugTools.Assert(LocalSession != null, "Received player state before Session finished setup.");
var myState = list.FirstOrDefault(s => s.UserId == LocalPlayer.UserId);
var state = list.FirstOrDefault(s => s.UserId == LocalSession.UserId);
if (myState != null)
bool dirty = false;
if (state != null)
{
var uid = _entManager.GetEntity(myState.ControlledEntity);
if (myState.ControlledEntity is {Valid: true} && !_entManager.EntityExists(uid))
dirty = true;
if (!EntManager.TryGetEntity(state.ControlledEntity, out var uid)
&& state.ControlledEntity is { Valid:true } )
{
_sawmill.Error($"Received player state for local player with an unknown net entity!");
Sawmill.Error($"Received player state for local player with an unknown net entity!");
_pendingStates[state.UserId] = state;
}
else
{
_pendingStates.Remove(state.UserId);
}
UpdateAttachedEntity(uid);
UpdateSessionStatus(myState.Status);
SetAttachedEntity(LocalSession, uid, out _, true);
SetStatus(LocalSession, state.Status);
}
UpdatePlayerList(list);
}
/// <summary>
/// Compares the server sessionStatus to the client one, and updates if needed.
/// </summary>
private void UpdateSessionStatus(SessionStatus myStateStatus)
{
if (LocalPlayer!.Session.Status != myStateStatus)
LocalPlayer.SwitchState(myStateStatus);
}
/// <summary>
/// Compares the server attachedEntity to the client one, and updates if needed.
/// </summary>
/// <param name="entity">AttachedEntity in the server session.</param>
private void UpdateAttachedEntity(EntityUid? entity)
{
if (LocalPlayer!.ControlledEntity == entity)
{
return;
}
if (entity == null)
{
LocalPlayer.DetachEntity();
return;
}
LocalPlayer.AttachEntity(entity.Value, _entManager, _client);
return UpdatePlayerList(list, fullList) || dirty;
}
/// <summary>
@@ -175,117 +230,88 @@ namespace Robust.Client.Player
/// </summary>
private void HandlePlayerList(MsgPlayerList msg)
{
UpdatePlayerList(msg.Plyrs);
ApplyPlayerStates(msg.Plyrs);
}
/// <summary>
/// Compares the server player list to the client one, and updates if needed.
/// </summary>
private void UpdatePlayerList(IEnumerable<PlayerState> remotePlayers)
private bool UpdatePlayerList(IEnumerable<SessionState> remotePlayers, bool fullList)
{
var dirty = false;
var hitSet = new List<NetUserId>();
var users = new List<NetUserId>();
foreach (var state in remotePlayers)
{
hitSet.Add(state.UserId);
users.Add(state.UserId);
if (_sessions.TryGetValue(state.UserId, out var session))
if (!EntManager.TryGetEntity(state.ControlledEntity, out var controlled)
&& state.ControlledEntity is {Valid: true})
{
var local = (PlayerSession) session;
var controlled = _entManager.GetEntity(state.ControlledEntity);
// Exists, update data.
if (local.Name == state.Name
&& local.Status == state.Status
&& local.Ping == state.Ping
&& local.AttachedEntity == controlled)
{
continue;
}
dirty = true;
local.Name = state.Name;
local.Status = state.Status;
local.Ping = state.Ping;
local.AttachedEntity = controlled;
_pendingStates[state.UserId] = state;
}
else
{
// New, give him a slot.
dirty = true;
var newSession = new PlayerSession(state.UserId)
{
Name = state.Name,
Status = state.Status,
Ping = state.Ping,
AttachedEntity = _entManager.GetEntity(state.ControlledEntity),
};
_sessions.Add(state.UserId, newSession);
if (state.UserId == LocalPlayer!.UserId)
{
LocalPlayer.InternalSession = newSession;
newSession.ConnectedClient = _network.ServerChannel!;
// We just connected to the server, hurray!
LocalPlayer.SwitchState(SessionStatus.Connecting, newSession.Status);
}
_pendingStates.Remove(state.UserId);
}
}
foreach (var existing in _sessions.Keys.ToArray())
{
// clear slot, player left
if (!hitSet.Contains(existing))
if (!InternalSessions.TryGetValue(state.UserId, out var session))
{
DebugTools.Assert(LocalPlayer!.UserId != existing || _client.RunLevel == ClientRunLevel.SinglePlayerGame, // replays apply player states.
"I'm still connected to the server, but i left?");
_sessions.Remove(existing);
// This is a new userid, so we create a new session.
DebugTools.Assert(state.UserId != LocalPlayer?.UserId);
var newSession = (CommonSession) CreateAndAddSession(state.UserId, state.Name);
newSession.Ping = state.Ping;
SetStatus(newSession, state.Status);
SetAttachedEntity(newSession, controlled, out _, true);
dirty = true;
continue;
}
// Check if the data is actually different
if (session.Name == state.Name
&& session.Status == state.Status
&& session.Ping == state.Ping
&& session.AttachedEntity == controlled)
{
continue;
}
dirty = true;
var local = (CommonSession) session;
local.Name = state.Name;
local.Ping = state.Ping;
SetStatus(local, state.Status);
SetAttachedEntity(local, controlled, out _, true);
}
// Remove old users. This only works if the provided state is a list of all players
if (fullList)
{
foreach (var oldUser in InternalSessions.Keys.ToArray())
{
if (users.Contains(oldUser))
continue;
if (InternalSessions[oldUser].ClientSide)
continue;
DebugTools.Assert(oldUser != LocalUser
|| LocalUser == null
|| LocalUser == default(NetUserId),
"Client is still connected to the server but not in the list of players?");
RemoveSession(oldUser);
_pendingStates.Remove(oldUser);
dirty = true;
}
}
if (dirty)
{
PlayerListUpdated?.Invoke(this, EventArgs.Empty);
}
return dirty;
}
private void OnRunLevelChanged(object? sender, RunLevelChangedEventArgs e)
public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
{
if (e.NewLevel != ClientRunLevel.SinglePlayerGame)
return;
DebugTools.AssertNotNull(LocalPlayer);
// We do some further setup steps for singleplayer here...
// The local player's GUID in singleplayer will always be the default.
var guid = default(NetUserId);
var session = new PlayerSession(guid)
if (LocalEntity == uid)
{
Name = LocalPlayer!.Name,
Ping = 0,
};
LocalPlayer.UserId = guid;
LocalPlayer.InternalSession = session;
// Add the local session to the list.
_sessions.Add(guid, session);
LocalPlayer.SwitchState(SessionStatus.InGame);
PlayerListUpdated?.Invoke(this, EventArgs.Empty);
}
public bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
{
if (LocalPlayer?.ControlledEntity == uid)
{
session = LocalPlayer.Session;
session = LocalSession!;
return true;
}

View File

@@ -1,60 +0,0 @@
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
namespace Robust.Client.Player
{
internal sealed class PlayerSession : ICommonSession
{
internal SessionStatus Status { get; set; } = SessionStatus.Connecting;
/// <inheritdoc />
SessionStatus ICommonSession.Status
{
get => this.Status;
set => this.Status = value;
}
/// <inheritdoc />
[ViewVariables]
public EntityUid? AttachedEntity { get; set; }
/// <inheritdoc />
[ViewVariables]
public NetUserId UserId { get; }
[ViewVariables]
internal string Name { get; set; } = "<Unknown>";
/// <inheritdoc />
string ICommonSession.Name
{
get => this.Name;
set => this.Name = value;
}
[ViewVariables]
internal short Ping { get; set; }
/// <inheritdoc />
[ViewVariables]
public INetChannel ConnectedClient { get; internal set; } = null!;
/// <inheritdoc />
short ICommonSession.Ping
{
get => this.Ping;
set => this.Ping = value;
}
/// <summary>
/// Creates an instance of a PlayerSession.
/// </summary>
public PlayerSession(NetUserId user)
{
UserId = user;
}
}
}

View File

@@ -7,7 +7,6 @@ using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Threading.Tasks;
using Robust.Client.Upload.Commands;
using Robust.Shared;
using Robust.Shared.GameObjects;
using Robust.Shared.Replays;
@@ -101,7 +100,7 @@ public sealed partial class ReplayLoadManager
await callback(0, states.Count, LoadingState.ProcessingFiles, true);
var playerSpan = state0.PlayerStates.Value;
Dictionary<NetUserId, PlayerState> playerStates = new(playerSpan.Count);
Dictionary<NetUserId, SessionState> playerStates = new(playerSpan.Count);
foreach (var player in playerSpan)
{
playerStates.Add(player.UserId, player);
@@ -391,7 +390,7 @@ public sealed partial class ReplayLoadManager
return new EntityState(newState.NetEntity, combined, newState.EntityLastModified, newState.NetComponents ?? oldNetComps);
}
private void UpdatePlayerStates(ReadOnlySpan<PlayerState> span, Dictionary<NetUserId, PlayerState> playerStates)
private void UpdatePlayerStates(ReadOnlySpan<SessionState> span, Dictionary<NetUserId, SessionState> playerStates)
{
foreach (var player in span)
{

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Replays;
using Robust.Shared.Serialization.Markdown.Mapping;
@@ -128,6 +129,11 @@ public interface IReplayPlaybackManager
/// </summary>
event Action? ReplayUnpaused;
/// <summary>
/// Invoked just before a replay applies a game state.
/// </summary>
event Action<(GameState Current, GameState? Next)>? BeforeApplyState;
/// <summary>
/// If currently replaying a client-side recording, this is the user that recorded the replay.
/// Useful for setting default observer spawn positions.
@@ -137,5 +143,5 @@ public interface IReplayPlaybackManager
/// <summary>
/// Fetches the entity that the <see cref="Recorder"/> is currently attached to.
/// </summary>
public bool TryGetRecorderEntity([NotNullWhen(true)] out EntityUid? uid);
bool TryGetRecorderEntity([NotNullWhen(true)] out EntityUid? uid);
}

View File

@@ -66,6 +66,7 @@ internal sealed partial class ReplayPlaybackManager
_gameState.ClearDetachQueue();
EnsureDetachedExist(checkpoint);
_gameState.DetachImmediate(checkpoint.Detached);
BeforeApplyState?.Invoke((checkpoint.State, next));
_gameState.ApplyGameState(checkpoint.State, next);
}

View File

@@ -1,4 +1,5 @@
using System;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Shared.Utility;
@@ -58,7 +59,13 @@ internal sealed partial class ReplayPlaybackManager
_timing.LastRealTick = _timing.LastProcessedTick = _timing.CurTick = Replay.CurTick;
_gameState.UpdateFullRep(state, cloneDelta: true);
_gameState.ApplyGameState(state, Replay.NextState);
// Clear existing lerps
_entMan.EntitySysManager.GetEntitySystem<TransformSystem>().Reset();
var next = Replay.NextState;
BeforeApplyState?.Invoke((state, next));
_gameState.ApplyGameState(state, next);
ProcessMessages(Replay.CurMessages, skipEffectEvents);
// TODO REPLAYS block audio

View File

@@ -42,7 +42,9 @@ internal sealed partial class ReplayPlaybackManager
{
var state = Replay.CurState;
_gameState.UpdateFullRep(state, cloneDelta: true);
_gameState.ApplyGameState(state, Replay.NextState);
var next = Replay.NextState;
BeforeApplyState?.Invoke((state, next));
_gameState.ApplyGameState(state, next);
DebugTools.Assert(Replay.LastApplied >= state.FromSequence);
DebugTools.Assert(Replay.LastApplied + 1 <= state.ToSequence);
Replay.LastApplied = state.ToSequence;

View File

@@ -12,6 +12,7 @@ using Robust.Client.Upload;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
@@ -44,6 +45,7 @@ internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
public event Action? ReplayPlaybackStopped;
public event Action? ReplayPaused;
public event Action? ReplayUnpaused;
public event Action<(GameState Current, GameState? Next)>? BeforeApplyState;
public ReplayData? Replay { get; private set; }
public NetUserId? Recorder => Replay?.Recorder;

View File

@@ -9,7 +9,7 @@ using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Replays;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Value;
@@ -138,9 +138,9 @@ internal sealed class ReplayRecordingManager : SharedReplayRecordingManager
return (state, detachMsg);
}
private PlayerState GetPlayerState(ICommonSession session)
private SessionState GetPlayerState(ICommonSession session)
{
return new PlayerState
return new SessionState
{
UserId = session.UserId,
Status = session.Status,

View File

@@ -27,9 +27,9 @@
<PackageReference Include="SpaceWizards.Sodium" Version="0.2.1" PrivateAssets="compile" />
</ItemGroup>
<ItemGroup Condition="'$(EnableClientScripting)' == 'True'">
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.0.1" PrivateAssets="compile" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.0.1" PrivateAssets="compile" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" PrivateAssets="compile" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.1.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.1.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.1.0" />
<ProjectReference Include="..\Robust.Shared.Scripting\Robust.Shared.Scripting.csproj" />
</ItemGroup>

View File

@@ -20,6 +20,7 @@ namespace Robust.Client.UserInterface.Controls
public RichTextLabel()
{
IoCManager.InjectDependencies(this);
VerticalAlignment = VAlignment.Center;
}
public void SetMessage(FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null)

View File

@@ -70,6 +70,7 @@ namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
return;
var mapSystem = _entityManager.System<SharedMapSystem>();
var xformSystem = _entityManager.System<SharedTransformSystem>();
if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid))
{
@@ -80,7 +81,7 @@ namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
{
mouseGridPos = new EntityCoordinates(_mapManager.GetMapEntityId(mouseWorldMap.MapId),
mouseWorldMap.Position);
tile = new TileRef(EntityUid.Invalid, mouseGridPos.ToVector2i(_entityManager, _mapManager), Tile.Empty);
tile = new TileRef(EntityUid.Invalid, mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty);
}
var controlHovered = UserInterfaceManager.CurrentlyHovered;
@@ -90,35 +91,35 @@ Screen Size: {screenSize} (scale: {screenScale})
Mouse Pos:
Screen: {mouseScreenPos}
{mouseWorldMap}
{mouseGridPos}
{_entityManager.GetNetCoordinates(mouseGridPos)}
{tile}
GUI: {controlHovered}");
_textBuilder.AppendLine("\nAttached Entity:");
var controlledEntity = _playerManager?.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid;
_textBuilder.AppendLine("\nAttached NetEntity:");
var controlledEntity = _playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid;
if (controlledEntity == EntityUid.Invalid)
{
_textBuilder.AppendLine("No attached entity.");
_textBuilder.AppendLine("No attached netentity.");
}
else
{
var entityTransform = _entityManager.GetComponent<TransformComponent>(controlledEntity);
var playerWorldOffset = entityTransform.MapPosition;
var playerWorldOffset = xformSystem.GetMapCoordinates(entityTransform);
var playerScreen = _eyeManager.WorldToScreen(playerWorldOffset.Position);
var playerCoordinates = entityTransform.Coordinates;
var playerRotation = entityTransform.WorldRotation;
var playerRotation = xformSystem.GetWorldRotation(entityTransform);
var gridRotation = entityTransform.GridUid != null
? _entityManager.GetComponent<TransformComponent>(entityTransform.GridUid.Value)
.WorldRotation
? xformSystem.GetWorldRotation(entityTransform.GridUid.Value)
: Angle.Zero;
_textBuilder.Append($@" Screen: {playerScreen}
{playerWorldOffset}
{playerCoordinates}
{_entityManager.GetNetCoordinates(playerCoordinates)}
Rotation: {playerRotation.Degrees:F2}°
EntId: {controlledEntity}
GridUid: {entityTransform.GridUid}
NEntId: {_entityManager.GetNetEntity(controlledEntity)}
Grid NEntId: {_entityManager.GetNetEntity(entityTransform.GridUid)}
Grid Rotation: {gridRotation.Degrees:F2}°");
}

View File

@@ -15,7 +15,7 @@ using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;

View File

@@ -62,7 +62,7 @@ namespace Robust.Client.ViewVariables.Editors
void OnEntered(LineEdit.LineEditEventArgs e)
{
var gridVal = EntityUid.Parse(gridId.Text);
var gridVal = EntityUid.Parse(gridId.Text, "-1");
var mapManager = IoCManager.Resolve<IMapManager>();
var xVal = float.Parse(x.Text, CultureInfo.InvariantCulture);
var yVal = float.Parse(y.Text, CultureInfo.InvariantCulture);

View File

@@ -28,7 +28,7 @@ namespace Robust.Client.ViewVariables.Editors
if (!ReadOnly)
{
lineEdit.OnTextEntered += e =>
ValueChanged(EntityUid.Parse(e.Text));
ValueChanged(EntityUid.Parse(e.Text, ", -1"));
}
var vvButton = new Button()

View File

@@ -61,7 +61,7 @@ internal sealed class AssetPassPackRsis : AssetPass
foreach (var (key, dat) in _foundRsis)
{
if (dat.MetaJson == null)
return;
continue;
RunJob(() =>
{

View File

@@ -21,11 +21,11 @@ public sealed class RobustClientAssetGraph
/// </summary>
public IReadOnlyCollection<AssetPass> AllPasses { get; }
public RobustClientAssetGraph()
public RobustClientAssetGraph(bool parallel = true)
{
// The code injecting the list of source files is assumed to be pretty single-threaded.
// We use a parallelizing input to break out all the work on files coming in onto multiple threads.
Input = new AssetPassPipe { Name = "RobustClientAssetGraphInput", Parallelize = true };
Input = new AssetPassPipe { Name = "RobustClientAssetGraphInput", Parallelize = parallel };
PresetPasses = new AssetPassPipe { Name = "RobustClientAssetGraphPresetPasses" };
Output = new AssetPassPipe { Name = "RobustClientAssetGraphOutput", CheckDuplicates = true };
NormalizeText = new AssetPassNormalizeText { Name = "RobustClientAssetGraphNormalizeText" };

View File

@@ -4,9 +4,10 @@ namespace Robust.Packaging;
public sealed class RobustClientPackaging
{
public static IReadOnlySet<string> ClientIgnoresResources { get; } = new HashSet<string>
public static IReadOnlySet<string> ClientIgnoredResources { get; } = new HashSet<string>
{
"Maps",
"ConfigPresets",
// Leaving this here for future archaeologists to ponder at.
"emotes.xml",
"Groups",
@@ -18,48 +19,8 @@ public sealed class RobustClientPackaging
AssetPass pass,
CancellationToken cancel = default)
{
var ignoreSet = ClientIgnoresResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
var ignoreSet = ClientIgnoredResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
await RobustSharedPackaging.DoResourceCopy(Path.Combine(contentDir, "Resources"), pass, ignoreSet, cancel);
}
public static async Task WriteContentAssemblies(
AssetPass pass,
string contentDir,
string binDir,
IEnumerable<string> contentAssemblies,
CancellationToken cancel = default)
{
await WriteContentAssemblies("Assemblies", pass, contentDir, binDir, contentAssemblies, cancel);
}
public static Task WriteContentAssemblies(
string target,
AssetPass pass,
string contentDir,
string binDir,
IEnumerable<string> contentAssemblies,
CancellationToken cancel = default)
{
var files = new List<string>();
var sourceDir = Path.Combine(contentDir, "bin", binDir);
foreach (var asm in contentAssemblies)
{
files.Add($"{asm}.dll");
var pdbPath = $"{asm}.pdb";
if (File.Exists(Path.Combine(sourceDir, pdbPath)))
files.Add(pdbPath);
}
foreach (var f in files)
{
cancel.ThrowIfCancellationRequested();
pass.InjectFileFromDisk($"{target}/{f}", Path.Combine(sourceDir, f));
}
return Task.CompletedTask;
await RobustSharedPackaging.DoResourceCopy(Path.Combine(contentDir, "Resources"), pass, ignoreSet, cancel: cancel);
}
}

View File

@@ -0,0 +1,33 @@
using Robust.Packaging.AssetProcessing;
namespace Robust.Packaging;
public sealed class RobustServerPackaging
{
public static IReadOnlySet<string> ServerIgnoresResources { get; } = new HashSet<string>
{
"Audio",
"Textures",
"Fonts",
"Shaders",
};
public static async Task WriteServerResources(
string contentDir,
AssetPass pass,
CancellationToken cancel = default)
{
var ignoreSet = ServerIgnoresResources.Union(RobustSharedPackaging.SharedIgnoredResources).ToHashSet();
await RobustSharedPackaging.DoResourceCopy(Path.Combine(contentDir, "Resources"),
pass,
ignoreSet,
"Resources",
cancel);
await RobustSharedPackaging.DoResourceCopy(Path.Combine("RobustToolbox", "Resources"),
pass,
ignoreSet,
"Resources",
cancel);
}
}

View File

@@ -15,10 +15,53 @@ public sealed class RobustSharedPackaging
".DS_Store"
};
// IDK what these are supposed to correspond to but targetDir is the target directory.
public static async Task WriteContentAssemblies(
AssetPass pass,
string contentDir,
string binDir,
IEnumerable<string> contentAssemblies,
string targetDir = "Assemblies",
CancellationToken cancel = default)
{
await WriteContentAssemblies(targetDir, pass, contentDir, binDir, contentAssemblies, cancel);
}
public static Task WriteContentAssemblies(
string target,
AssetPass pass,
string contentDir,
string binDir,
IEnumerable<string> contentAssemblies,
CancellationToken cancel = default)
{
var files = new List<string>();
var sourceDir = Path.Combine(contentDir, "bin", binDir);
foreach (var asm in contentAssemblies)
{
files.Add($"{asm}.dll");
var pdbPath = $"{asm}.pdb";
if (File.Exists(Path.Combine(sourceDir, pdbPath)))
files.Add(pdbPath);
}
foreach (var f in files)
{
cancel.ThrowIfCancellationRequested();
pass.InjectFileFromDisk($"{target}/{f}", Path.Combine(sourceDir, f));
}
return Task.CompletedTask;
}
public static Task DoResourceCopy(
string diskSource,
AssetPass pass,
IReadOnlySet<string> ignoreSet,
string targetDir = "",
CancellationToken cancel = default)
{
foreach (var path in Directory.EnumerateFileSystemEntries(diskSource))
@@ -29,7 +72,7 @@ public sealed class RobustSharedPackaging
if (ignoreSet.Contains(filename))
continue;
var targetPath = filename;
var targetPath = Path.Combine(targetDir, filename);
if (Directory.Exists(path))
CopyDirIntoZip(path, targetPath, pass);
else
@@ -44,11 +87,11 @@ public sealed class RobustSharedPackaging
foreach (var file in Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories))
{
var relPath = Path.GetRelativePath(directory, file);
if (Path.DirectorySeparatorChar != '/')
relPath = relPath.Replace(Path.DirectorySeparatorChar, '/');
var zipPath = $"{basePath}/{relPath}";
if (Path.DirectorySeparatorChar != '/')
zipPath = zipPath.Replace(Path.DirectorySeparatorChar, '/');
// Console.WriteLine($"{directory}/{zipPath} -> /{zipPath}");
pass.InjectFileFromDisk(zipPath, file);
}

View File

@@ -28,6 +28,7 @@ using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
@@ -659,10 +660,14 @@ namespace Robust.Server
{
// Write down exception log
var logPath = _config.GetCVar(CVars.LogPath);
var relPath = PathHelpers.ExecutableRelativeFile(logPath);
Directory.CreateDirectory(relPath);
var pathToWrite = Path.Combine(relPath,
if (!Path.IsPathRooted(logPath))
{
logPath = PathHelpers.ExecutableRelativeFile(logPath);
}
var pathToWrite = Path.Combine(logPath,
"Runtime-" + DateTime.Now.ToString("yyyy-MM-dd-THH-mm-ss") + ".txt");
Directory.CreateDirectory(logPath);
File.WriteAllText(pathToWrite, _runtimeLog.Display(), EncodingHelpers.UTF8);
}

View File

@@ -21,7 +21,7 @@ namespace Robust.Server.Console.Commands
var sb = new StringBuilder();
var players = _players.ServerSessions;
var players = _players.Sessions;
sb.AppendLine($"{"Player Name",20} {"Status",12} {"Playing Time",14} {"Ping",9} {"IP EndPoint",20}");
sb.AppendLine("-------------------------------------------------------------------------------");
@@ -50,8 +50,8 @@ namespace Robust.Server.Console.Commands
{
if (args.Length < 1)
{
var player = shell.Player as IPlayerSession;
var toKickPlayer = player ?? _players.ServerSessions.FirstOrDefault();
var player = shell.Player;
var toKickPlayer = player ?? _players.Sessions.FirstOrDefault();
if (toKickPlayer == null)
{
shell.WriteLine("You need to provide a player to kick.");
@@ -79,7 +79,7 @@ namespace Robust.Server.Console.Commands
{
if (args.Length == 1)
{
var options = _players.ServerSessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
var options = _players.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
return CompletionResult.FromHintOptions(options, "<PlayerIndex>");
}

View File

@@ -43,6 +43,7 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
GC.Collect();
Span<EntityUid> ents = stackalloc EntityUid[amount];
var stopwatch = new Stopwatch();
stopwatch.Start();
@@ -50,12 +51,17 @@ public sealed class ProfileEntitySpawningCommand : IConsoleCommand
for (var i = 0; i < amount; i++)
{
_entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
ents[i] = _entities.SpawnEntity(prototype, MapCoordinates.Nullspace);
}
MeasureProfiler.SaveData();
shell.WriteLine($"Server: Profiled spawning {amount} entities in {stopwatch.Elapsed.TotalMilliseconds:N3} ms");
foreach (var ent in ents)
{
_entities.DeleteEntity(ent);
}
}
}
#endif

View File

@@ -1,9 +1,8 @@
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Players;
using Robust.Shared.Player;
namespace Robust.Server.Console.Commands
{
@@ -17,7 +16,7 @@ namespace Robust.Server.Console.Commands
{
var session = shell.Player;
if (session is not IPlayerSession playerSession)
if (session is not { } playerSession)
{
shell.WriteError($"Unable to find {nameof(ICommonSession)} for shell");
return;
@@ -54,7 +53,7 @@ namespace Robust.Server.Console.Commands
{
var session = shell.Player;
if (session is not IPlayerSession playerSession)
if (session is not { } playerSession)
{
shell.WriteError($"Unable to find {nameof(ICommonSession)} for shell");
return;

View File

@@ -1,6 +1,4 @@
using Robust.Server.Player;
using Robust.Shared.Map;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Errors;
@@ -10,27 +8,27 @@ namespace Robust.Server.Console
{
public IConGroupControllerImplementation? Implementation { get; set; }
public bool CanCommand(IPlayerSession session, string cmdName)
public bool CanCommand(ICommonSession session, string cmdName)
{
return Implementation?.CanCommand(session, cmdName) ?? false;
}
public bool CanAdminPlace(IPlayerSession session)
public bool CanAdminPlace(ICommonSession session)
{
return Implementation?.CanAdminPlace(session) ?? false;
}
public bool CanScript(IPlayerSession session)
public bool CanScript(ICommonSession session)
{
return Implementation?.CanScript(session) ?? false;
}
public bool CanAdminMenu(IPlayerSession session)
public bool CanAdminMenu(ICommonSession session)
{
return Implementation?.CanAdminMenu(session) ?? false;
}
public bool CanAdminReloadPrototypes(IPlayerSession session)
public bool CanAdminReloadPrototypes(ICommonSession session)
{
return Implementation?.CanAdminReloadPrototypes(session) ?? false;
}

View File

@@ -1,14 +1,14 @@
using Robust.Server.Player;
using Robust.Shared.Player;
using Robust.Shared.Toolshed;
namespace Robust.Server.Console
{
public interface IConGroupControllerImplementation : IPermissionController
{
bool CanCommand(IPlayerSession session, string cmdName);
bool CanAdminPlace(IPlayerSession session);
bool CanScript(IPlayerSession session);
bool CanAdminMenu(IPlayerSession session);
bool CanAdminReloadPrototypes(IPlayerSession session);
bool CanCommand(ICommonSession session, string cmdName);
bool CanAdminPlace(ICommonSession session);
bool CanScript(ICommonSession session);
bool CanAdminMenu(ICommonSession session);
bool CanAdminReloadPrototypes(ICommonSession session);
}
}

View File

@@ -7,7 +7,7 @@ using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Utility;
@@ -41,7 +41,7 @@ namespace Robust.Server.Console
var msg = new MsgConCmd();
msg.Text = command;
NetManager.ServerSendMessage(msg, ((IPlayerSession)session).ConnectedClient);
NetManager.ServerSendMessage(msg, session.Channel);
}
/// <inheritdoc />
@@ -49,18 +49,12 @@ namespace Robust.Server.Console
{
var msg = new FormattedMessage();
msg.AddText(text);
if (session is IPlayerSession playerSession)
OutputText(playerSession, msg, false);
else
OutputText(null, msg, false);
OutputText(session, msg, false);
}
public override void WriteLine(ICommonSession? session, FormattedMessage msg)
{
if (session is IPlayerSession playerSession)
OutputText(playerSession, msg, false);
else
OutputText(null, msg, false);
OutputText(session, msg, false);
}
/// <inheritdoc />
@@ -68,10 +62,7 @@ namespace Robust.Server.Console
{
var msg = new FormattedMessage();
msg.AddText(text);
if (session is IPlayerSession playerSession)
OutputText(playerSession, msg, true);
else
OutputText(null, msg, true);
OutputText(session, msg, true);
}
public bool IsCmdServer(IConsoleCommand cmd) => true;
@@ -155,7 +146,7 @@ namespace Robust.Server.Console
private bool ShellCanExecute(IConsoleShell shell, string cmdName)
{
return shell.Player == null || _groupController.CanCommand((IPlayerSession)shell.Player, cmdName);
return shell.Player == null || _groupController.CanCommand(shell.Player, cmdName);
}
private void HandleRegistrationRequest(INetChannel senderConnection)
@@ -213,7 +204,7 @@ namespace Robust.Server.Console
ExecuteCommand(session, text);
}
private void OutputText(IPlayerSession? session, FormattedMessage text, bool error)
private void OutputText(ICommonSession? session, FormattedMessage text, bool error)
{
if (session != null)
{

View File

@@ -1,13 +0,0 @@
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Robust.Server.GameObjects
{
[RegisterComponent]
public sealed partial class ActorComponent : Component
{
[ViewVariables]
public IPlayerSession PlayerSession { get; internal set; } = default!;
}
}

View File

@@ -1,12 +1,12 @@
using System.Collections.Generic;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
namespace Robust.Server.GameObjects
{
[RegisterComponent]
internal sealed partial class ViewSubscriberComponent : Component
{
internal readonly HashSet<IPlayerSession> SubscribedSessions = new();
internal readonly HashSet<ICommonSession> SubscribedSessions = new();
}
}

View File

@@ -1,175 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Utility;
namespace Robust.Server.GameObjects
{
/// <summary>
/// System that handles players being attached/detached from entities.
/// </summary>
[UsedImplicitly]
public sealed class ActorSystem : EntitySystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActorComponent, ComponentShutdown>(OnActorShutdown);
}
/// <summary>
/// Attaches a player session to an entity, optionally kicking any sessions already attached to it.
/// </summary>
/// <param name="uid">The entity to attach the player to</param>
/// <param name="player">The player to attach to the entity</param>
/// <param name="force">Whether to kick any existing players from the entity</param>
/// <returns>Whether the attach succeeded, or not.</returns>
public bool Attach(EntityUid? uid, IPlayerSession player, bool force = false)
{
return Attach(uid, player, false, out _);
}
/// <summary>
/// Attaches a player session to an entity, optionally kicking any sessions already attached to it.
/// </summary>
/// <param name="entity">The entity to attach the player to</param>
/// <param name="player">The player to attach to the entity</param>
/// <param name="force">Whether to kick any existing players from the entity</param>
/// <param name="forceKicked">The player that was forcefully kicked, or null.</param>
/// <returns>Whether the attach succeeded, or not.</returns>
public bool Attach(EntityUid? entity, IPlayerSession player, bool force, out IPlayerSession? forceKicked)
{
// Null by default.
forceKicked = null;
if (player.AttachedEntity == entity)
return true;
if (entity is not { } uid)
return Detach(player);
// Cannot attach to a deleted, nonexisting or terminating entity.
if (!TryComp(uid, out MetaDataComponent? meta) || meta.EntityLifeStage > EntityLifeStage.MapInitialized)
{
return false;
}
// Check if there was a player attached to the entity already...
if (EntityManager.TryGetComponent(uid, out ActorComponent? actor))
{
// If we're not forcing the attach, this fails.
if (!force)
return false;
// Set the event's force-kicked session before detaching it.
forceKicked = actor.PlayerSession;
Detach(uid, actor);
}
// Detach from the currently attached entity.
if (!Detach(player))
return false;
// We add the actor component.
actor = EntityManager.AddComponent<ActorComponent>(uid);
EntityManager.EnsureComponent<EyeComponent>(uid);
actor.PlayerSession = player;
player.SetAttachedEntity(uid);
// The player is fully attached now, raise an event!
RaiseLocalEvent(uid, new PlayerAttachedEvent(uid, player, forceKicked), true);
DebugTools.Assert(player.AttachedEntity == entity);
return true;
}
/// <summary>
/// Detaches an attached session from the entity, if any.
/// </summary>
/// <param name="entity">The entity player sessions will be detached from.</param>
/// <returns>Whether any player session was detached.</returns>
public bool Detach(EntityUid uid, ActorComponent? actor = null)
{
if (!Resolve(uid, ref actor, false))
return false;
RemComp(uid, actor);
return true;
}
/// <summary>
/// Detaches this player from its attached entity, if any.
/// </summary>
/// <param name="player">The player session that will be detached from any attached entities.</param>
/// <returns>Whether the player is now detached from any entities.
/// This returns true if the player wasn't attached to any entity.</returns>
public bool Detach(IPlayerSession player)
{
var uid = player.AttachedEntity;
return uid == null || Detach(uid.Value);
}
private void OnActorShutdown(EntityUid entity, ActorComponent component, ComponentShutdown args)
{
component.PlayerSession.SetAttachedEntity(null);
// The player is fully detached now that the component has shut down.
RaiseLocalEvent(entity, new PlayerDetachedEvent(entity, component.PlayerSession), true);
}
public bool TryGetActorFromUserId(NetUserId? userId, [NotNullWhen(true)] out IPlayerSession? actor, [MaybeNullWhen(true)] out EntityUid? actorEntity)
{
actor = null;
actorEntity = null;
if (userId != null)
{
if (!_playerManager.TryGetSessionById(userId.Value, out actor))
return false;
actorEntity = actor.AttachedEntity;
}
return actor != null;
}
}
/// <summary>
/// Event for when a player has been attached to an entity.
/// </summary>
public sealed class PlayerAttachedEvent : EntityEventArgs
{
public EntityUid Entity { get; }
public IPlayerSession Player { get; }
/// <summary>
/// The player session that was forcefully kicked from the entity, if any.
/// </summary>
public IPlayerSession? Kicked { get; }
public PlayerAttachedEvent(EntityUid entity, IPlayerSession player, IPlayerSession? kicked = null)
{
Entity = entity;
Player = player;
Kicked = kicked;
}
}
/// <summary>
/// Event for when a player has been detached from an entity.
/// </summary>
public sealed class PlayerDetachedEvent : EntityEventArgs
{
public EntityUid Entity { get; }
public IPlayerSession Player { get; }
public PlayerDetachedEvent(EntityUid entity, IPlayerSession player)
{
Entity = entity;
Player = player;
}
}
}

View File

@@ -6,7 +6,6 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Players;
namespace Robust.Server.GameObjects;
[UsedImplicitly]

View File

@@ -1,5 +1,4 @@
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
namespace Robust.Server.GameObjects;

View File

@@ -5,6 +5,7 @@ using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Player;
namespace Robust.Server.GameObjects
{
@@ -15,10 +16,10 @@ namespace Robust.Server.GameObjects
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
private readonly Dictionary<IPlayerSession, IPlayerCommandStates> _playerInputs = new();
private readonly Dictionary<ICommonSession, IPlayerCommandStates> _playerInputs = new();
private readonly Dictionary<IPlayerSession, uint> _lastProcessedInputCmd = new();
private readonly Dictionary<ICommonSession, uint> _lastProcessedInputCmd = new();
/// <inheritdoc />
public override void Initialize()
@@ -48,7 +49,7 @@ namespace Robust.Server.GameObjects
if (!Enum.IsDefined(typeof(BoundKeyState), msg.State))
return;
var session = (IPlayerSession) eventArgs.SenderSession;
var session = eventArgs.SenderSession;
if (_lastProcessedInputCmd[session] < msg.InputSequence)
_lastProcessedInputCmd[session] = msg.InputSequence;
@@ -65,12 +66,12 @@ namespace Robust.Server.GameObjects
}
}
public IPlayerCommandStates GetInputStates(IPlayerSession session)
public IPlayerCommandStates GetInputStates(ICommonSession session)
{
return _playerInputs[session];
}
public uint GetLastInputCommand(IPlayerSession session)
public uint GetLastInputCommand(ICommonSession session)
{
return _lastProcessedInputCmd[session];
}

View File

@@ -5,6 +5,9 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using Arch.Core;
using Arch.Core.Utils;
using Collections.Pooled;
using Robust.Server.Maps;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
@@ -154,10 +157,11 @@ public sealed class MapLoaderSystem : EntitySystem
var sw = new Stopwatch();
sw.Start();
result = Deserialize(data);
_logLoader.Debug($"Loaded map in {sw.Elapsed}");
var mapEnt = _mapManager.GetMapEntityId(mapId);
_logLoader.Info($"Loaded map {resPath} in {sw.Elapsed}");
var xformQuery = _serverEntityManager.GetEntityQuery<TransformComponent>();
var mapEnt = _mapManager.GetMapEntityId(mapId);
var rootEnts = new List<EntityUid>();
// aeoeoeieioe content
@@ -174,6 +178,8 @@ public sealed class MapLoaderSystem : EntitySystem
}
}
// EntityManager.CleanupArch();
rootUids = rootEnts;
}
@@ -290,6 +296,9 @@ public sealed class MapLoaderSystem : EntitySystem
ReadGrids(data);
// grids prior to engine v175 might've been serialized with empty chunks which now throw debug asserts.
RemoveEmptyChunks(data);
// Then, go hierarchically in order and do the entity things.
StartupEntities(data);
@@ -305,6 +314,25 @@ public sealed class MapLoaderSystem : EntitySystem
return true;
}
private void RemoveEmptyChunks(MapData data)
{
var gridQuery = _serverEntityManager.GetEntityQuery<MapGridComponent>();
foreach (var uid in data.EntitiesToDeserialize.Keys)
{
if (!gridQuery.TryGetComponent(uid, out var gridComp))
continue;
foreach (var (index, chunk) in gridComp.Chunks)
{
if (chunk.FilledTiles > 0)
continue;
Log.Warning($"Encountered empty chunk while deserializing map. Grid: {ToPrettyString(uid)}. Chunk index: {index}");
gridComp.Chunks.Remove(index);
}
}
}
private bool VerifyEntitiesExist(MapData data, BeforeEntityReadEvent ev)
{
_stopwatch.Restart();
@@ -406,6 +434,10 @@ public sealed class MapLoaderSystem : EntitySystem
if (data.Version >= 4)
{
var metaEntities = data.RootMappingNode.Get<SequenceDataNode>("entities");
using var mapSaveCompType = new PooledSet<Type>()
{
typeof(MapSaveIdComponent)
};
foreach (var metaDef in metaEntities.Cast<MappingDataNode>())
{
@@ -425,9 +457,30 @@ public sealed class MapLoaderSystem : EntitySystem
var entities = (SequenceDataNode) metaDef["entities"];
EntityPrototype? proto = null;
var count = entities.Count;
var entTotal = data.Entities.Count + count;
data.Entities.EnsureCapacity(entTotal);
data.UidEntityMap.EnsureCapacity(entTotal);
data.EntitiesToDeserialize.EnsureCapacity(entTotal);
if (type != null)
_prototypeManager.TryIndex(type, out proto);
{
if (_prototypeManager.TryIndex(type, out proto) && count > 1)
{
ComponentType[] compTypes;
if (data.Options.StoreMapUids)
{
compTypes = EntityManager.GetComponentType(proto, mapSaveCompType);
}
else
{
compTypes = EntityManager.GetComponentType(proto);
}
EntityManager.Reserve(compTypes, count);
}
}
foreach (var entityDef in entities.Cast<MappingDataNode>())
{
@@ -442,6 +495,7 @@ public sealed class MapLoaderSystem : EntitySystem
{
deletedPrototypeUids.Add(entity);
}
// TODO: Move this elsewhere?
else if (data.Options.StoreMapUids)
{
var comp = _serverEntityManager.AddComponent<MapSaveIdComponent>(entity);
@@ -550,6 +604,7 @@ public sealed class MapLoaderSystem : EntitySystem
_context.CurrentlyIgnoredComponents = missingComponentList.Cast<ValueDataNode>().Select(x => x.Value).ToHashSet();
_serverEntityManager.FinishEntityLoad(uid, meta.EntityPrototype, _context);
if (_context.CurrentlyIgnoredComponents.Count > 0)
meta.LastComponentRemoved = _timing.CurTick;
}

View File

@@ -24,11 +24,6 @@ namespace Robust.Server.GameObjects
_cfg.OnValueChanged(CVars.GameDeleteEmptyGrids, SetGridDeletion, true);
}
protected override void OnMapAdd(EntityUid uid, MapComponent component, ComponentAdd args)
{
EnsureComp<PhysicsMapComponent>(uid);
}
private void SetGridDeletion(bool value)
{
_deleteEmptyGrids = value;
@@ -66,9 +61,8 @@ namespace Robust.Server.GameObjects
private void HandleGridEmpty(EntityUid uid, MapGridComponent component, EmptyGridEvent args)
{
if (!_deleteEmptyGrids) return;
if (!EntityManager.EntityExists(uid)) return;
if (EntityManager.GetComponent<MetaDataComponent>(uid).EntityLifeStage >= EntityLifeStage.Terminating) return;
if (!_deleteEmptyGrids || TerminatingOrDeleted(uid) || HasComp<MapComponent>(uid))
return;
MapManager.DeleteGrid(args.GridId);
}

View File

@@ -1,6 +1,7 @@
using Robust.Server.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
namespace Robust.Server.GameObjects;

View File

@@ -1,13 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Server.GameObjects
@@ -21,8 +20,6 @@ namespace Robust.Server.GameObjects
private readonly List<ICommonSession> _sessionCache = new();
private readonly Dictionary<ICommonSession, List<PlayerBoundUserInterface>> _openInterfaces = new();
/// <inheritdoc />
public override void Initialize()
{
@@ -46,7 +43,7 @@ namespace Robust.Server.GameObjects
if (args.NewStatus != SessionStatus.Disconnected)
return;
if (!_openInterfaces.TryGetValue(args.Session, out var buis))
if (!OpenInterfaces.TryGetValue(args.Session, out var buis))
return;
foreach (var bui in buis.ToArray())
@@ -92,7 +89,7 @@ namespace Robust.Server.GameObjects
/// <summary>
/// Verify that the subscribed clients are still in range of the interface.
/// </summary>
private void CheckRange(EntityUid uid, Shared.GameObjects.ActiveUserInterfaceComponent activeUis, PlayerBoundUserInterface ui, TransformComponent transform, EntityQuery<TransformComponent> query)
private void CheckRange(EntityUid uid, ActiveUserInterfaceComponent activeUis, PlayerBoundUserInterface ui, TransformComponent transform, EntityQuery<TransformComponent> query)
{
if (ui.InteractionRange <= 0)
return;
@@ -139,11 +136,6 @@ namespace Robust.Server.GameObjects
}
}
private void ActivateInterface(PlayerBoundUserInterface ui)
{
EnsureComp<ActiveUserInterfaceComponent>(ui.Owner).Interfaces.Add(ui);
}
#region Get BUI
public bool HasUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
@@ -168,20 +160,14 @@ namespace Robust.Server.GameObjects
? bui
: null;
}
public bool TryGetUi(EntityUid uid, Enum uiKey, [NotNullWhen(true)] out PlayerBoundUserInterface? bui, UserInterfaceComponent? ui = null)
{
bui = null;
return Resolve(uid, ref ui, false) && ui.Interfaces.TryGetValue(uiKey, out bui);
}
/// <summary>
/// Return UIs a session has open.
/// Null if empty.
/// <summary>
/// </summary>
public List<PlayerBoundUserInterface>? GetAllUIsForSession(ICommonSession session)
{
_openInterfaces.TryGetValue(session, out var value);
OpenInterfaces.TryGetValue(session, out var value);
return value;
}
#endregion
@@ -194,7 +180,7 @@ namespace Robust.Server.GameObjects
return bui.SubscribedSessions.Count > 0;
}
public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, IPlayerSession session, UserInterfaceComponent? ui = null)
public bool SessionHasOpenUi(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
@@ -218,7 +204,7 @@ namespace Robust.Server.GameObjects
public bool TrySetUiState(EntityUid uid,
Enum uiKey,
BoundUserInterfaceState state,
IPlayerSession? session = null,
ICommonSession? session = null,
UserInterfaceComponent? ui = null,
bool clearOverrides = true)
{
@@ -259,94 +245,14 @@ namespace Robust.Server.GameObjects
bui.StateDirty = true;
}
/// <summary>
/// Switches between closed and open for a specific client.
/// </summary>
public bool TryToggleUi(EntityUid uid, Enum uiKey, ICommonSession session, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
ToggleUi(bui, session);
return true;
}
/// <summary>
/// Switches between closed and open for a specific client.
/// </summary>
public void ToggleUi(PlayerBoundUserInterface bui, ICommonSession session)
{
if (bui._subscribedSessions.Contains(session))
CloseUi(bui, session);
else
OpenUi(bui, session);
}
#region Open
public bool TryOpen(EntityUid uid, Enum uiKey, IPlayerSession session, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return OpenUi(bui, session);
}
/// <summary>
/// Opens this interface for a specific client.
/// </summary>
public bool OpenUi(PlayerBoundUserInterface bui, ICommonSession session)
{
if (session.Status == SessionStatus.Connecting || session.Status == SessionStatus.Disconnected)
return false;
if (!bui._subscribedSessions.Add(session))
return false;
_openInterfaces.GetOrNew(session).Add(bui);
RaiseLocalEvent(bui.Owner, new BoundUIOpenedEvent(bui.UiKey, bui.Owner, session));
RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), new OpenBoundInterfaceMessage(), bui.UiKey), session.ConnectedClient);
// Fun fact, clients needs to have BUIs open before they can receive the state.....
if (bui.LastStateMsg != null)
RaiseNetworkEvent(bui.LastStateMsg, session.ConnectedClient);
ActivateInterface(bui);
return true;
}
#endregion
#region Close
public bool TryClose(EntityUid uid, Enum uiKey, IPlayerSession session, UserInterfaceComponent? ui = null)
{
if (!TryGetUi(uid, uiKey, out var bui, ui))
return false;
return CloseUi(bui, session);
}
/// <summary>
/// Close this interface for a specific client.
/// </summary>
public bool CloseUi(PlayerBoundUserInterface bui, ICommonSession session, ActiveUserInterfaceComponent? activeUis = null)
{
if (!bui._subscribedSessions.Remove(session))
return false;
RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), new CloseBoundInterfaceMessage(), bui.UiKey), session.ConnectedClient);
CloseShared(bui, session, activeUis);
return true;
}
protected override void CloseShared(PlayerBoundUserInterface bui, ICommonSession session, ActiveUserInterfaceComponent? activeUis = null)
{
var owner = bui.Owner;
bui._subscribedSessions.Remove(session);
bui.PlayerStateOverrides.Remove(session);
if (_openInterfaces.TryGetValue(session, out var buis))
if (OpenInterfaces.TryGetValue(session, out var buis))
buis.Remove(bui);
RaiseLocalEvent(owner, new BoundUIClosedEvent(bui.UiKey, owner, session));

View File

@@ -1,5 +1,5 @@
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
namespace Robust.Server.GameObjects
{
@@ -18,7 +18,7 @@ namespace Robust.Server.GameObjects
/// <summary>
/// Subscribes the session to get PVS updates from the point of view of the specified entity.
/// </summary>
public void AddViewSubscriber(EntityUid uid, IPlayerSession session)
public void AddViewSubscriber(EntityUid uid, ICommonSession session)
{
// If the entity doesn't have the component, it will be added.
var viewSubscriber = EntityManager.EnsureComponent<ViewSubscriberComponent>(uid);
@@ -27,7 +27,7 @@ namespace Robust.Server.GameObjects
return; // Already subscribed, do nothing else.
viewSubscriber.SubscribedSessions.Add(session);
session.AddViewSubscription(uid);
session.ViewSubscriptions.Add(uid);
RaiseLocalEvent(uid, new ViewSubscriberAddedEvent(uid, session), true);
}
@@ -35,7 +35,7 @@ namespace Robust.Server.GameObjects
/// <summary>
/// Unsubscribes the session from getting PVS updates from the point of view of the specified entity.
/// </summary>
public void RemoveViewSubscriber(EntityUid uid, IPlayerSession session)
public void RemoveViewSubscriber(EntityUid uid, ICommonSession session)
{
if(!EntityManager.TryGetComponent(uid, out ViewSubscriberComponent? viewSubscriber))
return; // Entity didn't have any subscriptions, do nothing.
@@ -43,7 +43,7 @@ namespace Robust.Server.GameObjects
if (!viewSubscriber.SubscribedSessions.Remove(session))
return; // Session wasn't subscribed, do nothing.
session.RemoveViewSubscription(uid);
session.ViewSubscriptions.Remove(uid);
RaiseLocalEvent(uid, new ViewSubscriberRemovedEvent(uid, session), true);
}
@@ -51,7 +51,7 @@ namespace Robust.Server.GameObjects
{
foreach (var session in component.SubscribedSessions)
{
session.RemoveViewSubscription(uid);
session.ViewSubscriptions.Remove(uid);
}
}
}
@@ -62,9 +62,9 @@ namespace Robust.Server.GameObjects
public sealed class ViewSubscriberAddedEvent : EntityEventArgs
{
public EntityUid View { get; }
public IPlayerSession Subscriber { get; }
public ICommonSession Subscriber { get; }
public ViewSubscriberAddedEvent(EntityUid view, IPlayerSession subscriber)
public ViewSubscriberAddedEvent(EntityUid view, ICommonSession subscriber)
{
View = view;
Subscriber = subscriber;
@@ -78,9 +78,9 @@ namespace Robust.Server.GameObjects
public sealed class ViewSubscriberRemovedEvent : EntityEventArgs
{
public EntityUid View { get; }
public IPlayerSession Subscriber { get; }
public ICommonSession Subscriber { get; }
public ViewSubscriberRemovedEvent(EntityUid view, IPlayerSession subscriber)
public ViewSubscriberRemovedEvent(EntityUid view, ICommonSession subscriber)
{
View = view;
Subscriber = subscriber;

View File

@@ -1,12 +1,10 @@
using System.Collections.Generic;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;
using Robust.Shared.Player;
namespace Robust.Server.GameObjects
{
public interface IServerEntityNetworkManager : IEntityNetworkManager
{
uint GetLastMessageSequence(IPlayerSession session);
uint GetLastMessageSequence(ICommonSession session);
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Prometheus;
using Robust.Server.Player;
@@ -15,6 +14,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
@@ -26,7 +26,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 partial class ServerEntityManager : EntityManager, IServerEntityManagerInternal
{
private static readonly Gauge EntitiesCount = Metrics.CreateGauge(
"robust_entities_count",
@@ -62,7 +62,7 @@ namespace Robust.Server.GameObjects
EntityUid IServerEntityManagerInternal.AllocEntity(EntityPrototype? prototype)
{
return AllocEntity(prototype, out _);
return AllocEntity(prototype, out _, out _);
}
void IServerEntityManagerInternal.FinishEntityLoad(EntityUid entity, IEntityLoadContext? context)
@@ -85,15 +85,15 @@ namespace Robust.Server.GameObjects
StartEntity(entity);
}
private protected override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
private protected override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, out TransformComponent xform, IEntityLoadContext? context = null)
{
if (prototypeName == null)
return base.CreateEntity(prototypeName, out metadata, context);
return base.CreateEntity(prototypeName, out metadata, out xform, context);
if (!PrototypeManager.TryIndex<EntityPrototype>(prototypeName, out var prototype))
throw new EntityCreationException($"Attempted to spawn an entity with an invalid prototype: {prototypeName}");
var entity = base.CreateEntity(prototype, out metadata, context);
var entity = base.CreateEntity(prototype, out metadata, out xform, context);
// At this point in time, all data configure on the entity *should* be purely from the prototype.
// As such, we can reset the modified ticks to Zero,
@@ -131,7 +131,7 @@ namespace Robust.Server.GameObjects
private readonly PriorityQueue<MsgEntity> _queue = new(new MessageSequenceComparer());
private readonly Dictionary<IPlayerSession, uint> _lastProcessedSequencesCmd =
private readonly Dictionary<ICommonSession, uint> _lastProcessedSequencesCmd =
new();
private bool _logLateMsgs;
@@ -159,10 +159,10 @@ namespace Robust.Server.GameObjects
base.TickUpdate(frameTime, noPredictions, histogram);
EntitiesCount.Set(Entities.Count);
EntitiesCount.Set(EntityCount);
}
public uint GetLastMessageSequence(IPlayerSession session)
public uint GetLastMessageSequence(ICommonSession session)
{
return _lastProcessedSequencesCmd[session];
}

View File

@@ -1,6 +1,6 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Robust.Server.GameStates

View File

@@ -8,7 +8,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;

View File

@@ -1,5 +1,5 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Players;
using Robust.Shared.Player;
namespace Robust.Server.GameStates;

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Robust.Shared.GameObjects;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;

View File

@@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -68,7 +68,7 @@ namespace Robust.Server.GameStates
return true;
}
public void CleanupDirty(IEnumerable<IPlayerSession> sessions)
public void CleanupDirty(IEnumerable<ICommonSession> sessions)
{
if (!CullingEnabled)
{

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
@@ -15,7 +16,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -35,10 +36,16 @@ internal sealed partial class PvsSystem : EntitySystem
public const float ChunkSize = 8;
// TODO make this a cvar. Make it in terms of seconds and tie it to tick rate?
// Main issue is that I CBF figuring out the logic for handling it changing mid-game.
public const int DirtyBufferSize = 20;
// Note: If a client has ping higher than TickBuffer / TickRate, then the server will treat every entity as if it
// had entered PVS for the first time. Note that due to the PVS budget, this buffer is easily overwhelmed.
/// <summary>
/// See <see cref="CVars.NetForceAckThreshold"/>.
/// </summary>
public int ForceAckThreshold { get; private set; }
/// <summary>
/// Maximum number of pooled objects
/// </summary>
@@ -139,6 +146,7 @@ internal sealed partial class PvsSystem : EntitySystem
_configManager.OnValueChanged(CVars.NetPVS, SetPvs, true);
_configManager.OnValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged, true);
_configManager.OnValueChanged(CVars.NetForceAckThreshold, OnForceAckChanged, true);
_serverGameStateManager.ClientAck += OnClientAck;
_serverGameStateManager.ClientRequestFull += OnClientRequestFull;
@@ -156,6 +164,7 @@ internal sealed partial class PvsSystem : EntitySystem
_configManager.UnsubValueChanged(CVars.NetPVS, SetPvs);
_configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, OnViewsizeChanged);
_configManager.UnsubValueChanged(CVars.NetForceAckThreshold, OnForceAckChanged);
_serverGameStateManager.ClientAck -= OnClientAck;
_serverGameStateManager.ClientRequestFull -= OnClientRequestFull;
@@ -199,7 +208,7 @@ internal sealed partial class PvsSystem : EntitySystem
// return last acked to pool, but only if it is not still in the OverflowDictionary.
if (sessionData.LastAcked != null && !sessionData.SentEntities.ContainsKey(sessionData.LastAcked.Value.Tick))
{
DebugTools.Assert(sessionData.SentEntities.Values.Contains(sessionData.LastAcked.Value.Data));
DebugTools.Assert(!sessionData.SentEntities.Values.Contains(sessionData.LastAcked.Value.Data));
_visSetPool.Return(sessionData.LastAcked.Value.Data);
}
@@ -212,6 +221,11 @@ internal sealed partial class PvsSystem : EntitySystem
_viewSize = obj * 2;
}
private void OnForceAckChanged(int value)
{
ForceAckThreshold = value;
}
private void SetPvs(bool value)
{
_seenAllEnts.Clear();
@@ -258,6 +272,7 @@ internal sealed partial class PvsSystem : EntitySystem
{
sessionData.LastSeenAt.Remove(metadata.NetEntity);
sessionData.LastLeftView.Remove(metadata.NetEntity);
if (sessionData.SentEntities.TryGetValue(previousTick, out var ents))
ents.Remove(metadata.NetEntity);
}
@@ -420,10 +435,15 @@ internal sealed partial class PvsSystem : EntitySystem
#endregion
public (List<(int, IChunkIndexLocation)> , HashSet<int>[], EntityUid[][] viewers) GetChunks(IPlayerSession[] sessions)
public List<(int, IChunkIndexLocation)> GetChunks(
ICommonSession[] sessions,
ref HashSet<int>[] playerChunks,
ref EntityUid[][] viewerEntities)
{
var playerChunks = new HashSet<int>[sessions.Length];
var viewerEntities = new EntityUid[sessions.Length][];
// Pass these in to avoid allocating new ones every tick, 99% of the time sessions length is going to be the same size.
// These values will get overridden here and the old values have already been returned to the pool by this point.
Array.Resize(ref playerChunks, sessions.Length);
Array.Resize(ref viewerEntities, sessions.Length);
_chunkList.Clear();
// Keep track of the index of each chunk we use for a faster index lookup.
@@ -446,8 +466,8 @@ internal sealed partial class PvsSystem : EntitySystem
var session = sessions[i];
playerChunks[i] = _playerChunkPool.Get();
var viewers = GetSessionViewers(session);
viewerEntities[i] = viewers;
ref var viewers = ref viewerEntities[i];
GetSessionViewers(session, ref viewers);
for (var j = 0; j < viewers.Length; j++)
{
@@ -539,7 +559,7 @@ internal sealed partial class PvsSystem : EntitySystem
}
}
return (_chunkList, playerChunks, viewerEntities);
return _chunkList;
}
public void RegisterNewPreviousChunkTrees(
@@ -556,23 +576,20 @@ internal sealed partial class PvsSystem : EntitySystem
_reusedTrees.Add(chunks[i]);
}
var previousIndices = _previousTrees.Keys.ToArray();
for (var i = 0; i < previousIndices.Length; i++)
foreach (var (index, chunk) in _previousTrees)
{
var index = previousIndices[i];
// ReSharper disable once InconsistentlySynchronizedField
if (_reusedTrees.Contains(index)) continue;
var chunk = _previousTrees[index];
if (chunk.HasValue)
if (_reusedTrees.Contains(index))
continue;
if (chunk != null)
{
_chunkCachePool.Return(chunk.Value.metadata);
_treePool.Return(chunk.Value.tree);
}
if (!chunks.Contains(index))
{
_previousTrees.Remove(index);
}
}
_previousTrees.EnsureCapacity(chunks.Count);
@@ -689,7 +706,7 @@ internal sealed partial class PvsSystem : EntitySystem
}
internal (List<EntityState>? updates, List<NetEntity>? deletions, List<NetEntity>? leftPvs, GameTick fromTick)
CalculateEntityStates(IPlayerSession session,
CalculateEntityStates(ICommonSession session,
GameTick fromTick,
GameTick toTick,
(Dictionary<NetEntity, MetaDataComponent> metadata, RobustTree<NetEntity> tree)?[] chunks,
@@ -697,8 +714,8 @@ internal sealed partial class PvsSystem : EntitySystem
EntityUid[] viewers)
{
DebugTools.Assert(session.Status == SessionStatus.InGame);
var newEntityBudget = _netConfigManager.GetClientCVar(session.ConnectedClient, CVars.NetPVSEntityBudget);
var enteredEntityBudget = _netConfigManager.GetClientCVar(session.ConnectedClient, CVars.NetPVSEntityEnterBudget);
var newEntityBudget = _netConfigManager.GetClientCVar(session.Channel, CVars.NetPVSEntityBudget);
var enteredEntityBudget = _netConfigManager.GetClientCVar(session.Channel, CVars.NetPVSEntityEnterBudget);
var newEntityCount = 0;
var enteredEntityCount = 0;
var sessionData = PlayerData[session];
@@ -875,7 +892,7 @@ internal sealed partial class PvsSystem : EntitySystem
return null;
var tick = _gameTiming.CurTick;
var minSize = Math.Max(0, lastSent.Count - lastSent.Count);
var minSize = Math.Max(0, lastSent.Count - visibleEnts.Count);
var leftView = new List<NetEntity>(minSize);
foreach (var netEntity in lastSent.Keys)
@@ -1300,30 +1317,44 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
return entState;
}
private EntityUid[] GetSessionViewers(ICommonSession session)
private void GetSessionViewers(ICommonSession session, [NotNull] ref EntityUid[]? viewers)
{
if (session.Status != SessionStatus.InGame || session is not IPlayerSession sess)
return Array.Empty<EntityUid>();
if (session.Status != SessionStatus.InGame)
{
viewers = Array.Empty<EntityUid>();
return;
}
// Fast path
if (sess.ViewSubscriptionCount == 0)
if (session.ViewSubscriptions.Count == 0)
{
if (session.AttachedEntity == null)
return Array.Empty<EntityUid>();
{
viewers = Array.Empty<EntityUid>();
return;
}
return new[] { session.AttachedEntity.Value };
Array.Resize(ref viewers, 1);
viewers[0] = session.AttachedEntity.Value;
return;
}
var viewers = new HashSet<EntityUid>();
if (session.AttachedEntity != null)
viewers.Add(session.AttachedEntity.Value);
foreach (var uid in sess.ViewSubscriptions)
int i = 0;
if (session.AttachedEntity is { } local)
{
viewers.Add(uid);
DebugTools.Assert(!session.ViewSubscriptions.Contains(local));
Array.Resize(ref viewers, session.ViewSubscriptions.Count + 1);
viewers[i++] = local;
}
else
{
Array.Resize(ref viewers, session.ViewSubscriptions.Count);
}
return viewers.ToArray();
foreach (var ent in session.ViewSubscriptions)
{
viewers[i++] = ent;
}
}
// Read Safe
@@ -1412,7 +1443,7 @@ Transform last modified: {Transform(uid).LastModifiedTick}");
[ByRefEvent]
public struct ExpandPvsEvent
{
public readonly IPlayerSession Session;
public readonly ICommonSession Session;
/// <summary>
/// List of entities that will get added to this session's PVS set.
@@ -1425,7 +1456,7 @@ public struct ExpandPvsEvent
/// </summary>
public List<EntityUid>? RecursiveEntities;
public ExpandPvsEvent(IPlayerSession session)
public ExpandPvsEvent(ICommonSession session)
{
Session = session;
}

View File

@@ -26,7 +26,7 @@ using Prometheus;
using Robust.Server.Replays;
using Robust.Shared.Console;
using Robust.Shared.Map.Components;
using Robust.Shared.Players;
using Robust.Shared.Player;
namespace Robust.Server.GameStates
{
@@ -37,6 +37,9 @@ namespace Robust.Server.GameStates
// Mapping of net UID of clients -> last known acked state.
private GameTick _lastOldestAck = GameTick.Zero;
private HashSet<int>[] _playerChunks = Array.Empty<HashSet<int>>();
private EntityUid[][] _viewerEntities = Array.Empty<EntityUid[]>();
private PvsSystem _pvs = default!;
[Dependency] private readonly EntityManager _entityManager = default!;
@@ -168,7 +171,7 @@ Oldest acked clients: {string.Join(", ", players)}
/// <inheritdoc />
public void SendGameStateUpdate()
{
var players = _playerManager.ServerSessions.Where(o => o.Status == SessionStatus.InGame).ToArray();
var players = _playerManager.Sessions.Where(o => o.Status == SessionStatus.InGame).ToArray();
// Update entity positions in PVS chunks/collections
// TODO disable processing if culling is disabled? Need to check if toggling PVS breaks anything.
@@ -224,7 +227,7 @@ Oldest acked clients: {string.Join(", ", players)}
_pvs.CullDeletionHistory(oldestAck);
}
private GameTick SendStates(IPlayerSession[] players, PvsData? pvsData)
private GameTick SendStates(ICommonSession[] players, PvsData? pvsData)
{
var inputSystem = _systemManager.GetEntitySystem<InputSystem>();
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
@@ -265,9 +268,9 @@ Oldest acked clients: {string.Join(", ", players)}
public (Dictionary<NetEntity, MetaDataComponent> metadata, RobustTree<NetEntity> tree)?[] ChunkCache;
}
private PvsData? GetPVSData(IPlayerSession[] players)
private PvsData? GetPVSData(ICommonSession[] players)
{
var (chunks, playerChunks, viewerEntities) = _pvs.GetChunks(players);
var chunks= _pvs.GetChunks(players, ref _playerChunks, ref _viewerEntities);
const int ChunkBatchSize = 2;
var chunksCount = chunks.Count;
var chunkBatches = (int)MathF.Ceiling((float)chunksCount / ChunkBatchSize);
@@ -310,8 +313,8 @@ Oldest acked clients: {string.Join(", ", players)}
ArrayPool<bool>.Shared.Return(reuse);
return new PvsData()
{
PlayerChunks = playerChunks,
ViewerEntities = viewerEntities,
PlayerChunks = _playerChunks,
ViewerEntities = _viewerEntities,
ChunkCache = chunkCache,
};
}
@@ -319,11 +322,11 @@ Oldest acked clients: {string.Join(", ", players)}
private void SendStateUpdate(int i,
PvsThreadResources resources,
InputSystem inputSystem,
IPlayerSession session,
ICommonSession session,
PvsData? pvsData,
ref uint oldestAckValue)
{
var channel = session.ConnectedClient;
var channel = session.Channel;
var sessionData = _pvs.PlayerData[session];
var lastAck = sessionData.LastReceivedAck;
List<NetEntity>? leftPvs = null;
@@ -347,7 +350,7 @@ Oldest acked clients: {string.Join(", ", players)}
(entStates, deletions, fromTick) = _pvs.GetAllEntityStates(session, lastAck, _gameTiming.CurTick);
}
var playerStates = _playerManager.GetPlayerStates(lastAck);
var playerStates = _playerManager.GetPlayerStates(fromTick);
// lastAck varies with each client based on lag and such, we can't just make 1 global state and send it to everyone
var lastInputCommand = inputSystem.GetLastInputCommand(session);
@@ -368,11 +371,25 @@ Oldest acked clients: {string.Join(", ", players)}
stateUpdateMessage.State = state;
stateUpdateMessage.CompressionContext = resources.CompressionContext;
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
// If the state is too big we let Lidgren send it reliably. This is to avoid a situation where a state is so
// large that it (or part of it) consistently gets dropped. When we send reliably, we immediately update the
// ack so that the next state will not also be huge.
//
// We also do this if the client's last ack is too old. This helps prevent things like the entity deletion
// history from becoming too bloated if a bad client fails to send acks for whatever reason.
if (_gameTiming.CurTick.Value > lastAck.Value + _pvs.ForceAckThreshold)
{
stateUpdateMessage.ForceSendReliably = true;
#if FULL_RELEASE
var connectedTime = (DateTime.UtcNow - session.ConnectedTime).TotalMinutes;
if (lastAck > GameTick.Zero && connectedTime > 1)
_logger.Warning($"Client {session} exceeded ack-tick threshold. Last ack: {lastAck}. Cur tick: {_gameTiming.CurTick}. Connect time: {connectedTime} minutes");
#endif
}
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
if (stateUpdateMessage.ShouldSendReliably())
{
sessionData.LastReceivedAck = _gameTiming.CurTick;

View File

@@ -12,6 +12,7 @@ using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
using Robust.Shared.Utility;
namespace Robust.Server.Maps;
@@ -93,6 +94,7 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
IDependencyCollection dependencies, bool alwaysWrite = false,
ISerializationContext? context = null)
{
DebugTools.Assert(value.FilledTiles > 0, "Attempting to write an empty chunk");
var root = new MappingDataNode();
var ind = new ValueDataNode($"{value.X},{value.Y}");
root.Add("ind", ind);

View File

@@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Robust.Server.Console;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
@@ -15,7 +14,7 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Players;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -92,9 +91,7 @@ namespace Robust.Server.Physics
private void OnDebugRequest(RequestGridNodesMessage msg, EntitySessionEventArgs args)
{
var pSession = (PlayerSession) args.SenderSession;
if (!_conGroup.CanCommand(pSession, ShowGridNodesCommand)) return;
if (!_conGroup.CanCommand(args.SenderSession, ShowGridNodesCommand)) return;
AddDebugSubscriber(args.SenderSession);
}

View File

@@ -15,6 +15,7 @@ using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Placement;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Robust.Server.Placement
@@ -30,6 +31,10 @@ namespace Robust.Server.Placement
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private EntityLookupSystem _lookup => _entityManager.System<EntityLookupSystem>();
private SharedMapSystem _maps => _entityManager.System<SharedMapSystem>();
private SharedTransformSystem _xformSystem => _entityManager.System<SharedTransformSystem>();
//TO-DO: Expand for multiple permission per mob?
// Add support for multi-use placeables (tiles etc.).
public List<PlacementInformation> BuildPermissions { get; set; } = new();
@@ -44,6 +49,7 @@ namespace Robust.Server.Placement
public void Initialize()
{
// Someday PlacementManagerSystem my beloved.
_sawmill = _logManager.GetSawmill("placement");
_networkManager.RegisterNetMessage<MsgPlacement>(HandleNetMessage);
@@ -142,7 +148,7 @@ namespace Robust.Server.Placement
if (_entityManager.TryGetComponent<MapGridComponent>(gridUid, out var grid))
{
var replacementQuery = _entityManager.GetEntityQuery<PlacementReplacementComponent>();
var anc = grid.GetAnchoredEntitiesEnumerator(grid.LocalToTile(coordinates));
var anc = _maps.GetAnchoredEntitiesEnumerator(gridUid.Value, grid, _maps.LocalToTile(gridUid.Value, grid, coordinates));
var toDelete = new ValueList<EntityUid>();
while (anc.MoveNext(out var ent))
@@ -185,14 +191,11 @@ namespace Robust.Server.Placement
MapGridComponent? grid;
_mapManager.TryGetGrid(coordinates.EntityId, out grid);
if (grid == null)
_mapManager.TryFindGridAt(coordinates.ToMap(_entityManager), out _, out grid);
if (grid != null) // stick to existing grid
EntityUid gridId = coordinates.EntityId;
if (_entityManager.TryGetComponent(coordinates.EntityId, out grid)
|| _mapManager.TryFindGridAt(coordinates.ToMap(_entityManager, _xformSystem), out gridId, out grid))
{
grid.SetTile(coordinates, new Tile(tileType));
_maps.SetTile(gridId, grid, coordinates, new Tile(tileType));
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
@@ -201,9 +204,9 @@ namespace Robust.Server.Placement
{
var newGrid = _mapManager.CreateGridEntity(coordinates.GetMapId(_entityManager));
var newGridXform = _entityManager.GetComponent<TransformComponent>(newGrid);
newGridXform.WorldPosition = coordinates.Position - newGrid.Comp.TileSizeHalfVector; // assume bottom left tile origin
_xformSystem.SetWorldPosition(newGridXform, coordinates.Position - newGrid.Comp.TileSizeHalfVector); // assume bottom left tile origin
var tilePos = newGrid.Comp.WorldToTile(coordinates.Position);
newGrid.Comp.SetTile(tilePos, new Tile(tileType));
_maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType));
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
@@ -227,11 +230,16 @@ namespace Robust.Server.Placement
{
EntityCoordinates start = _entityManager.GetCoordinates(msg.NetCoordinates);
Vector2 rectSize = msg.RectSize;
foreach (EntityUid entity in EntitySystem.Get<EntityLookupSystem>().GetEntitiesIntersecting(start.GetMapId(_entityManager),
foreach (var entity in _lookup.GetEntitiesIntersecting(start.GetMapId(_entityManager),
new Box2(start.Position, start.Position + rectSize)))
{
if (_entityManager.Deleted(entity) || _entityManager.HasComponent<MapGridComponent>(entity) || _entityManager.HasComponent<ActorComponent>(entity))
if (_entityManager.Deleted(entity) ||
_entityManager.HasComponent<MapGridComponent>(entity) ||
_entityManager.HasComponent<ActorComponent>(entity))
{
continue;
}
var placementEraseEvent = new PlacementEntityEvent(entity, _entityManager.GetComponent<TransformComponent>(entity).Coordinates, PlacementEventAction.Erase, msg.MsgChannel.UserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
_entityManager.DeleteEntity(entity);
@@ -243,19 +251,19 @@ namespace Robust.Server.Placement
/// </summary>
public void SendPlacementBegin(EntityUid mob, int range, string objectType, string alignOption)
{
if (!_entityManager.TryGetComponent<ActorComponent?>(mob, out var actor))
if (!_entityManager.TryGetComponent(mob, out ActorComponent? actor))
return;
var playerConnection = actor.PlayerSession.ConnectedClient;
if (playerConnection == null)
return;
var playerConnection = actor.PlayerSession.Channel;
var message = new MsgPlacement();
message.PlaceType = PlacementManagerMessage.StartPlacement;
message.Range = range;
message.IsTile = false;
message.ObjType = objectType;
message.AlignOption = alignOption;
var message = new MsgPlacement
{
PlaceType = PlacementManagerMessage.StartPlacement,
Range = range,
IsTile = false,
ObjType = objectType,
AlignOption = alignOption
};
_networkManager.ServerSendMessage(message, playerConnection);
}
@@ -264,19 +272,19 @@ namespace Robust.Server.Placement
/// </summary>
public void SendPlacementBeginTile(EntityUid mob, int range, string tileType, string alignOption)
{
if (!_entityManager.TryGetComponent<ActorComponent?>(mob, out var actor))
if (!_entityManager.TryGetComponent(mob, out ActorComponent? actor))
return;
var playerConnection = actor.PlayerSession.ConnectedClient;
if (playerConnection == null)
return;
var playerConnection = actor.PlayerSession.Channel;
var message = new MsgPlacement();
message.PlaceType = PlacementManagerMessage.StartPlacement;
message.Range = range;
message.IsTile = true;
message.ObjType = tileType;
message.AlignOption = alignOption;
var message = new MsgPlacement
{
PlaceType = PlacementManagerMessage.StartPlacement,
Range = range,
IsTile = true,
ObjType = tileType,
AlignOption = alignOption
};
_networkManager.ServerSendMessage(message, playerConnection);
}
@@ -285,15 +293,15 @@ namespace Robust.Server.Placement
/// </summary>
public void SendPlacementCancel(EntityUid mob)
{
if (!_entityManager.TryGetComponent<ActorComponent?>(mob, out var actor))
if (!_entityManager.TryGetComponent(mob, out ActorComponent? actor))
return;
var playerConnection = actor.PlayerSession.ConnectedClient;
if (playerConnection == null)
return;
var playerConnection = actor.PlayerSession.Channel;
var message = new MsgPlacement();
message.PlaceType = PlacementManagerMessage.CancelPlacement;
var message = new MsgPlacement
{
PlaceType = PlacementManagerMessage.CancelPlacement
};
_networkManager.ServerSendMessage(message, playerConnection);
}

View File

@@ -1,88 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Enums;
using Robust.Shared.GameStates;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Timing;
namespace Robust.Server.Player
namespace Robust.Server.Player;
/// <summary>
/// Manages each players session when connected to the server.
/// </summary>
public interface IPlayerManager : ISharedPlayerManager
{
/// <summary>
/// Manages each players session when connected to the server.
/// </summary>
public interface IPlayerManager : ISharedPlayerManager
{
BoundKeyMap KeyMap { get; }
/// <summary>
/// Same as the common sessions, but the server version.
/// </summary>
IEnumerable<IPlayerSession> ServerSessions { get; }
/// <summary>
/// Raised when the <see cref="SessionStatus" /> of a <see cref="IPlayerSession" /> is changed.
/// </summary>
event EventHandler<SessionStatusEventArgs> PlayerStatusChanged;
/// <summary>
/// Initializes the manager.
/// </summary>
/// <param name="maxPlayers">Maximum number of players that can connect to this server at one time.</param>
void Initialize(int maxPlayers);
void Shutdown();
bool TryGetSessionByUsername(string username, [NotNullWhen(true)] out IPlayerSession? session);
/// <summary>
/// Returns the client session of the networkId.
/// </summary>
/// <returns></returns>
IPlayerSession GetSessionByUserId(NetUserId index);
IPlayerSession GetSessionByChannel(INetChannel channel);
bool TryGetSessionByChannel(INetChannel channel, [NotNullWhen(true)] out IPlayerSession? session);
bool TryGetSessionById(NetUserId userId, [NotNullWhen(true)] out IPlayerSession? session);
/// <summary>
/// Checks to see if a PlayerIndex is a valid session.
/// </summary>
bool ValidSessionId(NetUserId index);
IPlayerData GetPlayerData(NetUserId userId);
bool TryGetPlayerData(NetUserId userId, [NotNullWhen(true)] out IPlayerData? data);
bool TryGetPlayerDataByUsername(string userName, [NotNullWhen(true)] out IPlayerData? data);
bool HasPlayerData(NetUserId userId);
/// <summary>
/// Tries to get the user ID of the user with the specified username.
/// </summary>
/// <remarks>
/// This only works if this user has already connected once before during this server run.
/// It does still work if the user has since disconnected.
/// </remarks>
bool TryGetUserId(string userName, out NetUserId userId);
IEnumerable<IPlayerData> GetAllPlayerData();
[Obsolete]
void DetachAll();
[Obsolete("Use player Filter or Inline me!")]
List<IPlayerSession> GetPlayersInRange(MapCoordinates worldPos, int range);
[Obsolete("Use player Filter or Inline me!")]
List<IPlayerSession> GetPlayersInRange(EntityCoordinates worldPos, int range);
[Obsolete("Use player Filter or Inline me!")]
List<IPlayerSession> GetPlayersBy(Func<IPlayerSession, bool> predicate);
[Obsolete("Use player Filter or Inline me!")]
List<IPlayerSession> GetAllPlayers();
List<PlayerState>? GetPlayerStates(GameTick fromTick);
}
}
BoundKeyMap KeyMap { get; }
}

View File

@@ -1,77 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
namespace Robust.Server.Player
{
public interface IPlayerSession : ICommonSession
{
DateTime ConnectedTime { get; }
event EventHandler<SessionStatusEventArgs> PlayerStatusChanged;
void JoinGame();
LoginType AuthType { get; }
/// <summary>
/// Attaches this player to an entity.
/// NOTE: The content pack almost certainly has an alternative for this.
/// Do not call this directly for most content code.
/// </summary>
/// <param name="entity">The entity to attach to.</param>
[Obsolete("Use ActorSystem.Attach() instead.")]
void AttachToEntity(EntityUid? entity);
/// <summary>
/// Attaches this player to an entity.
/// NOTE: The content pack almost certainly has an alternative for this.
/// Do not call this directly for most content code.
/// </summary>
/// <param name="uid">The entity to attach to.</param>
[Obsolete("Use ActorSystem.Attach() instead.")]
void AttachToEntity(EntityUid uid);
/// <summary>
/// Detaches this player from an entity.
/// NOTE: The content pack almost certainly has an alternative for this.
/// Do not call this directly for most content code.
/// </summary>
[Obsolete("Use ActorSystem.Detach() instead.")]
void DetachFromEntity();
void OnConnect();
void OnDisconnect();
IReadOnlySet<EntityUid> ViewSubscriptions { get; }
int ViewSubscriptionCount { get; }
/// <summary>
/// Persistent data for this player.
/// </summary>
IPlayerData Data { get; }
/// <summary>
/// Internal method to set <see cref="ICommonSession.AttachedEntity"/> and update the player's status.
/// Do NOT use this unless you know what you're doing, you probably want <see cref="AttachToEntity"/>
/// and <see cref="DetachFromEntity"/> instead.
/// </summary>
internal void SetAttachedEntity(EntityUid? entity);
/// <summary>
/// Internal method to add an entity Uid to <see cref="ViewSubscriptions"/>.
/// Do NOT use this outside of <see cref="ViewSubscriberSystem"/>.
/// </summary>
internal void AddViewSubscription(EntityUid eye);
/// <summary>
/// Internal method to remove an entity Uid from <see cref="ViewSubscriptions"/>.
/// Do NOT use this outside of <see cref="ViewSubscriberSystem"/>.
/// </summary>
internal void RemoveViewSubscription(EntityUid eye);
}
}

View File

@@ -1,24 +0,0 @@
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.ViewVariables;
namespace Robust.Server.Player
{
sealed class PlayerData : IPlayerData
{
public PlayerData(NetUserId userId, string userName)
{
UserId = userId;
UserName = userName;
}
[ViewVariables]
public NetUserId UserId { get; }
[ViewVariables]
public string UserName { get; }
[ViewVariables]
public object? ContentDataUncast { get; set; }
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Prometheus;
using Robust.Server.Configuration;
@@ -12,22 +11,19 @@ using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Server.Player
{
/// <summary>
/// This class will manage connected player sessions.
/// </summary>
public sealed class PlayerManager : IPlayerManager
internal sealed class PlayerManager : SharedPlayerManager, IPlayerManager
{
private static readonly Gauge PlayerCountMetric = Metrics
.CreateGauge("robust_player_count", "Number of players on the server.");
@@ -41,79 +37,13 @@ namespace Robust.Server.Player
public BoundKeyMap KeyMap { get; private set; } = default!;
private GameTick _lastStateUpdate;
private readonly ReaderWriterLockSlim _sessionsLock = new();
/// <summary>
/// Active sessions of connected clients to the server.
/// </summary>
[ViewVariables]
private readonly Dictionary<NetUserId, PlayerSession> _sessions = new();
[ViewVariables]
private readonly Dictionary<NetUserId, PlayerData> _playerData = new();
[ViewVariables]
private readonly Dictionary<string, NetUserId> _userIdMap = new();
/// <inheritdoc />
public IEnumerable<ICommonSession> NetworkedSessions => Sessions;
/// <inheritdoc />
public IEnumerable<ICommonSession> Sessions
{
get
{
_sessionsLock.EnterReadLock();
try
{
return _sessions.Values;
}
finally
{
_sessionsLock.ExitReadLock();
}
}
}
public IEnumerable<IPlayerSession> ServerSessions => Sessions.Cast<IPlayerSession>();
/// <inheritdoc />
[ViewVariables]
public int PlayerCount
{
get
{
_sessionsLock.EnterReadLock();
try
{
return _sessions.Count;
}
finally
{
_sessionsLock.ExitReadLock();
}
}
}
/// <inheritdoc />
[ViewVariables]
public int MaxPlayers { get; private set; } = 32;
public ICommonSession? LocalSession => null;
/// <inheritdoc />
public event EventHandler<SessionStatusEventArgs>? PlayerStatusChanged;
/// <inheritdoc />
public void Initialize(int maxPlayers)
public override void Initialize(int maxPlayers)
{
base.Initialize(maxPlayers);
KeyMap = new BoundKeyMap(_reflectionManager);
KeyMap.PopulateKeyFunctionsMap();
MaxPlayers = maxPlayers;
_network.RegisterNetMessage<MsgPlayerListReq>(HandlePlayerListReq);
_network.RegisterNetMessage<MsgPlayerList>();
_network.RegisterNetMessage<MsgSyncTimeBase>();
@@ -123,8 +53,9 @@ namespace Robust.Server.Player
_network.Disconnect += EndSession;
}
public void Shutdown()
public override void Shutdown()
{
base.Shutdown();
KeyMap = default!;
_network.Connecting -= OnConnecting;
@@ -132,250 +63,6 @@ namespace Robust.Server.Player
_network.Disconnect -= EndSession;
}
public bool TryGetSessionByUsername(string username, [NotNullWhen(true)] out IPlayerSession? session)
{
if (!_userIdMap.TryGetValue(username, out var userId))
{
session = null;
return false;
}
_sessionsLock.EnterReadLock();
try
{
if (_sessions.TryGetValue(userId, out var iSession))
{
session = iSession;
return true;
}
}
finally
{
_sessionsLock.ExitReadLock();
}
session = null;
return false;
}
IPlayerSession IPlayerManager.GetSessionByChannel(INetChannel channel) => GetSessionByChannel(channel);
public bool TryGetSessionByChannel(INetChannel channel, [NotNullWhen(true)] out IPlayerSession? session)
{
_sessionsLock.EnterReadLock();
try
{
// Should only be one session per client. Returns that session, in theory.
if (_sessions.TryGetValue(channel.UserId, out var concrete))
{
session = concrete;
return true;
}
session = null;
return false;
}
finally
{
_sessionsLock.ExitReadLock();
}
}
private PlayerSession GetSessionByChannel(INetChannel channel)
{
_sessionsLock.EnterReadLock();
try
{
// Should only be one session per client. Returns that session, in theory.
return _sessions[channel.UserId];
}
finally
{
_sessionsLock.ExitReadLock();
}
}
/// <inheritdoc />
public IPlayerSession GetSessionByUserId(NetUserId index)
{
_sessionsLock.EnterReadLock();
try
{
return _sessions[index];
}
finally
{
_sessionsLock.ExitReadLock();
}
}
public bool ValidSessionId(NetUserId index)
{
_sessionsLock.EnterReadLock();
try
{
return _sessions.ContainsKey(index);
}
finally
{
_sessionsLock.ExitReadLock();
}
}
public bool TryGetSessionById(NetUserId userId, [NotNullWhen(true)] out IPlayerSession? session)
{
_sessionsLock.EnterReadLock();
try
{
if (_sessions.TryGetValue(userId, out var playerSession))
{
session = playerSession;
return true;
}
}
finally
{
_sessionsLock.ExitReadLock();
}
session = default;
return false;
}
/// <summary>
/// Causes all sessions to switch from the lobby to the the game.
/// </summary>
[Obsolete]
public void SendJoinGameToAll()
{
_sessionsLock.EnterReadLock();
try
{
foreach (var s in _sessions.Values)
{
s.JoinGame();
}
}
finally
{
_sessionsLock.ExitReadLock();
}
}
public bool TryGetUserId(string userName, out NetUserId userId)
{
return _userIdMap.TryGetValue(userName, out userId);
}
public IEnumerable<IPlayerData> GetAllPlayerData()
{
return _playerData.Values;
}
/// <summary>
/// Causes all sessions to detach from their entity.
/// </summary>
[Obsolete]
public void DetachAll()
{
_sessionsLock.EnterReadLock();
try
{
foreach (var s in _sessions.Values)
{
s.DetachFromEntity();
}
}
finally
{
_sessionsLock.ExitReadLock();
}
}
/// <summary>
/// Gets all players inside of a circle.
/// </summary>
/// <param name="worldPos">Position of the circle in world-space.</param>
/// <param name="range">Radius of the circle in world units.</param>
/// <returns></returns>
[Obsolete("Use player Filter or Inline me!")]
public List<IPlayerSession> GetPlayersInRange(MapCoordinates worldPos, int range)
{
return Filter.Empty()
.AddInRange(worldPos, range)
.Recipients
.Cast<IPlayerSession>()
.ToList();
}
/// <summary>
/// Gets all players inside of a circle.
/// </summary>
/// <param name="worldPos">Position of the circle in world-space.</param>
/// <param name="range">Radius of the circle in world units.</param>
/// <returns></returns>
[Obsolete("Use player Filter or Inline me!")]
public List<IPlayerSession> GetPlayersInRange(EntityCoordinates worldPos, int range)
{
return Filter.Empty()
.AddInRange(worldPos.ToMap(_entityManager), range)
.Recipients
.Cast<IPlayerSession>()
.ToList();
}
[Obsolete("Use player Filter or Inline me!")]
public List<IPlayerSession> GetPlayersBy(Func<IPlayerSession, bool> predicate)
{
return Filter.Empty()
.AddWhere((session => predicate((IPlayerSession)session)))
.Recipients
.Cast<IPlayerSession>()
.ToList();
}
/// <summary>
/// Gets all players in the server.
/// </summary>
/// <returns></returns>
[Obsolete("Use player Filter or Inline me!")]
public List<IPlayerSession> GetAllPlayers()
{
return ServerSessions.ToList();
}
/// <summary>
/// Gets all player states in the server.
/// </summary>
/// <param name="fromTick"></param>
/// <returns></returns>
public List<PlayerState>? GetPlayerStates(GameTick fromTick)
{
if (_lastStateUpdate < fromTick)
{
return null;
}
_sessionsLock.EnterReadLock();
try
{
#if FULL_RELEASE
return _sessions.Values
.Select(s => s.PlayerState)
.ToList();
#else
// Integration tests need to clone data before "sending" it to the client. Otherwise they reference the
// same object.
return _sessions.Values
.Select(s => s.PlayerState.Clone())
.ToList();
#endif
}
finally
{
_sessionsLock.ExitReadLock();
}
}
private Task OnConnecting(NetConnectingArgs args)
{
if (PlayerCount >= _baseServer.MaxPlayers)
@@ -393,30 +80,8 @@ namespace Robust.Server.Player
/// <param name="args"></param>
private void NewSession(object? sender, NetChannelArgs args)
{
if (!_playerData.TryGetValue(args.Channel.UserId, out var data))
{
data = new PlayerData(args.Channel.UserId, args.Channel.UserName);
_playerData.Add(args.Channel.UserId, data);
}
_userIdMap[args.Channel.UserName] = args.Channel.UserId;
var session = new PlayerSession(this, args.Channel, data);
session.PlayerStatusChanged += (_, sessionArgs) => OnPlayerStatusChanged(session, sessionArgs.OldStatus, sessionArgs.NewStatus);
_sessionsLock.EnterWriteLock();
try
{
_sessions.Add(args.Channel.UserId, session);
}
finally
{
_sessionsLock.ExitWriteLock();
}
CreateAndAddSession(args.Channel);
PlayerCountMetric.Set(PlayerCount);
// Synchronize base time.
var msgTimeBase = new MsgSyncTimeBase();
(msgTimeBase.Time, msgTimeBase.Tick) = _timing.TimeBase;
@@ -425,11 +90,6 @@ namespace Robust.Server.Player
_cfg.SyncConnectingClient(args.Channel);
}
private void OnPlayerStatusChanged(IPlayerSession session, SessionStatus oldStatus, SessionStatus newStatus)
{
PlayerStatusChanged?.Invoke(this, new SessionStatusEventArgs(session, oldStatus, newStatus));
}
/// <summary>
/// Ends a clients session, and disconnects them.
/// </summary>
@@ -441,20 +101,18 @@ namespace Robust.Server.Player
}
// make sure nothing got messed up during the life of the session
DebugTools.Assert(session.ConnectedClient == args.Channel);
DebugTools.Assert(session.Channel == args.Channel);
//Detach the entity and (don't)delete it.
session.OnDisconnect();
_sessionsLock.EnterWriteLock();
try
SetStatus(session, SessionStatus.Disconnected);
SetAttachedEntity(session, null, out _, true);
var viewSys = EntManager.System<ViewSubscriberSystem>();
foreach (var eye in session.ViewSubscriptions.ToArray())
{
_sessions.Remove(session.UserId);
}
finally
{
_sessionsLock.ExitWriteLock();
viewSys.RemoveViewSubscriber(eye, session);
}
RemoveSession(session.UserId);
PlayerCountMetric.Set(PlayerCount);
Dirty();
}
@@ -469,17 +127,18 @@ namespace Robust.Server.Player
// This is done before the packet is built, so that the client
// can see themselves Connected.
var session = GetSessionByChannel(channel);
session.OnConnect();
session.ConnectedTime = DateTime.UtcNow;
SetStatus(session, SessionStatus.Connected);
var list = new List<PlayerState>();
var list = new List<SessionState>();
foreach (var client in players)
{
var info = new PlayerState
var info = new SessionState
{
UserId = client.UserId,
Name = client.Name,
Status = client.Status,
Ping = client.ConnectedClient.Ping
Ping = client.Channel!.Ping
};
list.Add(info);
}
@@ -489,46 +148,7 @@ namespace Robust.Server.Player
channel.SendMessage(netMsg);
}
public void Dirty()
{
_lastStateUpdate = _timing.CurTick;
}
public IPlayerData GetPlayerData(NetUserId userId)
{
return _playerData[userId];
}
public bool TryGetPlayerData(NetUserId userId, [NotNullWhen(true)] out IPlayerData? data)
{
if (_playerData.TryGetValue(userId, out var playerData))
{
data = playerData;
return true;
}
data = default;
return false;
}
public bool TryGetPlayerDataByUsername(string userName, [NotNullWhen(true)] out IPlayerData? data)
{
if (!_userIdMap.TryGetValue(userName, out var userId))
{
data = null;
return false;
}
// PlayerData is initialized together with the _userIdMap so we can trust that it'll be present.
data = _playerData[userId];
return true;
}
public bool HasPlayerData(NetUserId userId)
{
return _playerData.ContainsKey(userId);
}
public bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session)
{
if (!_entityManager.TryGetComponent(uid, out ActorComponent? actor))
{
@@ -540,18 +160,4 @@ namespace Robust.Server.Player
return true;
}
}
public sealed class SessionStatusEventArgs : EventArgs
{
public SessionStatusEventArgs(IPlayerSession session, SessionStatus oldStatus, SessionStatus newStatus)
{
Session = session;
OldStatus = oldStatus;
NewStatus = newStatus;
}
public IPlayerSession Session { get; }
public SessionStatus OldStatus { get; }
public SessionStatus NewStatus { get; }
}
}

View File

@@ -1,224 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Server.GameObjects;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
namespace Robust.Server.Player
{
/// <summary>
/// This is the session of a connected client.
/// </summary>
internal sealed class PlayerSession : IPlayerSession
{
private readonly PlayerManager _playerManager;
public readonly PlayerState PlayerState;
private readonly HashSet<EntityUid> _viewSubscriptions = new();
public PlayerSession(PlayerManager playerManager, INetChannel client, PlayerData data)
{
_playerManager = playerManager;
UserId = client.UserId;
Name = client.UserName;
_data = data;
PlayerState = new PlayerState
{
UserId = client.UserId,
};
ConnectedClient = client;
UpdatePlayerState();
}
[ViewVariables] public IReadOnlySet<EntityUid> ViewSubscriptions => _viewSubscriptions;
public int ViewSubscriptionCount => _viewSubscriptions.Count;
[ViewVariables] public INetChannel ConnectedClient { get; }
/// <inheritdoc />
[ViewVariables] public EntityUid? AttachedEntity { get; set; }
private SessionStatus _status = SessionStatus.Connecting;
[ViewVariables]
internal string Name { get; set; }
/// <inheritdoc />
string ICommonSession.Name
{
get => this.Name;
set => this.Name = value;
}
[ViewVariables]
internal short Ping
{
get => ConnectedClient.Ping;
set => throw new NotSupportedException();
}
short ICommonSession.Ping
{
get => this.Ping;
set => this.Ping = value;
}
[ViewVariables]
internal SessionStatus Status
{
get => _status;
set
{
if (_status == value)
return;
var old = _status;
_status = value;
UpdatePlayerState();
PlayerStatusChanged?.Invoke(this, new SessionStatusEventArgs(this, old, value));
}
}
/// <inheritdoc />
SessionStatus ICommonSession.Status
{
get => this.Status;
set => this.Status = value;
}
/// <inheritdoc />
public DateTime ConnectedTime { get; private set; }
[ViewVariables(VVAccess.ReadWrite)]
public int VisibilityMask { get; set; } = 1;
/// <inheritdoc />
[ViewVariables]
public NetUserId UserId { get; }
private readonly PlayerData _data;
[ViewVariables] public IPlayerData Data => _data;
/// <inheritdoc />
public event EventHandler<SessionStatusEventArgs>? PlayerStatusChanged;
/// <inheritdoc />
[Obsolete("Use ActorSystem.Attach() instead.")]
public void AttachToEntity(EntityUid? entity)
{
EntitySystem.Get<ActorSystem>().Attach(entity, this);
}
/// <inheritdoc />
[Obsolete("Use ActorSystem.Attach() instead.")]
public void AttachToEntity(EntityUid uid)
{
EntitySystem.Get<ActorSystem>().Attach(uid, this);
}
/// <inheritdoc />
[Obsolete("Use ActorSystem.Detach() instead.")]
public void DetachFromEntity()
{
if (AttachedEntity == null)
return;
if (IoCManager.Resolve<IEntityManager>().Deleted(AttachedEntity!.Value))
{
Logger.Error($"Player \"{this}\" was attached to an entity that was deleted. THIS SHOULD NEVER HAPPEN, BUT DOES.");
// We can't contact ActorSystem because trying to fire an entity event would crash.
// Work around it.
AttachedEntity = null;
UpdatePlayerState();
return;
}
EntitySystem.Get<ActorSystem>().Detach(AttachedEntity.Value);
}
/// <inheritdoc />
public void OnConnect()
{
ConnectedTime = DateTime.UtcNow;
Status = SessionStatus.Connected;
UpdatePlayerState();
}
/// <inheritdoc />
public void OnDisconnect()
{
Status = SessionStatus.Disconnected;
UnsubscribeAllViews();
DetachFromEntity();
UpdatePlayerState();
}
/// <summary>
/// Causes the session to switch from the lobby to the game.
/// </summary>
public void JoinGame()
{
if (ConnectedClient == null || Status == SessionStatus.InGame)
return;
Status = SessionStatus.InGame;
UpdatePlayerState();
}
public LoginType AuthType => ConnectedClient.AuthType;
/// <inheritdoc />
void IPlayerSession.SetAttachedEntity(EntityUid? entity)
{
AttachedEntity = entity;
UpdatePlayerState();
}
void IPlayerSession.AddViewSubscription(EntityUid eye)
{
_viewSubscriptions.Add(eye);
}
void IPlayerSession.RemoveViewSubscription(EntityUid eye)
{
_viewSubscriptions.Remove(eye);
}
private void UnsubscribeAllViews()
{
var viewSubscriberSystem = EntitySystem.Get<ViewSubscriberSystem>();
foreach (var eye in _viewSubscriptions)
{
viewSubscriberSystem.RemoveViewSubscriber(eye, this);
}
}
private void UpdatePlayerState()
{
PlayerState.Status = Status;
PlayerState.Name = Name;
PlayerState.ControlledEntity = IoCManager.Resolve<IEntityManager>().GetNetEntity(AttachedEntity);
_playerManager.Dirty();
}
/// <inheritdoc />
public override string ToString()
{
return Name;
}
}
}

View File

@@ -22,7 +22,7 @@
<PackageReference Include="Microsoft.Extensions.Primitives" Version="6.0.0" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.20348-rc2" PrivateAssets="compile" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="6.0.2" PrivateAssets="compile" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="7.0.0" PrivateAssets="compile" />
<PackageReference Include="SpaceWizards.Sodium" Version="0.2.1" PrivateAssets="compile" />
<PackageReference Include="SharpZstd.Interop" Version="1.5.2-beta2" PrivateAssets="compile" />
<PackageReference Condition="'$(FullRelease)' != 'True'" Include="JetBrains.Profiler.Api" Version="1.2.0" />

View File

@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
@@ -20,6 +20,7 @@ using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
using Robust.Shared.Scripting;
using Robust.Shared.Utility;
@@ -38,7 +39,7 @@ namespace Robust.Server.Scripting
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly ILogManager _logManager = default!;
readonly Dictionary<IPlayerSession, Dictionary<int, ScriptInstance>> _instances =
readonly Dictionary<ICommonSession, Dictionary<int, ScriptInstance>> _instances =
new();
private ISawmill _sawmill = default!;
@@ -294,9 +295,9 @@ namespace Robust.Server.Scripting
loader: TextLoader.From(TextAndVersion.Create(SourceText.From(message.Code), VersionStamp.Create()))
));
var results = await CompletionService
.GetService(document)
.GetCompletionsAsync(document, message.Cursor);
var results = await (CompletionService
.GetService(document)?
.GetCompletionsAsync(document, message.Cursor) ?? Task.FromResult<CompletionList?>(null));
if (results is not null)
{

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