Compare commits

..

4 Commits

Author SHA1 Message Date
PJB3005
b422d3fb3e Version: 237.2.2 2025-09-19 09:17:39 +02:00
Skye
e4101aae8b Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:39 +02:00
PJB3005
74fe177985 Version: 237.2.1 2025-09-14 14:58:28 +02:00
PJB3005
bb90d79a3f Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

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

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

    Move CEF cache out of data directory

    Don't want content messing with this...

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

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

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

    Update SpaceWizards.NFluidSynth to 0.2.2

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

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
(cherry picked from commit 4413695c77fb705054c2f81fa18ec0a189b685dd)
2025-09-14 14:58:27 +02:00
105 changed files with 831 additions and 3826 deletions

View File

@@ -5,30 +5,30 @@ on:
- cron: "0 0 * * 0"
jobs:
docfx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 8.0.x
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore
- name: Install dependencies
run: dotnet restore
- name: Build Project
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- 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: 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 }}
- 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

@@ -2,32 +2,33 @@ name: Build & Test
on:
push:
branches: [master]
branches: [ master ]
pull_request:
branches: [master]
branches: [ master ]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest] # , macos-latest] - temporarily disabled due to libfreetype.dll errors.
os: [ubuntu-latest, windows-latest ] # , macos-latest] - temporarily disabled due to libfreetype.dll errors.
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
- uses: actions/checkout@v3.6.0
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- name: Robust.UnitTesting
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -- NUnit.ConsoleOut=0
- name: Robust.Analyzers.Tests
run: dotnet test --no-build Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj -- NUnit.ConsoleOut=0
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- name: Robust.UnitTesting
run: dotnet test --no-build Robust.UnitTesting/Robust.UnitTesting.csproj -- NUnit.ConsoleOut=0
- name: Robust.Analyzers.Tests
run: dotnet test --no-build Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj -- NUnit.ConsoleOut=0

View File

@@ -11,8 +11,14 @@
#
name: "CodeQL"
on:
workflow_dispatch
#on:
# push:
# branches: [ master ]
# pull_request:
# # The branches below must be a subset of the branches above
# branches: [ master ]
# schedule:
# - cron: '30 18 * * 6'
jobs:
analyze:
@@ -22,50 +28,50 @@ jobs:
strategy:
fail-fast: false
matrix:
language: ["csharp"]
language: [ 'csharp' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Checkout repository
uses: actions/checkout@v3.6.0
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 7.0.x
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 7.0.x
- name: Build
run: dotnet build
- name: Build
run: dotnet build
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -3,50 +3,51 @@
on:
push:
tags:
- "v*"
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Parse version
id: parse_version
shell: pwsh
run: |
$ver = [regex]::Match($env:GITHUB_REF, "refs/tags/v?(.+)").Groups[1].Value
echo ("::set-output name=version::{0}" -f $ver)
- name: Parse version
id: parse_version
shell: pwsh
run: |
$ver = [regex]::Match($env:GITHUB_REF, "refs/tags/v?(.+)").Groups[1].Value
echo ("::set-output name=version::{0}" -f $ver)
- uses: actions/checkout@v4.2.2
with:
submodules: true
- uses: actions/checkout@v3.6.0
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 8.0.x
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 8.0.x
- name: Package client
run: Tools/package_client_build.py -p windows mac linux
- name: Package client
run: Tools/package_client_build.py -p windows mac linux
- name: Shuffle files around
run: |
mkdir "release/${{ steps.parse_version.outputs.version }}"
mv release/*.zip "release/${{ steps.parse_version.outputs.version }}"
- name: Shuffle files around
run: |
mkdir "release/${{ steps.parse_version.outputs.version }}"
mv release/*.zip "release/${{ steps.parse_version.outputs.version }}"
- name: Upload files to Suns
uses: appleboy/scp-action@master
with:
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
source: "release/${{ steps.parse_version.outputs.version }}"
target: "/var/lib/robust-builds/builds/"
strip_components: 1
- name: Upload files to Suns
uses: appleboy/scp-action@master
with:
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
source: "release/${{ steps.parse_version.outputs.version }}"
target: "/var/lib/robust-builds/builds/"
strip_components: 1
- name: Update manifest JSON
uses: appleboy/ssh-action@master
with:
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }}
- name: Update manifest JSON
uses: appleboy/ssh-action@master
with:
host: suns.spacestation14.com
username: robust-build-push
key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }}
script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }}

View File

@@ -2,39 +2,40 @@ name: Test content master against engine
on:
push:
branches: [master]
branches: [ master ]
pull_request:
branches: [master]
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out content
uses: actions/checkout@v4.2.2
with:
repository: space-wizards/space-station-14
submodules: recursive
- name: Check out content
uses: actions/checkout@v3.6.0
with:
repository: space-wizards/space-station-14
submodules: recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 8.0.x
- name: Disable submodule autoupdate
run: touch BuildChecker/DISABLE_SUBMODULE_AUTOUPDATE
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 8.0.x
- name: Disable submodule autoupdate
run: touch BuildChecker/DISABLE_SUBMODULE_AUTOUPDATE
- name: Check out engine version
run: |
cd RobustToolbox
git fetch origin ${{ github.sha }}
git checkout FETCH_HEAD
git submodule update --init --recursive
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Tools --no-restore
- name: Content.Tests
run: dotnet test --no-build Content.Tests/Content.Tests.csproj -v n
- name: Content.IntegrationTests
run: COMPlus_gcServer=1 dotnet test --no-build Content.IntegrationTests/Content.IntegrationTests.csproj -v n
- name: Check out engine version
run: |
cd RobustToolbox
git fetch origin ${{ github.sha }}
git checkout FETCH_HEAD
git submodule update --init --recursive
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Tools --no-restore
- name: Content.Tests
run: dotnet test --no-build Content.Tests/Content.Tests.csproj -v n
- name: Content.IntegrationTests
run: COMPlus_gcServer=1 dotnet test --no-build Content.IntegrationTests/Content.IntegrationTests.csproj -v n

View File

@@ -58,7 +58,7 @@
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="System.Numerics.Vectors" Version="4.5.0" />

View File

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

View File

@@ -54,32 +54,10 @@ END TEMPLATE-->
*None yet*
## 237.4.0
### New features
* Implement automatic field-level delta states via AutoGenerateComponentState via opt-in.
### Bugfixes
* Remove redundant TransformComponentState bool.
## 237.2.2
## 237.3.0
### New features
* Added stack-like functions to `ValueList<T>` and added an `AddRange(ReadOnlySpan<T>)` overload.
* Added new `AssetPassFilterDrop`.
* Added a new RayCastSystem with the latest Box2D raycast + shapecasts implemented.
### Bugfixes
* Fixed `IPrototypeManager.TryGetKindFrom()` not working for prototypes with automatically inferred kind names.
### Other
* Sandbox error reference locator now works with generic method calls.
## 237.2.1
## 237.2.0

View File

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

View File

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

View File

@@ -504,7 +504,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
var (entity, component) = CreateAndStartPlayingStream(audioParams, stream);
component.Global = true;
component.Source.Global = true;
DirtyField(entity, component, nameof(AudioComponent.Global));
Dirty(entity, component);
return (entity, component);
}

View File

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

View File

@@ -5,6 +5,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Utility;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -240,7 +241,7 @@ namespace Robust.Client.GameObjects
#if DEBUG
var uid = GetEntity(netEntity);
if (TryComp(uid, out MetaDataComponent? meta))
if (TryComp<MetaDataComponent>(uid, out var meta))
{
DebugTools.Assert((meta.Flags & ( MetaDataFlags.Detached | MetaDataFlags.InContainer) ) == MetaDataFlags.Detached,
$"Adding entity {ToPrettyString(uid)} to list of expected entities for container {container.ID} in {ToPrettyString(container.Owner)}, despite it already being in a container.");

View File

@@ -26,7 +26,6 @@ namespace Robust.Client.GameStates
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly IConsoleHost _host = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
private const int HistorySize = 60 * 5; // number of ticks to keep in history.
@@ -79,7 +78,7 @@ namespace Robust.Client.GameStates
string? entStateString = null;
string? entDelString = null;
var conShell = _host.LocalShell;
var conShell = IoCManager.Resolve<IConsoleHost>().LocalShell;
var entStates = args.AppliedState.EntityStates;
if (entStates.HasContents)

View File

@@ -362,11 +362,6 @@ namespace Robust.Client.Graphics.Clyde
rect.BottomLeft, rect.BottomRight, color, subRegion);
}
public override void DrawTexture(Texture texture, Vector2 position, Color? modulate = null)
{
base.DrawTexture(texture, position, modulate);
}
/// <summary>
/// Draws an entity.
/// </summary>

View File

@@ -211,8 +211,6 @@ namespace Robust.Client.Graphics
public abstract void DrawLine(Vector2 from, Vector2 to, Color color);
public abstract void RenderInRenderTarget(IRenderTarget target, Action a, Color? clearColor);
public abstract void DrawTexture(Texture texture, Vector2 position, Color? modulate = null);
}
/// <summary>

View File

@@ -92,7 +92,7 @@ namespace Robust.Client.Graphics
public abstract void DrawTextureRectRegion(Texture texture, UIBox2 rect, UIBox2? subRegion = null, Color? modulate = null);
public override void DrawTexture(Texture texture, Vector2 position, Color? modulate = null)
public void DrawTexture(Texture texture, Vector2 position, Color? modulate = null)
{
CheckDisposed();

View File

@@ -74,7 +74,7 @@ namespace Robust.Client.Graphics
/// <remarks>
/// The sprite will have it's local dimensions calculated so that it has <see cref="EyeManager.PixelsPerMeter"/> texels per meter in the world.
/// </remarks>
public override void DrawTexture(Texture texture, Vector2 position, Color? modulate = null)
public void DrawTexture(Texture texture, Vector2 position, Color? modulate = null)
{
CheckDisposed();

View File

@@ -53,7 +53,7 @@ namespace Robust.Client.Graphics
/// <param name="fallback">If the character is not available, render "<22>" instead.</param>
/// <returns>How much to advance the cursor to draw the next character.</returns>
public abstract float DrawChar(
DrawingHandleBase handle, Rune rune, Vector2 baseline, float scale,
DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale,
Color color, bool fallback=true);
/// <summary>
@@ -109,7 +109,7 @@ namespace Robust.Client.Graphics
public override int GetDescent(float scale) => Handle.GetDescent(scale);
public override int GetLineHeight(float scale) => Handle.GetLineHeight(scale);
public override float DrawChar(DrawingHandleBase handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
public override float DrawChar(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
{
var metrics = Handle.GetCharMetrics(rune, scale);
if (!metrics.HasValue)
@@ -132,10 +132,7 @@ namespace Robust.Client.Graphics
}
baseline += new Vector2(metrics.Value.BearingX, -metrics.Value.BearingY);
if(handle is DrawingHandleWorld worldhandle)
worldhandle.DrawTextureRect(texture, Box2.FromDimensions(baseline, texture.Size));
else
handle.DrawTexture(texture, baseline, color);
handle.DrawTexture(texture, baseline, color);
return metrics.Value.Advance;
}
@@ -172,7 +169,7 @@ namespace Robust.Client.Graphics
public override int GetLineHeight(float scale) => _main.GetLineHeight(scale);
// DrawChar just proxies to the stack, or invokes _main's fallback.
public override float DrawChar(DrawingHandleBase handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
public override float DrawChar(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
{
foreach (var f in Stack)
{
@@ -210,7 +207,7 @@ namespace Robust.Client.Graphics
public override int GetDescent(float scale) => default;
public override int GetLineHeight(float scale) => default;
public override float DrawChar(DrawingHandleBase handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
public override float DrawChar(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
{
// Nada, it's a dummy after all.
return 0;

View File

@@ -13,9 +13,8 @@ namespace Robust.Client.Physics
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
{
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
public bool EnableDebug
{
@@ -29,7 +28,7 @@ namespace Robust.Client.Physics
if (_enableDebug)
{
var overlay = new GridSplitNodeOverlay(_mapManager, this, _transform, _map);
var overlay = new GridSplitNodeOverlay(_map, this, _transform);
_overlay.AddOverlay(overlay);
RaiseNetworkEvent(new RequestGridNodesMessage());
}
@@ -75,14 +74,12 @@ namespace Robust.Client.Physics
private readonly IMapManager _mapManager;
private readonly GridFixtureSystem _system;
private readonly SharedTransformSystem _transform;
private readonly SharedMapSystem _map;
public GridSplitNodeOverlay(IMapManager mapManager, GridFixtureSystem system, SharedTransformSystem transform, SharedMapSystem map)
public GridSplitNodeOverlay(IMapManager mapManager, GridFixtureSystem system, SharedTransformSystem transform)
{
_mapManager = mapManager;
_system = system;
_transform = transform;
_map = map;
}
protected internal override void Draw(in OverlayDrawArgs args)
@@ -92,7 +89,7 @@ namespace Robust.Client.Physics
var state = (_system, _transform, args.WorldBounds, worldHandle);
_mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref state,
(EntityUid uid, MapGridComponent grid,
static (EntityUid uid, MapGridComponent grid,
ref (GridFixtureSystem system, SharedTransformSystem transform, Box2Rotated worldBounds, DrawingHandleWorld worldHandle) tuple) =>
{
// May not have received nodes yet.
@@ -100,7 +97,7 @@ namespace Robust.Client.Physics
return true;
tuple.worldHandle.SetTransform(tuple.transform.GetWorldMatrix(uid));
var chunkEnumerator = _map.GetMapChunks(uid, grid, tuple.worldBounds);
var chunkEnumerator = grid.GetMapChunks(tuple.worldBounds);
while (chunkEnumerator.MoveNext(out var chunk))
{

View File

@@ -25,9 +25,7 @@ namespace Robust.Client.Placement.Modes
{
var viewportSize = (Vector2)pManager.Clyde.ScreenSize;
var gridUid = pManager.EntityManager.System<SharedTransformSystem>().GetGrid(MouseCoords);
var gridPosition = pManager.EntityManager.System<SharedMapSystem>().MapToGrid(gridUid!.Value, pManager.EyeManager.ScreenToMap(Vector2.Zero));
var gridPosition = Grid.MapToGrid(pManager.EyeManager.ScreenToMap(Vector2.Zero));
var gridstart = pManager.EyeManager.CoordinatesToScreen(
gridPosition.WithPosition(new Vector2(MathF.Floor(gridPosition.X), MathF.Floor(gridPosition.Y))));
@@ -54,9 +52,9 @@ namespace Robust.Client.Placement.Modes
{
MouseCoords = ScreenToCursorGrid(mouseScreen);
var gridIdOpt = pManager.EntityManager.System<SharedTransformSystem>().GetGrid(MouseCoords);
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
SnapSize = 1f;
if (gridIdOpt is { } gridId && gridId.IsValid())
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
{
Grid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
SnapSize = Grid.TileSize; //Find snap size for the grid.

View File

@@ -27,7 +27,6 @@ namespace Robust.Client.Placement
{
public sealed partial class PlacementManager : IPlacementManager, IDisposable, IEntityEventSubscriber
{
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IClientNetManager _networkManager = default!;
[Dependency] internal readonly IPlayerManager PlayerManager = default!;
[Dependency] internal readonly IResourceCache ResourceCache = default!;
@@ -43,11 +42,6 @@ namespace Robust.Client.Placement
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] internal readonly IClyde Clyde = default!;
private ISawmill _sawmill = default!;
private SharedMapSystem Maps => EntityManager.System<SharedMapSystem>();
private SharedTransformSystem XformSystem => EntityManager.System<SharedTransformSystem>();
/// <summary>
/// How long before a pending tile change is dropped.
/// </summary>
@@ -194,7 +188,6 @@ namespace Robust.Client.Placement
public void Initialize()
{
_drawingShader = _prototypeManager.Index<ShaderPrototype>("unshaded").Instance();
_sawmill = _logManager.GetSawmill("placement");
_networkManager.RegisterNetMessage<MsgPlacement>(HandlePlacementMessage);
@@ -344,11 +337,7 @@ namespace Robust.Client.Placement
private void HandleTileChanged(ref TileChangedEvent args)
{
var coords = Maps.GridTileToLocal(
args.NewTile.GridUid,
EntityManager.GetComponent<MapGridComponent>(args.NewTile.GridUid),
args.NewTile.GridIndices);
var coords = EntityManager.GetComponent<MapGridComponent>(args.NewTile.GridUid).GridTileToLocal(args.NewTile.GridIndices);
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
}
@@ -486,7 +475,7 @@ namespace Robust.Client.Placement
if (!_modeDictionary.TryFirstOrNull(pair => pair.Key.Equals(CurrentPermission.PlacementOption), out KeyValuePair<string, Type>? placeMode))
{
_sawmill.Log(LogLevel.Warning, $"Invalid placement mode `{CurrentPermission.PlacementOption}`");
Logger.LogS(LogLevel.Warning, nameof(PlacementManager), $"Invalid placement mode `{CurrentPermission.PlacementOption}`");
Clear();
return;
}
@@ -542,7 +531,8 @@ namespace Robust.Client.Placement
coordinates = new EntityCoordinates();
return false;
}
coordinates = XformSystem.ToCoordinates(ent, EyeManager.PixelToMap(InputManager.MouseScreenPosition));
coordinates = EntityCoordinates.FromMap(MapManager,
EyeManager.PixelToMap(InputManager.MouseScreenPosition));
return true;
}
}
@@ -658,7 +648,7 @@ namespace Robust.Client.Placement
PlayerManager.LocalEntity is not {Valid: true} controlled)
return;
var worldPos = XformSystem.GetWorldPosition(controlled);
var worldPos = EntityManager.GetComponent<TransformComponent>(controlled).WorldPosition;
args.WorldHandle.DrawCircle(worldPos, CurrentPermission.Range, new Color(1, 1, 1, 0.25f));
}
@@ -764,14 +754,14 @@ namespace Robust.Client.Placement
if (CurrentPermission.IsTile)
{
var gridIdOpt = XformSystem.GetGrid(coordinates);
var gridIdOpt = EntityManager.System<SharedTransformSystem>().GetGrid(coordinates);
// If we have actually placed something on a valid grid...
if (gridIdOpt is { } gridId && gridId.IsValid())
{
var grid = EntityManager.GetComponent<MapGridComponent>(gridId);
// no point changing the tile to the same thing.
if (Maps.GetTileRef(gridId, grid, coordinates).Tile.TypeId == CurrentPermission.TileType)
if (EntityManager.System<SharedMapSystem>().GetTileRef(gridId, grid, coordinates).Tile.TypeId == CurrentPermission.TileType)
return;
}

View File

@@ -143,9 +143,9 @@ namespace Robust.Client.ResourceManagement
}
});
var atlasLookup = rsiList.ToLookup(ShouldMetaAtlas);
var atlasList = atlasLookup[true].ToArray();
var nonAtlasList = atlasLookup[false].ToArray();
// Do not meta-atlas RSIs with custom load parameters.
var atlasList = rsiList.Where(x => x.LoadParameters == TextureLoadParameters.Default).ToArray();
var nonAtlasList = rsiList.Where(x => x.LoadParameters != TextureLoadParameters.Default).ToArray();
foreach (var data in nonAtlasList)
{
@@ -225,9 +225,8 @@ namespace Robust.Client.ResourceManagement
void FinalizeMetaAtlas(int toIndex, Image<Rgba32> sheet)
{
var fromIndex = finalized + 1;
var atlas = Clyde.LoadTextureFromImage(sheet, $"Meta atlas {fromIndex}-{toIndex}");
for (int i = fromIndex; i <= toIndex; i++)
var atlas = Clyde.LoadTextureFromImage(sheet);
for (int i = finalized + 1; i <= toIndex; i++)
{
var rsi = atlasList[i];
rsi.AtlasTexture = atlas;
@@ -283,11 +282,7 @@ namespace Robust.Client.ResourceManagement
nonAtlasList.Length,
errors,
sw.Elapsed);
}
private static bool ShouldMetaAtlas(RSIResource.LoadStepData rsi)
{
return rsi.MetaAtlas && rsi.LoadParameters == TextureLoadParameters.Default;
}
}
}

View File

@@ -183,7 +183,6 @@ namespace Robust.Client.ResourceManagement
data.DimX = dimensionX;
data.CallbackOffsets = callbackOffsets;
data.LoadParameters = metadata.LoadParameters;
data.MetaAtlas = metadata.MetaAtlas;
}
internal static void LoadPostTexture(LoadStepData data)
@@ -387,7 +386,6 @@ namespace Robust.Client.ResourceManagement
public Vector2i AtlasOffset;
public RSI Rsi = default!;
public TextureLoadParameters LoadParameters;
public bool MetaAtlas;
}
internal struct StateReg

View File

@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
@@ -86,6 +89,8 @@ public sealed class ColorSelectorSliders : Control
private OptionButton _typeSelector;
private List<ColorSelectorType> _types = new();
private static ShaderInstance _shader = default!;
private ColorSelectorStyleBox _topStyle;
private ColorSelectorStyleBox _middleStyle;
private ColorSelectorStyleBox _bottomStyle;

View File

@@ -302,6 +302,7 @@ namespace Robust.Client.UserInterface.Controls
{
if (!child.Visible)
{
index--;
continue;
}

View File

@@ -176,7 +176,7 @@ namespace Robust.Client.UserInterface
public readonly void Draw(
MarkupTagManager tagManager,
DrawingHandleBase handle,
DrawingHandleScreen handle,
Font defaultFont,
UIBox2 drawBox,
float verticalOffset,

View File

@@ -1,18 +0,0 @@
namespace Robust.Packaging.AssetProcessing.Passes;
/// <summary>
/// Asset pass that drops all files that match a predicate. Files that do not match are ignored.
/// </summary>
public sealed class AssetPassFilterDrop(Func<AssetFile, bool> predicate) : AssetPass
{
public Func<AssetFile, bool> Predicate { get; } = predicate;
protected override AssetFileAcceptResult AcceptFile(AssetFile file)
{
// Just do nothing with the file so it gets discarded.
if (Predicate(file))
return AssetFileAcceptResult.Consumed;
return base.AcceptFile(file);
}
}

View File

@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -14,8 +13,6 @@ public class Generator : IIncrementalGenerator
private const string TypeCopierInterfaceNamespace = "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopier";
private const string TypeCopyCreatorInterfaceNamespace = "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopyCreator";
private const string SerializationHooksNamespace = "Robust.Shared.Serialization.ISerializationHooks";
private const string AutoStateAttributeName = "Robust.Shared.Analyzers.AutoGenerateComponentStateAttribute";
private const string ComponentDeltaInterfaceName = "Robust.Shared.GameObjects.IComponentDelta";
public void Initialize(IncrementalGeneratorInitializationContext initContext)
{
@@ -38,7 +35,6 @@ public class Generator : IIncrementalGenerator
var builder = new StringBuilder();
var containingTypes = new Stack<INamedTypeSymbol>();
var declarationsGenerated = new HashSet<string>();
var deltaType = compilation.GetTypeByMetadataName(ComponentDeltaInterfaceName)!;
foreach (var declaration in declarations)
{
@@ -110,9 +106,9 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
{
{{GetConstructor(definition)}}
{{GetCopyMethods(definition, deltaType)}}
{{GetCopyMethods(definition)}}
{{GetInstantiators(definition, deltaType)}}
{{GetInstantiators(definition)}}
}
{{containingTypesEnd}}
@@ -196,7 +192,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
return builder.ToString();
}
private static string GetCopyMethods(DataDefinition definition, ITypeSymbol deltaType)
private static string GetCopyMethods(DataDefinition definition)
{
var builder = new StringBuilder();
@@ -267,7 +263,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
{{baseCopy}}
""");
foreach (var @interface in InternalGetImplicitDataDefinitionInterfaces(definition.Type, true, deltaType))
foreach (var @interface in GetImplicitDataDefinitionInterfaces(definition.Type, true))
{
var interfaceModifiers = baseType != null && baseType.AllInterfaces.Contains(@interface, SymbolEqualityComparer.Default)
? "override "
@@ -296,7 +292,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
return builder.ToString();
}
private static string GetInstantiators(DataDefinition definition, ITypeSymbol deltaType)
private static string GetInstantiators(DataDefinition definition)
{
var builder = new StringBuilder();
var modifiers = string.Empty;
@@ -330,7 +326,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
""");
}
foreach (var @interface in InternalGetImplicitDataDefinitionInterfaces(definition.Type, false, deltaType))
foreach (var @interface in GetImplicitDataDefinitionInterfaces(definition.Type, false))
{
var interfaceName = @interface.ToDisplayString();
builder.AppendLine($$"""
@@ -349,31 +345,6 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
return builder.ToString();
}
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
private static IEnumerable<ITypeSymbol> InternalGetImplicitDataDefinitionInterfaces(ITypeSymbol type, bool all, ITypeSymbol deltaType)
{
var symbols = GetImplicitDataDefinitionInterfaces(type, all);
// TODO SOURCE GEN
// fix this jank
// The comp-state source generator will add an "IComponentDelta" interface to classes with the auto state
// attribute, and this generator creates methods that those classes then have to implement because
// IComponentDelta a DataDefinition via the ImplicitDataDefinitionForInheritorsAttribute on IComponent.
if (!TryGetAttribute(type, AutoStateAttributeName, out var data))
return symbols;
// If it doesn't have field deltas then ignore
if (data.ConstructorArguments[1] is not { Value: bool fields and true })
{
return symbols;
}
if (symbols.Any(x => x.ToDisplayString() == deltaType.ToDisplayString()))
return symbols;
return symbols.Append(deltaType);
}
// TODO serveronly? do we care? who knows!!
private static StringBuilder CopyDataFields(DataDefinition definition)
{

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -303,21 +302,6 @@ internal static class Types
return false;
}
internal static bool TryGetAttribute(ISymbol symbol, string attributeName, [NotNullWhen(true)] out AttributeData? data)
{
foreach (var attribute in symbol.GetAttributes())
{
if (attribute.AttributeClass?.ToDisplayString() == attributeName)
{
data = attribute;
return true;
}
}
data = null;
return false;
}
internal static IEnumerable<ITypeSymbol> GetBaseTypes(ITypeSymbol type)
{
var baseType = type.BaseType;

View File

@@ -223,10 +223,10 @@ namespace Robust.Server
if (!Path.IsPathRooted(fullPath))
{
fullPath = PathHelpers.ExecutableRelativeFile(fullPath);
logPath = PathHelpers.ExecutableRelativeFile(fullPath);
}
logHandler = new FileLogHandler(fullPath);
logHandler = new FileLogHandler(logPath);
}
_log.RootSawmill.Level = _config.GetCVar(CVars.LogLevel);
@@ -297,7 +297,7 @@ namespace Robust.Server
: null;
// Set up the VFS
_resources.Initialize(dataDir);
_resources.Initialize(dataDir, hideUserDataDir: false);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;

View File

@@ -143,7 +143,7 @@ namespace Robust.Server.Placement
_factory.GetComponentName(typeof(PlacementReplacementComponent)), out var compRegistry))
{
var key = ((PlacementReplacementComponent)compRegistry.Component).Key;
var gridUid = _xformSystem.GetGrid(coordinates);
var gridUid = coordinates.GetGridUid(_entityManager);
if (_entityManager.TryGetComponent<MapGridComponent>(gridUid, out var grid))
{
@@ -191,7 +191,7 @@ namespace Robust.Server.Placement
EntityUid gridId = coordinates.EntityId;
if (_entityManager.TryGetComponent(coordinates.EntityId, out grid)
|| _mapManager.TryFindGridAt(_xformSystem.ToMapCoordinates(coordinates), out gridId, out grid))
|| _mapManager.TryFindGridAt(coordinates.ToMap(_entityManager, _xformSystem), out gridId, out grid))
{
_maps.SetTile(gridId, grid, coordinates, new Tile(tileType));
@@ -200,13 +200,10 @@ namespace Robust.Server.Placement
}
else if (tileType != 0) // create a new grid
{
var newGrid = _mapManager.CreateGridEntity(_xformSystem.GetMapId(coordinates));
var newGridXform = new Entity<TransformComponent>(
newGrid.Owner,
_entityManager.GetComponent<TransformComponent>(newGrid));
var newGrid = _mapManager.CreateGridEntity(coordinates.GetMapId(_entityManager));
var newGridXform = _entityManager.GetComponent<TransformComponent>(newGrid);
_xformSystem.SetWorldPosition(newGridXform, coordinates.Position - newGrid.Comp.TileSizeHalfVector); // assume bottom left tile origin
var tilePos = _maps.WorldToTile(newGrid.Owner, newGrid.Comp, coordinates.Position);
var tilePos = newGrid.Comp.WorldToTile(coordinates.Position);
_maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType));
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
@@ -231,7 +228,7 @@ namespace Robust.Server.Placement
{
EntityCoordinates start = _entityManager.GetCoordinates(msg.NetCoordinates);
Vector2 rectSize = msg.RectSize;
foreach (var entity in _lookup.GetEntitiesIntersecting(_xformSystem.GetMapId(start),
foreach (var entity in _lookup.GetEntitiesIntersecting(start.GetMapId(_entityManager),
new Box2(start.Position, start.Position + rectSize)))
{
if (_entityManager.Deleted(entity) ||

View File

@@ -24,6 +24,7 @@ namespace Robust.Server.ServerStatus;
internal sealed partial class StatusHost
{
private (string binFolder, string[] assemblies)? _aczInfo;
private IMagicAczProvider? _magicAczProvider;
private IFullHybridAczProvider? _fullHybridAczProvider;
@@ -157,7 +158,8 @@ internal sealed partial class StatusHost
{
_aczSawmill.Verbose("Using default magic ACZ provider");
// Default provider
var (binFolderPath, assemblyNames) = ("Content.Client", new[] { "Content.Client", "Content.Shared" });
var (binFolderPath, assemblyNames) =
_aczInfo ?? ("Content.Client", new[] { "Content.Client", "Content.Shared" });
var info = new DefaultMagicAczInfo(binFolderPath, assemblyNames);
provider = new DefaultMagicAczProvider(info, _deps);

View File

@@ -103,22 +103,6 @@ namespace Robust.Server.ServerStatus
["desc"] = _serverDescCache,
};
var privacyPolicyLink = _cfg.GetCVar(CVars.StatusPrivacyPolicyLink);
var privacyPolicyIdentifier = _cfg.GetCVar(CVars.StatusPrivacyPolicyIdentifier);
var privacyPolicyVersion = _cfg.GetCVar(CVars.StatusPrivacyPolicyVersion);
if (!string.IsNullOrEmpty(privacyPolicyLink)
&& !string.IsNullOrEmpty(privacyPolicyIdentifier)
&& !string.IsNullOrEmpty(privacyPolicyVersion))
{
jObject["privacy_policy"] = new JsonObject
{
["identifier"] = privacyPolicyIdentifier,
["version"] = privacyPolicyVersion,
["link"] = privacyPolicyLink,
};
}
OnInfoRequest?.Invoke(jObject);
await context.RespondJsonAsync(jObject);

View File

@@ -36,9 +36,7 @@ namespace Robust.Shared.CompNetworkGenerator
private static readonly SymbolDisplayFormat FullNullableFormat =
FullyQualifiedFormat.WithMiscellaneousOptions(IncludeNullableReferenceTypeModifier);
private static string? GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp,
bool raiseAfterAutoHandle,
bool fieldDeltas)
private static string? GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp, bool raiseAfterAutoHandle)
{
var nameSpace = classSymbol.ContainingNamespace.ToDisplayString();
var componentName = classSymbol.Name;
@@ -136,154 +134,54 @@ namespace Robust.Shared.CompNetworkGenerator
// component.Count = state.Count;
var handleStateSetters = new StringBuilder();
// Builds the string for copying delta fields in IComponentDelta to a new full state.
var deltaCreate = new StringBuilder();
// Delta field states
var deltaGetFields = new StringBuilder();
var deltaHandleFields = new StringBuilder();
// Apply the delta field to the full state.
var deltaApply = new List<string>();
var index = -1;
var fieldsStr = new StringBuilder();
var fieldStates = new StringBuilder();
var networkedTypes = new List<string>();
foreach (var (type, name) in fields)
{
index++;
if (index == 0)
{
fieldsStr.Append(@$"""{name}""");
}
else
{
fieldsStr.Append(@$", ""{name}""");
}
var typeDisplayStr = type.ToDisplayString(FullNullableFormat);
var nullable = type.NullableAnnotation == NullableAnnotation.Annotated;
var nullableAnnotation = nullable ? "?" : string.Empty;
string deltaStateName = $"{name}_FieldComponentState";
// The type used for networking, e.g. EntityUid -> NetEntity
string networkedType;
string getField;
string? cast;
// TODO: Uhh I just need casts or something.
var castString = typeDisplayStr.Substring(8);
deltaGetFields.Append(@$"
case {Math.Pow(2, index)}:
args.State = new {deltaStateName}()
{{
");
deltaHandleFields.Append(@$"
case {deltaStateName} {deltaStateName}_State:
{{
");
var fieldHandleValue = $"{deltaStateName}_State.{name}!";
switch (typeDisplayStr)
{
case GlobalEntityUidName:
case GlobalNullableEntityUidName:
networkedType = $"NetEntity{nullableAnnotation}";
stateFields.Append($@"
public {networkedType} {name} = default!;");
getField = $"GetNetEntity(component.{name})";
cast = $"(NetEntity{nullableAnnotation})";
public NetEntity{nullableAnnotation} {name} = default!;");
getStateInit.Append($@"
{name} = GetNetEntity(component.{name}),");
handleStateSetters.Append($@"
component.{name} = EnsureEntity<{componentName}>(state.{name}, uid);");
deltaHandleFields.Append($@"
component.{name} = EnsureEntity<{componentName}>({cast} {fieldHandleValue}, uid);");
deltaCreate.Append($@"
{name} = fullState.{name},");
deltaApply.Add($@"
fullState.{name} = {name};");
component.{name} = EnsureEntity<{componentName}>(state.{name}, uid);");
break;
case GlobalEntityCoordinatesName:
case GlobalNullableEntityCoordinatesName:
networkedType = $"NetCoordinates{nullableAnnotation}";
stateFields.Append($@"
public {networkedType} {name} = default!;");
getField = $"GetNetCoordinates(component.{name})";
cast = $"(NetCoordinates{nullableAnnotation})";
public NetCoordinates{nullableAnnotation} {name} = default!;");
getStateInit.Append($@"
{name} = GetNetCoordinates(component.{name}),");
handleStateSetters.Append($@"
component.{name} = EnsureCoordinates<{componentName}>(state.{name}, uid);");
deltaHandleFields.Append($@"
component.{name} = EnsureCoordinates<{componentName}>({cast} {fieldHandleValue}, uid);");
deltaCreate.Append($@"
{name} = fullState.{name},");
deltaApply.Add($@"
fullState.{name} = {name};");
component.{name} = EnsureCoordinates<{componentName}>(state.{name}, uid);");
break;
case GlobalEntityUidSetName:
networkedType = $"{GlobalNetEntityUidSetName}";
stateFields.Append($@"
public {networkedType} {name} = default!;");
getField = $"GetNetEntitySet(component.{name})";
cast = $"({GlobalNetEntityUidSetName})";
public {GlobalNetEntityUidSetName} {name} = default!;");
getStateInit.Append($@"
{name} = GetNetEntitySet(component.{name}),");
handleStateSetters.Append($@"
EnsureEntitySet<{componentName}>(state.{name}, uid, component.{name});");
deltaHandleFields.Append($@"
EnsureEntitySet<{componentName}>({cast} {fieldHandleValue}, uid, component.{name});");
deltaCreate.Append($@"
{name} = new(fullState.{name}),");
deltaApply.Add($@"
fullState.{name} = {name};");
EnsureEntitySet<{componentName}>(state.{name}, uid, component.{name});");
break;
case GlobalEntityUidListName:
networkedType = $"{GlobalNetEntityUidListName}";
stateFields.Append($@"
public {networkedType} {name} = default!;");
getField = $"GetNetEntityList(component.{name})";
cast = $"({GlobalNetEntityUidListName})";
public {GlobalNetEntityUidListName} {name} = default!;");
getStateInit.Append($@"
{name} = GetNetEntityList(component.{name}),");
handleStateSetters.Append($@"
EnsureEntityList<{componentName}>(state.{name}, uid, component.{name});");
deltaHandleFields.Append($@"
EnsureEntityList<{componentName}>({cast} {fieldHandleValue}, uid, component.{name});");
deltaCreate.Append($@"
{name} = new(fullState.{name}),");
deltaApply.Add($@"
fullState.{name} = {name};");
EnsureEntityList<{componentName}>(state.{name}, uid, component.{name});");
break;
default:
@@ -307,150 +205,69 @@ namespace Robust.Shared.CompNetworkGenerator
ensureGeneric = componentName;
}
networkedType = $"Dictionary<{key}, {value}>";
stateFields.Append($@"
public {networkedType} {name} = default!;");
public Dictionary<{key}, {value}> {name} = default!;");
getField = $"GetNetEntityDictionary(component.{name})";
getStateInit.Append($@"
{name} = GetNetEntityDictionary(component.{name}),");
if (valueNullable && value is not GlobalNetEntityName and not GlobalNetEntityNullableName)
{
cast = $"(Dictionary<{key}, {value}>)";
handleStateSetters.Append($@"
EnsureEntityDictionaryNullableValue<{componentName}, {value}>(state.{name}, uid, component.{name});");
deltaHandleFields.Append($@"
EnsureEntityDictionaryNullableValue<{componentName}, {value}>({cast} {fieldHandleValue}, uid, component.{name});");
EnsureEntityDictionaryNullableValue<{componentName}, {value}>(state.{name}, uid, component.{name});");
}
else
{
cast = $"({castString})";
handleStateSetters.Append($@"
EnsureEntityDictionary<{ensureGeneric}>(state.{name}, uid, component.{name})");
deltaHandleFields.Append($@"
EnsureEntityDictionary<{ensureGeneric}>({cast} {fieldHandleValue}, uid, component.{name});");
EnsureEntityDictionary<{ensureGeneric}>(state.{name}, uid, component.{name});");
}
deltaCreate.Append($@"
{name} = new(fullState.{name}),");
deltaApply.Add($@"
fullState.{name} = {name};");
break;
}
if (value is GlobalEntityUidName or GlobalNullableEntityUidName)
{
networkedType = $"Dictionary<{key}, {value}>";
value = valueNullable ? GlobalNetEntityNullableName : GlobalNetEntityName;
stateFields.Append($@"
public {networkedType} {name} = default!;");
getField = $"GetNetEntityDictionary(component.{name})";
cast = $"(Dictionary<{key}, {value}>)";
public Dictionary<{key}, {value}> {name} = default!;");
getStateInit.Append($@"
{name} = GetNetEntityDictionary(component.{name}),");
handleStateSetters.Append($@"
EnsureEntityDictionary<{componentName}, {key}>(state.{name}, uid, component.{name});");
deltaHandleFields.Append($@"
EnsureEntityDictionary<{componentName}, {key}>({cast} {fieldHandleValue}, uid, component.{name});");
deltaCreate.Append($@"
{name} = new(fullState.{name}),");
deltaApply.Add($@"
fullState.{name} = {name};");
EnsureEntityDictionary<{componentName}, {key}>(state.{name}, uid, component.{name});");
break;
}
}
networkedType = $"{typeDisplayStr}";
stateFields.Append($@"
public {networkedType} {name} = default!;");
public {typeDisplayStr} {name} = default!;");
if (IsCloneType(type))
{
getField = $"component.{name}";
cast = $"({castString})";
var nullCast = nullable ? castString.Substring(0, castString.Length - 1) : castString;
// get first ctor arg of the field attribute, which determines whether the field should be cloned
// (like if its a dict or list)
getStateInit.Append($@"
{name} = component.{name},");
handleStateSetters.Append($@"
if (state.{name} == null)
component.{name} = null!;
else
component.{name} = new(state.{name});");
deltaHandleFields.Append($@"
var {name}Value = {cast} {fieldHandleValue};
if ({name}Value == null)
component.{name} = null!;
else
component.{name} = new {nullCast}({name}Value);");
if (nullable)
{
deltaCreate.Append($@"
{name} = fullState.{name} == null ? null : new(fullState.{name}),");
}
else
{
deltaCreate.Append($@"
{name} = new(fullState.{name}),");
}
deltaApply.Add($@"
if ({name} == null)
fullState.{name} = null!;
else
fullState.{name} = new({name});");
}
else
{
getField = $"component.{name}";
cast = $"({castString})";
getStateInit.Append($@"
{name} = component.{name},");
handleStateSetters.Append($@"
component.{name} = state.{name};");
deltaHandleFields.Append($@"
component.{name} = {cast} {fieldHandleValue};");
deltaCreate.Append($@"
{name} = fullState.{name},");
deltaApply.Add($@"
fullState.{name} = {name};");
}
break;
}
/*
* End loop stuff
*/
networkedTypes.Add(networkedType);
getStateInit.Append($@"
{name} = {getField},");
deltaGetFields.Append(@$" {name} = {getField}
}};
return;");
deltaHandleFields.Append($@"
}}
break;");
}
var eventRaise = "";
@@ -461,94 +278,19 @@ namespace Robust.Shared.CompNetworkGenerator
EntityManager.EventBus.RaiseComponentEvent(uid, component, ref ev);";
}
var deltaGetState = "";
var deltaHandleState = "";
var deltaInterface = "";
var deltaCompFields = "";
var deltaNetRegister = "";
if (fieldDeltas)
{
for (var i = 0; i < fields.Count; i++)
{
var name = fields[i].FieldName;
string deltaStateName = $"{name}_FieldComponentState";
var networkedType = networkedTypes[i];
var apply = deltaApply[i];
// Creates a state per field
fieldStates.Append($@"
[Serializable, NetSerializable]
public sealed class {deltaStateName} : IComponentDeltaState<{stateName}>
{{
public {networkedType} {name} = default!;
public void ApplyToFullState({stateName} fullState)
{{{apply}
}}
public {stateName} CreateNewFullState({stateName} fullState)
{{
var newState = new {stateName}
{{{deltaCreate}
}};
{apply}
return newState;
}}
}}
");
}
deltaNetRegister = $@"EntityManager.ComponentFactory.RegisterNetworkedFields<{classSymbol}>({fieldsStr});";
deltaGetState = @$"// Delta state
if (component is IComponentDelta delta && args.FromTick > component.CreationTick && delta.LastFieldUpdate >= args.FromTick)
{{
var fields = EntityManager.GetModifiedFields(component, args.FromTick);
// Try and get a matching delta state for the relevant dirty fields, otherwise fall back to full state.
switch (fields)
{{{deltaGetFields}
default:
break;
}}
}}";
deltaHandleState = $@"switch(args.Current)
{{{deltaHandleFields}
default:
break;
}}";
deltaInterface = " : IComponentDelta";
deltaCompFields = @$"/// <inheritdoc />
public GameTick LastFieldUpdate {{ get; set; }} = GameTick.Zero;
/// <inheritdoc />
public GameTick[] LastModifiedFields {{ get; set; }} = Array.Empty<GameTick>();";
}
return $@"// <auto-generated />
#nullable enable
using System;
using Robust.Shared.GameStates;
using Robust.Shared.GameObjects;
using Robust.Shared.Analyzers;
using Robust.Shared.Collections;
using Robust.Shared.Serialization;
using Robust.Shared.Map;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Collections.Generic;
namespace {nameSpace};
public partial class {componentName}{deltaInterface}
public partial class {componentName}
{{
{deltaCompFields}
[System.Serializable, NetSerializable]
public sealed class {stateName} : IComponentState
{{{stateFields}
@@ -559,16 +301,12 @@ public partial class {componentName}{deltaInterface}
{{
public override void Initialize()
{{
{deltaNetRegister}
SubscribeLocalEvent<{componentName}, ComponentGetState>(OnGetState);
SubscribeLocalEvent<{componentName}, ComponentHandleState>(OnHandleState);
}}
private void OnGetState(EntityUid uid, {componentName} component, ref ComponentGetState args)
{{
{deltaGetState}
// Get full state
args.State = new {stateName}
{{{getStateInit}
}};
@@ -576,15 +314,11 @@ public partial class {componentName}{deltaInterface}
private void OnHandleState(EntityUid uid, {componentName} component, ref ComponentHandleState args)
{{
{deltaHandleState}
if (args.Current is not {stateName} state)
return;
{handleStateSetters}{eventRaise}
}}
}}
{fieldStates}
}}
";
}
@@ -607,15 +341,13 @@ public partial class {componentName}{deltaInterface}
{
var attr = type.Attribute;
var raiseEv = false;
var fieldDeltas = false;
if (attr.ConstructorArguments is [{Value: bool raise}, {Value: bool fields}])
if (attr.ConstructorArguments is [{Value: bool raise}])
{
// Get the afterautohandle bool, which is first constructor arg
raiseEv = raise;
fieldDeltas = fields;
}
var source = GenerateSource(context, type.Type, comp, raiseEv, fieldDeltas);
var source = GenerateSource(context, type.Type, comp, raiseEv);
// can be null if no members marked with network field, which already has a diagnostic, so
// just continue
if (source == null)

View File

@@ -1,9 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"Comp State Generator": {
"commandName": "DebugRoslynComponent",
"targetProject": "../../Content.Shared/Content.Shared.csproj"
}
}
}

View File

@@ -745,21 +745,6 @@ namespace Robust.Shared.Maths
return remainder == T.Zero ? value : (value | mask) + T.One;
}
public static bool IsValid(this float value)
{
if (float.IsNaN(value))
{
return false;
}
if (float.IsInfinity(value))
{
return false;
}
return true;
}
#endregion Public Members
}
}

