Compare commits

..

22 Commits

Author SHA1 Message Date
metalgearsloth
9c30fdf5fd Version: 237.3.0 2024-12-16 16:33:24 +11:00
metalgearsloth
0b7e8c2560 Add shapecasts + raycasts (#5440)
* Add shapecasts + raycasts

Actual raycasts. Need this for AI LIDAR experiment.

* cassette

* more cudin

* Mostly ported

* more work

* More ports

* the big house

* rays

* builds

* Janky not working raycasts

* Fix GJK

* Test fixes

* Shapecast + fixes

* free

* tests

* More fixes

* Minor changes

* Not these

* Release notes
2024-12-16 16:07:24 +11:00
IProduceWidgets
7982aa236c TryFindComponentOnEntityContainerOrParent no fail please (#5322)
* TryFindComponentsOnEntityContainerOrParent no fail please

* remove my comments because Im dumb

* out makes sense to me!

* Revert "out makes sense to me!" because PJB no want breaky

This reverts commit 54f4a6d50c.
2024-12-14 17:16:43 +01:00
PJB3005
0559339143 Add AssetPassFilterDrop
Planning to use this to drop .svg files from SS14's resources folder. Tiny opt.
2024-12-14 17:07:57 +01:00
Zachary Higgs
89fcd1dd2b Add InterfaceData constructor (#5559)
* Add InterfaceData constructor

Add InterfaceData constructor to allow for dynamic UI assignment via
SharedUiSystem::setUi

* empty commit because of heisentest
2024-12-14 16:19:05 +01:00
metalgearsloth
649378e59a BUI helpers (#5558)
* BUI helpers

- Some virtual methods for BUI to make it slightly easier. Haven't though of a good way to do it via sourcegen yet.
- TryGetUiState which is occasionally useful.

* Also this one
2024-12-13 01:33:17 +01:00
SpaceManiac
0c7ace16d1 Fix most non-obsolete warnings (#5555) 2024-12-13 01:25:00 +01:00
metalgearsloth
27f7f5ee36 Add Pure attribute to some entmanager methods (#5557) 2024-12-12 18:24:20 +01:00
Pieter-Jan Briers
fe0fcbd851 Add RSI key to disable meta-atlas (#5544)
This allows huge textures (e.g. Ratvar) to be removed from the meta-atlas. This saves a significant chunk of VRAM from the meta atlas in SS14 (~126 MB) which might help a bit with low-VRAM systems.
2024-12-08 23:49:12 +01:00
Pieter-Jan Briers
aca7847933 Add CVars for privacy policy information (#5545)
* Add CVars for privacy policy information

Engine side of https://github.com/space-wizards/SS14.Launcher/issues/194

* Improve/fix cvar desc
2024-12-08 23:48:50 +01:00
Leon Friedrich
1621d25a92 Fix UserInterfaceSystem debug assert (#5546) 2024-11-30 13:09:51 +01:00
Amy
b7e0a9bc03 Make font drawing more generic (#5533)
* make richtextentry more generic

* font

* oops
2024-11-29 11:28:27 +01:00
metalgearsloth
9909416006 Fix grid container layout (#5543)
This decrements index and cooks the layout if controls are invisible.
2024-11-29 10:54:34 +01:00
Leon Friedrich
c3e487b61c Fix IPrototypeManager.TryGetKindFrom() (#5542) 2024-11-29 01:48:02 +01:00
Nikolai Korolev
89ad8b6c9f Upgrade GitHub actions in workflows (#5536)
* Upgrade github workflows to node20

* Fix incorrect version for actions/checkout in test-content.yml
2024-11-28 19:52:14 +01:00
Nikolai Korolev
efbc9ef2bf Fix codeql-analysis breaking in every fork repo that enables GitHub Actions (#5537) 2024-11-28 19:50:39 +01:00
Pieter-Jan Briers
ce240773e8 ValueList<T> extensions (#5534)
Stack-like functions. Just some code I had lying around and never committed.

Add ROS overload for AddRange
2024-11-28 19:49:51 +01:00
SpaceManiac
8563466011 Fix wrong filename used when log.enabled is set (#5541) 2024-11-28 19:46:53 +01:00
Nikolai Korolev
af4d53fb54 No need for disabling RA0003 warning in FastNoise (#5535) 2024-11-25 00:37:05 +01:00
Pieter-Jan Briers
3086fc446c Sandbox error reference locator now works with generic method calls
This means resolving the MethodSpec table entry for it.
2024-11-22 18:06:33 +01:00
Nikolai Korolev
5f3a54376d Fix warnings for using async without any await (#5532)
* Fix warnings for using async without any await

* Fix async without await warning in EntityTypeParser

* Fix async without await in EnumTypeParser

* Update SessionTypeParser.cs

* Update EntityTypeParser.cs

* Update BoolTypeParser.cs

* Update EntityTypeParser.cs

* Update EnumTypeParser.cs

* Update SessionTypeParser.cs

* Fix compilation and formatting
2024-11-22 02:18:35 +01:00
Nikolai Korolev
9bb7af364e Fix warning for using non-generic variant of TryComp for MetaDataComponent and TransformComponent RA0030 (#5531)
* Fix warning for using non-generic variant of TryComp for MetaDataComponent RA0030 (Use non-generic variant)

* Use non-generic variant of TryComp for TransformComponent
2024-11-22 01:30:55 +01:00
79 changed files with 2734 additions and 511 deletions

View File

@@ -5,30 +5,30 @@ on:
- cron: "0 0 * * 0"
jobs:
docfx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
with:
submodules: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 8.0.x
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.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,33 +2,32 @@ 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@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
- uses: actions/checkout@v4.2.2
with:
submodules: true
- 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,14 +11,8 @@
#
name: "CodeQL"
#on:
# push:
# branches: [ master ]
# pull_request:
# # The branches below must be a subset of the branches above
# branches: [ master ]
# schedule:
# - cron: '30 18 * * 6'
on:
workflow_dispatch
jobs:
analyze:
@@ -28,50 +22,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@v3.6.0
with:
submodules: true
- name: Checkout repository
uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 7.0.x
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.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,51 +3,50 @@
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@v3.6.0
with:
submodules: true
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 8.0.x
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.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: 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: 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 }}

View File

@@ -2,40 +2,39 @@ 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@v3.6.0
with:
repository: space-wizards/space-station-14
submodules: recursive
- name: Check out content
uses: actions/checkout@v4.2.2
with:
repository: space-wizards/space-station-14
submodules: recursive
- 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: 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: 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.2.2" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<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,13 +54,21 @@ END TEMPLATE-->
*None yet*
## 237.2.3
## 237.3.0
### New features
## 237.2.2
* 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
## 237.2.1
* 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.0

View File

@@ -6,7 +6,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
internal static class Program
public 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,7 +5,6 @@ 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;
@@ -25,7 +24,6 @@ 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!;
@@ -63,10 +61,7 @@ namespace Robust.Client.WebView.Cef
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
var settings = new CefSettings()
{

View File

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

View File

@@ -5,7 +5,6 @@ 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;
@@ -241,7 +240,7 @@ namespace Robust.Client.GameObjects
#if DEBUG
var uid = GetEntity(netEntity);
if (TryComp<MetaDataComponent>(uid, out var meta))
if (TryComp(uid, out MetaDataComponent? 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

@@ -362,6 +362,11 @@ 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,6 +211,8 @@ 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 void DrawTexture(Texture texture, Vector2 position, Color? modulate = null)
public override 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 void DrawTexture(Texture texture, Vector2 position, Color? modulate = null)
public override 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(
DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale,
DrawingHandleBase 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(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
public override float DrawChar(DrawingHandleBase handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
{
var metrics = Handle.GetCharMetrics(rune, scale);
if (!metrics.HasValue)
@@ -132,7 +132,10 @@ namespace Robust.Client.Graphics
}
baseline += new Vector2(metrics.Value.BearingX, -metrics.Value.BearingY);
handle.DrawTexture(texture, baseline, color);
if(handle is DrawingHandleWorld worldhandle)
worldhandle.DrawTextureRect(texture, Box2.FromDimensions(baseline, texture.Size));
else
handle.DrawTexture(texture, baseline, color);
return metrics.Value.Advance;
}
@@ -169,7 +172,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(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
public override float DrawChar(DrawingHandleBase handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
{
foreach (var f in Stack)
{
@@ -207,7 +210,7 @@ namespace Robust.Client.Graphics
public override int GetDescent(float scale) => default;
public override int GetLineHeight(float scale) => default;
public override float DrawChar(DrawingHandleScreen handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
public override float DrawChar(DrawingHandleBase handle, Rune rune, Vector2 baseline, float scale, Color color, bool fallback=true)
{
// Nada, it's a dummy after all.
return 0;

View File

@@ -143,9 +143,9 @@ namespace Robust.Client.ResourceManagement
}
});
// 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();
var atlasLookup = rsiList.ToLookup(ShouldMetaAtlas);
var atlasList = atlasLookup[true].ToArray();
var nonAtlasList = atlasLookup[false].ToArray();
foreach (var data in nonAtlasList)
{
@@ -225,8 +225,9 @@ namespace Robust.Client.ResourceManagement
void FinalizeMetaAtlas(int toIndex, Image<Rgba32> sheet)
{
var atlas = Clyde.LoadTextureFromImage(sheet);
for (int i = finalized + 1; i <= toIndex; i++)
var fromIndex = finalized + 1;
var atlas = Clyde.LoadTextureFromImage(sheet, $"Meta atlas {fromIndex}-{toIndex}");
for (int i = fromIndex; i <= toIndex; i++)
{
var rsi = atlasList[i];
rsi.AtlasTexture = atlas;
@@ -282,7 +283,11 @@ 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,6 +183,7 @@ namespace Robust.Client.ResourceManagement
data.DimX = dimensionX;
data.CallbackOffsets = callbackOffsets;
data.LoadParameters = metadata.LoadParameters;
data.MetaAtlas = metadata.MetaAtlas;
}
internal static void LoadPostTexture(LoadStepData data)
@@ -386,6 +387,7 @@ namespace Robust.Client.ResourceManagement
public Vector2i AtlasOffset;
public RSI Rsi = default!;
public TextureLoadParameters LoadParameters;
public bool MetaAtlas;
}
internal struct StateReg

View File

@@ -1,8 +1,5 @@
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;
@@ -89,8 +86,6 @@ 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,7 +302,6 @@ 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,
DrawingHandleScreen handle,
DrawingHandleBase handle,
Font defaultFont,
UIBox2 drawBox,
float verticalOffset,

View File

@@ -0,0 +1,18 @@
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

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

View File

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

View File

@@ -103,6 +103,22 @@ 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

@@ -745,6 +745,21 @@ 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 Vector2(t.M11, t.M12).ToAngle();
return new Angle(Math.Atan2(t.M12, t.M11));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

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

View File

@@ -1,6 +1,7 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace Robust.Shared.Maths;
@@ -14,6 +15,34 @@ 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)
{
@@ -255,6 +284,12 @@ 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

@@ -628,6 +628,43 @@ 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,6 +5,7 @@ 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;
@@ -589,6 +590,12 @@ 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);
@@ -618,4 +625,72 @@ 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.Resolve(xform.ParentUid, ref foundComponent, false))
if (entityQuery.TryComp(xform.ParentUid, out foundComponent))
return true;
return TryFindComponentOnEntityContainerOrParent(xform.ParentUid, entityQuery, ref foundComponent);

View File

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

View File

@@ -39,7 +39,7 @@ internal sealed partial class AssemblyTypeChecker
{
if (instruction.TryGetEntityHandle(out var handle))
{
if (refs.Contains(handle))
if (refs.Overlaps(ExpandHandle(reader, handle)))
{
var type = GetTypeFromDefinition(reader, methodDef.GetDeclaringType());
_sawmill.Error(
@@ -56,6 +56,12 @@ 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);
@@ -92,6 +98,17 @@ 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

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

View File

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

View File

@@ -14,11 +14,7 @@ namespace Robust.Shared.ContentPack
/// The directory to use for user data.
/// If null, a virtual temporary file system is used instead.
/// </param>
/// <param name="hideUserDataDir">
/// If true, <see cref="IWritableDirProvider.RootDir"/> will be hidden on
/// <see cref="IResourceManager.UserData"/>.
/// </param>
void Initialize(string? userData, bool hideUserDataDir);
void Initialize(string? userData);
/// <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 or the path is protected (e.g. on the client).
/// Can be null if it's a virtual provider.
/// </summary>
string? RootDir { get; }

View File

@@ -93,23 +93,19 @@ namespace Robust.Shared.ContentPack
{
var sw = Stopwatch.StartNew();
Sawmill.Debug("LOADING modules");
var files = new Dictionary<string, (ResPath Path, MemoryStream data, string[] references)>();
var files = new Dictionary<string, (ResPath Path, string[] references)>();
// Find all modules we want to load.
foreach (var fullPath in paths)
{
using var asmFile = _res.ContentFileRead(fullPath);
var ms = new MemoryStream();
asmFile.CopyTo(ms);
ms.Position = 0;
var refData = GetAssemblyReferenceData(ms);
var refData = GetAssemblyReferenceData(asmFile);
if (refData == null)
continue;
var (asmRefs, asmName) = refData.Value;
if (!files.TryAdd(asmName, (fullPath, ms, asmRefs)))
if (!files.TryAdd(asmName, (fullPath, asmRefs)))
{
Sawmill.Error("Found multiple modules with the same assembly name " +
$"'{asmName}', A: {files[asmName].Path}, B: {fullPath}.");
@@ -126,10 +122,10 @@ namespace Robust.Shared.ContentPack
Parallel.ForEach(files, pair =>
{
var (name, (_, data, _)) = pair;
var (name, (path, _)) = pair;
data.Position = 0;
if (!typeChecker.CheckAssembly(data, resolver))
using var stream = _res.ContentFileRead(path);
if (!typeChecker.CheckAssembly(stream, resolver))
{
throw new TypeCheckFailedException($"Assembly {name} failed type checks.");
}
@@ -141,15 +137,14 @@ namespace Robust.Shared.ContentPack
var nodes = TopologicalSort.FromBeforeAfter(
files,
kv => kv.Key,
kv => kv.Value,
kv => kv.Value.Path,
_ => Array.Empty<string>(),
kv => kv.Value.references,
allowMissing: true); // missing refs would be non-content assemblies so allow that.
// Actually load them in the order they depend on each other.
foreach (var item in TopologicalSort.Sort(nodes))
foreach (var path in TopologicalSort.Sort(nodes))
{
var (path, memory, _) = item;
Sawmill.Debug($"Loading module: '{path}'");
try
{
@@ -161,9 +156,9 @@ namespace Robust.Shared.ContentPack
}
else
{
memory.Position = 0;
using var assemblyStream = _res.ContentFileRead(path);
using var symbolsStream = _res.ContentFileReadOrNull(path.WithExtension("pdb"));
LoadGameAssembly(memory, symbolsStream, skipVerify: true);
LoadGameAssembly(assemblyStream, symbolsStream, skipVerify: true);
}
}
catch (Exception e)
@@ -179,7 +174,7 @@ namespace Robust.Shared.ContentPack
private (string[] refs, string name)? GetAssemblyReferenceData(Stream stream)
{
using var reader = ModLoader.MakePEReader(stream, leaveOpen: true);
using var reader = ModLoader.MakePEReader(stream);
var metaReader = reader.GetMetadataReader();
var name = metaReader.GetString(metaReader.GetAssemblyDefinition().Name);

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
{
@@ -64,27 +63,5 @@ 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, bool hideRootDir)
public virtual void Initialize(string? userData)
{
Sawmill = _logManager.GetSawmill("res");
if (userData != null)
{
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
}
else
{
@@ -379,13 +379,7 @@ namespace Robust.Shared.ContentPack
{
if (root is DirLoader loader)
{
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);
yield return new ResPath(loader.GetPath(new ResPath(@"/")));
}
}
}

View File

@@ -17,10 +17,6 @@ WhitelistedNamespaces:
- Content
- OpenDreamShared
AllowedAssemblyPrefixes:
- OpenDream
- Content
# The type whitelist does NOT care about which assembly types come from.
# This is because types switch assembly all the time.
# Just look up stuff like StreamReader on https://apisof.net.

View File

@@ -10,22 +10,17 @@ namespace Robust.Shared.ContentPack
/// <inheritdoc />
internal sealed class WritableDirProvider : IWritableDirProvider
{
private readonly bool _hideRootDir;
/// <inheritdoc />
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>
/// <param name="hideRootDir">If true, <see cref="IWritableDirProvider.RootDir"/> is reported as null.</param>
public WritableDirProvider(DirectoryInfo rootDir, bool hideRootDir)
public WritableDirProvider(DirectoryInfo rootDir)
{
// FullName does not have a trailing separator, and we MUST have a separator.
RootDir = rootDir.FullName + Path.DirectorySeparatorChar.ToString();
_hideRootDir = hideRootDir;
}
#region File Access
@@ -124,7 +119,7 @@ namespace Robust.Shared.ContentPack
throw new FileNotFoundException();
var dirInfo = new DirectoryInfo(GetFullPath(path));
return new WritableDirProvider(dirInfo, _hideRootDir);
return new WritableDirProvider(dirInfo);
}
/// <inheritdoc />
@@ -185,7 +180,20 @@ namespace Robust.Shared.ContentPack
path = path.Clean();
return PathHelpers.SafeGetResourcePath(RootDir, path);
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));
}
}
}

View File

@@ -59,6 +59,27 @@ 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

@@ -69,6 +69,12 @@ 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

@@ -707,6 +707,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent<T>(EntityUid uid) where T : IComponent
{
var dict = _entTraitArray[CompIdx.ArrayIndex<T>()];
@@ -716,6 +717,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent<T>(EntityUid? uid) where T : IComponent
{
return uid.HasValue && HasComponent<T>(uid.Value);
@@ -723,6 +725,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent(EntityUid uid, Type type)
{
var dict = _entTraitDict[type];
@@ -731,6 +734,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent(EntityUid? uid, Type type)
{
if (!uid.HasValue)
@@ -744,6 +748,7 @@ 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))
@@ -754,6 +759,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure]
public bool HasComponent(EntityUid? uid, ushort netId, MetaDataComponent? meta = null)
{
if (!uid.HasValue)
@@ -821,6 +827,7 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T GetComponent<T>(EntityUid uid) where T : IComponent
{
@@ -837,6 +844,7 @@ 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];
@@ -850,6 +858,7 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
[Pure]
public IComponent GetComponent(EntityUid uid, Type type)
{
// ReSharper disable once InvertIf
@@ -866,12 +875,14 @@ 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];
@@ -1450,6 +1461,7 @@ 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()}");

View File

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

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<TransformComponent>(uid, out var xform) && xform.MapUid != null)
if (TryComp(uid, out TransformComponent? xform) && xform.MapUid != null)
{
RemoveGrid(uid, component, xform.MapUid.Value);
}

View File

@@ -275,6 +275,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
if (ent.Comp.States.TryGetValue(key, out var state))
{
bui.UpdateState(state);
bui.Update();
}
}
}
@@ -295,7 +296,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
DebugTools.Assert(!ent.Comp.Actors.ContainsKey(key));
}
DebugTools.AssertEqual(ent.Comp.ClientOpenInterfaces.Count, 0);
DebugTools.Assert(ent.Comp.ClientOpenInterfaces.Values.All(x => _queuedCloses.Contains(x)));
}
private void OnUserInterfaceGetState(Entity<UserInterfaceComponent> ent, ref ComponentGetState args)
@@ -428,6 +429,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
cBui.State = buiState;
cBui.UpdateState(buiState);
cBui.Update();
}
// If UI not open then open it
@@ -490,6 +492,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
{
boundUserInterface.State = buiState;
boundUserInterface.UpdateState(buiState);
boundUserInterface.Update();
}
#if EXCEPTION_TOLERANCE
}
@@ -682,6 +685,13 @@ 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();
}
Dirty(entity);
}
@@ -1027,6 +1037,18 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
Dirty(ent, ent.Comp);
}
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;
}
/// <summary>
/// Verify that the subscribed clients are still in range of the interface.
/// </summary>

View File

@@ -28,24 +28,17 @@ public sealed class BeforeEntityReadEvent
}
/// <summary>
/// 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.
/// This event is broadcast just before an entity gets serialized.
/// </summary>
public sealed class BeforeSaveEvent
public sealed class BeforeSaveEvent(EntityUid entity, EntityUid? map)
{
/// <summary>
/// The entity that is going to be saved. usually a map or grid.
/// </summary>
public EntityUid Entity;
public EntityUid Entity = entity;
/// <summary>
/// The map that the <see cref="Entity"/> is on.
/// </summary>
public EntityUid? Map;
public BeforeSaveEvent(EntityUid entity, EntityUid? map)
{
Entity = entity;
Map = map;
}
public EntityUid? Map = map;
}

View File

@@ -41,10 +41,8 @@ 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,6 +27,7 @@ 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
@@ -943,6 +944,217 @@ 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,6 +24,7 @@ public sealed class DynamicTreeBroadPhase : IBroadPhase
}
public int Count => _tree.NodeCount;
public B2DynamicTree<FixtureProxy> Tree => _tree;
public Box2 GetFatAabb(DynamicTree.Proxy proxy)
{

View File

@@ -45,6 +45,12 @@ 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.
@@ -143,6 +149,13 @@ 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>
@@ -306,6 +319,16 @@ 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)
@@ -329,6 +352,226 @@ 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,6 +27,7 @@ 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;
@@ -172,6 +173,13 @@ 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

@@ -10,6 +10,8 @@ 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 = Array.Empty<Vector2>();
Normals = Array.Empty<Vector2>();
Vertices = [];
Normals = [];
return;
}

View File

@@ -0,0 +1,736 @@
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

@@ -0,0 +1,465 @@
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,6 +4,7 @@ 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;
@@ -471,38 +472,54 @@ namespace Robust.Shared.Physics.Systems
TouchProxies(xform.MapUid.Value, matrix, fixture);
}
// 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)
internal void GetBroadphases(MapId mapId, Box2 aabb,BroadphaseCallback callback)
{
// 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.
var internalState = (callback, _broadphaseQuery);
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))
_mapManager.FindGridsIntersecting(mapId,
aabb,
ref internalState,
static (
EntityUid uid,
MapGridComponent grid,
ref (BroadphaseCallback callback, EntityQuery<BroadphaseComponent> _broadphaseQuery) tuple) =>
{
yield return (bUid, broadphase);
continue;
}
if (!tuple._broadphaseQuery.TryComp(uid, out var broadphase))
return true;
// Won't worry about accurate bounds checks as it's probably slower in most use cases.
var chunkEnumerator = _map.GetMapChunks(bUid, mapGrid, aabb);
if (chunkEnumerator.MoveNext(out _))
{
yield return (bUid, broadphase);
}
}
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);
}
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 (!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;
}
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

@@ -1,6 +1,7 @@
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;
@@ -34,37 +35,48 @@ 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>>();
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) =>
_broadphase.GetBroadphases(mapId,
collider,
broadphase =>
{
if (proxy.Fixture.CollisionLayer == 0x0)
return true;
var gridCollider = _transform.GetInvWorldMatrix(broadphase).TransformBox(collider);
if (proxy.AABB.Intersects(gridCollider))
{
state.found = true;
return false;
}
return true;
}, gridCollider, approximate);
broadphase.Comp.StaticTree.QueryAabb(ref state,
(ref (Box2 collider, MapId map, bool found) state, in FixtureProxy proxy) =>
{
if (proxy.Fixture.CollisionLayer == 0x0)
return true;
broadphase.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;
}
if (proxy.AABB.Intersects(gridCollider))
{
state.found = true;
return false;
}
return true;
}, gridCollider, approximate);
}
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);
});
return state.found;
}
@@ -130,22 +142,27 @@ 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);
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))
_broadphase.GetBroadphases(mapId, worldAABB, ref state, static
(
Entity<BroadphaseComponent> entity,
ref (SharedTransformSystem _transform, HashSet<PhysicsComponent> bodies, Box2 aabb) tuple) =>
{
bodies.Add(proxy.Body);
}
var gridAABB = tuple._transform.GetInvWorldMatrix(entity.Owner).TransformBox(tuple.aabb);
foreach (var proxy in broadphase.DynamicTree.QueryAabb(gridAABB, false))
{
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);
}
});
return bodies;
}
@@ -160,20 +177,27 @@ namespace Robust.Shared.Physics.Systems
var bodies = new HashSet<Entity<PhysicsComponent>>();
foreach (var (uid, broadphase) in _broadphase.GetBroadphases(mapId, worldBounds.CalcBoundingBox()))
{
var gridAABB = _transform.GetInvWorldMatrix(uid).TransformBox(worldBounds);
var state = (_transform, bodies, worldBounds);
foreach (var proxy in broadphase.StaticTree.QueryAabb(gridAABB, false))
_broadphase.GetBroadphases(mapId, worldBounds.CalcBoundingBox(), ref state,
static (
Entity<BroadphaseComponent> entity,
ref (SharedTransformSystem _transform, HashSet<Entity<PhysicsComponent>> bodies, Box2Rotated
worldBounds
) tuple) =>
{
bodies.Add(new Entity<PhysicsComponent>(proxy.Entity, proxy.Body));
}
var gridAABB = tuple._transform.GetInvWorldMatrix(entity.Owner).TransformBox(tuple.worldBounds);
foreach (var proxy in broadphase.DynamicTree.QueryAabb(gridAABB, false))
{
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));
}
});
return bodies;
}
@@ -263,72 +287,91 @@ namespace Robust.Shared.Physics.Systems
var rayBox = new Box2(Vector2.Min(ray.Position, endPoint),
Vector2.Max(ray.Position, endPoint));
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) =>
_broadphase.GetBroadphases(mapId,
rayBox,
broadphase =>
{
if (returnOnFirstHit && results.Count > 0)
return true;
var (_, rot, matrix, invMatrix) =
_transform.GetWorldPositionRotationMatrixWithInv(broadphase.Owner);
if (distFromOrigin > maxLength)
return true;
var position = Vector2.Transform(ray.Position, invMatrix);
var gridRot = new Angle(-rot.Theta);
var direction = gridRot.RotateVec(ray.Direction);
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
var gridRay = new CollisionRay(position, direction, ray.CollisionMask);
if (!proxy.Fixture.Hard)
return true;
broadphase.Comp.StaticTree.QueryRay(
(in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (returnOnFirstHit && results.Count > 0)
return true;
if (predicate.Invoke(proxy.Entity, state) == true)
return true;
if (distFromOrigin > maxLength)
return true;
// TODO: Shape raycast here
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
// Need to convert it back to world-space.
var result = new RayCastResults(distFromOrigin, Vector2.Transform(point, matrix), proxy.Entity);
results.Add(result);
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);
#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.DynamicTree.QueryRay((in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (returnOnFirstHit && results.Count > 0)
return true;
broadphase.Comp.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)
@@ -374,54 +417,68 @@ namespace Robust.Shared.Physics.Systems
var rayBox = new Box2(Vector2.Min(ray.Position, endPoint),
Vector2.Max(ray.Position, endPoint));
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) =>
_broadphase.GetBroadphases(mapId,
rayBox,
broadphase =>
{
if (distFromOrigin > maxLength || proxy.Entity == ignoredEnt)
return true;
var (_, rot, invMatrix) = _transform.GetWorldPositionRotationInvMatrix(broadphase);
if (!proxy.Fixture.Hard)
return true;
var position = Vector2.Transform(ray.Position, invMatrix);
var gridRot = new Angle(-rot.Theta);
var direction = gridRot.RotateVec(ray.Direction);
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
var gridRay = new CollisionRay(position, direction, ray.CollisionMask);
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);
broadphase.Comp.StaticTree.QueryRay(
(in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (distFromOrigin > maxLength || proxy.Entity == ignoredEnt)
return true;
broadphase.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.Hard)
return true;
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
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();
}
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);
}
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);
});
// 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

