mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acb1d37b99 | ||
|
|
82c94fc8b0 | ||
|
|
9837c33de7 | ||
|
|
ac30ad1820 | ||
|
|
9c30fdf5fd | ||
|
|
0b7e8c2560 | ||
|
|
7982aa236c | ||
|
|
0559339143 | ||
|
|
89fcd1dd2b | ||
|
|
649378e59a | ||
|
|
0c7ace16d1 | ||
|
|
27f7f5ee36 | ||
|
|
fe0fcbd851 | ||
|
|
aca7847933 | ||
|
|
1621d25a92 | ||
|
|
b7e0a9bc03 | ||
|
|
9909416006 | ||
|
|
c3e487b61c | ||
|
|
89ad8b6c9f | ||
|
|
efbc9ef2bf | ||
|
|
ce240773e8 | ||
|
|
8563466011 | ||
|
|
af4d53fb54 | ||
|
|
3086fc446c | ||
|
|
5f3a54376d | ||
|
|
9bb7af364e |
44
.github/workflows/build-docfx.yml
vendored
44
.github/workflows/build-docfx.yml
vendored
@@ -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 }}
|
||||
|
||||
37
.github/workflows/build-test.yml
vendored
37
.github/workflows/build-test.yml
vendored
@@ -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
|
||||
|
||||
78
.github/workflows/codeql-analysis.yml
vendored
78
.github/workflows/codeql-analysis.yml
vendored
@@ -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
|
||||
|
||||
73
.github/workflows/publish-client.yml
vendored
73
.github/workflows/publish-client.yml
vendored
@@ -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 }}
|
||||
|
||||
55
.github/workflows/test-content.yml
vendored
55
.github/workflows/test-content.yml
vendored
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -54,10 +54,32 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 237.2.2
|
||||
## 237.4.0
|
||||
|
||||
### New features
|
||||
|
||||
* Implement automatic field-level delta states via AutoGenerateComponentState via opt-in.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Remove redundant TransformComponentState bool.
|
||||
|
||||
|
||||
## 237.2.1
|
||||
## 237.3.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added stack-like functions to `ValueList<T>` and added an `AddRange(ReadOnlySpan<T>)` overload.
|
||||
* Added new `AssetPassFilterDrop`.
|
||||
* Added a new RayCastSystem with the latest Box2D raycast + shapecasts implemented.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed `IPrototypeManager.TryGetKindFrom()` not working for prototypes with automatically inferred kind names.
|
||||
|
||||
### Other
|
||||
|
||||
* Sandbox error reference locator now works with generic method calls.
|
||||
|
||||
|
||||
## 237.2.0
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -504,7 +504,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
var (entity, component) = CreateAndStartPlayingStream(audioParams, stream);
|
||||
component.Global = true;
|
||||
component.Source.Global = true;
|
||||
Dirty(entity, component);
|
||||
DirtyField(entity, component, nameof(AudioComponent.Global));
|
||||
return (entity, component);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace Robust.Client.GameStates
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IClientGameStateManager _gameStateManager = default!;
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
[Dependency] private readonly IConsoleHost _host = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
private const int HistorySize = 60 * 5; // number of ticks to keep in history.
|
||||
@@ -78,7 +79,7 @@ namespace Robust.Client.GameStates
|
||||
|
||||
string? entStateString = null;
|
||||
string? entDelString = null;
|
||||
var conShell = IoCManager.Resolve<IConsoleHost>().LocalShell;
|
||||
var conShell = _host.LocalShell;
|
||||
|
||||
var entStates = args.AppliedState.EntityStates;
|
||||
if (entStates.HasContents)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,8 +13,9 @@ namespace Robust.Client.Physics
|
||||
internal sealed class GridFixtureSystem : SharedGridFixtureSystem
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlay = default!;
|
||||
[Dependency] private readonly IMapManager _map = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedMapSystem _map = default!;
|
||||
|
||||
public bool EnableDebug
|
||||
{
|
||||
@@ -28,7 +29,7 @@ namespace Robust.Client.Physics
|
||||
|
||||
if (_enableDebug)
|
||||
{
|
||||
var overlay = new GridSplitNodeOverlay(_map, this, _transform);
|
||||
var overlay = new GridSplitNodeOverlay(_mapManager, this, _transform, _map);
|
||||
_overlay.AddOverlay(overlay);
|
||||
RaiseNetworkEvent(new RequestGridNodesMessage());
|
||||
}
|
||||
@@ -74,12 +75,14 @@ namespace Robust.Client.Physics
|
||||
private readonly IMapManager _mapManager;
|
||||
private readonly GridFixtureSystem _system;
|
||||
private readonly SharedTransformSystem _transform;
|
||||
private readonly SharedMapSystem _map;
|
||||
|
||||
public GridSplitNodeOverlay(IMapManager mapManager, GridFixtureSystem system, SharedTransformSystem transform)
|
||||
public GridSplitNodeOverlay(IMapManager mapManager, GridFixtureSystem system, SharedTransformSystem transform, SharedMapSystem map)
|
||||
{
|
||||
_mapManager = mapManager;
|
||||
_system = system;
|
||||
_transform = transform;
|
||||
_map = map;
|
||||
}
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
@@ -89,7 +92,7 @@ namespace Robust.Client.Physics
|
||||
var state = (_system, _transform, args.WorldBounds, worldHandle);
|
||||
|
||||
_mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref state,
|
||||
static (EntityUid uid, MapGridComponent grid,
|
||||
(EntityUid uid, MapGridComponent grid,
|
||||
ref (GridFixtureSystem system, SharedTransformSystem transform, Box2Rotated worldBounds, DrawingHandleWorld worldHandle) tuple) =>
|
||||
{
|
||||
// May not have received nodes yet.
|
||||
@@ -97,7 +100,7 @@ namespace Robust.Client.Physics
|
||||
return true;
|
||||
|
||||
tuple.worldHandle.SetTransform(tuple.transform.GetWorldMatrix(uid));
|
||||
var chunkEnumerator = grid.GetMapChunks(tuple.worldBounds);
|
||||
var chunkEnumerator = _map.GetMapChunks(uid, grid, tuple.worldBounds);
|
||||
|
||||
while (chunkEnumerator.MoveNext(out var chunk))
|
||||
{
|
||||
|
||||
@@ -25,7 +25,9 @@ namespace Robust.Client.Placement.Modes
|
||||
{
|
||||
var viewportSize = (Vector2)pManager.Clyde.ScreenSize;
|
||||
|
||||
var gridPosition = Grid.MapToGrid(pManager.EyeManager.ScreenToMap(Vector2.Zero));
|
||||
var gridUid = pManager.EntityManager.System<SharedTransformSystem>().GetGrid(MouseCoords);
|
||||
|
||||
var gridPosition = pManager.EntityManager.System<SharedMapSystem>().MapToGrid(gridUid!.Value, pManager.EyeManager.ScreenToMap(Vector2.Zero));
|
||||
|
||||
var gridstart = pManager.EyeManager.CoordinatesToScreen(
|
||||
gridPosition.WithPosition(new Vector2(MathF.Floor(gridPosition.X), MathF.Floor(gridPosition.Y))));
|
||||
@@ -52,9 +54,9 @@ namespace Robust.Client.Placement.Modes
|
||||
{
|
||||
MouseCoords = ScreenToCursorGrid(mouseScreen);
|
||||
|
||||
var gridIdOpt = MouseCoords.GetGridUid(pManager.EntityManager);
|
||||
var gridIdOpt = pManager.EntityManager.System<SharedTransformSystem>().GetGrid(MouseCoords);
|
||||
SnapSize = 1f;
|
||||
if (gridIdOpt is EntityUid gridId && gridId.IsValid())
|
||||
if (gridIdOpt is { } gridId && gridId.IsValid())
|
||||
{
|
||||
Grid = pManager.EntityManager.GetComponent<MapGridComponent>(gridId);
|
||||
SnapSize = Grid.TileSize; //Find snap size for the grid.
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace Robust.Client.Placement
|
||||
{
|
||||
public sealed partial class PlacementManager : IPlacementManager, IDisposable, IEntityEventSubscriber
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IClientNetManager _networkManager = default!;
|
||||
[Dependency] internal readonly IPlayerManager PlayerManager = default!;
|
||||
[Dependency] internal readonly IResourceCache ResourceCache = default!;
|
||||
@@ -42,6 +43,11 @@ namespace Robust.Client.Placement
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] internal readonly IClyde Clyde = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private SharedMapSystem Maps => EntityManager.System<SharedMapSystem>();
|
||||
private SharedTransformSystem XformSystem => EntityManager.System<SharedTransformSystem>();
|
||||
|
||||
/// <summary>
|
||||
/// How long before a pending tile change is dropped.
|
||||
/// </summary>
|
||||
@@ -188,6 +194,7 @@ namespace Robust.Client.Placement
|
||||
public void Initialize()
|
||||
{
|
||||
_drawingShader = _prototypeManager.Index<ShaderPrototype>("unshaded").Instance();
|
||||
_sawmill = _logManager.GetSawmill("placement");
|
||||
|
||||
_networkManager.RegisterNetMessage<MsgPlacement>(HandlePlacementMessage);
|
||||
|
||||
@@ -337,7 +344,11 @@ namespace Robust.Client.Placement
|
||||
|
||||
private void HandleTileChanged(ref TileChangedEvent args)
|
||||
{
|
||||
var coords = EntityManager.GetComponent<MapGridComponent>(args.NewTile.GridUid).GridTileToLocal(args.NewTile.GridIndices);
|
||||
var coords = Maps.GridTileToLocal(
|
||||
args.NewTile.GridUid,
|
||||
EntityManager.GetComponent<MapGridComponent>(args.NewTile.GridUid),
|
||||
args.NewTile.GridIndices);
|
||||
|
||||
_pendingTileChanges.RemoveAll(c => c.Item1 == coords);
|
||||
}
|
||||
|
||||
@@ -475,7 +486,7 @@ namespace Robust.Client.Placement
|
||||
|
||||
if (!_modeDictionary.TryFirstOrNull(pair => pair.Key.Equals(CurrentPermission.PlacementOption), out KeyValuePair<string, Type>? placeMode))
|
||||
{
|
||||
Logger.LogS(LogLevel.Warning, nameof(PlacementManager), $"Invalid placement mode `{CurrentPermission.PlacementOption}`");
|
||||
_sawmill.Log(LogLevel.Warning, $"Invalid placement mode `{CurrentPermission.PlacementOption}`");
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
@@ -531,8 +542,7 @@ namespace Robust.Client.Placement
|
||||
coordinates = new EntityCoordinates();
|
||||
return false;
|
||||
}
|
||||
coordinates = EntityCoordinates.FromMap(MapManager,
|
||||
EyeManager.PixelToMap(InputManager.MouseScreenPosition));
|
||||
coordinates = XformSystem.ToCoordinates(ent, EyeManager.PixelToMap(InputManager.MouseScreenPosition));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -648,7 +658,7 @@ namespace Robust.Client.Placement
|
||||
PlayerManager.LocalEntity is not {Valid: true} controlled)
|
||||
return;
|
||||
|
||||
var worldPos = EntityManager.GetComponent<TransformComponent>(controlled).WorldPosition;
|
||||
var worldPos = XformSystem.GetWorldPosition(controlled);
|
||||
|
||||
args.WorldHandle.DrawCircle(worldPos, CurrentPermission.Range, new Color(1, 1, 1, 0.25f));
|
||||
}
|
||||
@@ -754,14 +764,14 @@ namespace Robust.Client.Placement
|
||||
|
||||
if (CurrentPermission.IsTile)
|
||||
{
|
||||
var gridIdOpt = EntityManager.System<SharedTransformSystem>().GetGrid(coordinates);
|
||||
var gridIdOpt = XformSystem.GetGrid(coordinates);
|
||||
// If we have actually placed something on a valid grid...
|
||||
if (gridIdOpt is { } gridId && gridId.IsValid())
|
||||
{
|
||||
var grid = EntityManager.GetComponent<MapGridComponent>(gridId);
|
||||
|
||||
// no point changing the tile to the same thing.
|
||||
if (EntityManager.System<SharedMapSystem>().GetTileRef(gridId, grid, coordinates).Tile.TypeId == CurrentPermission.TileType)
|
||||
if (Maps.GetTileRef(gridId, grid, coordinates).Tile.TypeId == CurrentPermission.TileType)
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -302,7 +302,6 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
if (!child.Visible)
|
||||
{
|
||||
index--;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
public readonly void Draw(
|
||||
MarkupTagManager tagManager,
|
||||
DrawingHandleScreen handle,
|
||||
DrawingHandleBase handle,
|
||||
Font defaultFont,
|
||||
UIBox2 drawBox,
|
||||
float verticalOffset,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
@@ -13,6 +14,8 @@ public class Generator : IIncrementalGenerator
|
||||
private const string TypeCopierInterfaceNamespace = "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopier";
|
||||
private const string TypeCopyCreatorInterfaceNamespace = "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopyCreator";
|
||||
private const string SerializationHooksNamespace = "Robust.Shared.Serialization.ISerializationHooks";
|
||||
private const string AutoStateAttributeName = "Robust.Shared.Analyzers.AutoGenerateComponentStateAttribute";
|
||||
private const string ComponentDeltaInterfaceName = "Robust.Shared.GameObjects.IComponentDelta";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext initContext)
|
||||
{
|
||||
@@ -35,6 +38,7 @@ public class Generator : IIncrementalGenerator
|
||||
var builder = new StringBuilder();
|
||||
var containingTypes = new Stack<INamedTypeSymbol>();
|
||||
var declarationsGenerated = new HashSet<string>();
|
||||
var deltaType = compilation.GetTypeByMetadataName(ComponentDeltaInterfaceName)!;
|
||||
|
||||
foreach (var declaration in declarations)
|
||||
{
|
||||
@@ -106,9 +110,9 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
{
|
||||
{{GetConstructor(definition)}}
|
||||
|
||||
{{GetCopyMethods(definition)}}
|
||||
{{GetCopyMethods(definition, deltaType)}}
|
||||
|
||||
{{GetInstantiators(definition)}}
|
||||
{{GetInstantiators(definition, deltaType)}}
|
||||
}
|
||||
|
||||
{{containingTypesEnd}}
|
||||
@@ -192,7 +196,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string GetCopyMethods(DataDefinition definition)
|
||||
private static string GetCopyMethods(DataDefinition definition, ITypeSymbol deltaType)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
@@ -263,7 +267,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
{{baseCopy}}
|
||||
""");
|
||||
|
||||
foreach (var @interface in GetImplicitDataDefinitionInterfaces(definition.Type, true))
|
||||
foreach (var @interface in InternalGetImplicitDataDefinitionInterfaces(definition.Type, true, deltaType))
|
||||
{
|
||||
var interfaceModifiers = baseType != null && baseType.AllInterfaces.Contains(@interface, SymbolEqualityComparer.Default)
|
||||
? "override "
|
||||
@@ -292,7 +296,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string GetInstantiators(DataDefinition definition)
|
||||
private static string GetInstantiators(DataDefinition definition, ITypeSymbol deltaType)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
var modifiers = string.Empty;
|
||||
@@ -326,7 +330,7 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
""");
|
||||
}
|
||||
|
||||
foreach (var @interface in GetImplicitDataDefinitionInterfaces(definition.Type, false))
|
||||
foreach (var @interface in InternalGetImplicitDataDefinitionInterfaces(definition.Type, false, deltaType))
|
||||
{
|
||||
var interfaceName = @interface.ToDisplayString();
|
||||
builder.AppendLine($$"""
|
||||
@@ -345,6 +349,31 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
|
||||
private static IEnumerable<ITypeSymbol> InternalGetImplicitDataDefinitionInterfaces(ITypeSymbol type, bool all, ITypeSymbol deltaType)
|
||||
{
|
||||
var symbols = GetImplicitDataDefinitionInterfaces(type, all);
|
||||
|
||||
// TODO SOURCE GEN
|
||||
// fix this jank
|
||||
// The comp-state source generator will add an "IComponentDelta" interface to classes with the auto state
|
||||
// attribute, and this generator creates methods that those classes then have to implement because
|
||||
// IComponentDelta a DataDefinition via the ImplicitDataDefinitionForInheritorsAttribute on IComponent.
|
||||
if (!TryGetAttribute(type, AutoStateAttributeName, out var data))
|
||||
return symbols;
|
||||
|
||||
// If it doesn't have field deltas then ignore
|
||||
if (data.ConstructorArguments[1] is not { Value: bool fields and true })
|
||||
{
|
||||
return symbols;
|
||||
}
|
||||
|
||||
if (symbols.Any(x => x.ToDisplayString() == deltaType.ToDisplayString()))
|
||||
return symbols;
|
||||
|
||||
return symbols.Append(deltaType);
|
||||
}
|
||||
|
||||
// TODO serveronly? do we care? who knows!!
|
||||
private static StringBuilder CopyDataFields(DataDefinition definition)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
@@ -302,6 +303,21 @@ internal static class Types
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool TryGetAttribute(ISymbol symbol, string attributeName, [NotNullWhen(true)] out AttributeData? data)
|
||||
{
|
||||
foreach (var attribute in symbol.GetAttributes())
|
||||
{
|
||||
if (attribute.AttributeClass?.ToDisplayString() == attributeName)
|
||||
{
|
||||
data = attribute;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
data = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static IEnumerable<ITypeSymbol> GetBaseTypes(ITypeSymbol type)
|
||||
{
|
||||
var baseType = type.BaseType;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace Robust.Server.Placement
|
||||
_factory.GetComponentName(typeof(PlacementReplacementComponent)), out var compRegistry))
|
||||
{
|
||||
var key = ((PlacementReplacementComponent)compRegistry.Component).Key;
|
||||
var gridUid = coordinates.GetGridUid(_entityManager);
|
||||
var gridUid = _xformSystem.GetGrid(coordinates);
|
||||
|
||||
if (_entityManager.TryGetComponent<MapGridComponent>(gridUid, out var grid))
|
||||
{
|
||||
@@ -191,7 +191,7 @@ namespace Robust.Server.Placement
|
||||
|
||||
EntityUid gridId = coordinates.EntityId;
|
||||
if (_entityManager.TryGetComponent(coordinates.EntityId, out grid)
|
||||
|| _mapManager.TryFindGridAt(coordinates.ToMap(_entityManager, _xformSystem), out gridId, out grid))
|
||||
|| _mapManager.TryFindGridAt(_xformSystem.ToMapCoordinates(coordinates), out gridId, out grid))
|
||||
{
|
||||
_maps.SetTile(gridId, grid, coordinates, new Tile(tileType));
|
||||
|
||||
@@ -200,10 +200,13 @@ namespace Robust.Server.Placement
|
||||
}
|
||||
else if (tileType != 0) // create a new grid
|
||||
{
|
||||
var newGrid = _mapManager.CreateGridEntity(coordinates.GetMapId(_entityManager));
|
||||
var newGridXform = _entityManager.GetComponent<TransformComponent>(newGrid);
|
||||
var newGrid = _mapManager.CreateGridEntity(_xformSystem.GetMapId(coordinates));
|
||||
var newGridXform = new Entity<TransformComponent>(
|
||||
newGrid.Owner,
|
||||
_entityManager.GetComponent<TransformComponent>(newGrid));
|
||||
|
||||
_xformSystem.SetWorldPosition(newGridXform, coordinates.Position - newGrid.Comp.TileSizeHalfVector); // assume bottom left tile origin
|
||||
var tilePos = newGrid.Comp.WorldToTile(coordinates.Position);
|
||||
var tilePos = _maps.WorldToTile(newGrid.Owner, newGrid.Comp, coordinates.Position);
|
||||
_maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType));
|
||||
|
||||
var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId);
|
||||
@@ -228,7 +231,7 @@ namespace Robust.Server.Placement
|
||||
{
|
||||
EntityCoordinates start = _entityManager.GetCoordinates(msg.NetCoordinates);
|
||||
Vector2 rectSize = msg.RectSize;
|
||||
foreach (var entity in _lookup.GetEntitiesIntersecting(start.GetMapId(_entityManager),
|
||||
foreach (var entity in _lookup.GetEntitiesIntersecting(_xformSystem.GetMapId(start),
|
||||
new Box2(start.Position, start.Position + rectSize)))
|
||||
{
|
||||
if (_entityManager.Deleted(entity) ||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -36,7 +36,9 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
private static readonly SymbolDisplayFormat FullNullableFormat =
|
||||
FullyQualifiedFormat.WithMiscellaneousOptions(IncludeNullableReferenceTypeModifier);
|
||||
|
||||
private static string? GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp, bool raiseAfterAutoHandle)
|
||||
private static string? GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp,
|
||||
bool raiseAfterAutoHandle,
|
||||
bool fieldDeltas)
|
||||
{
|
||||
var nameSpace = classSymbol.ContainingNamespace.ToDisplayString();
|
||||
var componentName = classSymbol.Name;
|
||||
@@ -134,54 +136,154 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
// component.Count = state.Count;
|
||||
var handleStateSetters = new StringBuilder();
|
||||
|
||||
// Builds the string for copying delta fields in IComponentDelta to a new full state.
|
||||
var deltaCreate = new StringBuilder();
|
||||
|
||||
// Delta field states
|
||||
var deltaGetFields = new StringBuilder();
|
||||
|
||||
var deltaHandleFields = new StringBuilder();
|
||||
|
||||
// Apply the delta field to the full state.
|
||||
var deltaApply = new List<string>();
|
||||
|
||||
var index = -1;
|
||||
|
||||
var fieldsStr = new StringBuilder();
|
||||
var fieldStates = new StringBuilder();
|
||||
|
||||
var networkedTypes = new List<string>();
|
||||
|
||||
foreach (var (type, name) in fields)
|
||||
{
|
||||
index++;
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
fieldsStr.Append(@$"""{name}""");
|
||||
}
|
||||
else
|
||||
{
|
||||
fieldsStr.Append(@$", ""{name}""");
|
||||
}
|
||||
|
||||
var typeDisplayStr = type.ToDisplayString(FullNullableFormat);
|
||||
var nullable = type.NullableAnnotation == NullableAnnotation.Annotated;
|
||||
var nullableAnnotation = nullable ? "?" : string.Empty;
|
||||
|
||||
string deltaStateName = $"{name}_FieldComponentState";
|
||||
|
||||
// The type used for networking, e.g. EntityUid -> NetEntity
|
||||
string networkedType;
|
||||
|
||||
string getField;
|
||||
string? cast;
|
||||
// TODO: Uhh I just need casts or something.
|
||||
var castString = typeDisplayStr.Substring(8);
|
||||
|
||||
deltaGetFields.Append(@$"
|
||||
case {Math.Pow(2, index)}:
|
||||
args.State = new {deltaStateName}()
|
||||
{{
|
||||
");
|
||||
|
||||
deltaHandleFields.Append(@$"
|
||||
case {deltaStateName} {deltaStateName}_State:
|
||||
{{
|
||||
");
|
||||
|
||||
var fieldHandleValue = $"{deltaStateName}_State.{name}!";
|
||||
|
||||
switch (typeDisplayStr)
|
||||
{
|
||||
case GlobalEntityUidName:
|
||||
case GlobalNullableEntityUidName:
|
||||
stateFields.Append($@"
|
||||
public NetEntity{nullableAnnotation} {name} = default!;");
|
||||
networkedType = $"NetEntity{nullableAnnotation}";
|
||||
|
||||
stateFields.Append($@"
|
||||
public {networkedType} {name} = default!;");
|
||||
|
||||
getField = $"GetNetEntity(component.{name})";
|
||||
cast = $"(NetEntity{nullableAnnotation})";
|
||||
|
||||
getStateInit.Append($@"
|
||||
{name} = GetNetEntity(component.{name}),");
|
||||
handleStateSetters.Append($@"
|
||||
component.{name} = EnsureEntity<{componentName}>(state.{name}, uid);");
|
||||
component.{name} = EnsureEntity<{componentName}>(state.{name}, uid);");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
component.{name} = EnsureEntity<{componentName}>({cast} {fieldHandleValue}, uid);");
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = fullState.{name},");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
|
||||
break;
|
||||
case GlobalEntityCoordinatesName:
|
||||
case GlobalNullableEntityCoordinatesName:
|
||||
stateFields.Append($@"
|
||||
public NetCoordinates{nullableAnnotation} {name} = default!;");
|
||||
networkedType = $"NetCoordinates{nullableAnnotation}";
|
||||
|
||||
stateFields.Append($@"
|
||||
public {networkedType} {name} = default!;");
|
||||
|
||||
getField = $"GetNetCoordinates(component.{name})";
|
||||
cast = $"(NetCoordinates{nullableAnnotation})";
|
||||
|
||||
getStateInit.Append($@"
|
||||
{name} = GetNetCoordinates(component.{name}),");
|
||||
handleStateSetters.Append($@"
|
||||
component.{name} = EnsureCoordinates<{componentName}>(state.{name}, uid);");
|
||||
component.{name} = EnsureCoordinates<{componentName}>(state.{name}, uid);");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
component.{name} = EnsureCoordinates<{componentName}>({cast} {fieldHandleValue}, uid);");
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = fullState.{name},");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
|
||||
break;
|
||||
case GlobalEntityUidSetName:
|
||||
stateFields.Append($@"
|
||||
public {GlobalNetEntityUidSetName} {name} = default!;");
|
||||
networkedType = $"{GlobalNetEntityUidSetName}";
|
||||
|
||||
stateFields.Append($@"
|
||||
public {networkedType} {name} = default!;");
|
||||
|
||||
getField = $"GetNetEntitySet(component.{name})";
|
||||
cast = $"({GlobalNetEntityUidSetName})";
|
||||
|
||||
getStateInit.Append($@"
|
||||
{name} = GetNetEntitySet(component.{name}),");
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntitySet<{componentName}>(state.{name}, uid, component.{name});");
|
||||
EnsureEntitySet<{componentName}>(state.{name}, uid, component.{name});");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
EnsureEntitySet<{componentName}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = new(fullState.{name}),");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
|
||||
break;
|
||||
case GlobalEntityUidListName:
|
||||
stateFields.Append($@"
|
||||
public {GlobalNetEntityUidListName} {name} = default!;");
|
||||
networkedType = $"{GlobalNetEntityUidListName}";
|
||||
|
||||
stateFields.Append($@"
|
||||
public {networkedType} {name} = default!;");
|
||||
|
||||
getField = $"GetNetEntityList(component.{name})";
|
||||
cast = $"({GlobalNetEntityUidListName})";
|
||||
|
||||
getStateInit.Append($@"
|
||||
{name} = GetNetEntityList(component.{name}),");
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntityList<{componentName}>(state.{name}, uid, component.{name});");
|
||||
EnsureEntityList<{componentName}>(state.{name}, uid, component.{name});");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
EnsureEntityList<{componentName}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = new(fullState.{name}),");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
|
||||
break;
|
||||
default:
|
||||
@@ -205,69 +307,150 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
ensureGeneric = componentName;
|
||||
}
|
||||
|
||||
stateFields.Append($@"
|
||||
public Dictionary<{key}, {value}> {name} = default!;");
|
||||
networkedType = $"Dictionary<{key}, {value}>";
|
||||
|
||||
getStateInit.Append($@"
|
||||
{name} = GetNetEntityDictionary(component.{name}),");
|
||||
stateFields.Append($@"
|
||||
public {networkedType} {name} = default!;");
|
||||
|
||||
getField = $"GetNetEntityDictionary(component.{name})";
|
||||
|
||||
if (valueNullable && value is not GlobalNetEntityName and not GlobalNetEntityNullableName)
|
||||
{
|
||||
cast = $"(Dictionary<{key}, {value}>)";
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntityDictionaryNullableValue<{componentName}, {value}>(state.{name}, uid, component.{name});");
|
||||
EnsureEntityDictionaryNullableValue<{componentName}, {value}>(state.{name}, uid, component.{name});");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
EnsureEntityDictionaryNullableValue<{componentName}, {value}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
}
|
||||
else
|
||||
{
|
||||
cast = $"({castString})";
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntityDictionary<{ensureGeneric}>(state.{name}, uid, component.{name});");
|
||||
EnsureEntityDictionary<{ensureGeneric}>(state.{name}, uid, component.{name})");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
EnsureEntityDictionary<{ensureGeneric}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
}
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = new(fullState.{name}),");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (value is GlobalEntityUidName or GlobalNullableEntityUidName)
|
||||
{
|
||||
networkedType = $"Dictionary<{key}, {value}>";
|
||||
value = valueNullable ? GlobalNetEntityNullableName : GlobalNetEntityName;
|
||||
|
||||
stateFields.Append($@"
|
||||
public Dictionary<{key}, {value}> {name} = default!;");
|
||||
public {networkedType} {name} = default!;");
|
||||
|
||||
getField = $"GetNetEntityDictionary(component.{name})";
|
||||
cast = $"(Dictionary<{key}, {value}>)";
|
||||
|
||||
getStateInit.Append($@"
|
||||
{name} = GetNetEntityDictionary(component.{name}),");
|
||||
handleStateSetters.Append($@"
|
||||
EnsureEntityDictionary<{componentName}, {key}>(state.{name}, uid, component.{name});");
|
||||
EnsureEntityDictionary<{componentName}, {key}>(state.{name}, uid, component.{name});");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
EnsureEntityDictionary<{componentName}, {key}>({cast} {fieldHandleValue}, uid, component.{name});");
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = new(fullState.{name}),");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
networkedType = $"{typeDisplayStr}";
|
||||
|
||||
stateFields.Append($@"
|
||||
public {typeDisplayStr} {name} = default!;");
|
||||
public {networkedType} {name} = default!;");
|
||||
|
||||
if (IsCloneType(type))
|
||||
{
|
||||
// get first ctor arg of the field attribute, which determines whether the field should be cloned
|
||||
// (like if its a dict or list)
|
||||
getStateInit.Append($@"
|
||||
{name} = component.{name},");
|
||||
getField = $"component.{name}";
|
||||
cast = $"({castString})";
|
||||
|
||||
var nullCast = nullable ? castString.Substring(0, castString.Length - 1) : castString;
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
if (state.{name} == null)
|
||||
component.{name} = null!;
|
||||
else
|
||||
component.{name} = new(state.{name});");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
var {name}Value = {cast} {fieldHandleValue};
|
||||
if ({name}Value == null)
|
||||
component.{name} = null!;
|
||||
else
|
||||
component.{name} = new {nullCast}({name}Value);");
|
||||
|
||||
if (nullable)
|
||||
{
|
||||
deltaCreate.Append($@"
|
||||
{name} = fullState.{name} == null ? null : new(fullState.{name}),");
|
||||
}
|
||||
else
|
||||
{
|
||||
deltaCreate.Append($@"
|
||||
{name} = new(fullState.{name}),");
|
||||
}
|
||||
|
||||
deltaApply.Add($@"
|
||||
if ({name} == null)
|
||||
fullState.{name} = null!;
|
||||
else
|
||||
fullState.{name} = new({name});");
|
||||
}
|
||||
else
|
||||
{
|
||||
getStateInit.Append($@"
|
||||
{name} = component.{name},");
|
||||
getField = $"component.{name}";
|
||||
cast = $"({castString})";
|
||||
|
||||
handleStateSetters.Append($@"
|
||||
component.{name} = state.{name};");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
component.{name} = {cast} {fieldHandleValue};");
|
||||
|
||||
deltaCreate.Append($@"
|
||||
{name} = fullState.{name},");
|
||||
|
||||
deltaApply.Add($@"
|
||||
fullState.{name} = {name};");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* End loop stuff
|
||||
*/
|
||||
|
||||
networkedTypes.Add(networkedType);
|
||||
|
||||
getStateInit.Append($@"
|
||||
{name} = {getField},");
|
||||
|
||||
deltaGetFields.Append(@$" {name} = {getField}
|
||||
}};
|
||||
return;");
|
||||
|
||||
deltaHandleFields.Append($@"
|
||||
}}
|
||||
break;");
|
||||
|
||||
}
|
||||
|
||||
var eventRaise = "";
|
||||
@@ -278,19 +461,94 @@ namespace Robust.Shared.CompNetworkGenerator
|
||||
EntityManager.EventBus.RaiseComponentEvent(uid, component, ref ev);";
|
||||
}
|
||||
|
||||
var deltaGetState = "";
|
||||
var deltaHandleState = "";
|
||||
var deltaInterface = "";
|
||||
var deltaCompFields = "";
|
||||
var deltaNetRegister = "";
|
||||
|
||||
if (fieldDeltas)
|
||||
{
|
||||
for (var i = 0; i < fields.Count; i++)
|
||||
{
|
||||
var name = fields[i].FieldName;
|
||||
string deltaStateName = $"{name}_FieldComponentState";
|
||||
var networkedType = networkedTypes[i];
|
||||
var apply = deltaApply[i];
|
||||
|
||||
// Creates a state per field
|
||||
fieldStates.Append($@"
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class {deltaStateName} : IComponentDeltaState<{stateName}>
|
||||
{{
|
||||
public {networkedType} {name} = default!;
|
||||
|
||||
public void ApplyToFullState({stateName} fullState)
|
||||
{{{apply}
|
||||
}}
|
||||
|
||||
public {stateName} CreateNewFullState({stateName} fullState)
|
||||
{{
|
||||
var newState = new {stateName}
|
||||
{{{deltaCreate}
|
||||
}};
|
||||
{apply}
|
||||
|
||||
return newState;
|
||||
}}
|
||||
}}
|
||||
");
|
||||
}
|
||||
|
||||
deltaNetRegister = $@"EntityManager.ComponentFactory.RegisterNetworkedFields<{classSymbol}>({fieldsStr});";
|
||||
|
||||
deltaGetState = @$"// Delta state
|
||||
if (component is IComponentDelta delta && args.FromTick > component.CreationTick && delta.LastFieldUpdate >= args.FromTick)
|
||||
{{
|
||||
var fields = EntityManager.GetModifiedFields(component, args.FromTick);
|
||||
|
||||
// Try and get a matching delta state for the relevant dirty fields, otherwise fall back to full state.
|
||||
switch (fields)
|
||||
{{{deltaGetFields}
|
||||
default:
|
||||
break;
|
||||
}}
|
||||
}}";
|
||||
|
||||
deltaHandleState = $@"switch(args.Current)
|
||||
{{{deltaHandleFields}
|
||||
default:
|
||||
break;
|
||||
}}";
|
||||
|
||||
deltaInterface = " : IComponentDelta";
|
||||
|
||||
deltaCompFields = @$"/// <inheritdoc />
|
||||
public GameTick LastFieldUpdate {{ get; set; }} = GameTick.Zero;
|
||||
|
||||
/// <inheritdoc />
|
||||
public GameTick[] LastModifiedFields {{ get; set; }} = Array.Empty<GameTick>();";
|
||||
}
|
||||
|
||||
return $@"// <auto-generated />
|
||||
#nullable enable
|
||||
using System;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace {nameSpace};
|
||||
|
||||
public partial class {componentName}
|
||||
public partial class {componentName}{deltaInterface}
|
||||
{{
|
||||
{deltaCompFields}
|
||||
|
||||
[System.Serializable, NetSerializable]
|
||||
public sealed class {stateName} : IComponentState
|
||||
{{{stateFields}
|
||||
@@ -301,12 +559,16 @@ public partial class {componentName}
|
||||
{{
|
||||
public override void Initialize()
|
||||
{{
|
||||
{deltaNetRegister}
|
||||
SubscribeLocalEvent<{componentName}, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<{componentName}, ComponentHandleState>(OnHandleState);
|
||||
}}
|
||||
|
||||
private void OnGetState(EntityUid uid, {componentName} component, ref ComponentGetState args)
|
||||
{{
|
||||
{deltaGetState}
|
||||
|
||||
// Get full state
|
||||
args.State = new {stateName}
|
||||
{{{getStateInit}
|
||||
}};
|
||||
@@ -314,11 +576,15 @@ public partial class {componentName}
|
||||
|
||||
private void OnHandleState(EntityUid uid, {componentName} component, ref ComponentHandleState args)
|
||||
{{
|
||||
{deltaHandleState}
|
||||
|
||||
if (args.Current is not {stateName} state)
|
||||
return;
|
||||
{handleStateSetters}{eventRaise}
|
||||
}}
|
||||
}}
|
||||
|
||||
{fieldStates}
|
||||
}}
|
||||
";
|
||||
}
|
||||
@@ -341,13 +607,15 @@ public partial class {componentName}
|
||||
{
|
||||
var attr = type.Attribute;
|
||||
var raiseEv = false;
|
||||
if (attr.ConstructorArguments is [{Value: bool raise}])
|
||||
var fieldDeltas = false;
|
||||
if (attr.ConstructorArguments is [{Value: bool raise}, {Value: bool fields}])
|
||||
{
|
||||
// Get the afterautohandle bool, which is first constructor arg
|
||||
raiseEv = raise;
|
||||
fieldDeltas = fields;
|
||||
}
|
||||
|
||||
var source = GenerateSource(context, type.Type, comp, raiseEv);
|
||||
var source = GenerateSource(context, type.Type, comp, raiseEv, fieldDeltas);
|
||||
// can be null if no members marked with network field, which already has a diagnostic, so
|
||||
// just continue
|
||||
if (source == null)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"Comp State Generator": {
|
||||
"commandName": "DebugRoslynComponent",
|
||||
"targetProject": "../../Content.Shared/Content.Shared.csproj"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -19,9 +19,15 @@ public sealed class AutoGenerateComponentStateAttribute : Attribute
|
||||
/// </summary>
|
||||
public bool RaiseAfterAutoHandleState;
|
||||
|
||||
public AutoGenerateComponentStateAttribute(bool raiseAfterAutoHandleState = false)
|
||||
/// <summary>
|
||||
/// Should delta states be generated for every field.
|
||||
/// </summary>
|
||||
public bool FieldDeltas;
|
||||
|
||||
public AutoGenerateComponentStateAttribute(bool raiseAfterAutoHandleState = false, bool fieldDeltas = false)
|
||||
{
|
||||
RaiseAfterAutoHandleState = raiseAfterAutoHandleState;
|
||||
FieldDeltas = fieldDeltas;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ namespace Robust.Shared.Audio.Components;
|
||||
/// <summary>
|
||||
/// Stores the audio data for an audio entity.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), Access(typeof(SharedAudioSystem))]
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true, fieldDeltas: true), Access(typeof(SharedAudioSystem))]
|
||||
public sealed partial class AudioComponent : Component, IAudioSource
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite), AutoNetworkedField, DataField, Access(Other = AccessPermissions.ReadWriteExecute)]
|
||||
[AutoNetworkedField, DataField, Access(Other = AccessPermissions.ReadWriteExecute)]
|
||||
public AudioFlags Flags = AudioFlags.None;
|
||||
|
||||
#region Filter
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Robust.Shared.Audio.Components;
|
||||
/// <summary>
|
||||
/// Marks this entity as being spawned for audio presets in case we need to reload.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedAudioSystem))]
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), Access(typeof(SharedAudioSystem))]
|
||||
public sealed partial class AudioPresetComponent : Component
|
||||
{
|
||||
[AutoNetworkedField]
|
||||
|
||||
@@ -105,6 +105,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
if (entity.Comp.PauseTime != null)
|
||||
{
|
||||
entity.Comp.PauseTime = entity.Comp.PauseTime.Value + timeOffset;
|
||||
DirtyField(entity, nameof(AudioComponent.PauseTime));
|
||||
|
||||
// Paused audio doesn't have TimedDespawn so.
|
||||
}
|
||||
@@ -112,6 +113,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
{
|
||||
// Bump it back so the actual playback positions moves forward
|
||||
entity.Comp.AudioStart -= timeOffset;
|
||||
DirtyField(entity, nameof(AudioComponent.AudioStart));
|
||||
|
||||
// need to ensure it doesn't despawn too early.
|
||||
if (TryComp(entity.Owner, out TimedDespawnComponent? despawn))
|
||||
@@ -121,8 +123,6 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
}
|
||||
|
||||
entity.Comp.PlaybackPosition = position;
|
||||
// Network the new playback position.
|
||||
Dirty(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -191,6 +191,9 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
var pauseOffset = Timing.CurTime - component.PauseTime;
|
||||
component.AudioStart += pauseOffset ?? TimeSpan.Zero;
|
||||
component.PlaybackPosition = (float) (Timing.CurTime - component.AudioStart).TotalSeconds;
|
||||
|
||||
DirtyField(entity.Value, component, nameof(AudioComponent.AudioStart));
|
||||
DirtyField(entity.Value, component, nameof(AudioComponent.PlaybackPosition));
|
||||
}
|
||||
|
||||
// If we were stopped then played then restart audiostart to now.
|
||||
@@ -198,6 +201,9 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
{
|
||||
component.AudioStart = Timing.CurTime;
|
||||
component.PauseTime = null;
|
||||
|
||||
DirtyField(entity.Value, component, nameof(AudioComponent.AudioStart));
|
||||
DirtyField(entity.Value, component, nameof(AudioComponent.PauseTime));
|
||||
}
|
||||
|
||||
switch (state)
|
||||
@@ -205,17 +211,21 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
case AudioState.Stopped:
|
||||
component.AudioStart = Timing.CurTime;
|
||||
component.PauseTime = null;
|
||||
DirtyField(entity.Value, component, nameof(AudioComponent.AudioStart));
|
||||
DirtyField(entity.Value, component, nameof(AudioComponent.PauseTime));
|
||||
component.StopPlaying();
|
||||
RemComp<TimedDespawnComponent>(entity.Value);
|
||||
break;
|
||||
case AudioState.Paused:
|
||||
// Set it to current time so we can easily unpause it later.
|
||||
component.PauseTime = Timing.CurTime;
|
||||
DirtyField(entity.Value, component, nameof(AudioComponent.PauseTime));
|
||||
component.Pause();
|
||||
RemComp<TimedDespawnComponent>(entity.Value);
|
||||
break;
|
||||
case AudioState.Playing:
|
||||
component.PauseTime = null;
|
||||
DirtyField(entity.Value, component, nameof(AudioComponent.PauseTime));
|
||||
component.StartPlaying();
|
||||
|
||||
// Reset TimedDespawn so the audio still gets cleaned up.
|
||||
@@ -230,7 +240,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
}
|
||||
|
||||
component.State = state;
|
||||
Dirty(entity.Value, component);
|
||||
DirtyField(entity.Value, component, nameof(AudioComponent.State));
|
||||
}
|
||||
|
||||
protected void SetZOffset(float value)
|
||||
@@ -375,7 +385,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
|
||||
component.Params.Volume = value;
|
||||
component.Volume = value;
|
||||
Dirty(entity.Value, component);
|
||||
DirtyField(entity.Value, component, nameof(AudioComponent.Params));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(@"/")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +175,41 @@ namespace Robust.Shared.GameObjects
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterNetworkedFields<T>(params string[] fields) where T : IComponent
|
||||
{
|
||||
var compReg = GetRegistration(CompIdx.Index<T>());
|
||||
RegisterNetworkedFields(compReg, fields);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterNetworkedFields(ComponentRegistration compReg, params string[] fields)
|
||||
{
|
||||
// Nothing to do.
|
||||
if (compReg.NetworkedFields.Length > 0 || fields.Length == 0)
|
||||
return;
|
||||
|
||||
DebugTools.Assert(fields.Length <= 32);
|
||||
|
||||
if (fields.Length > 32)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Components with more than 32 networked fields unsupported! Consider splitting it up or making a pr for 64-bit flags");
|
||||
}
|
||||
|
||||
compReg.NetworkedFields = fields;
|
||||
var lookup = new Dictionary<string, int>(fields.Length);
|
||||
var i = 0;
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
lookup[field] = i;
|
||||
i++;
|
||||
}
|
||||
|
||||
compReg.NetworkedFieldLookup = lookup.ToFrozenDictionary();
|
||||
}
|
||||
|
||||
public void IgnoreMissingComponents(string postfix = "")
|
||||
{
|
||||
if (_ignoreMissingComponentPostfix != null && _ignoreMissingComponentPostfix != postfix)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameStates;
|
||||
@@ -39,6 +40,13 @@ public sealed class ComponentRegistration
|
||||
/// </summary>
|
||||
public Type Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Fields that are networked for this component. Used for delta states.
|
||||
/// </summary>
|
||||
public string[] NetworkedFields = [];
|
||||
|
||||
public FrozenDictionary<string, int> NetworkedFieldLookup = FrozenDictionary<string, int>.Empty;
|
||||
|
||||
// Internal for sandboxing.
|
||||
// Avoid content passing an instance of this to ComponentFactory to get any type they want instantiated.
|
||||
internal ComponentRegistration(string name, Type type, CompIdx idx, bool unsaved = false)
|
||||
|
||||
@@ -1,30 +1,15 @@
|
||||
using System;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// An optimisation component for stuff that should be set as collidable when it's awake and non-collidable when asleep.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
[Access(typeof(CollisionWakeSystem))]
|
||||
public sealed partial class CollisionWakeComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// An optimisation component for stuff that should be set as collidable when it's awake and non-collidable when asleep.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent()]
|
||||
[Access(typeof(CollisionWakeSystem))]
|
||||
public sealed partial class CollisionWakeComponent : Component
|
||||
{
|
||||
[DataField("enabled")]
|
||||
public bool Enabled = true;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class CollisionWakeState : ComponentState
|
||||
{
|
||||
public bool Enabled { get; }
|
||||
|
||||
public CollisionWakeState(bool enabled)
|
||||
{
|
||||
Enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Enabled = true;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedEyeSystem)), AutoGenerateComponentState(true)]
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedEyeSystem)), AutoGenerateComponentState(true, fieldDeltas: true)]
|
||||
public sealed partial class EyeComponent : Component
|
||||
{
|
||||
public const int DefaultVisibilityMask = 1;
|
||||
@@ -24,33 +24,33 @@ namespace Robust.Shared.GameObjects
|
||||
/// that without messing with the main viewport's eye. This is important as there are some overlays that are
|
||||
/// only be drawn if that viewport's eye belongs to the currently controlled entity.
|
||||
/// </remarks>
|
||||
[ViewVariables, DataField("target"), AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntityUid? Target;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("drawFov"), AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool DrawFov = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
|
||||
[AutoNetworkedField]
|
||||
public bool DrawLight = true;
|
||||
|
||||
// yes it's not networked, don't ask.
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("rotation")]
|
||||
[DataField]
|
||||
public Angle Rotation;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("zoom")]
|
||||
[DataField]
|
||||
public Vector2 Zoom = Vector2.One;
|
||||
|
||||
/// <summary>
|
||||
/// Eye offset, relative to the map, and not affected by <see cref="Rotation"/>
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("offset"), AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public Vector2 Offset;
|
||||
|
||||
/// <summary>
|
||||
/// The visibility mask for this eye.
|
||||
/// The player will be able to get updates for entities whose layers match the mask.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("visMask", customTypeSerializer:typeof(FlagSerializer<VisibilityMaskLayer>)), AutoNetworkedField]
|
||||
[DataField("visMask", customTypeSerializer:typeof(FlagSerializer<VisibilityMaskLayer>)), AutoNetworkedField]
|
||||
public int VisibilityMask = DefaultVisibilityMask;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,14 +3,22 @@ using System.Collections.Generic;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedUserInterfaceSystem))]
|
||||
public sealed partial class UserInterfaceComponent : Component
|
||||
public sealed partial class UserInterfaceComponent : Component, IComponentDelta
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public GameTick LastFieldUpdate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public GameTick[] LastModifiedFields { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The currently open interfaces. Used clientside to store the UI.
|
||||
/// </summary>
|
||||
@@ -30,19 +38,71 @@ namespace Robust.Shared.GameObjects
|
||||
/// Legacy data, new BUIs should be using comp states.
|
||||
/// </summary>
|
||||
public Dictionary<Enum, BoundUserInterfaceState> States = new();
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
internal sealed class UserInterfaceComponentState(
|
||||
Dictionary<Enum, List<NetEntity>> actors,
|
||||
Dictionary<Enum, BoundUserInterfaceState> states,
|
||||
Dictionary<Enum, InterfaceData> data)
|
||||
: IComponentState
|
||||
[Serializable, NetSerializable]
|
||||
internal sealed class UserInterfaceComponentState(
|
||||
Dictionary<Enum, List<NetEntity>> actors,
|
||||
Dictionary<Enum, BoundUserInterfaceState> states,
|
||||
Dictionary<Enum, InterfaceData> data)
|
||||
: IComponentState
|
||||
{
|
||||
public Dictionary<Enum, List<NetEntity>> Actors = actors;
|
||||
|
||||
public Dictionary<Enum, BoundUserInterfaceState> States = states;
|
||||
|
||||
public Dictionary<Enum, InterfaceData> Data = data;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
internal sealed class UserInterfaceActorsDeltaState : IComponentDeltaState<UserInterfaceComponentState>
|
||||
{
|
||||
public Dictionary<Enum, List<NetEntity>> Actors = new();
|
||||
|
||||
public void ApplyToFullState(UserInterfaceComponentState fullState)
|
||||
{
|
||||
public Dictionary<Enum, List<NetEntity>> Actors = actors;
|
||||
fullState.Actors.Clear();
|
||||
|
||||
public Dictionary<Enum, BoundUserInterfaceState> States = states;
|
||||
foreach (var (key, value) in Actors)
|
||||
{
|
||||
fullState.Actors.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<Enum, InterfaceData> Data = data;
|
||||
public UserInterfaceComponentState CreateNewFullState(UserInterfaceComponentState fullState)
|
||||
{
|
||||
var newState = new UserInterfaceComponentState(
|
||||
new Dictionary<Enum, List<NetEntity>>(Actors),
|
||||
new(fullState.States),
|
||||
new(fullState.Data));
|
||||
|
||||
return newState;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
internal sealed class UserInterfaceStatesDeltaState : IComponentDeltaState<UserInterfaceComponentState>
|
||||
{
|
||||
public Dictionary<Enum, BoundUserInterfaceState> States = new();
|
||||
|
||||
public void ApplyToFullState(UserInterfaceComponentState fullState)
|
||||
{
|
||||
fullState.States.Clear();
|
||||
|
||||
foreach (var (key, value) in States)
|
||||
{
|
||||
fullState.States.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public UserInterfaceComponentState CreateNewFullState(UserInterfaceComponentState fullState)
|
||||
{
|
||||
var newState = new UserInterfaceComponentState(
|
||||
new Dictionary<Enum, List<NetEntity>>(fullState.Actors),
|
||||
new(States),
|
||||
new(fullState.Data));
|
||||
|
||||
return newState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +129,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;
|
||||
|
||||
78
Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs
Normal file
78
Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
public abstract partial class EntityManager
|
||||
{
|
||||
public uint GetModifiedFields(IComponentDelta delta, GameTick fromTick)
|
||||
{
|
||||
uint fields = 0;
|
||||
|
||||
for (var i = 0; i < delta.LastModifiedFields.Length; i++)
|
||||
{
|
||||
var lastUpdate = delta.LastModifiedFields[i];
|
||||
|
||||
// Field not dirty
|
||||
if (lastUpdate < fromTick)
|
||||
continue;
|
||||
|
||||
fields |= (uint) (1 << i);
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
public void DirtyField(EntityUid uid, IComponentDelta comp, string fieldName, MetaDataComponent? metadata = null)
|
||||
{
|
||||
var compReg = ComponentFactory.GetRegistration(comp);
|
||||
|
||||
if (!compReg.NetworkedFieldLookup.TryGetValue(fieldName, out var idx))
|
||||
{
|
||||
_sawmill.Error($"Tried to dirty delta field {fieldName} on {ToPrettyString(uid)} that isn't implemented.");
|
||||
return;
|
||||
}
|
||||
|
||||
var curTick = _gameTiming.CurTick;
|
||||
comp.LastFieldUpdate = curTick;
|
||||
comp.LastModifiedFields[idx] = curTick;
|
||||
Dirty(uid, comp, metadata);
|
||||
}
|
||||
|
||||
public void DirtyField<T>(EntityUid uid, T comp, string fieldName, MetaDataComponent? metadata = null)
|
||||
where T : IComponentDelta
|
||||
{
|
||||
var compReg = ComponentFactory.GetRegistration(CompIdx.Index<T>());
|
||||
|
||||
if (!compReg.NetworkedFieldLookup.TryGetValue(fieldName, out var idx))
|
||||
{
|
||||
_sawmill.Error($"Tried to dirty delta field {fieldName} on {ToPrettyString(uid)} that isn't implemented.");
|
||||
return;
|
||||
}
|
||||
|
||||
var curTick = _gameTiming.CurTick;
|
||||
comp.LastFieldUpdate = curTick;
|
||||
comp.LastModifiedFields[idx] = curTick;
|
||||
Dirty(uid, comp, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates this component supports delta states.
|
||||
/// </summary>
|
||||
public partial interface IComponentDelta : IComponent
|
||||
{
|
||||
// TODO: This isn't entirely robust but not sure how else to handle this?
|
||||
/// <summary>
|
||||
/// Track last time a field was dirtied. if the full component dirty exceeds this then we send a full state update.
|
||||
/// </summary>
|
||||
public GameTick LastFieldUpdate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the last modified tick for fields.
|
||||
/// </summary>
|
||||
public GameTick[] LastModifiedFields
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
@@ -372,6 +372,13 @@ namespace Robust.Shared.GameObjects
|
||||
metadata.NetComponents.Add(netId, component);
|
||||
}
|
||||
|
||||
if (component is IComponentDelta delta)
|
||||
{
|
||||
var curTick = _gameTiming.CurTick;
|
||||
delta.LastModifiedFields = new GameTick[reg.NetworkedFields.Length];
|
||||
Array.Fill(delta.LastModifiedFields, curTick);
|
||||
}
|
||||
|
||||
component.Networked = reg.NetID != null;
|
||||
|
||||
var eventArgs = new AddedComponentEventArgs(new ComponentEventArgs(component, uid), reg);
|
||||
@@ -707,6 +714,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,13 +724,15 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool HasComponent<T>(EntityUid? uid) where T : IComponent
|
||||
[Pure]
|
||||
public bool HasComponent<T>([NotNullWhen(true)] EntityUid? uid) where T : IComponent
|
||||
{
|
||||
return uid.HasValue && HasComponent<T>(uid.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public bool HasComponent(EntityUid uid, Type type)
|
||||
{
|
||||
var dict = _entTraitDict[type];
|
||||
@@ -731,7 +741,8 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool HasComponent(EntityUid? uid, Type type)
|
||||
[Pure]
|
||||
public bool HasComponent([NotNullWhen(true)] EntityUid? uid, Type type)
|
||||
{
|
||||
if (!uid.HasValue)
|
||||
{
|
||||
@@ -744,6 +755,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,7 +766,8 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool HasComponent(EntityUid? uid, ushort netId, MetaDataComponent? meta = null)
|
||||
[Pure]
|
||||
public bool HasComponent([NotNullWhen(true)] EntityUid? uid, ushort netId, MetaDataComponent? meta = null)
|
||||
{
|
||||
if (!uid.HasValue)
|
||||
{
|
||||
@@ -821,6 +834,7 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Pure]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T GetComponent<T>(EntityUid uid) where T : IComponent
|
||||
{
|
||||
@@ -837,6 +851,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 +865,7 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Pure]
|
||||
public IComponent GetComponent(EntityUid uid, Type type)
|
||||
{
|
||||
// ReSharper disable once InvertIf
|
||||
@@ -866,12 +882,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 +1468,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()}");
|
||||
@@ -1578,7 +1597,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public bool HasComp(EntityUid? uid) => HasComponent(uid);
|
||||
public bool HasComp([NotNullWhen(true)] EntityUid? uid) => HasComponent(uid);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
@@ -1589,7 +1608,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Pure]
|
||||
public bool HasComponent(EntityUid? uid)
|
||||
public bool HasComponent([NotNullWhen(true)] EntityUid? uid)
|
||||
{
|
||||
return uid != null && HasComponent(uid.Value);
|
||||
}
|
||||
|
||||
@@ -148,6 +148,29 @@ public partial class EntitySystem
|
||||
EntityManager.Dirty(uid, component, meta);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected void DirtyField(EntityUid uid, IComponentDelta delta, string fieldName, MetaDataComponent? meta = null)
|
||||
{
|
||||
EntityManager.DirtyField(uid, delta, fieldName, meta);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected void DirtyField<T>(Entity<T?> entity, string fieldName, MetaDataComponent? meta = null)
|
||||
where T : IComponentDelta
|
||||
{
|
||||
if (!Resolve(entity.Owner, ref entity.Comp))
|
||||
return;
|
||||
|
||||
EntityManager.DirtyField(entity.Owner, entity.Comp, fieldName, meta);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected void DirtyField<T>(EntityUid uid, T component, string fieldName, MetaDataComponent? meta = null)
|
||||
where T : IComponentDelta
|
||||
{
|
||||
EntityManager.DirtyField(uid, component, fieldName, meta);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a component as dirty. This also implicitly dirties the entity this component belongs to.
|
||||
/// </summary>
|
||||
|
||||
@@ -77,6 +77,10 @@ namespace Robust.Shared.GameObjects
|
||||
/// <returns>The availability of the component.</returns>
|
||||
ComponentAvailability GetComponentAvailability(string componentName, bool ignoreCase = false);
|
||||
|
||||
public void RegisterNetworkedFields<T>(params string[] fields) where T : IComponent;
|
||||
|
||||
public void RegisterNetworkedFields(ComponentRegistration compReg, params string[] fields);
|
||||
|
||||
/// <summary>
|
||||
/// Slow-path for Type -> CompIdx mapping without generics.
|
||||
/// </summary>
|
||||
|
||||
@@ -6,6 +6,11 @@ namespace Robust.Shared.GameObjects;
|
||||
|
||||
public partial interface IEntityManager
|
||||
{
|
||||
public void DirtyField(EntityUid uid, IComponentDelta delta, string fieldName, MetaDataComponent? metadata = null);
|
||||
|
||||
public void DirtyField<T>(EntityUid uid, T component, string fieldName, MetaDataComponent? metadata = null)
|
||||
where T : IComponentDelta;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a string as a NetEntity and return the relevant EntityUid.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
@@ -17,9 +16,6 @@ namespace Robust.Shared.GameObjects
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<CollisionWakeComponent, ComponentShutdown>(OnRemove);
|
||||
|
||||
SubscribeLocalEvent<CollisionWakeComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<CollisionWakeComponent, ComponentHandleState>(OnHandleState);
|
||||
|
||||
SubscribeLocalEvent<CollisionWakeComponent, JointAddedEvent>(OnJointAdd);
|
||||
SubscribeLocalEvent<CollisionWakeComponent, JointRemovedEvent>(OnJointRemove);
|
||||
|
||||
@@ -43,22 +39,6 @@ namespace Robust.Shared.GameObjects
|
||||
Dirty(uid, component);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, CollisionWakeComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is CollisionWakeComponent.CollisionWakeState state)
|
||||
component.Enabled = state.Enabled;
|
||||
|
||||
// Note, this explicitly does not update PhysicsComponent.CanCollide. The physics component should perform
|
||||
// its own state-handling logic. Additionally, if we wanted to set it you would have to ensure that things
|
||||
// like the join-component and physics component have already handled their states, otherwise CanCollide may
|
||||
// be set incorrectly and leave the client with a bad state.
|
||||
}
|
||||
|
||||
private void OnGetState(EntityUid uid, CollisionWakeComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new CollisionWakeComponent.CollisionWakeState(component.Enabled);
|
||||
}
|
||||
|
||||
private void OnRemove(EntityUid uid, CollisionWakeComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (component.Enabled
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -63,7 +63,7 @@ public abstract class SharedEyeSystem : EntitySystem
|
||||
|
||||
eyeComponent.Offset = value;
|
||||
eyeComponent.Eye.Offset = value;
|
||||
Dirty(uid, eyeComponent);
|
||||
DirtyField(uid, eyeComponent, nameof(EyeComponent.Offset));
|
||||
}
|
||||
|
||||
public void SetDrawFov(EntityUid uid, bool value, EyeComponent? eyeComponent = null)
|
||||
@@ -76,7 +76,7 @@ public abstract class SharedEyeSystem : EntitySystem
|
||||
|
||||
eyeComponent.DrawFov = value;
|
||||
eyeComponent.Eye.DrawFov = value;
|
||||
Dirty(uid, eyeComponent);
|
||||
DirtyField(uid, eyeComponent, nameof(EyeComponent.DrawFov));
|
||||
}
|
||||
|
||||
public void SetDrawLight(Entity<EyeComponent?> entity, bool value)
|
||||
@@ -89,7 +89,7 @@ public abstract class SharedEyeSystem : EntitySystem
|
||||
|
||||
entity.Comp.DrawLight = value;
|
||||
entity.Comp.Eye.DrawLight = value;
|
||||
Dirty(entity);
|
||||
DirtyField(entity, nameof(EyeComponent.DrawLight));
|
||||
}
|
||||
|
||||
public void SetRotation(EntityUid uid, Angle rotation, EyeComponent? eyeComponent = null)
|
||||
@@ -127,7 +127,7 @@ public abstract class SharedEyeSystem : EntitySystem
|
||||
}
|
||||
|
||||
eyeComponent.Target = value;
|
||||
Dirty(uid, eyeComponent);
|
||||
DirtyField(uid, eyeComponent, nameof(EyeComponent.Target));
|
||||
}
|
||||
|
||||
public void SetZoom(EntityUid uid, Vector2 value, EyeComponent? eyeComponent = null)
|
||||
@@ -168,6 +168,6 @@ public abstract class SharedEyeSystem : EntitySystem
|
||||
return;
|
||||
|
||||
eyeComponent.VisibilityMask = value;
|
||||
Dirty(uid, eyeComponent);
|
||||
DirtyField(uid, eyeComponent, nameof(EyeComponent.VisibilityMask));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -719,8 +719,7 @@ public abstract partial class SharedTransformSystem
|
||||
component.LocalRotation,
|
||||
parent,
|
||||
component.NoLocalRotation,
|
||||
component.Anchored,
|
||||
component.GridTraversal);
|
||||
component.Anchored);
|
||||
}
|
||||
|
||||
internal void OnHandleState(EntityUid uid, TransformComponent xform, ref ComponentHandleState args)
|
||||
|
||||
@@ -337,8 +337,6 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
public readonly bool Anchored;
|
||||
|
||||
public readonly bool GridTraversal;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new state snapshot of a TransformComponent.
|
||||
/// </summary>
|
||||
@@ -346,14 +344,13 @@ namespace Robust.Shared.GameObjects
|
||||
/// <param name="rotation">Current direction offset of this entity.</param>
|
||||
/// <param name="parentId">Current parent transform of this entity.</param>
|
||||
/// <param name="noLocalRotation"></param>
|
||||
public TransformComponentState(Vector2 localPosition, Angle rotation, NetEntity parentId, bool noLocalRotation, bool anchored, bool gridTraversal)
|
||||
public TransformComponentState(Vector2 localPosition, Angle rotation, NetEntity parentId, bool noLocalRotation, bool anchored)
|
||||
{
|
||||
LocalPosition = localPosition;
|
||||
Rotation = rotation;
|
||||
ParentID = parentId;
|
||||
NoLocalRotation = noLocalRotation;
|
||||
Anchored = anchored;
|
||||
GridTraversal = gridTraversal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
EntityManager.ComponentFactory.RegisterNetworkedFields<UserInterfaceComponent>(
|
||||
nameof(UserInterfaceComponent.Actors),
|
||||
nameof(UserInterfaceComponent.Interfaces),
|
||||
nameof(UserInterfaceComponent.States));
|
||||
|
||||
_ignoreUIRangeQuery = GetEntityQuery<IgnoreUIRangeComponent>();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
UIQuery = GetEntityQuery<UserInterfaceComponent>();
|
||||
@@ -204,7 +209,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (actors.Count == 0)
|
||||
ent.Comp.Actors.Remove(key);
|
||||
|
||||
Dirty(ent);
|
||||
DirtyField(ent, nameof(UserInterfaceComponent.Actors));
|
||||
|
||||
// If the actor is also deleting then don't worry about updating what they have open.
|
||||
if (!TerminatingOrDeleted(actor)
|
||||
@@ -256,7 +261,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
// Let state handling open the UI clientside.
|
||||
actorComp.OpenInterfaces.GetOrNew(ent.Owner).Add(key);
|
||||
ent.Comp.Actors.GetOrNew(key).Add(actor);
|
||||
Dirty(ent);
|
||||
DirtyField(ent, nameof(UserInterfaceComponent.Actors));
|
||||
|
||||
var ev = new BoundUIOpenedEvent(key, ent.Owner, actor);
|
||||
RaiseLocalEvent(ent.Owner, ev);
|
||||
@@ -275,6 +280,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (ent.Comp.States.TryGetValue(key, out var state))
|
||||
{
|
||||
bui.UpdateState(state);
|
||||
bui.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,25 +301,54 @@ 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)
|
||||
{
|
||||
// TODO delta states.
|
||||
// I.e., don't resend the whole BUI state just because a new user opened it.
|
||||
if (ent.Comp.LastFieldUpdate >= args.FromTick)
|
||||
{
|
||||
var fields = EntityManager.GetModifiedFields(ent.Comp, args.FromTick);
|
||||
|
||||
switch (fields)
|
||||
{
|
||||
case 1 << 0:
|
||||
{
|
||||
var state = new UserInterfaceActorsDeltaState();
|
||||
AddActors(ent, state.Actors, ref args);
|
||||
|
||||
args.State = state;
|
||||
return;
|
||||
}
|
||||
case 1 << 2:
|
||||
{
|
||||
var state = new UserInterfaceStatesDeltaState()
|
||||
{
|
||||
States = new Dictionary<Enum, BoundUserInterfaceState>(ent.Comp.States),
|
||||
};
|
||||
|
||||
args.State = state;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var actors = new Dictionary<Enum, List<NetEntity>>();
|
||||
|
||||
var dataCopy = new Dictionary<Enum, InterfaceData>();
|
||||
var dataCopy = new Dictionary<Enum, InterfaceData>(ent.Comp.Interfaces.Count);
|
||||
|
||||
foreach (var (weh, a) in ent.Comp.Interfaces)
|
||||
{
|
||||
dataCopy[weh] = new InterfaceData(a);
|
||||
}
|
||||
|
||||
args.State = new UserInterfaceComponent.UserInterfaceComponentState(actors, ent.Comp.States, dataCopy);
|
||||
args.State = new UserInterfaceComponentState(actors, new(ent.Comp.States), dataCopy);
|
||||
|
||||
// Ensure that only the player that currently has the UI open gets to know what they have it open.
|
||||
AddActors(ent, actors, ref args);
|
||||
}
|
||||
|
||||
private void AddActors(Entity<UserInterfaceComponent> ent, Dictionary<Enum, List<NetEntity>> actors, ref ComponentGetState args)
|
||||
{
|
||||
// Ensure that only the player that currently has the UI open gets to know what they have it open.
|
||||
if (args.ReplayState)
|
||||
{
|
||||
@@ -335,106 +370,140 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
|
||||
private void OnUserInterfaceHandleState(Entity<UserInterfaceComponent> ent, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not UserInterfaceComponent.UserInterfaceComponentState state)
|
||||
Dictionary<Enum, List<NetEntity>>? stateActors = null;
|
||||
Dictionary<Enum, InterfaceData>? stateData = null;
|
||||
Dictionary<Enum, BoundUserInterfaceState>? stateStates = null;
|
||||
|
||||
if (args.Current is UserInterfaceComponentState state)
|
||||
{
|
||||
stateActors = state.Actors;
|
||||
stateData = state.Data;
|
||||
stateStates = state.States;
|
||||
}
|
||||
else if (args.Current is UserInterfaceActorsDeltaState actorDelta)
|
||||
{
|
||||
stateActors = actorDelta.Actors;
|
||||
}
|
||||
else if (args.Current is UserInterfaceStatesDeltaState stateDelta)
|
||||
{
|
||||
stateStates = stateDelta.States;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
|
||||
ent.Comp.Interfaces.Clear();
|
||||
|
||||
foreach (var data in state.Data)
|
||||
{
|
||||
ent.Comp.Interfaces[data.Key] = new(data.Value);
|
||||
}
|
||||
|
||||
foreach (var key in ent.Comp.Actors.Keys)
|
||||
// Interfaces
|
||||
if (stateData != null)
|
||||
{
|
||||
if (!state.Actors.ContainsKey(key))
|
||||
CloseUi(ent!, key);
|
||||
}
|
||||
ent.Comp.Interfaces.Clear();
|
||||
|
||||
var toRemoveActors = new ValueList<EntityUid>();
|
||||
var newSet = new HashSet<EntityUid>();
|
||||
foreach (var (key, stateActors) in state.Actors)
|
||||
{
|
||||
var actors = ent.Comp.Actors.GetOrNew(key);
|
||||
|
||||
newSet.Clear();
|
||||
foreach (var netEntity in stateActors)
|
||||
foreach (var data in stateData)
|
||||
{
|
||||
var uid = EnsureEntity<UserInterfaceComponent>(netEntity, ent.Owner);
|
||||
if (uid.IsValid())
|
||||
newSet.Add(uid);
|
||||
ent.Comp.Interfaces[data.Key] = new(data.Value);
|
||||
}
|
||||
|
||||
foreach (var actor in newSet)
|
||||
{
|
||||
if (!actors.Contains(actor))
|
||||
OpenUiInternal(ent!, key, actor);
|
||||
}
|
||||
|
||||
foreach (var actor in actors)
|
||||
{
|
||||
if (!newSet.Contains(actor))
|
||||
toRemoveActors.Add(actor);
|
||||
}
|
||||
|
||||
foreach (var actor in toRemoveActors)
|
||||
{
|
||||
CloseUiInternal(ent!, key, actor);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var key in ent.Comp.States.Keys)
|
||||
{
|
||||
if (!state.States.ContainsKey(key))
|
||||
ent.Comp.States.Remove(key);
|
||||
}
|
||||
|
||||
var attachedEnt = Player.LocalEntity;
|
||||
var clientBuis = new ValueList<Enum>(ent.Comp.ClientOpenInterfaces.Keys);
|
||||
|
||||
// Check if the UI is open by us, otherwise dispose of it.
|
||||
foreach (var key in clientBuis)
|
||||
// Actors
|
||||
if (stateActors != null)
|
||||
{
|
||||
if (ent.Comp.Actors.TryGetValue(key, out var actors) &&
|
||||
(attachedEnt == null || actors.Contains(attachedEnt.Value)))
|
||||
foreach (var key in ent.Comp.Actors.Keys)
|
||||
{
|
||||
continue;
|
||||
if (!stateActors.ContainsKey(key))
|
||||
CloseUi(ent!, key);
|
||||
}
|
||||
|
||||
var bui = ent.Comp.ClientOpenInterfaces[key];
|
||||
|
||||
if (bui.DeferredClose)
|
||||
_queuedCloses.Add(bui);
|
||||
else
|
||||
var toRemoveActors = new ValueList<EntityUid>();
|
||||
var newSet = new HashSet<EntityUid>();
|
||||
foreach (var (key, acts) in stateActors)
|
||||
{
|
||||
ent.Comp.ClientOpenInterfaces.Remove(key);
|
||||
bui.Dispose();
|
||||
var actors = ent.Comp.Actors.GetOrNew(key);
|
||||
|
||||
newSet.Clear();
|
||||
foreach (var netEntity in acts)
|
||||
{
|
||||
var uid = EnsureEntity<UserInterfaceComponent>(netEntity, ent.Owner);
|
||||
if (uid.IsValid())
|
||||
newSet.Add(uid);
|
||||
}
|
||||
|
||||
foreach (var actor in newSet)
|
||||
{
|
||||
if (!actors.Contains(actor))
|
||||
OpenUiInternal(ent!, key, actor);
|
||||
}
|
||||
|
||||
foreach (var actor in actors)
|
||||
{
|
||||
if (!newSet.Contains(actor))
|
||||
toRemoveActors.Add(actor);
|
||||
}
|
||||
|
||||
foreach (var actor in toRemoveActors)
|
||||
{
|
||||
CloseUiInternal(ent!, key, actor);
|
||||
}
|
||||
}
|
||||
|
||||
var clientBuis = new ValueList<Enum>(ent.Comp.ClientOpenInterfaces.Keys);
|
||||
|
||||
// Check if the UI is open by us, otherwise dispose of it.
|
||||
foreach (var key in clientBuis)
|
||||
{
|
||||
if (ent.Comp.Actors.TryGetValue(key, out var actors) &&
|
||||
(attachedEnt == null || actors.Contains(attachedEnt.Value)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var bui = ent.Comp.ClientOpenInterfaces[key];
|
||||
|
||||
if (bui.DeferredClose)
|
||||
_queuedCloses.Add(bui);
|
||||
else
|
||||
{
|
||||
ent.Comp.ClientOpenInterfaces.Remove(key);
|
||||
bui.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update any states we have open
|
||||
foreach (var (key, buiState) in state.States)
|
||||
// States
|
||||
if (stateStates != null)
|
||||
{
|
||||
if (ent.Comp.States.TryGetValue(key, out var existing) &&
|
||||
existing.Equals(buiState))
|
||||
foreach (var key in ent.Comp.States.Keys)
|
||||
{
|
||||
continue;
|
||||
if (!stateStates.ContainsKey(key))
|
||||
ent.Comp.States.Remove(key);
|
||||
}
|
||||
|
||||
ent.Comp.States[key] = buiState;
|
||||
// update any states we have open
|
||||
foreach (var (key, buiState) in stateStates)
|
||||
{
|
||||
if (ent.Comp.States.TryGetValue(key, out var existing) &&
|
||||
existing.Equals(buiState))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui))
|
||||
continue;
|
||||
ent.Comp.States[key] = buiState;
|
||||
|
||||
cBui.State = buiState;
|
||||
cBui.UpdateState(buiState);
|
||||
if (!ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui))
|
||||
continue;
|
||||
|
||||
cBui.State = buiState;
|
||||
cBui.UpdateState(buiState);
|
||||
cBui.Update();
|
||||
}
|
||||
}
|
||||
|
||||
// If UI not open then open it
|
||||
// If we get the first state for an ent coming in then don't open BUIs yet, just defer it until later.
|
||||
var open = ent.Comp.LifeStage > ComponentLifeStage.Added;
|
||||
|
||||
if (attachedEnt != null)
|
||||
if (attachedEnt != null && stateActors != null)
|
||||
{
|
||||
foreach (var (key, value) in ent.Comp.Interfaces)
|
||||
{
|
||||
@@ -490,6 +559,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
{
|
||||
boundUserInterface.State = buiState;
|
||||
boundUserInterface.UpdateState(buiState);
|
||||
boundUserInterface.Update();
|
||||
}
|
||||
#if EXCEPTION_TOLERANCE
|
||||
}
|
||||
@@ -669,7 +739,7 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
if (!entity.Comp.States.Remove(key))
|
||||
return;
|
||||
|
||||
Dirty(entity);
|
||||
DirtyField(entity, nameof(UserInterfaceComponent.States));
|
||||
}
|
||||
// Non-null state, check if it matches existing.
|
||||
else
|
||||
@@ -682,7 +752,14 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
stateRef = state;
|
||||
}
|
||||
|
||||
Dirty(entity);
|
||||
// Predict the change on client
|
||||
if (state != null && _netManager.IsClient && entity.Comp.ClientOpenInterfaces.TryGetValue(key, out var bui))
|
||||
{
|
||||
bui.UpdateState(state);
|
||||
bui.Update();
|
||||
}
|
||||
|
||||
DirtyField(entity, nameof(UserInterfaceComponent.States));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1024,7 +1101,19 @@ public abstract class SharedUserInterfaceSystem : EntitySystem
|
||||
ent.Comp = AddComp<UserInterfaceComponent>(ent);
|
||||
|
||||
ent.Comp.Interfaces[key] = data;
|
||||
Dirty(ent, ent.Comp);
|
||||
DirtyField(ent, nameof(UserInterfaceComponent.Interfaces));
|
||||
}
|
||||
|
||||
public bool TryGetUiState<T>(Entity<UserInterfaceComponent?> ent, Enum key, [NotNullWhen(true)] out T? state) where T : BoundUserInterfaceState
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false) || !ent.Comp.States.TryGetValue(key, out var stateComp))
|
||||
{
|
||||
state = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
state = (T)stateComp;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -30,13 +30,17 @@ using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Dynamics.Contacts;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Shared.Physics.Components;
|
||||
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class PhysicsComponent : Component
|
||||
public sealed partial class PhysicsComponent : Component, IComponentDelta
|
||||
{
|
||||
public GameTick LastFieldUpdate { get; set; }
|
||||
public GameTick[] LastModifiedFields { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Has this body been added to an island previously in this tick.
|
||||
/// </summary>
|
||||
@@ -57,10 +61,10 @@ public sealed partial class PhysicsComponent : Component
|
||||
/// </summary>
|
||||
internal readonly LinkedList<Contact> Contacts = new();
|
||||
|
||||
[DataField("ignorePaused"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public bool IgnorePaused;
|
||||
|
||||
[DataField("bodyType"), Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
|
||||
[DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
|
||||
public BodyType BodyType = BodyType.Static;
|
||||
|
||||
// We'll also block Static bodies from ever being awake given they don't need to move.
|
||||
@@ -74,13 +78,11 @@ public sealed partial class PhysicsComponent : Component
|
||||
/// body will be woken.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if sleeping is allowed; otherwise, <c>false</c>.</value>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("sleepingAllowed"),
|
||||
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
|
||||
[DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
|
||||
Other = AccessPermissions.Read)]
|
||||
public bool SleepingAllowed = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("sleepTime"),
|
||||
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
|
||||
[DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
|
||||
Other = AccessPermissions.Read)]
|
||||
public float SleepTime = 0f;
|
||||
|
||||
@@ -90,8 +92,7 @@ public sealed partial class PhysicsComponent : Component
|
||||
/// <remarks>
|
||||
/// Also known as Enabled in Box2D
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("canCollide"),
|
||||
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
|
||||
[DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
|
||||
Other = AccessPermissions.Read)]
|
||||
public bool CanCollide = true;
|
||||
|
||||
@@ -168,7 +169,7 @@ public sealed partial class PhysicsComponent : Component
|
||||
/// <summary>
|
||||
/// Is the body allowed to have angular velocity.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("fixedRotation"),
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField,
|
||||
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
|
||||
public bool FixedRotation = true;
|
||||
|
||||
@@ -189,7 +190,7 @@ public sealed partial class PhysicsComponent : Component
|
||||
/// The force is applied to the center of mass.
|
||||
/// https://en.wikipedia.org/wiki/Force
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("force"),
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField,
|
||||
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
|
||||
public Vector2 Force;
|
||||
|
||||
@@ -200,7 +201,7 @@ public sealed partial class PhysicsComponent : Component
|
||||
/// The torque rotates around the Z axis on the object.
|
||||
/// https://en.wikipedia.org/wiki/Torque
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("torque"),
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField,
|
||||
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
|
||||
public float Torque;
|
||||
|
||||
@@ -216,7 +217,7 @@ public sealed partial class PhysicsComponent : Component
|
||||
/// This is a set amount that the body's linear velocity is reduced by every tick.
|
||||
/// Combined with the tile friction.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("linearDamping"),
|
||||
[DataField,
|
||||
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
|
||||
Other = AccessPermissions.Read)]
|
||||
public float LinearDamping = 0.2f;
|
||||
@@ -226,7 +227,7 @@ public sealed partial class PhysicsComponent : Component
|
||||
/// Combined with the tile friction.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("angularDamping"),
|
||||
[DataField,
|
||||
Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute,
|
||||
Other = AccessPermissions.Read)]
|
||||
public float AngularDamping = 0.2f;
|
||||
@@ -260,7 +261,7 @@ public sealed partial class PhysicsComponent : Component
|
||||
/// <summary>
|
||||
/// The current status of the object
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("bodyStatus"), Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
|
||||
[DataField, Access(typeof(SharedPhysicsSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
|
||||
public BodyStatus BodyStatus { get; set; }
|
||||
|
||||
[ViewVariables, Access(typeof(SharedPhysicsSystem))]
|
||||
|
||||
@@ -1,35 +1,97 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Robust.Shared.Physics.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Average use-case of only linear velocity update
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public readonly record struct PhysicsComponentState(
|
||||
bool CanCollide,
|
||||
bool SleepingAllowed,
|
||||
bool FixedRotation,
|
||||
BodyStatus Status,
|
||||
Vector2 LinearVelocity,
|
||||
float AngularVelocity,
|
||||
BodyType BodyType,
|
||||
float Friction,
|
||||
float LinearDamping,
|
||||
float AngularDamping)
|
||||
: IComponentState
|
||||
public record struct PhysicsLinearVelocityDeltaState : IComponentDeltaState<PhysicsComponentState>
|
||||
{
|
||||
public readonly bool CanCollide = CanCollide;
|
||||
public readonly bool SleepingAllowed = SleepingAllowed;
|
||||
public readonly bool FixedRotation = FixedRotation;
|
||||
public readonly BodyStatus Status = Status;
|
||||
public Vector2 LinearVelocity;
|
||||
|
||||
public readonly Vector2 LinearVelocity = LinearVelocity;
|
||||
public readonly float AngularVelocity = AngularVelocity;
|
||||
public readonly BodyType BodyType = BodyType;
|
||||
public void ApplyToFullState(PhysicsComponentState fullState)
|
||||
{
|
||||
fullState.LinearVelocity = LinearVelocity;
|
||||
}
|
||||
|
||||
public readonly float Friction = Friction;
|
||||
public readonly float LinearDamping = LinearDamping;
|
||||
public readonly float AngularDamping = AngularDamping;
|
||||
public PhysicsComponentState CreateNewFullState(PhysicsComponentState fullState)
|
||||
{
|
||||
var copy = new PhysicsComponentState(fullState)
|
||||
{
|
||||
LinearVelocity = LinearVelocity,
|
||||
};
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2nd-most typical usecase of just velocity updates
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public record struct PhysicsVelocityDeltaState : IComponentDeltaState<PhysicsComponentState>
|
||||
{
|
||||
public Vector2 LinearVelocity;
|
||||
public float AngularVelocity;
|
||||
|
||||
public void ApplyToFullState(PhysicsComponentState fullState)
|
||||
{
|
||||
fullState.LinearVelocity = LinearVelocity;
|
||||
fullState.AngularVelocity = AngularVelocity;
|
||||
}
|
||||
|
||||
public PhysicsComponentState CreateNewFullState(PhysicsComponentState fullState)
|
||||
{
|
||||
var copy = new PhysicsComponentState(fullState)
|
||||
{
|
||||
LinearVelocity = LinearVelocity,
|
||||
AngularVelocity = AngularVelocity
|
||||
};
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PhysicsComponentState : IComponentState
|
||||
{
|
||||
public bool CanCollide;
|
||||
public bool SleepingAllowed;
|
||||
public bool FixedRotation;
|
||||
public BodyStatus Status;
|
||||
|
||||
public Vector2 LinearVelocity;
|
||||
public float AngularVelocity;
|
||||
public BodyType BodyType;
|
||||
|
||||
public float Friction;
|
||||
public float LinearDamping;
|
||||
public float AngularDamping;
|
||||
|
||||
public Vector2 Force;
|
||||
public float Torque;
|
||||
|
||||
public PhysicsComponentState() {}
|
||||
|
||||
public PhysicsComponentState(PhysicsComponentState existing)
|
||||
{
|
||||
CanCollide = existing.CanCollide;
|
||||
SleepingAllowed = existing.SleepingAllowed;
|
||||
FixedRotation = existing.FixedRotation;
|
||||
Status = existing.Status;
|
||||
|
||||
LinearVelocity = existing.LinearVelocity;
|
||||
AngularVelocity = existing.AngularVelocity;
|
||||
BodyType = existing.BodyType;
|
||||
|
||||
Friction = existing.Friction;
|
||||
LinearDamping = existing.LinearDamping;
|
||||
AngularDamping = existing.AngularDamping;
|
||||
|
||||
Force = existing.Force;
|
||||
Torque = existing.Torque;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -87,8 +87,8 @@ internal record struct Polygon : IPhysShape
|
||||
|
||||
if (hull.Count < 3)
|
||||
{
|
||||
Vertices = Array.Empty<Vector2>();
|
||||
Normals = Array.Empty<Vector2>();
|
||||
Vertices = [];
|
||||
Normals = [];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
736
Robust.Shared/Physics/Systems/RayCastSystem.Geometry.cs
Normal file
736
Robust.Shared/Physics/Systems/RayCastSystem.Geometry.cs
Normal 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;
|
||||
}
|
||||
465
Robust.Shared/Physics/Systems/RayCastSystem.cs
Normal file
465
Robust.Shared/Physics/Systems/RayCastSystem.cs
Normal 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);
|
||||
@@ -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!;
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
@@ -69,39 +71,97 @@ public partial class SharedPhysicsSystem
|
||||
|
||||
private void OnPhysicsGetState(EntityUid uid, PhysicsComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new PhysicsComponentState(
|
||||
component.CanCollide,
|
||||
component.SleepingAllowed,
|
||||
component.FixedRotation,
|
||||
component.BodyStatus,
|
||||
component.LinearVelocity,
|
||||
component.AngularVelocity,
|
||||
component.BodyType,
|
||||
component._friction,
|
||||
component.LinearDamping,
|
||||
component.AngularDamping);
|
||||
if (args.FromTick > component.CreationTick && component.LastFieldUpdate >= args.FromTick)
|
||||
{
|
||||
var slowPath = false;
|
||||
|
||||
for (var i = 0; i < _angularVelocityIndex; i++)
|
||||
{
|
||||
var field = component.LastModifiedFields[i];
|
||||
|
||||
if (field < args.FromTick)
|
||||
continue;
|
||||
|
||||
slowPath = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// We can do a smaller delta with no list index overhead.
|
||||
if (!slowPath)
|
||||
{
|
||||
var angularDirty = component.LastModifiedFields[_angularVelocityIndex] >= args.FromTick;
|
||||
|
||||
if (angularDirty)
|
||||
{
|
||||
args.State = new PhysicsVelocityDeltaState()
|
||||
{
|
||||
AngularVelocity = component.AngularVelocity,
|
||||
LinearVelocity = component.LinearVelocity,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
args.State = new PhysicsLinearVelocityDeltaState()
|
||||
{
|
||||
LinearVelocity = component.LinearVelocity,
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
args.State = new PhysicsComponentState
|
||||
{
|
||||
CanCollide = component.CanCollide,
|
||||
SleepingAllowed = component.SleepingAllowed,
|
||||
FixedRotation = component.FixedRotation,
|
||||
Status = component.BodyStatus,
|
||||
LinearVelocity = component.LinearVelocity,
|
||||
AngularVelocity = component.AngularVelocity,
|
||||
BodyType = component.BodyType,
|
||||
Friction = component._friction,
|
||||
LinearDamping = component.LinearDamping,
|
||||
AngularDamping = component.AngularDamping,
|
||||
Force = component.Force,
|
||||
Torque = component.Torque,
|
||||
};
|
||||
}
|
||||
|
||||
private void OnPhysicsHandleState(EntityUid uid, PhysicsComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not PhysicsComponentState newState)
|
||||
if (args.Current == null)
|
||||
return;
|
||||
|
||||
SetSleepingAllowed(uid, component, newState.SleepingAllowed);
|
||||
SetFixedRotation(uid, newState.FixedRotation, body: component);
|
||||
SetCanCollide(uid, newState.CanCollide, body: component);
|
||||
component.BodyStatus = newState.Status;
|
||||
|
||||
// So transform doesn't apply MapId in the HandleComponentState because ??? so MapId can still be 0.
|
||||
// Fucking kill me, please. You have no idea deep the rabbit hole of shitcode goes to make this work.
|
||||
TryComp<FixturesComponent>(uid, out var manager);
|
||||
_fixturesQuery.TryComp(uid, out var manager);
|
||||
|
||||
SetLinearVelocity(uid, newState.LinearVelocity, body: component, manager: manager);
|
||||
SetAngularVelocity(uid, newState.AngularVelocity, body: component, manager: manager);
|
||||
SetBodyType(uid, newState.BodyType, manager, component);
|
||||
SetFriction(uid, component, newState.Friction);
|
||||
SetLinearDamping(uid, component, newState.LinearDamping);
|
||||
SetAngularDamping(uid, component, newState.AngularDamping);
|
||||
if (args.Current is PhysicsLinearVelocityDeltaState linearState)
|
||||
{
|
||||
SetLinearVelocity(uid, linearState.LinearVelocity, body: component, manager: manager);
|
||||
}
|
||||
else if (args.Current is PhysicsVelocityDeltaState velocityState)
|
||||
{
|
||||
SetLinearVelocity(uid, velocityState.LinearVelocity, body: component, manager: manager);
|
||||
SetAngularVelocity(uid, velocityState.AngularVelocity, body: component, manager: manager);
|
||||
}
|
||||
else if (args.Current is PhysicsComponentState newState)
|
||||
{
|
||||
SetSleepingAllowed(uid, component, newState.SleepingAllowed);
|
||||
SetFixedRotation(uid, newState.FixedRotation, body: component);
|
||||
SetCanCollide(uid, newState.CanCollide, body: component);
|
||||
component.BodyStatus = newState.Status;
|
||||
|
||||
SetLinearVelocity(uid, newState.LinearVelocity, body: component, manager: manager);
|
||||
SetAngularVelocity(uid, newState.AngularVelocity, body: component, manager: manager);
|
||||
SetBodyType(uid, newState.BodyType, manager, component);
|
||||
SetFriction(uid, component, newState.Friction);
|
||||
SetLinearDamping(uid, component, newState.LinearDamping);
|
||||
SetAngularDamping(uid, component, newState.AngularDamping);
|
||||
component.Force = newState.Force;
|
||||
component.Torque = newState.Torque;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -132,7 +192,6 @@ public partial class SharedPhysicsSystem
|
||||
|
||||
body.Force += force;
|
||||
body.Torque += Vector2Helpers.Cross(point - body._localCenter, force);
|
||||
Dirty(uid, body);
|
||||
}
|
||||
|
||||
public void ApplyForce(EntityUid uid, Vector2 force, FixturesComponent? manager = null, PhysicsComponent? body = null)
|
||||
@@ -143,7 +202,6 @@ public partial class SharedPhysicsSystem
|
||||
}
|
||||
|
||||
body.Force += force;
|
||||
Dirty(uid, body);
|
||||
}
|
||||
|
||||
public void ApplyTorque(EntityUid uid, float torque, FixturesComponent? manager = null, PhysicsComponent? body = null)
|
||||
@@ -154,7 +212,7 @@ public partial class SharedPhysicsSystem
|
||||
}
|
||||
|
||||
body.Torque += torque;
|
||||
Dirty(uid, body);
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.Torque));
|
||||
}
|
||||
|
||||
public void ApplyLinearImpulse(EntityUid uid, Vector2 impulse, FixturesComponent? manager = null, PhysicsComponent? body = null)
|
||||
@@ -212,34 +270,29 @@ public partial class SharedPhysicsSystem
|
||||
/// </summary>
|
||||
public void ResetDynamics(EntityUid uid, PhysicsComponent body, bool dirty = true)
|
||||
{
|
||||
var updated = false;
|
||||
|
||||
if (body.Torque != 0f)
|
||||
{
|
||||
body.Torque = 0f;
|
||||
updated = true;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.Torque));
|
||||
}
|
||||
|
||||
if (body.AngularVelocity != 0f)
|
||||
{
|
||||
body.AngularVelocity = 0f;
|
||||
updated = true;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
|
||||
}
|
||||
|
||||
if (body.Force != Vector2.Zero)
|
||||
{
|
||||
body.Force = Vector2.Zero;
|
||||
updated = true;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.Force));
|
||||
}
|
||||
|
||||
if (body.LinearVelocity != Vector2.Zero)
|
||||
{
|
||||
body.LinearVelocity = Vector2.Zero;
|
||||
updated = true;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
|
||||
}
|
||||
|
||||
if (updated && dirty)
|
||||
Dirty(uid, body);
|
||||
}
|
||||
|
||||
public void ResetMassData(EntityUid uid, FixturesComponent? manager = null, PhysicsComponent? body = null)
|
||||
@@ -275,7 +328,6 @@ public partial class SharedPhysicsSystem
|
||||
if (((int) body.BodyType & (int) (BodyType.Kinematic | BodyType.Static)) != 0)
|
||||
{
|
||||
body._localCenter = Vector2.Zero;
|
||||
Dirty(uid, body);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -309,8 +361,13 @@ public partial class SharedPhysicsSystem
|
||||
body._localCenter = localCenter;
|
||||
|
||||
// Update center of mass velocity.
|
||||
body.LinearVelocity += Vector2Helpers.Cross(body.AngularVelocity, localCenter - oldCenter);
|
||||
Dirty(uid, body);
|
||||
var comVelocityDiff = Vector2Helpers.Cross(body.AngularVelocity, localCenter - oldCenter);
|
||||
|
||||
if (comVelocityDiff != Vector2.Zero)
|
||||
{
|
||||
body.LinearVelocity += comVelocityDiff;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
|
||||
}
|
||||
|
||||
if (body._mass == oldMass && body._inertia == oldInertia && oldCenter == localCenter)
|
||||
return;
|
||||
@@ -340,9 +397,7 @@ public partial class SharedPhysicsSystem
|
||||
return false;
|
||||
|
||||
body.AngularVelocity = value;
|
||||
|
||||
if (dirty)
|
||||
Dirty(uid, body);
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -368,10 +423,7 @@ public partial class SharedPhysicsSystem
|
||||
return false;
|
||||
|
||||
body.LinearVelocity = velocity;
|
||||
|
||||
if (dirty)
|
||||
Dirty(uid, body);
|
||||
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -381,9 +433,7 @@ public partial class SharedPhysicsSystem
|
||||
return;
|
||||
|
||||
body.AngularDamping = value;
|
||||
|
||||
if (dirty)
|
||||
Dirty(uid, body);
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.AngularDamping));
|
||||
}
|
||||
|
||||
public void SetLinearDamping(EntityUid uid, PhysicsComponent body, float value, bool dirty = true)
|
||||
@@ -392,9 +442,7 @@ public partial class SharedPhysicsSystem
|
||||
return;
|
||||
|
||||
body.LinearDamping = value;
|
||||
|
||||
if (dirty)
|
||||
Dirty(uid, body);
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearDamping));
|
||||
}
|
||||
|
||||
[Obsolete("Use SetAwake with EntityUid<PhysicsComponent>")]
|
||||
@@ -446,7 +494,6 @@ public partial class SharedPhysicsSystem
|
||||
}
|
||||
|
||||
UpdateMapAwakeState(uid, body);
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
public void TrySetBodyType(EntityUid uid, BodyType value, FixturesComponent? manager = null, PhysicsComponent? body = null, TransformComponent? xform = null)
|
||||
@@ -474,8 +521,18 @@ public partial class SharedPhysicsSystem
|
||||
if (body.BodyType == BodyType.Static)
|
||||
{
|
||||
SetAwake((uid, body), false);
|
||||
body.LinearVelocity = Vector2.Zero;
|
||||
body.AngularVelocity = 0.0f;
|
||||
|
||||
if (body.LinearVelocity != Vector2.Zero)
|
||||
{
|
||||
body.LinearVelocity = Vector2.Zero;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.LinearVelocity));
|
||||
}
|
||||
|
||||
if (body.AngularVelocity != 0f)
|
||||
{
|
||||
body.AngularVelocity = 0f;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
|
||||
}
|
||||
}
|
||||
// Even if it's dynamic if it can't collide then don't force it awake.
|
||||
else if (body.CanCollide)
|
||||
@@ -483,8 +540,11 @@ public partial class SharedPhysicsSystem
|
||||
SetAwake((uid, body), true);
|
||||
}
|
||||
|
||||
body.Force = Vector2.Zero;
|
||||
body.Torque = 0.0f;
|
||||
if (body.Torque != 0f)
|
||||
{
|
||||
body.Torque = 0f;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.Torque));
|
||||
}
|
||||
|
||||
_broadphase.RegenerateContacts(uid, body, manager, xform);
|
||||
|
||||
@@ -501,9 +561,7 @@ public partial class SharedPhysicsSystem
|
||||
return;
|
||||
|
||||
body.BodyStatus = status;
|
||||
|
||||
if (dirty)
|
||||
Dirty(uid, body);
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.BodyStatus));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -554,10 +612,7 @@ public partial class SharedPhysicsSystem
|
||||
var ev = new CollisionChangeEvent(uid, body, value);
|
||||
RaiseLocalEvent(ref ev);
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
Dirty(uid, body);
|
||||
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.CanCollide));
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -567,11 +622,15 @@ public partial class SharedPhysicsSystem
|
||||
return;
|
||||
|
||||
body.FixedRotation = value;
|
||||
body.AngularVelocity = 0.0f;
|
||||
ResetMassData(uid, manager: manager, body: body);
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.FixedRotation));
|
||||
|
||||
if (dirty)
|
||||
Dirty(uid, body);
|
||||
if (body.AngularVelocity != 0f)
|
||||
{
|
||||
body.AngularVelocity = 0.0f;
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.AngularVelocity));
|
||||
}
|
||||
|
||||
ResetMassData(uid, manager: manager, body: body);
|
||||
}
|
||||
|
||||
public void SetFriction(EntityUid uid, PhysicsComponent body, float value, bool dirty = true)
|
||||
@@ -580,9 +639,7 @@ public partial class SharedPhysicsSystem
|
||||
return;
|
||||
|
||||
body._friction = value;
|
||||
|
||||
if (dirty)
|
||||
Dirty(uid, body);
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.Friction));
|
||||
}
|
||||
|
||||
public void SetInertia(EntityUid uid, PhysicsComponent body, float value, bool dirty = true)
|
||||
@@ -598,9 +655,7 @@ public partial class SharedPhysicsSystem
|
||||
body._inertia = value - body.Mass * Vector2.Dot(body._localCenter, body._localCenter);
|
||||
DebugTools.Assert(body._inertia > 0.0f);
|
||||
body.InvI = 1.0f / body._inertia;
|
||||
|
||||
if (dirty)
|
||||
Dirty(uid, body);
|
||||
// Not networked
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,6 +666,7 @@ public partial class SharedPhysicsSystem
|
||||
if (value.EqualsApprox(body._localCenter)) return;
|
||||
|
||||
body._localCenter = value;
|
||||
// Not networked
|
||||
}
|
||||
|
||||
public void SetSleepingAllowed(EntityUid uid, PhysicsComponent body, bool value, bool dirty = true)
|
||||
@@ -622,9 +678,7 @@ public partial class SharedPhysicsSystem
|
||||
SetAwake((uid, body), true);
|
||||
|
||||
body.SleepingAllowed = value;
|
||||
|
||||
if (dirty)
|
||||
Dirty(uid, body);
|
||||
DirtyField(uid, body, nameof(PhysicsComponent.SleepingAllowed));
|
||||
}
|
||||
|
||||
public void SetSleepTime(PhysicsComponent body, float value)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -74,10 +74,32 @@ namespace Robust.Shared.Physics.Systems
|
||||
protected EntityQuery<PhysicsMapComponent> PhysMapQuery;
|
||||
protected EntityQuery<MapComponent> MapQuery;
|
||||
|
||||
private ComponentRegistration _physicsReg = default!;
|
||||
private byte _angularVelocityIndex;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_physicsReg = EntityManager.ComponentFactory.GetRegistration(CompIdx.Index<PhysicsComponent>());
|
||||
|
||||
// If you update this then update the delta state + GetState + HandleState!
|
||||
EntityManager.ComponentFactory.RegisterNetworkedFields(_physicsReg,
|
||||
nameof(PhysicsComponent.CanCollide),
|
||||
nameof(PhysicsComponent.BodyStatus),
|
||||
nameof(PhysicsComponent.BodyType),
|
||||
nameof(PhysicsComponent.SleepingAllowed),
|
||||
nameof(PhysicsComponent.FixedRotation),
|
||||
nameof(PhysicsComponent.Friction),
|
||||
nameof(PhysicsComponent.Force),
|
||||
nameof(PhysicsComponent.Torque),
|
||||
nameof(PhysicsComponent.LinearDamping),
|
||||
nameof(PhysicsComponent.AngularDamping),
|
||||
nameof(PhysicsComponent.AngularVelocity),
|
||||
nameof(PhysicsComponent.LinearVelocity));
|
||||
|
||||
_angularVelocityIndex = 10;
|
||||
|
||||
_fixturesQuery = GetEntityQuery<FixturesComponent>();
|
||||
PhysicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace Robust.Shared.Serialization.Manager
|
||||
{
|
||||
foreach (var child in _reflectionManager.GetAllChildren(type))
|
||||
{
|
||||
if (child.IsAbstract || child.IsGenericTypeDefinition)
|
||||
if (child.IsAbstract || child.IsGenericTypeDefinition || child.IsInterface)
|
||||
continue;
|
||||
|
||||
yield return child;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -49,18 +49,18 @@ namespace Robust.UnitTesting.Client.GameObjects.Components
|
||||
var childTrans = entMan.GetComponent<TransformComponent>(child);
|
||||
ComponentHandleState handleState;
|
||||
|
||||
var compState = new TransformComponentState(new Vector2(5, 5), new Angle(0), entMan.GetNetEntity(gridIdB), false, false, true);
|
||||
var compState = new TransformComponentState(new Vector2(5, 5), new Angle(0), entMan.GetNetEntity(gridIdB), false, false);
|
||||
handleState = new ComponentHandleState(compState, null);
|
||||
xformSystem.OnHandleState(parent, parentTrans, ref handleState);
|
||||
|
||||
compState = new TransformComponentState(new Vector2(6, 6), new Angle(0), entMan.GetNetEntity(gridIdB), false, false, true);
|
||||
compState = new TransformComponentState(new Vector2(6, 6), new Angle(0), entMan.GetNetEntity(gridIdB), false, false);
|
||||
handleState = new ComponentHandleState(compState, null);
|
||||
xformSystem.OnHandleState(child, childTrans, ref handleState);
|
||||
// World pos should be 6, 6 now.
|
||||
|
||||
// Act
|
||||
var oldWpos = xformSystem.GetWorldPosition(childTrans);
|
||||
compState = new TransformComponentState(new Vector2(1, 1), new Angle(0), entMan.GetNetEntity(parent), false, false, true);
|
||||
compState = new TransformComponentState(new Vector2(1, 1), new Angle(0), entMan.GetNetEntity(parent), false, false);
|
||||
handleState = new ComponentHandleState(compState, null);
|
||||
xformSystem.OnHandleState(child, childTrans, ref handleState);
|
||||
var newWpos = xformSystem.GetWorldPosition(childTrans);
|
||||
@@ -98,15 +98,15 @@ namespace Robust.UnitTesting.Client.GameObjects.Components
|
||||
var node2Trans = entMan.GetComponent<TransformComponent>(node2);
|
||||
var node3Trans = entMan.GetComponent<TransformComponent>(node3);
|
||||
|
||||
var compState = new TransformComponentState(new Vector2(6, 6), Angle.FromDegrees(135), entMan.GetNetEntity(gridIdB), false, false, true);
|
||||
var compState = new TransformComponentState(new Vector2(6, 6), Angle.FromDegrees(135), entMan.GetNetEntity(gridIdB), false, false);
|
||||
var handleState = new ComponentHandleState(compState, null);
|
||||
xformSystem.OnHandleState(node1, node1Trans, ref handleState);
|
||||
|
||||
compState = new TransformComponentState(new Vector2(1, 1), Angle.FromDegrees(45), entMan.GetNetEntity(node1), false, false, true);
|
||||
compState = new TransformComponentState(new Vector2(1, 1), Angle.FromDegrees(45), entMan.GetNetEntity(node1), false, false);
|
||||
handleState = new ComponentHandleState(compState, null);
|
||||
xformSystem.OnHandleState(node2, node2Trans, ref handleState);
|
||||
|
||||
compState = new TransformComponentState(new Vector2(0, 0), Angle.FromDegrees(45), entMan.GetNetEntity(node2), false, false, true);
|
||||
compState = new TransformComponentState(new Vector2(0, 0), Angle.FromDegrees(45), entMan.GetNetEntity(node2), false, false);
|
||||
handleState = new ComponentHandleState(compState, null);
|
||||
xformSystem.OnHandleState(node3, node3Trans, ref handleState);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user