View File

@@ -73,7 +73,7 @@ public static class Matrix3Helpers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Angle Rotation(this Matrix3x2 t)
{
return new Angle(Math.Atan2(t.M12, t.M11));
return new Vector2(t.M11, t.M12).ToAngle();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -1,7 +1,6 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace Robust.Shared.Maths;
@@ -15,34 +14,6 @@ public static class Vector2Helpers
/// </summary>
public static readonly Vector2 Half = new(0.5f, 0.5f);
public static bool IsValid(this Vector2 v)
{
if (float.IsNaN(v.X) || float.IsNaN(v.Y))
{
return false;
}
if (float.IsInfinity(v.X) || float.IsInfinity(v.Y))
{
return false;
}
return true;
}
public static Vector2 GetLengthAndNormalize(this Vector2 v, ref float length)
{
length = v.Length();
if (length < float.Epsilon)
{
return Vector2.Zero;
}
float invLength = 1.0f / length;
var n = new Vector2(invLength * v.X, invLength * v.Y);
return n;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector2 InterpolateCubic(Vector2 preA, Vector2 a, Vector2 b, Vector2 postB, float t)
{
@@ -284,12 +255,6 @@ public static class Vector2Helpers
return new(-s * a.Y, s * a.X);
}
[Pure]
public static Vector2 RightPerp(this Vector2 v)
{
return new Vector2(v.Y, -v.X);
}
/// <summary>
/// Perform the cross product on a scalar and a vector. In 2D this produces
/// a vector.

View File

@@ -19,15 +19,9 @@ public sealed class AutoGenerateComponentStateAttribute : Attribute
/// </summary>
public bool RaiseAfterAutoHandleState;
/// <summary>
/// Should delta states be generated for every field.
/// </summary>
public bool FieldDeltas;
public AutoGenerateComponentStateAttribute(bool raiseAfterAutoHandleState = false, bool fieldDeltas = false)
public AutoGenerateComponentStateAttribute(bool raiseAfterAutoHandleState = false)
{
RaiseAfterAutoHandleState = raiseAfterAutoHandleState;
FieldDeltas = fieldDeltas;
}
}

View File

@@ -16,10 +16,10 @@ namespace Robust.Shared.Audio.Components;
/// <summary>
/// Stores the audio data for an audio entity.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true, fieldDeltas: true), Access(typeof(SharedAudioSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), Access(typeof(SharedAudioSystem))]
public sealed partial class AudioComponent : Component, IAudioSource
{
[AutoNetworkedField, DataField, Access(Other = AccessPermissions.ReadWriteExecute)]
[ViewVariables(VVAccess.ReadWrite), AutoNetworkedField, DataField, Access(Other = AccessPermissions.ReadWriteExecute)]
public AudioFlags Flags = AudioFlags.None;
#region Filter

View File

@@ -7,7 +7,7 @@ namespace Robust.Shared.Audio.Components;
/// <summary>
/// Marks this entity as being spawned for audio presets in case we need to reload.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), Access(typeof(SharedAudioSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedAudioSystem))]
public sealed partial class AudioPresetComponent : Component
{
[AutoNetworkedField]

View File

@@ -105,7 +105,6 @@ public abstract partial class SharedAudioSystem : EntitySystem
if (entity.Comp.PauseTime != null)
{
entity.Comp.PauseTime = entity.Comp.PauseTime.Value + timeOffset;
DirtyField(entity, nameof(AudioComponent.PauseTime));
// Paused audio doesn't have TimedDespawn so.
}
@@ -113,7 +112,6 @@ public abstract partial class SharedAudioSystem : EntitySystem
{
// Bump it back so the actual playback positions moves forward
entity.Comp.AudioStart -= timeOffset;
DirtyField(entity, nameof(AudioComponent.AudioStart));
// need to ensure it doesn't despawn too early.
if (TryComp(entity.Owner, out TimedDespawnComponent? despawn))
@@ -123,6 +121,8 @@ public abstract partial class SharedAudioSystem : EntitySystem
}
entity.Comp.PlaybackPosition = position;
// Network the new playback position.
Dirty(entity);
}
/// <summary>
@@ -191,9 +191,6 @@ public abstract partial class SharedAudioSystem : EntitySystem
var pauseOffset = Timing.CurTime - component.PauseTime;
component.AudioStart += pauseOffset ?? TimeSpan.Zero;
component.PlaybackPosition = (float) (Timing.CurTime - component.AudioStart).TotalSeconds;
DirtyField(entity.Value, component, nameof(AudioComponent.AudioStart));
DirtyField(entity.Value, component, nameof(AudioComponent.PlaybackPosition));
}
// If we were stopped then played then restart audiostart to now.
@@ -201,9 +198,6 @@ public abstract partial class SharedAudioSystem : EntitySystem
{
component.AudioStart = Timing.CurTime;
component.PauseTime = null;
DirtyField(entity.Value, component, nameof(AudioComponent.AudioStart));
DirtyField(entity.Value, component, nameof(AudioComponent.PauseTime));
}
switch (state)
@@ -211,21 +205,17 @@ public abstract partial class SharedAudioSystem : EntitySystem
case AudioState.Stopped:
component.AudioStart = Timing.CurTime;
component.PauseTime = null;
DirtyField(entity.Value, component, nameof(AudioComponent.AudioStart));
DirtyField(entity.Value, component, nameof(AudioComponent.PauseTime));
component.StopPlaying();
RemComp<TimedDespawnComponent>(entity.Value);
break;
case AudioState.Paused:
// Set it to current time so we can easily unpause it later.
component.PauseTime = Timing.CurTime;
DirtyField(entity.Value, component, nameof(AudioComponent.PauseTime));
component.Pause();
RemComp<TimedDespawnComponent>(entity.Value);
break;
case AudioState.Playing:
component.PauseTime = null;
DirtyField(entity.Value, component, nameof(AudioComponent.PauseTime));
component.StartPlaying();
// Reset TimedDespawn so the audio still gets cleaned up.
@@ -240,7 +230,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
}
component.State = state;
DirtyField(entity.Value, component, nameof(AudioComponent.State));
Dirty(entity.Value, component);
}
protected void SetZOffset(float value)
@@ -385,7 +375,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
component.Params.Volume = value;
component.Volume = value;
DirtyField(entity.Value, component, nameof(AudioComponent.Params));
Dirty(entity.Value, component);
}
#endregion