@@ -38,6 +38,12 @@ 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;
@@ -56,6 +62,16 @@ 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;
@@ -64,12 +80,14 @@ 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;
@@ -81,6 +99,7 @@ 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]
@@ -93,8 +112,15 @@ 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
@@ -184,5 +210,51 @@ 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

@@ -9,6 +9,7 @@
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
[assembly: InternalsVisibleTo("OpenToolkit.GraphicsLibraryFramework")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Gives access to Castle(Moq)
[assembly: InternalsVisibleTo("Content.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Benchmarks")]
[assembly: InternalsVisibleTo("Robust.Client.WebView")]
[assembly: InternalsVisibleTo("Robust.Packaging")]

View File

@@ -15,6 +15,9 @@ 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;
@@ -23,7 +26,7 @@ public class PrototypeAttribute : Attribute
Type = type;
LoadPriority = loadPriority;
}
public PrototypeAttribute(int loadPriority)
{
Type = null;

View File

@@ -824,22 +824,10 @@ namespace Robust.Shared.Prototypes
public bool TryGetKindFrom(Type type, [NotNullWhen(true)] out string? kind)
{
kind = null;
// If the type doesn't implement IPrototype, this fails.
if (!(typeof(IPrototype).IsAssignableFrom(type)))
if (!_kinds.TryGetValue(type, out var kindData))
return false;
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;
kind = kindData.Name;
return true;
}
@@ -945,13 +933,13 @@ namespace Robust.Shared.Prototypes
"No " + nameof(PrototypeAttribute) + " to give it a type string.");
}
attribute.Type ??= CalculatePrototypeName(kind);
var name = attribute.Type ?? CalculatePrototypeName(kind);
if (_kindNames.TryGetValue(attribute.Type, out var name))
if (_kindNames.TryGetValue(name, out var existing))
{
throw new InvalidImplementationException(kind,
typeof(IPrototype),
$"Duplicate prototype type ID: {attribute.Type}. Current: {name}");
$"Duplicate prototype type ID: {attribute.Type}. Current: {existing}");
}
var foundIdAttribute = false;
@@ -1019,10 +1007,10 @@ namespace Robust.Shared.Prototypes
$"Did not find any member annotated with the {nameof(ParentDataFieldAttribute)} and/or {nameof(AbstractDataFieldAttribute)}");
}
_kindNames[attribute.Type] = kind;
_kindNames[name] = kind;
_kindPriorities[kind] = attribute.LoadPriority;
var kindData = new KindData(kind);
var kindData = new KindData(kind, name);
kinds[kind] = kindData;
if (kind.IsAssignableTo(typeof(IInheritingPrototype)))
@@ -1032,7 +1020,7 @@ namespace Robust.Shared.Prototypes
/// <inheritdoc />
public event Action<PrototypesReloadedEventArgs>? PrototypesReloaded;
private sealed class KindData(Type kind)
private sealed class KindData(Type kind, string name)
{
public Dictionary<string, IPrototype>? UnfrozenInstances;
@@ -1041,6 +1029,7 @@ 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);
return new RsiMetadata(size, states, textureParams, manifestJson.MetaAtlas);
}
public static void Warmup()
@@ -114,11 +114,12 @@ internal static class RsiLoading
JsonSerializer.Deserialize<RsiJsonMetadata>(warmupJson, SerializerOptions);
}
internal sealed class RsiMetadata(Vector2i size, StateMetadata[] states, TextureLoadParameters loadParameters)
internal sealed class RsiMetadata(Vector2i size, StateMetadata[] states, TextureLoadParameters loadParameters, bool metaAtlas)
{
public readonly Vector2i Size = size;
public readonly StateMetadata[] States = states;
public readonly TextureLoadParameters LoadParameters = loadParameters;
public readonly bool MetaAtlas = metaAtlas;
}
internal sealed class StateMetadata
@@ -140,7 +141,11 @@ internal static class RsiLoading
// To be directly deserialized.
[UsedImplicitly]
private sealed record RsiJsonMetadata(Vector2i Size, StateJsonMetadata[] States, RsiJsonLoad? Load);
private sealed record RsiJsonMetadata(
Vector2i Size,
StateJsonMetadata[] States,
RsiJsonLoad? Load,
bool MetaAtlas = true);
[UsedImplicitly]
private sealed record StateJsonMetadata(string Name, int? Directions, float[][]? Delays);

