Compare commits

...

141 Commits

Author SHA1 Message Date
Paul
cd50e89aec v0.8.77 2022-03-24 23:51:55 +01:00
metalgearsloth
25b648b929 Reduce PVS allocs a bunch (again (again)) (#2630) 2022-03-24 23:48:37 +01:00
metalgearsloth
1dbbb08250 Reduce server allocs a bunch (#2625) 2022-03-24 23:39:52 +01:00
DrSmugleaf
5432f7311e Fix keyboard focused being released and grabbed again immediately afterwards (#2634) 2022-03-24 15:25:25 +01:00
Zoldorf
e662802b4c Update build-docfx.yml 2022-03-23 17:32:12 -06:00
Vera Aguilera Puerto
bae2c390bb Improve Color.FromHex exception message.
It now includes the invalid hexcode argument.
Something in content kept getting these exceptions on prod and knowing the invalid argument would be very helpful when debugging these in the future.
2022-03-23 16:00:28 +01:00
Leon Friedrich
7976926eaf fix sequence node Except() (#2610)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2022-03-23 23:34:21 +11:00
metalgearsloth
4aee730753 Version: 0.8.76 2022-03-23 20:48:42 +11:00
metalgearsloth
e87c342aa2 inline robusttree pool (#2628) 2022-03-23 20:47:53 +11:00
Paul
d335d74d8f adds maxpoolsize to treepool 2022-03-22 18:02:00 +01:00
metalgearsloth
bc68b0e3ec Version: 0.8.75 2022-03-22 13:57:31 +11:00
metalgearsloth
b1eadbc58f Fix PVS allocation issues (#2623) 2022-03-22 13:55:57 +11:00
Silver
2d3165a16f remove inheritedmembers div 2022-03-21 18:02:35 -06:00
Zoldorf
d99d25de37 Create build-docfx.yml 2022-03-21 17:22:46 -06:00
Zoldorf
be035ab002 :mistake: 2022-03-21 17:22:08 -06:00
Zoldorf
12c8883eea Update build-test.yml 2022-03-21 15:36:41 -06:00
Zoldorf
758c7c1869 Update build-test.yml 2022-03-21 15:28:12 -06:00
Zoldorf
72ab1f5a1a Update build-test.yml 2022-03-21 15:25:40 -06:00
Zoldorf
6b9c1369c0 Update build-test.yml 2022-03-21 15:22:32 -06:00
Zoldorf
fd1f1dc64a Delete build-docfx.yml 2022-03-21 15:12:24 -06:00
Zoldorf
e211368bc4 Update build-docfx.yml 2022-03-21 15:07:41 -06:00
Zoldorf
39307f916d build-docfx.yml 2022-03-21 15:04:24 -06:00
metalgearsloth
7378cc50a1 Version: 0.8.74 2022-03-21 20:09:16 +11:00
Silver
867b9ad932 Adds Docfx to RobustToolbox 2022-03-20 23:08:59 -06:00
Paul Ritter
03064255f6 Makes Robusttree Children nullable to save allocations (#2570) 2022-03-21 03:02:52 +11:00
Pieter-Jan Briers
c9ccd0b873 Fix file dialogs crashing on macOS 2022-03-20 16:47:20 +01:00
metalgearsloth
650eceea7e Remove IoC + GetComp for every tile intersection (#2617) 2022-03-20 16:54:05 +11:00
DrSmugleaf
452ad5a6e5 Version: 0.8.73 2022-03-19 19:41:22 +01:00
Leon Friedrich
7c292c75d4 Add more information to reflection errors. (#2613) 2022-03-20 00:11:42 +11:00
DrSmugleaf
cab4113697 Add myself to CODEOWNERS 2022-03-19 11:27:54 +01:00
metalgearsloth
f1a5de79b5 Change entitylookup bounds to method (#2611) 2022-03-19 16:14:24 +11:00
metalgearsloth
191e80c175 Add EntityQuery support to filters (#2608) 2022-03-19 15:54:13 +11:00
Leon Friedrich
241bce56c8 Add Resolve() to component queries (#2607) 2022-03-19 15:53:39 +11:00
metalgearsloth
b105639b31 More transform ECS methods (#2606) 2022-03-18 10:32:49 +11:00
ElectroJr
419e63ecd5 Version: 0.8.72 2022-03-16 15:32:19 +13:00
metalgearsloth
46e632bf9d Fix effect overlay crash (#2604) 2022-03-15 18:05:13 +11:00
Leon Friedrich
a8871f4ff4 Add an assert to positional audio (#2602) 2022-03-15 14:54:50 +11:00
Paul
65f28c023d also write + bugfix + ratio 2022-03-15 00:43:40 +01:00
Paul
e506f6aba2 error for invalid customtypeserializer 2022-03-15 00:36:05 +01:00
metalgearsloth
93ae74b5a7 Update nuget dependencies (#2581) 2022-03-14 08:57:20 +01:00
Kara D
7b25fa98ee Version: 0.8.71 2022-03-13 19:30:44 -07:00
mirrorcult
e320b7abb4 Merge pull request #2599 from Sanctuary-Station/staging 2022-03-13 19:26:16 -07:00
Acruid
6b9b56ca83 Grid Components Allocate Grids (#2597)
* Deleting a map is now routed through MapComponent deletion.

* Remove map and grid deletions from map networking, just delete the entity if you want to remove the map.

* Moved the chunkSize property of created grids from the networked MapData to the MapGridComponent state.

* Remove unused IMapManager.DefaultMap property.

* Removed MapCreationTick field from MapManager.MapCollection.

* Removed _maps hashset field from MapManager.

* Removed CreatedMaps array from network MapData.

* MapGrid.ParentMapId is now derived from the bound TransformComponent, and isn't required in the MapGrid ctor.
Removed MapGrid.CreatedTick, it can be found on MapGridComponent.CreationTick.

* Remove a bunch of ApplyGameStatePre code duplication.

* Completely refactored CreateGrid.

* Adds AddComponentUninitialized to the ECS system. This allows you to access a component before it is initialized in a using block.

* Use AddComponentUninitialized to allocate a grid after the component is allocated.

* MapLoader now creates the grids after creating the map entities.

* Chunksize and TileSize properties are now actually read out of the map yaml.
TileSize has a public Setter.

* Minor cleanup.

* Moved grid allocation onto the MapGridComponent.

* Final Cleanup.

* Merge Fail.

* Fixed test, grid was getting deleted because it was empty.

* Remove DeletedChunkDatum from grid networking.

* ApplyMapGridState moved from map manager to the MapGridComponent.
2022-03-13 18:28:59 -07:00
Jezithyr
4d19790b3d This should fix the font caching issue? 2022-03-13 16:51:09 -07:00
Jezithyr
2027863d4c Addressing PR Feedback 2022-03-13 15:59:32 -07:00
Vera Aguilera Puerto
4a4444a1ee Adds MemoryContentRoot. (#2590) 2022-03-13 23:52:34 +01:00
Jezithyr
f10316d384 Fixing missing semicolons 2022-03-13 15:23:45 -07:00
Jezithyr
778be40c86 Condensing code 2022-03-13 14:43:24 -07:00
Jesse Rougeau
2046323c71 Update Robust.Client/UserInterface/UserInterfaceManager.cs
More condensing

Co-authored-by: Paul Ritter <ritter.paul1@googlemail.com>
2022-03-13 13:06:02 -07:00
Jesse Rougeau
71bb5cd58e Update Robust.Client/UserInterface/UserInterfaceManager.cs
Condensing sizing logic

Co-authored-by: Paul Ritter <ritter.paul1@googlemail.com>
2022-03-13 13:05:30 -07:00
Jezithyr
71c6e437fa Removing debug and fixing indentation 2022-03-12 20:36:23 -07:00
Jezithyr
26e1474179 Implemented auto UI Scaling :D
- Implemented autoUI scaling, scaling supports down to 520 by 520 pixel screens(Idk why you'd even have this)
- UI scaling can be adjusted by overriding the window root class
2022-03-12 20:31:13 -07:00
Kara D
89a7459fda Update .NET version for build-test workflow
This should hopefully make it catch CS8983
2022-03-12 19:06:23 -07:00
Paul Ritter
a61adf5631 makes budgets clientside & replicated (#2593) 2022-03-13 00:06:40 +11:00
Leon Friedrich
97db1712f3 Fix grid traversal velocities (#2594)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2022-03-12 23:57:30 +11:00
Paul
ea7012d114 version 0.8.70 2022-03-09 20:58:17 +01:00
Moony
d3eaea5697 Add support for tile variants. (#2577) 2022-03-09 20:56:58 +01:00
metalgearsloth
4ca3fbe4bf Version: 0.8.69 2022-03-10 00:29:08 +11:00
Vera Aguilera Puerto
81fea7595a AttachToGridOrMap checks if the grid/map is being terminated/is deleted. (#2592) 2022-03-10 00:27:18 +11:00
metalgearsloth
2e0806cc1f Version: 0.8.68 2022-03-08 15:48:39 +11:00
metalgearsloth
5325650e92 Fix disappearing grids (#2587)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
2022-03-08 12:25:03 +11:00
Acruid
31764d3c2d Mark C# events on MapManager as Obsolete (#2586)
* Obsoleted C# grid events from MapManager.GridCollection. Use the ECS EventBus events instead.

* Obsoleted C# events from MapManager.MapCollection. Use the ECS EventBus events instead.
2022-03-07 15:51:00 -08:00
Leon Friedrich
9df5152610 Decrease error tolerance for angular velocity updates (#2584) 2022-03-06 23:55:56 +11:00
metalgearsloth
c6cd780ab5 Remove funky merge artifact 2022-03-06 18:26:02 +11:00
Vera Aguilera Puerto
023f845c48 SpriteComponent exception specifies failing RSI path, if any. 2022-03-03 21:29:01 +01:00
Vera Aguilera Puerto
5e977da9bc Fix BaseServer.Shutdown bug when not specifying a reason.
If a shutdown was requested before the main loop had been started, it would only work if the reason was non-null. This is because `string? _shutdownReason` essentially acts as a shutdown request flag with its nullability.
2022-03-03 15:41:44 +01:00
metalgearsloth
fa1de04df7 Version: 0.8.67 2022-03-03 21:18:00 +11:00
metalgearsloth
33251222cd EntityLookup as a system (#2573) 2022-03-03 21:17:01 +11:00
Kara D
70c1e8680f Version: 0.8.66 2022-03-01 15:06:19 -07:00
Kara D
0d214769e6 vs2022 fix 2022-03-01 15:05:49 -07:00
Paul
495bbe891a obsoletes serhooks 2022-03-01 21:49:24 +01:00
Rember
5f68f569c3 BaseContainer.Insert() will now reset the entity's rotation after res… (#2576) 2022-03-01 23:23:56 +11:00
Leon Friedrich
52c883cb1f Fix lsmap and lsgrid (#2574) 2022-03-01 20:20:03 +11:00
metalgearsloth
4b20bae597 RobustUnitTest tweaks (#2568) 2022-03-01 13:35:32 +11:00
Leon Friedrich
64ff046ceb loadmap command tweak (#2572)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2022-02-28 21:17:58 +11:00
Kara D
5f2566a97d Version: 0.8.65 2022-02-27 12:59:54 -07:00
Kara D
c3735d1d5e vs2022 moment 2022-02-27 12:59:25 -07:00
metalgearsloth
749e547773 Version: 0.8.64 2022-02-28 00:45:53 +11:00
metalgearsloth
af8a010f43 Optimise PVS cangetcompstate (#2560) 2022-02-28 00:45:16 +11:00
metalgearsloth
a8a29e814f Allow content to override occlusion directions (#2528) 2022-02-27 23:15:45 +11:00
metalgearsloth
c3cb5406f6 Fix culling disabled (#2485)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
2022-02-27 22:59:36 +11:00
mirrorcult
73f26d93ca Merge pull request #2565 from ElectroJr/mapping-actions 2022-02-26 20:06:48 -07:00
metalgearsloth
5e86a99060 Version: 0.8.63 2022-02-27 13:00:41 +11:00
Paul Ritter
ce9070b966 changes pvs chunkorder to use a tree-structure, should fix children being sent without parents (#2562)
Co-authored-by: Paul Ritter <ritter.paul1@gmail.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2022-02-27 13:00:10 +11:00
Vera Aguilera Puerto
aa5bcefaf2 Add System.Threading.Monitor (lock keyword) to sandbox whitelist. 2022-02-26 16:50:41 +01:00
ElectroJr
2df267fc28 Merge remote-tracking branch 'upstream/master' into mapping-actions 2022-02-24 04:13:45 +13:00
ElectroJr
306fcce337 remove todo 2022-02-24 04:05:04 +13:00
ElectroJr
3683b66ef8 Allow SpriteSpecifier to load from entity prototype 2022-02-23 23:37:05 +13:00
Acruid
08a52fb892 MapChunk Cleanup (#2555)
* All IPhysShapes now expose a property to get the local AABB.

* Removed IMapChunk. It's internal, we only have 1 implementation in the engine, no need for abstraction, and removing it helps perf.

* Cleaned up issues in MapChunk file.

* Encapsulate _tiles access inside MapChunk.

* Remove IEnumerable<TileRef> from MapChunk.

* Remove CollidesWithChunk

* Move CalcWorldAABB and RegenerateCollision from MapChunk to MapGrid.
Remove MapChunk.GridId.

* Removed MapChunk.GetAllTiles

* Removed the terrible mocked unit tests.

* Moved the GetTileRef functions from MapChunk to MapGrid.

* Add an event raised on MapChunk when a tile is modified.
Completely remove the IMapGrid dependency from MapChunk.

* Fix bug where you cannot change the tile damage of a tile.
2022-02-21 20:49:30 -08:00
Pieter-Jan Briers
fd3c54b373 Seal some classes. 2022-02-21 14:02:34 +01:00
Pieter-Jan Briers
adee05b009 Make client start with non-seekable streams.
Not marking #2439 as fixed yet due to lack of thorough testing.
2022-02-21 11:23:37 +01:00
Pieter-Jan Briers
a66d40eb19 CVar to force ResourceManager to provide seekability for streams.
To help with #2439
2022-02-21 11:23:37 +01:00
metalgearsloth
d065a96e01 Don't serialize grid fixtures (#2477) 2022-02-21 20:15:30 +11:00
Kara D
ef7709aa78 Version: 0.8.62 2022-02-20 19:34:25 -07:00
Paul Ritter
305b330075 bandaid 2022-02-21 02:30:03 +01:00
Acruid
bec4297ce1 Move Map Pause data from MapManager to MapComponent. (#2543)
* Encapsulated existing _pausedMaps access in MapManager.Pause.
Added more unit tests.

* Moved the MapPaused flag from MapManager.Pause to MapComponent.

* Moved the MapPreInit flag from MapManager.Pause to MapComponent.

* Changed visibility so content can't access the flags directly.

* It's not the code that is wrong, it's the tests that are wrong!

* Removed completely obsolete bookkeeping event.
2022-02-20 13:36:45 -08:00
metalgearsloth
18b21b3d60 Version: 0.8.61 2022-02-20 17:44:49 +11:00
mirrorcult
9b263417b9 Merge pull request #2541 from metalgearsloth/2022-02-14_1 2022-02-19 12:33:46 -07:00
Acruid
13da0a7925 Adds command buffer to client, and input command injection (#2538)
Adds the command buffer to the console.
adds wait <ticks> command to console.
Adds the `incmd` command to inject input commands directly into the simulation, bypassing the frontend.
Removes client side permission check for debug builds.
2022-02-18 19:27:38 -08:00
Kara D
bb28db2412 Version: 0.8.60 2022-02-18 15:50:15 -07:00
mirrorcult
489d150ae0 Merge pull request #2558 from PaulRitter/2022_02_18_real_pvs_hours 2022-02-18 15:49:20 -07:00
Paul Ritter
077dbaf933 yo this slaps 2022-02-18 20:49:03 +01:00
Paul Ritter
26f83ac7a2 bored trainride 2022-02-17 15:08:55 +01:00
metalgearsloth
822009b429 Version: 0.8.59 2022-02-17 18:05:50 +11:00
Leon Friedrich
e07a4e516c Add random vector functions to IRobustRandom (#2551) 2022-02-17 18:05:12 +11:00
metalgearsloth
9ec38b5538 Optimise PVS recursion (#2524) 2022-02-17 18:03:14 +11:00
metalgearsloth
97da770978 More physics ECS (#2529) 2022-02-17 17:52:27 +11:00
metalgearsloth
3acbc8235d Funny struct enumerator for xform kids Part 2 (#2492) 2022-02-17 17:19:08 +11:00
Sam Weaver
a32359d5d4 Only include shadow casting lights in the "max lights per scene" count (#2446)
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2022-02-17 14:24:36 +11:00
Paul
78fd39aee2 could fix a racecondition? maybe? 2022-02-16 23:55:04 +01:00
Kara D
fca5c14c67 Version: 0.8.58 2022-02-16 15:53:10 -07:00
Kara D
f60e3a14ef Fix VS2022 17.1 issue 2022-02-16 15:49:19 -07:00
Leon Friedrich
503a7032f9 Hopefully fix appearance component mispredict reconciliation (#2460) 2022-02-16 23:26:13 +01:00
metalgearsloth
918bbd3f01 Version: 0.8.57 2022-02-17 01:06:34 +11:00
Leon Friedrich
a1441d5051 Properly support sprite layer transforms. (#2533) 2022-02-17 00:06:14 +11:00
Leon Friedrich
58d6189c40 Allow sprite layer data to be set using PrototypeLayerData. (#2442) 2022-02-16 19:37:40 +11:00
Leon Friedrich
58defed1d2 Fix InteractionOutline 2: Electric boogaloo (#2511)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2022-02-16 18:32:40 +11:00
Paul
27a94384d0 v0.8.56 2022-02-15 18:16:07 +01:00
Paul Ritter
4b8f5815db pvs caching (#2547)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2022-02-15 18:14:44 +01:00
metalgearsloth
d830eef435 Version: 0.8.55 2022-02-15 21:16:35 +11:00
metalgearsloth
e7c4bf7341 Don't use worldbounds for PVS (#2473) 2022-02-15 21:07:41 +11:00
ike709
162e646404 Fix a weird issue with config (#2546)
Co-authored-by: ike709 <ike709@github.com>
2022-02-15 20:57:32 +11:00
metalgearsloth
93049fcacf More transform optimisations (#2519) 2022-02-15 20:43:49 +11:00
metalgearsloth
5b90db4beb inline 2022-02-14 18:30:33 +11:00
metalgearsloth
7692ff736b Some more ECS stuff
- Proxy methods on EntityManager
- Transform system stuff
2022-02-14 18:24:52 +11:00
Kara D
f25ad8ece9 Version: 0.8.54 2022-02-13 20:25:00 -07:00
mirrorcult
1aaf3b9250 Merge pull request #2521 from ElectroJr/public-AppearanceSystem 2022-02-13 20:24:23 -07:00
mirrorcult
11f6cff6df Merge pull request #2539 from Acruid/vv_fix_entName 2022-02-13 20:01:53 -07:00
mirrorcult
c2ea57c95a Merge pull request #2503 from Ygg01/linguini-multiline-error-fix 2022-02-13 20:00:47 -07:00
Ygg01
f61ae5da6b Update Linguini and line info in errors 2022-02-14 01:50:11 +01:00
Acruid
8ae9a5f2da Fix a typo where setting the description of an entity in VV sets the name. 2022-02-13 16:35:51 -08:00
Ygg01
3f7e89e006 Merge remote-tracking branch 'origin/master' into linguini-multiline-error-fix 2022-02-13 23:35:28 +01:00
Ygg01
cee4e4d62e Enable Fluent logs for only .ftl files (#2532) 2022-02-12 21:59:08 +11:00
metalgearsloth
14a3783760 Version: 0.8.53 2022-02-12 15:56:21 +11:00
mirrorcult
b4607f7b1f Draw effects below FOV (#2534) 2022-02-12 15:54:50 +11:00
Acruid
5a28c16cae Map Pausing Fixes (#2520) 2022-02-12 15:54:03 +11:00
ElectroJr
9e8bf861ea Merge remote-tracking branch 'upstream/master' into public-AppearanceSystem 2022-02-11 20:43:19 +13:00
ElectroJr
4cfb9210d0 Make AppearanceSystem public 2022-02-09 04:55:31 +13:00
Ygg01
681f77a796 Change newline append 2022-02-04 02:01:52 +01:00
Ygg01
2a7f1cbf48 Ensure that tests work regardless of platform 2022-02-03 04:06:53 +01:00
Ygg01
c4e63cfdc7 Fix multiline errors, add some tests 2022-02-03 01:43:45 +01:00
205 changed files with 7922 additions and 4184 deletions

2
.github/CODEOWNERS vendored
View File

@@ -11,7 +11,7 @@
/Robust.Analyzers @PaulRitter
/Robust.*/GameStates @PaulRitter
/Robust.Shared/Analyzers @PaulRitter
/Robust.*/Serialization @PaulRitter
/Robust.*/Serialization @PaulRitter @DrSmugleaf
/Robust.*/Prototypes @PaulRitter
/Robust.Shared/GameObjects/ComponentDependencies @PaulRitter
/Robust.*/Containers @PaulRitter

34
.github/workflows/build-docfx.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Build & Publish DocFX
on:
schedule:
- cron: "0 0 * * 0"
jobs:
docfx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Install dependencies
run: dotnet restore
- name: Build Project
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- name: Build DocFX
uses: nikeee/docfx-action@v1.0.0
with:
args: Robust.Docfx/docfx.json
- name: Publish Docfx Documentation on GitHub Pages
uses: maxheld83/ghpages@master
env:
BUILD_DIR: Robust.Docfx/_robust-site
GH_PAT: ${{ secrets.GH_PAT }}

View File

@@ -22,10 +22,12 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.100
dotnet-version: 6.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- name: Test Engine
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -v n

2
.gitignore vendored
View File

@@ -76,3 +76,5 @@ MSBuild/Robust.Custom.targets
release/
Robust.Docfx/*-site
Robust.Docfx/api

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

View File

@@ -6,9 +6,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.0.1" />
</ItemGroup>
</Project>

View File

@@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="16.8.0" />
<PackageReference Include="Microsoft.Build.Framework" Version="17.0.0" />
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
<PackageReference Include="Pidgin" Version="2.5.0" />
</ItemGroup>

View File

@@ -5,8 +5,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

View File

@@ -26,7 +26,6 @@ namespace Robust.Client
[Dependency] private readonly IPlayerManager _playMan = default!;
[Dependency] private readonly INetConfigurationManager _configManager = default!;
[Dependency] private readonly IClientEntityManager _entityManager = default!;
[Dependency] private readonly IEntityLookup _entityLookup = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IDiscordRichPresence _discord = default!;
[Dependency] private readonly IGameTiming _timing = default!;
@@ -213,7 +212,6 @@ namespace Robust.Client
{
_entityManager.Startup();
_mapManager.Startup();
_entityLookup.Startup();
_timing.ResetSimTime();
_timing.Paused = false;
@@ -224,7 +222,6 @@ namespace Robust.Client
IoCManager.Resolve<INetConfigurationManager>().FlushMessages();
_gameStates.Reset();
_playMan.Shutdown();
_entityLookup.Shutdown();
_entityManager.Shutdown();
_mapManager.Shutdown();
_discord.ClearPresence();

View File

@@ -47,7 +47,6 @@ namespace Robust.Client
IoCManager.Register<IMapManagerInternal, NetworkedMapManager>();
IoCManager.Register<INetworkedMapManager, NetworkedMapManager>();
IoCManager.Register<IEntityManager, ClientEntityManager>();
IoCManager.Register<IEntityLookup, EntityLookup>();
IoCManager.Register<IReflectionManager, ClientReflectionManager>();
IoCManager.Register<IConsoleHost, ClientConsoleHost>();
IoCManager.Register<IClientConsoleHost, ClientConsoleHost>();
@@ -72,7 +71,6 @@ namespace Robust.Client
IoCManager.Register<IStateManager, StateManager>();
IoCManager.Register<IUserInterfaceManager, UserInterfaceManager>();
IoCManager.Register<IUserInterfaceManagerInternal, UserInterfaceManager>();
IoCManager.Register<IDebugDrawing, DebugDrawing>();
IoCManager.Register<ILightManager, LightManager>();
IoCManager.Register<IDiscordRichPresence, DiscordRichPresence>();
IoCManager.Register<IMidiManager, MidiManager>();

View File

@@ -109,13 +109,13 @@ namespace Robust.Client.Console
if (AvailableCommands.ContainsKey(commandName))
{
var playerManager = IoCManager.Resolve<IPlayerManager>();
#if !DEBUG
if (!_conGroup.CanCommand(commandName) && playerManager.LocalPlayer?.Session.Status > SessionStatus.Connecting)
{
WriteError(null, $"Insufficient perms for command: {commandName}");
return;
}
#endif
var command1 = AvailableCommands[commandName];
args.RemoveAt(0);
var shell = new ConsoleShell(this, null);

View File

@@ -168,7 +168,7 @@ namespace Robust.Client.Console.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mgr = IoCManager.Resolve<IDebugDrawing>();
var mgr = EntitySystem.Get<DebugDrawingSystem>();
mgr.DebugPositions = !mgr.DebugPositions;
}
}
@@ -861,7 +861,7 @@ namespace Robust.Client.Console.Commands
var chunkIndex = grid.LocalToChunkIndices(grid.MapToGrid(mousePos));
var chunk = internalGrid.GetChunk(chunkIndex);
shell.WriteLine($"worldBounds: {chunk.CalcWorldAABB()} localBounds: {chunk.CalcLocalBounds()}");
shell.WriteLine($"worldBounds: {internalGrid.CalcWorldAABB(chunk)} localBounds: {chunk.CachedBounds}");
}
}

View File

@@ -6,17 +6,21 @@ using Robust.Shared.Maths;
namespace Robust.Client.Debugging
{
/// <inheritdoc />
public sealed class DebugDrawing : IDebugDrawing
/// <summary>
/// A collection of visual debug overlays for the client game.
/// </summary>
public sealed class DebugDrawingSystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IEntityLookup _lookup = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
private bool _debugPositions;
/// <inheritdoc />
/// <summary>
/// Toggles the visual overlay of the local origin for each entity on screen.
/// </summary>
public bool DebugPositions
{
get => _debugPositions;
@@ -42,13 +46,13 @@ namespace Robust.Client.Debugging
private sealed class EntityPositionOverlay : Overlay
{
private readonly IEntityLookup _lookup;
private readonly EntityLookupSystem _lookup;
private readonly IEyeManager _eyeManager;
private readonly IEntityManager _entityManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityPositionOverlay(IEntityLookup lookup, IEyeManager eyeManager, IEntityManager entityManager)
public EntityPositionOverlay(EntityLookupSystem lookup, IEyeManager eyeManager, IEntityManager entityManager)
{
_lookup = lookup;
_eyeManager = eyeManager;
@@ -61,13 +65,11 @@ namespace Robust.Client.Debugging
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var viewport = _eyeManager.GetWorldViewport();
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
foreach (var entity in _lookup.GetEntitiesIntersecting(_eyeManager.CurrentMap, viewport))
{
var transform = _entityManager.GetComponent<TransformComponent>(entity);
var center = transform.WorldPosition;
var worldRotation = transform.WorldRotation;
var (center, worldRotation) = xformQuery.GetComponent(entity).GetWorldPositionRotation();
var xLine = worldRotation.RotateVec(Vector2.UnitX);
var yLine = worldRotation.RotateVec(Vector2.UnitY);

View File

@@ -1,13 +0,0 @@
namespace Robust.Client.Debugging
{
/// <summary>
/// A collection of visual debug overlays for the client game.
/// </summary>
public interface IDebugDrawing
{
/// <summary>
/// Toggles the visual overlay of the local origin for each entity on screen.
/// </summary>
bool DebugPositions { get; set; }
}
}

View File

@@ -50,7 +50,6 @@ namespace Robust.Client
[Dependency] private readonly IClientConsoleHost _console = default!;
[Dependency] private readonly ITimerManager _timerManager = default!;
[Dependency] private readonly IClientEntityManager _entityManager = default!;
[Dependency] private readonly IEntityLookup _lookup = default!;
[Dependency] private readonly IPlacementManager _placementManager = default!;
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
[Dependency] private readonly IOverlayManagerInternal _overlayManager = default!;
@@ -455,6 +454,7 @@ namespace Robust.Client
private void Tick(FrameEventArgs frameEventArgs)
{
_modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs);
_console.CommandBufferExecute();
_timerManager.UpdateTimers(frameEventArgs);
_taskManager.ProcessPendingTasks();
@@ -470,7 +470,6 @@ namespace Robust.Client
// The last real tick is the current tick! This way we won't be in "prediction" mode.
_gameTiming.LastRealTick = _gameTiming.CurTick;
_entityManager.TickUpdate(frameEventArgs.DeltaSeconds, noPredictions: false);
_lookup.Update();
}
_modLoader.BroadcastUpdate(ModUpdateLevel.PostEngine, frameEventArgs);
@@ -510,7 +509,7 @@ namespace Robust.Client
logManager.GetSawmill("discord").Level = LogLevel.Warning;
logManager.GetSawmill("net.predict").Level = LogLevel.Info;
logManager.GetSawmill("szr").Level = LogLevel.Info;
logManager.GetSawmill("loc").Level = LogLevel.Error;
logManager.GetSawmill("loc").Level = LogLevel.Warning;
#if DEBUG_ONLY_FCE_INFO
#if DEBUG_ONLY_FCE_LOG
@@ -575,7 +574,6 @@ namespace Robust.Client
_networkManager.Shutdown("Client shutting down");
_midiManager.Shutdown();
IoCManager.Resolve<IEntityLookup>().Shutdown();
_entityManager.Shutdown();
_clyde.Shutdown();
_clydeAudio.Shutdown();

View File

@@ -8,7 +8,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
{
[ComponentReference(typeof(OccluderComponent))]
internal sealed class ClientOccluderComponent : OccluderComponent
public sealed class ClientOccluderComponent : OccluderComponent
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
@@ -71,13 +71,29 @@ namespace Robust.Client.GameObjects
{
Occluding = OccluderDir.None;
if (Deleted || !_entityManager.GetComponent<TransformComponent>(Owner).Anchored)
if (Deleted)
return;
// Content may want to override the default behavior for occlusion.
var xform = _entityManager.GetComponent<TransformComponent>(Owner);
var ev = new OccluderDirectionsEvent
{
Component = xform,
};
_entityManager.EventBus.RaiseLocalEvent(Owner, ref ev);
if (ev.Handled)
{
Occluding = ev.Directions;
return;
}
var grid = _mapManager.GetGrid(_entityManager.GetComponent<TransformComponent>(Owner).GridID);
var position = _entityManager.GetComponent<TransformComponent>(Owner).Coordinates;
if (!xform.Anchored)
return;
var grid = _mapManager.GetGrid(xform.GridID);
var position = xform.Coordinates;
void CheckDir(Direction dir, OccluderDir oclDir)
{
foreach (var neighbor in grid.GetInDir(position, dir))
@@ -90,7 +106,7 @@ namespace Robust.Client.GameObjects
}
}
var angle = _entityManager.GetComponent<TransformComponent>(Owner).LocalRotation;
var angle = xform.LocalRotation;
var dirRolling = angle.GetCardinalDir();
// dirRolling starts at effective south
@@ -105,15 +121,28 @@ namespace Robust.Client.GameObjects
CheckDir(dirRolling, OccluderDir.East);
}
}
[Flags]
internal enum OccluderDir : byte
{
None = 0,
North = 1,
East = 1 << 1,
South = 1 << 2,
West = 1 << 3,
}
[Flags]
public enum OccluderDir : byte
{
None = 0,
North = 1,
East = 1 << 1,
South = 1 << 2,
West = 1 << 3,
}
/// <summary>
/// Raised by occluders when trying to get occlusion directions.
/// </summary>
[ByRefEvent]
public struct OccluderDirectionsEvent
{
public bool Handled = false;
public OccluderDir Directions = OccluderDir.None;
public TransformComponent Component = default!;
public OccluderDirectionsEvent() {}
}
}

View File

@@ -119,7 +119,8 @@ namespace Robust.Client.GameObjects
/// This is useful to allow layer map configs to be defined in prototypes,
/// while still allowing code to create configs if they're absent.
/// </remarks>
void LayerMapReserveBlank(object key);
/// <returns>Index of the new layer.</returns>
int LayerMapReserveBlank(object key);
/// <summary>
/// Adds a layer without texture (thus falling back to the error texture).
@@ -145,8 +146,8 @@ namespace Robust.Client.GameObjects
void RemoveLayer(int layer);
void RemoveLayer(object layerKey);
void LayerSetShader(int layer, ShaderInstance shader);
void LayerSetShader(object layerKey, ShaderInstance shader);
void LayerSetShader(int layer, ShaderInstance shader, string? prototype = null);
void LayerSetShader(object layerKey, ShaderInstance shader, string? prototype = null);
void LayerSetShader(int layer, string shaderName);
void LayerSetShader(object layerKey, string shaderName);
@@ -217,8 +218,8 @@ namespace Robust.Client.GameObjects
int GetLayerDirectionCount(ISpriteLayer layer);
/// <summary>
/// Calculate sprite bounding box in world-space coordinates.
/// Calculate the rotated sprite bounding box in world-space coordinates.
/// </summary>
Box2 CalculateBoundingBox(Vector2 worldPos);
Box2Rotated CalculateRotatedBoundingBox(Vector2 worldPosition, Angle worldRotation, IEye? eye = null);
}
}

View File

@@ -25,8 +25,6 @@ namespace Robust.Client.GameObjects
RSI.State.Direction EffectiveDirection(Angle worldRotation);
Vector2 LocalToLayer(Vector2 localPos);
/// <summary>
/// Layer size in pixels.
/// Don't account layer scale or sprite world transform.
@@ -37,6 +35,6 @@ namespace Robust.Client.GameObjects
/// Calculate layer bounding box in sprite local-space coordinates.
/// </summary>
/// <returns>Bounding box in sprite local-space coordinates.</returns>
Box2 CalculateBoundingBox();
Box2 CalculateBoundingBox(Angle worldAngle);
}
}

View File

@@ -84,10 +84,16 @@ namespace Robust.Client.GameObjects
foreach (var sprite in comp.SpriteTree.QueryAabb(localAABB))
{
var worldPos = _entityManager.GetComponent<TransformComponent>(sprite.Owner).WorldPosition;
var bounds = sprite.CalculateBoundingBox(worldPos);
var (worldPos, worldRot) = _entityManager.GetComponent<TransformComponent>(sprite.Owner).GetWorldPositionRotation();
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot);
// Get scaled down bounds used to indicate the "south" of a sprite.
var localBound = bounds.Box;
var smallLocal = localBound.Scale(0.2f).Translated(-new Vector2(0f, localBound.Extents.Y));
var southIndicator = new Box2Rotated(smallLocal, bounds.Rotation, bounds.Origin);
handle.DrawRect(bounds, Color.Red.WithAlpha(0.2f));
handle.DrawRect(bounds.Scale(0.2f).Translated(-new Vector2(0f, bounds.Extents.Y)), Color.Blue.WithAlpha(0.5f));
handle.DrawRect(southIndicator, Color.Blue.WithAlpha(0.5f));
}
}
}

View File

@@ -20,6 +20,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
using RSIDirection = Robust.Client.Graphics.RSI.State.Direction;
namespace Robust.Client.GameObjects
{
@@ -31,6 +32,8 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IResourceCache resourceCache = default!;
[Dependency] private readonly IPrototypeManager prototypes = default!;
[Dependency] private readonly IEntityManager entities = default!;
[Dependency] private readonly IReflectionManager reflection = default!;
[Dependency] private readonly IEyeManager eyeManager = default!;
[DataField("visible")]
private bool _visible = true;
@@ -72,7 +75,11 @@ namespace Robust.Client.GameObjects
public Vector2 Scale
{
get => scale;
set => scale = value;
set
{
scale = value;
UpdateLocalMatrix();
}
}
[DataField("rotation")]
@@ -83,7 +90,11 @@ namespace Robust.Client.GameObjects
public Angle Rotation
{
get => rotation;
set => rotation = value;
set
{
rotation = value;
UpdateLocalMatrix();
}
}
[DataField("offset")]
@@ -97,12 +108,18 @@ namespace Robust.Client.GameObjects
public Vector2 Offset
{
get => offset;
set => offset = value;
set
{
offset = value;
UpdateLocalMatrix();
}
}
[DataField("color")]
private Color color = Color.White;
public Matrix3 LocalMatrix = Matrix3.Identity;
[Animatable]
[ViewVariables(VVAccess.ReadWrite)]
public Color Color
@@ -134,113 +151,7 @@ namespace Robust.Client.GameObjects
Layers.Clear();
foreach (var layerDatum in value)
{
var anyTextureAttempted = false;
var layer = new Layer(this);
if (!string.IsNullOrWhiteSpace(layerDatum.RsiPath))
{
var path = TextureRoot / layerDatum.RsiPath;
if (IoCManager.Resolve<IResourceCache>().TryGetResource(path, out RSIResource? resource))
{
layer.RSI = resource.RSI;
}
else
{
Logger.ErrorS(LogCategory, "Unable to load layer RSI '{0}'.", path);
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.State))
{
anyTextureAttempted = true;
var theRsi = layer.RSI ?? BaseRSI;
if (theRsi == null)
{
Logger.ErrorS(LogCategory,
"Layer has no RSI to load states from. Cannot use 'state' property. ({0})",
layerDatum.State);
}
else
{
var stateid = new RSI.StateId(layerDatum.State);
layer.State = stateid;
if (theRsi.TryGetState(stateid, out var state))
{
// Always use south because this layer will be cached in the serializer.
layer.AnimationTimeLeft = state.GetDelay(0);
}
else
{
Logger.ErrorS(LogCategory,
$"State '{stateid}' not found in RSI: '{theRsi.Path}'.",
stateid);
}
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.TexturePath))
{
anyTextureAttempted = true;
if (layer.State.IsValid)
{
Logger.ErrorS(LogCategory,
"Cannot specify 'texture' on a layer if it has an RSI state specified."
);
}
else
{
layer.Texture =
IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>(TextureRoot / layerDatum.TexturePath);
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.Shader))
{
if (IoCManager.Resolve<IPrototypeManager>().TryIndex<ShaderPrototype>(layerDatum.Shader, out var prototype))
{
layer.Shader = prototype.Instance();
}
else
{
Logger.ErrorS(LogCategory,
"Shader prototype '{0}' does not exist.",
layerDatum.Shader);
}
}
layer.Color = layerDatum.Color;
layer.Rotation = layerDatum.Rotation;
layer._offset = layerDatum.Offset;
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
layer.Visible = anyTextureAttempted && layerDatum.Visible;
layer.Scale = layerDatum.Scale;
Layers.Add(layer);
if (layerDatum.MapKeys != null)
{
var index = Layers.Count - 1;
foreach (var keyString in layerDatum.MapKeys)
{
object key;
if (IoCManager.Resolve<IReflectionManager>().TryParseEnumReference(keyString, out var @enum))
{
key = @enum;
}
else
{
key = keyString;
}
if (LayerMap.ContainsKey(key))
{
Logger.ErrorS(LogCategory, "Duplicate layer map key definition: {0}", key);
continue;
}
LayerMap.Add(key, index);
}
}
AddLayer(layerDatum);
}
_layerMapShared = true;
@@ -336,11 +247,13 @@ namespace Robust.Client.GameObjects
void ISerializationHooks.AfterDeserialization()
{
IoCManager.InjectDependencies(this);
{
if (!string.IsNullOrWhiteSpace(rsi))
{
var rsiPath = TextureRoot / rsi;
if(IoCManager.Resolve<IResourceCache>().TryGetResource(rsiPath, out RSIResource? resource))
if(resourceCache.TryGetResource(rsiPath, out RSIResource? resource))
{
BaseRSI = resource.RSI;
}
@@ -373,6 +286,8 @@ namespace Robust.Client.GameObjects
LayerMap.Clear();
LayerDatums = layerDatums;
}
UpdateLocalMatrix();
}
/// <summary>
@@ -390,6 +305,7 @@ namespace Robust.Client.GameObjects
offset = other.offset;
rotation = other.rotation;
scale = other.scale;
UpdateLocalMatrix();
drawDepth = other.drawDepth;
_screenLock = other._screenLock;
_overrideDirection = other._overrideDirection;
@@ -415,9 +331,14 @@ namespace Robust.Client.GameObjects
RenderOrder = other.RenderOrder;
}
internal void UpdateLocalMatrix()
{
LocalMatrix = Matrix3.CreateTransform(in offset, in rotation, in scale);
}
public Matrix3 GetLocalMatrix()
{
return Matrix3.CreateTransform(in offset, in rotation, in scale);
return LocalMatrix;
}
/// <inheritdoc />
@@ -462,14 +383,17 @@ namespace Robust.Client.GameObjects
_layerMapShared = false;
}
public void LayerMapReserveBlank(object key)
public int LayerMapReserveBlank(object key)
{
if (LayerMapTryGet(key, out var _))
if (LayerMapTryGet(key, out var index))
{
return;
return index;
}
LayerMapSet(key, AddBlankLayer());
index = AddBlankLayer();
LayerMapSet(key, index);
return index;
}
public int AddBlankLayer(int? newIndex = null)
@@ -478,6 +402,19 @@ namespace Robust.Client.GameObjects
return AddLayer(layer, newIndex);
}
/// <summary>
/// Add a new layer based on some <see cref="PrototypeLayerData"/>.
/// </summary>
public int AddLayer(PrototypeLayerData layerDatum, int? newIndex = null)
{
var layer = new Layer(this);
var index = AddLayer(layer, newIndex);
LayerSetData(index, layerDatum);
return index;
}
public int AddLayer(string texturePath, int? newIndex = null)
{
return AddLayer(new ResourcePath(texturePath), newIndex);
@@ -653,7 +590,144 @@ namespace Robust.Client.GameObjects
RemoveLayer(layer);
}
public void LayerSetShader(int layer, ShaderInstance? shader)
/// <summary>
/// Fills in a layer's values using some <see cref="PrototypeLayerData"/>.
/// </summary>
public void LayerSetData(int index, PrototypeLayerData layerDatum)
{
if (Layers.Count <= index)
{
Logger.ErrorS(LogCategory, "Layer with index '{0}' does not exist, cannot set layer data! Trace:\n{1}",
index, Environment.StackTrace);
return;
}
var layer = Layers[index];
var anyTextureAttempted = false;
if (!string.IsNullOrWhiteSpace(layerDatum.RsiPath))
{
var path = TextureRoot / layerDatum.RsiPath;
if (resourceCache.TryGetResource(path, out RSIResource? resource))
{
layer.RSI = resource.RSI;
}
else
{
Logger.ErrorS(LogCategory, "Unable to load layer RSI '{0}'.", path);
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.State))
{
anyTextureAttempted = true;
var theRsi = layer.RSI ?? BaseRSI;
if (theRsi == null)
{
Logger.ErrorS(LogCategory,
"Layer has no RSI to load states from. Cannot use 'state' property. ({0})",
layerDatum.State);
}
else
{
var stateid = new RSI.StateId(layerDatum.State);
layer.State = stateid;
if (theRsi.TryGetState(stateid, out var state))
{
// Always use south because this layer will be cached in the serializer.
layer.AnimationTimeLeft = state.GetDelay(0);
}
else
{
Logger.ErrorS(LogCategory,
$"State '{stateid}' not found in RSI: '{theRsi.Path}'.",
stateid);
}
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.TexturePath))
{
anyTextureAttempted = true;
if (layer.State.IsValid)
{
Logger.ErrorS(LogCategory,
"Cannot specify 'texture' on a layer if it has an RSI state specified."
);
}
else
{
layer.Texture =
resourceCache.GetResource<TextureResource>(TextureRoot / layerDatum.TexturePath);
}
}
if (!string.IsNullOrWhiteSpace(layerDatum.Shader))
{
if (prototypes.TryIndex<ShaderPrototype>(layerDatum.Shader, out var prototype))
{
layer.ShaderPrototype = layerDatum.Shader;
layer.Shader = prototype.Instance();
}
else
{
Logger.ErrorS(LogCategory,
"Shader prototype '{0}' does not exist.",
layerDatum.Shader);
}
}
if (layerDatum.MapKeys != null)
{
foreach (var keyString in layerDatum.MapKeys)
{
object key;
if (reflection.TryParseEnumReference(keyString, out var @enum))
{
key = @enum;
}
else
{
key = keyString;
}
if (LayerMap.TryGetValue(key, out var mappedIndex))
{
if (mappedIndex != index)
Logger.ErrorS(LogCategory, "Duplicate layer map key definition: {0}", key);
continue;
}
_layerMapEnsurePrivate();
LayerMap[key] = index;
}
}
layer.Color = layerDatum.Color;
layer._rotation = layerDatum.Rotation;
layer._offset = layerDatum.Offset;
layer._scale = layerDatum.Scale;
layer.UpdateLocalMatrix();
// If neither state: nor texture: were provided we assume that they want a blank invisible layer.
layer.Visible = anyTextureAttempted && layerDatum.Visible;
}
public void LayerSetData(object layerKey, PrototypeLayerData data)
{
if (!LayerMapTryGet(layerKey, out var layer))
{
Logger.ErrorS(LogCategory, "Layer with key '{0}' does not exist, cannot set shader! Trace:\n{1}",
layerKey, Environment.StackTrace);
return;
}
LayerSetData(layer, data);
}
public void LayerSetShader(int layer, ShaderInstance? shader, string? prototype = null)
{
if (Layers.Count <= layer)
{
@@ -664,9 +738,10 @@ namespace Robust.Client.GameObjects
var theLayer = Layers[layer];
theLayer.Shader = shader;
theLayer.ShaderPrototype = prototype;
}
public void LayerSetShader(object layerKey, ShaderInstance shader)
public void LayerSetShader(object layerKey, ShaderInstance shader, string? prototype = null)
{
if (!LayerMapTryGet(layerKey, out var layer))
{
@@ -675,7 +750,7 @@ namespace Robust.Client.GameObjects
return;
}
LayerSetShader(layer, shader);
LayerSetShader(layer, shader, prototype);
}
public void LayerSetShader(int layer, string shaderName)
@@ -684,10 +759,12 @@ namespace Robust.Client.GameObjects
{
Logger.ErrorS(LogCategory, "Shader prototype '{0}' does not exist. Trace:\n{1}", shaderName,
Environment.StackTrace);
LayerSetShader(layer, null, null);
return;
}
// This will set the shader to null if it does not exist.
LayerSetShader(layer, prototype?.Instance());
LayerSetShader(layer, prototype.Instance(), shaderName);
}
public void LayerSetShader(object layerKey, string shaderName)
@@ -1148,7 +1225,7 @@ namespace Robust.Client.GameObjects
return;
}
Layers[layer].SetOffset(layerOffset);
Layers[layer].Offset = layerOffset;
}
public void LayerSetOffset(object layerKey, Vector2 layerOffset)
@@ -1230,25 +1307,6 @@ namespace Robust.Client.GameObjects
RenderInternal(drawingHandle, eyeRotation, worldRotation, worldPosition, overrideDir);
}
private void CalcModelMatrix(int numDirs, Angle eyeRotation, Angle worldRotation, Vector2 worldPosition, out Matrix3 modelMatrix)
{
Angle angle;
if (_screenLock)
{
// Negate the eye rotation in the model matrix, so that later when the view matrix is applied the
// sprite will be locked upright to the screen
angle = new Angle(-eyeRotation.Theta);
}
else
{
angle = CalcRectWorldAngle(worldRotation + eyeRotation, numDirs) - eyeRotation;
}
var sWorldRotation = angle;
modelMatrix = Matrix3.CreateTransform(in worldPosition, in sWorldRotation);
}
private void RenderInternal(DrawingHandleWorld drawingHandle, Angle eyeRotation, Angle worldRotation, Vector2 worldPosition, Direction? overrideDirection)
{
// Reduce the angles to fix math shenanigans
@@ -1257,56 +1315,16 @@ namespace Robust.Client.GameObjects
if (worldRotation.Theta < 0)
worldRotation = new Angle(worldRotation.Theta + Math.Tau);
// sprite matrix, WITHOUT offset.
// offset is applied after sprite numDirs snapping/rotation correction
// --> apply at same time as layer offset
var spriteMatrix = Matrix3.CreateTransform(Vector2.Zero, rotation, scale);
// worldRotation + eyeRotation should be the angle of the entity on-screen. If no-rot is enabled this is just set to zero.
// However, at some point later the eye-matrix is applied separately, so we subtract -eye rotation for now:
var entityMatrix = Matrix3.CreateTransform(worldPosition, NoRotation ? -eyeRotation : worldRotation);
Matrix3.Multiply(ref LocalMatrix, ref entityMatrix, out var transform);
var angle = worldRotation + eyeRotation; // angle on-screen. Used to decide the direction of 4/8 directional RSIs
foreach (var layer in Layers)
{
if (!layer.Visible)
{
continue;
}
var numDirs = GetLayerDirectionCount(layer);
var layerRotation = worldRotation + layer.Rotation;
var layerPosition = worldPosition + layerRotation.RotateVec(layer._offset + offset);
CalcModelMatrix(numDirs, eyeRotation, layerRotation, layerPosition, out var modelMatrix);
Matrix3.Multiply(ref spriteMatrix, ref modelMatrix, out var transformMatrix);
drawingHandle.SetTransform(in transformMatrix);
RenderLayer(drawingHandle, layer, eyeRotation, layerRotation, overrideDirection);
}
}
private void RenderLayer(DrawingHandleWorld drawingHandle, Layer layer, Angle eyeRotation, Angle worldRotation, Direction? overrideDirection)
{
var texture = GetRenderTexture(layer, worldRotation + eyeRotation, overrideDirection);
if (layer.Shader != null)
{
drawingHandle.UseShader(layer.Shader);
}
var layerColor = color * layer.Color;
var position = -(Vector2)texture.Size / (2f * EyeManager.PixelsPerMeter);
var textureSize = texture.Size / (float)EyeManager.PixelsPerMeter;
var quad = Box2.FromDimensions(position, textureSize);
// TODO: Implement layer-specific rotation and scale.
// Apply these directly to the box.
// Oh and when you do update Layer.LocalToLayer so content doesn't break.
// handle.Modulate changes the color
// drawingHandle.SetTransform() is set above, turning the quad into local space vertices
drawingHandle.DrawTextureRectRegion(texture, quad, layerColor);
if (layer.Shader != null)
{
drawingHandle.UseShader(null);
layer.Render(drawingHandle, ref transform, angle, overrideDirection);
}
}
@@ -1342,27 +1360,6 @@ namespace Robust.Client.GameObjects
};
}
private Texture GetRenderTexture(Layer layer, Angle worldRotation, Direction? overrideDirection)
{
var texture = layer.Texture;
if (layer.State.IsValid)
{
// Pull texture from RSI state instead.
var rsi = layer.RSI ?? BaseRSI;
if (rsi == null || !rsi.TryGetState(layer.State, out var state))
{
state = GetFallbackState(resourceCache);
}
var layerSpecificDir = layer.EffectiveDirection(state, worldRotation, overrideDirection);
texture = state.GetFrame(layerSpecificDir, layer.AnimationFrame);
}
texture ??= resourceCache.GetFallback<TextureResource>().Texture;
return texture;
}
public void FrameUpdate(float delta)
{
foreach (var t in Layers)
@@ -1417,12 +1414,14 @@ namespace Robust.Client.GameObjects
Visible = thestate.Visible;
DrawDepth = thestate.DrawDepth;
Scale = thestate.Scale;
Rotation = thestate.Rotation;
Offset = thestate.Offset;
scale = thestate.Scale;
rotation = thestate.Rotation;
offset = thestate.Offset;
UpdateLocalMatrix();
Color = thestate.Color;
RenderOrder = thestate.RenderOrder;
if (thestate.BaseRsiPath != null && BaseRSI != null)
{
if (resourceCache.TryGetResource<RSIResource>(TextureRoot / thestate.BaseRsiPath, out var res))
@@ -1438,43 +1437,8 @@ namespace Robust.Client.GameObjects
}
}
// Maybe optimize this to NOT full clear.
Layers.Clear();
for (var i = 0; i < thestate.Layers.Count; i++)
{
var netlayer = thestate.Layers[i];
var layer = new Layer(this)
{
// These are easy so do them here.
Scale = netlayer.Scale,
Rotation = netlayer.Rotation,
Visible = netlayer.Visible,
Color = netlayer.Color
};
Layers.Add(layer);
// Using the public API to handle errors.
// Probably slow as crap.
// Who am I kidding, DEFINITELY.
if (netlayer.Shader != null)
{
LayerSetShader(i, netlayer.Shader);
}
if (netlayer.RsiPath != null)
{
LayerSetRSI(i, netlayer.RsiPath);
}
if (netlayer.TexturePath != null)
{
LayerSetTexture(i, netlayer.TexturePath);
}
else if (netlayer.State != null)
{
LayerSetState(i, netlayer.State);
}
}
// Maybe optimize this to NOT fully clear the layers. (see LayerDatums setter function)
LayerDatums = thestate.Layers;
}
private void QueueUpdateIsInert()
@@ -1553,35 +1517,61 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc/>
public Box2 CalculateBoundingBox(Vector2 worldPos)
public Box2Rotated CalculateRotatedBoundingBox(Vector2 worldPosition, Angle worldRotation, IEye? eye = null)
{
// fast check for empty sprites
if (Layers.Count == 0)
return new Box2(worldPos, worldPos);
if (!Visible || Layers.Count == 0)
{
return new Box2Rotated(new Box2(worldPosition, worldPosition), Angle.Zero, worldPosition);
}
// We need to modify world rotation so that it lies between 0 and 2pi.
// This matters for 4 or 8 directional sprites deciding which quadrant (octant?) they lie in.
// the 0->2pi convention is set by the sprite-rendering code that selects the layers.
// See RenderInternal().
worldRotation = worldRotation.Reduced();
if (worldRotation.Theta < 0)
worldRotation = new Angle(worldRotation.Theta + Math.Tau);
eye ??= eyeManager.CurrentEye;
// Need relative angle on screen for determining the sprite rsi direction.
Angle relativeRotation = NoRotation
? Angle.Zero
: worldRotation + eye.Rotation;
// we need to calculate bounding box taking into account all nested layers
// because layers can have offsets, scale or rotation we need to calculate a new BB
// based on lowest bottomLeft and hightest topRight points from all layers
var box = Layers[0].CalculateBoundingBox();
// because layers can have offsets, scale or rotation, we need to calculate a new BB
// based on lowest bottomLeft and highest topRight points from all layers
var box = Layers[0].CalculateBoundingBox(relativeRotation);
for (int i = 1; i < Layers.Count; i++)
{
var layer = Layers[i];
var layerBB = layer.CalculateBoundingBox();
if (!layer.Visible) continue;
var layerBB = layer.CalculateBoundingBox(relativeRotation);
box = box.Union(layerBB);
}
// apply sprite transformations and calculate sprite bounding box
// we can optimize it a bit, if sprite doesn't have rotation
var spriteBox = box.Scale(Scale);
var spriteHasRotation = !Rotation.EqualsApprox(Angle.Zero);
var spriteBB = spriteHasRotation ?
new Box2Rotated(spriteBox, Rotation).CalcBoundingBox() : spriteBox;
// Next, what we do is take the box2 and apply the sprite's transform, and then the entity's transform. We
// could do this via Matrix3.TransformBox, but that only yields bounding boxes. So instead we manually
// transform our box by the combination of these matrices:
// move it all to world transform system (with sprite offset)
var worldBB = spriteBB.Translated(Offset + worldPos);
return worldBB;
if (Scale != Vector2.One)
box = box.Scale(Scale);
var adjustedOffset = NoRotation
? (-eye.Rotation).RotateVec(Offset)
: worldRotation.RotateVec(Offset);
Vector2 position = adjustedOffset + worldPosition;
Angle finalRotation = NoRotation
? Rotation - eye.Rotation
: Rotation + worldRotation;
return new Box2Rotated(box.Translated(position), finalRotation, position);
}
internal void UpdateBounds()
@@ -1615,10 +1605,11 @@ namespace Robust.Client.GameObjects
Flip = 3,
}
public sealed class Layer : ISpriteLayer
public sealed class Layer : ISpriteLayer, ISerializationHooks
{
[ViewVariables] private readonly SpriteComponent _parent;
[ViewVariables] public string? ShaderPrototype;
[ViewVariables] public ShaderInstance? Shader;
[ViewVariables] public Texture? Texture;
@@ -1628,11 +1619,37 @@ namespace Robust.Client.GameObjects
[ViewVariables] public float AnimationTime;
[ViewVariables] public int AnimationFrame;
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 Scale { get; set; } = Vector2.One;
public Matrix3 LocalMatrix = Matrix3.Identity;
[ViewVariables(VVAccess.ReadWrite)]
public Angle Rotation { get; set; }
public Vector2 Scale
{
get => _scale;
set
{
if (_scale.EqualsApprox(value)) return;
_scale = value;
UpdateLocalMatrix();
_parent.UpdateBounds();
}
}
internal Vector2 _scale = Vector2.One;
[ViewVariables(VVAccess.ReadWrite)]
public Angle Rotation
{
get => _rotation;
set
{
if (_rotation.EqualsApprox(value)) return;
_rotation = value;
UpdateLocalMatrix();
_parent.UpdateBounds();
}
}
internal Angle _rotation = Angle.Zero;
[ViewVariables(VVAccess.ReadWrite)]
public bool Visible = true;
@@ -1652,6 +1669,7 @@ namespace Robust.Client.GameObjects
if (_offset.EqualsApprox(value)) return;
_offset = value;
UpdateLocalMatrix();
_parent.UpdateBounds();
}
}
@@ -1675,6 +1693,7 @@ namespace Robust.Client.GameObjects
if (toClone.Shader != null)
{
Shader = toClone.Shader.Mutable ? toClone.Shader.Duplicate() : toClone.Shader;
ShaderPrototype = toClone.ShaderPrototype;
}
Texture = toClone.Texture;
RSI = toClone.RSI;
@@ -1682,14 +1701,26 @@ namespace Robust.Client.GameObjects
AnimationTimeLeft = toClone.AnimationTimeLeft;
AnimationTime = toClone.AnimationTime;
AnimationFrame = toClone.AnimationFrame;
Scale = toClone.Scale;
Rotation = toClone.Rotation;
_scale = toClone.Scale;
_rotation = toClone.Rotation;
_offset = toClone.Offset;
UpdateLocalMatrix();
Visible = toClone.Visible;
Color = toClone.Color;
DirOffset = toClone.DirOffset;
AutoAnimated = toClone.AutoAnimated;
}
void ISerializationHooks.AfterDeserialization()
{
UpdateLocalMatrix();
}
internal void UpdateLocalMatrix()
{
LocalMatrix = Matrix3.CreateTransform(in _offset, in _rotation, in _scale);
}
RSI? ISpriteLayer.Rsi { get => RSI; set => SetRsi(value); }
RSI.StateId ISpriteLayer.RsiState { get => State; set => SetState(value); }
Texture? ISpriteLayer.Texture { get => Texture; set => SetTexture(value); }
@@ -1701,7 +1732,7 @@ namespace Robust.Client.GameObjects
Color = Color,
Rotation = Rotation,
Scale = Scale,
//todo Shader = Shader,
Shader = ShaderPrototype,
State = State.Name,
Visible = Visible,
RsiPath = RSI?.Path?.ToString(),
@@ -1730,7 +1761,7 @@ namespace Robust.Client.GameObjects
set => SetAutoAnimated(value);
}
public RSI.State.Direction EffectiveDirection(Angle worldRotation)
public RSIDirection EffectiveDirection(Angle worldRotation)
{
if (State == default)
{
@@ -1751,22 +1782,16 @@ namespace Robust.Client.GameObjects
return default;
}
public Vector2 LocalToLayer(Vector2 localPos)
{
// TODO: scale & rotation for layers is currently unimplemented.
return localPos;
}
public RSI.State.Direction EffectiveDirection(RSI.State state, Angle worldRotation,
public RSIDirection EffectiveDirection(RSI.State state, Angle worldRotation,
Direction? overrideDirection)
{
if (state.Directions == RSI.State.DirectionType.Dir1)
{
return RSI.State.Direction.South;
return RSIDirection.South;
}
else
{
RSI.State.Direction dir;
RSIDirection dir;
if (overrideDirection != null)
{
dir = overrideDirection.Value.Convert(state.Directions);
@@ -1849,7 +1874,7 @@ namespace Robust.Client.GameObjects
}
else
{
Logger.ErrorS(LogCategory, "State '{0}' does not exist in set RSI. Trace:\n{1}", State,
Logger.ErrorS(LogCategory, "State '{0}' does not exist in set RSI ({1}). Trace:\n{2}", State, rsi?.Path?.ToString() ?? "null",
Environment.StackTrace);
Texture = null;
}
@@ -1898,11 +1923,6 @@ namespace Robust.Client.GameObjects
_parent.QueueUpdateIsInert();
}
public void SetOffset(Vector2 offset)
{
Offset = offset;
}
/// <inheritdoc/>
public Vector2i PixelSize
{
@@ -1923,10 +1943,121 @@ namespace Robust.Client.GameObjects
}
/// <inheritdoc/>
public Box2 CalculateBoundingBox()
public Box2 CalculateBoundingBox(Angle angle)
{
// TODO: scale & rotation for layers is currently unimplemented.
return Box2.CenteredAround(Offset, PixelSize / EyeManager.PixelsPerMeter);
// Other than some special cases for simple layers, this will basically just apply the matrix obtained
// via GetLayerDrawMatrix() to this layer's bounding box.
var rsiState = GetActualState();
var dir = (rsiState == null || rsiState.Directions == RSI.State.DirectionType.Dir1)
? RSIDirection.South
: angle.ToRsiDirection(rsiState.Directions);
// special case for simple layers. The vast majority of layers are like this.
if (_rotation == Angle.Zero)
{
var textureSize = PixelSize / EyeManager.PixelsPerMeter;
// this switch block is basically an explicit version of the `rsiDirectionMatrix` in `GetLayerDrawMatrix()`.
var box = dir switch
{
// No rotation:
RSIDirection.South or RSIDirection.North => Box2.CenteredAround(Offset, textureSize),
// rotate 90 degrees:
RSIDirection.East or RSIDirection.West => Box2.CenteredAround(Offset, (textureSize.Y, textureSize.X)),
// rotated 45 degrees (any 45 degree rotated rectangle has a square bounding box with sides of length (x+y)/sqrt(2) )
_ => Box2.CenteredAround(Offset, Vector2.One * (textureSize.X + textureSize.Y) / MathF.Sqrt(2))
};
return _scale == Vector2.One ? box : box.Scale(_scale);
}
// Welp we have some non-zero _rotation, so lets just apply the generalized layer transform and get the bounding box from where;
GetLayerDrawMatrix(dir, out var layerDrawMatrix);
return layerDrawMatrix.TransformBox(Box2.CentredAroundZero(PixelSize / EyeManager.PixelsPerMeter));
}
internal RSI.State? GetActualState()
{
if (!State.IsValid)
return null;
// Pull texture from RSI state
var rsi = RSI ?? _parent.BaseRSI;
if (rsi == null || !rsi.TryGetState(State, out var rsiState))
{
rsiState = GetFallbackState(_parent.resourceCache);
}
return rsiState;
}
/// <summary>
/// Given the apparent rotation of an entity on screen (world + eye rotation), get layer's matrix for drawing &
/// relevant RSI direction.
/// </summary>
public void GetLayerDrawMatrix(RSIDirection dir, out Matrix3 layerDrawMatrix)
{
if (_parent.NoRotation)
layerDrawMatrix = LocalMatrix;
else
{
var rsiDirectionMatrix = Matrix3.CreateTransform(Vector2.Zero, -dir.Convert().ToAngle());
Matrix3.Multiply(ref rsiDirectionMatrix, ref LocalMatrix, out layerDrawMatrix);
}
}
internal void Render(DrawingHandleWorld drawingHandle, ref Matrix3 spriteMatrix, Angle angle, Direction? overrideDirection)
{
if (!Visible)
return;
var rsiState = GetActualState();
var dir = (rsiState == null || rsiState.Directions == RSI.State.DirectionType.Dir1)
? RSIDirection.South
: angle.ToRsiDirection(rsiState.Directions);
// Set the drawing transform for this layer
GetLayerDrawMatrix(dir, out var layerMatrix);
Matrix3.Multiply(ref layerMatrix, ref spriteMatrix, out var transformMatrix);
drawingHandle.SetTransform(in transformMatrix);
// The direction used to draw the sprite can differ from the one that the angle would naively suggest,
// due to direction overrides or offsets.
if (overrideDirection != null && rsiState != null)
dir = overrideDirection.Value.Convert(rsiState.Directions);
dir = dir.OffsetRsiDir(DirOffset);
// Get the correct directional texture from the state, and draw it!
var texture = GetRenderTexture(rsiState, dir);
RenderTexture(drawingHandle, texture);
}
private void RenderTexture(DrawingHandleWorld drawingHandle, Texture texture)
{
if (Shader != null)
drawingHandle.UseShader(Shader);
var layerColor = _parent.color * Color;
var position = -(Vector2)texture.Size / (2f * EyeManager.PixelsPerMeter);
var textureSize = texture.Size / (float)EyeManager.PixelsPerMeter;
var quad = Box2.FromDimensions(position, textureSize);
drawingHandle.DrawTextureRectRegion(texture, quad, layerColor);
if (Shader != null)
drawingHandle.UseShader(null);
}
private Texture GetRenderTexture(RSI.State? state, RSIDirection dir)
{
if (state == null)
return Texture ?? _parent.resourceCache.GetFallback<TextureResource>().Texture;
return state.GetFrame(dir, AnimationFrame);
}
}
@@ -2034,6 +2165,7 @@ namespace Robust.Client.GameObjects
return results;
}
[Obsolete("Use SpriteSystem")]
public static IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
var icon = IconComponent.GetPrototypeIcon(prototype, resourceCache);

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
@@ -6,7 +7,7 @@ using Robust.Shared.GameStates;
namespace Robust.Client.GameObjects
{
[UsedImplicitly]
internal sealed class AppearanceSystem : SharedAppearanceSystem
public sealed class AppearanceSystem : SharedAppearanceSystem
{
private readonly Queue<ClientAppearanceComponent> _queuedUpdates = new();
@@ -50,10 +51,33 @@ namespace Robust.Client.GameObjects
if (!stateDiff) return;
component.AppearanceData = actualState.Data;
component.AppearanceData = CloneAppearanceData(actualState.Data);
MarkDirty(component);
}
/// <summary>
/// Take in an appearance data dictionary and attempt to clone it.
/// </summary>
/// <remarks>
/// As some appearance data values are not simple value-type objects, this is not just a shallow clone.
/// </remarks>
private Dictionary<object, object> CloneAppearanceData(Dictionary<object, object> data)
{
Dictionary<object, object> newDict = new(data.Count);
foreach (var (key, value) in data)
{
if (value.GetType().IsValueType)
newDict[key] = value;
else if (value is ICloneable cloneable)
newDict[key] = cloneable.Clone();
else
throw new NotSupportedException("Invalid object in appearance data dictionary. Appearance data must be cloneable");
}
return newDict;
}
public override void MarkDirty(AppearanceComponent component)
{
if (component.AppearanceDirty)
@@ -107,7 +131,7 @@ namespace Robust.Client.GameObjects
[ByRefEvent]
public struct AppearanceChangeEvent
{
public AppearanceComponent Component = default!;
public IReadOnlyDictionary<object, object> AppearanceData = default!;
public AppearanceComponent Component;
public IReadOnlyDictionary<object, object> AppearanceData;
}
}

View File

@@ -168,7 +168,7 @@ namespace Robust.Client.GameObjects
// This container is expecting an entity... but it got parented to some other entity???
// Ah well, the sever should send a new container state that updates expected entities so just ignore it for now.
return;
}
}
RemoveExpectedEntity(message.Entity);
@@ -210,68 +210,101 @@ namespace Robust.Client.GameObjects
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
var pointQuery = EntityManager.GetEntityQuery<PointLightComponent>();
var spriteQuery = EntityManager.GetEntityQuery<SpriteComponent>();
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
foreach (var toUpdate in _updateQueue)
{
if (EntityManager.Deleted(toUpdate))
{
if (Deleted(toUpdate))
continue;
}
UpdateEntityRecursively(toUpdate);
UpdateEntityRecursively(toUpdate, xformQuery, pointQuery, spriteQuery);
}
_updateQueue.Clear();
}
private void UpdateEntityRecursively(EntityUid entity)
private void UpdateEntityRecursively(
EntityUid entity,
EntityQuery<TransformComponent> xformQuery,
EntityQuery<PointLightComponent> pointQuery,
EntityQuery<SpriteComponent> spriteQuery)
{
// TODO: Since we are recursing down,
// we could cache ShowContents data here to speed it up for children.
// Am lazy though.
UpdateEntity(entity);
// Recursively go up parents and containers to see whether both sprites and lights need to be occluded
// Could maybe optimise this more by checking nearest parent that has sprite / light and whether it's container
// occluded but this probably isn't a big perf issue.
var xform = xformQuery.GetComponent(entity);
var parent = xform.ParentUid;
var child = entity;
var spriteOccluded = false;
var lightOccluded = false;
foreach (var child in EntityManager.GetComponent<TransformComponent>(entity).Children)
while (parent.IsValid() && !spriteOccluded && !lightOccluded)
{
UpdateEntityRecursively(child.Owner);
var parentXform = xformQuery.GetComponent(parent);
if (TryComp<ContainerManagerComponent>(parent, out var manager) && manager.TryGetContainer(child, out var container))
{
spriteOccluded = spriteOccluded || !container.ShowContents;
lightOccluded = lightOccluded || container.OccludesLight;
}
child = parent;
parent = parentXform.ParentUid;
}
// Alright so
// This is the CBT bit.
// The issue is we need to go through the children and re-check whether they are or are not contained.
// if they are contained then the occlusion values may need updating for all those children
UpdateEntity(entity, xform, xformQuery, pointQuery, spriteQuery, spriteOccluded, lightOccluded);
}
private void UpdateEntity(EntityUid entity)
private void UpdateEntity(
EntityUid entity,
TransformComponent xform,
EntityQuery<TransformComponent> xformQuery,
EntityQuery<PointLightComponent> pointQuery,
EntityQuery<SpriteComponent> spriteQuery,
bool spriteOccluded,
bool lightOccluded)
{
if (EntityManager.TryGetComponent(entity, out SpriteComponent? sprite))
if (spriteQuery.TryGetComponent(entity, out var sprite))
{
sprite.ContainerOccluded = false;
// We have to recursively scan for containers upwards in case of nested containers.
var tempParent = entity;
while (tempParent.TryGetContainer(out var container))
{
if (!container.ShowContents)
{
sprite.ContainerOccluded = true;
break;
}
tempParent = container.Owner;
}
sprite.ContainerOccluded = spriteOccluded;
}
if (EntityManager.TryGetComponent(entity, out PointLightComponent? light))
if (pointQuery.TryGetComponent(entity, out var light))
{
light.ContainerOccluded = false;
light.ContainerOccluded = lightOccluded;
}
// We have to recursively scan for containers upwards in case of nested containers.
var tempParent = entity;
while (tempParent.TryGetContainer(out var container))
var childEnumerator = xform.ChildEnumerator;
// Try to avoid TryComp if we already know stuff is occluded.
if ((!spriteOccluded || !lightOccluded) && TryComp<ContainerManagerComponent>(entity, out var manager))
{
while (childEnumerator.MoveNext(out var child))
{
if (container.OccludesLight)
// Thank god it's by value and not by ref.
var childSpriteOccluded = spriteOccluded;
var childLightOccluded = lightOccluded;
// We already know either sprite or light is not occluding so need to check container.
if (manager.TryGetContainer(child.Value, out var container))
{
light.ContainerOccluded = true;
break;
childSpriteOccluded = childSpriteOccluded || !container.ShowContents;
childLightOccluded = childLightOccluded || container.OccludesLight;
}
tempParent = container.Owner;
UpdateEntity(child.Value, xformQuery.GetComponent(child.Value), xformQuery, pointQuery, spriteQuery, childSpriteOccluded, childLightOccluded);
}
}
else
{
while (childEnumerator.MoveNext(out var child))
{
UpdateEntity(child.Value, xformQuery.GetComponent(child.Value), xformQuery, pointQuery, spriteQuery, spriteOccluded, lightOccluded);
}
}
}

View File

@@ -25,7 +25,7 @@ namespace Robust.Client.GameObjects
if (_enabled)
{
_lightOverlay = new DebugLightOverlay(
IoCManager.Resolve<IEntityLookup>(),
EntitySystem.Get<EntityLookupSystem>(),
IoCManager.Resolve<IEyeManager>(),
IoCManager.Resolve<IMapManager>(),
Get<RenderingTreeSystem>());
@@ -44,7 +44,7 @@ namespace Robust.Client.GameObjects
private sealed class DebugLightOverlay : Overlay
{
private IEntityLookup _lookup;
private EntityLookupSystem _lookup;
private IEyeManager _eyeManager;
private IMapManager _mapManager;
@@ -52,7 +52,7 @@ namespace Robust.Client.GameObjects
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public DebugLightOverlay(IEntityLookup lookup, IEyeManager eyeManager, IMapManager mapManager, RenderingTreeSystem tree)
public DebugLightOverlay(EntityLookupSystem lookup, IEyeManager eyeManager, IMapManager mapManager, RenderingTreeSystem tree)
{
_lookup = lookup;
_eyeManager = eyeManager;
@@ -72,7 +72,7 @@ namespace Robust.Client.GameObjects
{
foreach (var light in tree.LightTree)
{
var aabb = _lookup.GetWorldAabbFromEntity(light.Owner);
var aabb = _lookup.GetWorldAABB(light.Owner);
if (!aabb.Intersects(worldBounds)) continue;
args.WorldHandle.DrawRect(aabb, Color.Green.WithAlpha(0.1f));

View File

@@ -323,7 +323,7 @@ namespace Robust.Client.GameObjects
{
private readonly IPlayerManager _playerManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
private readonly ShaderInstance _unshadedShader;
private readonly EffectSystem _owner;
@@ -377,6 +377,9 @@ namespace Robust.Client.GameObjects
(attachedXform?.Coordinates ?? effect.Coordinates)
.Offset(effect.AttachedOffset);
// If we've never seen the entity before then can't resolve coordinates.
if (!coordinates.IsValid(_entityManager)) continue;
// ???
var rotation = attachedXform?.WorldRotation ?? _entityManager.GetComponent<TransformComponent>(coordinates.EntityId).WorldRotation;

View File

@@ -0,0 +1,19 @@
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
namespace Robust.Client.GameObjects;
internal class GridRenderingSystem : EntitySystem
{
private readonly IClydeInternal _clyde;
public GridRenderingSystem(IClydeInternal clyde)
{
_clyde = clyde;
}
public override void Initialize()
{
_clyde.RegisterGridEcsEvents();
}
}

View File

@@ -4,11 +4,15 @@ 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.Timing;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
@@ -21,6 +25,8 @@ namespace Robust.Client.GameObjects
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientGameStateManager _stateManager = default!;
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly IPlayerCommandStates _cmdStates = new PlayerCommandStates();
@@ -108,6 +114,43 @@ namespace Robust.Client.GameObjects
public override void Initialize()
{
SubscribeLocalEvent<PlayerAttachSysMessage>(OnAttachedEntityChanged);
_conHost.RegisterCommand("incmd",
"Inserts an input command into the simulation",
"incmd <KeyFunction> <d|u KeyState> <wxPos> <wyPos>",
GenerateInputCommand);
}
public override void Shutdown()
{
base.Shutdown();
_conHost.UnregisterCommand("incmd");
}
private void GenerateInputCommand(IConsoleShell shell, string argstr, string[] args)
{
var localPlayer = _playerManager.LocalPlayer;
if(localPlayer is null)
return;
var pent = localPlayer.ControlledEntity;
if(pent is null)
return;
BoundKeyFunction keyFunction = new BoundKeyFunction(args[0]);
BoundKeyState state = args[1] == "u" ? BoundKeyState.Up: BoundKeyState.Down;
var pxform = Transform(pent.Value);
var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3]));
var coords = EntityCoordinates.FromMap(EntityManager, pent.Value, new MapCoordinates(wPos, pxform.MapID));
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);
var message = new FullInputCmdMessage(_timing.CurTick, _timing.TickFraction, funcId, state,
coords, new ScreenCoordinates(0, 0, default), EntityUid.Invalid);
HandleInputCommand(localPlayer.Session, keyFunction, message);
}
private void OnAttachedEntityChanged(PlayerAttachSysMessage message)

View File

@@ -70,8 +70,9 @@ namespace Robust.Client.GameObjects
UpdatesAfter.Add(typeof(TransformSystem));
UpdatesAfter.Add(typeof(PhysicsSystem));
_mapManager.MapCreated += MapManagerOnMapCreated;
_mapManager.OnGridCreated += MapManagerOnGridCreated;
SubscribeLocalEvent<MapChangedEvent>(MapManagerOnMapCreated);
SubscribeLocalEvent<GridInitializeEvent>(MapManagerOnGridCreated);
// Due to how recursion works, this must be done.
SubscribeLocalEvent<MoveEvent>(AnythingMoved);
@@ -113,12 +114,18 @@ namespace Robust.Client.GameObjects
private void AnythingMoved(ref MoveEvent args)
{
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
var pointQuery = EntityManager.GetEntityQuery<PointLightComponent>();
var spriteQuery = EntityManager.GetEntityQuery<SpriteComponent>();
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
AnythingMovedSubHandler(args.Sender, xforms);
AnythingMovedSubHandler(args.Sender, xformQuery, pointQuery, spriteQuery);
}
private void AnythingMovedSubHandler(EntityUid uid, EntityQuery<TransformComponent> xforms)
private void AnythingMovedSubHandler(
EntityUid uid,
EntityQuery<TransformComponent> xformQuery,
EntityQuery<PointLightComponent> pointQuery,
EntityQuery<SpriteComponent> spriteQuery)
{
// To avoid doing redundant updates (and we don't need to update a grid's children ever)
if (!_checkedChildren.Add(uid) || EntityManager.HasComponent<RenderingTreeComponent>(uid)) return;
@@ -127,17 +134,19 @@ namespace Robust.Client.GameObjects
// WHATEVER YOU DO, DON'T REPLACE THIS WITH SPAMMING EVENTS UNLESS YOU HAVE A GUARANTEE IT WON'T LAG THE GC.
// (Struct-based events ok though)
// Ironically this was lagging the GC lolz
if (EntityManager.TryGetComponent(uid, out SpriteComponent? sprite))
if (spriteQuery.TryGetComponent(uid, out var sprite))
QueueSpriteUpdate(sprite);
if (EntityManager.TryGetComponent(uid, out PointLightComponent? light))
if (pointQuery.TryGetComponent(uid, out var light))
QueueLightUpdate(light);
if (!xforms.TryGetComponent(uid, out var xform)) return;
if (!xformQuery.TryGetComponent(uid, out var xform)) return;
foreach (var child in xform.ChildEntities)
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
AnythingMovedSubHandler(child, xforms);
AnythingMovedSubHandler(child.Value, xformQuery, pointQuery, spriteQuery);
}
}
@@ -212,13 +221,6 @@ namespace Robust.Client.GameObjects
}
#endregion
public override void Shutdown()
{
base.Shutdown();
_mapManager.MapCreated -= MapManagerOnMapCreated;
_mapManager.OnGridCreated -= MapManagerOnGridCreated;
}
private void OnTreeRemove(EntityUid uid, RenderingTreeComponent component, ComponentRemove args)
{
foreach (var sprite in component.SpriteTree)
@@ -235,9 +237,9 @@ namespace Robust.Client.GameObjects
component.LightTree.Clear();
}
private void MapManagerOnMapCreated(object? sender, MapEventArgs e)
private void MapManagerOnMapCreated(MapChangedEvent e)
{
if (e.Map == MapId.Nullspace)
if (e.Destroyed || e.Map == MapId.Nullspace)
{
return;
}
@@ -245,9 +247,9 @@ namespace Robust.Client.GameObjects
EntityManager.EnsureComponent<RenderingTreeComponent>(_mapManager.GetMapEntityId(e.Map));
}
private void MapManagerOnGridCreated(MapId mapId, GridId gridId)
private void MapManagerOnGridCreated(GridInitializeEvent ev)
{
EntityManager.EnsureComponent<RenderingTreeComponent>(_mapManager.GetGrid(gridId).GridEntityId);
EntityManager.EnsureComponent<RenderingTreeComponent>(_mapManager.GetGrid(ev.GridId).GridEntityId);
}
private RenderingTreeComponent? GetRenderTree(EntityUid entity, EntityQuery<TransformComponent> xforms)
@@ -369,13 +371,10 @@ namespace Robust.Client.GameObjects
private Box2 SpriteAabbFunc(in SpriteComponent value)
{
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
var xform = xforms.GetComponent(value.Owner);
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
var bounds = new Box2Rotated(value.CalculateBoundingBox(worldPos), worldRot, worldPos);
var tree = GetRenderTree(value.Owner, xforms);
return tree == null ? bounds.CalcBoundingBox() : xforms.GetComponent(tree.Owner).InvWorldMatrix.TransformBox(bounds);
return SpriteAabbFunc(value, worldPos, worldRot, xforms);
}
private Box2 LightAabbFunc(in PointLightComponent value)
@@ -392,7 +391,7 @@ namespace Robust.Client.GameObjects
private Box2 SpriteAabbFunc(SpriteComponent value, Vector2 worldPos, Angle worldRot, EntityQuery<TransformComponent> xforms)
{
var bounds = new Box2Rotated(value.CalculateBoundingBox(worldPos), worldRot, worldPos);
var bounds = value.CalculateRotatedBoundingBox(worldPos, worldRot);
var tree = GetRenderTree(value.Owner, xforms);
return tree == null ? bounds.CalcBoundingBox() : xforms.GetComponent(tree.Owner).InvWorldMatrix.TransformBox(bounds);

View File

@@ -0,0 +1,84 @@
using System;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects;
public sealed partial class SpriteSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Pure]
public Texture Frame0(SpriteSpecifier specifier)
{
return RsiStateLike(specifier).Default;
}
[Pure]
public IRsiStateLike RsiStateLike(SpriteSpecifier specifier)
{
switch (specifier)
{
case SpriteSpecifier.Texture tex:
return tex.GetTexture(_resourceCache);
case SpriteSpecifier.Rsi rsi:
return GetState(rsi);
case SpriteSpecifier.EntityPrototype prototypeIcon:
if (!_proto.TryIndex<EntityPrototype>(prototypeIcon.EntityPrototypeId, out var prototype))
{
Logger.Error("Failed to load PrototypeIcon {0}", prototypeIcon.EntityPrototypeId);
return SpriteComponent.GetFallbackState(_resourceCache);
}
return SpriteComponent.GetPrototypeIcon(prototype, _resourceCache);
default:
throw new NotSupportedException();
}
}
[Pure]
public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype, IResourceCache resourceCache)
{
var icon = IconComponent.GetPrototypeIcon(prototype, _resourceCache);
if (icon != null) return icon;
if (!prototype.Components.ContainsKey("Sprite"))
{
return SpriteComponent.GetFallbackState(resourceCache);
}
var dummy = Spawn(prototype.ID, MapCoordinates.Nullspace);
var spriteComponent = EnsureComp<SpriteComponent>(dummy);
var result = spriteComponent.Icon ?? SpriteComponent.GetFallbackState(resourceCache);
Del(dummy);
return result;
}
[Pure]
public RSI.State GetState(SpriteSpecifier.Rsi rsiSpecifier)
{
if (_resourceCache.TryGetResource<RSIResource>(
SharedSpriteComponent.TextureRoot / rsiSpecifier.RsiPath,
out var theRsi) &&
theRsi.RSI.TryGetState(rsiSpecifier.RsiState, out var state))
{
return state;
}
Logger.Error("Failed to load RSI {0}", rsiSpecifier.RsiPath);
return SpriteComponent.GetFallbackState(_resourceCache);
}
}

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.GameObjects
/// Updates the layer animation for every visible sprite.
/// </summary>
[UsedImplicitly]
public sealed class SpriteSystem : EntitySystem
public sealed partial class SpriteSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly RenderingTreeSystem _treeSystem = default!;

View File

@@ -45,7 +45,6 @@ namespace Robust.Client.GameStates
[Dependency] private readonly IComponentFactory _compFactory = default!;
[Dependency] private readonly IClientEntityManagerInternal _entities = default!;
[Dependency] private readonly IEntityLookup _lookup = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IClientNetManager _network = default!;
[Dependency] private readonly IBaseClient _client = default!;
@@ -332,8 +331,6 @@ namespace Robust.Client.GameStates
}
_entities.TickUpdate((float) _timing.TickPeriod.TotalSeconds, noPredictions: !IsPredictionEnabled);
_lookup.Update();
}
private void ResetPredictedEntities(GameTick curTick)
@@ -424,7 +421,6 @@ namespace Robust.Client.GameStates
var createdEntities = ApplyEntityStates(curState.EntityStates.Span, curState.EntityDeletions.Span,
nextState != null ? nextState.EntityStates.Span : default);
_players.ApplyPlayerStates(curState.PlayerStates.Value ?? Array.Empty<PlayerState>());
_mapManager.ApplyGameStatePost(curState.MapData);
GameStateApplied?.Invoke(new GameStateAppliedArgs(curState));
return createdEntities;

View File

@@ -15,6 +15,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Vector2 = Robust.Shared.Maths.Vector2;
using Robust.Shared.Utility;
namespace Robust.Client.Graphics.Audio
{
@@ -193,8 +194,10 @@ namespace Robust.Client.Graphics.Audio
{
_didPositionWarning = true;
Logger.WarningS("clyde.oal",
"Attempting to set position on audio source with multiple audio channels! Stream: '{0}'",
"Attempting to set position on audio source with multiple audio channels! Stream: '{0}'. Make sure the audio is MONO, not stereo.",
_sourceStream.Name);
// warning isn't enough, people just ignore it :(
DebugTools.Assert(false, $"Attempting to set position on audio source with multiple audio channels! Stream: '{_sourceStream.Name}'. Make sure the audio is MONO, not stereo.");
}
#endif

View File

@@ -1,7 +1,5 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -18,8 +16,8 @@ namespace Robust.Client.Graphics.Clyde
private readonly Dictionary<GridId, Dictionary<Vector2i, MapChunkData>> _mapChunkData =
new();
private int _verticesPerChunk(IMapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * 4;
private int _indicesPerChunk(IMapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * GetQuadBatchIndexCount();
private int _verticesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * 4;
private int _indicesPerChunk(MapChunk chunk) => chunk.ChunkSize * chunk.ChunkSize * GetQuadBatchIndexCount();
private void _drawGrids(Viewport viewport, Box2Rotated worldBounds, IEye eye)
{
@@ -78,7 +76,7 @@ namespace Robust.Client.Graphics.Clyde
}
}
private void _updateChunkMesh(IMapGrid grid, IMapChunk chunk)
private void _updateChunkMesh(IMapGrid grid, MapChunk chunk)
{
var data = _mapChunkData[grid.Index];
@@ -91,25 +89,41 @@ namespace Robust.Client.Graphics.Clyde
Span<Vertex2D> vertexBuffer = stackalloc Vertex2D[_verticesPerChunk(chunk)];
var i = 0;
foreach (var tile in chunk)
var cSz = grid.ChunkSize;
var cScaled = chunk.Indices * cSz;
for (ushort x = 0; x < cSz; x++)
{
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile.Tile);
if (regionMaybe == null)
for (ushort y = 0; y < cSz; y++)
{
continue;
var tile = chunk.GetTile(x, y);
if (tile.IsEmpty)
continue;
var regionMaybe = _tileDefinitionManager.TileAtlasRegion(tile);
Box2 region;
if (regionMaybe == null || regionMaybe.Length <= tile.Variant)
{
region = _tileDefinitionManager.ErrorTileRegion;
}
else
{
region = regionMaybe[tile.Variant];
}
var gx = x + cScaled.X;
var gy = y + cScaled.Y;
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(gx, gy, region.Left, region.Bottom);
vertexBuffer[vIdx + 1] = new Vertex2D(gx + 1, gy, region.Right, region.Bottom);
vertexBuffer[vIdx + 2] = new Vertex2D(gx + 1, gy + 1, region.Right, region.Top);
vertexBuffer[vIdx + 3] = new Vertex2D(gx, gy + 1, region.Left, region.Top);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort)(i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
i += 1;
}
var region = regionMaybe.Value;
var vIdx = i * 4;
vertexBuffer[vIdx + 0] = new Vertex2D(tile.X, tile.Y, region.Left, region.Bottom);
vertexBuffer[vIdx + 1] = new Vertex2D(tile.X + 1, tile.Y, region.Right, region.Bottom);
vertexBuffer[vIdx + 2] = new Vertex2D(tile.X + 1, tile.Y + 1, region.Right, region.Top);
vertexBuffer[vIdx + 3] = new Vertex2D(tile.X, tile.Y + 1, region.Left, region.Top);
var nIdx = i * GetQuadBatchIndexCount();
var tIdx = (ushort) (i * 4);
QuadBatchIndexWrite(indexBuffer, ref nIdx, tIdx);
i += 1;
}
GL.BindVertexArray(datum.VAO);
@@ -122,7 +136,7 @@ namespace Robust.Client.Graphics.Clyde
datum.TileCount = i;
}
private MapChunkData _initChunkBuffers(IMapGrid grid, IMapChunk chunk)
private MapChunkData _initChunkBuffers(IMapGrid grid, MapChunk chunk)
{
var vao = GenVertexArray();
BindVertexArray(vao);
@@ -159,7 +173,7 @@ namespace Robust.Client.Graphics.Clyde
return datum;
}
private bool _isChunkDirty(IMapGrid grid, IMapChunk chunk)
private bool _isChunkDirty(IMapGrid grid, MapChunk chunk)
{
var data = _mapChunkData[grid.Index];
return !data.TryGetValue(chunk.Indices, out var datum) || datum.Dirty;
@@ -175,7 +189,7 @@ namespace Robust.Client.Graphics.Clyde
// 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(object? sender, GridChangedEventArgs args)
private void _updateOnGridModified(GridModifiedEvent args)
{
foreach (var (pos, _) in args.Modified)
{
@@ -185,21 +199,23 @@ namespace Robust.Client.Graphics.Clyde
}
}
private void _updateTileMapOnUpdate(object? sender, TileChangedEventArgs args)
private void _updateTileMapOnUpdate(TileChangedEvent args)
{
var grid = _mapManager.GetGrid(args.NewTile.GridIndex);
var chunk = grid.GridTileToChunkIndices(new Vector2i(args.NewTile.X, args.NewTile.Y));
_setChunkDirty(grid, chunk);
}
private void _updateOnGridCreated(MapId mapId, GridId gridId)
private void _updateOnGridCreated(GridStartupEvent ev)
{
var gridId = ev.GridId;
Logger.DebugS("grid", $"Adding {gridId} to grid renderer");
_mapChunkData.Add(gridId, new Dictionary<Vector2i, MapChunkData>());
}
private void _updateOnGridRemoved(MapId mapId, GridId gridId)
private void _updateOnGridRemoved(GridRemovalEvent ev)
{
var gridId = ev.GridId;
Logger.DebugS("grid", $"Removing {gridId} from grid renderer");
var data = _mapChunkData[gridId];

View File

@@ -24,11 +24,17 @@ namespace Robust.Client.Graphics.Clyde
{
public ClydeDebugLayers DebugLayers { get; set; }
private readonly RefList<(SpriteComponent sprite, Matrix3 worldMatrix, Angle worldRotation, float yWorldPos)>
private readonly RefList<(SpriteComponent sprite, Vector2 worldPos, Angle worldRotation, Box2 spriteScreenBB)>
_drawingSpriteList
=
new();
// TODO allow this scale to be passed with PostShader as variable
/// <summary>
/// Some shaders that enlarge the final sprite, like emission or highlight effects, need to use a slightly larger render target.
/// </summary>
public static float PostShadeScale = 1.25f;
public void Render()
{
CheckTransferringScreenshots();
@@ -217,9 +223,8 @@ namespace Robust.Client.Graphics.Clyde
RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowEntities, worldAABB, worldBounds);
var screenSize = viewport.Size;
eye.GetViewMatrix(out var eyeMatrix, eye.Scale);
ProcessSpriteEntities(mapId, eyeMatrix, worldBounds, _drawingSpriteList);
ProcessSpriteEntities(mapId, viewport, eye, worldBounds, _drawingSpriteList);
var worldOverlays = new List<Overlay>();
@@ -276,30 +281,12 @@ namespace Robust.Client.Graphics.Clyde
break;
}
var matrix = entry.worldMatrix;
var worldPosition = new Vector2(matrix.R0C2, matrix.R1C2);
RenderTexture? entityPostRenderTarget = null;
Vector2i roundedPos = default;
if (entry.sprite.PostShader != null)
{
// calculate world bounding box
var spriteBB = entry.sprite.CalculateBoundingBox(worldPosition);
var spriteLB = spriteBB.BottomLeft;
var spriteRT = spriteBB.TopRight;
// finally we can calculate screen bounding in pixels
var screenLB = viewport.WorldToLocal(spriteLB);
var screenRT = viewport.WorldToLocal(spriteRT);
// we need to scale RT a for effects like emission or highlight
// scale can be passed with PostShader as variable in future
var postShadeScale = 1.25f;
var screenSpriteSize = (Vector2i) ((screenRT - screenLB) * postShadeScale).Rounded();
// Rotate the vector by the eye angle, otherwise the bounding box will be incorrect
screenSpriteSize = (Vector2i) eye.Rotation.RotateVec(screenSpriteSize).Rounded();
screenSpriteSize.Y = -screenSpriteSize.Y;
// get the size of the sprite on screen, scaled slightly to allow for shaders that increase the final sprite size.
var screenSpriteSize = (Vector2i) (entry.spriteScreenBB.Size * PostShadeScale).Rounded();
// I'm not 100% sure why it works, but without it post-shader
// can be lower or upper by 1px than original sprite depending on sprite rotation or scale
@@ -322,16 +309,14 @@ namespace Robust.Client.Graphics.Clyde
// Calculate viewport so that the entity thinks it's drawing to the same position,
// which is necessary for light application,
// but it's ACTUALLY drawing into the center of the render target.
var spritePos = spriteBB.Center;
var screenPos = viewport.WorldToLocal(spritePos);
var (roundedX, roundedY) = roundedPos = (Vector2i) screenPos;
var flippedPos = new Vector2i(roundedX, screenSize.Y - roundedY);
roundedPos = (Vector2i) entry.spriteScreenBB.Center;
var flippedPos = new Vector2i(roundedPos.X, screenSize.Y - roundedPos.Y);
flippedPos -= entityPostRenderTarget.Size / 2;
_renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize));
}
}
entry.sprite.Render(_renderHandle.DrawingHandleWorld, eye.Rotation, in entry.worldRotation, in worldPosition);
entry.sprite.Render(_renderHandle.DrawingHandleWorld, eye.Rotation, in entry.worldRotation, in entry.worldPos);
if (entry.sprite.PostShader != null && entityPostRenderTarget != null)
{
@@ -369,17 +354,26 @@ namespace Robust.Client.Graphics.Clyde
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void ProcessSpriteEntities(MapId map, Matrix3 eyeMatrix, Box2Rotated worldBounds,
RefList<(SpriteComponent sprite, Matrix3 matrix, Angle worldRot, float yWorldPos)> list)
private void ProcessSpriteEntities(MapId map, Viewport view, IEye eye, Box2Rotated worldBounds,
RefList<(SpriteComponent sprite, Vector2 worldPos, Angle worldRot, Box2 spriteScreenBB)> list)
{
var xforms = _entityManager.GetEntityQuery<TransformComponent>();
// Construct a matrix equivalent for Viewport.WorldToLocal()
eye.GetViewMatrix(out var viewMatrix, view.RenderScale);
var uiProjmatrix = Matrix3.Identity;
uiProjmatrix.R0C0 = EyeManager.PixelsPerMeter;
uiProjmatrix.R1C1 = -EyeManager.PixelsPerMeter;
uiProjmatrix.R0C2 = view.Size.X / 2f;
uiProjmatrix.R1C2 = view.Size.Y / 2f;
var worldToLocal = viewMatrix * uiProjmatrix;
foreach (var comp in _entitySystemManager.GetEntitySystem<RenderingTreeSystem>().GetRenderTrees(map, worldBounds))
{
var bounds = xforms.GetComponent(comp.Owner).InvWorldMatrix.TransformBox(worldBounds);
comp.SpriteTree.QueryAabb(ref list, (
ref RefList<(SpriteComponent sprite, Matrix3 matrix, Angle worldRot, float yWorldPos)> state,
ref RefList<(SpriteComponent sprite, Vector2 worldPos, Angle worldRot, Box2 spriteScreenBB)> state,
in SpriteComponent value) =>
{
var entity = value.Owner;
@@ -387,12 +381,10 @@ namespace Robust.Client.Graphics.Clyde
ref var entry = ref state.AllocAdd();
entry.sprite = value;
Vector2 worldPos;
(worldPos, entry.worldRot, entry.matrix) = transform.GetWorldPositionRotationMatrix();
var eyePos = eyeMatrix.Transform(worldPos);
// Didn't use the bounds from the query as that has to be re-calculated (and is probably more expensive than this).
var bounds = value.CalculateBoundingBox(eyePos);
entry.yWorldPos = eyePos.Y - bounds.Extents.Y;
(entry.worldPos, entry.worldRot) = transform.GetWorldPositionRotation();
var spriteWorldBB = value.CalculateRotatedBoundingBox(entry.worldPos, entry.worldRot, eye);
entry.spriteScreenBB = worldToLocal.TransformBox(spriteWorldBB);
return true;
}, bounds, true);

View File

@@ -514,14 +514,14 @@ namespace Robust.Client.Graphics.Clyde
var enlargedBounds = worldAABB.Enlarged(renderingTreeSystem.MaxLightRadius);
// Use worldbounds for this one as we only care if the light intersects our actual bounds
var state = (this, worldAABB, count: 0);
var state = (this, worldAABB, count: 0, shadowCastingCount: 0);
var xforms = _entityManager.GetEntityQuery<TransformComponent>();
foreach (var comp in renderingTreeSystem.GetRenderTrees(map, enlargedBounds))
{
var bounds = xforms.GetComponent(comp.Owner).InvWorldMatrix.TransformBox(worldBounds);
comp.LightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldAABB, int count) state, in PointLightComponent light) =>
comp.LightTree.QueryAabb(ref state, (ref (Clyde clyde, Box2 worldAABB, int count, int shadowCastingCount) state, in PointLightComponent light) =>
{
if (state.count >= LightsToRenderListSize)
{
@@ -543,6 +543,9 @@ namespace Robust.Client.Graphics.Clyde
return true;
}
// If the light is a shadow casting light, keep a separate track of that
if (light.CastShadows) state.shadowCastingCount++;
float distanceSquared = (state.worldAABB.Center - lightPos).LengthSquared;
state.clyde._lightsToRenderList[state.count++] = (light, lightPos, distanceSquared);
@@ -550,17 +553,29 @@ namespace Robust.Client.Graphics.Clyde
}, bounds);
}
if (state.count > _maxLightsPerScene)
if (state.shadowCastingCount > _maxLightsPerScene)
{
// There are too many lights to fit in the scene.
// There are too many lights casting shadows to fit in the scene.
// This check must occur before occluder expansion, or else bad things happen.
// Sort lights by distance.
// 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;
}));
// 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);
}));
// Then effectively delete the furthest lights.
state.count = _maxLightsPerScene;
// 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.)
state.count -= state.shadowCastingCount - _maxLightsPerScene;
}
// When culling occluders later, we can't just remove any occluders outside the worldBounds.
@@ -846,7 +861,6 @@ namespace Robust.Client.Graphics.Clyde
// 3D geometry used during depth projection.
// 2D mask geometry used to apply wall bleed.
// TODO: This code probably does not work correctly with rotated camera.
// TODO: Yes this function throws and index exception if you reach maxOccluders.
const int maxOccluders = 2048;

View File

@@ -994,9 +994,9 @@ namespace Robust.Client.Graphics.Clyde
private sealed class SpriteDrawingOrderComparer : IComparer<int>
{
private readonly RefList<(SpriteComponent, Matrix3, Angle, float)> _drawList;
private readonly RefList<(SpriteComponent, Vector2, Angle, Box2)> _drawList;
public SpriteDrawingOrderComparer(RefList<(SpriteComponent, Matrix3, Angle, float)> drawList)
public SpriteDrawingOrderComparer(RefList<(SpriteComponent, Vector2, Angle, Box2)> drawList)
{
_drawList = drawList;
}
@@ -1019,7 +1019,8 @@ namespace Robust.Client.Graphics.Clyde
return cmp;
}
cmp = _drawList[y].Item4.CompareTo(_drawList[x].Item4);
// compare the top of the sprite's BB for y-sorting. Because screen coordinates are flipped, the "top" of the BB is actually the "bottom".
cmp = _drawList[x].Item4.Top.CompareTo(_drawList[y].Item4.Top);
if (cmp != 0)
{

View File

@@ -437,6 +437,13 @@ namespace Robust.Client.Graphics.Clyde
_windowing!.WindowSetVisible(reg, visible);
}
public void RunOnWindowThread(Action a)
{
DebugTools.AssertNotNull(_windowing);
_windowing!.RunOnWindowThread(a);
}
private abstract class WindowReg
{
public bool IsDisposed;

View File

@@ -22,7 +22,7 @@ namespace Robust.Client.Graphics.Clyde
/// <summary>
/// Responsible for most things rendering on OpenGL mode.
/// </summary>
internal sealed partial class Clyde : IClydeInternal, IPostInjectInit
internal sealed partial class Clyde : IClydeInternal, IPostInjectInit, IEntityEventSubscriber
{
[Dependency] private readonly IClydeTileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
@@ -134,16 +134,19 @@ namespace Robust.Client.Graphics.Clyde
public void PostInject()
{
_mapManager.TileChanged += _updateTileMapOnUpdate;
_mapManager.OnGridCreated += _updateOnGridCreated;
_mapManager.OnGridRemoved += _updateOnGridRemoved;
_mapManager.GridChanged += _updateOnGridModified;
// This cvar does not modify the actual GL version requested or anything,
// it overrides the version we detect to detect GL features.
RegisterBlockCVars();
}
public void RegisterGridEcsEvents()
{
_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);
}
private void GLInitBindings(bool gles)
{
_glBindingsContext = _glContext!.BindingsContext;

View File

@@ -76,6 +76,11 @@ namespace Robust.Client.Graphics.Clyde
return null;
}
public void RegisterGridEcsEvents()
{
// Nada.
}
public void SetWindowTitle(string title)
{
// Nada.
@@ -246,6 +251,11 @@ namespace Robust.Client.Graphics.Clyde
// Nada.
}
public void RunOnWindowThread(Action action)
{
action();
}
private sealed class DummyCursor : ICursor
{
public void Dispose()

View File

@@ -118,6 +118,10 @@ namespace Robust.Client.Graphics.Clyde
case CmdWinCursorSet cmd:
WinThreadWinCursorSet(cmd);
break;
case CmdRunAction cmd:
cmd.Action();
break;
}
}
@@ -169,6 +173,11 @@ namespace Robust.Client.Graphics.Clyde
}
}
public void RunOnWindowThread(Action action)
{
SendCmd(new CmdRunAction(action));
}
private abstract record CmdBase;
private sealed record CmdTerminate : CmdBase;
@@ -245,6 +254,10 @@ namespace Robust.Client.Graphics.Clyde
private sealed record CmdCursorDestroy(
ClydeHandle Cursor
) : CmdBase;
private sealed record CmdRunAction(
Action Action
) : CmdBase;
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Robust.Client.Input;
using Robust.Shared.Maths;
using SixLabors.ImageSharp;
@@ -58,6 +59,9 @@ namespace Robust.Client.Graphics.Clyde
void GLMakeContextCurrent(WindowReg? reg);
void GLSwapInterval(int interval);
unsafe void* GLGetProcAddress(string procName);
// Misc
void RunOnWindowThread(Action a);
}
}
}

View File

@@ -9,6 +9,7 @@ using Robust.Shared.Utility;
using SharpFont;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using TerraFX.Interop.Windows;
namespace Robust.Client.Graphics
{
@@ -58,6 +59,14 @@ namespace Robust.Client.Graphics
return instance;
}
public void ClearFontCache()
{
foreach (var fontInstance in _loadedInstances)
{
fontInstance.Value.ClearSizeData();
}
}
private ScaledFontData _generateScaledDatum(FontInstanceHandle instance, float scale)
{
var ftFace = instance.FaceHandle.Face;
@@ -246,6 +255,18 @@ namespace Robust.Client.Graphics
FaceHandle = faceHandle;
}
public void ClearSizeData()
{
foreach (var scaleData in _scaledData)
{
foreach (var ownedTexture in scaleData.Value.AtlasTextures)
{
ownedTexture.Dispose();
}
}
_scaledData.Clear();
}
public Texture? GetCharTexture(Rune codePoint, float scale)
{
var glyph = GetGlyph(codePoint);

View File

@@ -61,5 +61,9 @@ namespace Robust.Client.Graphics
/// <returns>Null if not running on X11.</returns>
uint? GetX11WindowId();
void RegisterGridEcsEvents();
void RunOnWindowThread(Action action);
}
}

View File

@@ -5,9 +5,8 @@ namespace Robust.Client.Graphics
{
public interface IFontManager
{
public void ClearFontCache();
}
internal interface IFontManagerInternal : IFontManager
{
IFontFaceHandle Load(Stream stream);

View File

@@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.Utility;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -22,11 +22,20 @@ namespace Robust.Client.Map
public Texture TileTextureAtlas => _tileTextureAtlas ?? Texture.Transparent;
private readonly Dictionary<ushort, Box2> _tileRegions = new();
private readonly Dictionary<ushort, Box2[]> _tileRegions = new();
public Box2? TileAtlasRegion(Tile tile)
public Box2 ErrorTileRegion { get; private set; }
/// <inheritdoc />
public Box2[]? TileAtlasRegion(Tile tile)
{
if (_tileRegions.TryGetValue(tile.TypeId, out var region))
return TileAtlasRegion(tile.TypeId);
}
/// <inheritdoc />
public Box2[]? TileAtlasRegion(ushort tileType)
{
if (_tileRegions.TryGetValue(tileType, out var region))
{
return region;
}
@@ -51,39 +60,79 @@ namespace Robust.Client.Map
const int tileSize = EyeManager.PixelsPerMeter;
var dimensionX = (int) Math.Ceiling(Math.Sqrt(defList.Count));
var dimensionY = (int) Math.Ceiling((float) defList.Count / dimensionX);
var tileCount = defList.Select(x => (int)x.Variants).Sum() + 1;
var sheet = new Image<Rgba32>(dimensionX * tileSize, dimensionY * tileSize);
var dimensionX = (int) Math.Ceiling(Math.Sqrt(tileCount));
var dimensionY = (int) Math.Ceiling((float) tileCount / dimensionX);
for (var i = 0; i < defList.Count; i++)
var imgWidth = dimensionX * tileSize;
var imgHeight = dimensionY * tileSize;
var sheet = new Image<Rgba32>(imgWidth, imgHeight);
// Add in the missing tile texture sprite as tile texture 0.
{
var def = defList[i];
var column = i % dimensionX;
var row = i / dimensionX;
var w = (float) sheet.Width;
var h = (float) sheet.Height;
ErrorTileRegion = Box2.FromDimensions(
0, (h - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h);
Image<Rgba32> image;
using (var stream = _resourceCache.ContentFileRead("/Textures/noTile.png"))
{
image = Image.Load<Rgba32>(stream);
}
image.Blit(new UIBox2i(0, 0, tileSize, tileSize), sheet, Vector2i.Zero);
}
if (imgWidth >= 2048 || imgHeight >= 2048)
{
// Sanity warning, some machines don't have textures larger than this and need multiple atlases.
Logger.WarningS("clyde",
$"Tile texture atlas is ({imgWidth} x {imgHeight}), larger than 2048 x 2048. If you really need {tileCount} tiles, file an issue on RobustToolbox.");
}
var column = 1;
var row = 0;
foreach (var def in defList)
{
Image<Rgba32> image;
using (var stream = _resourceCache.ContentFileRead(new ResourcePath(def.Path) / $"{def.SpriteName}.png"))
{
image = Image.Load<Rgba32>(stream);
}
if (image.Width != tileSize || image.Height != tileSize)
if (image.Width != (tileSize * def.Variants) || image.Height != tileSize)
{
throw new NotSupportedException("Unable to use tiles with a dimension other than 32x32.");
throw new NotSupportedException(
$"Unable to load {new ResourcePath(def.Path) / $"{def.SpriteName}.png"}, due to being unable to use tile texture with a dimension other than {tileSize}x({tileSize} * Variants).");
}
var point = new Vector2i(column * tileSize, row * tileSize);
var regionList = new Box2[def.Variants];
image.Blit(new UIBox2i(0, 0, image.Width, image.Height), sheet, point);
for (var j = 0; j < def.Variants; j++)
{
var point = new Vector2i(column * tileSize, row * tileSize);
var w = (float) sheet.Width;
var h = (float) sheet.Height;
var box = new UIBox2i(0, 0, tileSize, tileSize).Translated(new Vector2i(j * tileSize, 0));
image.Blit(box, sheet, point);
_tileRegions.Add(def.TileId,
Box2.FromDimensions(
var w = (float) sheet.Width;
var h = (float) sheet.Height;
regionList[j] = Box2.FromDimensions(
point.X / w, (h - point.Y - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h));
tileSize / w, tileSize / h);
column++;
if (column >= dimensionX)
{
column = 0;
row++;
}
}
_tileRegions.Add(def.TileId, regionList);
}
_tileTextureAtlas = Texture.LoadFromImage(sheet, "Tile Atlas");

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -9,6 +10,8 @@ namespace Robust.Client.Map
/// </summary>
internal interface IClydeTileDefinitionManager : ITileDefinitionManager
{
Box2 ErrorTileRegion { get; }
/// <summary>
/// The texture atlas containing all the tiles.
/// </summary>
@@ -18,6 +21,12 @@ namespace Robust.Client.Map
/// Gets the region inside the texture atlas to use to draw a tile.
/// </summary>
/// <returns>If null, do not draw the tile at all.</returns>
Box2? TileAtlasRegion(Tile tile);
Box2[]? TileAtlasRegion(Tile tile);
/// <summary>
/// Gets the region inside the texture atlas to use to draw a tile type.
/// </summary>
/// <returns>If null, do not draw the tile at all.</returns>
Box2[]? TileAtlasRegion(ushort tileType);
}
}

View File

@@ -37,7 +37,7 @@ namespace Robust.Client.Physics
SimulateWorld((float) diff.TotalSeconds, true);
}
protected override void HandleMapCreated(object? sender, MapEventArgs eventArgs)
protected override void HandleMapCreated(MapChangedEvent eventArgs)
{
if (eventArgs.Map == MapId.Nullspace) return;
var mapUid = MapManager.GetMapEntityId(eventArgs.Map);

View File

@@ -31,7 +31,7 @@ namespace Robust.Client.Placement.Modes
var mapId = MouseCoords.GetMapId(pManager.EntityManager);
var snapToEntities = IoCManager.Resolve<IEntityLookup>().GetEntitiesInRange(MouseCoords, SnapToRange)
var snapToEntities = EntitySystem.Get<EntityLookupSystem>().GetEntitiesInRange(MouseCoords, SnapToRange)
.Where(entity => pManager.EntityManager.GetComponent<MetaDataComponent>(entity).EntityPrototype == pManager.CurrentPrototype && pManager.EntityManager.GetComponent<TransformComponent>(entity).MapID == mapId)
.OrderBy(entity => (pManager.EntityManager.GetComponent<TransformComponent>(entity).WorldPosition - MouseCoords.ToMapPos(pManager.EntityManager)).LengthSquared)
.ToList();

View File

@@ -53,7 +53,7 @@ namespace Robust.Client.Placement.Modes
var topRight = new Vector2(CurrentTile.X + 0.99f, CurrentTile.Y + 0.99f);
var box = new Box2(bottomLeft, topRight);
return !IoCManager.Resolve<IEntityLookup>().AnyEntitiesIntersecting(map, box);
return !EntitySystem.Get<EntityLookupSystem>().AnyEntitiesIntersecting(map, box);
}
}
}

View File

@@ -22,7 +22,7 @@ using Robust.Shared.Utility;
namespace Robust.Client.Placement
{
public sealed partial class PlacementManager : IPlacementManager, IDisposable
public sealed partial class PlacementManager : IPlacementManager, IDisposable, IEntityEventSubscriber
{
[Dependency] private readonly IClientNetManager NetworkManager = default!;
[Dependency] public readonly IPlayerManager PlayerManager = default!;
@@ -185,7 +185,7 @@ namespace Robust.Client.Placement
_modeDictionary.Add(type.Name, type);
}
MapManager.TileChanged += HandleTileChanged;
EntityManager.EventBus.SubscribeEvent<TileChangedEvent>(EventSource.Local, this, HandleTileChanged);
_drawOverlay = new PlacementOverlay(this);
_overlayManager.AddOverlay(_drawOverlay);
@@ -328,7 +328,7 @@ namespace Robust.Client.Placement
}
}
private void HandleTileChanged(object? sender, TileChangedEventArgs args)
private void HandleTileChanged(TileChangedEvent args)
{
var coords = MapManager.GetGrid(args.NewTile.GridIndex).GridTileToLocal(args.NewTile.GridIndices);
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);

View File

@@ -230,7 +230,7 @@ namespace Robust.Client.ResourceManagement
using (var manifestFile = cache.ContentFileRead(manifestPath))
{
if (manifestFile.Length <= 4096)
if (manifestFile.CanSeek && manifestFile.Length <= 4096)
{
// Most RSIs are actually tiny so if that's the case just load them into a stackalloc buffer.
// Avoids a ton of allocations with stream reader etc

View File

@@ -13,8 +13,8 @@
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.3" />
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.2" />
<PackageReference Include="nfluidsynth" Version="0.3.1" />
<PackageReference Include="NVorbis" Version="0.10.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
@@ -25,9 +25,9 @@
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.20348-rc2" />
</ItemGroup>
<ItemGroup Condition="'$(EnableClientScripting)' == 'True'">
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />
<ProjectReference Include="..\Robust.Shared.Scripting\Robust.Shared.Scripting.csproj" />
</ItemGroup>
@@ -53,6 +53,7 @@
<Compile Update="UserInterface\CustomControls\DefaultWindow.xaml.cs">
<DependentUpon>DefaultWindow.xaml</DependentUpon>
</Compile>
<Compile Remove="Debugging\IDebugDrawing.cs" />
</ItemGroup>
<Import Project="..\MSBuild\Robust.Engine.targets" />
<PropertyGroup>

View File

@@ -1,4 +1,6 @@
using Robust.Client.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface.Controls
{
@@ -9,6 +11,30 @@ namespace Robust.Client.UserInterface.Controls
Window = window;
}
/// <summary>
/// Enable the UI autoscale system, this will scale down the UI for lower resolutions
/// </summary>
[ViewVariables]
public bool AutoScale { get; set; } = false;
/// <summary>
/// Minimum resolution to start clamping autoscale to 1
/// </summary>
[ViewVariables]
public Vector2i AutoScaleUpperCutoff { get; set; } = new Vector2i(1080, 720);
/// <summary>
/// Maximum resolution to start clamping autos scale to autoscale minimum
/// </summary>
[ViewVariables]
public Vector2i AutoScaleLowerCutoff { get; set; } = new Vector2i(520, 520);
/// <summary>
/// The minimum ui scale value that autoscale will scale to
/// </summary>
[ViewVariables]
public float AutoScaleMinimum { get; set; } = 0.5f;
public override float UIScale => UIScaleSet;
internal float UIScaleSet { get; set; }
public override IClydeWindow Window { get; }

View File

@@ -8,6 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using Robust.Client.Graphics;
using Robust.Shared;
using Robust.Shared.Console;
using Robust.Shared.Asynchronous;
using Robust.Shared.IoC;
using Robust.Shared.Log;
@@ -164,12 +165,11 @@ namespace Robust.Client.UserInterface
{
if (OperatingSystem.IsMacOS())
{
// macOS seems pretty annoying about having the file dialog opened from the main thread.
// So we are forced to execute this synchronously on the main thread.
// Also I'm calling RunOnMainThread here to provide safety in case this is ran from a different thread.
// nativefiledialog doesn't provide any form of async API, so this WILL lock up the client.
// macOS seems pretty annoying about having the file dialog opened from the main windowing thread.
// So we are forced to execute this synchronously on the main windowing thread.
// nativefiledialog doesn't provide any form of async API, so this WILL lock up half the client.
var tcs = new TaskCompletionSource<string?>();
_taskManager.RunOnMainThread(() => tcs.SetResult(action()));
_clyde.RunOnWindowThread(() => tcs.SetResult(action()));
return tcs.Task;
}
@@ -388,4 +388,17 @@ namespace Robust.Client.UserInterface
SW_NFD_CANCEL,
}
}
public sealed class OpenFileCommand : IConsoleCommand
{
public string Command => "testopenfile";
public string Description => "";
public string Help => "";
public async void Execute(IConsoleShell shell, string argStr, string[] args)
{
var stream = await IoCManager.Resolve<IFileDialogManager>().OpenFile();
stream?.Dispose();
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Graphics;
@@ -23,6 +23,7 @@ namespace Robust.Client.UserInterface
internal sealed class UserInterfaceManager : IUserInterfaceManagerInternal
{
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IFontManager _fontManager = default!;
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -371,16 +372,21 @@ namespace Robust.Client.UserInterface
}
}
ReleaseKeyboardFocus();
if (hit == null)
{
ReleaseKeyboardFocus();
hitData = null;
return false;
}
var (control, rel) = hit.Value;
if (control != KeyboardFocused)
{
ReleaseKeyboardFocus();
}
ControlFocused = control;
if (ControlFocused.CanKeyboardFocus && ControlFocused.KeyboardFocusOnClick)
@@ -951,14 +957,37 @@ namespace Robust.Client.UserInterface
private void WindowContentScaleChanged(WindowContentScaleEventArgs args)
{
if (_windowsToRoot.TryGetValue(args.Window.Id, out var root))
{
UpdateUIScale(root);
_fontManager.ClearFontCache();
}
}
private float CalculateAutoScale(WindowRoot root)
{
//Grab the OS UIScale or the value set through CVAR debug
var osScale = _configurationManager.GetCVar(CVars.DisplayUIScale);
osScale = osScale == 0f ? root.Window.ContentScale.X : osScale;
var windowSize = root.Window.RenderTarget.Size;
//Only run autoscale if it is enabled, otherwise default to just use OS UIScale
if (!root.AutoScale && (windowSize.X <= 0 || windowSize.Y <= 0)) return osScale;
var maxScaleRes = root.AutoScaleUpperCutoff;
var minScaleRes = root.AutoScaleLowerCutoff;
var autoScaleMin = root.AutoScaleMinimum;
float scaleRatioX;
float scaleRatioY;
//Calculate the scale ratios and clamp it between the maximums and minimums
scaleRatioX = Math.Clamp(((float) windowSize.X - minScaleRes.X) / (maxScaleRes.X - minScaleRes.X) * osScale, autoScaleMin, osScale);
scaleRatioY = Math.Clamp(((float) windowSize.Y - minScaleRes.Y) / (maxScaleRes.Y - minScaleRes.Y) * osScale, autoScaleMin, osScale);
//Take the smallest UIScale value and use it for UI scaling
return Math.Min(scaleRatioX, scaleRatioY);
}
private void UpdateUIScale(WindowRoot root)
{
var newVal = _configurationManager.GetCVar(CVars.DisplayUIScale);
root.UIScaleSet = newVal == 0f ? root.Window.ContentScale.X : newVal;
root.UIScaleSet = CalculateAutoScale(root);
_propagateUIScaleChanged(root);
root.InvalidateMeasure();
}
@@ -977,7 +1006,7 @@ namespace Robust.Client.UserInterface
{
if (!_windowsToRoot.TryGetValue(windowResizedEventArgs.Window.Id, out var root))
return;
UpdateUIScale(root);
root.InvalidateMeasure();
}

View File

@@ -22,6 +22,7 @@ namespace Robust.Client.Utility
.Texture;
}
[Obsolete("Use SpriteSystem")]
public static RSI.State GetState(this SpriteSpecifier.Rsi rsiSpecifier, IResourceCache cache)
{
if (cache.TryGetResource<RSIResource>(
@@ -36,6 +37,7 @@ namespace Robust.Client.Utility
return SpriteComponent.GetFallbackState(cache);
}
[Obsolete("Use SpriteSystem")]
public static Texture Frame0(this SpriteSpecifier specifier)
{
return specifier.RsiStateLike().Default;
@@ -80,6 +82,7 @@ namespace Robust.Client.Utility
return specifier.RsiStateLike();
}
[Obsolete("Use SpriteSystem")]
public static IRsiStateLike RsiStateLike(this SpriteSpecifier specifier)
{
var resC = IoCManager.Resolve<IResourceCache>();

View File

@@ -0,0 +1 @@
# Add your introductions here!

View File

@@ -0,0 +1,2 @@
- name: Example-article
href: example-article.md

214
Robust.Docfx/docfx.json Normal file
View File

@@ -0,0 +1,214 @@
{
"metadata": [
{
"src":
[
{
"files": [
"**.csproj"
],
"exclude": [
"**/bin/**",
"**/obj/**",
"_site/**",
"**.xaml"
],
"src": "../Robust.UnitTesting"
}
],
"disableGitFeatures": false,
"disableDefaultFilter": false,
"dest": "api/Robust.UnitTesting"
},
{
"src":
[
{
"files": [
"**.csproj"
],
"exclude": [
"**/bin/**",
"**/obj/**",
"_site/**",
"**.xaml"
],
"src": "../Robust.Shared"
}
],
"disableGitFeatures": false,
"disableDefaultFilter": false,
"dest": "api/Robust.Shared"
},
{
"src":
[
{
"files": [
"**.csproj"
],
"exclude": [
"**/bin/**",
"**/obj/**",
"_site/**",
"**.xaml"
],
"src": "../Robust.Shared.Maths"
}
],
"disableGitFeatures": false,
"disableDefaultFilter": false,
"dest": "api/Robust.Shared.Maths"
},
{
"src":
[
{
"files": [
"**.csproj"
],
"exclude": [
"**/bin/**",
"**/obj/**",
"_site/**",
"**.xaml"
],
"src": "../Robust.Shared.Scripting"
}
],
"disableGitFeatures": false,
"disableDefaultFilter": false,
"dest": "api/Robust.Shared.Scripting"
},
{
"src":
[
{
"files": [
"**.csproj"
],
"exclude": [
"**/bin/**",
"**/obj/**",
"_site/**",
"**.xaml"
],
"src": "../Robust.Client"
}
],
"disableGitFeatures": false,
"disableDefaultFilter": false,
"dest": "api/Robust.Client"
},
{
"src":
[
{
"files": [
"**.csproj"
],
"exclude": [
"**/bin/**",
"**/obj/**",
"_site/**",
"**.xaml"
],
"src": "../Robust.Client.WebView"
}
],
"disableGitFeatures": false,
"disableDefaultFilter": false,
"dest": "api/Robust.Client.WebView"
},
{
"src":
[
{
"files": [
"**.csproj"
],
"exclude": [
"**/bin/**",
"**/obj/**",
"_site/**",
"**.xaml"
],
"src": "../Robust.Server"
}
],
"disableGitFeatures": false,
"disableDefaultFilter": false,
"dest": "api/Robust.Server"
},
{
"src":
[
{
"files": [
"**.csproj"
],
"exclude": [
"**/bin/**",
"**/obj/**",
"_site/**",
"**.xaml"
],
"src": "../Robust.LoaderApi"
}
],
"disableGitFeatures": false,
"disableDefaultFilter": false,
"dest": "api/Robust.LoaderApi/Robust.LoaderApi"
}
],
"build": {
"content": [
{
"files": [
"api/**/**.yml"
]
},
{
"files": [
"articles/**.md",
"articles/**/toc.yml",
"toc.yml",
"*.md"
]
}
],
"resource": [
{
"files": [
"images/**",
"favicon.ico",
"icon.svg"
]
}
],
"overwrite": [
{
"files": [
"apidoc/**.md"
],
"exclude": [
"obj/**",
"_site/**"
]
}
],
"dest": "_robust-site",
"globalMetadataFiles": [],
"fileMetadataFiles": [],
"template": [
"default",
"templates/darkfx"
],
"postProcessors": [],
"markdownEngineName": "markdig",
"noLangKeyword": false,
"keepFileLink": false,
"cleanupCacheHistory": false,
"disableGitFeatures": false
}
}

6
Robust.Docfx/index.md Normal file
View File

@@ -0,0 +1,6 @@
# RobustToolbox DocFX
![](https://i.imgur.com/Sv3Swxg.png)
## Welcome to the RobustToolbox DocFX instance.
### Click one of the tabs above to see documentation for that particular project/namespace

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Steffen Wilke
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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.

View File

@@ -0,0 +1,40 @@
{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
<div class="hidden-sm col-md-2" role="complementary">
<div class="sideaffix">
{{^_disableContribution}}
<div class="contribution">
<ul class="nav">
{{#docurl}}
<li>
<a href="{{docurl}}" class="contribution-link">{{__global.improveThisDoc}}</a>
</li>
{{/docurl}}
{{#sourceurl}}
<li>
<a href="{{sourceurl}}" class="contribution-link">{{__global.viewSource}}</a>
</li>
{{/sourceurl}}
</ul>
</div>
{{/_disableContribution}}
<div class="toggle-mode">
<div class="icon">
<i aria-hidden="true">☀</i>
</div>
<label class="switch">
<input type="checkbox" id="switch-style">
<span class="slider round"></span>
</label>
<div class="icon">
<i aria-hidden="true">☾</i>
</div>
</div>
<nav class="bs-docs-sidebar hidden-print hidden-xs hidden-sm affix" id="affix">
<h5>{{__global.inThisArticle}}</h5>
<div></div>
<!-- <p><a class="back-to-top" href="#top">Back to top</a><p> -->
</nav>
</div>
</div>

View File

@@ -0,0 +1,108 @@
{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
<h1 id="{{id}}" data-uid="{{uid}}" class="text-break">{{>partials/title}}</h1>
<div class="markdown level0 summary">{{{summary}}}</div>
<div class="markdown level0 conceptual">{{{conceptual}}}</div>
{{#inClass}}
<div class="inheritance">
<h5>{{__global.inheritance}}</h5>
{{#inheritance}}
<div class="level{{index}}">{{{specName.0.value}}}</div>
{{/inheritance}}
<div class="level{{level}}"><span class="xref">{{name.0.value}}</span></div>
{{#derivedClasses}}
<div class="level{{index}}">{{{specName.0.value}}}</div>
{{/derivedClasses}}
</div>
{{/inClass}}
{{#implements.0}}
<div classs="implements">
<h5>{{__global.implements}}</h5>
{{/implements.0}}
{{#implements}}
<div>{{{specName.0.value}}}</div>
{{/implements}}
{{#implements.0}}
</div>
{{/implements.0}}
{{#inheritedMembers.0}}
<div class="inheritedMembers">
</div>
{{/inheritedMembers.0}}
<h6><strong>{{__global.namespace}}</strong>: {{{namespace.specName.0.value}}}</h6>
<h6><strong>{{__global.assembly}}</strong>: {{assemblies.0}}.dll</h6>
<h5 id="{{id}}_syntax">{{__global.syntax}}</h5>
<div class="codewrapper">
<pre><code class="lang-{{_lang}} hljs">{{syntax.content.0.value}}</code></pre>
</div>
{{#syntax.parameters.0}}
<h5 class="parameters">{{__global.parameters}}</h5>
<table class="table table-bordered table-striped table-condensed">
<thead>
<tr>
<th>{{__global.type}}</th>
<th>{{__global.name}}</th>
<th>{{__global.description}}</th>
</tr>
</thead>
<tbody>
{{/syntax.parameters.0}}
{{#syntax.parameters}}
<tr>
<td>{{{type.specName.0.value}}}</td>
<td><span class="parametername">{{{id}}}</span></td>
<td>{{{description}}}</td>
</tr>
{{/syntax.parameters}}
{{#syntax.parameters.0}}
</tbody>
</table>
{{/syntax.parameters.0}}
{{#syntax.return}}
<h5 class="returns">{{__global.returns}}</h5>
<table class="table table-bordered table-striped table-condensed">
<thead>
<tr>
<th>{{__global.type}}</th>
<th>{{__global.description}}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{{type.specName.0.value}}}</td>
<td>{{{description}}}</td>
</tr>
</tbody>
</table>
{{/syntax.return}}
{{#syntax.typeParameters.0}}
<h5 class="typeParameters">{{__global.typeParameters}}</h5>
<table class="table table-bordered table-striped table-condensed">
<thead>
<tr>
<th>{{__global.name}}</th>
<th>{{__global.description}}</th>
</tr>
</thead>
<tbody>
{{/syntax.typeParameters.0}}
{{#syntax.typeParameters}}
<tr>
<td><span class="parametername">{{{id}}}</span></td>
<td>{{{description}}}</td>
</tr>
{{/syntax.typeParameters}}
{{#syntax.typeParameters.0}}
</tbody>
</table>
{{/syntax.typeParameters.0}}
{{#remarks}}
<h5 id="{{id}}_remarks"><strong>{{__global.remarks}}</strong></h5>
<div class="markdown level0 remarks">{{{remarks}}}</div>
{{/remarks}}
{{#example.0}}
<h5 id="{{id}}_examples"><strong>{{__global.examples}}</strong></h5>
{{/example.0}}
{{#example}}
{{{.}}}
{{/example}}

View File

@@ -0,0 +1,29 @@
{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
<footer>
<div class="grad-bottom"></div>
<div class="footer">
<div class="container">
<span class="pull-right">
<a href="#top">Back to top</a>
</span>
<div class="pull-left">
{{{_appFooter}}}
{{^_appFooter}}<span>Generated by <strong>DocFX</strong></span>{{/_appFooter}}
</div>
<div class="toggle-mode pull-right visible-sm visible-xs">
<div class="icon">
<i aria-hidden="true">☀</i>
</div>
<label class="switch">
<input type="checkbox" id="switch-style-m">
<span class="slider round"></span>
</label>
<div class="icon">
<i aria-hidden="true">☾</i>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="{{_rel}}styles/toggle-theme.js"></script>
</footer>

View File

@@ -0,0 +1,20 @@
{{!Copyright (c) Oscar Vasquez. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}}</title>
<meta name="viewport" content="width=device-width">
<meta name="title" content="{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}}">
<meta name="generator" content="docfx {{_docfxVersion}}">
{{#_description}}<meta name="description" content="{{_description}}">{{/_description}}
<link rel="shortcut icon" href="{{_rel}}{{{_appFaviconPath}}}{{^_appFaviconPath}}favicon.ico{{/_appFaviconPath}}">
<link rel="stylesheet" href="{{_rel}}styles/docfx.vendor.css">
<link rel="stylesheet" href="{{_rel}}styles/docfx.css">
<link rel="stylesheet" href="{{_rel}}styles/main.css">
<meta property="docfx:navrel" content="{{_navRel}}">
<meta property="docfx:tocrel" content="{{_tocRel}}">
{{#_noindex}}<meta name="searchOption" content="noindex">{{/_noindex}}
{{#_enableSearch}}<meta property="docfx:rel" content="{{_rel}}">{{/_enableSearch}}
{{#_enableNewTab}}<meta property="docfx:newtab" content="true">{{/_enableNewTab}}
</head>

View File

@@ -0,0 +1,470 @@
:root, body.dark-theme {
--color-foreground: #ccd5dc;
--color-navbar: #66666d;
--color-breadcrumb: #999;
--color-underline: #ddd;
--color-toc-hover: #fff;
--color-background: #2d2d30;
--color-background-subnav: #333337;
--color-background-dark: #1e1e1e;
--color-background-table-alt: #212123;
--color-background-quote: #69696e;
}
body.light-theme {
--color-foreground: #171717;
--color-breadcrumb: #4a4a4a;
--color-toc-hover: #4c4c4c;
--color-background: #ffffff;
--color-background-subnav: #f5f5f5;
--color-background-dark: #ddd;
--color-background-table-alt: #f9f9f9;
}
body {
color: var(--color-foreground);
line-height: 1.5;
font-size: 14px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
word-wrap: break-word;
background-color: var(--color-background);
}
.btn.focus, .btn:focus, .btn:hover {
color: var(--color-foreground);
}
h1 {
font-weight: 600;
font-size: 32px;
}
h2 {
font-weight: 600;
font-size: 24px;
line-height: 1.8;
}
h3 {
font-weight: 600;
font-size: 20px;
line-height: 1.8;
}
h5 {
font-size: 14px;
padding: 10px 0px;
}
article h1, article h2, article h3, article h4 {
margin-top: 35px;
margin-bottom: 15px;
}
article h4 {
padding-bottom: 8px;
border-bottom: 2px solid var(--color-underline);
}
.navbar-brand>img {
color: var(--color-background);
}
.navbar {
border: none;
}
.subnav {
border-top: 1px solid var(--color-underline);
background-color: var(--color-background-subnav);
}
.sidenav, .fixed_header, .toc {
background-color: var(--color-background);
}
.navbar-inverse {
background-color: var(--color-background-dark);
z-index: 100;
}
.navbar-inverse .navbar-nav>li>a, .navbar-inverse .navbar-text {
color: var(--color-navbar);
background-color: var(--color-background-dark);
border-bottom: 3px solid transparent;
padding-bottom: 12px;
}
.navbar-inverse .navbar-nav>li>a:focus, .navbar-inverse .navbar-nav>li>a:hover {
color: var(--color-foreground);
background-color: var(--color-background-dark);
border-bottom: 3px solid var(--color-background-subnav);
transition: all ease 0.25s;
}
.navbar-inverse .navbar-nav>.active>a, .navbar-inverse .navbar-nav>.active>a:focus, .navbar-inverse .navbar-nav>.active>a:hover {
color: var(--color-foreground);
background-color: var(--color-background-dark);
border-bottom: 3px solid var(--color-foreground);
transition: all ease 0.25s;
}
.navbar-form .form-control {
border: none;
border-radius: 0;
}
.light-theme .navbar-brand svg {
filter: brightness(20%);
}
.toc .level1>li {
font-weight: 400;
}
.toc .nav>li>a {
color: var(--color-foreground);
}
.sidefilter {
background-color: var(--color-background);
border-left: none;
border-right: none;
}
.sidefilter {
background-color: var(--color-background);
border-left: none;
border-right: none;
}
.toc-filter {
padding: 10px;
margin: 0;
background-color: var(--color-background);
}
.toc-filter>input {
border: none;
border-radius: unset;
background-color: var(--color-background-subnav);
padding: 5px 0 5px 20px;
font-size: 90%
}
.toc-filter>.clear-icon {
position: absolute;
top: 17px;
right: 15px;
}
.toc-filter>input:focus {
color: var(--color-foreground);
transition: all ease 0.25s;
}
.toc-filter>.filter-icon {
display: none;
}
.sidetoc>.toc {
background-color: var(--color-background);
overflow-x: hidden;
}
.sidetoc {
background-color: var(--color-background);
border: none;
}
.alert {
background-color: inherit;
border: none;
padding: 10px 0;
border-radius: 0;
}
.alert>p {
margin-bottom: 0;
padding: 5px 10px;
border-bottom: 1px solid;
background-color: var(--color-background-dark);
}
.alert>h5 {
padding: 10px 15px;
margin-top: 0;
margin-bottom: 0;
text-transform: uppercase;
font-weight: bold;
border-top: 2px solid;
background-color: var(--color-background-dark);
border-radius: none;
}
.alert>ul {
margin-bottom: 0;
padding: 5px 40px;
}
.alert-info {
color: #1976d2;
}
.alert-warning {
color: #f57f17;
}
.alert-danger {
color: #d32f2f;
}
pre {
padding: 9.5px;
margin: 0 0 10px;
font-size: 13px;
word-break: break-all;
word-wrap: break-word;
background-color: var(--color-background-dark);
border-radius: 0;
border: none;
}
code {
background: var(--color-background-dark) !important;
border-radius: 2px;
}
.hljs {
color: var(--color-foreground);
}
.toc .nav>li.active>.expand-stub::before, .toc .nav>li.in>.expand-stub::before, .toc .nav>li.in.active>.expand-stub::before, .toc .nav>li.filtered>.expand-stub::before {
content: "▾";
}
.toc .nav>li>.expand-stub::before, .toc .nav>li.active>.expand-stub::before {
content: "▸";
}
.affix ul ul>li>a:before {
content: "|";
}
.breadcrumb {
background-color: var(--color-background-subnav);
}
.breadcrumb .label.label-primary {
background: #444;
border-radius: 0;
font-weight: normal;
font-size: 100%;
}
#breadcrumb .breadcrumb>li a {
border-radius: 0;
font-weight: normal;
font-size: 85%;
display: inline;
padding: 0 .6em 0;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
color: var(--color-breadcrumb);
}
#breadcrumb .breadcrumb>li a:hover {
color: var(--color-foreground);
transition: all ease 0.25s;
}
.breadcrumb>li+li:before {
content: "⯈";
font-size: 75%;
color: var(--color-background-dark);
padding: 0;
}
.light-theme .breadcrumb>li+li:before {
color: var(--color-foreground)
}
.toc .level1>li {
font-weight: 600;
font-size: 130%;
padding-left: 5px;
}
.footer {
border-top: none;
background-color: var(--color-background-dark);
padding: 15px 0;
font-size: 90%;
}
.toc .nav>li>a:hover, .toc .nav>li>a:focus {
color: var(--color-toc-hover);
transition: all ease 0.1s;
}
.form-control {
background-color: var(--color-background-subnav);
border: none;
border-radius: 0;
-webkit-box-shadow: none;
box-shadow: none;
}
.form-control:focus {
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: none;
box-shadow: none;
}
input#search-query:focus {
color: var(--color-foreground);
}
.table-bordered, .table-bordered>tbody>tr>td, .table-bordered>tbody>tr>th, .table-bordered>tfoot>tr>td, .table-bordered>tfoot>tr>th, .table-bordered>thead>tr>td, .table-bordered>thead>tr>th {
border: 1px solid var(--color-background-dark);
}
.table-striped>tbody>tr:nth-of-type(odd) {
background-color: var(--color-background-table-alt);
}
blockquote {
padding: 10px 20px;
margin: 0 0 10px;
font-size: 110%;
border-left: 5px solid var(--color-background-quote);
color: var(--color-background-quote);
}
.pagination>.disabled>a, .pagination>.disabled>a:focus, .pagination>.disabled>a:hover, .pagination>.disabled>span, .pagination>.disabled>span:focus, .pagination>.disabled>span:hover {
background-color: var(--color-background-subnav);
border-color: var(--color-background-subnav);
}
.breadcrumb>li, .pagination {
display: inline;
}
.tabGroup a[role="tab"] {
border-bottom: 2px solid var(--color-background-dark);
}
.tabGroup a[role="tab"][aria-selected="true"] {
color: var(--color-foreground);
}
.tabGroup section[role="tabpanel"] {
border: 1px solid var(--color-background-dark);
}
.sideaffix > div.contribution > ul > li > a.contribution-link:hover {
background-color: var(--color-background);
}
.switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 4px;
bottom: 3px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #337ab7;
}
input:focus + .slider {
box-shadow: 0 0 1px #337ab7;
}
input:checked + .slider:before {
-webkit-transform: translateX(19px);
-ms-transform: translateX(19px);
transform: translateX(19px);
}
/* Rounded sliders */
.slider.round {
border-radius: 20px;
}
.slider.round:before {
border-radius: 50%;
}
.toggle-mode .icon {
display: inline-block;
}
.toggle-mode .icon i {
font-style: normal;
font-size: 17px;
display: inline-block;
padding-right: 7px;
padding-left: 7px;
vertical-align: middle;
}
@media (min-width: 1600px) {
.container {
width: 100%;
}
.sidefilter {
width: 18%;
}
.sidetoc {
width: 18%;
}
.article.grid-right {
margin-left: 19%;
}
.sideaffix {
width: 11.5%;
}
.affix ul>li.active>a {
white-space: initial;
}
.affix ul>li>a {
width: 99%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}

View File

@@ -0,0 +1,35 @@
const sw = document.getElementById("switch-style"), sw_mobile = document.getElementById("switch-style-m"), b = document.body;
if (b) {
function toggleTheme(target, dark) {
target.classList.toggle("dark-theme", dark)
target.classList.toggle("light-theme", !dark)
}
function switchEventListener() {
toggleTheme(b, this.checked);
if (window.localStorage) {
this.checked ? localStorage.setItem("theme", "dark-theme") : localStorage.setItem("theme", "light-theme")
}
}
var isDarkTheme = !window.localStorage || !window.localStorage.getItem("theme") || window.localStorage && localStorage.getItem("theme") === "dark-theme";
if(sw && sw_mobile){
sw.checked = isDarkTheme;
sw_mobile.checked = isDarkTheme;
sw.addEventListener("change", switchEventListener);
sw_mobile.addEventListener("change", switchEventListener);
// sync state between switches
sw.addEventListener("change", function() {
sw_mobile.checked = this.checked;
});
sw_mobile.addEventListener("change", function() {
sw.checked = this.checked;
});
}
toggleTheme(b, isDarkTheme);
}

29
Robust.Docfx/toc.yml Normal file
View File

@@ -0,0 +1,29 @@
- name: Articles
href: articles/
# Client
- name: Robust.Client
href: api/Robust.Client/
homepage: api/Robust.Client/Robust.Client.html
- name: Robust.Client.WebView
href: api/Robust.Client.WebView/
#Loader Api
- name: Robust.LoaderApi
href: api/Robust.LoaderApi/Robust.LoaderApi/
# Server
- name: Robust.Server
href: api/Robust.Server/
# Shared
- name: Robust.Shared
href: api/Robust.Shared/
- name: Robust.Shared.Maths
href: api/Robust.Shared.Maths/
- name: Robust.Shared.Scripting
href: api/Robust.Shared.Scripting/
# Unit testing
- name: Robust.UnitTesting
href: api/Robust.UnitTesting/

View File

@@ -66,18 +66,17 @@ namespace Robust.Server
[Dependency] private readonly IConfigurationManagerInternal _config = default!;
[Dependency] private readonly IServerEntityManager _entityManager = default!;
[Dependency] private readonly IEntityLookup _lookup = default!;
[Dependency] private readonly ILogManager _log = default!;
[Dependency] private readonly IRobustSerializer _serializer = default!;
[Dependency] private readonly IGameTiming _time = default!;
[Dependency] private readonly IResourceManagerInternal _resources = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly ITimerManager timerManager = default!;
[Dependency] private readonly ITimerManager _timerManager = default!;
[Dependency] private readonly IServerGameStateManager _stateManager = default!;
[Dependency] private readonly IServerNetManager _network = default!;
[Dependency] private readonly ISystemConsoleManager _systemConsole = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly IRuntimeLog runtimeLog = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
[Dependency] private readonly IModLoaderInternal _modLoader = default!;
[Dependency] private readonly IWatchdogApi _watchdogApi = default!;
[Dependency] private readonly HubManager _hubManager = default!;
@@ -127,7 +126,7 @@ namespace Robust.Server
else
Logger.InfoS("srv", $"{reason}, shutting down...");
_shutdownReason = reason;
_shutdownReason = reason ?? "Shutting down";
if (_mainLoop != null) _mainLoop.Running = false;
if (_logHandler != null)
@@ -344,7 +343,6 @@ namespace Robust.Server
_consoleHost.Initialize();
_entityManager.Startup();
_mapManager.Startup();
IoCManager.Resolve<IEntityLookup>().Startup();
_stateManager.Initialize();
var reg = _entityManager.ComponentFactory.GetRegistration<TransformComponent>();
@@ -603,7 +601,6 @@ namespace Robust.Server
_network.Shutdown($"Server shutting down: {_shutdownReason}");
// shutdown entities
IoCManager.Resolve<IEntityLookup>().Shutdown();
_entityManager.Cleanup();
if (_config.GetCVar(CVars.LogRuntimeLog))
@@ -614,7 +611,7 @@ namespace Robust.Server
Directory.CreateDirectory(relPath);
var pathToWrite = Path.Combine(relPath,
"Runtime-" + DateTime.Now.ToString("yyyy-MM-dd-THH-mm-ss") + ".txt");
File.WriteAllText(pathToWrite, runtimeLog.Display(), EncodingHelpers.UTF8);
File.WriteAllText(pathToWrite, _runtimeLog.Display(), EncodingHelpers.UTF8);
}
AppDomain.CurrentDomain.ProcessExit -= ProcessExiting;
@@ -659,7 +656,8 @@ namespace Robust.Server
using (TickUsage.WithLabels("Timers").NewTimer())
{
timerManager.UpdateTimers(frameEventArgs);
_consoleHost.CommandBufferExecute();
_timerManager.UpdateTimers(frameEventArgs);
}
using (TickUsage.WithLabels("AsyncTasks").NewTimer())
@@ -670,8 +668,6 @@ namespace Robust.Server
// Pass Histogram into the IEntityManager.Update so it can do more granular measuring.
_entityManager.TickUpdate(frameEventArgs.DeltaSeconds, noPredictions: false, TickUsage);
_lookup.Update();
using (TickUsage.WithLabels("PostEngine").NewTimer())
{
_modLoader.BroadcastUpdate(ModUpdateLevel.PostEngine, frameEventArgs);

View File

@@ -311,7 +311,7 @@ namespace Robust.Server.Bql
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
var radius = (float)(double)arguments[0];
var entityLookup = IoCManager.Resolve<IEntityLookup>();
var entityLookup = EntitySystem.Get<EntityLookupSystem>();
// TODO: Make this a foreach and reduce LINQ chain because it'll allocate a LOT
//BUG: GetEntitiesInRange effectively uses manhattan distance. This is not intended, near is supposed to be circular.

View File

@@ -29,14 +29,13 @@ namespace Robust.Server.Console.Commands
var mapId = new MapId(int.Parse(args[0]));
var mapMgr = IoCManager.Resolve<IMapManager>();
var pauseMgr = IoCManager.Resolve<IPauseManager>();
if (!mapMgr.MapExists(mapId))
{
mapMgr.CreateMap(mapId);
if (args.Length >= 2 && args[1] == "false")
{
pauseMgr.AddUninitializedMap(mapId);
mapMgr.AddUninitializedMap(mapId);
}
shell.WriteLine($"Map with ID {mapId} created.");
@@ -219,7 +218,11 @@ namespace Robust.Server.Console.Commands
}
IoCManager.Resolve<IMapLoader>().LoadMap(mapId, args[1]);
shell.WriteLine($"Map {mapId} has been loaded from {args[1]}.");
if (mapManager.MapExists(mapId))
shell.WriteLine($"Map {mapId} has been loaded from {args[1]}.");
else
shell.WriteError($"Error while loading map from {args[1]}.");
}
}
@@ -318,7 +321,6 @@ namespace Robust.Server.Console.Commands
}
var mapManager = IoCManager.Resolve<IMapManager>();
var pauseManager = IoCManager.Resolve<IPauseManager>();
var arg = args[0];
var mapId = new MapId(int.Parse(arg, CultureInfo.InvariantCulture));
@@ -329,13 +331,13 @@ namespace Robust.Server.Console.Commands
return;
}
if (pauseManager.IsMapInitialized(mapId))
if (mapManager.IsMapInitialized(mapId))
{
shell.WriteError("Map is already initialized!");
return;
}
pauseManager.DoMapInitialize(mapId);
mapManager.DoMapInitialize(mapId);
}
}
@@ -348,17 +350,16 @@ namespace Robust.Server.Console.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mapManager = IoCManager.Resolve<IMapManager>();
var pauseManager = IoCManager.Resolve<IPauseManager>();
var msg = new StringBuilder();
foreach (var mapId in mapManager.GetAllMapIds().OrderBy(id => id.Value))
{
msg.AppendFormat("{0}: init: {1}, paused: {2}, ent: {3}, grids: {4}\n",
mapId, pauseManager.IsMapInitialized(mapId),
pauseManager.IsMapPaused(mapId),
string.Join(",", mapManager.GetAllMapGrids(mapId).Select(grid => grid.Index)),
mapManager.GetMapEntityId(mapId));
mapId, mapManager.IsMapInitialized(mapId),
mapManager.IsMapPaused(mapId),
mapManager.GetMapEntityId(mapId),
string.Join(",", mapManager.GetAllMapGrids(mapId).Select(grid => grid.Index)));
}
shell.WriteLine(msg.ToString());
@@ -380,7 +381,7 @@ namespace Robust.Server.Console.Commands
foreach (var grid in mapManager.GetAllGrids().OrderBy(grid => grid.Index.Value))
{
msg.AppendFormat("{0}: map: {1}, ent: {2}, pos: {3} \n",
grid.Index, grid.ParentMapId, grid.WorldPosition, grid.GridEntityId);
grid.Index, grid.ParentMapId, grid.GridEntityId, grid.WorldPosition);
}
shell.WriteLine(msg.ToString());

View File

@@ -109,9 +109,8 @@ namespace Robust.Server.Console.Commands
private void SetupPlayer(MapId mapId, IConsoleShell shell, IPlayerSession? player, IMapManager mapManager)
{
if (mapId == MapId.Nullspace) return;
var pauseManager = IoCManager.Resolve<IPauseManager>();
pauseManager.SetMapPaused(mapId, false);
var mapUid = IoCManager.Resolve<IMapManager>().GetMapEntityIdOrThrow(mapId);
mapManager.SetMapPaused(mapId, false);
var mapUid = mapManager.GetMapEntityIdOrThrow(mapId);
IoCManager.Resolve<IEntityManager>().GetComponent<SharedPhysicsMapComponent>(mapUid).Gravity = new Vector2(0, -9.8f);
return;

View File

@@ -3,7 +3,7 @@ using Robust.Shared.GameStates;
namespace Robust.Server.GameObjects;
internal sealed class AppearanceSystem : SharedAppearanceSystem
public sealed class AppearanceSystem : SharedAppearanceSystem
{
public override void Initialize()
{

View File

@@ -1,4 +1,4 @@
using JetBrains.Annotations;
using JetBrains.Annotations;
using Robust.Server.Physics;
using Robust.Shared;
using Robust.Shared.Configuration;
@@ -37,7 +37,7 @@ namespace Robust.Server.GameObjects
collideComp.BodyType = BodyType.Static;
}
protected override void HandleMapCreated(object? sender, MapEventArgs eventArgs)
protected override void HandleMapCreated(MapChangedEvent eventArgs)
{
if (eventArgs.Map == MapId.Nullspace) return;
var mapUid = MapManager.GetMapEntityIdOrThrow(eventArgs.Map);

View File

@@ -5,30 +5,31 @@ namespace Robust.Server.GameStates;
public struct ChunkIndicesEnumerator
{
private Vector2i _topLeft;
private Vector2i _bottomRight;
private Vector2i _bottomLeft;
private Vector2i _topRight;
private int _x;
private int _y;
public ChunkIndicesEnumerator(Box2 viewBox, float chunkSize)
public ChunkIndicesEnumerator(Vector2 viewPos, float range, float chunkSize)
{
_topLeft = (viewBox.TopLeft / chunkSize).Floored();
_bottomRight = (viewBox.BottomRight / chunkSize).Floored();
_bottomLeft = ((viewPos - range) / chunkSize).Floored();
// Also floor this as we get the whole chunk anyway.
_topRight = ((viewPos + range) / chunkSize).Floored();
_x = _topLeft.X;
_y = _bottomRight.Y;
_x = _bottomLeft.X;
_y = _bottomLeft.Y;
}
public bool MoveNext([NotNullWhen(true)] out Vector2i? chunkIndices)
{
if (_y > _topLeft.Y)
if (_y > _topRight.Y)
{
_x++;
_y = _bottomRight.Y;
_y = _bottomLeft.Y;
}
if (_x > _bottomRight.X)
if (_x > _topRight.X)
{
chunkIndices = null;
return false;

View File

@@ -37,7 +37,6 @@ public interface IPVSCollection
public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : IComparable<TIndex>, IEquatable<TIndex>
{
[Shared.IoC.Dependency] private readonly IEntityManager _entityManager = default!;
[Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector2i GetChunkIndices(Vector2 coordinates)
@@ -81,19 +80,24 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
private readonly List<(GameTick tick, TIndex index)> _deletionHistory = new();
/// <summary>
/// An index containing the <see cref="IndexLocation"/>s of all <see cref="TIndex"/>.
/// An index containing the <see cref="IIndexLocation"/>s of all <see cref="TIndex"/>.
/// </summary>
private readonly Dictionary<TIndex, IndexLocation> _indexLocations = new();
private readonly Dictionary<TIndex, IIndexLocation> _indexLocations = new();
/// <summary>
/// Buffer of all locationchanges since the last process call
/// </summary>
private readonly Dictionary<TIndex, IndexLocation> _locationChangeBuffer = new();
private readonly Dictionary<TIndex, IIndexLocation> _locationChangeBuffer = new();
/// <summary>
/// Buffer of all indexremovals since the last process call
/// </summary>
private readonly Dictionary<TIndex, GameTick> _removalBuffer = new();
/// <summary>
/// To avoid re-allocating the hashset every tick we'll just store it.
/// </summary>
private HashSet<TIndex> _changedIndices = new();
public PVSCollection()
{
IoCManager.InjectDependencies(this);
@@ -101,13 +105,18 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
public void Process()
{
var changedIndices = new HashSet<TIndex>(_locationChangeBuffer.Keys);
_changedIndices.EnsureCapacity(_locationChangeBuffer.Count);
var changedChunkLocations = new HashSet<IndexLocation>();
foreach (var (key, _) in _locationChangeBuffer)
{
_changedIndices.Add(key);
}
var changedChunkLocations = new HashSet<IIndexLocation>();
foreach (var (index, tick) in _removalBuffer)
{
//changes dont need to be computed if we are removing the index anyways
if (changedIndices.Remove(index) && !_indexLocations.ContainsKey(index))
if (_changedIndices.Remove(index) && !_indexLocations.ContainsKey(index))
{
//this index wasnt added yet, so we can safely just skip the deletion
continue;
@@ -135,13 +144,14 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
}
}
foreach (var index in changedIndices)
foreach (var index in _changedIndices)
{
RemoveIndexInternal(index);
AddIndexInternal(index, _locationChangeBuffer[index]);
}
_changedIndices.Clear();
_locationChangeBuffer.Clear();
_removalBuffer.Clear();
}
@@ -154,7 +164,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
public HashSet<TIndex>.Enumerator GetElementsForSession(ICommonSession session) => _localOverrides[session].GetEnumerator();
private void AddIndexInternal(TIndex index, IndexLocation location)
private void AddIndexInternal(TIndex index, IIndexLocation location)
{
switch (location)
{
@@ -186,7 +196,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
_indexLocations.Add(index, location);
}
private IndexLocation? RemoveIndexInternal(TIndex index)
private IIndexLocation? RemoveIndexInternal(TIndex index)
{
// the index might be gone due to disconnects/grid-/map-deletions
if (!_indexLocations.TryGetValue(index, out var location))
@@ -321,6 +331,9 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
if(!removeFromOverride && IsOverride(index))
return;
if (_indexLocations.TryGetValue(index, out var oldLocation) &&
oldLocation is GlobalOverride) return;
RegisterUpdate(index, new GlobalOverride());
}
@@ -335,6 +348,10 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
if(!removeFromOverride && IsOverride(index))
return;
if (_indexLocations.TryGetValue(index, out var oldLocation) &&
oldLocation is LocalOverride local &&
local.Session == session) return;
RegisterUpdate(index, new LocalOverride(session));
}
@@ -352,14 +369,14 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
var gridId = coordinates.GetGridId(_entityManager);
if (gridId != GridId.Invalid)
{
var gridIndices = GetChunkIndices(_mapManager.GetGrid(gridId).LocalToGrid(coordinates));
var gridIndices = GetChunkIndices(coordinates.Position);
UpdateIndex(index, gridId, gridIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
return;
}
var mapId = coordinates.GetMapId(_entityManager);
var mapIndices = GetChunkIndices(coordinates.ToMapPos(_entityManager));
UpdateIndex(index, mapId, mapIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
var mapCoordinates = coordinates.ToMap(_entityManager);
var mapIndices = GetChunkIndices(coordinates.Position);
UpdateIndex(index, mapCoordinates.MapId, mapIndices, true); //skip overridecheck bc we already did it (saves some dict lookups)
}
/// <summary>
@@ -374,6 +391,11 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
if(!removeFromOverride && IsOverride(index))
return;
if (_indexLocations.TryGetValue(index, out var oldLocation) &&
oldLocation is GridChunkLocation oldGrid &&
oldGrid.ChunkIndices == chunkIndices &&
oldGrid.GridId == gridId) return;
RegisterUpdate(index, new GridChunkLocation(gridId, chunkIndices));
}
@@ -389,25 +411,92 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
if(!removeFromOverride && IsOverride(index))
return;
if (_indexLocations.TryGetValue(index, out var oldLocation) &&
oldLocation is MapChunkLocation oldMap &&
oldMap.ChunkIndices == chunkIndices &&
oldMap.MapId == mapId) return;
RegisterUpdate(index, new MapChunkLocation(mapId, chunkIndices));
}
private void RegisterUpdate(TIndex index, IndexLocation location)
private void RegisterUpdate(TIndex index, IIndexLocation location)
{
if(_indexLocations.TryGetValue(index, out var oldLocation) && oldLocation == location) return;
_locationChangeBuffer[index] = location;
}
#endregion
#region IndexLocations
private abstract record IndexLocation;
private record MapChunkLocation(MapId MapId, Vector2i ChunkIndices) : IndexLocation;
private record GridChunkLocation(GridId GridId, Vector2i ChunkIndices) : IndexLocation;
private record GlobalOverride : IndexLocation;
private record LocalOverride(ICommonSession Session) : IndexLocation;
#endregion
}
#region IndexLocations
public interface IIndexLocation {};
public interface IChunkIndexLocation{ };
public struct MapChunkLocation : IIndexLocation, IChunkIndexLocation, IEquatable<MapChunkLocation>
{
public MapChunkLocation(MapId mapId, Vector2i chunkIndices)
{
MapId = mapId;
ChunkIndices = chunkIndices;
}
public MapId MapId { get; init; }
public Vector2i ChunkIndices { get; init; }
public bool Equals(MapChunkLocation other)
{
return MapId.Equals(other.MapId) && ChunkIndices.Equals(other.ChunkIndices);
}
public override bool Equals(object? obj)
{
return obj is MapChunkLocation other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(MapId, ChunkIndices);
}
}
public struct GridChunkLocation : IIndexLocation, IChunkIndexLocation, IEquatable<GridChunkLocation>
{
public GridChunkLocation(GridId gridId, Vector2i chunkIndices)
{
GridId = gridId;
ChunkIndices = chunkIndices;
}
public GridId GridId { get; init; }
public Vector2i ChunkIndices { get; init; }
public bool Equals(GridChunkLocation other)
{
return GridId.Equals(other.GridId) && ChunkIndices.Equals(other.ChunkIndices);
}
public override bool Equals(object? obj)
{
return obj is GridChunkLocation other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(GridId, ChunkIndices);
}
}
public struct GlobalOverride : IIndexLocation { }
public struct LocalOverride : IIndexLocation
{
public LocalOverride(ICommonSession session)
{
Session = session;
}
public ICommonSession Session { get; init; }
}
#endregion

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.ObjectPool;
namespace Robust.Server.GameStates;
public sealed class RobustTree<T> where T : notnull
{
private Dictionary<T, TreeNode> _nodeIndex = new();
private Dictionary<T, T> _parents = new();
public readonly HashSet<T> RootNodes = new();
private ObjectPool<HashSet<T>> _pool;
public RobustTree(ObjectPool<HashSet<T>>? pool = null)
{
_pool = pool ?? new DefaultObjectPool<HashSet<T>>(new PVSSystem.SetPolicy<T>());
}
public void Clear()
{
foreach (var value in _nodeIndex.Values)
{
if(value.Children != null)
_pool.Return(value.Children);
}
_nodeIndex.Clear();
_parents.Clear();
RootNodes.Clear();
}
public TreeNode this[T index] => _nodeIndex[index];
public void Remove(T value, bool mend = false)
{
if (!_nodeIndex.TryGetValue(value, out var node))
throw new InvalidOperationException("Node doesnt exist.");
if (RootNodes.Contains(value))
{
if (node.Children != null)
{
foreach (var child in node.Children)
{
_parents.Remove(child);
RootNodes.Add(child);
}
_pool.Return(node.Children);
}
RootNodes.Remove(value);
_nodeIndex.Remove(value);
return;
}
if (_parents.TryGetValue(value, out var parent))
{
if (node.Children != null)
{
foreach (var child in node.Children)
{
if (mend)
{
_parents[child] = parent;
var children = _nodeIndex[parent].Children;
if (children == null)
{
children = _pool.Get();
_nodeIndex[parent] = _nodeIndex[parent].WithChildren(children);
}
children.Add(child);
}
else
{
_parents.Remove(child);
RootNodes.Add(child);
}
}
_pool.Return(node.Children);
}
_parents.Remove(value);
_nodeIndex.Remove(value);
}
throw new InvalidOperationException("Node neither had a parent nor was a RootNode.");
}
public TreeNode Set(T rootNode)
{
//root node, for now
if (_nodeIndex.TryGetValue(rootNode, out var node))
{
if(!RootNodes.Contains(rootNode))
throw new InvalidOperationException("Node already exists as non-root node.");
return node;
}
node = new TreeNode(rootNode);
_nodeIndex.Add(rootNode, node);
RootNodes.Add(rootNode);
return node;
}
public TreeNode Set(T child, T parent)
{
if (!_nodeIndex.TryGetValue(parent, out var parentNode))
parentNode = Set(parent);
if (parentNode.Children == null)
{
_nodeIndex[parent] = parentNode = parentNode.WithChildren(_pool.Get());
}
if (_nodeIndex.TryGetValue(child, out var existingNode))
{
if (RootNodes.Contains(child))
{
parentNode.Children!.Add(existingNode.Value);
RootNodes.Remove(child);
_parents.Add(child, parent);
return existingNode;
}
if (!_parents.TryGetValue(child, out var previousParent) || _nodeIndex.TryGetValue(previousParent, out var previousParentNode))
throw new InvalidOperationException("Could not find old parent for non-root node.");
previousParentNode.Children?.Remove(existingNode.Value);
parentNode.Children!.Add(existingNode.Value);
_parents[child] = parent;
return existingNode;
}
existingNode = new TreeNode(child);
_nodeIndex.Add(child, existingNode);
parentNode.Children!.Add(existingNode.Value);
_parents.Add(child, parent);
return existingNode;
}
public readonly struct TreeNode : IEquatable<TreeNode>
{
public readonly T Value;
public readonly HashSet<T>? Children;
public TreeNode(T value, HashSet<T>? children = null)
{
Value = value;
Children = children;
}
public bool Equals(TreeNode other)
{
return Value.Equals(other.Value) && Children?.Equals(other.Children) == true;
}
public override bool Equals(object? obj)
{
return obj is TreeNode other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Value, Children);
}
public TreeNode WithChildren(HashSet<T> children)
{
return new TreeNode(Value, children);
}
}
}

View File

@@ -1,9 +1,12 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.ObjectPool;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Enums;
@@ -125,6 +128,37 @@ namespace Robust.Server.GameStates
// people not in the game don't get states
var players = _playerManager.ServerSessions.Where(o => o.Status == SessionStatus.InGame).ToArray();
//todo paul oh my god make this less shit
EntityQuery<MetaDataComponent> metadataQuery = default!;
EntityQuery<TransformComponent> transformQuery = default!;
HashSet<int>[] playerChunks = default!;
EntityUid[][] viewerEntities = default!;
(Dictionary<EntityUid, MetaDataComponent> metadata, RobustTree<EntityUid> tree)?[] chunkCache = default!;
if (_pvs.CullingEnabled)
{
List<(uint, IChunkIndexLocation)> chunks;
(chunks, playerChunks, viewerEntities) = _pvs.GetChunks(players);
const int ChunkBatchSize = 2;
var chunksCount = chunks.Count;
var chunkBatches = (int) MathF.Ceiling((float) chunksCount / ChunkBatchSize);
chunkCache =
new (Dictionary<EntityUid, MetaDataComponent> metadata, RobustTree<EntityUid> tree)?[chunksCount];
transformQuery = _entityManager.GetEntityQuery<TransformComponent>();
metadataQuery = _entityManager.GetEntityQuery<MetaDataComponent>();
Parallel.For(0, chunkBatches, i =>
{
var start = i * ChunkBatchSize;
var end = Math.Min(start + ChunkBatchSize, chunksCount);
for (var j = start; j < end; ++j)
{
var (visMask, chunkIndexLocation) = chunks[j];
chunkCache[j] = _pvs.CalculateChunk(chunkIndexLocation, visMask, transformQuery, metadataQuery);
}
});
}
const int BatchSize = 2;
var batches = (int) MathF.Ceiling((float) players.Length / BatchSize);
@@ -135,11 +169,9 @@ namespace Robust.Server.GameStates
for (var j = start; j < end; ++j)
{
var session = players[j];
try
{
SendStateUpdate(session);
SendStateUpdate(j);
}
catch (Exception e) // Catch EVERY exception
{
@@ -148,8 +180,10 @@ namespace Robust.Server.GameStates
}
});
void SendStateUpdate(IPlayerSession session)
void SendStateUpdate(int sessionIndex)
{
var session = players[sessionIndex];
// KILL IT WITH FIRE
if(mainThread != Thread.CurrentThread)
IoCManager.InitThread(new DependencyCollection(parentDeps), true);
@@ -161,7 +195,10 @@ namespace Robust.Server.GameStates
DebugTools.Assert("Why does this channel not have an entry?");
}
var (entStates, deletions) = _pvs.CalculateEntityStates(session, lastAck, _gameTiming.CurTick);
var (entStates, deletions) = _pvs.CullingEnabled
? _pvs.CalculateEntityStates(session, lastAck, _gameTiming.CurTick, chunkCache,
playerChunks[sessionIndex], metadataQuery, transformQuery, viewerEntities[sessionIndex])
: _pvs.GetAllEntityStates(session, lastAck, _gameTiming.CurTick);
var playerStates = _playerManager.GetPlayerStates(lastAck);
var mapData = _mapManager.GetStateData(lastAck);
@@ -172,8 +209,6 @@ namespace Robust.Server.GameStates
InterlockedHelper.Min(ref oldestAckValue, lastAck.Value);
DebugTools.Assert(state.MapData?.CreatedMaps is null || (state.MapData?.CreatedMaps is not null && state.EntityStates.HasContents), "Sending new maps, but no entity state.");
// actually send the state
var stateUpdateMessage = _networkManager.CreateNetMessage<MsgState>();
stateUpdateMessage.State = state;
@@ -194,6 +229,8 @@ namespace Robust.Server.GameStates
_networkManager.ServerSendMessage(stateUpdateMessage, channel);
}
if(_pvs.CullingEnabled)
_pvs.ReturnToPool(chunkCache, playerChunks);
_pvs.Cleanup(_playerManager.ServerSessions);
var oldestAck = new GameTick(oldestAckValue);

View File

@@ -24,7 +24,6 @@ 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.Timing;
using Robust.Shared.Utility;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
@@ -44,7 +43,6 @@ namespace Robust.Server.Maps
[Dependency] private readonly IMapManagerInternal _mapManager = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IServerEntityManagerInternal _serverEntityManager = default!;
[Dependency] private readonly IPauseManager _pauseManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public event Action<YamlStream, string>? LoadedMapData;
@@ -54,7 +52,7 @@ namespace Robust.Server.Maps
{
var grid = _mapManager.GetGrid(gridId);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, _prototypeManager);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _prototypeManager);
context.RegisterGrid(grid);
var root = context.Serialize();
var document = new YamlDocument(root);
@@ -100,7 +98,7 @@ namespace Robust.Server.Maps
throw new InvalidDataException("Cannot instance map with multiple grids as blueprint.");
}
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager,
_prototypeManager, (YamlMappingNode) data.RootNode, mapId, options);
context.Deserialize();
grid = context.Grids[0];
@@ -120,7 +118,7 @@ namespace Robust.Server.Maps
_serverEntityManager.GetComponent<MetaDataComponent>(entity).EntityLifeStage = EntityLifeStage.MapInitialized;
}
}
else if (_pauseManager.IsMapInitialized(mapId))
else if (_mapManager.IsMapInitialized(mapId))
{
foreach (var entity in context.Entities)
{
@@ -128,7 +126,7 @@ namespace Robust.Server.Maps
}
}
if (_pauseManager.IsMapPaused(mapId))
if (_mapManager.IsMapPaused(mapId))
{
foreach (var entity in context.Entities)
{
@@ -141,7 +139,7 @@ namespace Robust.Server.Maps
public void SaveMap(MapId mapId, string yamlPath)
{
Logger.InfoS("map", $"Saving map {mapId} to {yamlPath}");
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager, _prototypeManager);
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _prototypeManager);
foreach (var grid in _mapManager.GetAllMapGrids(mapId))
{
context.RegisterGrid(grid);
@@ -207,7 +205,7 @@ namespace Robust.Server.Maps
LoadedMapData?.Invoke(data.Stream, resPath.ToString());
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager,
_prototypeManager, (YamlMappingNode) data.RootNode, mapId, options);
context.Deserialize();
@@ -226,12 +224,12 @@ namespace Robust.Server.Maps
private readonly IMapManagerInternal _mapManager;
private readonly ITileDefinitionManager _tileDefinitionManager;
private readonly IServerEntityManagerInternal _serverEntityManager;
private readonly IPauseManager _pauseManager;
private readonly IPrototypeManager _prototypeManager;
private readonly MapLoadOptions? _loadOptions;
private readonly Dictionary<GridId, int> GridIDMap = new();
public readonly List<MapGrid> Grids = new();
private readonly List<GridId> _readGridIndices = new();
private readonly Dictionary<EntityUid, int> EntityUidMap = new();
private readonly Dictionary<int, EntityUid> UidEntityMap = new();
@@ -260,12 +258,11 @@ namespace Robust.Server.Maps
public bool MapIsPostInit { get; private set; }
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs,
IServerEntityManagerInternal entities, IPauseManager pauseManager, IPrototypeManager prototypeManager)
IServerEntityManagerInternal entities, IPrototypeManager prototypeManager)
{
_mapManager = maps;
_tileDefinitionManager = tileDefs;
_serverEntityManager = entities;
_pauseManager = pauseManager;
_prototypeManager = prototypeManager;
RootNode = new YamlMappingNode();
@@ -283,13 +280,12 @@ namespace Robust.Server.Maps
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs,
IServerEntityManagerInternal entities,
IPauseManager pauseManager, IPrototypeManager prototypeManager,
IPrototypeManager prototypeManager,
YamlMappingNode node, MapId targetMapId, MapLoadOptions options)
{
_mapManager = maps;
_tileDefinitionManager = tileDefs;
_serverEntityManager = entities;
_pauseManager = pauseManager;
_loadOptions = options;
RootNode = node;
@@ -319,9 +315,8 @@ namespace Robust.Server.Maps
// Create the new map.
AllocMap();
// Load grids.
ReadTileMapSection();
ReadGridSection();
// Maps grid section indices to GridIds, for deserializing GridIds on entities.
ReadGridSectionIndices();
// Entities are first allocated. This allows us to know the future UID of all entities on the map before
// even ExposeData is loaded. This allows us to resolve serialized EntityUid instances correctly.
@@ -330,8 +325,11 @@ namespace Robust.Server.Maps
// Actually instance components and run ExposeData on them.
FinishEntitiesLoad();
// Finish binding MapGrids to their respective MapGridComponents.
BindGridComponents();
// Load grids.
ReadTileMapSection();
// Reads the grid section, allocates MapGrids, and maps them to their respective MapGridComponents.
ReadGridSection();
// Clear the net tick numbers so that components from prototypes (not modified by map)
// aren't sent over the wire initially.
@@ -479,18 +477,20 @@ namespace Robust.Server.Maps
}
}
private void BindGridComponents()
private void ReadGridSection()
{
// There were no new grids, nothing to do here.
if(Grids.Count == 0)
if(_readGridIndices.Count == 0)
return;
// MapGrids already contain their assigned GridId from their ctor, and the MapComponents just got deserialized.
// Now we need to actually bind the MapGrids to their components so that you can resolve GridId -> EntityUid
// After doing this, it should be 100% safe to use the MapManager API like normal.
var yamlGrids = RootNode.GetNode<YamlSequenceNode>("grids");
// get ents that the grids will bind to
var gridComps = new Dictionary<GridId, MapGridComponent>(Grids.Count);
var gridComps = new Dictionary<GridId, MapGridComponent>(_readGridIndices.Count);
// linear search for new grid comps
foreach (var tuple in _entitiesToDeserialize)
@@ -507,15 +507,54 @@ namespace Robust.Server.Maps
gridComps[gridComp.GridIndex] = gridComp;
}
// Actually bind them
foreach (var mapGrid in Grids)
for (var index = 0; index < _readGridIndices.Count; index++)
{
// Here is where the implicit index pairing magic happens from the yaml.
var gridIndex = _readGridIndices[index];
var yamlGrid = (YamlMappingNode)yamlGrids.Children[index];
// designed to throw if something is broken, every grid must map to an ent
var gridComp = gridComps[mapGrid.Index];
_mapManager.BindGrid(gridComp, mapGrid);
var gridComp = gridComps[gridIndex];
DebugTools.Assert(gridComp.GridIndex == gridIndex);
YamlMappingNode yamlGridInfo = (YamlMappingNode)yamlGrid["settings"];
YamlSequenceNode yamlGridChunks = (YamlSequenceNode)yamlGrid["chunks"];
var grid = AllocateMapGrid(gridComp, yamlGridInfo);
foreach (var chunkNode in yamlGridChunks.Cast<YamlMappingNode>())
{
YamlGridSerializer.DeserializeChunk(_mapManager, grid, chunkNode, _tileMap!, _tileDefinitionManager);
}
Grids.Add(grid); // Grids are kept in index order
}
}
private static MapGrid AllocateMapGrid(MapGridComponent gridComp, YamlMappingNode yamlGridInfo)
{
// sane defaults
ushort csz = 16;
ushort tsz = 1;
foreach (var kvInfo in yamlGridInfo)
{
var key = kvInfo.Key.ToString();
var val = kvInfo.Value.ToString();
if (key == "chunksize")
csz = ushort.Parse(val);
else if (key == "tilesize")
tsz = ushort.Parse(val);
else if (key == "snapsize")
continue; // obsolete
}
var grid = gridComp.AllocMapGrid(csz, tsz);
return grid;
}
private void AttachMapEntities()
{
var mapEntity = _mapManager.GetMapEntityIdOrThrow(TargetMap);
@@ -582,21 +621,15 @@ namespace Robust.Server.Maps
}
}
private void ReadGridSection()
private void ReadGridSectionIndices()
{
// sets up the mapping so the serializer can properly deserialize GridIds.
var yamlGrids = RootNode.GetNode<YamlSequenceNode>("grids");
foreach (var yamlGrid in yamlGrids)
for (var i = 0; i < yamlGrids.Children.Count; i++)
{
var grid = YamlGridSerializer.DeserializeGrid(
_mapManager, TargetMap,
(YamlMappingNode) yamlGrid["settings"],
(YamlSequenceNode) yamlGrid["chunks"],
_tileMap!,
_tileDefinitionManager
);
Grids.Add(grid);
_readGridIndices.Add(_mapManager.GenerateGridId(null));
}
}
@@ -612,7 +645,7 @@ namespace Robust.Server.Maps
if (!MapIsPostInit)
{
_pauseManager.AddUninitializedMap(TargetMap);
_mapManager.AddUninitializedMap(TargetMap);
}
}
}
@@ -719,7 +752,7 @@ namespace Robust.Server.Maps
var isPostInit = false;
foreach (var grid in Grids)
{
if (_pauseManager.IsMapInitialized(grid.ParentMapId))
if (_mapManager.IsMapInitialized(grid.ParentMapId))
{
isPostInit = true;
break;
@@ -928,6 +961,13 @@ namespace Robust.Server.Maps
return true;
}
[Virtual]
public class MapLoadException : Exception
{
public MapLoadException(string? message)
: base(message) { }
}
public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node,
IDependencyCollection dependencies,
bool skipHook,
@@ -936,19 +976,18 @@ namespace Robust.Server.Maps
// This is the code that deserializes the Grids index into the GridId. This has to happen between Grid allocation
// and when grids are bound to their entities.
if (node.Value == "null") return new DeserializedValue<GridId>(GridId.Invalid);
if (node.Value == "null")
{
throw new MapLoadException($"Error in map file: found local grid ID '{node.Value}' which does not exist.");
}
var val = int.Parse(node.Value);
if (val >= Grids.Count)
if (val >= _readGridIndices.Count)
{
Logger.ErrorS("map", "Error in map file: found local grid ID '{0}' which does not exist.", val);
}
else
{
return new DeserializedValue<GridId>(Grids[val].Index);
throw new MapLoadException($"Error in map file: found local grid ID '{val}' which does not exist.");
}
return new DeserializedValue<GridId>(GridId.Invalid);
return new DeserializedValue<GridId>(_readGridIndices[val]);
}
ValidationNode ITypeValidator<EntityUid, ValueDataNode>.Validate(ISerializationManager serializationManager,

View File

@@ -36,7 +36,7 @@ namespace Robust.Server.Maps
return gridn;
}
private static YamlNode SerializeChunk(IMapChunk chunk)
private static YamlNode SerializeChunk(MapChunk chunk)
{
var root = new YamlMappingNode();
var value = new YamlScalarNode($"{chunk.X},{chunk.Y}");
@@ -51,7 +51,7 @@ namespace Robust.Server.Maps
return root;
}
private static string SerializeTiles(IMapChunk chunk)
private static string SerializeTiles(MapChunk chunk)
{
// number of bytes written per tile, because sizeof(Tile) is useless.
const int structSize = 4;
@@ -68,7 +68,8 @@ namespace Robust.Server.Maps
{
var tile = chunk.GetTile(x, y);
writer.Write(tile.TypeId);
writer.Write(tile.Data);
writer.Write((byte)tile.Flags);
writer.Write(tile.Variant);
}
}
}
@@ -76,38 +77,10 @@ namespace Robust.Server.Maps
return Convert.ToBase64String(barr);
}
public static MapGrid DeserializeGrid(IMapManagerInternal mapMan, MapId mapId, YamlMappingNode info,
YamlSequenceNode chunks, IReadOnlyDictionary<ushort, string> tileDefMapping,
public static void DeserializeChunk(IMapManager mapMan, IMapGridInternal grid,
YamlMappingNode chunkData,
IReadOnlyDictionary<ushort, string> tileDefMapping,
ITileDefinitionManager tileDefinitionManager)
{
ushort csz = 0;
ushort tsz = 0;
float sgsz = 0.0f;
foreach (var kvInfo in info)
{
var key = kvInfo.Key.ToString();
var val = kvInfo.Value.ToString();
if (key == "chunksize")
csz = ushort.Parse(val);
else if (key == "tilesize")
tsz = ushort.Parse(val);
else if (key == "snapsize")
sgsz = float.Parse(val, CultureInfo.InvariantCulture);
}
//TODO: Pass in options
var grid = mapMan.CreateUnboundGrid(mapId);
foreach (var chunkNode in chunks.Cast<YamlMappingNode>())
{
DeserializeChunk(mapMan, grid, chunkNode, tileDefMapping, tileDefinitionManager);
}
return grid;
}
private static void DeserializeChunk(IMapManager mapMan, IMapGridInternal grid, YamlMappingNode chunkData, IReadOnlyDictionary<ushort, string> tileDefMapping, ITileDefinitionManager tileDefinitionManager)
{
var indNode = chunkData["ind"];
var tileNode = chunkData["tiles"];
@@ -129,12 +102,13 @@ namespace Robust.Server.Maps
for (ushort x = 0; x < grid.ChunkSize; x++)
{
var id = reader.ReadUInt16();
var data = reader.ReadUInt16();
var flags = (TileRenderFlag)reader.ReadByte();
var variant = reader.ReadByte();
var defName = tileDefMapping[id];
id = tileDefinitionManager[defName].TileId;
var tile = new Tile(id, data);
var tile = new Tile(id, flags, variant);
chunk.SetTile(x, y, tile);
}
}

View File

@@ -209,7 +209,7 @@ namespace Robust.Server.Placement
{
EntityCoordinates start = msg.EntityCoordinates;
Vector2 rectSize = msg.RectSize;
foreach (EntityUid entity in IoCManager.Resolve<IEntityLookup>().GetEntitiesIntersecting(start.GetMapId(_entityManager),
foreach (EntityUid entity in EntitySystem.Get<EntityLookupSystem>().GetEntitiesIntersecting(start.GetMapId(_entityManager),
new Box2(start.Position, start.Position + rectSize)))
{
if (_entityManager.Deleted(entity) || _entityManager.HasComponent<IMapGridComponent>(entity) || _entityManager.HasComponent<ActorComponent>(entity))

View File

@@ -44,6 +44,8 @@ namespace Robust.Server.Player
IReadOnlySet<EntityUid> ViewSubscriptions { get; }
int ViewSubscriptionCount { get; }
/// <summary>
/// Persistent data for this player.
/// </summary>

View File

@@ -39,6 +39,7 @@ namespace Robust.Server.Player
}
[ViewVariables] public IReadOnlySet<EntityUid> ViewSubscriptions => _viewSubscriptions;
public int ViewSubscriptionCount => _viewSubscriptions.Count;
[ViewVariables] public INetChannel ConnectedClient { get; }

View File

@@ -10,12 +10,12 @@
</PropertyGroup>
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
<!-- -->
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.7" />
<PackageReference Include="prometheus-net" Version="4.1.1" />
<PackageReference Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
<PackageReference Include="Microsoft.Extensions.Primitives" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Primitives" Version="6.0.0" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -50,7 +50,6 @@ namespace Robust.Server
IoCManager.Register<IMapManagerInternal, NetworkedMapManager>();
IoCManager.Register<INetworkedMapManager, NetworkedMapManager>();
IoCManager.Register<IEntityManager, ServerEntityManager>();
IoCManager.Register<IEntityLookup, EntityLookup>();
IoCManager.Register<IEntityNetworkManager, ServerEntityManager>();
IoCManager.Register<IServerEntityNetworkManager, ServerEntityManager>();
IoCManager.Register<IMapLoader, MapLoader>();

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -135,5 +135,17 @@ namespace Robust.Shared.Maths
{
return $"({Left}, {Bottom}, {Right}, {Top})";
}
/// <summary>
/// Multiplies each side of the box by the scalar.
/// </summary>
public Box2i Scale(int scalar)
{
return new Box2i(
Left * scalar,
Bottom * scalar,
Right * scalar,
Top * scalar);
}
}
}

View File

@@ -880,7 +880,7 @@ namespace Robust.Shared.Maths
return color.Value;
if (fallback.HasValue)
return fallback.Value;
throw new ArgumentException("Invalid color code and no fallback provided.", nameof(hexColor));
throw new ArgumentException($"Invalid color code \"{new string(hexColor)}\" and no fallback provided.", nameof(hexColor));
}
public static Color FromXaml(string name)

View File

@@ -8,7 +8,7 @@
</PropertyGroup>
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
<PackageReference Condition="'$(TargetFramework)' == 'net472'" Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.2" />
</ItemGroup>
</Project>

View File

@@ -130,6 +130,12 @@ namespace Robust.Shared.Maths
return new((int) MathF.Floor(X), (int) MathF.Floor(Y));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Vector2i Ceiled()
{
return new((int) MathF.Ceiling(X), (int) MathF.Ceiling(Y));
}
/// <summary>
/// Subtracts a vector from another, returning a new vector.
/// </summary>

View File

@@ -10,10 +10,10 @@
<Import Project="..\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>

View File

@@ -128,13 +128,13 @@ namespace Robust.Shared
/// The amount of new entities that can be sent to a client in a single game state, under PVS.
/// </summary>
public static readonly CVarDef<int> NetPVSNewEntityBudget =
CVarDef.Create("net.pvs_new_budget", 20, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
CVarDef.Create("net.pvs_new_budget", 20, CVar.ARCHIVE | CVar.REPLICATED);
/// <summary>
/// The amount of entity updates that can be sent to a client in a single game state, under PVS.
/// The amount of entered entities that can be sent to a client in a single game state, under PVS.
/// </summary>
public static readonly CVarDef<int> NetPVSEntityBudget =
CVarDef.Create("net.pvs_budget", 50, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
CVarDef.Create("net.pvs_budget", 50, CVar.ARCHIVE | CVar.REPLICATED);
/// <summary>
/// Log late input messages from clients.
@@ -1006,6 +1006,17 @@ namespace Robust.Shared
public static readonly CVarDef<bool> ResTexturePreloadCache =
CVarDef.Create("res.texture_preload_cache", true, CVar.CLIENTONLY);
/// <summary>
/// Override seekability of resource streams returned by ResourceManager.
/// See <see cref="ContentPack.StreamSeekMode"/> for int values.
/// </summary>
/// <remarks>
/// This is intended to be a debugging tool primarily.
/// Non-default seek modes WILL result in worse performance.
/// </remarks>
public static readonly CVarDef<int> ResStreamSeekMode =
CVarDef.Create("res.stream_seek_mode", (int)ContentPack.StreamSeekMode.None);
/*
* DEBUG
*/

View File

@@ -20,6 +20,16 @@ namespace Robust.Shared.Configuration
/// </summary>
void SetupNetworking();
/// <summary>
/// Get a replicated client CVar for a specific client.
/// </summary>
/// <typeparam name="T">CVar type.</typeparam>
/// <param name="channel">channel of the connected client.</param>
/// <param name="definition">The CVar.</param>
/// <returns>Replicated CVar of the client.</returns>
public T GetClientCVar<T>(INetChannel channel, CVarDef<T> definition) where T : notnull =>
GetClientCVar<T>(channel, definition.Name);
/// <summary>
/// Get a replicated client CVar for a specific client.
/// </summary>

View File

@@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Robust.Shared.Console;
public sealed class CommandBuffer
{
private const string DelayMarker = "-DELAY-";
private int _tickrate = 0;
private int _delay = 0;
private readonly LinkedList<string> _commandBuffer = new();
public void Append(string command)
{
_commandBuffer.AddLast(command);
}
public void Insert(string command)
{
_commandBuffer.AddFirst(command);
}
public void Tick(byte tickRate)
{
_tickrate = tickRate;
if (_delay > 0)
{
_delay -= 1;
}
}
public bool TryGetCommand([MaybeNullWhen(false)]out string command)
{
var next = _commandBuffer.First;
if (next is null) // nothing to do here
{
command = null;
return false;
}
if (next.Value.Equals(DelayMarker))
{
if (_delay == 0) // just finished
{
_commandBuffer.RemoveFirst();
return TryGetCommand(out command);
}
else // currently counting down delay
{
command = null;
return false;
}
}
if (next.Value.StartsWith("wait "))
{
var sTicks = next.Value.Substring(5);
_commandBuffer.RemoveFirst();
if (string.IsNullOrWhiteSpace(sTicks) || !int.TryParse(sTicks, out var ticks)) // messed up command
{
return TryGetCommand(out command);
}
// Setup Timing
_commandBuffer.AddFirst(DelayMarker);
_delay = ticks;
command = null;
return false;
}
// normal command
_commandBuffer.RemoveFirst();
command = next.Value;
return true;
}
}

View File

@@ -48,7 +48,7 @@ namespace Robust.Shared.Console.Commands
continue;
}
shell.ExecuteCommand(line);
shell.ConsoleHost.AppendCommand(line);
}
}
}

View File

@@ -7,6 +7,7 @@ using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Console
@@ -17,13 +18,16 @@ namespace Robust.Shared.Console
protected const string SawmillName = "con";
[Dependency] protected readonly ILogManager LogManager = default!;
[Dependency] protected readonly IReflectionManager ReflectionManager = default!;
[Dependency] private readonly IReflectionManager ReflectionManager = default!;
[Dependency] protected readonly INetManager NetManager = default!;
[Dependency] private readonly IDynamicTypeFactoryInternal _typeFactory = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[ViewVariables]
protected readonly Dictionary<string, IConsoleCommand> AvailableCommands = new();
private readonly CommandBuffer _commandBuffer = new CommandBuffer();
/// <inheritdoc />
public bool IsServer => NetManager.IsServer;
@@ -116,6 +120,36 @@ namespace Robust.Shared.Console
ExecuteCommand(null, command);
}
/// <inheritdoc />
public void AppendCommand(string command)
{
_commandBuffer.Append(command);
}
/// <inheritdoc />
public void InsertCommand(string command)
{
_commandBuffer.Insert(command);
}
/// <inheritdoc />
public void CommandBufferExecute()
{
_commandBuffer.Tick(_timing.TickRate);
while (_commandBuffer.TryGetCommand(out var cmd))
{
try
{
ExecuteCommand(cmd);
}
catch (Exception e)
{
LocalShell.WriteError(e.Message);
}
}
}
/// <summary>
/// A console command that was registered inline through <see cref="IConsoleHost"/>.
/// </summary>

View File

@@ -81,11 +81,35 @@ namespace Robust.Shared.Console
IConsoleShell GetSessionShell(ICommonSession session);
/// <summary>
/// Execute a command string on the local shell.
/// Execute a command string immediately on the local shell, bypassing the command buffer completely.
/// </summary>
/// <param name="command">Command string to execute.</param>
void ExecuteCommand(string command);
/// <summary>
/// Appends a command into the end of the command buffer on the local shell.
/// </summary>
/// <remarks>
/// This command will be ran *sometime* in the future, depending on how many waits are in the buffer.
/// </remarks>
/// <param name="command">Command string to execute.</param>
void AppendCommand(string command);
/// <summary>
/// Inserts a command into the front of the command buffer on the local shell.
/// </summary>
/// <remarks>
/// This command will preempt the next command executed in the command buffer.
/// </remarks>
/// <param name="command">Command string to execute.</param>
void InsertCommand(string command);
/// <summary>
/// Processes any contents of the command buffer on the local shell. This needs to be called regularly (once a tick),
/// inside the simulation. Pausing the server should prevent the buffer from being processed.
/// </summary>
void CommandBufferExecute();
/// <summary>
/// Executes a command string on this specific session shell. If the command does not exist, the command will be forwarded
/// to the

View File

@@ -81,6 +81,7 @@ namespace Robust.Shared.Containers
// spatially move the object to the location of the container. If you don't want this functionality, the
// calling code can save the local position before calling this function, and apply it afterwords.
transform.LocalPosition = Vector2.Zero;
transform.LocalRotation = Angle.Zero;
return true;
}

View File

@@ -209,6 +209,43 @@ namespace Robust.Shared.Containers
return false;
}
/// <summary>
/// Gets the top-most container in the hierarchy for this entity, if it exists.
/// </summary>
public bool TryGetOuterContainer(EntityUid uid, TransformComponent xform, [NotNullWhen(true)] out IContainer? container)
{
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
return TryGetOuterContainer(uid, xform, out container, xformQuery);
}
public bool TryGetOuterContainer(EntityUid uid, TransformComponent xform,
[NotNullWhen(true)] out IContainer? container, EntityQuery<TransformComponent> xformQuery)
{
container = null;
if (!uid.IsValid())
return false;
var conQuery = EntityManager.GetEntityQuery<ContainerManagerComponent>();
var child = uid;
var parent = xform.ParentUid;
while (parent.IsValid())
{
if (conQuery.TryGetComponent(parent, out var conManager) &&
conManager.TryGetContainer(child, out var parentContainer))
{
container = parentContainer;
}
var parentXform = xformQuery.GetComponent(parent);
child = parent;
parent = parentXform.ParentUid;
}
return container != null;
}
#endregion
// Eject entities from their parent container if the parent change is done by the transform only.

View File

@@ -111,7 +111,7 @@ namespace Robust.Shared.ContentPack
var fullStopwatch = Stopwatch.StartNew();
var resolver = CreateResolver();
using var peReader = new PEReader(assembly, PEStreamOptions.LeaveOpen);
using var peReader = ModLoader.MakePEReader(assembly, leaveOpen: true);
var reader = peReader.GetMetadataReader();
var asmName = reader.GetString(reader.GetAssemblyDefinition().Name);
@@ -853,13 +853,13 @@ namespace Robust.Shared.ContentPack
continue;
}
return ReaderFromStream(File.OpenRead(path));
return ModLoader.MakePEReader(File.OpenRead(path));
}
var extraStream = _parent.ExtraRobustLoader?.Invoke(dllName);
if (extraStream != null)
{
return ReaderFromStream(extraStream);
return ModLoader.MakePEReader(extraStream);
}
foreach (var resLoadPath in _resLoadPaths)
@@ -867,7 +867,7 @@ namespace Robust.Shared.ContentPack
try
{
var path = resLoadPath / dllName;
return ReaderFromStream(_parent._res.ContentFileRead(path));
return ModLoader.MakePEReader(_parent._res.ContentFileRead(path));
}
catch (FileNotFoundException)
{
@@ -877,23 +877,6 @@ namespace Robust.Shared.ContentPack
return null;
}
private static PEReader ReaderFromStream(Stream stream)
{
if (OperatingSystem.IsLinux() && stream is FileStream)
{
// PEReader is bugged on Linux and not properly thread safe when doing memory mapping.
// As such, we never pass it a file stream so it uses a different, non-bugged code path.
// See https://github.com/dotnet/runtime/issues/60545
var ms = new MemoryStream();
stream.CopyTo(ms);
ms.Seek(0, SeekOrigin.Begin);
stream.Dispose();
stream = ms;
}
return new PEReader(stream);
}
public PEReader? Resolve(string simpleName)
{
return _dictionary.GetOrAdd(simpleName, ResolveCore);

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