View File

@@ -628,43 +628,6 @@ namespace Robust.Shared
public static readonly CVarDef<string> StatusConnectAddress =
CVarDef.Create("status.connectaddress", "", CVar.ARCHIVE | CVar.SERVERONLY);
/// <summary>
/// HTTP(S) link to a privacy policy that the user must accept to connect to the server.
/// </summary>
/// <remarks>
/// This must be set along with <see cref="StatusPrivacyPolicyIdentifier"/> and
/// <see cref="StatusPrivacyPolicyVersion"/> for the user to be prompted about a privacy policy.
/// </remarks>
public static readonly CVarDef<string> StatusPrivacyPolicyLink =
CVarDef.Create("status.privacy_policy_link", "https://example.com/privacy", CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// An identifier for privacy policy specified by <see cref="StatusPrivacyPolicyLink"/>.
/// This must be globally unique.
/// </summary>
/// <remarks>
/// <para>
/// This value must be globally unique per server community. Servers that want to enforce a
/// privacy policy should set this to a value that is unique to their server and, preferably, recognizable.
/// </para>
/// <para>
/// This value is stored by the launcher to keep track of what privacy policies a player has accepted.
/// </para>
/// </remarks>
public static readonly CVarDef<string> StatusPrivacyPolicyIdentifier =
CVarDef.Create("status.privacy_policy_identifier", "", CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// A "version" for the privacy policy specified by <see cref="StatusPrivacyPolicyLink"/>.
/// </summary>
/// <remarks>
/// <para>
/// This parameter is stored by the launcher and should be modified whenever your server's privacy policy changes.
/// </para>
/// </remarks>
public static readonly CVarDef<string> StatusPrivacyPolicyVersion =
CVarDef.Create("status.privacy_policy_version", "", CVar.SERVER | CVar.REPLICATED);
/*
* BUILD
*/

View File

@@ -5,7 +5,6 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -590,12 +589,6 @@ public struct ValueList<T> : IEnumerable<T>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddRange(Span<T> span)
{
AddRange((ReadOnlySpan<T>) span);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddRange(ReadOnlySpan<T> span)
{
var spanCount = span.Length;
EnsureCapacity(Count + spanCount);
@@ -625,72 +618,4 @@ public struct ValueList<T> : IEnumerable<T>
Add(result);
}
}
/// <summary>
/// Push a value onto the end of this list. This is equivalent to <see cref="Add"/>.
/// </summary>
/// <remarks>
/// This method is added to provide completeness with other stack-like functions.
/// </remarks>
/// <param name="item">The item to add to the list.</param>
public void Push(T item)
{
Add(item);
}
/// <summary>
/// Remove and return the value at the end of the list.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the list is empty.</exception>
public T Pop()
{
if (!TryPop(out var value))
throw new InvalidOperationException("List is empty");
return value;
}
/// <summary>
/// Return the value at the end of the list, but do not remove it.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the list is empty.</exception>
public T Peek()
{
if (!TryPeek(out var value))
throw new InvalidOperationException("List is empty");
return value;
}
/// <summary>
/// Remove and return the value at the end of the list, only if the list is not empty.
/// </summary>
/// <returns>True if the list was not empty and an item was removed.</returns>
public bool TryPop([MaybeNullWhen(false)] out T value)
{
if (Count == 0)
{
value = default;
return false;
}
value = _items![--Count];
return true;
}
/// <summary>
/// Remove and return the value at the end of the list, only if the list is not empty.
/// </summary>
/// <returns>True if the list was not empty and an item was removed.</returns>
public bool TryPeek([MaybeNullWhen(false)] out T value)
{
if (Count == 0)
{
value = default;
return false;
}
value = _items![Count];
return true;
}
}

View File

@@ -367,7 +367,7 @@ namespace Robust.Shared.Containers
if (!xform.ParentUid.Valid)
return false;
if (entityQuery.TryComp(xform.ParentUid, out foundComponent))
if (entityQuery.Resolve(xform.ParentUid, ref foundComponent, false))
return true;
return TryFindComponentOnEntityContainerOrParent(xform.ParentUid, entityQuery, ref foundComponent);

View File

@@ -39,7 +39,7 @@ internal sealed partial class AssemblyTypeChecker
{
if (instruction.TryGetEntityHandle(out var handle))
{
if (refs.Overlaps(ExpandHandle(reader, handle)))
if (refs.Contains(handle))
{
var type = GetTypeFromDefinition(reader, methodDef.GetDeclaringType());
_sawmill.Error(
@@ -56,12 +56,6 @@ internal sealed partial class AssemblyTypeChecker
{
switch (handle.Kind)
{
case HandleKind.MethodSpecification:
var methodSpec = reader.GetMethodSpecification((MethodSpecificationHandle)handle);
var methodProvider = new TypeProvider();
var spec = methodSpec.DecodeSignature(methodProvider, 0);
return $"{DisplayHandle(reader, methodSpec.Method)}<{string.Join(", ", spec.Select(t => t.ToString()))}>";
case HandleKind.MemberReference:
var memberRef = reader.GetMemberReference((MemberReferenceHandle)handle);
var name = reader.GetString(memberRef.Name);
@@ -98,17 +92,6 @@ internal sealed partial class AssemblyTypeChecker
handles.UnionWith(toAdd);
}
private static IEnumerable<EntityHandle> ExpandHandle(MetadataReader reader, EntityHandle handle)
{
// Annoying, S.R.M gives no way to iterate over the MethodSpec table.
// This means the only way to correlate MethodSpec references is to do it for each handle.
yield return handle;
if (handle.Kind == HandleKind.MethodSpecification)
yield return reader.GetMethodSpecification((MethodSpecificationHandle)handle).Method;
}
private readonly struct ILInstruction
{
public readonly ILOpCode OpCode;

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ namespace Robust.Shared.ContentPack
{
/// <summary>
/// The root path of this provider.
/// Can be null if it's a virtual provider.
/// Can be null if it's a virtual provider or the path is protected (e.g. on the client).
/// </summary>
string? RootDir { get; }

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
{
@@ -63,5 +64,27 @@ namespace Robust.Shared.ContentPack
!OperatingSystem.IsWindows()
&& !OperatingSystem.IsMacOS();
internal static string SafeGetResourcePath(string baseDir, ResPath path)
{
var relSysPath = path.ToRelativeSystemPath();
if (relSysPath.Contains("\\..") || relSysPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
var retPath = Path.GetFullPath(Path.Join(baseDir, relSysPath));
// better safe than sorry check
if (!retPath.StartsWith(baseDir))
{
// Allow path to match if it's just missing the directory separator at the end.
if (retPath != baseDir.TrimEnd(Path.DirectorySeparatorChar))
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return retPath;
}
}
}

View File

@@ -41,13 +41,13 @@ namespace Robust.Shared.ContentPack
public IWritableDirProvider UserData { get; private set; } = default!;
/// <inheritdoc />
public virtual void Initialize(string? userData)
public virtual void Initialize(string? userData, bool hideRootDir)
{
Sawmill = _logManager.GetSawmill("res");
if (userData != null)
{
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
}
else
{
@@ -379,7 +379,13 @@ namespace Robust.Shared.ContentPack
{
if (root is DirLoader loader)
{
yield return new ResPath(loader.GetPath(new ResPath(@"/")));
var rootDir = loader.GetPath(new ResPath(@"/"));
// TODO: GET RID OF THIS.
// This code shouldn't be passing OS disk paths through ResPath.
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
yield return new ResPath(rootDir);
}
}
}

View File

@@ -10,17 +10,22 @@ namespace Robust.Shared.ContentPack
/// <inheritdoc />
internal sealed class WritableDirProvider : IWritableDirProvider
{
/// <inheritdoc />
private readonly bool _hideRootDir;
public string RootDir { get; }
string? IWritableDirProvider.RootDir => _hideRootDir ? null : RootDir;
/// <summary>
/// Constructs an instance of <see cref="WritableDirProvider"/>.
/// </summary>
/// <param name="rootDir">Root file system directory to allow writing.</param>
public WritableDirProvider(DirectoryInfo rootDir)
/// <param name="hideRootDir">If true, <see cref="IWritableDirProvider.RootDir"/> is reported as null.</param>
public WritableDirProvider(DirectoryInfo rootDir, bool hideRootDir)
{
// FullName does not have a trailing separator, and we MUST have a separator.
RootDir = rootDir.FullName + Path.DirectorySeparatorChar.ToString();
_hideRootDir = hideRootDir;
}
#region File Access
@@ -119,7 +124,7 @@ namespace Robust.Shared.ContentPack
throw new FileNotFoundException();
var dirInfo = new DirectoryInfo(GetFullPath(path));
return new WritableDirProvider(dirInfo);
return new WritableDirProvider(dirInfo, _hideRootDir);
}
/// <inheritdoc />
@@ -180,20 +185,7 @@ namespace Robust.Shared.ContentPack
path = path.Clean();
return GetFullPath(RootDir, path);
}
private static string GetFullPath(string root, ResPath path)
{
var relPath = path.ToRelativeSystemPath();
if (relPath.Contains("\\..") || relPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return Path.GetFullPath(Path.Combine(root, relPath));
return PathHelpers.SafeGetResourcePath(RootDir, path);
}
}
}

View File

@@ -175,41 +175,6 @@ namespace Robust.Shared.GameObjects
return name;
}
/// <inheritdoc />
public void RegisterNetworkedFields<T>(params string[] fields) where T : IComponent
{
var compReg = GetRegistration(CompIdx.Index<T>());
RegisterNetworkedFields(compReg, fields);
}
/// <inheritdoc />
public void RegisterNetworkedFields(ComponentRegistration compReg, params string[] fields)
{
// Nothing to do.
if (compReg.NetworkedFields.Length > 0 || fields.Length == 0)
return;
DebugTools.Assert(fields.Length <= 32);
if (fields.Length > 32)
{
throw new NotSupportedException(
"Components with more than 32 networked fields unsupported! Consider splitting it up or making a pr for 64-bit flags");
}
compReg.NetworkedFields = fields;
var lookup = new Dictionary<string, int>(fields.Length);
var i = 0;
foreach (var field in fields)
{
lookup[field] = i;
i++;
}
compReg.NetworkedFieldLookup = lookup.ToFrozenDictionary();
}
public void IgnoreMissingComponents(string postfix = "")
{
if (_ignoreMissingComponentPostfix != null && _ignoreMissingComponentPostfix != postfix)

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using Robust.Shared.Collections;
using Robust.Shared.GameStates;
@@ -40,13 +39,6 @@ public sealed class ComponentRegistration
/// </summary>
public Type Type { get; }
/// <summary>
/// Fields that are networked for this component. Used for delta states.
/// </summary>
public string[] NetworkedFields = [];
public FrozenDictionary<string, int> NetworkedFieldLookup = FrozenDictionary<string, int>.Empty;
// Internal for sandboxing.
// Avoid content passing an instance of this to ComponentFactory to get any type they want instantiated.
internal ComponentRegistration(string name, Type type, CompIdx idx, bool unsaved = false)

View File

@@ -1,15 +1,30 @@
using System;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.GameObjects;
/// <summary>
/// An optimisation component for stuff that should be set as collidable when it's awake and non-collidable when asleep.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(CollisionWakeSystem))]
public sealed partial class CollisionWakeComponent : Component
namespace Robust.Shared.GameObjects
{
[DataField, AutoNetworkedField]
public bool Enabled = true;
/// <summary>
/// An optimisation component for stuff that should be set as collidable when it's awake and non-collidable when asleep.
/// </summary>
[RegisterComponent, NetworkedComponent()]
[Access(typeof(CollisionWakeSystem))]
public sealed partial class CollisionWakeComponent : Component
{
[DataField("enabled")]
public bool Enabled = true;
[Serializable, NetSerializable]
public sealed class CollisionWakeState : ComponentState
{
public bool Enabled { get; }
public CollisionWakeState(bool enabled)
{
Enabled = enabled;
}
}
}
}

View File

@@ -8,7 +8,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects
{
[RegisterComponent, NetworkedComponent, Access(typeof(SharedEyeSystem)), AutoGenerateComponentState(true, fieldDeltas: true)]
[RegisterComponent, NetworkedComponent, Access(typeof(SharedEyeSystem)), AutoGenerateComponentState(true)]
public sealed partial class EyeComponent : Component
{
public const int DefaultVisibilityMask = 1;
@@ -24,33 +24,33 @@ namespace Robust.Shared.GameObjects
/// that without messing with the main viewport's eye. This is important as there are some overlays that are
/// only be drawn if that viewport's eye belongs to the currently controlled entity.
/// </remarks>
[DataField, AutoNetworkedField]
[ViewVariables, DataField("target"), AutoNetworkedField]
public EntityUid? Target;
[DataField, AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite), DataField("drawFov"), AutoNetworkedField]
public bool DrawFov = true;
[AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public bool DrawLight = true;
// yes it's not networked, don't ask.
[DataField]
[ViewVariables(VVAccess.ReadWrite), DataField("rotation")]
public Angle Rotation;
[DataField]
[ViewVariables(VVAccess.ReadWrite), DataField("zoom")]
public Vector2 Zoom = Vector2.One;
/// <summary>
/// Eye offset, relative to the map, and not affected by <see cref="Rotation"/>
/// </summary>
[DataField, AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite), DataField("offset"), AutoNetworkedField]
public Vector2 Offset;
/// <summary>
/// The visibility mask for this eye.
/// The player will be able to get updates for entities whose layers match the mask.
/// </summary>
[DataField("visMask", customTypeSerializer:typeof(FlagSerializer<VisibilityMaskLayer>)), AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite), DataField("visMask", customTypeSerializer:typeof(FlagSerializer<VisibilityMaskLayer>)), AutoNetworkedField]
public int VisibilityMask = DefaultVisibilityMask;
/// <summary>

View File

@@ -59,27 +59,6 @@ namespace Robust.Shared.GameObjects
{
}
/// <summary>
/// Calls <see cref="UpdateState"/> if the supplied state exists and calls <see cref="Update"/>
/// </summary>
public void Update<T>() where T : BoundUserInterfaceState
{
if (UiSystem.TryGetUiState<T>(Owner, UiKey, out var state))
{
UpdateState(state);
}
Update();
}
/// <summary>
/// Generic update method called whenever the BUI should update.
/// </summary>
public virtual void Update()
{
}
/// <summary>
/// Helper method that gets called upon prototype reload.
/// </summary>

View File

@@ -3,22 +3,14 @@ using System.Collections.Generic;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.GameObjects
{
[RegisterComponent, NetworkedComponent, Access(typeof(SharedUserInterfaceSystem))]
public sealed partial class UserInterfaceComponent : Component, IComponentDelta
public sealed partial class UserInterfaceComponent : Component
{
/// <inheritdoc />
public GameTick LastFieldUpdate { get; set; }
/// <inheritdoc />
public GameTick[] LastModifiedFields { get; set; }
/// <summary>
/// The currently open interfaces. Used clientside to store the UI.
/// </summary>
@@ -38,71 +30,19 @@ namespace Robust.Shared.GameObjects
/// Legacy data, new BUIs should be using comp states.
/// </summary>
public Dictionary<Enum, BoundUserInterfaceState> States = new();
}
[Serializable, NetSerializable]
internal sealed class UserInterfaceComponentState(
Dictionary<Enum, List<NetEntity>> actors,
Dictionary<Enum, BoundUserInterfaceState> states,
Dictionary<Enum, InterfaceData> data)
: IComponentState
{
public Dictionary<Enum, List<NetEntity>> Actors = actors;
public Dictionary<Enum, BoundUserInterfaceState> States = states;
public Dictionary<Enum, InterfaceData> Data = data;
}
[Serializable, NetSerializable]
internal sealed class UserInterfaceActorsDeltaState : IComponentDeltaState<UserInterfaceComponentState>
{
public Dictionary<Enum, List<NetEntity>> Actors = new();
public void ApplyToFullState(UserInterfaceComponentState fullState)
[Serializable, NetSerializable]
internal sealed class UserInterfaceComponentState(
Dictionary<Enum, List<NetEntity>> actors,
Dictionary<Enum, BoundUserInterfaceState> states,
Dictionary<Enum, InterfaceData> data)
: IComponentState
{
fullState.Actors.Clear();
public Dictionary<Enum, List<NetEntity>> Actors = actors;
foreach (var (key, value) in Actors)
{
fullState.Actors.Add(key, value);
}
}
public Dictionary<Enum, BoundUserInterfaceState> States = states;
public UserInterfaceComponentState CreateNewFullState(UserInterfaceComponentState fullState)
{
var newState = new UserInterfaceComponentState(
new Dictionary<Enum, List<NetEntity>>(Actors),
new(fullState.States),
new(fullState.Data));
return newState;
}
}
[Serializable, NetSerializable]
internal sealed class UserInterfaceStatesDeltaState : IComponentDeltaState<UserInterfaceComponentState>
{
public Dictionary<Enum, BoundUserInterfaceState> States = new();
public void ApplyToFullState(UserInterfaceComponentState fullState)
{
fullState.States.Clear();
foreach (var (key, value) in States)
{
fullState.States.Add(key, value);
}
}
public UserInterfaceComponentState CreateNewFullState(UserInterfaceComponentState fullState)
{
var newState = new UserInterfaceComponentState(
new Dictionary<Enum, List<NetEntity>>(fullState.Actors),
new(States),
new(fullState.Data));
return newState;
public Dictionary<Enum, InterfaceData> Data = data;
}
}
@@ -129,12 +69,6 @@ namespace Robust.Shared.GameObjects
[DataField]
public bool RequireInputValidation = true;
public InterfaceData(string clientType, float interactionRange = 2f, bool requireInputValidation = true)
{
ClientType = clientType;
InteractionRange = interactionRange;
RequireInputValidation = requireInputValidation;
}
public InterfaceData(InterfaceData data)
{
ClientType = data.ClientType;

View File

@@ -1,78 +0,0 @@
using Robust.Shared.Timing;
namespace Robust.Shared.GameObjects;
public abstract partial class EntityManager
{
public uint GetModifiedFields(IComponentDelta delta, GameTick fromTick)
{
uint fields = 0;
for (var i = 0; i < delta.LastModifiedFields.Length; i++)
{
var lastUpdate = delta.LastModifiedFields[i];
// Field not dirty
if (lastUpdate < fromTick)
continue;
fields |= (uint) (1 << i);
}
return fields;
}
public void DirtyField(EntityUid uid, IComponentDelta comp, string fieldName, MetaDataComponent? metadata = null)
{
var compReg = ComponentFactory.GetRegistration(comp);
if (!compReg.NetworkedFieldLookup.TryGetValue(fieldName, out var idx))
{
_sawmill.Error($"Tried to dirty delta field {fieldName} on {ToPrettyString(uid)} that isn't implemented.");
return;
}
var curTick = _gameTiming.CurTick;
comp.LastFieldUpdate = curTick;
comp.LastModifiedFields[idx] = curTick;
Dirty(uid, comp, metadata);
}
public void DirtyField<T>(EntityUid uid, T comp, string fieldName, MetaDataComponent? metadata = null)
where T : IComponentDelta
{
var compReg = ComponentFactory.GetRegistration(CompIdx.Index<T>());
if (!compReg.NetworkedFieldLookup.TryGetValue(fieldName, out var idx))
{
_sawmill.Error($"Tried to dirty delta field {fieldName} on {ToPrettyString(uid)} that isn't implemented.");
return;
}
var curTick = _gameTiming.CurTick;
comp.LastFieldUpdate = curTick;
comp.LastModifiedFields[idx] = curTick;
Dirty(uid, comp, metadata);
}
}
/// <summary>
/// Indicates this component supports delta states.
/// </summary>
public partial interface IComponentDelta : IComponent
{
// TODO: This isn't entirely robust but not sure how else to handle this?
/// <summary>
/// Track last time a field was dirtied. if the full component dirty exceeds this then we send a full state update.
/// </summary>
public GameTick LastFieldUpdate { get; set; }
/// <summary>
/// Stores the last modified tick for fields.
/// </summary>
public GameTick[] LastModifiedFields
{
get;
set;
}
}

View File

@@ -372,13 +372,6 @@ namespace Robust.Shared.GameObjects
metadata.NetComponents.Add(netId, component);
}
if (component is IComponentDelta delta)
{
var curTick = _gameTiming.CurTick;
delta.LastModifiedFields = new GameTick[reg.NetworkedFields.Length];
Array.Fill(delta.LastModifiedFields, curTick);
}
component.Networked = reg.NetID != null;
var eventArgs = new AddedComponentEventArgs(new ComponentEventArgs(component, uid), reg);
@@ -714,7 +707,6 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent<T>(EntityUid uid) where T : IComponent
{
var dict = _entTraitArray[CompIdx.ArrayIndex<T>()];
@@ -724,15 +716,13 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent<T>([NotNullWhen(true)] EntityUid? uid) where T : IComponent
public bool HasComponent<T>(EntityUid? uid) where T : IComponent
{
return uid.HasValue && HasComponent<T>(uid.Value);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent(EntityUid uid, Type type)
{
var dict = _entTraitDict[type];
@@ -741,8 +731,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent([NotNullWhen(true)] EntityUid? uid, Type type)
public bool HasComponent(EntityUid? uid, Type type)
{
if (!uid.HasValue)
{
@@ -755,7 +744,6 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent(EntityUid uid, ushort netId, MetaDataComponent? meta = null)
{
if (!MetaQuery.Resolve(uid, ref meta))
@@ -766,8 +754,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent([NotNullWhen(true)] EntityUid? uid, ushort netId, MetaDataComponent? meta = null)
public bool HasComponent(EntityUid? uid, ushort netId, MetaDataComponent? meta = null)
{
if (!uid.HasValue)
{
@@ -834,7 +821,6 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T GetComponent<T>(EntityUid uid) where T : IComponent
{
@@ -851,7 +837,6 @@ namespace Robust.Shared.GameObjects
throw new KeyNotFoundException($"Entity {uid} does not have a component of type {typeof(T)}");
}
[Pure]
public IComponent GetComponent(EntityUid uid, CompIdx type)
{
var dict = _entTraitArray[type.Value];
@@ -865,7 +850,6 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
[Pure]
public IComponent GetComponent(EntityUid uid, Type type)
{
// ReSharper disable once InvertIf
@@ -882,14 +866,12 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
[Pure]
public IComponent GetComponent(EntityUid uid, ushort netId, MetaDataComponent? meta = null)
{
return (meta ?? MetaQuery.GetComponentInternal(uid)).NetComponents[netId];
}
/// <inheritdoc />
[Pure]
public IComponent GetComponentInternal(EntityUid uid, CompIdx type)
{
var dict = _entTraitArray[type.Value];
@@ -1468,7 +1450,6 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
[Pure]
public IComponentState? GetComponentState(IEventBus eventBus, IComponent component, ICommonSession? session, GameTick fromTick)
{
DebugTools.Assert(component.NetSyncEnabled, $"Attempting to get component state for an un-synced component: {component.GetType()}");
@@ -1597,7 +1578,7 @@ namespace Robust.Shared.GameObjects
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComp([NotNullWhen(true)] EntityUid? uid) => HasComponent(uid);
public bool HasComp(EntityUid? uid) => HasComponent(uid);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
@@ -1608,7 +1589,7 @@ namespace Robust.Shared.GameObjects
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent([NotNullWhen(true)] EntityUid? uid)
public bool HasComponent(EntityUid? uid)
{
return uid != null && HasComponent(uid.Value);
}

View File

@@ -148,29 +148,6 @@ public partial class EntitySystem
EntityManager.Dirty(uid, component, meta);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void DirtyField(EntityUid uid, IComponentDelta delta, string fieldName, MetaDataComponent? meta = null)
{
EntityManager.DirtyField(uid, delta, fieldName, meta);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void DirtyField<T>(Entity<T?> entity, string fieldName, MetaDataComponent? meta = null)
where T : IComponentDelta
{
if (!Resolve(entity.Owner, ref entity.Comp))
return;
EntityManager.DirtyField(entity.Owner, entity.Comp, fieldName, meta);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void DirtyField<T>(EntityUid uid, T component, string fieldName, MetaDataComponent? meta = null)
where T : IComponentDelta
{
EntityManager.DirtyField(uid, component, fieldName, meta);
}
/// <summary>
/// Marks a component as dirty. This also implicitly dirties the entity this component belongs to.
/// </summary>

View File

@@ -77,10 +77,6 @@ namespace Robust.Shared.GameObjects
/// <returns>The availability of the component.</returns>
ComponentAvailability GetComponentAvailability(string componentName, bool ignoreCase = false);
public void RegisterNetworkedFields<T>(params string[] fields) where T : IComponent;
public void RegisterNetworkedFields(ComponentRegistration compReg, params string[] fields);
/// <summary>
/// Slow-path for Type -> CompIdx mapping without generics.
/// </summary>

View File

@@ -6,11 +6,6 @@ namespace Robust.Shared.GameObjects;
public partial interface IEntityManager
{
public void DirtyField(EntityUid uid, IComponentDelta delta, string fieldName, MetaDataComponent? metadata = null);
public void DirtyField<T>(EntityUid uid, T component, string fieldName, MetaDataComponent? metadata = null)
where T : IComponentDelta;
/// <summary>
/// Tries to parse a string as a NetEntity and return the relevant EntityUid.
/// </summary>

View File

@@ -1,3 +1,4 @@
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics;
@@ -16,6 +17,9 @@ namespace Robust.Shared.GameObjects
base.Initialize();
SubscribeLocalEvent<CollisionWakeComponent, ComponentShutdown>(OnRemove);
SubscribeLocalEvent<CollisionWakeComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<CollisionWakeComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<CollisionWakeComponent, JointAddedEvent>(OnJointAdd);
SubscribeLocalEvent<CollisionWakeComponent, JointRemovedEvent>(OnJointRemove);
@@ -39,6 +43,22 @@ namespace Robust.Shared.GameObjects
Dirty(uid, component);
}
private void OnHandleState(EntityUid uid, CollisionWakeComponent component, ref ComponentHandleState args)
{
if (args.Current is CollisionWakeComponent.CollisionWakeState state)
component.Enabled = state.Enabled;
// Note, this explicitly does not update PhysicsComponent.CanCollide. The physics component should perform
// its own state-handling logic. Additionally, if we wanted to set it you would have to ensure that things
// like the join-component and physics component have already handled their states, otherwise CanCollide may
// be set incorrectly and leave the client with a bad state.
}
private void OnGetState(EntityUid uid, CollisionWakeComponent component, ref ComponentGetState args)
{
args.State = new CollisionWakeComponent.CollisionWakeState(component.Enabled);
}
private void OnRemove(EntityUid uid, CollisionWakeComponent component, ComponentShutdown args)
{
if (component.Enabled

View File

@@ -175,7 +175,7 @@ public sealed partial class EntityLookupSystem
return;
static bool PhysicsQuery(ref QueryState<T> state, in FixtureProxy value)
static bool PhysicsQuery<T>(ref QueryState<T> state, in FixtureProxy value) where T : IComponent
{
if (!state.Sensors && !value.Fixture.Hard)
return true;
@@ -196,7 +196,7 @@ public sealed partial class EntityLookupSystem
return true;
}
static bool SundriesQuery(ref QueryState<T> state, in EntityUid value)
static bool SundriesQuery<T>(ref QueryState<T> state, in EntityUid value) where T : IComponent
{
if (!state.Query.TryGetComponent(value, out var comp))
return true;
@@ -318,7 +318,7 @@ public sealed partial class EntityLookupSystem
return state.Found;
static bool PhysicsQuery(ref AnyQueryState<T> state, in FixtureProxy value)
static bool PhysicsQuery<T>(ref AnyQueryState<T> state, in FixtureProxy value) where T : IComponent
{
if (value.Entity == state.Ignored)
return true;

View File

@@ -63,7 +63,7 @@ public abstract class SharedEyeSystem : EntitySystem
eyeComponent.Offset = value;
eyeComponent.Eye.Offset = value;
DirtyField(uid, eyeComponent, nameof(EyeComponent.Offset));
Dirty(uid, eyeComponent);
}
public void SetDrawFov(EntityUid uid, bool value, EyeComponent? eyeComponent = null)
@@ -76,7 +76,7 @@ public abstract class SharedEyeSystem : EntitySystem
eyeComponent.DrawFov = value;
eyeComponent.Eye.DrawFov = value;
DirtyField(uid, eyeComponent, nameof(EyeComponent.DrawFov));
Dirty(uid, eyeComponent);
}
public void SetDrawLight(Entity<EyeComponent?> entity, bool value)
@@ -89,7 +89,7 @@ public abstract class SharedEyeSystem : EntitySystem
entity.Comp.DrawLight = value;
entity.Comp.Eye.DrawLight = value;
DirtyField(entity, nameof(EyeComponent.DrawLight));
Dirty(entity);
}
public void SetRotation(EntityUid uid, Angle rotation, EyeComponent? eyeComponent = null)
@@ -127,7 +127,7 @@ public abstract class SharedEyeSystem : EntitySystem
}
eyeComponent.Target = value;
DirtyField(uid, eyeComponent, nameof(EyeComponent.Target));
Dirty(uid, eyeComponent);
}
public void SetZoom(EntityUid uid, Vector2 value, EyeComponent? eyeComponent = null)
@@ -168,6 +168,6 @@ public abstract class SharedEyeSystem : EntitySystem
return;
eyeComponent.VisibilityMask = value;
DirtyField(uid, eyeComponent, nameof(EyeComponent.VisibilityMask));
Dirty(uid, eyeComponent);
}
}

View File

@@ -532,7 +532,7 @@ public abstract partial class SharedMapSystem
private void OnGridRemove(EntityUid uid, MapGridComponent component, ComponentShutdown args)
{
Log.Info($"Removing grid {ToPrettyString(uid)}");
if (TryComp(uid, out TransformComponent? xform) && xform.MapUid != null)
if (TryComp<TransformComponent>(uid, out var xform) && xform.MapUid != null)
{
RemoveGrid(uid, component, xform.MapUid.Value);
}

View File

@@ -719,7 +719,8 @@ public abstract partial class SharedTransformSystem
component.LocalRotation,
parent,
component.NoLocalRotation,
component.Anchored);
component.Anchored,
component.GridTraversal);
}
internal void OnHandleState(EntityUid uid, TransformComponent xform, ref ComponentHandleState args)

View File

@@ -337,6 +337,8 @@ namespace Robust.Shared.GameObjects
/// </summary>
public readonly bool Anchored;
public readonly bool GridTraversal;
/// <summary>
/// Constructs a new state snapshot of a TransformComponent.
/// </summary>
@@ -344,13 +346,14 @@ namespace Robust.Shared.GameObjects
/// <param name="rotation">Current direction offset of this entity.</param>
/// <param name="parentId">Current parent transform of this entity.</param>
/// <param name="noLocalRotation"></param>
public TransformComponentState(Vector2 localPosition, Angle rotation, NetEntity parentId, bool noLocalRotation, bool anchored)
public TransformComponentState(Vector2 localPosition, Angle rotation, NetEntity parentId, bool noLocalRotation, bool anchored, bool gridTraversal)
{
LocalPosition = localPosition;
Rotation = rotation;
ParentID = parentId;
NoLocalRotation = noLocalRotation;
Anchored = anchored;
GridTraversal = gridTraversal;
}
}
}

View File

@@ -44,11 +44,6 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
{
base.Initialize();
EntityManager.ComponentFactory.RegisterNetworkedFields<UserInterfaceComponent>(
nameof(UserInterfaceComponent.Actors),
nameof(UserInterfaceComponent.Interfaces),
nameof(UserInterfaceComponent.States));
_ignoreUIRangeQuery = GetEntityQuery<IgnoreUIRangeComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
UIQuery = GetEntityQuery<UserInterfaceComponent>();
@@ -209,7 +204,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
if (actors.Count == 0)
ent.Comp.Actors.Remove(key);
DirtyField(ent, nameof(UserInterfaceComponent.Actors));
Dirty(ent);
// If the actor is also deleting then don't worry about updating what they have open.
if (!TerminatingOrDeleted(actor)
@@ -261,7 +256,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
// Let state handling open the UI clientside.
actorComp.OpenInterfaces.GetOrNew(ent.Owner).Add(key);
ent.Comp.Actors.GetOrNew(key).Add(actor);
DirtyField(ent, nameof(UserInterfaceComponent.Actors));
Dirty(ent);
var ev = new BoundUIOpenedEvent(key, ent.Owner, actor);
RaiseLocalEvent(ent.Owner, ev);
@@ -280,7 +275,6 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
if (ent.Comp.States.TryGetValue(key, out var state))
{
bui.UpdateState(state);
bui.Update();
}
}
}
@@ -301,54 +295,25 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
DebugTools.Assert(!ent.Comp.Actors.ContainsKey(key));
}
DebugTools.Assert(ent.Comp.ClientOpenInterfaces.Values.All(x => _queuedCloses.Contains(x)));
DebugTools.AssertEqual(ent.Comp.ClientOpenInterfaces.Count, 0);
}
private void OnUserInterfaceGetState(Entity<UserInterfaceComponent> ent, ref ComponentGetState args)
{
if (ent.Comp.LastFieldUpdate >= args.FromTick)
{
var fields = EntityManager.GetModifiedFields(ent.Comp, args.FromTick);
switch (fields)
{
case 1 << 0:
{
var state = new UserInterfaceActorsDeltaState();
AddActors(ent, state.Actors, ref args);
args.State = state;
return;
}
case 1 << 2:
{
var state = new UserInterfaceStatesDeltaState()
{
States = new Dictionary<Enum, BoundUserInterfaceState>(ent.Comp.States),
};
args.State = state;
return;
}
}
}
// TODO delta states.
// I.e., don't resend the whole BUI state just because a new user opened it.
var actors = new Dictionary<Enum, List<NetEntity>>();
var dataCopy = new Dictionary<Enum, InterfaceData>(ent.Comp.Interfaces.Count);
var dataCopy = new Dictionary<Enum, InterfaceData>();
foreach (var (weh, a) in ent.Comp.Interfaces)
{
dataCopy[weh] = new InterfaceData(a);
}
args.State = new UserInterfaceComponentState(actors, new(ent.Comp.States), dataCopy);
args.State = new UserInterfaceComponent.UserInterfaceComponentState(actors, ent.Comp.States, dataCopy);
// Ensure that only the player that currently has the UI open gets to know what they have it open.
AddActors(ent, actors, ref args);
}
private void AddActors(Entity<UserInterfaceComponent> ent, Dictionary<Enum, List<NetEntity>> actors, ref ComponentGetState args)
{
// Ensure that only the player that currently has the UI open gets to know what they have it open.
if (args.ReplayState)
{
@@ -370,140 +335,106 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
private void OnUserInterfaceHandleState(Entity<UserInterfaceComponent> ent, ref ComponentHandleState args)
{
Dictionary<Enum, List<NetEntity>>? stateActors = null;
Dictionary<Enum, InterfaceData>? stateData = null;
Dictionary<Enum, BoundUserInterfaceState>? stateStates = null;
if (args.Current is UserInterfaceComponentState state)
{
stateActors = state.Actors;
stateData = state.Data;
stateStates = state.States;
}
else if (args.Current is UserInterfaceActorsDeltaState actorDelta)
{
stateActors = actorDelta.Actors;
}
else if (args.Current is UserInterfaceStatesDeltaState stateDelta)
{
stateStates = stateDelta.States;
}
else
{
if (args.Current is not UserInterfaceComponent.UserInterfaceComponentState state)
return;
ent.Comp.Interfaces.Clear();
foreach (var data in state.Data)
{
ent.Comp.Interfaces[data.Key] = new(data.Value);
}
// Interfaces
if (stateData != null)
foreach (var key in ent.Comp.Actors.Keys)
{
ent.Comp.Interfaces.Clear();
if (!state.Actors.ContainsKey(key))
CloseUi(ent!, key);
}
foreach (var data in stateData)
var toRemoveActors = new ValueList<EntityUid>();
var newSet = new HashSet<EntityUid>();
foreach (var (key, stateActors) in state.Actors)
{
var actors = ent.Comp.Actors.GetOrNew(key);
newSet.Clear();
foreach (var netEntity in stateActors)
{
ent.Comp.Interfaces[data.Key] = new(data.Value);
var uid = EnsureEntity<UserInterfaceComponent>(netEntity, ent.Owner);
if (uid.IsValid())
newSet.Add(uid);
}
foreach (var actor in newSet)
{
if (!actors.Contains(actor))
OpenUiInternal(ent!, key, actor);
}
foreach (var actor in actors)
{
if (!newSet.Contains(actor))
toRemoveActors.Add(actor);
}
foreach (var actor in toRemoveActors)
{
CloseUiInternal(ent!, key, actor);
}
}
foreach (var key in ent.Comp.States.Keys)
{
if (!state.States.ContainsKey(key))
ent.Comp.States.Remove(key);
}
var attachedEnt = Player.LocalEntity;
var clientBuis = new ValueList<Enum>(ent.Comp.ClientOpenInterfaces.Keys);
// Actors
if (stateActors != null)
// Check if the UI is open by us, otherwise dispose of it.
foreach (var key in clientBuis)
{
foreach (var key in ent.Comp.Actors.Keys)
if (ent.Comp.Actors.TryGetValue(key, out var actors) &&
(attachedEnt == null || actors.Contains(attachedEnt.Value)))
{
if (!stateActors.ContainsKey(key))
CloseUi(ent!, key);
continue;
}
var toRemoveActors = new ValueList<EntityUid>();
var newSet = new HashSet<EntityUid>();
foreach (var (key, acts) in stateActors)
var bui = ent.Comp.ClientOpenInterfaces[key];
if (bui.DeferredClose)
_queuedCloses.Add(bui);
else
{
var actors = ent.Comp.Actors.GetOrNew(key);
newSet.Clear();
foreach (var netEntity in acts)
{
var uid = EnsureEntity<UserInterfaceComponent>(netEntity, ent.Owner);
if (uid.IsValid())
newSet.Add(uid);
}
foreach (var actor in newSet)
{
if (!actors.Contains(actor))
OpenUiInternal(ent!, key, actor);
}
foreach (var actor in actors)
{
if (!newSet.Contains(actor))
toRemoveActors.Add(actor);
}
foreach (var actor in toRemoveActors)
{
CloseUiInternal(ent!, key, actor);
}
}
var clientBuis = new ValueList<Enum>(ent.Comp.ClientOpenInterfaces.Keys);
// Check if the UI is open by us, otherwise dispose of it.
foreach (var key in clientBuis)
{
if (ent.Comp.Actors.TryGetValue(key, out var actors) &&
(attachedEnt == null || actors.Contains(attachedEnt.Value)))
{
continue;
}
var bui = ent.Comp.ClientOpenInterfaces[key];
if (bui.DeferredClose)
_queuedCloses.Add(bui);
else
{
ent.Comp.ClientOpenInterfaces.Remove(key);
bui.Dispose();
}
ent.Comp.ClientOpenInterfaces.Remove(key);
bui.Dispose();
}
}
// States
if (stateStates != null)
// update any states we have open
foreach (var (key, buiState) in state.States)
{
foreach (var key in ent.Comp.States.Keys)
if (ent.Comp.States.TryGetValue(key, out var existing) &&
existing.Equals(buiState))
{
if (!stateStates.ContainsKey(key))
ent.Comp.States.Remove(key);
continue;
}
// update any states we have open
foreach (var (key, buiState) in stateStates)
{
if (ent.Comp.States.TryGetValue(key, out var existing) &&
existing.Equals(buiState))
{
continue;
}
ent.Comp.States[key] = buiState;
ent.Comp.States[key] = buiState;
if (!ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui))
continue;
if (!ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui))
continue;
cBui.State = buiState;
cBui.UpdateState(buiState);
cBui.Update();
}
cBui.State = buiState;
cBui.UpdateState(buiState);
}
// If UI not open then open it
// If we get the first state for an ent coming in then don't open BUIs yet, just defer it until later.
var open = ent.Comp.LifeStage > ComponentLifeStage.Added;
if (attachedEnt != null && stateActors != null)
if (attachedEnt != null)
{
foreach (var (key, value) in ent.Comp.Interfaces)
{
@@ -559,7 +490,6 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
{
boundUserInterface.State = buiState;
boundUserInterface.UpdateState(buiState);
boundUserInterface.Update();
}
#if EXCEPTION_TOLERANCE
}
@@ -739,7 +669,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
if (!entity.Comp.States.Remove(key))
return;
DirtyField(entity, nameof(UserInterfaceComponent.States));
Dirty(entity);
}
// Non-null state, check if it matches existing.
else
@@ -752,14 +682,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
stateRef = state;
}
// Predict the change on client
if (state != null && _netManager.IsClient && entity.Comp.ClientOpenInterfaces.TryGetValue(key, out var bui))
{
bui.UpdateState(state);
bui.Update();
}
DirtyField(entity, nameof(UserInterfaceComponent.States));
Dirty(entity);
}
/// <summary>
@@ -1101,19 +1024,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
ent.Comp = AddComp<UserInterfaceComponent>(ent);
ent.Comp.Interfaces[key] = data;
DirtyField(ent, nameof(UserInterfaceComponent.Interfaces));
}
public bool TryGetUiState<T>(Entity<UserInterfaceComponent?> ent, Enum key, [NotNullWhen(true)] out T? state) where T : BoundUserInterfaceState
{
if (!Resolve(ent, ref ent.Comp, false) || !ent.Comp.States.TryGetValue(key, out var stateComp))
{
state = null;
return false;
}
state = (T)stateComp;
return true;
Dirty(ent, ent.Comp);
}
/// <summary>

View File

@@ -28,17 +28,24 @@ public sealed class BeforeEntityReadEvent
}
/// <summary>
/// This event is broadcast just before an entity gets serialized.
/// This event is broadcast just before the map loader reads the entity section. It can be used to somewhat modify
/// how the map data is read, as a super basic kind of map migration tool.
/// </summary>
public sealed class BeforeSaveEvent(EntityUid entity, EntityUid? map)
public sealed class BeforeSaveEvent
{
/// <summary>
/// The entity that is going to be saved. usually a map or grid.
/// </summary>
public EntityUid Entity = entity;
public EntityUid Entity;
/// <summary>
/// The map that the <see cref="Entity"/> is on.
/// </summary>
public EntityUid? Map = map;
public EntityUid? Map;
public BeforeSaveEvent(EntityUid entity, EntityUid? map)
{
Entity = entity;
Map = map;
}
}

View File

@@ -41,8 +41,10 @@ using System.Runtime.CompilerServices;
namespace Robust.Shared.Noise
{
#pragma warning disable RA0003
[Obsolete("Use FastNoiseLite")]
public sealed class FastNoise
#pragma warning restore RA0003
{
private const MethodImplOptions FN_INLINE = MethodImplOptions.AggressiveInlining;
private const int FN_CELLULAR_INDEX_MAX = 3;

View File

@@ -27,7 +27,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics
@@ -944,217 +943,6 @@ namespace Robust.Shared.Physics
private static readonly RayQueryCallback<RayQueryCallback> EasyRayQueryCallback =
(ref RayQueryCallback callback, Proxy proxy, in Vector2 hitPos, float distance) => callback(proxy, hitPos, distance);
internal delegate float RayCallback(RayCastInput input, T context, ref WorldRayCastContext state);
internal void RayCastNew(RayCastInput input, long mask, ref WorldRayCastContext state, RayCallback callback)
{
var p1 = input.Origin;
var d = input.Translation;
var r = d.Normalized();
// v is perpendicular to the segment.
var v = Vector2Helpers.Cross(1.0f, r);
var abs_v = Vector2.Abs(v);
// Separating axis for segment (Gino, p80).
// |dot(v, p1 - c)| > dot(|v|, h)
float maxFraction = input.MaxFraction;
var p2 = Vector2.Add(p1, maxFraction * d);
// Build a bounding box for the segment.
var segmentAABB = new Box2(Vector2.Min(p1, p2), Vector2.Max(p1, p2));
var stack = new GrowableStack<Proxy>(stackalloc Proxy[256]);
ref var baseRef = ref _nodes[0];
stack.Push(_root);
var subInput = input;
while (stack.GetCount() > 0)
{
var nodeId = stack.Pop();
if (nodeId == Proxy.Free)
{
continue;
}
var node = Unsafe.Add(ref baseRef, nodeId);
if (!node.Aabb.Intersects(segmentAABB))// || ( node->categoryBits & maskBits ) == 0 )
{
continue;
}
// Separating axis for segment (Gino, p80).
// |dot(v, p1 - c)| > dot(|v|, h)
// radius extension is added to the node in this case
var c = node.Aabb.Center;
var h = node.Aabb.Extents;
float term1 = MathF.Abs(Vector2.Dot(v, Vector2.Subtract(p1, c)));
float term2 = Vector2.Dot(abs_v, h);
if ( term2 < term1 )
{
continue;
}
if (node.IsLeaf)
{
subInput.MaxFraction = maxFraction;
float value = callback(subInput, node.UserData, ref state);
if (value == 0.0f)
{
// The client has terminated the ray cast.
return;
}
if (0.0f < value && value < maxFraction)
{
// Update segment bounding box.
maxFraction = value;
p2 = Vector2.Add(p1, maxFraction * d);
segmentAABB.BottomLeft = Vector2.Min( p1, p2 );
segmentAABB.TopRight = Vector2.Max( p1, p2 );
}
}
else
{
var stackCount = stack.GetCount();
Assert( stackCount < 256 - 1 );
if (stackCount < 256 - 1 )
{
// TODO_ERIN just put one node on the stack, continue on a child node
// TODO_ERIN test ordering children by nearest to ray origin
stack.Push(node.Child1);
stack.Push(node.Child2);
}
}
}
}
/// This function receives clipped ray-cast input for a proxy. The function
/// returns the new ray fraction.
/// - return a value of 0 to terminate the ray-cast
/// - return a value less than input->maxFraction to clip the ray
/// - return a value of input->maxFraction to continue the ray cast without clipping
internal delegate float TreeShapeCastCallback(ShapeCastInput input, T userData, ref WorldRayCastContext state);
internal void ShapeCast(ShapeCastInput input, long maskBits, TreeShapeCastCallback callback, ref WorldRayCastContext state)
{
if (input.Count == 0)
{
return;
}
var originAABB = new Box2(input.Points[0], input.Points[0]);
for (var i = 1; i < input.Count; ++i)
{
originAABB.BottomLeft = Vector2.Min(originAABB.BottomLeft, input.Points[i]);
originAABB.TopRight = Vector2.Max(originAABB.TopRight, input.Points[i]);
}
var radius = new Vector2(input.Radius, input.Radius);
originAABB.BottomLeft = Vector2.Subtract(originAABB.BottomLeft, radius);
originAABB.TopRight = Vector2.Add(originAABB.TopRight, radius );
var p1 = originAABB.Center;
var extension = originAABB.Extents;
// v is perpendicular to the segment.
var r = input.Translation;
var v = Vector2Helpers.Cross(1.0f, r);
var abs_v = Vector2.Abs(v);
// Separating axis for segment (Gino, p80).
// |dot(v, p1 - c)| > dot(|v|, h)
float maxFraction = input.MaxFraction;
// Build total box for the shape cast
var t = Vector2.Multiply(maxFraction, input.Translation);
var totalAABB = new Box2(
Vector2.Min(originAABB.BottomLeft, Vector2.Add(originAABB.BottomLeft, t)),
Vector2.Max(originAABB.TopRight, Vector2.Add( originAABB.TopRight, t))
);
var subInput = input;
ref var baseRef = ref _nodes[0];
var stack = new GrowableStack<Proxy>(stackalloc Proxy[256]);
stack.Push(_root);
while (stack.GetCount() > 0)
{
var nodeId = stack.Pop();
if (nodeId == Proxy.Free)
{
continue;
}
var node = Unsafe.Add(ref baseRef, nodeId);
if (!node.Aabb.Intersects(totalAABB))// || ( node->categoryBits & maskBits ) == 0 )
{
continue;
}
// Separating axis for segment (Gino, p80).
// |dot(v, p1 - c)| > dot(|v|, h)
// radius extension is added to the node in this case
var c = node.Aabb.Center;
var h = Vector2.Add(node.Aabb.Extents, extension);
float term1 = MathF.Abs(Vector2.Dot(v, Vector2.Subtract(p1, c)));
float term2 = Vector2.Dot(abs_v, h);
if (term2 < term1)
{
continue;
}
if (node.IsLeaf)
{
subInput.MaxFraction = maxFraction;
float value = callback(subInput, node.UserData, ref state);
if ( value == 0.0f )
{
// The client has terminated the ray cast.
return;
}
if (0.0f < value && value < maxFraction)
{
// Update segment bounding box.
maxFraction = value;
t = Vector2.Multiply(maxFraction, input.Translation);
totalAABB.BottomLeft = Vector2.Min( originAABB.BottomLeft, Vector2.Add(originAABB.BottomLeft, t));
totalAABB.TopRight = Vector2.Max( originAABB.TopRight, Vector2.Add( originAABB.TopRight, t));
}
}
else
{
var stackCount = stack.GetCount();
Assert(stackCount < 256 - 1);
if (stackCount < 255)
{
// TODO_ERIN just put one node on the stack, continue on a child node
// TODO_ERIN test ordering children by nearest to ray origin
stack.Push(node.Child1);
stack.Push(node.Child2);
}
}
}
}
public void RayCast(RayQueryCallback callback, in Ray input)
{
RayCast(ref callback, EasyRayQueryCallback, input);

View File

@@ -24,7 +24,6 @@ public sealed class DynamicTreeBroadPhase : IBroadPhase
}
public int Count => _tree.NodeCount;
public B2DynamicTree<FixtureProxy> Tree => _tree;
public Box2 GetFatAabb(DynamicTree.Proxy proxy)
{

View File

@@ -45,12 +45,6 @@ internal ref struct DistanceProxy
// GJK using Voronoi regions (Christer Ericson) and Barycentric coordinates.
internal DistanceProxy(Vector2[] vertices, float radius)
{
Vertices = vertices;
Radius = radius;
}
/// <summary>
/// Initialize the proxy using the given shape. The shape
/// must remain in scope while the proxy is in use.
@@ -149,13 +143,6 @@ internal ref struct DistanceProxy
return Vertices[bestIndex];
}
internal static DistanceProxy MakeProxy(Vector2[] vertices, int count, float radius )
{
count = Math.Min(count, PhysicsConstants.MaxPolygonVertices);
var proxy = new DistanceProxy(vertices[..count], radius);
return proxy;
}
}
/// <summary>
@@ -319,16 +306,6 @@ internal struct Simplex
}
}
public static Vector2 Weight2( float a1, Vector2 w1, float a2, Vector2 w2 )
{
return new Vector2(a1 * w1.X + a2 * w2.X, a1 * w1.Y + a2 * w2.Y);
}
public static Vector2 Weight3(float a1, Vector2 w1, float a2, Vector2 w2, float a3, Vector2 w3 )
{
return new Vector2(a1 * w1.X + a2 * w2.X + a3 * w3.X, a1 * w1.Y + a2 * w2.Y + a3 * w3.Y);
}
internal Vector2 GetClosestPoint()
{
switch (Count)
@@ -352,226 +329,6 @@ internal struct Simplex
}
}
public static Vector2 ComputeSimplexClosestPoint(Simplex s)
{
switch (s.Count)
{
case 0:
DebugTools.Assert(false);
return Vector2.Zero;
case 1:
return s.V._00.W;
case 2:
return Weight2(s.V._00.A, s.V._00.W, s.V._01.A, s.V._01.W);
case 3:
return Vector2.Zero;
default:
DebugTools.Assert(false);
return Vector2.Zero;
}
}
public static void ComputeSimplexWitnessPoints(ref Vector2 a, ref Vector2 b, Simplex s)
{
switch (s.Count)
{
case 0:
DebugTools.Assert(false);
break;
case 1:
a = s.V._00.WA;
b = s.V._00.WB;
break;
case 2:
a = Weight2(s.V._00.A, s.V._00.WA, s.V._01.A, s.V._01.WA);
b = Weight2(s.V._00.A, s.V._00.WB, s.V._01.A, s.V._01.WB);
break;
case 3:
a = Weight3(s.V._00.A, s.V._00.WA, s.V._01.A, s.V._01.WA, s.V._02.A, s.V._02.WA);
// TODO_ERIN why are these not equal?
//*b = b2Weight3(s->v1.a, s->v1.wB, s->v2.a, s->v2.wB, s->v3.a, s->v3.wB);
b = a;
break;
default:
DebugTools.Assert(false);
break;
}
}
// Solve a line segment using barycentric coordinates.
//
// p = a1 * w1 + a2 * w2
// a1 + a2 = 1
//
// The vector from the origin to the closest point on the line is
// perpendicular to the line.
// e12 = w2 - w1
// dot(p, e) = 0
// a1 * dot(w1, e) + a2 * dot(w2, e) = 0
//
// 2-by-2 linear system
// [1 1 ][a1] = [1]
// [w1.e12 w2.e12][a2] = [0]
//
// Define
// d12_1 = dot(w2, e12)
// d12_2 = -dot(w1, e12)
// d12 = d12_1 + d12_2
//
// Solution
// a1 = d12_1 / d12
// a2 = d12_2 / d12
public static void SolveSimplex2(ref Simplex s)
{
var w1 = s.V._00.W;
var w2 = s.V._01.W;
var e12 = Vector2.Subtract(w2, w1);
// w1 region
float d12_2 = -Vector2.Dot(w1, e12);
if (d12_2 <= 0.0f)
{
// a2 <= 0, so we clamp it to 0
s.V._00.A = 1.0f;
s.Count = 1;
return;
}
// w2 region
float d12_1 = Vector2.Dot(w2, e12);
if (d12_1 <= 0.0f)
{
// a1 <= 0, so we clamp it to 0
s.V._01.A = 1.0f;
s.Count = 1;
s.V._00 = s.V._01;
return;
}
// Must be in e12 region.
float inv_d12 = 1.0f / ( d12_1 + d12_2 );
s.V._00.A = d12_1 * inv_d12;
s.V._01.A = d12_2 * inv_d12;
s.Count = 2;
}
public static void SolveSimplex3(ref Simplex s)
{
var w1 = s.V._00.W;
var w2 = s.V._01.W;
var w3 = s.V._02.W;
// Edge12
// [1 1 ][a1] = [1]
// [w1.e12 w2.e12][a2] = [0]
// a3 = 0
var e12 = Vector2.Subtract(w2, w1);
float w1e12 = Vector2.Dot(w1, e12);
float w2e12 = Vector2.Dot(w2, e12);
float d12_1 = w2e12;
float d12_2 = -w1e12;
// Edge13
// [1 1 ][a1] = [1]
// [w1.e13 w3.e13][a3] = [0]
// a2 = 0
var e13 = Vector2.Subtract(w3, w1);
float w1e13 = Vector2.Dot(w1, e13);
float w3e13 = Vector2.Dot(w3, e13);
float d13_1 = w3e13;
float d13_2 = -w1e13;
// Edge23
// [1 1 ][a2] = [1]
// [w2.e23 w3.e23][a3] = [0]
// a1 = 0
var e23 = Vector2.Subtract(w3, w2);
float w2e23 = Vector2.Dot(w2, e23);
float w3e23 = Vector2.Dot(w3, e23);
float d23_1 = w3e23;
float d23_2 = -w2e23;
// Triangle123
float n123 = Vector2Helpers.Cross(e12, e13);
float d123_1 = n123 * Vector2Helpers.Cross(w2, w3);
float d123_2 = n123 * Vector2Helpers.Cross(w3, w1);
float d123_3 = n123 * Vector2Helpers.Cross(w1, w2);
// w1 region
if (d12_2 <= 0.0f && d13_2 <= 0.0f)
{
s.V._00.A = 1.0f;
s.Count = 1;
return;
}
// e12
if (d12_1 > 0.0f && d12_2 > 0.0f && d123_3 <= 0.0f)
{
float inv_d12 = 1.0f / ( d12_1 + d12_2 );
s.V._00.A = d12_1 * inv_d12;
s.V._01.A = d12_2 * inv_d12;
s.Count = 2;
return;
}
// e13
if (d13_1 > 0.0f && d13_2 > 0.0f && d123_2 <= 0.0f)
{
float inv_d13 = 1.0f / ( d13_1 + d13_2 );
s.V._00.A = d13_1 * inv_d13;
s.V._02.A = d13_2 * inv_d13;
s.Count = 2;
s.V._01 = s.V._02;
return;
}
// w2 region
if (d12_1 <= 0.0f && d23_2 <= 0.0f)
{
s.V._01.A = 1.0f;
s.Count = 1;
s.V._00 = s.V._01;
return;
}
// w3 region
if (d13_1 <= 0.0f && d23_1 <= 0.0f)
{
s.V._02.A = 1.0f;
s.Count = 1;
s.V._00 = s.V._02;
return;
}
// e23
if (d23_1 > 0.0f && d23_2 > 0.0f && d123_1 <= 0.0f)
{
float inv_d23 = 1.0f / ( d23_1 + d23_2 );
s.V._01.A = d23_1 * inv_d23;
s.V._02.A = d23_2 * inv_d23;
s.Count = 2;
s.V._00 = s.V._02;
return;
}
// Must be in triangle123
float inv_d123 = 1.0f / (d123_1 + d123_2 + d123_3);
s.V._00.A = d123_1 * inv_d123;
s.V._01.A = d123_2 * inv_d123;
s.V._02.A = d123_3 * inv_d123;
s.Count = 3;
}
internal void GetWitnessPoints(out Vector2 pA, out Vector2 pB)
{
switch (Count)

View File

@@ -27,7 +27,6 @@ using System.Runtime.InteropServices;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Shapes;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -173,13 +172,6 @@ namespace Robust.Shared.Physics.Collision.Shapes
{
}
internal PolygonShape(Polygon poly)
{
Vertices = poly.Vertices;
Normals = poly.Normals;
Centroid = poly.Centroid;
}
public PolygonShape(float radius)
{
Radius = radius;

View File

@@ -30,17 +30,13 @@ using Robust.Shared.Maths;
using Robust.Shared.Physics.Dynamics.Contacts;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Physics.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class PhysicsComponent : Component, IComponentDelta
public sealed partial class PhysicsComponent : Component
{
public GameTick LastFieldUpdate { get; set; }
public GameTick[] LastModifiedFields { get; set; }
/// <summary>
/// Has this body been added to an island previously in this tick.
/// </summary>
@@ -61,10 +57,10 @@ public sealed partial class PhysicsComponent : Component, IComponentDelta
/// </summary>
internal readonly LinkedList<Contact> Contacts = new();
[DataField]
[DataField("ignorePaused"), ViewVariables(VVAccess.ReadWrite)]
public bool IgnorePaused;
[DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
[DataField("bodyType"), Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
public BodyType BodyType = BodyType.Static;
// We'll also block Static bodies from ever being awake given they don't need to move.
@@ -78,11 +74,13 @@ public sealed partial class PhysicsComponent : Component, IComponentDelta
/// body will be woken.
/// </summary>
/// <value><c>true</c> if sleeping is allowed; otherwise, <c>false</c>.</value>
[DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
[ViewVariables(VVAccess.ReadWrite), DataField("sleepingAllowed"),
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
Other = AccessPermissions.Read)]
public bool SleepingAllowed = true;
[DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
[ViewVariables(VVAccess.ReadWrite), DataField("sleepTime"),
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
Other = AccessPermissions.Read)]
public float SleepTime = 0f;
@@ -92,7 +90,8 @@ public sealed partial class PhysicsComponent : Component, IComponentDelta
/// <remarks>
/// Also known as Enabled in Box2D
/// </remarks>
[DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
[ViewVariables(VVAccess.ReadWrite), DataField("canCollide"),
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
Other = AccessPermissions.Read)]
public bool CanCollide = true;
@@ -169,7 +168,7 @@ public sealed partial class PhysicsComponent : Component, IComponentDelta
/// <summary>
/// Is the body allowed to have angular velocity.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField,
[ViewVariables(VVAccess.ReadWrite), DataField("fixedRotation"),
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
public bool FixedRotation = true;
@@ -190,7 +189,7 @@ public sealed partial class PhysicsComponent : Component, IComponentDelta
/// The force is applied to the center of mass.
/// https://en.wikipedia.org/wiki/Force
/// </remarks>
[ViewVariables(VVAccess.ReadWrite), DataField,
[ViewVariables(VVAccess.ReadWrite), DataField("force"),
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
public Vector2 Force;
@@ -201,7 +200,7 @@ public sealed partial class PhysicsComponent : Component, IComponentDelta
/// The torque rotates around the Z axis on the object.
/// https://en.wikipedia.org/wiki/Torque
/// </remarks>
[ViewVariables(VVAccess.ReadWrite), DataField,
[ViewVariables(VVAccess.ReadWrite), DataField("torque"),
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
public float Torque;
@@ -217,7 +216,7 @@ public sealed partial class PhysicsComponent : Component, IComponentDelta
/// This is a set amount that the body's linear velocity is reduced by every tick.
/// Combined with the tile friction.
/// </summary>
[DataField,
[ViewVariables(VVAccess.ReadWrite), DataField("linearDamping"),
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
Other = AccessPermissions.Read)]
public float LinearDamping = 0.2f;
@@ -227,7 +226,7 @@ public sealed partial class PhysicsComponent : Component, IComponentDelta
/// Combined with the tile friction.
/// </summary>
/// <returns></returns>
[DataField,
[ViewVariables(VVAccess.ReadWrite), DataField("angularDamping"),
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
Other = AccessPermissions.Read)]
public float AngularDamping = 0.2f;
@@ -261,7 +260,7 @@ public sealed partial class PhysicsComponent : Component, IComponentDelta
/// <summary>
/// The current status of the object
/// </summary>
[DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
[ViewVariables(VVAccess.ReadWrite), DataField("bodyStatus"), Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
public BodyStatus BodyStatus { get; set; }
[ViewVariables, Access(typeof(SharedPhysicsSystem))]

View File

@@ -1,97 +1,35 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
namespace Robust.Shared.Physics.Components;
/// <summary>
/// Average use-case of only linear velocity update
/// </summary>
[Serializable, NetSerializable]
public record struct PhysicsLinearVelocityDeltaState : IComponentDeltaState<PhysicsComponentState>
public readonly record struct PhysicsComponentState(
bool CanCollide,
bool SleepingAllowed,
bool FixedRotation,
BodyStatus Status,
Vector2 LinearVelocity,
float AngularVelocity,
BodyType BodyType,
float Friction,
float LinearDamping,
float AngularDamping)
: IComponentState
{
public Vector2 LinearVelocity;
public readonly bool CanCollide = CanCollide;
public readonly bool SleepingAllowed = SleepingAllowed;
public readonly bool FixedRotation = FixedRotation;
public readonly BodyStatus Status = Status;
public void ApplyToFullState(PhysicsComponentState fullState)
{
fullState.LinearVelocity = LinearVelocity;
}
public readonly Vector2 LinearVelocity = LinearVelocity;
public readonly float AngularVelocity = AngularVelocity;
public readonly BodyType BodyType = BodyType;
public PhysicsComponentState CreateNewFullState(PhysicsComponentState fullState)
{
var copy = new PhysicsComponentState(fullState)
{
LinearVelocity = LinearVelocity,
};
return copy;
}
}
/// <summary>
/// 2nd-most typical usecase of just velocity updates
/// </summary>
[Serializable, NetSerializable]
public record struct PhysicsVelocityDeltaState : IComponentDeltaState<PhysicsComponentState>
{
public Vector2 LinearVelocity;
public float AngularVelocity;
public void ApplyToFullState(PhysicsComponentState fullState)
{
fullState.LinearVelocity = LinearVelocity;
fullState.AngularVelocity = AngularVelocity;
}
public PhysicsComponentState CreateNewFullState(PhysicsComponentState fullState)
{
var copy = new PhysicsComponentState(fullState)
{
LinearVelocity = LinearVelocity,
AngularVelocity = AngularVelocity
};
return copy;
}
}
[Serializable, NetSerializable]
public sealed class PhysicsComponentState : IComponentState
{
public bool CanCollide;
public bool SleepingAllowed;
public bool FixedRotation;
public BodyStatus Status;
public Vector2 LinearVelocity;
public float AngularVelocity;
public BodyType BodyType;
public float Friction;
public float LinearDamping;
public float AngularDamping;
public Vector2 Force;
public float Torque;
public PhysicsComponentState() {}
public PhysicsComponentState(PhysicsComponentState existing)
{
CanCollide = existing.CanCollide;
SleepingAllowed = existing.SleepingAllowed;
FixedRotation = existing.FixedRotation;
Status = existing.Status;
LinearVelocity = existing.LinearVelocity;
AngularVelocity = existing.AngularVelocity;
BodyType = existing.BodyType;
Friction = existing.Friction;
LinearDamping = existing.LinearDamping;
AngularDamping = existing.AngularDamping;
Force = existing.Force;
Torque = existing.Torque;
}
public readonly float Friction = Friction;
public readonly float LinearDamping = LinearDamping;
public readonly float AngularDamping = AngularDamping;
}

View File

@@ -10,8 +10,6 @@ public interface IBroadPhase
{
int Count { get; }
public B2DynamicTree<FixtureProxy> Tree { get; }
Box2 GetFatAabb(DynamicTree.Proxy proxy);
DynamicTree.Proxy AddProxy(ref FixtureProxy proxy);

View File

@@ -87,8 +87,8 @@ internal record struct Polygon : IPhysShape
if (hull.Count < 3)
{
Vertices = [];
Normals = [];
Vertices = Array.Empty<Vector2>();
Normals = Array.Empty<Vector2>();
return;
}

View File

@@ -1,736 +0,0 @@
using System;
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Shapes;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics.Systems;
public sealed partial class RayCastSystem
{
/*
* This is really "geometry and friends" as it has all the private methods.
*/
#region Callbacks
/// <summary>
/// Returns every entity from the callback.
/// </summary>
public static float RayCastAllCallback(FixtureProxy proxy, Vector2 point, Vector2 normal, float fraction, ref RayResult result)
{
result.Results.Add(new RayHit(proxy.Entity, normal, fraction)
{
Point = point,
});
return 1f;
}
/// <summary>
/// Gets the closest entity from the callback.
/// </summary>
public static float RayCastClosestCallback(FixtureProxy proxy, Vector2 point, Vector2 normal, float fraction, ref RayResult result)
{
var add = false;
if (result.Results.Count > 0)
{
if (result.Results[0].Fraction > fraction)
{
add = true;
result.Results.Clear();
}
}
else
{
add = true;
}
if (add)
{
result.Results.Add(new RayHit(proxy.Entity, normal, fraction)
{
Point = point,
});
}
return fraction;
}
#endregion
#region Raycast
private CastOutput RayCastShape(RayCastInput input, IPhysShape shape, Transform transform)
{
var localInput = input;
localInput.Origin = Physics.Transform.InvTransformPoint(transform, input.Origin);
localInput.Translation = Quaternion2D.InvRotateVector(transform.Quaternion2D, input.Translation);
CastOutput output = new();
switch (shape)
{
/*
case b2_capsuleShape:
output = b2RayCastCapsule( &localInput, &shape->capsule );
break;
*/
case PhysShapeCircle circle:
output = RayCastCircle(localInput, circle);
break;
case PolygonShape polyShape:
{
output = RayCastPolygon(localInput, (Polygon) polyShape);
}
break;
case Polygon poly:
{
output = RayCastPolygon(localInput, poly);
}
break;
default:
return output;
}
output.Point = Physics.Transform.Mul(transform, output.Point);
output.Normal = Quaternion2D.RotateVector(transform.Quaternion2D, output.Normal);
return output;
}
/// <summary>
/// This callback is invoked upon every AABB collision.
/// </summary>
private static float RayCastCallback(RayCastInput input, FixtureProxy proxy, ref WorldRayCastContext worldContext)
{
if ((proxy.Fixture.CollisionLayer & worldContext.Filter.MaskBits) == 0 && (proxy.Fixture.CollisionMask & worldContext.Filter.LayerBits) == 0)
{
return input.MaxFraction;
}
if (worldContext.Filter.IsIgnored?.Invoke(proxy.Entity) == true)
{
return input.MaxFraction;
}
var transform = worldContext.Physics.GetLocalPhysicsTransform(proxy.Entity);
var output = worldContext.System.RayCastShape(input, proxy.Fixture.Shape, transform);
if (output.Hit)
{
// Fraction returned determines what B2Dynamictree will do, i.e. shrink the AABB or not.
var fraction = worldContext.fcn(proxy, output.Point, output.Normal, output.Fraction, ref worldContext.Result);
return fraction;
}
return input.MaxFraction;
}
// Precision Improvements for Ray / Sphere Intersection - Ray Tracing Gems 2019
// http://www.codercorner.com/blog/?p=321
internal CastOutput RayCastCircle(RayCastInput input, PhysShapeCircle shape)
{
DebugTools.Assert(input.IsValidRay());
var p = shape.Position;
var output = new CastOutput();
// Shift ray so circle center is the origin
var s = Vector2.Subtract(input.Origin, p);
float length = 0f;
var d = input.Translation.GetLengthAndNormalize(ref length);
if (length == 0.0f)
{
// zero length ray
return output;
}
// Find closest point on ray to origin
// solve: dot(s + t * d, d) = 0
float t = -Vector2.Dot(s, d);
// c is the closest point on the line to the origin
var c = Vector2.Add(s, t * d);
float cc = Vector2.Dot(c, c);
float r = shape.Radius;
float rr = r * r;
if (cc > rr)
{
// closest point is outside the circle
return output;
}
// Pythagorus
float h = MathF.Sqrt(rr - cc);
float fraction = t - h;
if ( fraction < 0.0f || input.MaxFraction * length < fraction )
{
// outside the range of the ray segment
return output;
}
var hitPoint = Vector2.Add(s, fraction * d);
output.Fraction = fraction / length;
output.Normal = hitPoint.Normalized();
output.Point = Vector2.Add(p, shape.Radius * output.Normal);
output.Hit = true;
return output;
}
private CastOutput RayCastPolygon(RayCastInput input, Polygon shape)
{
if (shape.Radius == 0.0f)
{
// Put the ray into the polygon's frame of reference.
var p1 = input.Origin;
var d = input.Translation;
float lower = 0.0f, upper = input.MaxFraction;
var index = -1;
var output = new CastOutput()
{
Fraction = 0f,
};
for ( var i = 0; i < shape.VertexCount; ++i )
{
// p = p1 + a * d
// dot(normal, p - v) = 0
// dot(normal, p1 - v) + a * dot(normal, d) = 0
float numerator = Vector2.Dot(shape.Normals[i], Vector2.Subtract( shape.Vertices[i], p1 ) );
float denominator = Vector2.Dot(shape.Normals[i], d );
if ( denominator == 0.0f )
{
if ( numerator < 0.0f )
{
return output;
}
}
else
{
// Note: we want this predicate without division:
// lower < numerator / denominator, where denominator < 0
// Since denominator < 0, we have to flip the inequality:
// lower < numerator / denominator <==> denominator * lower > numerator.
if ( denominator < 0.0f && numerator < lower * denominator )
{
// Increase lower.
// The segment enters this half-space.
lower = numerator / denominator;
index = i;
}
else if ( denominator > 0.0f && numerator < upper * denominator )
{
// Decrease upper.
// The segment exits this half-space.
upper = numerator / denominator;
}
}
// The use of epsilon here causes the B2_ASSERT on lower to trip
// in some cases. Apparently the use of epsilon was to make edge
// shapes work, but now those are handled separately.
// if (upper < lower - b2_epsilon)
if ( upper < lower )
{
return output;
}
}
DebugTools.Assert( 0.0f <= lower && lower <= input.MaxFraction );
if (index >= 0)
{
output.Fraction = lower;
output.Normal = shape.Normals[index];
output.Point = Vector2.Add(p1, lower * d);
output.Hit = true;
}
return output;
}
// TODO_ERIN this is not working for ray vs box (zero radii)
var castInput = new ShapeCastPairInput
{
ProxyA = DistanceProxy.MakeProxy(shape.Vertices, shape.VertexCount, shape.Radius),
ProxyB = DistanceProxy.MakeProxy([input.Origin], 1, 0.0f),
TransformA = Physics.Transform.Empty,
TransformB = Physics.Transform.Empty,
TranslationB = input.Translation,
MaxFraction = input.MaxFraction
};
return ShapeCast(castInput);
}
// Ray vs line segment
private CastOutput RayCastSegment(RayCastInput input, EdgeShape shape, bool oneSided)
{
var output = new CastOutput();
if (oneSided)
{
// Skip left-side collision
float offset = Vector2Helpers.Cross(Vector2.Subtract(input.Origin, shape.Vertex0), Vector2.Subtract( shape.Vertex1, shape.Vertex0));
if ( offset < 0.0f )
{
return output;
}
}
// Put the ray into the edge's frame of reference.
var p1 = input.Origin;
var d = input.Translation;
var v1 = shape.Vertex0;
var v2 = shape.Vertex1;
var e = Vector2.Subtract( v2, v1 );
float length = 0f;
var eUnit = e.GetLengthAndNormalize(ref length);
if (length == 0.0f)
{
return output;
}
// Normal points to the right, looking from v1 towards v2
var normal = eUnit.RightPerp();
// Intersect ray with infinite segment using normal
// Similar to intersecting a ray with an infinite plane
// p = p1 + t * d
// dot(normal, p - v1) = 0
// dot(normal, p1 - v1) + t * dot(normal, d) = 0
float numerator = Vector2.Dot(normal, Vector2.Subtract(v1, p1));
float denominator = Vector2.Dot(normal, d);
if (denominator == 0.0f)
{
// parallel
return output;
}
float t = numerator / denominator;
if ( t < 0.0f || input.MaxFraction < t )
{
// out of ray range
return output;
}
// Intersection point on infinite segment
var p = Vector2.Add(p1, t * d);
// Compute position of p along segment
// p = v1 + s * e
// s = dot(p - v1, e) / dot(e, e)
float s = Vector2.Dot(Vector2.Subtract(p, v1), eUnit);
if ( s < 0.0f || length < s )
{
// out of segment range
return output;
}
if ( numerator > 0.0f )
{
normal = -normal;
}
output.Fraction = t;
output.Point = Vector2.Add(p1, t * d);
output.Normal = normal;
output.Hit = true;
return output;
}
#endregion
#region Shape
private CastOutput ShapeCastShape(ShapeCastInput input, IPhysShape shape, Transform transform)
{
var localInput = input;
for ( int i = 0; i < localInput.Count; ++i )
{
localInput.Points[i] = Physics.Transform.MulT(transform, input.Points[i]);
}
localInput.Translation = Quaternion2D.InvRotateVector(transform.Quaternion2D, input.Translation);
CastOutput output;
switch (shape)
{
case PhysShapeCircle circle:
output = ShapeCastCircle(localInput, circle);
break;
case PolygonShape pShape:
output = ShapeCastPolygon(localInput, (Polygon) pShape);
break;
case Polygon poly:
output = ShapeCastPolygon(localInput, poly);
break;
default:
return new CastOutput();
}
output.Point = Physics.Transform.Mul(transform, output.Point);
output.Normal = Quaternion2D.RotateVector(transform.Quaternion2D, output.Normal);
return output;
}
/// <summary>
/// This callback is invoked upon getting the AABB inside of B2DynamicTree.
/// </summary>
/// <returns>The max fraction to continue checking for. If this is lower then we will start dropping more shapes early</returns>
private float ShapeCastCallback(ShapeCastInput input, FixtureProxy proxy, ref WorldRayCastContext worldContext)
{
var filter = worldContext.Filter;
if ((proxy.Fixture.CollisionLayer & filter.MaskBits) == 0 && (proxy.Fixture.CollisionMask & filter.LayerBits) == 0)
{
return input.MaxFraction;
}
if ((filter.Flags & QueryFlags.Sensors) == 0x0 && !proxy.Fixture.Hard)
{
return input.MaxFraction;
}
if (worldContext.Filter.IsIgnored?.Invoke(proxy.Entity) == true)
{
return input.MaxFraction;
}
var transform = worldContext.Physics.GetLocalPhysicsTransform(proxy.Entity);
var output = ShapeCastShape(input, proxy.Fixture.Shape, transform);
if (output.Hit)
{
var fraction = worldContext.fcn(proxy, output.Point, output.Normal, output.Fraction, ref worldContext.Result);
return fraction;
}
return input.MaxFraction;
}
// GJK-raycast
// Algorithm by Gino van den Bergen.
// "Smooth Mesh Contacts with GJK" in Game Physics Pearls. 2010
// todo this is failing when used to raycast a box
// todo this converges slowly with a radius
private CastOutput ShapeCast(ShapeCastPairInput input)
{
var output = new CastOutput()
{
Fraction = input.MaxFraction,
};
var proxyA = input.ProxyA;
var count = input.ProxyB.Vertices.Length;
var xfA = input.TransformA;
var xfB = input.TransformB;
var xf = Physics.Transform.InvMulTransforms(xfA, xfB);
// Put proxyB in proxyA's frame to reduce round-off error
var proxyBVerts = new Vector2[input.ProxyB.Vertices.Length];
for ( int i = 0; i < count; ++i )
{
proxyBVerts[i] = Physics.Transform.Mul(xf, input.ProxyB.Vertices[i]);
}
var proxyB = DistanceProxy.MakeProxy(proxyBVerts, count, input.ProxyB.Radius);
DebugTools.Assert(proxyB.Vertices.Length <= PhysicsConstants.MaxPolygonVertices);
float radius = proxyA.Radius + proxyB.Radius;
var r = Quaternion2D.RotateVector(xf.Quaternion2D, input.TranslationB);
float lambda = 0.0f;
float maxFraction = input.MaxFraction;
// Initial simplex
Simplex simplex;
simplex = new()
{
Count = 0,
V = new FixedArray4<SimplexVertex>()
};
// Get an initial point in A - B
int indexA = FindSupport(proxyA, -r);
var wA = proxyA.Vertices[indexA];
int indexB = FindSupport(proxyB, r);
var wB = proxyB.Vertices[indexB];
var v = Vector2.Subtract(wA, wB);
// Sigma is the target distance between proxies
const float linearSlop = PhysicsConstants.LinearSlop;
var sigma = MathF.Max(linearSlop, radius - linearSlop);
// Main iteration loop.
const int k_maxIters = 20;
int iter = 0;
while ( iter < k_maxIters && v.Length() > sigma + 0.5f * linearSlop )
{
DebugTools.Assert(simplex.Count < 3);
output.Iterations += 1;
// Support in direction -v (A - B)
indexA = FindSupport(proxyA, -v);
wA = proxyA.Vertices[indexA];
indexB = FindSupport(proxyB, v);
wB = proxyB.Vertices[indexB];
var p = Vector2.Subtract(wA, wB);
// -v is a normal at p, normalize to work with sigma
v = v.Normalized();
// Intersect ray with plane
float vp = Vector2.Dot(v, p);
float vr = Vector2.Dot(v, r);
if ( vp - sigma > lambda * vr )
{
if ( vr <= 0.0f )
{
// miss
return output;
}
lambda = ( vp - sigma ) / vr;
if ( lambda > maxFraction )
{
// too far
return output;
}
// reset the simplex
simplex.Count = 0;
}
// Reverse simplex since it works with B - A.
// Shift by lambda * r because we want the closest point to the current clip point.
// Note that the support point p is not shifted because we want the plane equation
// to be formed in unshifted space.
ref var vertex = ref simplex.V.AsSpan[simplex.Count];
vertex.IndexA = indexB;
vertex.WA = new Vector2(wB.X + lambda * r.X, wB.Y + lambda * r.Y);
vertex.IndexB = indexA;
vertex.WB = wA;
vertex.W = Vector2.Subtract(vertex.WB, vertex.WA);
vertex.A = 1.0f;
simplex.Count += 1;
switch (simplex.Count)
{
case 1:
break;
case 2:
Simplex.SolveSimplex2(ref simplex);
break;
case 3:
Simplex.SolveSimplex3(ref simplex);
break;
default:
throw new NotImplementedException();
}
// If we have 3 points, then the origin is in the corresponding triangle.
if ( simplex.Count == 3 )
{
// Overlap
// Yes this means you need to manually query for overlaps.
return output;
}
// Get search direction.
// todo use more accurate segment perpendicular
v = Simplex.ComputeSimplexClosestPoint(simplex);
// Iteration count is equated to the number of support point calls.
++iter;
}
if ( iter == 0 || lambda == 0.0f )
{
// Initial overlap
return output;
}
// Prepare output.
Vector2 pointA = Vector2.Zero, pointB = Vector2.Zero;
Simplex.ComputeSimplexWitnessPoints(ref pointB, ref pointA, simplex);
var n = (-v).Normalized();
var point = new Vector2(pointA.X + proxyA.Radius * n.X, pointA.Y + proxyA.Radius * n.Y);
output.Point = Physics.Transform.Mul(xfA, point);
output.Normal = Quaternion2D.RotateVector(xfA.Quaternion2D, n);
output.Fraction = lambda;
output.Iterations = iter;
output.Hit = true;
return output;
}
private int FindSupport(DistanceProxy proxy, Vector2 direction)
{
int bestIndex = 0;
float bestValue = Vector2.Dot(proxy.Vertices[0], direction);
for ( int i = 1; i < proxy.Vertices.Length; ++i )
{
float value = Vector2.Dot(proxy.Vertices[i], direction);
if ( value > bestValue )
{
bestIndex = i;
bestValue = value;
}
}
return bestIndex;
}
private CastOutput ShapeCastCircle(ShapeCastInput input, PhysShapeCircle shape)
{
var pairInput = new ShapeCastPairInput
{
ProxyA = DistanceProxy.MakeProxy([shape.Position], 1, shape.Radius ),
ProxyB = DistanceProxy.MakeProxy(input.Points, input.Count, input.Radius ),
TransformA = Physics.Transform.Empty,
TransformB = Physics.Transform.Empty,
TranslationB = input.Translation,
MaxFraction = input.MaxFraction
};
var output = ShapeCast(pairInput);
return output;
}
private CastOutput ShapeCastPolygon(ShapeCastInput input, Polygon shape)
{
var pairInput = new ShapeCastPairInput
{
ProxyA = DistanceProxy.MakeProxy(shape.Vertices, shape.VertexCount, shape.Radius),
ProxyB = DistanceProxy.MakeProxy(input.Points, input.Count, input.Radius),
TransformA = Physics.Transform.Empty,
TransformB = Physics.Transform.Empty,
TranslationB = input.Translation,
MaxFraction = input.MaxFraction
};
var output = ShapeCast(pairInput);
return output;
}
private CastOutput ShapeCastSegment(ShapeCastInput input, EdgeShape shape)
{
var pairInput = new ShapeCastPairInput();
pairInput.ProxyA = DistanceProxy.MakeProxy([shape.Vertex0], 2, 0.0f);
pairInput.ProxyB = DistanceProxy.MakeProxy(input.Points, input.Count, input.Radius);
pairInput.TransformA = Physics.Transform.Empty;
pairInput.TransformB = Physics.Transform.Empty;
pairInput.TranslationB = input.Translation;
pairInput.MaxFraction = input.MaxFraction;
var output = ShapeCast(pairInput);
return output;
}
#endregion
}
internal ref struct WorldRayCastContext
{
public RayCastSystem System;
public SharedPhysicsSystem Physics;
public CastResult fcn;
public QueryFilter Filter;
public float Fraction;
public RayResult Result;
}
internal ref struct ShapeCastPairInput
{
public DistanceProxy ProxyA;
public DistanceProxy ProxyB;
public Transform TransformA;
public Transform TransformB;
public Vector2 TranslationB;
/// <summary>
/// The fraction of the translation to consider, typically 1
/// </summary>
public float MaxFraction;
}
internal record struct ShapeCastInput
{
public Transform Origin;
/// A point cloud to cast
public Vector2[] Points;
/// The number of points
public int Count;
/// The radius around the point cloud
public float Radius;
/// The translation of the shape cast
public Vector2 Translation;
/// The maximum fraction of the translation to consider, typically 1
public float MaxFraction;
}
internal record struct RayCastInput
{
public Vector2 Origin;
public Vector2 Translation;
public float MaxFraction;
public bool IsValidRay()
{
bool isValid = Origin.IsValid() && Translation.IsValid() && MaxFraction.IsValid() &&
0.0f <= MaxFraction && MaxFraction < float.MaxValue;
return isValid;
}
}
internal ref struct CastOutput
{
public Vector2 Normal;
public Vector2 Point;
public float Fraction;
public int Iterations;
public bool Hit;
}

View File

@@ -1,465 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Shapes;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics.Systems;
public sealed partial class RayCastSystem : EntitySystem
{
/*
* A few things to keep in mind with the below:
* - Raycasts are done relative to the corresponding broadphases.
* - The raycast results need to be transformed into Map terms.
* - If you wish to add more helper methods make a new partial and dump them there and have them call the below methods.
*/
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
private readonly RayComparer _rayComparer = new();
#region RayCast
private sealed class RayComparer : IComparer<RayHit>
{
public int Compare(RayHit x, RayHit y)
{
return x.Fraction.CompareTo(y.Fraction);
}
}
private void AdjustResults(ref RayResult result, int index, Transform xf)
{
for (var i = index; i < result.Results.Count; i++)
{
result.Results[i].Point = Physics.Transform.Mul(xf, result.Results[i].Point);
}
}
/*
* Raycasts that return all entities sorted.
*/
/// <summary>
/// Casts a ray against a broadphase.
/// </summary>
public void CastRay(Entity<BroadphaseComponent?> entity, ref RayResult result, Vector2 origin, Vector2 translation, QueryFilter filter, bool sorted = true)
{
if (!Resolve(entity.Owner, ref entity.Comp))
return;
DebugTools.Assert(origin.IsValid());
DebugTools.Assert(translation.IsValid());
var input = new RayCastInput()
{
Origin = origin,
Translation = translation,
MaxFraction = 1f,
};
var worldContext = new WorldRayCastContext()
{
fcn = RayCastAllCallback,
Filter = filter,
Fraction = 1f,
Physics = _physics,
System = this,
Result = result,
};
entity.Comp.DynamicTree.Tree.RayCastNew(input, filter.MaskBits, ref worldContext, RayCastCallback);
input.MaxFraction = worldContext.Fraction;
entity.Comp.StaticTree.Tree.RayCastNew(input, filter.MaskBits, ref worldContext, RayCastCallback);
result = worldContext.Result;
if (sorted)
{
result.Results.Sort(_rayComparer);
}
}
/// <summary>
/// Returns all entities hit in order.
/// </summary>
[Pure]
public RayResult CastRay(MapId mapId, Vector2 origin, Vector2 translation, QueryFilter filter)
{
DebugTools.Assert(origin.IsValid());
DebugTools.Assert(translation.IsValid());
var input = new RayCastInput
{
Origin = origin,
Translation = translation,
MaxFraction = 1.0f
};
var result = new RayResult();
var start = origin;
var end = origin + translation;
var aabb = new Box2(Vector2.Min(start, end), Vector2.Max(start, end));
var state = (input, filter, result, this, _physics);
_broadphase.GetBroadphases(mapId, aabb, ref state,
static (Entity<BroadphaseComponent> entity, ref (RayCastInput input, QueryFilter filter, RayResult result, RayCastSystem system, SharedPhysicsSystem Physics) tuple) =>
{
var transform = tuple.Physics.GetPhysicsTransform(entity.Owner);
var localOrigin = Physics.Transform.InvTransformPoint(transform, tuple.input.Origin);
var localTranslation = Physics.Transform.InvTransformPoint(transform, tuple.input.Origin + tuple.input.Translation) - localOrigin;
var oldIndex = tuple.result.Results.Count;
tuple.system.CastRay((entity.Owner, entity.Comp), ref tuple.result, localOrigin, localTranslation, filter: tuple.filter, sorted: false);
tuple.system.AdjustResults(ref tuple.result, oldIndex, transform);
});
result = state.result;
result.Results.Sort(_rayComparer);
return result;
}
/*
* Raycasts that only return the closest entity.
*/
/// <summary>
/// Casts a ray against a broadphase.
/// </summary>
public void CastRayClosest(Entity<BroadphaseComponent?> entity, ref RayResult result, Vector2 origin, Vector2 translation, QueryFilter filter)
{
if (!Resolve(entity.Owner, ref entity.Comp))
return;
DebugTools.Assert(origin.IsValid());
DebugTools.Assert(translation.IsValid());
var input = new RayCastInput()
{
Origin = origin,
Translation = translation,
MaxFraction = 1f,
};
var worldContext = new WorldRayCastContext()
{
fcn = RayCastClosestCallback,
Filter = filter,
Fraction = 1f,
Physics = _physics,
System = this,
Result = result,
};
entity.Comp.DynamicTree.Tree.RayCastNew(input, filter.MaskBits, ref worldContext, RayCastCallback);
input.MaxFraction = worldContext.Fraction;
entity.Comp.StaticTree.Tree.RayCastNew(input, filter.MaskBits, ref worldContext, RayCastCallback);
result = worldContext.Result;
DebugTools.Assert(result.Results.Count <= 1);
}
/// <summary>
/// Returns all entities hit in order.
/// </summary>
public RayResult CastRayClosest(MapId mapId, Vector2 origin, Vector2 translation, QueryFilter filter)
{
DebugTools.Assert(origin.IsValid());
DebugTools.Assert(translation.IsValid());
var input = new RayCastInput
{
Origin = origin,
Translation = translation,
MaxFraction = 1.0f
};
var result = new RayResult();
var end = origin + translation;
var aabb = new Box2(Vector2.Min(origin, end), Vector2.Max(origin, end));
var state = (input, filter, result, this, _physics);
_broadphase.GetBroadphases(mapId, aabb, ref state,
static (Entity<BroadphaseComponent> entity, ref (RayCastInput input, QueryFilter filter, RayResult result, RayCastSystem system, SharedPhysicsSystem _physics) tuple) =>
{
var transform = tuple._physics.GetPhysicsTransform(entity.Owner);
var localOrigin = Physics.Transform.InvTransformPoint(transform, tuple.input.Origin);
var localTranslation = Physics.Transform.InvTransformPoint(transform, tuple.input.Origin + tuple.input.Translation) - localOrigin;
var oldIndex = tuple.result.Results.Count;
tuple.system.CastRayClosest((entity.Owner, entity.Comp), ref tuple.result, localOrigin, localTranslation, filter: tuple.filter);
tuple.system.AdjustResults(ref tuple.result, oldIndex, transform);
});
result = state.result;
DebugTools.Assert(result.Results.Count <= 1);
return result;
}
#endregion
#region ShapeCast
/// <summary>
/// Convenience method for shape casts; only supports shapes with area.
/// </summary>
public RayResult CastShape(
MapId mapId,
IPhysShape shape,
Transform originTransform,
Vector2 translation,
QueryFilter filter,
CastResult callback)
{
DebugTools.Assert(originTransform.Position.IsValid());
DebugTools.Assert(originTransform.Quaternion2D.IsValid());
DebugTools.Assert(translation.IsValid());
// Need to get the entire shape AABB to know what broadphases to even query.
var startAabb = shape.ComputeAABB(originTransform, 0);
var endAabb = shape.ComputeAABB(new Transform(originTransform.Position + translation, originTransform.Quaternion2D.Angle), 0);
var aabb = startAabb.Union(endAabb);
var result = new RayResult();
var state = (originTransform, translation, shape: shape, filter, result, this, _physics, callback);
_broadphase.GetBroadphases(mapId, aabb, ref state,
static (
Entity<BroadphaseComponent> entity,
ref (Transform origin, Vector2 translation, IPhysShape shape, QueryFilter filter, RayResult result, RayCastSystem system, SharedPhysicsSystem _physics, CastResult callback
) tuple) =>
{
var transform = tuple._physics.GetPhysicsTransform(entity.Owner);
var localOrigin = Physics.Transform.MulT(transform, tuple.origin);
var localTranslation = Physics.Transform.InvTransformPoint(transform, tuple.origin.Position + tuple.translation) - localOrigin.Position;
var oldIndex = tuple.result.Results.Count;
tuple.system.CastShape((entity.Owner, entity.Comp), ref tuple.result, tuple.shape, localOrigin, localTranslation, filter: tuple.filter, callback: tuple.callback);
tuple.system.AdjustResults(ref tuple.result, oldIndex, transform);
});
result = state.result;
return result;
}
/// <summary>
/// Cast on the broadphase.
/// </summary>
public void CastShape(
Entity<BroadphaseComponent?> entity,
ref RayResult result,
IPhysShape shape,
Transform originTransform,
Vector2 translation,
QueryFilter filter,
CastResult callback)
{
if (!Resolve(entity.Owner, ref entity.Comp))
return;
switch (shape)
{
case PhysShapeCircle circle:
CastCircle(entity, ref result, circle, originTransform, translation, filter, callback);
break;
case Polygon poly:
CastPolygon(entity, ref result, new PolygonShape(poly), originTransform, translation, filter, callback);
break;
case PolygonShape polygon:
CastPolygon(entity, ref result, polygon, originTransform, translation, filter, callback);
break;
default:
Log.Error("Tried to shapecast for shape not implemented.");
DebugTools.Assert(false);
return;
}
}
public void CastCircle(
Entity<BroadphaseComponent?> entity,
ref RayResult result,
PhysShapeCircle circle,
Transform originTransform,
Vector2 translation,
QueryFilter filter,
CastResult callback)
{
if (!Resolve(entity.Owner, ref entity.Comp))
return;
var input = new ShapeCastInput()
{
Points = new Vector2[1],
Count = 1,
Radius = circle.Radius,
Translation = translation,
MaxFraction = 1f,
};
input.Points[0] = Physics.Transform.Mul(originTransform, circle.Position);
var worldContext = new WorldRayCastContext()
{
System = this,
Physics = _physics,
Filter = filter,
Fraction = 1f,
Result = result,
fcn = callback,
};
entity.Comp.StaticTree.Tree.ShapeCast(input, filter.MaskBits, ShapeCastCallback, ref worldContext);
input.MaxFraction = worldContext.Fraction;
entity.Comp.DynamicTree.Tree.ShapeCast(input, filter.MaskBits, ShapeCastCallback, ref worldContext);
result = worldContext.Result;
}
public void CastPolygon(
Entity<BroadphaseComponent?> entity,
ref RayResult result,
PolygonShape polygon,
Transform originTransform,
Vector2 translation,
QueryFilter filter,
CastResult callback)
{
if (!Resolve(entity.Owner, ref entity.Comp))
return;
ShapeCastInput input = new()
{
Points = new Vector2[polygon.VertexCount],
};
for ( int i = 0; i < polygon.VertexCount; ++i )
{
input.Points[i] = Physics.Transform.Mul(originTransform, polygon.Vertices[i]);
}
input.Count = polygon.VertexCount;
input.Radius = polygon.Radius;
input.Translation = translation;
input.MaxFraction = 1.0f;
var worldContext = new WorldRayCastContext()
{
System = this,
Physics = _physics,
Filter = filter,
Fraction = 1f,
Result = result,
fcn = callback,
};
if ((filter.Flags & QueryFlags.Static) == QueryFlags.Static)
{
entity.Comp.StaticTree.Tree.ShapeCast(input, filter.MaskBits, ShapeCastCallback, ref worldContext);
input.MaxFraction = worldContext.Fraction;
}
if ((filter.Flags & QueryFlags.Dynamic) == QueryFlags.Dynamic)
{
entity.Comp.DynamicTree.Tree.ShapeCast(input, filter.MaskBits, ShapeCastCallback, ref worldContext);
}
result = worldContext.Result;
}
#endregion
}
/// Result from b2World_RayCastClosest
/// @ingroup world
public record struct RayResult()
{
public ValueList<RayHit> Results = new();
public bool Hit => Results.Count > 0;
public static readonly RayResult Empty = new();
}
public record struct RayHit(EntityUid Entity, Vector2 LocalNormal, float Fraction)
{
public readonly EntityUid Entity = Entity;
public readonly Vector2 LocalNormal = LocalNormal;
public readonly float Fraction = Fraction;
// When this point gets added it's in broadphase terms, then the caller handles whether it gets turned into map-terms.
public Vector2 Point;
}
/// The query filter is used to filter collisions between queries and shapes. For example,
/// you may want a ray-cast representing a projectile to hit players and the static environment
/// but not debris.
/// @ingroup shape
public record struct QueryFilter()
{
/// <summary>
/// The collision category bits of this query. Normally you would just set one bit.
/// </summary>
public long LayerBits;
/// <summary>
/// The collision mask bits. This states the shape categories that this
/// query would accept for collision.
/// </summary>
public long MaskBits;
/// <summary>
/// Return whether to ignore an entity.
/// </summary>
public Func<EntityUid, bool>? IsIgnored;
public QueryFlags Flags = QueryFlags.Dynamic | QueryFlags.Static;
}
/// <summary>
/// Which trees we wish to query.
/// </summary>
[Flags]
public enum QueryFlags : byte
{
None = 0,
Dynamic = 1 << 0,
Static = 1 << 1,
Sensors = 1 << 2,
// StaticSundries = 1 << 3,
// Sundries = 1 << 4,
}
/// Prototype callback for ray casts.
/// Called for each shape found in the query. You control how the ray cast
/// proceeds by returning a float:
/// return -1: ignore this shape and continue
/// return 0: terminate the ray cast
/// return fraction: clip the ray to this point
/// return 1: don't clip the ray and continue
/// @param shapeId the shape hit by the ray
/// @param point the point of initial intersection
/// @param normal the normal vector at the point of intersection
/// @param fraction the fraction along the ray at the point of intersection
/// @param context the user context
/// @return -1 to filter, 0 to terminate, fraction to clip the ray for closest hit, 1 to continue
/// @see b2World_CastRay
/// @ingroup world
public delegate float CastResult(FixtureProxy proxy, Vector2 point, Vector2 normal, float fraction, ref RayResult result);

View File

@@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -472,54 +471,38 @@ namespace Robust.Shared.Physics.Systems
TouchProxies(xform.MapUid.Value, matrix, fixture);
}
internal void GetBroadphases(MapId mapId, Box2 aabb,BroadphaseCallback callback)
// TODO: The below is slow and should just query the map's broadphase directly. The problem is that
// there's some ordering stuff going on where the broadphase has queued all of its updates but hasn't applied
// them yet so this query will fail on initialization which chains into a whole lot of issues.
internal IEnumerable<(EntityUid uid, BroadphaseComponent comp)> GetBroadphases(MapId mapId, Box2 aabb)
{
var internalState = (callback, _broadphaseQuery);
// TODO Okay so problem: If we just do Encloses that's a lot faster BUT it also means we don't return the
// map's broadphase which avoids us iterating over it for 99% of bodies.
_mapManager.FindGridsIntersecting(mapId,
aabb,
ref internalState,
static (
EntityUid uid,
MapGridComponent grid,
ref (BroadphaseCallback callback, EntityQuery<BroadphaseComponent> _broadphaseQuery) tuple) =>
if (mapId == MapId.Nullspace) yield break;
var enumerator = AllEntityQuery<BroadphaseComponent, TransformComponent>();
while (enumerator.MoveNext(out var bUid, out var broadphase, out var xform))
{
if (xform.MapID != mapId) continue;
if (!EntityManager.TryGetComponent(bUid, out MapGridComponent? mapGrid))
{
if (!tuple._broadphaseQuery.TryComp(uid, out var broadphase))
return true;
yield return (bUid, broadphase);
continue;
}
tuple.callback((uid, broadphase));
return true;
// Approx because we don't really need accurate checks for these most of the time.
}, approx: true, includeMap: true);
}
// Won't worry about accurate bounds checks as it's probably slower in most use cases.
var chunkEnumerator = _map.GetMapChunks(bUid, mapGrid, aabb);
internal void GetBroadphases<TState>(MapId mapId, Box2 aabb, ref TState state, BroadphaseCallback<TState> callback)
{
var internalState = (state, callback, _broadphaseQuery);
_mapManager.FindGridsIntersecting(mapId,
aabb,
ref internalState,
static (
EntityUid uid,
MapGridComponent grid,
ref (TState state, BroadphaseCallback<TState> callback, EntityQuery<BroadphaseComponent> _broadphaseQuery) tuple) =>
if (chunkEnumerator.MoveNext(out _))
{
if (!tuple._broadphaseQuery.TryComp(uid, out var broadphase))
return true;
tuple.callback((uid, broadphase), ref tuple.state);
return true;
// Approx because we don't really need accurate checks for these most of the time.
}, approx: true, includeMap: true);
state = internalState.state;
yield return (bUid, broadphase);
}
}
}
internal delegate void BroadphaseCallback(Entity<BroadphaseComponent> entity);
internal delegate void BroadphaseCallback<TState>(Entity<BroadphaseComponent> entity, ref TState state);
private record struct BroadphaseContactJob() : IParallelRobustJob
{
public SharedBroadphaseSystem System = default!;

View File

@@ -23,9 +23,7 @@
*/
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
@@ -71,97 +69,39 @@ public partial class SharedPhysicsSystem
private void OnPhysicsGetState(EntityUid uid, PhysicsComponent component, ref ComponentGetState args)
{
if (args.FromTick > component.CreationTick && component.LastFieldUpdate >= args.FromTick)
{
var slowPath = false;
for (var i = 0; i < _angularVelocityIndex; i++)
{
var field = component.LastModifiedFields[i];
if (field < args.FromTick)
continue;
slowPath = true;
break;
}
// We can do a smaller delta with no list index overhead.
if (!slowPath)
{
var angularDirty = component.LastModifiedFields[_angularVelocityIndex] >= args.FromTick;
if (angularDirty)
{
args.State = new PhysicsVelocityDeltaState()
{
AngularVelocity = component.AngularVelocity,
LinearVelocity = component.LinearVelocity,
};
}
else
{
args.State = new PhysicsLinearVelocityDeltaState()
{
LinearVelocity = component.LinearVelocity,
};
}
return;
}
}
args.State = new PhysicsComponentState
{
CanCollide = component.CanCollide,
SleepingAllowed = component.SleepingAllowed,
FixedRotation = component.FixedRotation,
Status = component.BodyStatus,
LinearVelocity = component.LinearVelocity,
AngularVelocity = component.AngularVelocity,
BodyType = component.BodyType,
Friction = component._friction,
LinearDamping = component.LinearDamping,
AngularDamping = component.AngularDamping,
Force = component.Force,
Torque = component.Torque,
};
args.State = new PhysicsComponentState(
component.CanCollide,
component.SleepingAllowed,
component.FixedRotation,
component.BodyStatus,
component.LinearVelocity,
component.AngularVelocity,
component.BodyType,
component._friction,
component.LinearDamping,
component.AngularDamping);
}
private void OnPhysicsHandleState(EntityUid uid, PhysicsComponent component, ref ComponentHandleState args)
{
if (args.Current == null)
if (args.Current is not PhysicsComponentState newState)
return;
SetSleepingAllowed(uid, component, newState.SleepingAllowed);
SetFixedRotation(uid, newState.FixedRotation, body: component);
SetCanCollide(uid, newState.CanCollide, body: component);
component.BodyStatus = newState.Status;
// So transform doesn't apply MapId in the HandleComponentState because ??? so MapId can still be 0.
// Fucking kill me, please. You have no idea deep the rabbit hole of shitcode goes to make this work.
_fixturesQuery.TryComp(uid, out var manager);
TryComp<FixturesComponent>(uid, out var manager);
if (args.Current is PhysicsLinearVelocityDeltaState linearState)
{
SetLinearVelocity(uid, linearState.LinearVelocity, body: component, manager: manager);
}
else if (args.Current is PhysicsVelocityDeltaState velocityState)
{
SetLinearVelocity(uid, velocityState.LinearVelocity, body: component, manager: manager);
SetAngularVelocity(uid, velocityState.AngularVelocity, body: component, manager: manager);
}
else if (args.Current is PhysicsComponentState newState)
{
SetSleepingAllowed(uid, component, newState.SleepingAllowed);
SetFixedRotation(uid, newState.FixedRotation, body: component);
SetCanCollide(uid, newState.CanCollide, body: component);
component.BodyStatus = newState.Status;
SetLinearVelocity(uid, newState.LinearVelocity, body: component, manager: manager);
SetAngularVelocity(uid, newState.AngularVelocity, body: component, manager: manager);
SetBodyType(uid, newState.BodyType, manager, component);
SetFriction(uid, component, newState.Friction);
SetLinearDamping(uid, component, newState.LinearDamping);
SetAngularDamping(uid, component, newState.AngularDamping);
component.Force = newState.Force;
component.Torque = newState.Torque;
}
SetLinearVelocity(uid, newState.LinearVelocity, body: component, manager: manager);
SetAngularVelocity(uid, newState.AngularVelocity, body: component, manager: manager);
SetBodyType(uid, newState.BodyType, manager, component);
SetFriction(uid, component, newState.Friction);
SetLinearDamping(uid, component, newState.LinearDamping);
SetAngularDamping(uid, component, newState.AngularDamping);
}
#endregion
@@ -192,6 +132,7 @@ public partial class SharedPhysicsSystem
body.Force += force;
body.Torque += Vector2Helpers.Cross(point - body._localCenter, force);
Dirty(uid, body);
}
public void ApplyForce(EntityUid uid, Vector2 force, FixturesComponent? manager = null, PhysicsComponent? body = null)
@@ -202,6 +143,7 @@ public partial class SharedPhysicsSystem
}
body.Force += force;
Dirty(uid, body);
}
public void ApplyTorque(EntityUid uid, float torque, FixturesComponent? manager = null, PhysicsComponent? body = null)
@@ -212,7 +154,7 @@ public partial class SharedPhysicsSystem
}
body.Torque += torque;
DirtyField(uid, body, nameof(PhysicsComponent.Torque));
Dirty(uid, body);
}
public void ApplyLinearImpulse(EntityUid uid, Vector2 impulse, FixturesComponent? manager = null, PhysicsComponent? body = null)
@@ -270,29 +212,34 @@ public partial class SharedPhysicsSystem
/// </summary>
public void ResetDynamics(EntityUid uid, PhysicsComponent body, bool dirty = true)
{
var updated = false;
if (body.Torque != 0f)
{
body.Torque = 0f;
DirtyField(uid, body, nameof(PhysicsComponent.Torque));
updated = true;
}
if (body.AngularVelocity != 0f)
{
body.AngularVelocity = 0f;
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
updated = true;
}
if (body.Force != Vector2.Zero)
{
body.Force = Vector2.Zero;
DirtyField(uid, body, nameof(PhysicsComponent.Force));
updated = true;
}
if (body.LinearVelocity != Vector2.Zero)
{
body.LinearVelocity = Vector2.Zero;
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
updated = true;
}
if (updated && dirty)
Dirty(uid, body);
}
public void ResetMassData(EntityUid uid, FixturesComponent? manager = null, PhysicsComponent? body = null)
@@ -328,6 +275,7 @@ public partial class SharedPhysicsSystem
if (((int) body.BodyType & (int) (BodyType.Kinematic | BodyType.Static)) != 0)
{
body._localCenter = Vector2.Zero;
Dirty(uid, body);
return;
}
@@ -361,13 +309,8 @@ public partial class SharedPhysicsSystem
body._localCenter = localCenter;
// Update center of mass velocity.
var comVelocityDiff = Vector2Helpers.Cross(body.AngularVelocity, localCenter - oldCenter);
if (comVelocityDiff != Vector2.Zero)
{
body.LinearVelocity += comVelocityDiff;
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
}
body.LinearVelocity += Vector2Helpers.Cross(body.AngularVelocity, localCenter - oldCenter);
Dirty(uid, body);
if (body._mass == oldMass && body._inertia == oldInertia && oldCenter == localCenter)
return;
@@ -397,7 +340,9 @@ public partial class SharedPhysicsSystem
return false;
body.AngularVelocity = value;
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
if (dirty)
Dirty(uid, body);
return true;
}
@@ -423,7 +368,10 @@ public partial class SharedPhysicsSystem
return false;
body.LinearVelocity = velocity;
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
if (dirty)
Dirty(uid, body);
return true;
}
@@ -433,7 +381,9 @@ public partial class SharedPhysicsSystem
return;
body.AngularDamping = value;
DirtyField(uid, body, nameof(PhysicsComponent.AngularDamping));
if (dirty)
Dirty(uid, body);
}
public void SetLinearDamping(EntityUid uid, PhysicsComponent body, float value, bool dirty = true)
@@ -442,7 +392,9 @@ public partial class SharedPhysicsSystem
return;
body.LinearDamping = value;
DirtyField(uid, body, nameof(PhysicsComponent.LinearDamping));
if (dirty)
Dirty(uid, body);
}
[Obsolete("Use SetAwake with EntityUid<PhysicsComponent>")]
@@ -494,6 +446,7 @@ public partial class SharedPhysicsSystem
}
UpdateMapAwakeState(uid, body);
Dirty(ent);
}
public void TrySetBodyType(EntityUid uid, BodyType value, FixturesComponent? manager = null, PhysicsComponent? body = null, TransformComponent? xform = null)
@@ -521,18 +474,8 @@ public partial class SharedPhysicsSystem
if (body.BodyType == BodyType.Static)
{
SetAwake((uid, body), false);
if (body.LinearVelocity != Vector2.Zero)
{
body.LinearVelocity = Vector2.Zero;
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
}
if (body.AngularVelocity != 0f)
{
body.AngularVelocity = 0f;
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
}
body.LinearVelocity = Vector2.Zero;
body.AngularVelocity = 0.0f;
}
// Even if it's dynamic if it can't collide then don't force it awake.
else if (body.CanCollide)
@@ -540,11 +483,8 @@ public partial class SharedPhysicsSystem
SetAwake((uid, body), true);
}
if (body.Torque != 0f)
{
body.Torque = 0f;
DirtyField(uid, body, nameof(PhysicsComponent.Torque));
}
body.Force = Vector2.Zero;
body.Torque = 0.0f;
_broadphase.RegenerateContacts(uid, body, manager, xform);
@@ -561,7 +501,9 @@ public partial class SharedPhysicsSystem
return;
body.BodyStatus = status;
DirtyField(uid, body, nameof(PhysicsComponent.BodyStatus));
if (dirty)
Dirty(uid, body);
}
/// <summary>
@@ -612,7 +554,10 @@ public partial class SharedPhysicsSystem
var ev = new CollisionChangeEvent(uid, body, value);
RaiseLocalEvent(ref ev);
}
DirtyField(uid, body, nameof(PhysicsComponent.CanCollide));
if (dirty)
Dirty(uid, body);
return value;
}
@@ -622,15 +567,11 @@ public partial class SharedPhysicsSystem
return;
body.FixedRotation = value;
DirtyField(uid, body, nameof(PhysicsComponent.FixedRotation));
if (body.AngularVelocity != 0f)
{
body.AngularVelocity = 0.0f;
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
}
body.AngularVelocity = 0.0f;
ResetMassData(uid, manager: manager, body: body);
if (dirty)
Dirty(uid, body);
}
public void SetFriction(EntityUid uid, PhysicsComponent body, float value, bool dirty = true)
@@ -639,7 +580,9 @@ public partial class SharedPhysicsSystem
return;
body._friction = value;
DirtyField(uid, body, nameof(PhysicsComponent.Friction));
if (dirty)
Dirty(uid, body);
}
public void SetInertia(EntityUid uid, PhysicsComponent body, float value, bool dirty = true)
@@ -655,7 +598,9 @@ public partial class SharedPhysicsSystem
body._inertia = value - body.Mass * Vector2.Dot(body._localCenter, body._localCenter);
DebugTools.Assert(body._inertia > 0.0f);
body.InvI = 1.0f / body._inertia;
// Not networked
if (dirty)
Dirty(uid, body);
}
}
@@ -666,7 +611,6 @@ public partial class SharedPhysicsSystem
if (value.EqualsApprox(body._localCenter)) return;
body._localCenter = value;
// Not networked
}
public void SetSleepingAllowed(EntityUid uid, PhysicsComponent body, bool value, bool dirty = true)
@@ -678,7 +622,9 @@ public partial class SharedPhysicsSystem
SetAwake((uid, body), true);
body.SleepingAllowed = value;
DirtyField(uid, body, nameof(PhysicsComponent.SleepingAllowed));
if (dirty)
Dirty(uid, body);
}
public void SetSleepTime(PhysicsComponent body, float value)

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.Collections;
using Robust.Shared.Debugging;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -35,48 +34,37 @@ namespace Robust.Shared.Physics.Systems
public bool TryCollideRect(Box2 collider, MapId mapId, bool approximate = true)
{
var state = (collider, mapId, found: false);
var broadphases = new ValueList<Entity<BroadphaseComponent>>();
_broadphase.GetBroadphases(mapId,
collider,
broadphase =>
foreach (var (uid, broadphase) in _broadphase.GetBroadphases(mapId, collider))
{
var gridCollider = _transform.GetInvWorldMatrix(uid).TransformBox(collider);
broadphase.StaticTree.QueryAabb(ref state, (ref (Box2 collider, MapId map, bool found) state, in FixtureProxy proxy) =>
{
var gridCollider = _transform.GetInvWorldMatrix(broadphase).TransformBox(collider);
if (proxy.Fixture.CollisionLayer == 0x0)
return true;
broadphase.Comp.StaticTree.QueryAabb(ref state,
(ref (Box2 collider, MapId map, bool found) state, in FixtureProxy proxy) =>
{
if (proxy.Fixture.CollisionLayer == 0x0)
return true;
if (proxy.AABB.Intersects(gridCollider))
{
state.found = true;
return false;
}
return true;
}, gridCollider, approximate);
if (proxy.AABB.Intersects(gridCollider))
{
state.found = true;
return false;
}
broadphase.DynamicTree.QueryAabb(ref state, (ref (Box2 collider, MapId map, bool found) state, in FixtureProxy proxy) =>
{
if (proxy.Fixture.CollisionLayer == 0x0)
return true;
return true;
},
gridCollider,
approximate);
broadphase.Comp.DynamicTree.QueryAabb(ref state,
(ref (Box2 collider, MapId map, bool found) state, in FixtureProxy proxy) =>
{
if (proxy.Fixture.CollisionLayer == 0x0)
return true;
if (proxy.AABB.Intersects(gridCollider))
{
state.found = true;
return false;
}
return true;
},
gridCollider,
approximate);
});
if (proxy.AABB.Intersects(gridCollider))
{
state.found = true;
return false;
}
return true;
}, gridCollider, approximate);
}
return state.found;
}
@@ -142,27 +130,22 @@ namespace Robust.Shared.Physics.Systems
{
if (mapId == MapId.Nullspace) return Array.Empty<PhysicsComponent>();
var aabb = worldAABB;
var bodies = new HashSet<PhysicsComponent>();
var state = (_transform, bodies, aabb);
_broadphase.GetBroadphases(mapId, worldAABB, ref state, static
(
Entity<BroadphaseComponent> entity,
ref (SharedTransformSystem _transform, HashSet<PhysicsComponent> bodies, Box2 aabb) tuple) =>
foreach (var (uid, broadphase) in _broadphase.GetBroadphases(mapId, worldAABB))
{
var gridAABB = _transform.GetInvWorldMatrix(uid).TransformBox(worldAABB);
foreach (var proxy in broadphase.StaticTree.QueryAabb(gridAABB, false))
{
var gridAABB = tuple._transform.GetInvWorldMatrix(entity.Owner).TransformBox(tuple.aabb);
bodies.Add(proxy.Body);
}
foreach (var proxy in entity.Comp.StaticTree.QueryAabb(gridAABB, false))
{
tuple.bodies.Add(proxy.Body);
}
foreach (var proxy in entity.Comp.DynamicTree.QueryAabb(gridAABB, false))
{
tuple.bodies.Add(proxy.Body);
}
});
foreach (var proxy in broadphase.DynamicTree.QueryAabb(gridAABB, false))
{
bodies.Add(proxy.Body);
}
}
return bodies;
}
@@ -177,27 +160,20 @@ namespace Robust.Shared.Physics.Systems
var bodies = new HashSet<Entity<PhysicsComponent>>();
var state = (_transform, bodies, worldBounds);
foreach (var (uid, broadphase) in _broadphase.GetBroadphases(mapId, worldBounds.CalcBoundingBox()))
{
var gridAABB = _transform.GetInvWorldMatrix(uid).TransformBox(worldBounds);
_broadphase.GetBroadphases(mapId, worldBounds.CalcBoundingBox(), ref state,
static (
Entity<BroadphaseComponent> entity,
ref (SharedTransformSystem _transform, HashSet<Entity<PhysicsComponent>> bodies, Box2Rotated
worldBounds
) tuple) =>
foreach (var proxy in broadphase.StaticTree.QueryAabb(gridAABB, false))
{
var gridAABB = tuple._transform.GetInvWorldMatrix(entity.Owner).TransformBox(tuple.worldBounds);
bodies.Add(new Entity<PhysicsComponent>(proxy.Entity, proxy.Body));
}
foreach (var proxy in entity.Comp.StaticTree.QueryAabb(gridAABB, false))
{
tuple.bodies.Add((proxy.Entity, proxy.Body));
}
foreach (var proxy in entity.Comp.DynamicTree.QueryAabb(gridAABB, false))
{
tuple.bodies.Add((proxy.Entity, proxy.Body));
}
});
foreach (var proxy in broadphase.DynamicTree.QueryAabb(gridAABB, false))
{
bodies.Add(new Entity<PhysicsComponent>(proxy.Entity, proxy.Body));
}
}
return bodies;
}
@@ -287,91 +263,72 @@ namespace Robust.Shared.Physics.Systems
var rayBox = new Box2(Vector2.Min(ray.Position, endPoint),
Vector2.Max(ray.Position, endPoint));
_broadphase.GetBroadphases(mapId,
rayBox,
broadphase =>
foreach (var (uid, broadphase) in _broadphase.GetBroadphases(mapId, rayBox))
{
var (_, rot, matrix, invMatrix) = _transform.GetWorldPositionRotationMatrixWithInv(uid);
var position = Vector2.Transform(ray.Position, invMatrix);
var gridRot = new Angle(-rot.Theta);
var direction = gridRot.RotateVec(ray.Direction);
var gridRay = new CollisionRay(position, direction, ray.CollisionMask);
broadphase.StaticTree.QueryRay((in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
var (_, rot, matrix, invMatrix) =
_transform.GetWorldPositionRotationMatrixWithInv(broadphase.Owner);
if (returnOnFirstHit && results.Count > 0)
return true;
var position = Vector2.Transform(ray.Position, invMatrix);
var gridRot = new Angle(-rot.Theta);
var direction = gridRot.RotateVec(ray.Direction);
if (distFromOrigin > maxLength)
return true;
var gridRay = new CollisionRay(position, direction, ray.CollisionMask);
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
broadphase.Comp.StaticTree.QueryRay(
(in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (returnOnFirstHit && results.Count > 0)
return true;
if (!proxy.Fixture.Hard)
return true;
if (distFromOrigin > maxLength)
return true;
if (predicate.Invoke(proxy.Entity, state) == true)
return true;
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
// TODO: Shape raycast here
if (!proxy.Fixture.Hard)
return true;
if (predicate.Invoke(proxy.Entity, state) == true)
return true;
// TODO: Shape raycast here
// Need to convert it back to world-space.
var result = new RayCastResults(distFromOrigin,
Vector2.Transform(point, matrix),
proxy.Entity);
results.Add(result);
// Need to convert it back to world-space.
var result = new RayCastResults(distFromOrigin, Vector2.Transform(point, matrix), proxy.Entity);
results.Add(result);
#if DEBUG
_sharedDebugRaySystem.ReceiveLocalRayFromAnyThread(new(ray,
maxLength,
result,
_netMan.IsServer,
mapId));
_sharedDebugRaySystem.ReceiveLocalRayFromAnyThread(new(ray, maxLength, result, _netMan.IsServer, mapId));
#endif
return true;
},
gridRay);
return true;
}, gridRay);
broadphase.Comp.DynamicTree.QueryRay(
(in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (returnOnFirstHit && results.Count > 0)
return true;
broadphase.DynamicTree.QueryRay((in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (returnOnFirstHit && results.Count > 0)
return true;
if (distFromOrigin > maxLength)
return true;
if (distFromOrigin > maxLength)
return true;
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
if (!proxy.Fixture.Hard)
return true;
if (!proxy.Fixture.Hard)
return true;
if (predicate.Invoke(proxy.Entity, state) == true)
return true;
if (predicate.Invoke(proxy.Entity, state) == true)
return true;
// TODO: Shape raycast here
// TODO: Shape raycast here
// Need to convert it back to world-space.
var result = new RayCastResults(distFromOrigin,
Vector2.Transform(point, matrix),
proxy.Entity);
results.Add(result);
// Need to convert it back to world-space.
var result = new RayCastResults(distFromOrigin, Vector2.Transform(point, matrix), proxy.Entity);
results.Add(result);
#if DEBUG
_sharedDebugRaySystem.ReceiveLocalRayFromAnyThread(new(ray,
maxLength,
result,
_netMan.IsServer,
mapId));
_sharedDebugRaySystem.ReceiveLocalRayFromAnyThread(new(ray, maxLength, result, _netMan.IsServer, mapId));
#endif
return true;
},
gridRay);
});
return true;
}, gridRay);
}
#if DEBUG
if (results.Count == 0)
@@ -417,68 +374,54 @@ namespace Robust.Shared.Physics.Systems
var rayBox = new Box2(Vector2.Min(ray.Position, endPoint),
Vector2.Max(ray.Position, endPoint));
_broadphase.GetBroadphases(mapId,
rayBox,
broadphase =>
foreach (var (uid, broadphase) in _broadphase.GetBroadphases(mapId, rayBox))
{
var (_, rot, invMatrix) = _transform.GetWorldPositionRotationInvMatrix(uid);
var position = Vector2.Transform(ray.Position, invMatrix);
var gridRot = new Angle(-rot.Theta);
var direction = gridRot.RotateVec(ray.Direction);
var gridRay = new CollisionRay(position, direction, ray.CollisionMask);
broadphase.StaticTree.QueryRay((in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
var (_, rot, invMatrix) = _transform.GetWorldPositionRotationInvMatrix(broadphase);
if (distFromOrigin > maxLength || proxy.Entity == ignoredEnt)
return true;
var position = Vector2.Transform(ray.Position, invMatrix);
var gridRot = new Angle(-rot.Theta);
var direction = gridRot.RotateVec(ray.Direction);
if (!proxy.Fixture.Hard)
return true;
var gridRay = new CollisionRay(position, direction, ray.CollisionMask);
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
broadphase.Comp.StaticTree.QueryRay(
(in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (distFromOrigin > maxLength || proxy.Entity == ignoredEnt)
return true;
if (new Ray(point + gridRay.Direction * proxy.AABB.Size.Length() * 2, -gridRay.Direction).Intersects(
proxy.AABB, out _, out var exitPoint))
{
penetration += (point - exitPoint).Length();
}
return true;
}, gridRay);
if (!proxy.Fixture.Hard)
return true;
broadphase.DynamicTree.QueryRay((in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (distFromOrigin > maxLength || proxy.Entity == ignoredEnt)
return true;
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
if (!proxy.Fixture.Hard)
return true;
if (new Ray(point + gridRay.Direction * proxy.AABB.Size.Length() * 2, -gridRay.Direction)
.Intersects(
proxy.AABB,
out _,
out var exitPoint))
{
penetration += (point - exitPoint).Length();
}
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
return true;
},
gridRay);
broadphase.Comp.DynamicTree.QueryRay(
(in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (distFromOrigin > maxLength || proxy.Entity == ignoredEnt)
return true;
if (!proxy.Fixture.Hard)
return true;
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
if (new Ray(point + gridRay.Direction * proxy.AABB.Size.Length() * 2, -gridRay.Direction)
.Intersects(
proxy.AABB,
out _,
out var exitPoint))
{
penetration += (point - exitPoint).Length();
}
return true;
},
gridRay);
});
if (new Ray(point + gridRay.Direction * proxy.AABB.Size.Length() * 2, -gridRay.Direction).Intersects(
proxy.AABB, out _, out var exitPoint))
{
penetration += (point - exitPoint).Length();
}
return true;
}, gridRay);
}
// This hid rays that didn't penetrate something. Don't hide those because that causes rays to disappear that shouldn't.
#if DEBUG