View File

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

View File

@@ -38,10 +38,10 @@ internal sealed class EntityTypeParser : TypeParser<EntityUid>
return true;
}
public override async ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext,
public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext,
string? argName)
{
return (CompletionResult.FromHint("<NetEntity>"), null);
return new ValueTask<(CompletionResult?, IConError?)>((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 async ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName)
public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName)
{
return (CompletionResult.FromOptions(Enum.GetNames<T>()), null);
return new ValueTask<(CompletionResult?, IConError?)>((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 async ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext,
public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext,
string? argName)
{
var opts = CompletionHelper.SessionNames(true, _player);
return (CompletionResult.FromHintOptions(opts, "<player session>"), null);
return new ValueTask<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(opts, "<player session>"), null));
}
public record InvalidUsername(ILocalizationManager Loc, string Username) : IConError

View File

@@ -243,10 +243,12 @@ 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,6 +27,14 @@ 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()
{

View File

@@ -12,18 +12,17 @@ namespace Robust.UnitTesting.Shared.Maths
[TestOf(typeof(Matrix3x2))]
public sealed class Matrix3_Test
{
[Test]
public void GetRotationTest()
private static readonly TestCaseData[] Rotations = new TestCaseData[]
{
Assert.That(Matrix3x2.Identity.Rotation(), Is.EqualTo(Angle.Zero));
new(Matrix3x2.Identity, Angle.Zero),
new(Matrix3x2.CreateRotation(MathF.PI / 2f), new Angle(Math.PI / 2)),
new(Matrix3x2.CreateRotation(MathF.PI), new Angle(Math.PI)),
};
var piOver2 = new Angle(Math.PI / 2);
var piOver2Mat = Matrix3Helpers.CreateRotation(piOver2.Theta);
Assert.That(piOver2Mat.Rotation(), Is.EqualTo(piOver2));
var pi = new Angle(Math.PI);
var piMat = Matrix3Helpers.CreateRotation(pi.Theta);
Assert.That(piMat.Rotation(), Is.EqualTo(pi));
[Test, TestCaseSource(nameof(Rotations))]
public void GetRotationTest(Matrix3x2 matrix, Angle angle)
{
Assert.That(angle, Is.EqualTo(matrix.Rotation()));
}
[Test]
@@ -35,7 +34,7 @@ namespace Robust.UnitTesting.Shared.Maths
var origin = new Vector2(0, 0);
var result = Vector2.Transform(origin, matrix);
Assert.That(control == result, Is.True, result.ToString);
Assert.That(control, Is.EqualTo(result), result.ToString);
}
private static readonly IEnumerable<(Vector2, double)> _rotationTests = new[]

View File

@@ -0,0 +1,145 @@
using System.Linq;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Shapes;
using Robust.Shared.Physics.Systems;
using Robust.UnitTesting.Server;
namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
public sealed class RayCast_Test
{
private static TestCaseData[] _rayCases =
{
// Ray goes through
new(new Vector2(0f, 0.5f), Vector2.UnitY * 2f, new Vector2(0f, 1f - PhysicsConstants.PolygonRadius)),
// Ray stops inside
new(new Vector2(0f, 0.5f), Vector2.UnitY, new Vector2(0f, 1f - PhysicsConstants.PolygonRadius)),
// Ray starts inside
new(new Vector2(0f, 1.5f), Vector2.UnitY, null),
// No hit
new(new Vector2(0f, 0.5f), -Vector2.UnitY, null),
};
private static TestCaseData[] _shapeCases =
{
// Circle
// - Initial overlap, no shapecast
new(new PhysShapeCircle(0.5f, Vector2.Zero), new Transform(Vector2.UnitY / 2f, Angle.Zero), Vector2.UnitY, null),
// - Cast
new(new PhysShapeCircle(0.5f, Vector2.Zero), new Transform(Vector2.Zero, Angle.Zero), Vector2.UnitY, new Vector2(0f, 1f - PhysicsConstants.PolygonRadius)),
// - Miss
new(new PhysShapeCircle(0.5f, Vector2.Zero), new Transform(Vector2.Zero, Angle.Zero), -Vector2.UnitY, null),
// Polygon
// - Initial overlap, no shapecast
new(new Polygon(Box2.UnitCentered), new Transform(Vector2.UnitY / 2f, Angle.Zero), Vector2.UnitY, null),
// - Cast
new(new Polygon(Box2.UnitCentered), new Transform(Vector2.Zero, Angle.Zero), Vector2.UnitY, new Vector2(0.5f, 1f - PhysicsConstants.PolygonRadius)),
// - Miss
new(new Polygon(Box2.UnitCentered), new Transform(Vector2.Zero, Angle.Zero), -Vector2.UnitY, null),
};
[Test, TestCaseSource(nameof(_rayCases))]
public void RayCast(Vector2 origin, Vector2 direction, Vector2? point)
{
var sim = RobustServerSimulation.NewSimulation().RegisterEntitySystems(f =>
{
f.LoadExtraSystemType<RayCastSystem>();
}).InitializeInstance();
Setup(sim, out var mapId);
var raycast = sim.System<RayCastSystem>();
var hits = raycast.CastRayClosest(mapId,
origin,
direction,
new QueryFilter()
{
LayerBits = 1,
});
if (point == null)
{
Assert.That(!hits.Hit);
}
else
{
Assert.That(hits.Results.First().Point, Is.EqualTo(point.Value));
}
}
[Test, TestCaseSource(nameof(_shapeCases))]
public void ShapeCast(IPhysShape shape, Transform origin, Vector2 direction, Vector2? point)
{
var sim = RobustServerSimulation.NewSimulation().RegisterEntitySystems(f =>
{
f.LoadExtraSystemType<RayCastSystem>();
}).InitializeInstance();
Setup(sim, out var mapId);
var raycast = sim.System<RayCastSystem>();
var hits = raycast.CastShape(mapId,
shape,
origin,
direction,
new QueryFilter()
{
LayerBits = 1,
},
RayCastSystem.RayCastAllCallback);
if (point == null)
{
Assert.That(!hits.Hit);
}
else
{
Assert.That(hits.Results.First().Point, Is.EqualTo(point.Value));
}
}
private void Setup(ISimulation sim, out MapId mapId)
{
var entManager = sim.Resolve<IEntityManager>();
var mapSystem = entManager.System<SharedMapSystem>();
sim.System<SharedMapSystem>().CreateMap(out mapId);
var grid = sim.Resolve<IMapManager>().CreateGridEntity(mapId);
for (var i = 0; i < 3; i++)
{
mapSystem.SetTile(grid, new Vector2i(i, 0), new Tile(1));
}
// Spawn a wall in the middle tile.
var wall = entManager.SpawnEntity(null, new EntityCoordinates(grid.Owner, new Vector2(1.5f, 0.5f)));
var physics = entManager.AddComponent<PhysicsComponent>(wall);
var poly = new PolygonShape();
poly.SetAsBox(Box2.UnitCentered);
entManager.System<FixtureSystem>().CreateFixture(wall, "fix1", new Fixture(poly, 1, 1, true));
entManager.System<SharedPhysicsSystem>().SetCanCollide(wall, true, body: physics);
Assert.That(physics.CanCollide);
// Rotate it to be vertical
entManager.System<SharedTransformSystem>().SetLocalRotation(grid.Owner, Angle.FromDegrees(90));
entManager.System<SharedTransformSystem>().SetLocalPosition(grid.Owner, Vector2.UnitX / 2f);
}
}

View File

@@ -24,7 +24,7 @@ namespace Robust.UnitTesting.Shared.Resources
_testDir = Directory.CreateDirectory(_testDirPath);
var subDir = Path.Combine(_testDirPath, "writable");
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir), hideRootDir: false);
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir));
}
[OneTimeTearDown]