View File

@@ -74,32 +74,10 @@ namespace Robust.Shared.Physics.Systems
protected EntityQuery<PhysicsMapComponent> PhysMapQuery;
protected EntityQuery<MapComponent> MapQuery;
private ComponentRegistration _physicsReg = default!;
private byte _angularVelocityIndex;
public override void Initialize()
{
base.Initialize();
_physicsReg = EntityManager.ComponentFactory.GetRegistration(CompIdx.Index<PhysicsComponent>());
// If you update this then update the delta state + GetState + HandleState!
EntityManager.ComponentFactory.RegisterNetworkedFields(_physicsReg,
nameof(PhysicsComponent.CanCollide),
nameof(PhysicsComponent.BodyStatus),
nameof(PhysicsComponent.BodyType),
nameof(PhysicsComponent.SleepingAllowed),
nameof(PhysicsComponent.FixedRotation),
nameof(PhysicsComponent.Friction),
nameof(PhysicsComponent.Force),
nameof(PhysicsComponent.Torque),
nameof(PhysicsComponent.LinearDamping),
nameof(PhysicsComponent.AngularDamping),
nameof(PhysicsComponent.AngularVelocity),
nameof(PhysicsComponent.LinearVelocity));
_angularVelocityIndex = 10;
_fixturesQuery = GetEntityQuery<FixturesComponent>();
PhysicsQuery = GetEntityQuery<PhysicsComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();

View File

@@ -38,12 +38,6 @@ namespace Robust.Shared.Physics
public Vector2 Position;
public Quaternion2D Quaternion2D;
public Transform(Vector2 position, Quaternion2D quat)
{
Position = position;
Quaternion2D = quat;
}
public Transform(Vector2 position, float angle)
{
Position = position;
@@ -62,16 +56,6 @@ namespace Robust.Shared.Physics
Quaternion2D = new Quaternion2D(angle);
}
/// Inverse transform a point (e.g. world space to local space)
[Pure]
public static Vector2 InvTransformPoint(Transform t, Vector2 p)
{
float vx = p.X - t.Position.X;
float vy = p.Y - t.Position.Y;
return new Vector2(t.Quaternion2D.C * vx + t.Quaternion2D.S * vy, -t.Quaternion2D.S * vx + t.Quaternion2D.C * vy);
}
[Pure]
public static Vector2 Mul(in Transform transform, in Vector2 vector)
{
float x = (transform.Quaternion2D.C * vector.X - transform.Quaternion2D.S * vector.Y) + transform.Position.X;
@@ -80,14 +64,12 @@ namespace Robust.Shared.Physics
return new Vector2(x, y);
}
[Pure]
public static Vector2 MulT(in Vector2[] A, in Vector2 v)
{
DebugTools.Assert(A.Length == 2);
return new Vector2(v.X * A[0].X + v.Y * A[0].Y, v.X * A[1].X + v.Y * A[1].Y);
}
[Pure]
public static Vector2 MulT(in Transform T, in Vector2 v)
{
float px = v.X - T.Position.X;
@@ -99,7 +81,6 @@ namespace Robust.Shared.Physics
}
/// Transpose multiply two rotations: qT * r
[Pure]
public static Quaternion2D MulT(in Quaternion2D q, in Quaternion2D r)
{
// [ qc qs] * [rc -rs] = [qc*rc+qs*rs -qc*rs+qs*rc]
@@ -112,15 +93,8 @@ namespace Robust.Shared.Physics
return qr;
}
[Pure]
public static Transform InvMulTransforms(in Transform A, in Transform B)
{
return new Transform(Quaternion2D.InvRotateVector(A.Quaternion2D, Vector2.Subtract(B.Position, A.Position)), Quaternion2D.InvMulRot(A.Quaternion2D, B.Quaternion2D));
}
// v2 = A.q' * (B.q * v1 + B.p - A.p)
// = A.q' * B.q * v1 + A.q' * (B.p - A.p)
[Pure]
public static Transform MulT(in Transform A, in Transform B)
{
Transform C = new Transform
@@ -210,51 +184,5 @@ namespace Robust.Shared.Physics
// TODO_ERIN optimize
return new Quaternion2D(MathF.Cos(angle), MathF.Sin(angle));
}
/// Rotate a vector
[Pure]
public static Vector2 RotateVector(Quaternion2D q, Vector2 v )
{
return new Vector2(q.C * v.X - q.S * v.Y, q.S * v.X + q.C * v.Y);
}
/// Inverse rotate a vector
[Pure]
public static Vector2 InvRotateVector(Quaternion2D q, Vector2 v)
{
return new Vector2(q.C * v.X + q.S * v.Y, -q.S * v.X + q.C * v.Y);
}
public bool IsValid()
{
if (float.IsNaN(S ) || float.IsNaN(C))
{
return false;
}
if (float.IsInfinity(S) || float.IsInfinity(C))
{
return false;
}
return IsNormalized();
}
public bool IsNormalized()
{
// larger tolerance due to failure on mingw 32-bit
float qq = S * S + C * C;
return 1.0f - 0.0006f < qq && qq < 1.0f + 0.0006f;
}
[Pure]
public static Quaternion2D InvMulRot(Quaternion2D q, Quaternion2D r)
{
// [ qc qs] * [rc -rs] = [qc*rc+qs*rs -qc*rs+qs*rc]
// [-qs qc] [rs rc] [-qs*rc+qc*rs qs*rs+qc*rc]
// s(q - r) = qc * rs - qs * rc
// c(q - r) = qc * rc + qs * rs
return new Quaternion2D(q.C * r.C + q.S * r.S, q.C * r.S - q.S * r.C);
}
}
}

View File

@@ -15,9 +15,6 @@ namespace Robust.Shared.Prototypes;
[Virtual]
public class PrototypeAttribute : Attribute
{
/// <summary>
/// Override for the name of this kind of prototype. If not specified, this is automatically inferred via <see cref="PrototypeManager.CalculatePrototypeName"/>
/// </summary>
public string? Type { get; internal set; }
public readonly int LoadPriority = 1;
@@ -26,7 +23,7 @@ public class PrototypeAttribute : Attribute
Type = type;
LoadPriority = loadPriority;
}
public PrototypeAttribute(int loadPriority)
{
Type = null;

View File

@@ -824,10 +824,22 @@ namespace Robust.Shared.Prototypes
public bool TryGetKindFrom(Type type, [NotNullWhen(true)] out string? kind)
{
kind = null;
if (!_kinds.TryGetValue(type, out var kindData))
// If the type doesn't implement IPrototype, this fails.
if (!(typeof(IPrototype).IsAssignableFrom(type)))
return false;
kind = kindData.Name;
var attribute = (PrototypeAttribute?)Attribute.GetCustomAttribute(type, typeof(PrototypeAttribute));
// If the prototype type doesn't have the attribute, this fails.
if (attribute == null)
return false;
// If the variant isn't registered, this fails.
if (attribute.Type == null || !HasKind(attribute.Type))
return false;
kind = attribute.Type;
return true;
}
@@ -933,13 +945,13 @@ namespace Robust.Shared.Prototypes
"No " + nameof(PrototypeAttribute) + " to give it a type string.");
}
var name = attribute.Type ?? CalculatePrototypeName(kind);
attribute.Type ??= CalculatePrototypeName(kind);
if (_kindNames.TryGetValue(name, out var existing))
if (_kindNames.TryGetValue(attribute.Type, out var name))
{
throw new InvalidImplementationException(kind,
typeof(IPrototype),
$"Duplicate prototype type ID: {attribute.Type}. Current: {existing}");
$"Duplicate prototype type ID: {attribute.Type}. Current: {name}");
}
var foundIdAttribute = false;
@@ -1007,10 +1019,10 @@ namespace Robust.Shared.Prototypes
$"Did not find any member annotated with the {nameof(ParentDataFieldAttribute)} and/or {nameof(AbstractDataFieldAttribute)}");
}
_kindNames[name] = kind;
_kindNames[attribute.Type] = kind;
_kindPriorities[kind] = attribute.LoadPriority;
var kindData = new KindData(kind, name);
var kindData = new KindData(kind);
kinds[kind] = kindData;
if (kind.IsAssignableTo(typeof(IInheritingPrototype)))
@@ -1020,7 +1032,7 @@ namespace Robust.Shared.Prototypes
/// <inheritdoc />
public event Action<PrototypesReloadedEventArgs>? PrototypesReloaded;
private sealed class KindData(Type kind, string name)
private sealed class KindData(Type kind)
{
public Dictionary<string, IPrototype>? UnfrozenInstances;
@@ -1029,7 +1041,6 @@ namespace Robust.Shared.Prototypes
public readonly Dictionary<string, MappingDataNode> Results = new();
public readonly Type Type = kind;
public readonly string Name = name;
// Only initialized if prototype is inheriting.
public MultiRootInheritanceGraph<string>? Inheritance;

View File

@@ -104,7 +104,7 @@ internal static class RsiLoading
};
}
return new RsiMetadata(size, states, textureParams, manifestJson.MetaAtlas);
return new RsiMetadata(size, states, textureParams);
}
public static void Warmup()
@@ -114,12 +114,11 @@ internal static class RsiLoading
JsonSerializer.Deserialize<RsiJsonMetadata>(warmupJson, SerializerOptions);
}
internal sealed class RsiMetadata(Vector2i size, StateMetadata[] states, TextureLoadParameters loadParameters, bool metaAtlas)
internal sealed class RsiMetadata(Vector2i size, StateMetadata[] states, TextureLoadParameters loadParameters)
{
public readonly Vector2i Size = size;
public readonly StateMetadata[] States = states;
public readonly TextureLoadParameters LoadParameters = loadParameters;
public readonly bool MetaAtlas = metaAtlas;
}
internal sealed class StateMetadata
@@ -141,11 +140,7 @@ internal static class RsiLoading
// To be directly deserialized.
[UsedImplicitly]
private sealed record RsiJsonMetadata(
Vector2i Size,
StateJsonMetadata[] States,
RsiJsonLoad? Load,
bool MetaAtlas = true);
private sealed record RsiJsonMetadata(Vector2i Size, StateJsonMetadata[] States, RsiJsonLoad? Load);
[UsedImplicitly]
private sealed record StateJsonMetadata(string Name, int? Directions, float[][]? Delays);