View File

@@ -17,14 +17,18 @@ public sealed partial class CommunitaryLungTest : SerializationTest
Assert.That(def.Dict == copy.Dict, Is.False);
// Sanity check
#pragma warning disable CS1718 // Comparison made to same variable
Assert.That(def.Dict == def.Dict, Is.True);
#pragma warning restore CS1718 // Comparison made to same variable
Serialization.CopyTo(def, ref copy, notNullableOverride: true);
Assert.That(def.Dict == copy.Dict, Is.False);
// Sanity check
#pragma warning disable CS1718 // Comparison made to same variable
Assert.That(def.Dict == def.Dict, Is.True);
#pragma warning restore CS1718 // Comparison made to same variable
}
[Test]
@@ -38,7 +42,9 @@ public sealed partial class CommunitaryLungTest : SerializationTest
Assert.That(copy.Dict, Is.Not.Null);
// Sanity check
#pragma warning disable CS1718 // Comparison made to same variable
Assert.That(def.Dict == def.Dict, Is.True);
#pragma warning restore CS1718 // Comparison made to same variable
Serialization.CopyTo(def, ref copy, notNullableOverride: true);
@@ -46,7 +52,9 @@ public sealed partial class CommunitaryLungTest : SerializationTest
Assert.That(copy.Dict, Is.Not.Null);
// Sanity check
#pragma warning disable CS1718 // Comparison made to same variable
Assert.That(def.Dict == def.Dict, Is.True);
#pragma warning restore CS1718 // Comparison made to same variable
}
[Test]