View File

@@ -70,7 +70,7 @@ namespace Robust.Shared.Serialization.Manager
{
foreach (var child in _reflectionManager.GetAllChildren(type))
{
if (child.IsAbstract || child.IsGenericTypeDefinition || child.IsInterface)
if (child.IsAbstract || child.IsGenericTypeDefinition)
continue;
yield return child;

View File

@@ -11,6 +11,7 @@ namespace Robust.Shared.Toolshed.Commands.Entities;
internal sealed class WithCommand : ToolshedCommand
{
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[CommandImplementation]
public IEnumerable<EntityUid> With(

View File

@@ -35,8 +35,7 @@ public sealed class BoolTypeParser : TypeParser<bool>
result = true;
error = null;
return true;
}
else if (word == "false" || word == "f" || word == "0")
} else if (word == "false" || word == "f" || word == "0")
{
result = false;
error = null;
@@ -50,9 +49,9 @@ public sealed class BoolTypeParser : TypeParser<bool>
}
}
public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName)
public override async ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName)
{
return new ValueTask<(CompletionResult?, IConError?)>((CompletionResult.FromOptions(new[] { "true", "false" }), null));
return (CompletionResult.FromOptions(new[] {"true", "false"}), null);
}
}

View File

@@ -38,10 +38,10 @@ internal sealed class EntityTypeParser : TypeParser<EntityUid>
return true;
}
public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext,
public override async ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext,
string? argName)
{
return new ValueTask<(CompletionResult?, IConError?)>((CompletionResult.FromHint("<NetEntity>"), null));
return (CompletionResult.FromHint("<NetEntity>"), null);
}
}

View File

@@ -11,7 +11,7 @@ using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.TypeParsers;
public sealed class EnumTypeParser<T> : TypeParser<T>
where T : unmanaged, Enum
where T: unmanaged, Enum
{
public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result,
out IConError? error)
@@ -46,14 +46,14 @@ public sealed class EnumTypeParser<T> : TypeParser<T>
return true;
}
public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName)
public override async ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName)
{
return new ValueTask<(CompletionResult?, IConError?)>((CompletionResult.FromOptions(Enum.GetNames<T>()), null));
return (CompletionResult.FromOptions(Enum.GetNames<T>()), null);
}
}
public record InvalidEnum<T>(string Value) : IConError
where T : unmanaged, Enum
where T: unmanaged, Enum
{
public FormattedMessage DescribeInner()
{

View File

@@ -43,11 +43,11 @@ internal sealed class SessionTypeParser : TypeParser<ICommonSession>
return false;
}
public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext,
public override async ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext,
string? argName)
{
var opts = CompletionHelper.SessionNames(true, _player);
return new ValueTask<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(opts, "<player session>"), null));
return (CompletionResult.FromHintOptions(opts, "<player session>"), null);
}
public record InvalidUsername(ILocalizationManager Loc, string Username) : IConError

View File

@@ -49,18 +49,18 @@ namespace Robust.UnitTesting.Client.GameObjects.Components
var childTrans = entMan.GetComponent<TransformComponent>(child);
ComponentHandleState handleState;
var compState = new TransformComponentState(new Vector2(5, 5), new Angle(0), entMan.GetNetEntity(gridIdB), false, false);
var compState = new TransformComponentState(new Vector2(5, 5), new Angle(0), entMan.GetNetEntity(gridIdB), false, false, true);
handleState = new ComponentHandleState(compState, null);
xformSystem.OnHandleState(parent, parentTrans, ref handleState);
compState = new TransformComponentState(new Vector2(6, 6), new Angle(0), entMan.GetNetEntity(gridIdB), false, false);
compState = new TransformComponentState(new Vector2(6, 6), new Angle(0), entMan.GetNetEntity(gridIdB), false, false, true);
handleState = new ComponentHandleState(compState, null);
xformSystem.OnHandleState(child, childTrans, ref handleState);
// World pos should be 6, 6 now.
// Act
var oldWpos = xformSystem.GetWorldPosition(childTrans);
compState = new TransformComponentState(new Vector2(1, 1), new Angle(0), entMan.GetNetEntity(parent), false, false);
compState = new TransformComponentState(new Vector2(1, 1), new Angle(0), entMan.GetNetEntity(parent), false, false, true);
handleState = new ComponentHandleState(compState, null);
xformSystem.OnHandleState(child, childTrans, ref handleState);
var newWpos = xformSystem.GetWorldPosition(childTrans);
@@ -98,15 +98,15 @@ namespace Robust.UnitTesting.Client.GameObjects.Components
var node2Trans = entMan.GetComponent<TransformComponent>(node2);
var node3Trans = entMan.GetComponent<TransformComponent>(node3);
var compState = new TransformComponentState(new Vector2(6, 6), Angle.FromDegrees(135), entMan.GetNetEntity(gridIdB), false, false);
var compState = new TransformComponentState(new Vector2(6, 6), Angle.FromDegrees(135), entMan.GetNetEntity(gridIdB), false, false, true);
var handleState = new ComponentHandleState(compState, null);
xformSystem.OnHandleState(node1, node1Trans, ref handleState);
compState = new TransformComponentState(new Vector2(1, 1), Angle.FromDegrees(45), entMan.GetNetEntity(node1), false, false);
compState = new TransformComponentState(new Vector2(1, 1), Angle.FromDegrees(45), entMan.GetNetEntity(node1), false, false, true);
handleState = new ComponentHandleState(compState, null);
xformSystem.OnHandleState(node2, node2Trans, ref handleState);
compState = new TransformComponentState(new Vector2(0, 0), Angle.FromDegrees(45), entMan.GetNetEntity(node2), false, false);
compState = new TransformComponentState(new Vector2(0, 0), Angle.FromDegrees(45), entMan.GetNetEntity(node2), false, false, true);
handleState = new ComponentHandleState(compState, null);
xformSystem.OnHandleState(node3, node3Trans, ref handleState);

View File

@@ -243,12 +243,10 @@ namespace Robust.UnitTesting.Shared.IoC
{
[Dependency]
#pragma warning disable 649
#pragma warning disable RA0032
private readonly TestFieldInjection myself = default!;
[Dependency]
public TestFieldInjection myotherself = default!;
#pragma warning restore RA0032
#pragma warning restore 649
public virtual void Test()

View File

@@ -27,14 +27,6 @@ namespace Robust.UnitTesting.Shared.Maths
(0.92387953251128674f, -0.38268343236508978f, Direction.East, -System.Math.PI / 8.0)
};
[Test]
public void TestAngleDegrees()
{
const double degrees = 75d;
var angle = Angle.FromDegrees(degrees);
Assert.That(angle.Degrees, Is.EqualTo(degrees));
}
[Test]
public void TestAngleZero()
{

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