View File

@@ -1,4 +1,5 @@
using System.Globalization;
using System;
using System.Globalization;
using NUnit.Framework;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager;
@@ -11,24 +12,46 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers
[TestOf(typeof(AngleSerializer))]
public sealed class AngleSerializerTest : SerializationTest
{
[Test]
public void SerializationTest()
private static readonly TestCaseData[] _source = new[]
{
var degrees = 75d;
var angle = Angle.FromDegrees(degrees);
new TestCaseData(Math.PI),
new TestCaseData(Math.PI / 2),
new TestCaseData(Math.PI / 4),
new TestCaseData(0.515),
new TestCaseData(75),
};
[Test, TestCaseSource(nameof(_source))]
public void SerializationRadsTest(double radians)
{
var angle = new Angle(radians);
var node = Serialization.WriteValueAs<ValueDataNode>(angle);
var serializedValue = $"{MathHelper.DegreesToRadians(degrees).ToString(CultureInfo.InvariantCulture)} rad";
var serializedValue = $"{radians.ToString(CultureInfo.InvariantCulture)} rad";
Assert.That(node.Value, Is.EqualTo(serializedValue));
}
[Test]
public void DeserializationTest()
[Test, TestCaseSource(nameof(_source))]
public void DeserializationRadsTest(double radians)
{
var degrees = 75;
var node = new ValueDataNode(degrees.ToString());
var angle = new Angle(radians);
var node = new ValueDataNode($"{radians.ToString(CultureInfo.InvariantCulture)} rad");
var deserializedAngle = Serialization.Read<Angle>(node);
Assert.That(deserializedAngle, Is.EqualTo(angle));
}
/*
* Serialization of degrees test won't work because it's comparing degrees to radians.
*/
[Test, TestCaseSource(nameof(_source))]
public void DeserializationDegreesTest(double radians)
{
var degrees = MathHelper.RadiansToDegrees(radians);
var angle = Angle.FromDegrees(degrees);
var node = new ValueDataNode($"{degrees.ToString(CultureInfo.InvariantCulture)}");
var deserializedAngle = Serialization.Read<Angle>(node);
Assert.That(deserializedAngle, Is.EqualTo(angle));
}