mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
merge remote wizden/master
This commit is contained in:
commit
cf0741df48
604 changed files with 53875 additions and 44913 deletions
42
.github/ISSUE_TEMPLATE/heisentest_template.md
vendored
Normal file
42
.github/ISSUE_TEMPLATE/heisentest_template.md
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
title: "{{ env.NAME }} test failure on main ({{ env.FULLNAME }})"
|
||||
labels: autogenerated
|
||||
---
|
||||
## Test Failure On Main
|
||||
|
||||
A test failure has been detected on the main branch. This should never occur.
|
||||
|
||||
> [!NOTE]
|
||||
> This issue is automatically generated by a github action and will be updated when this failure reoccurs.
|
||||
> Do not edit the body of this issue.
|
||||
|
||||
## Failure Details
|
||||
<dl>
|
||||
<dt>Test name</dt>
|
||||
<dd>{{ env.NAME }}</dd>
|
||||
<dt>Fully qualified name</dt>
|
||||
<dd>{{ env.FULLNAME }}</dd>
|
||||
<dt>Last encountered</dt>
|
||||
<dd>{{ date | date('dddd, MMMM Do') }}</dd>
|
||||
</dl>
|
||||
|
||||
<details>
|
||||
<summary>Failure message</summary>
|
||||
|
||||
```
|
||||
{{ env.FAILURE }}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Test output</summary>
|
||||
|
||||
```
|
||||
{{ env.OUTPUT }}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Using this issue
|
||||
Please place troubleshooting notes in the comments of this issue.
|
||||
9
.github/config.yml
vendored
9
.github/config.yml
vendored
|
|
@ -1,9 +0,0 @@
|
|||
Project-Condor:
|
||||
org: space-wizards
|
||||
project: 2
|
||||
inbox: Inbox
|
||||
labels:
|
||||
"W: In Progress": "In Progress"
|
||||
"W: Discussion": "Design and Discussion"
|
||||
"W: Backlog": "Backlog"
|
||||
"W: Next": "Next"
|
||||
47
.github/workflows/benchmarks.yml
vendored
47
.github/workflows/benchmarks.yml
vendored
|
|
@ -1,47 +0,0 @@
|
|||
name: Benchmarks
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 8 * * *'
|
||||
|
||||
concurrency: benchmarks
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
name: Run Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
- name: Get Engine version
|
||||
run: |
|
||||
cd RobustToolbox
|
||||
git fetch --depth=1
|
||||
echo "::set-output name=out::$(git rev-parse HEAD)"
|
||||
id: engine_version
|
||||
- name: Run script on centcomm
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: centcomm.spacestation14.io
|
||||
username: robust-benchmark-runner
|
||||
key: ${{ secrets.CENTCOMM_ROBUST_BENCHMARK_RUNNER_KEY }}
|
||||
command_timeout: 100000m
|
||||
script: |
|
||||
mkdir benchmark_run_content_${{ github.sha }}
|
||||
cd benchmark_run_content_${{ github.sha }}
|
||||
git clone https://github.com/space-wizards/space-station-14.git repo_dir --recursive
|
||||
cd repo_dir
|
||||
git checkout ${{ github.sha }}
|
||||
cd Content.Benchmarks
|
||||
dotnet restore
|
||||
export ROBUST_BENCHMARKS_ENABLE_SQL=1
|
||||
export ROBUST_BENCHMARKS_SQL_ADDRESS="${{ secrets.BENCHMARKS_WRITE_ADDRESS }}"
|
||||
export ROBUST_BENCHMARKS_SQL_PORT="${{ secrets.BENCHMARKS_WRITE_PORT }}"
|
||||
export ROBUST_BENCHMARKS_SQL_USER="${{ secrets.BENCHMARKS_WRITE_USER }}"
|
||||
export ROBUST_BENCHMARKS_SQL_PASSWORD="${{ secrets.BENCHMARKS_WRITE_PASSWORD }}"
|
||||
export ROBUST_BENCHMARKS_SQL_DATABASE="content_benchmarks"
|
||||
export GITHUB_SHA="${{ github.sha }}"
|
||||
dotnet run --filter '*' --configuration Release
|
||||
cd ../../..
|
||||
rm -rf benchmark_run_content_${{ github.sha }}
|
||||
41
.github/workflows/build-docfx.yml
vendored
41
.github/workflows/build-docfx.yml
vendored
|
|
@ -1,41 +0,0 @@
|
|||
name: Build & Publish Docfx
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 0"
|
||||
|
||||
jobs:
|
||||
docfx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Setup submodule
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
- name: Pull engine updates
|
||||
uses: space-wizards/submodule-dependency@v0.1.5
|
||||
- name: Update Engine Submodules
|
||||
run: |
|
||||
cd RobustToolbox/
|
||||
git submodule update --init --recursive
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build Project
|
||||
run: dotnet build --no-restore
|
||||
|
||||
- name: Build DocFX
|
||||
uses: nikeee/docfx-action@v1.0.0
|
||||
with:
|
||||
args: Content.Docfx/docfx.json
|
||||
|
||||
- name: Publish Docfx Documentation on GitHub Pages
|
||||
uses: maxheld83/ghpages@master
|
||||
env:
|
||||
BUILD_DIR: Content.Docfx/_content-site
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
87
.github/workflows/heisendetector.yml
vendored
Normal file
87
.github/workflows/heisendetector.yml
vendored
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
name: Hunt for Heisentests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# every 15 minutes
|
||||
schedule:
|
||||
- cron: "*/15 * * * *"
|
||||
|
||||
jobs:
|
||||
hunt:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.get_fails.outputs.matrix }}
|
||||
count: ${{ steps.get_fails.outputs.count }}
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build Project
|
||||
run: dotnet build --configuration DebugOpt --no-restore /m
|
||||
|
||||
- name: Run Content.Tests
|
||||
id: tests
|
||||
shell: pwsh
|
||||
run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0 NUnit.TestOutputXml="logs" NUnit.WorkDirectory="$(pwd)/test_results"
|
||||
|
||||
- name: Run Content.IntegrationTests
|
||||
id: integration_tests
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:DOTNET_gcServer=1
|
||||
dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed NUnit.TestOutputXml="logs" NUnit.WorkDirectory="$(pwd)/test_results"
|
||||
|
||||
- name: Archive NUnit3 test results.
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nunit3-results
|
||||
path: test_results/*
|
||||
retention-days: 7
|
||||
compression-level: 9
|
||||
|
||||
- name: Extract Failures
|
||||
if: failure()
|
||||
id: get_fails
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install xmltodict
|
||||
python Tools/actions_extract_failures.py
|
||||
|
||||
report:
|
||||
needs: [ hunt ]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
strategy:
|
||||
matrix:
|
||||
value: ${{ fromJSON(needs.hunt.outputs.matrix) }}
|
||||
if: ${{ always() && failure() && (needs.hunt.outputs.count < 5) }}
|
||||
name: Error for ${{ matrix.value.name }}
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Create Issue
|
||||
uses: dblock/create-a-github-issue@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NAME: ${{ matrix.value.name }}
|
||||
FULLNAME: ${{ matrix.value.fullname }}
|
||||
OUTPUT: ${{ matrix.value.output }}
|
||||
FAILURE: ${{ matrix.value.failure }}
|
||||
with:
|
||||
filename: .github/ISSUE_TEMPLATE/heisentest_template.md
|
||||
update_existing: true
|
||||
search_existing: all
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -315,6 +315,9 @@ Resources/MapImages
|
|||
|
||||
*.bak
|
||||
|
||||
# Shader error output
|
||||
error.glsl
|
||||
|
||||
# Direnv stuff
|
||||
.direnv/
|
||||
|
||||
|
|
|
|||
196
Content.Benchmarks/EffectDispatchBenchmark.cs
Normal file
196
Content.Benchmarks/EffectDispatchBenchmark.cs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Content.IntegrationTests;
|
||||
using Content.IntegrationTests.Pair;
|
||||
using Content.Shared.EntityEffects;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Benchmarks;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark comparing EntityEffect dispatch strategies.
|
||||
/// Old: event bus SubscribeLocalEvent + RaiseLocalEvent
|
||||
/// New: static Dictionary{{Type, IEntityEffectHandler}} + direct interface call
|
||||
/// </summary>
|
||||
[Virtual]
|
||||
public partial class EffectDispatchBenchmark
|
||||
{
|
||||
private TestPair _pair = default!;
|
||||
private BenchSystem _sys = default!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
ProgramShared.PathOffset = "../../../../";
|
||||
PoolManager.Startup(typeof(BenchSystem).Assembly);
|
||||
_pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult();
|
||||
|
||||
var entMan = _pair.Server.EntMan;
|
||||
_sys = entMan.System<BenchSystem>();
|
||||
|
||||
_pair.Server.WaitPost(() =>
|
||||
{
|
||||
var hitUid = entMan.Spawn();
|
||||
entMan.AddComponent<DummyComponent>(hitUid);
|
||||
_sys.HitTarget = new(hitUid, entMan.GetComponent<TransformComponent>(hitUid));
|
||||
|
||||
var missUid = entMan.Spawn();
|
||||
_sys.MissTarget = new(missUid, entMan.GetComponent<TransformComponent>(missUid));
|
||||
|
||||
})
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
|
||||
[GlobalCleanup]
|
||||
public async Task Cleanup()
|
||||
{
|
||||
await _pair.DisposeAsync();
|
||||
PoolManager.Shutdown();
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public int EventBusDispatch_Hit()
|
||||
{
|
||||
return _sys.RaiseViaEventBusHit();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int EventBusDispatch_Miss()
|
||||
{
|
||||
return _sys.RaiseViaEventBusMiss();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int CurrentImplementation_Hit()
|
||||
{
|
||||
return _sys.RaiseViaCurrentImplementation();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int CurrentImplementation_Miss()
|
||||
{
|
||||
return _sys.RaiseViaCurrentImplementationMiss();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int CSharpEventDispatch_Hit()
|
||||
{
|
||||
return _sys.RaiseViaCSharpEventHit();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int CSharpEventDispatch_Miss()
|
||||
{
|
||||
return _sys.RaiseViaCSharpEventHit();
|
||||
}
|
||||
|
||||
public sealed partial class BenchSystem : EntitySystem
|
||||
{
|
||||
private SharedEntityEffectsSystem _effectsSystem = default!;
|
||||
|
||||
public Entity<TransformComponent> HitTarget;
|
||||
public Entity<TransformComponent> MissTarget;
|
||||
|
||||
private EntityEffect _effect = new TestEffect();
|
||||
|
||||
private int _counter;
|
||||
|
||||
public delegate void EffectHandler(EntityUid uid);
|
||||
|
||||
public event EffectHandler OnEffect;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DummyComponent, TestEffectEvent>(OnEventBus);
|
||||
|
||||
OnEffect += OnCSharpEvent;
|
||||
|
||||
_effectsSystem = EntityManager.System<SharedEntityEffectsSystem>();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void OnEventBus(Entity<DummyComponent> entity, ref TestEffectEvent args)
|
||||
{
|
||||
_counter++;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void OnCSharpEvent(EntityUid uid)
|
||||
{
|
||||
TryComp<DummyComponent>(uid, out _);
|
||||
_counter++;
|
||||
}
|
||||
|
||||
public int RaiseViaEventBusHit()
|
||||
{
|
||||
_counter = 0;
|
||||
var ev = new TestEffectEvent();
|
||||
RaiseLocalEvent(HitTarget.Owner, ref ev);
|
||||
return _counter;
|
||||
}
|
||||
|
||||
public int RaiseViaEventBusMiss()
|
||||
{
|
||||
_counter = 0;
|
||||
var ev = new TestEffectEvent();
|
||||
RaiseLocalEvent(MissTarget.Owner, ref ev);
|
||||
return _counter;
|
||||
}
|
||||
|
||||
public int RaiseViaCurrentImplementation()
|
||||
{
|
||||
_counter = 0;
|
||||
_effectsSystem.ApplyEffect(HitTarget, _effect);
|
||||
return _counter;
|
||||
}
|
||||
|
||||
public int RaiseViaCurrentImplementationMiss()
|
||||
{
|
||||
_counter = 0;
|
||||
_effectsSystem.ApplyEffect(MissTarget, _effect);
|
||||
return _counter;
|
||||
}
|
||||
|
||||
public int RaiseViaCSharpEventHit()
|
||||
{
|
||||
_counter = 0;
|
||||
OnEffect?.Invoke(HitTarget.Owner);
|
||||
return _counter;
|
||||
}
|
||||
|
||||
public int RaiseViaCSharpEventMiss()
|
||||
{
|
||||
_counter = 0;
|
||||
OnEffect?.Invoke(MissTarget.Owner);
|
||||
return _counter;
|
||||
}
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public struct TestEffectEvent
|
||||
{
|
||||
}
|
||||
|
||||
public sealed partial class TestEffectSystem : EntityEffectSystem<DummyComponent, TestEffect>
|
||||
{
|
||||
protected override void Effect(Entity<DummyComponent> entity, ref EntityEffectEvent<TestEffect> args)
|
||||
{
|
||||
}
|
||||
}
|
||||
public sealed partial class TestEffect : EntityEffectBase<TestEffect>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class DummyComponent : Component
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ using Robust.Shared.GameObjects;
|
|||
namespace Content.Benchmarks;
|
||||
|
||||
[Virtual]
|
||||
public class RaiseEventBenchmark
|
||||
public partial class RaiseEventBenchmark
|
||||
{
|
||||
private TestPair _pair = default!;
|
||||
private BenchSystem _sys = default!;
|
||||
|
|
@ -35,6 +35,48 @@ public class RaiseEventBenchmark
|
|||
_sys.Ent2 = new(_sys.Ent.Owner, _sys.Ent.Comp);
|
||||
_sys.NetId = fact.GetRegistration<TransformComponent>().NetID!.Value;
|
||||
_sys.EvSubs = bus.GetNetCompEventHandlers<BenchSystem.BenchEv>();
|
||||
|
||||
var id2 = entMan.Spawn();
|
||||
entMan.AddComponent<BenchSystem.Bench1Component>(id2);
|
||||
entMan.AddComponent<BenchSystem.Bench2Component>(id2);
|
||||
_sys.Ent2Comps = id2;
|
||||
|
||||
var id4 = entMan.Spawn();
|
||||
entMan.AddComponent<BenchSystem.Bench1Component>(id4);
|
||||
entMan.AddComponent<BenchSystem.Bench2Component>(id4);
|
||||
entMan.AddComponent<BenchSystem.Bench3Component>(id4);
|
||||
entMan.AddComponent<BenchSystem.Bench4Component>(id4);
|
||||
_sys.Ent4Comps = id4;
|
||||
|
||||
var id8 = entMan.Spawn();
|
||||
entMan.AddComponent<BenchSystem.Bench1Component>(id8);
|
||||
entMan.AddComponent<BenchSystem.Bench2Component>(id8);
|
||||
entMan.AddComponent<BenchSystem.Bench3Component>(id8);
|
||||
entMan.AddComponent<BenchSystem.Bench4Component>(id8);
|
||||
entMan.AddComponent<BenchSystem.Bench5Component>(id8);
|
||||
entMan.AddComponent<BenchSystem.Bench6Component>(id8);
|
||||
entMan.AddComponent<BenchSystem.Bench7Component>(id8);
|
||||
entMan.AddComponent<BenchSystem.Bench8Component>(id8);
|
||||
_sys.Ent8Comps = id8;
|
||||
|
||||
var id16 = entMan.Spawn();
|
||||
entMan.AddComponent<BenchSystem.Bench1Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench2Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench3Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench4Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench5Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench6Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench7Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench8Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench9Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench10Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench11Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench12Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench13Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench14Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench15Component>(id16);
|
||||
entMan.AddComponent<BenchSystem.Bench16Component>(id16);
|
||||
_sys.Ent16Comps = id16;
|
||||
})
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
|
@ -48,9 +90,33 @@ public class RaiseEventBenchmark
|
|||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public int RaiseEvent()
|
||||
public int RaiseEvent1()
|
||||
{
|
||||
return _sys.RaiseEvent();
|
||||
return _sys.RaiseEvent1();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int RaiseEvent2()
|
||||
{
|
||||
return _sys.RaiseEvent2();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int RaiseEvent4()
|
||||
{
|
||||
return _sys.RaiseEvent4();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int RaiseEvent8()
|
||||
{
|
||||
return _sys.RaiseEvent8();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int RaiseEvent16()
|
||||
{
|
||||
return _sys.RaiseEvent16();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
|
|
@ -77,10 +143,14 @@ public class RaiseEventBenchmark
|
|||
return _sys.CSharpEvent();
|
||||
}
|
||||
|
||||
public sealed class BenchSystem : EntitySystem
|
||||
public sealed partial class BenchSystem : EntitySystem
|
||||
{
|
||||
public Entity<TransformComponent> Ent;
|
||||
public Entity<IComponent> Ent2;
|
||||
public EntityUid Ent2Comps;
|
||||
public EntityUid Ent4Comps;
|
||||
public EntityUid Ent8Comps;
|
||||
public EntityUid Ent16Comps;
|
||||
|
||||
public delegate void EntityEventHandler(EntityUid uid, TransformComponent comp, ref BenchEv ev);
|
||||
|
||||
|
|
@ -92,16 +162,60 @@ public class RaiseEventBenchmark
|
|||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<TransformComponent, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench1Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench2Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench3Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench4Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench5Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench6Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench7Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench8Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench9Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench10Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench11Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench12Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench13Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench14Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench15Component, BenchEv>(OnEvent);
|
||||
SubscribeLocalEvent<Bench16Component, BenchEv>(OnEvent);
|
||||
OnCSharpEvent += OnEvent;
|
||||
}
|
||||
|
||||
public int RaiseEvent()
|
||||
public int RaiseEvent1()
|
||||
{
|
||||
var ev = new BenchEv();
|
||||
RaiseLocalEvent(Ent.Owner, ref ev);
|
||||
return ev.N;
|
||||
}
|
||||
|
||||
public int RaiseEvent2()
|
||||
{
|
||||
var ev = new BenchEv();
|
||||
RaiseLocalEvent(Ent2Comps, ref ev);
|
||||
return ev.N;
|
||||
}
|
||||
|
||||
public int RaiseEvent4()
|
||||
{
|
||||
var ev = new BenchEv();
|
||||
RaiseLocalEvent(Ent4Comps, ref ev);
|
||||
return ev.N;
|
||||
}
|
||||
|
||||
public int RaiseEvent8()
|
||||
{
|
||||
var ev = new BenchEv();
|
||||
RaiseLocalEvent(Ent8Comps, ref ev);
|
||||
return ev.N;
|
||||
}
|
||||
|
||||
public int RaiseEvent16()
|
||||
{
|
||||
var ev = new BenchEv();
|
||||
RaiseLocalEvent(Ent16Comps, ref ev);
|
||||
return ev.N;
|
||||
}
|
||||
|
||||
public int RaiseCompEvent()
|
||||
{
|
||||
var ev = new BenchEv();
|
||||
|
|
@ -134,7 +248,7 @@ public class RaiseEventBenchmark
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void OnEvent(EntityUid uid, TransformComponent component, ref BenchEv args)
|
||||
private void OnEvent<T>(EntityUid uid, T component, ref BenchEv args)
|
||||
{
|
||||
args.N += uid.Id;
|
||||
}
|
||||
|
|
@ -145,5 +259,53 @@ public class RaiseEventBenchmark
|
|||
{
|
||||
public int N;
|
||||
}
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench1Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench2Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench3Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench4Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench5Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench6Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench7Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench8Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench9Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench10Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench11Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench12Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench13Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench14Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench15Component : Component;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class Bench16Component : Component;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Linq;
|
||||
using System.Linq;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Administration;
|
||||
|
|
@ -59,7 +59,7 @@ internal sealed partial class AdminQuickInfoSystem : EntitySystem
|
|||
|
||||
public void OpenPopupFor(NetEntity[] entities)
|
||||
{
|
||||
var vBox = new VBox();
|
||||
var vBox = new VBox() { SeparationOverride = 6 };;
|
||||
var popup = new Popup
|
||||
{
|
||||
Children =
|
||||
|
|
@ -74,9 +74,19 @@ internal sealed partial class AdminQuickInfoSystem : EntitySystem
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
var first = true;
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
vBox.AddChild(new PanelContainer
|
||||
{
|
||||
StyleClasses = { StyleClass.LowDivider },
|
||||
HorizontalExpand = true,
|
||||
});
|
||||
first = false;
|
||||
}
|
||||
|
||||
var playerInfo = _adminSystem.PlayerList.FirstOrDefault(p => p.NetEntity == entity);
|
||||
var control = new InfoControl(this, entity, playerInfo);
|
||||
popup.OnPopupHide += () => control.Unsubscribe();
|
||||
|
|
@ -92,19 +102,21 @@ internal sealed partial class AdminQuickInfoSystem : EntitySystem
|
|||
{
|
||||
private readonly AdminQuickInfoSystem _system;
|
||||
private readonly NetEntity _entity;
|
||||
private readonly PlayerInfo? _playerInfo;
|
||||
private readonly bool _activeControl;
|
||||
private PlayerInfo? _playerInfo;
|
||||
private QuickInfoShared.SingleEntityInfo? _response;
|
||||
|
||||
private readonly RichTextLabel _contents = new();
|
||||
|
||||
private ILocalizationManager Loc => _system.Loc;
|
||||
private AdminSystem Admin => _system._adminSystem;
|
||||
|
||||
public InfoControl(AdminQuickInfoSystem system, NetEntity entity, PlayerInfo? playerInfo)
|
||||
{
|
||||
_system = system;
|
||||
_entity = entity;
|
||||
_playerInfo = playerInfo;
|
||||
|
||||
_activeControl = (playerInfo != null);
|
||||
AddChild(_contents);
|
||||
|
||||
system.EntityResponseReceived += OnEntityResponseReceived;
|
||||
|
|
@ -119,6 +131,7 @@ internal sealed partial class AdminQuickInfoSystem : EntitySystem
|
|||
return;
|
||||
|
||||
_response = ev;
|
||||
_playerInfo ??= Admin.PlayerList.FirstOrDefault(p => p.SessionId == ev.LastPlayer);
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +144,10 @@ internal sealed partial class AdminQuickInfoSystem : EntitySystem
|
|||
private void Rebuild()
|
||||
{
|
||||
var sb = new FormattedStringBuilder();
|
||||
if (!_activeControl)
|
||||
{
|
||||
sb.AppendMarkupLine(Loc.GetString("admin-quick-info-inactive"));
|
||||
}
|
||||
|
||||
if (_playerInfo != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -106,13 +106,35 @@ public sealed partial class NoteEdit : FancyWindow
|
|||
{
|
||||
PermanentCheckBox.Pressed = false;
|
||||
UpdatePermanentCheckboxFields();
|
||||
ExpiryLineEdit.Text = ExpiryTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
var timeLeft = ConvertDateToTimeFromNow(ExpiryTime.Value.ToLocalTime());
|
||||
|
||||
ExpiryLineEdit.Text = Math.Round(timeLeft.Item2, 2).ToString();
|
||||
ExpiryLengthDropdown.SelectId((int)timeLeft.Item1);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateSubmitButton();
|
||||
}
|
||||
|
||||
// Convert the given date time into a multiplier and value.
|
||||
// This is for having a simple format like 2 weeks instead of everything being in hours.
|
||||
// For example, a 2 weeks old date would return (Multipliers.Days, 14)
|
||||
private (Multipliers, double) ConvertDateToTimeFromNow(DateTime expirationDate)
|
||||
{
|
||||
var deltaTime = expirationDate - DateTime.Now;
|
||||
|
||||
if (deltaTime.TotalMinutes <= 0)
|
||||
return (Multipliers.Minutes, 0.0);
|
||||
|
||||
return deltaTime.TotalDays switch
|
||||
{
|
||||
< 1 => (Multipliers.Minutes, deltaTime.TotalMinutes), // Less than a day
|
||||
< 365 => (Multipliers.Days, deltaTime.TotalDays), // Less than a year
|
||||
_ => (Multipliers.Months, deltaTime.TotalDays / 30) // More than a year
|
||||
};
|
||||
}
|
||||
|
||||
private void OnSubmitButtonMouseEntered(GUIMouseHoverEventArgs args)
|
||||
{
|
||||
if (!SubmitButton.Disabled)
|
||||
|
|
@ -300,7 +322,7 @@ public sealed partial class NoteEdit : FancyWindow
|
|||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ExpiryLineEdit.Text) || !uint.TryParse(ExpiryLineEdit.Text, out var inputInt))
|
||||
if (string.IsNullOrWhiteSpace(ExpiryLineEdit.Text) || !double.TryParse(ExpiryLineEdit.Text, out var inputDouble) || inputDouble < 0)
|
||||
{
|
||||
ExpiryLineEdit.ModulateSelfOverride = Color.Red;
|
||||
return false;
|
||||
|
|
@ -317,7 +339,8 @@ public sealed partial class NoteEdit : FancyWindow
|
|||
(int) Multipliers.Centuries => TimeSpan.FromDays(36525).TotalMinutes,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(ExpiryLengthDropdown.SelectedId), "Multiplier out of range :(")
|
||||
};
|
||||
ExpiryTime = DateTime.UtcNow.AddMinutes(inputInt * mult);
|
||||
|
||||
ExpiryTime = DateTime.UtcNow.AddMinutes(inputDouble * mult);
|
||||
ExpiryLineEdit.ModulateSelfOverride = null;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -339,4 +362,4 @@ public sealed partial class NoteEdit : FancyWindow
|
|||
|
||||
SubmitPressed = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ using Robust.Client.Player;
|
|||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using static Content.Client.Administration.UI.Tabs.PlayerTab.PlayerTabHeader;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ public sealed partial class PlayerTab : Control
|
|||
[Dependency] private IEntityManager _entManager = default!;
|
||||
[Dependency] private IConfigurationManager _config = default!;
|
||||
[Dependency] private IPlayerManager _playerMan = default!;
|
||||
[Dependency] private IPrototypeManager _proto = default!;
|
||||
|
||||
private const string ArrowUp = "↑";
|
||||
private const string ArrowDown = "↓";
|
||||
|
|
@ -155,7 +157,12 @@ public sealed partial class PlayerTab : Control
|
|||
UpdateHeaderSymbols();
|
||||
|
||||
SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
|
||||
$"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
|
||||
$"{info.Username} " +
|
||||
$"{info.CharacterName} " +
|
||||
$"{info.IdentityName} " +
|
||||
$"{info.StartingJob} " +
|
||||
$"{(info.Subtype is not null ? Loc.GetString(info.Subtype) : string.Empty)} " +
|
||||
$"{(_proto.TryIndex(info.RoleProto, out var proto) ? Loc.GetString(proto.Name) : string.Empty)}"))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
<BoxContainer Orientation="Vertical" SeparationOverride="8">
|
||||
<GridContainer Columns="4">
|
||||
<controls:ConfirmButton Name="ServerShutdownButton" Text="{Loc server-shutdown}" />
|
||||
<cc:CommandButton Name="SetOocButton" Command="setooc" Text="{Loc server-ooc-toggle}" ToggleMode="True" />
|
||||
<cc:CommandButton Name="SetLoocButton" Command="setlooc" Text="{Loc server-looc-toggle}" ToggleMode="True" />
|
||||
<cc:CommandButton Name="SetDeadChatButton" Command="setdeadchat" Text="{Loc server-dead-chat-toggle}" ToggleMode="True" />
|
||||
<cc:CommandButton Name="SetOocButton" Command="togglechat OOC" Text="{Loc server-ooc-toggle}" ToggleMode="True" />
|
||||
<cc:CommandButton Name="SetLoocButton" Command="togglechat LOOC" Text="{Loc server-looc-toggle}" ToggleMode="True" />
|
||||
<cc:CommandButton Name="SetDeadChatButton" Command="togglechat Dead" Text="{Loc server-dead-chat-toggle}" ToggleMode="True" />
|
||||
</GridContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Content.Client.DisplacementMap;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Anomaly.Effects;
|
||||
using Content.Shared.Humanoid;
|
||||
|
|
@ -8,6 +9,9 @@ namespace Content.Client.Anomaly.Effects;
|
|||
public sealed partial class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
||||
{
|
||||
[Dependency] private SpriteSystem _sprite = default!;
|
||||
[Dependency] private DisplacementMapSystem _displacement = default!;
|
||||
|
||||
[Dependency] private EntityQuery<InnerBodyAnomalyVisualsComponent> _visualsQuery = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -37,6 +41,22 @@ public sealed partial class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomal
|
|||
|
||||
_sprite.LayerSetVisible((ent.Owner, sprite), index, true);
|
||||
sprite.LayerSetShader(index, "unshaded");
|
||||
|
||||
if (_visualsQuery.TryGetComponent(ent, out var visuals) && visuals.Displacement != null)
|
||||
{
|
||||
if (ProtoMan.Resolve(visuals.Displacement, out var displacement))
|
||||
{
|
||||
_displacement.TryAddDisplacement(displacement.Displacement,
|
||||
(ent.Owner, sprite),
|
||||
index,
|
||||
ent.Comp.LayerMap,
|
||||
out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
_displacement.EnsureDisplacementIsNotOnSprite((ent.Owner, sprite), ent.Comp.LayerMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCompShutdown(Entity<InnerBodyAnomalyComponent> ent, ref ComponentShutdown args)
|
||||
|
|
@ -46,5 +66,7 @@ public sealed partial class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomal
|
|||
|
||||
var index = _sprite.LayerMapGet((ent.Owner, sprite), ent.Comp.LayerMap);
|
||||
_sprite.LayerSetVisible((ent.Owner, sprite), index, false);
|
||||
|
||||
_displacement.EnsureDisplacementIsNotOnSprite((ent.Owner, sprite), ent.Comp.LayerMap);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
using Content.Shared.DisplacementMap;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Atmos.Components;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -39,4 +42,10 @@ public sealed partial class FireVisualsComponent : Component
|
|||
/// the burning entity as entities don't support having multiple point-lights.
|
||||
/// </summary>
|
||||
public EntityUid? LightEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the currently applied displacement, so that only new changes update the layer.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<DisplacementDataPrototype>? CurrentDisplacement;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
using Content.Client.Atmos.Components;
|
||||
using Content.Client.DisplacementMap;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.DisplacementMap;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
|
|
@ -12,6 +14,7 @@ namespace Content.Client.Atmos.EntitySystems;
|
|||
public sealed partial class FireVisualizerSystem : VisualizerSystem<FireVisualsComponent>
|
||||
{
|
||||
[Dependency] private PointLightSystem _lights = default!;
|
||||
[Dependency] private DisplacementMapSystem _displacement = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -65,6 +68,7 @@ public sealed partial class FireVisualizerSystem : VisualizerSystem<FireVisualsC
|
|||
|
||||
AppearanceSystem.TryGetData<bool>(uid, FireVisuals.OnFire, out var onFire, appearance);
|
||||
AppearanceSystem.TryGetData<float>(uid, FireVisuals.FireStacks, out var fireStacks, appearance);
|
||||
AppearanceSystem.TryGetData<string?>(uid, FireVisuals.FireDisplacement, out var fireDisplacement, appearance);
|
||||
SpriteSystem.LayerSetVisible((uid, sprite), index, onFire);
|
||||
|
||||
if (!onFire)
|
||||
|
|
@ -83,6 +87,16 @@ public sealed partial class FireVisualizerSystem : VisualizerSystem<FireVisualsC
|
|||
else
|
||||
SpriteSystem.LayerSetRsiState((uid, sprite), index, component.NormalState);
|
||||
|
||||
if (component.CurrentDisplacement != fireDisplacement)
|
||||
{
|
||||
if (fireDisplacement != null && ProtoMan.Resolve<DisplacementDataPrototype>(fireDisplacement, out var displacementProto))
|
||||
_displacement.TryAddDisplacement(displacementProto.Displacement, (uid, sprite), index, FireVisualLayers.Fire, out _);
|
||||
else
|
||||
_displacement.EnsureDisplacementIsNotOnSprite((uid, sprite), FireVisualLayers.Fire);
|
||||
|
||||
component.CurrentDisplacement = fireDisplacement;
|
||||
}
|
||||
|
||||
component.LightEntity ??= Spawn(null, new EntityCoordinates(uid, default));
|
||||
var light = EnsureComp<PointLightComponent>(component.LightEntity.Value);
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ public sealed partial class GasTileHeatBlurOverlay : Overlay
|
|||
_heatGradientTexture = _resourceCache.GetTexture("/Textures/Effects/HeatBlur/soft_circle.png");
|
||||
|
||||
_shader = _proto.Index(HeatOverlayShader).InstanceUnique();
|
||||
_configManager.OnValueChanged(CCVars.ReducedMotion, SetReducedMotion, invokeImmediately: true);
|
||||
_configManager.OnValueChanged(CCVars.DisableHeatDistortion, SetReducedMotion, invokeImmediately: true);
|
||||
}
|
||||
|
||||
private void SetReducedMotion(bool reducedMotion)
|
||||
|
|
@ -227,7 +227,7 @@ public sealed partial class GasTileHeatBlurOverlay : Overlay
|
|||
{
|
||||
_resources.Dispose();
|
||||
|
||||
_configManager.UnsubValueChanged(CCVars.ReducedMotion, SetReducedMotion);
|
||||
_configManager.UnsubValueChanged(CCVars.DisableHeatDistortion, SetReducedMotion);
|
||||
base.DisposeBehavior();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ public sealed partial class ContentAudioSystem
|
|||
{
|
||||
[Dependency] private IConfigurationManager _configManager = default!;
|
||||
[Dependency] private IGameTiming _timing = default!;
|
||||
[Dependency] private ILogManager _logManager = default!;
|
||||
[Dependency] private IPlayerManager _player = default!;
|
||||
[Dependency] private IRobustRandom _random = default!;
|
||||
[Dependency] private IStateManager _state = default!;
|
||||
|
|
@ -59,7 +58,7 @@ public sealed partial class ContentAudioSystem
|
|||
private void InitializeAmbientMusic()
|
||||
{
|
||||
Subs.CVar(_configManager, CCVars.AmbientMusicVolume, AmbienceCVarChanged, true);
|
||||
_sawmill = _logManager.GetSawmill("audio.ambience");
|
||||
_sawmill = LogManager.GetSawmill("audio.ambience");
|
||||
|
||||
// Reset audio
|
||||
_nextAudio = TimeSpan.MaxValue;
|
||||
|
|
|
|||
201
Content.Client/Audio/Midi/MidiFileCollectionManager.cs
Normal file
201
Content.Client/Audio/Midi/MidiFileCollectionManager.cs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Audio.Midi;
|
||||
|
||||
/// <summary>
|
||||
/// Handles storage/management of MIDI files stored inside the user data directory.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public sealed partial class MidiFileCollectionManager : IPostInjectInit
|
||||
{
|
||||
/// <summary>
|
||||
/// Directory path to use inside UserData for storing MIDIs.
|
||||
/// </summary>
|
||||
private static readonly ResPath UserMidiDirectory = new("/UserMidis/");
|
||||
|
||||
private const string SawmillCategory = "midifilecollection";
|
||||
|
||||
[Dependency] private IResourceManager _resManager = default!;
|
||||
[Dependency] private ILogManager _logManager = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private readonly List<ResPath> _filePaths = [];
|
||||
|
||||
/// <summary>
|
||||
/// Raised after a MIDI file has been added to the library. Contains added file path as argument.
|
||||
/// </summary>
|
||||
public event Action<ResPath>? MidiFileAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Raised after a MIDI file has been removed from the library. Contains removed file path as argument.
|
||||
/// </summary>
|
||||
public event Action<ResPath>? MidiFileRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Raised after more than one or two library changes occured at once. (i.e. initial load or when all items deleted)
|
||||
/// </summary>
|
||||
public event Action? MidiFilesReset;
|
||||
|
||||
///<inheritdoc cref="IPostInjectInit"/>
|
||||
public void PostInject()
|
||||
{
|
||||
EnsureMidiDirectoryExists();
|
||||
ReloadLibrary();
|
||||
_sawmill = _logManager.GetSawmill(SawmillCategory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the binary content of the given MIDI file.
|
||||
/// </summary>
|
||||
/// <param name="filePath">MIDI file path to get.</param>
|
||||
/// <returns>MIDI binary as a byte array or an empty byte array if the file doesn't exist.</returns>
|
||||
public byte[] GetMidiData(ResPath filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fullPath = UserMidiDirectory / filePath;
|
||||
return _resManager.UserData.ReadAllBytes(fullPath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Failed to read MIDI data from '{filePath}': {e.Message}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an enumeration of all available MIDI files.
|
||||
/// </summary>
|
||||
/// <returns>An enumeration of MIDI file paths.</returns>
|
||||
public IEnumerable<ResPath> GetMidiFiles()
|
||||
{
|
||||
return _filePaths;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the given byte stream with the given file path inside the <see cref="UserMidiDirectory"/> directory.
|
||||
/// </summary>
|
||||
/// <param name="filePath">File path to write.</param>
|
||||
/// <param name="data">Binary data to write.</param>
|
||||
/// <remarks>Raises <see cref="MidiFileAdded"/> on success.</remarks>
|
||||
public async Task<bool> AddMidiFile(ResPath filePath, Stream data)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var file = _resManager.UserData.OpenWrite(UserMidiDirectory / filePath);
|
||||
await data.CopyToAsync(file);
|
||||
_filePaths.Add(filePath);
|
||||
MidiFileAdded?.Invoke(filePath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Failed to store MIDI file '{filePath}' in library: {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the given byte array with the given file path inside the <see cref="UserMidiDirectory"/> directory.
|
||||
/// </summary>
|
||||
/// <param name="filePath">File path to write.</param>
|
||||
/// <param name="data">Binary data to write.</param>
|
||||
/// <remarks>Raises <see cref="MidiFileAdded"/> on success.</remarks>
|
||||
public async Task<bool> AddMidiFile(ResPath filePath, byte[] data)
|
||||
{
|
||||
return await AddMidiFile(filePath, new MemoryStream(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renames a MIDI file inside the library.
|
||||
/// </summary>
|
||||
/// <param name="oldPath">Current file path</param>
|
||||
/// <param name="newPath">New file path</param>
|
||||
/// <remarks>Raises <see cref="MidiFileRemoved"/> and <see cref="MidiFileAdded"/> on success.</remarks>
|
||||
public bool RenameMidiFile(ResPath oldPath, ResPath newPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fullOldPath = UserMidiDirectory / oldPath;
|
||||
var fullNewPath = UserMidiDirectory / newPath;
|
||||
fullOldPath = fullOldPath.Clean();
|
||||
fullNewPath = fullNewPath.Clean();
|
||||
_resManager.UserData.Rename(fullOldPath, fullNewPath);
|
||||
_filePaths.Remove(oldPath);
|
||||
MidiFileRemoved?.Invoke(oldPath);
|
||||
_filePaths.Add(newPath);
|
||||
MidiFileAdded?.Invoke(newPath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Failed to rename MIDI file '{oldPath}' with '{newPath}': {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Permanently removes the given MIDI file if it exists inside <see cref="UserMidiDirectory"/>.
|
||||
/// </summary>
|
||||
/// <param name="filePath">File path to remove.</param>
|
||||
/// <remarks>Raises <see cref="MidiFileRemoved"/></remarks>
|
||||
public void RemoveMidiFile(ResPath filePath)
|
||||
{
|
||||
DeleteMidiFile(filePath);
|
||||
_filePaths.Remove(filePath);
|
||||
MidiFileRemoved?.Invoke(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all registered MIDI files, permanently.
|
||||
/// </summary>
|
||||
/// <remarks>Raises <see cref="MidiFilesReset"/></remarks>
|
||||
public void RemoveAllMidiFiles()
|
||||
{
|
||||
foreach (var path in _filePaths)
|
||||
{
|
||||
DeleteMidiFile(path);
|
||||
}
|
||||
|
||||
_filePaths.Clear();
|
||||
MidiFilesReset?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears and reloads the entire MIDI library.
|
||||
/// </summary>
|
||||
public void ReloadLibrary()
|
||||
{
|
||||
_filePaths.Clear();
|
||||
if (!_resManager.UserData.IsDir(UserMidiDirectory))
|
||||
return;
|
||||
|
||||
foreach (var path in _resManager.UserData.DirectoryEntries(UserMidiDirectory))
|
||||
{
|
||||
var filePath = new ResPath(UserMidiDirectory + path);
|
||||
if (!filePath.Extension.Equals("midi") && !filePath.Extension.Equals("mid"))
|
||||
continue;
|
||||
|
||||
_filePaths.Add(new ResPath(path));
|
||||
}
|
||||
|
||||
MidiFilesReset?.Invoke();
|
||||
}
|
||||
|
||||
private void DeleteMidiFile(ResPath filePath)
|
||||
{
|
||||
var path = (UserMidiDirectory / filePath).Clean();
|
||||
_resManager.UserData.Delete(path);
|
||||
}
|
||||
|
||||
private void EnsureMidiDirectoryExists()
|
||||
{
|
||||
if (!_resManager.UserData.Exists(UserMidiDirectory))
|
||||
_resManager.UserData.CreateDir(UserMidiDirectory);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ using System.Linq;
|
|||
using Content.Client.DisplacementMap;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.DisplacementMap;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid;
|
||||
using Robust.Client.GameObjects;
|
||||
|
|
@ -73,6 +74,16 @@ public sealed partial class VisualBodySystem : SharedVisualBodySystem
|
|||
return;
|
||||
|
||||
_sprite.LayerSetData(target, index, ent.Comp.Data);
|
||||
|
||||
var displacement = ent.Comp.Displacement;
|
||||
if (displacement != null && ProtoMan.Resolve(displacement, out var displacementProto))
|
||||
{
|
||||
_displacement.TryAddDisplacement(displacementProto.Displacement,
|
||||
(target, Comp<SpriteComponent>(target)),
|
||||
index,
|
||||
ent.Comp.Layer,
|
||||
out _);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveVisual(Entity<VisualOrganComponent> ent, EntityUid target)
|
||||
|
|
@ -81,6 +92,8 @@ public sealed partial class VisualBodySystem : SharedVisualBodySystem
|
|||
return;
|
||||
|
||||
_sprite.LayerSetRsiState(target, index, RSI.StateId.Invalid);
|
||||
|
||||
_displacement.EnsureDisplacementIsNotOnSprite((target, Comp<SpriteComponent>(target)), ent.Comp.Layer);
|
||||
}
|
||||
|
||||
private void OnMarkingsGotInserted(Entity<VisualOrganMarkingsComponent> ent, ref OrganGotInsertedEvent args)
|
||||
|
|
@ -183,6 +196,7 @@ public sealed partial class VisualBodySystem : SharedVisualBodySystem
|
|||
|
||||
ent.Comp.MarkingsDisplacement.TryGetValue(proto.BodyPart, out var displacement);
|
||||
|
||||
var numDisplacements = 0;
|
||||
for (var i = 0; i < proto.Sprites.Count; i++)
|
||||
{
|
||||
var sprite = proto.Sprites[i];
|
||||
|
|
@ -195,7 +209,14 @@ public sealed partial class VisualBodySystem : SharedVisualBodySystem
|
|||
|
||||
if (!_sprite.LayerMapTryGet(target, layerId, out _, false))
|
||||
{
|
||||
var spriteLayer = _sprite.AddLayer(target, sprite, index + i + 1);
|
||||
// Having three separate indices and a magic +1 is cursed, but:
|
||||
// - index refers to the index of the organ the marking is applied to
|
||||
// - i is the current sprite of the marking that is being applied
|
||||
// - numDisplacements tracks how many displacements have been applied, and is
|
||||
// an additional offset to ensure that the order of the base sprites is correct
|
||||
// after inserting a displacement layer
|
||||
// - The +1 ensures that markings render on top of the base organ
|
||||
var spriteLayer = _sprite.AddLayer(target, sprite, index + i + numDisplacements + 1);
|
||||
_sprite.LayerMapSet(target, layerId, spriteLayer);
|
||||
_sprite.LayerSetSprite(target, layerId, rsi);
|
||||
}
|
||||
|
|
@ -206,7 +227,18 @@ public sealed partial class VisualBodySystem : SharedVisualBodySystem
|
|||
_sprite.LayerSetColor(target, layerId, Color.White);
|
||||
|
||||
if (displacement != null && proto.CanBeDisplaced)
|
||||
_displacement.TryAddDisplacement(displacement, (target, target.Comp), index + i + 1, layerId, out _);
|
||||
{
|
||||
_displacement.TryAddDisplacement(
|
||||
displacement,
|
||||
(target, target.Comp),
|
||||
// Similar logic as above, but this makes the displacement layer go below the
|
||||
// original sprite. So it should be all the displacements, then all the sprite layers on top
|
||||
index + i + 1,
|
||||
layerId,
|
||||
out _
|
||||
);
|
||||
numDisplacements++;
|
||||
}
|
||||
}
|
||||
|
||||
applied.Add(marking);
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ namespace Content.Client.CardboardBox;
|
|||
public sealed partial class CardboardBoxSystem : SharedCardboardBoxSystem
|
||||
{
|
||||
[Dependency] private EntityLookupSystem _entityLookup = default!;
|
||||
[Dependency] private TransformSystem _transform = default!;
|
||||
[Dependency] private ExamineSystemShared _examine = default!;
|
||||
[Dependency] private SpriteSystem _sprite = default!;
|
||||
[Dependency] private EntityQuery<MobStateComponent> _mobStateQuery = default!;
|
||||
[Dependency] private TransformSystem _transform = default!;
|
||||
|
||||
[Dependency] private EntityQuery<MobStateComponent> _mobStateQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -33,13 +34,13 @@ public sealed partial class CardboardBoxSystem : SharedCardboardBoxSystem
|
|||
var xform = Transform(source);
|
||||
var sourcePos = _transform.GetMapCoordinates(source, xform);
|
||||
|
||||
//Any mob that can move should be surprised?
|
||||
//God mind rework needs to come faster so it can just check for mind
|
||||
//TODO: Replace with Mind Query when mind rework is in.
|
||||
// Any mob that can move should be surprised?
|
||||
// God mind rework needs to come faster so it can just check for mind
|
||||
// TODO: Replace with Mind Query when mind rework is in.
|
||||
var mobMoverEntities = new List<EntityUid>();
|
||||
var mover = GetEntity(msg.Mover);
|
||||
|
||||
//Filter out entities in range to see that they're a mob and add them to the mobMoverEntities hash for faster lookup
|
||||
// Filter out entities in range to see that they're a mob and add them to the mobMoverEntities hash for faster lookup
|
||||
var movers = new HashSet<Entity<MobMoverComponent>>();
|
||||
_entityLookup.GetEntitiesInRange(xform.Coordinates, box.Distance, movers);
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ public sealed partial class CardboardBoxSystem : SharedCardboardBoxSystem
|
|||
mobMoverEntities.Add(uid);
|
||||
}
|
||||
|
||||
//Play the effect for the mobs as long as they can see the box and are in range.
|
||||
// Play the effect for the mobs as long as they can see the box and are in range.
|
||||
foreach (var mob in mobMoverEntities)
|
||||
{
|
||||
var mapPos = _transform.GetMapCoordinates(mob);
|
||||
|
|
@ -64,13 +65,11 @@ public sealed partial class CardboardBoxSystem : SharedCardboardBoxSystem
|
|||
continue;
|
||||
|
||||
var ent = Spawn(box.Effect, mapPos);
|
||||
|
||||
if (!TryComp(ent, out TransformComponent? entTransform) || !TryComp<SpriteComponent>(ent, out var sprite))
|
||||
continue;
|
||||
|
||||
_sprite.SetOffset((ent, sprite), new Vector2(0, 1));
|
||||
_transform.SetParent(ent, entTransform, mob);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using Content.Client.UserInterface.Fragments;
|
||||
using Content.Client.UserInterface.Fragments;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.CartridgeLoader;
|
||||
|
|
@ -67,7 +66,7 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface
|
|||
protected void ActivateCartridge(EntityUid cartridgeUid)
|
||||
{
|
||||
var message = new CartridgeLoaderUiMessage(_entManager.GetNetEntity(cartridgeUid), CartridgeUiMessageAction.Activate);
|
||||
SendMessage(message);
|
||||
SendPredictedMessage(message);
|
||||
}
|
||||
|
||||
protected void DeactivateActiveCartridge()
|
||||
|
|
@ -76,19 +75,19 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface
|
|||
return;
|
||||
|
||||
var message = new CartridgeLoaderUiMessage(_entManager.GetNetEntity(_activeProgram.Value), CartridgeUiMessageAction.Deactivate);
|
||||
SendMessage(message);
|
||||
SendPredictedMessage(message);
|
||||
}
|
||||
|
||||
protected void InstallCartridge(EntityUid cartridgeUid)
|
||||
{
|
||||
var message = new CartridgeLoaderUiMessage(_entManager.GetNetEntity(cartridgeUid), CartridgeUiMessageAction.Install);
|
||||
SendMessage(message);
|
||||
SendPredictedMessage(message);
|
||||
}
|
||||
|
||||
protected void UninstallCartridge(EntityUid cartridgeUid)
|
||||
{
|
||||
var message = new CartridgeLoaderUiMessage(_entManager.GetNetEntity(cartridgeUid), CartridgeUiMessageAction.Uninstall);
|
||||
SendMessage(message);
|
||||
SendPredictedMessage(message);
|
||||
}
|
||||
|
||||
private List<(EntityUid, CartridgeComponent)> GetCartridgeComponents(List<EntityUid> programs)
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
using Content.Shared.CartridgeLoader;
|
||||
|
||||
namespace Content.Client.CartridgeLoader;
|
||||
|
||||
public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
|
||||
{
|
||||
//Empty client system for component replication
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ public sealed partial class LogProbeUi : UIFragment
|
|||
{
|
||||
var ev = new LogProbePrintMessage();
|
||||
var message = new CartridgeUiMessage(ev);
|
||||
ui.SendMessage(message);
|
||||
ui.SendPredictedMessage(message);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
namespace Content.Client.CartridgeLoader.Cartridges;
|
||||
|
||||
public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem;
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
using System.Linq;
|
||||
using Content.Client.UserInterface.Fragments;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.CartridgeLoader.Cartridges;
|
||||
|
|
@ -44,7 +42,7 @@ public sealed partial class NanoTaskUi : UIFragment
|
|||
if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task)
|
||||
return;
|
||||
|
||||
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, new(
|
||||
userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, new(
|
||||
description: task.Data.Description,
|
||||
taskIsFor: task.Data.TaskIsFor,
|
||||
isTaskDone: !task.Data.IsTaskDone,
|
||||
|
|
@ -53,22 +51,22 @@ public sealed partial class NanoTaskUi : UIFragment
|
|||
};
|
||||
_popup.TaskSaved += (id, data) =>
|
||||
{
|
||||
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, data)))));
|
||||
userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, data)))));
|
||||
_popup.Close();
|
||||
};
|
||||
_popup.TaskDeleted += id =>
|
||||
{
|
||||
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskDeleteTask(id))));
|
||||
userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskDeleteTask(id))));
|
||||
_popup.Close();
|
||||
};
|
||||
_popup.TaskCreated += data =>
|
||||
{
|
||||
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskAddTask(data))));
|
||||
userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskAddTask(data))));
|
||||
_popup.Close();
|
||||
};
|
||||
_popup.TaskPrinted += data =>
|
||||
{
|
||||
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskPrintTask(data))));
|
||||
userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskPrintTask(data))));
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using Content.Client.UserInterface.Fragments;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.CartridgeLoader.Cartridges;
|
||||
|
|
@ -34,6 +33,6 @@ public sealed partial class NotekeeperUi : UIFragment
|
|||
{
|
||||
var notekeeperMessage = new NotekeeperUiMessageEvent(action, note);
|
||||
var message = new CartridgeUiMessage(notekeeperMessage);
|
||||
userInterface.SendMessage(message);
|
||||
userInterface.SendPredictedMessage(message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.Changeling.Systems;
|
||||
|
||||
namespace Content.Client.Changeling.Systems;
|
||||
|
||||
public sealed partial class ChangelingLastResortSystem : SharedChangelingLastResortSystem;
|
||||
|
|
@ -6,14 +6,17 @@ using Robust.Shared.Animations;
|
|||
namespace Content.Client.Chasm;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the falling animation for entities that fall into a chasm.
|
||||
/// Handles the falling animation for entities that fall into an entity with <see cref="ChasmComponent"/>.
|
||||
/// </summary>
|
||||
public sealed partial class ChasmFallingVisualsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private AnimationPlayerSystem _anim = default!;
|
||||
[Dependency] private SpriteSystem _sprite = default!;
|
||||
|
||||
private readonly string _chasmFallAnimationKey = "chasm_fall";
|
||||
[Dependency] private EntityQuery<AnimationPlayerComponent> _animationPlayerQuery;
|
||||
[Dependency] private EntityQuery<SpriteComponent> _spriteQuery;
|
||||
|
||||
private const string ChasmFallAnimationKey = "chasm_fall";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -23,60 +26,62 @@ public sealed partial class ChasmFallingVisualsSystem : EntitySystem
|
|||
SubscribeLocalEvent<ChasmFallingComponent, ComponentRemove>(OnComponentRemove);
|
||||
}
|
||||
|
||||
private void OnComponentInit(EntityUid uid, ChasmFallingComponent component, ComponentInit args)
|
||||
private void OnComponentInit(Entity<ChasmFallingComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite) ||
|
||||
TerminatingOrDeleted(uid))
|
||||
if (!_spriteQuery.TryComp(entity, out var sprite) ||
|
||||
TerminatingOrDeleted(entity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
component.OriginalScale = sprite.Scale;
|
||||
entity.Comp.OriginalScale = sprite.Scale;
|
||||
|
||||
if (!TryComp<AnimationPlayerComponent>(uid, out var player))
|
||||
return;
|
||||
|
||||
if (_anim.HasRunningAnimation(player, _chasmFallAnimationKey))
|
||||
return;
|
||||
|
||||
_anim.Play((uid, player), GetFallingAnimation(component), _chasmFallAnimationKey);
|
||||
}
|
||||
|
||||
private void OnComponentRemove(EntityUid uid, ChasmFallingComponent component, ComponentRemove args)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||
return;
|
||||
|
||||
_sprite.SetScale((uid, sprite), component.OriginalScale);
|
||||
|
||||
if (!TryComp<AnimationPlayerComponent>(uid, out var player))
|
||||
return;
|
||||
|
||||
if (_anim.HasRunningAnimation(player, _chasmFallAnimationKey))
|
||||
_anim.Stop((uid, player), _chasmFallAnimationKey);
|
||||
}
|
||||
|
||||
private Animation GetFallingAnimation(ChasmFallingComponent component)
|
||||
{
|
||||
var length = component.AnimationTime;
|
||||
|
||||
return new Animation()
|
||||
if (!_animationPlayerQuery.TryComp(entity, out var player) ||
|
||||
_anim.HasRunningAnimation(player, ChasmFallAnimationKey))
|
||||
{
|
||||
Length = length,
|
||||
return;
|
||||
}
|
||||
|
||||
_anim.Play((entity, player), GetFallingAnimation(entity.Comp), ChasmFallAnimationKey);
|
||||
}
|
||||
|
||||
private void OnComponentRemove(Entity<ChasmFallingComponent> entity, ref ComponentRemove args)
|
||||
{
|
||||
if (!_spriteQuery.TryComp(entity, out var sprite))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_sprite.SetScale((entity, sprite), entity.Comp.OriginalScale);
|
||||
|
||||
if (!_animationPlayerQuery.TryComp(entity, out var player) ||
|
||||
!_anim.HasRunningAnimation(player, ChasmFallAnimationKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_anim.Stop((entity, player), ChasmFallAnimationKey);
|
||||
}
|
||||
|
||||
private static Animation GetFallingAnimation(ChasmFallingComponent component)
|
||||
{
|
||||
return new Animation
|
||||
{
|
||||
Length = component.AnimationTime,
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackComponentProperty()
|
||||
new AnimationTrackComponentProperty
|
||||
{
|
||||
ComponentType = typeof(SpriteComponent),
|
||||
Property = nameof(SpriteComponent.Scale),
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackProperty.KeyFrame(component.OriginalScale, 0.0f),
|
||||
new AnimationTrackProperty.KeyFrame(component.AnimationScale, length.Seconds),
|
||||
new AnimationTrackProperty.KeyFrame(component.AnimationScale, component.AnimationTime.Seconds),
|
||||
},
|
||||
InterpolationMode = AnimationInterpolationMode.Cubic
|
||||
}
|
||||
}
|
||||
InterpolationMode = AnimationInterpolationMode.Cubic,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@
|
|||
</BoxContainer>
|
||||
<SplitContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
VerticalExpand="True"
|
||||
MouseFilter="Ignore">
|
||||
<ScrollContainer HScrollEnabled="False"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
|
|
|
|||
|
|
@ -13,13 +13,12 @@ namespace Content.Client.Crayon;
|
|||
public sealed partial class CrayonSystem : SharedCrayonSystem
|
||||
{
|
||||
[Dependency] private SharedChargesSystem _charges = default!;
|
||||
[Dependency] private EntityManager _entityManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Subs.ItemStatus<CrayonComponent>(ent => new StatusControl(ent, _charges, _entityManager));
|
||||
Subs.ItemStatus<CrayonComponent>(ent => new StatusControl(ent, _charges, EntityManager));
|
||||
}
|
||||
|
||||
private sealed class StatusControl : Control
|
||||
|
|
@ -43,8 +42,8 @@ public sealed partial class CrayonSystem : SharedCrayonSystem
|
|||
base.FrameUpdate(args);
|
||||
|
||||
_label.SetMarkup(Robust.Shared.Localization.Loc.GetString("crayon-drawing-label",
|
||||
("color",_crayon.Comp.Color),
|
||||
("state",_crayon.Comp.SelectedState),
|
||||
("color", _crayon.Comp.Color),
|
||||
("state", _crayon.Comp.SelectedState),
|
||||
("charges", _charges.GetCurrentCharges(_crayon.Owner)),
|
||||
("capacity", _capacity)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.DisplacementMap;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
|
|
@ -117,6 +119,13 @@ public sealed partial class DamageVisualsComponent : Component
|
|||
/// </summary>
|
||||
[DataField("damageOverlay")] public DamageVisualizerSprite? DamageOverlay;
|
||||
|
||||
/// <summary>
|
||||
/// Applies a displacement map to the damage visuals.
|
||||
/// Is set via <see cref="DamageableComponent.Displacement"/>.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public DisplacementData? Displacement;
|
||||
|
||||
public readonly List<Enum> TargetLayerMapKeys = new();
|
||||
public bool Disabled = false;
|
||||
public bool Valid = true;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
using System.Linq;
|
||||
using Content.Client.DisplacementMap;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.DisplacementMap;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
|
@ -29,6 +31,7 @@ namespace Content.Client.Damage;
|
|||
public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponent>
|
||||
{
|
||||
[Dependency] private DamageableSystem _damageable = default!;
|
||||
[Dependency] private DisplacementMapSystem _displacement = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -326,10 +329,14 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
|
|||
/// </summary>
|
||||
private void AddDamageLayerToSprite(Entity<SpriteComponent?> spriteEnt, DamageVisualizerSprite sprite, string state, string mapKey, int? index = null)
|
||||
{
|
||||
if (!Resolve(spriteEnt, ref spriteEnt.Comp))
|
||||
return;
|
||||
|
||||
var newLayer = SpriteSystem.AddLayer(
|
||||
spriteEnt,
|
||||
new SpriteSpecifier.Rsi(
|
||||
new(sprite.Sprite), state
|
||||
new(sprite.Sprite),
|
||||
state
|
||||
),
|
||||
index
|
||||
);
|
||||
|
|
@ -354,6 +361,12 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
|
|||
if (damageVisComp.Disabled)
|
||||
return;
|
||||
|
||||
if (AppearanceSystem.TryGetData<string>(uid, DamageVisualizerKeys.Displacement, out var displacement, args.Component) &&
|
||||
ProtoMan.Resolve<DisplacementDataPrototype>(displacement, out var displacementProto))
|
||||
damageVisComp.Displacement = displacementProto.Displacement;
|
||||
else
|
||||
damageVisComp.Displacement = null;
|
||||
|
||||
HandleDamage(uid, args.Component, damageVisComp);
|
||||
}
|
||||
|
||||
|
|
@ -503,7 +516,7 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
|
|||
}
|
||||
else
|
||||
{
|
||||
UpdateOverlay((entity, spriteComponent), threshold);
|
||||
UpdateOverlay((entity, spriteComponent), threshold, damageVisComp.Displacement);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -542,7 +555,7 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
|
|||
}
|
||||
else
|
||||
{
|
||||
UpdateOverlay((entity, spriteComponent, damageVisComp), damageGroup, threshold);
|
||||
UpdateOverlay((entity, spriteComponent, damageVisComp), damageGroup, threshold, entity.Comp3.Displacement);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -614,7 +627,9 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
|
|||
UpdateDamageLayerState(spriteEnt,
|
||||
spriteLayer,
|
||||
$"{layerState}",
|
||||
threshold);
|
||||
threshold,
|
||||
$"{layerMapKey}trackDamage",
|
||||
damageVisComp.Displacement);
|
||||
}
|
||||
}
|
||||
else if (!damageVisComp.Overlay)
|
||||
|
|
@ -625,7 +640,9 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
|
|||
UpdateDamageLayerState(spriteEnt,
|
||||
spriteLayer,
|
||||
$"{layerState}",
|
||||
threshold);
|
||||
threshold,
|
||||
$"{layerMapKey}",
|
||||
damageVisComp.Displacement);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -648,7 +665,9 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
|
|||
(entity, spriteComponent),
|
||||
spriteLayer,
|
||||
$"{layerState}_{damageGroup}",
|
||||
threshold);
|
||||
threshold,
|
||||
$"{layerMapKey}{damageGroup}",
|
||||
entity.Comp2.Displacement);
|
||||
}
|
||||
}
|
||||
else if (!damageVisComp.Overlay)
|
||||
|
|
@ -660,27 +679,31 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
|
|||
(entity, spriteComponent),
|
||||
spriteLayer,
|
||||
$"{layerState}_{damageGroup}",
|
||||
threshold);
|
||||
threshold,
|
||||
$"{layerMapKey}",
|
||||
entity.Comp2.Displacement);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an overlay that is tracking all damage.
|
||||
/// </summary>
|
||||
private void UpdateOverlay(Entity<SpriteComponent> spriteEnt, FixedPoint2 threshold)
|
||||
private void UpdateOverlay(Entity<SpriteComponent> spriteEnt, FixedPoint2 threshold, DisplacementData? displacement = null)
|
||||
{
|
||||
SpriteSystem.LayerMapTryGet(spriteEnt.AsNullable(), $"DamageOverlay", out var spriteLayer, false);
|
||||
|
||||
UpdateDamageLayerState(spriteEnt,
|
||||
spriteLayer,
|
||||
$"DamageOverlay",
|
||||
threshold);
|
||||
threshold,
|
||||
$"DamageOverlay",
|
||||
displacement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an overlay based on damage group.
|
||||
/// </summary>
|
||||
private void UpdateOverlay(Entity<SpriteComponent, DamageVisualsComponent> entity, string damageGroup, FixedPoint2 threshold)
|
||||
private void UpdateOverlay(Entity<SpriteComponent, DamageVisualsComponent> entity, string damageGroup, FixedPoint2 threshold, DisplacementData? displacement = null)
|
||||
{
|
||||
var spriteComponent = entity.Comp1;
|
||||
var damageVisComp = entity.Comp2;
|
||||
|
|
@ -695,7 +718,9 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
|
|||
(entity, spriteComponent),
|
||||
spriteLayer,
|
||||
$"DamageOverlay_{damageGroup}",
|
||||
threshold);
|
||||
threshold,
|
||||
$"DamageOverlay{damageGroup}",
|
||||
displacement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -706,7 +731,7 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
|
|||
/// function calls it), and what threshold
|
||||
/// was passed into it.
|
||||
/// </summary>
|
||||
private void UpdateDamageLayerState(Entity<SpriteComponent> spriteEnt, int spriteLayer, string statePrefix, FixedPoint2 threshold)
|
||||
private void UpdateDamageLayerState(Entity<SpriteComponent> spriteEnt, int spriteLayer, string statePrefix, FixedPoint2 threshold, string layerKey, DisplacementData? displacement)
|
||||
{
|
||||
if (threshold == 0)
|
||||
{
|
||||
|
|
@ -719,6 +744,14 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
|
|||
SpriteSystem.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, true);
|
||||
}
|
||||
SpriteSystem.LayerSetRsiState(spriteEnt.AsNullable(), spriteLayer, $"{statePrefix}_{threshold}");
|
||||
if (displacement != null)
|
||||
{
|
||||
_displacement.TryAddDisplacement(displacement, spriteEnt, spriteLayer, layerKey, out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
_displacement.EnsureDisplacementIsNotOnSprite(spriteEnt, layerKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ public sealed partial class DisplacementMapSystem : EntitySystem
|
|||
if (displacementKey is null)
|
||||
return false;
|
||||
|
||||
EnsureDisplacementIsNotOnSprite(sprite, key);
|
||||
if (EnsureDisplacementIsNotOnSprite(sprite, key))
|
||||
index--;
|
||||
|
||||
if (data.ShaderOverride is not null)
|
||||
{
|
||||
|
|
@ -91,10 +92,21 @@ public sealed partial class DisplacementMapSystem : EntitySystem
|
|||
|
||||
var displacementLayer = _serialization.CreateCopy(displacementDataLayer, notNullableOverride: true);
|
||||
|
||||
// This previously assigned a string reading "this is impossible" if key.ToString eval'd to false.
|
||||
// However, for the sake of sanity, we've changed this to assert non-null - !.
|
||||
// If this throws an error, we're not sorry. Nanotrasen thanks you for your service fixing this bug.
|
||||
displacementLayer.CopyToShaderParameters!.LayerKey = key.ToString()!;
|
||||
if (key is Enum)
|
||||
{
|
||||
// We are doing this enum-to-string conversion here because CopyToShaderParameters.LayerKey only takes a string,
|
||||
// but LayerMap keys are stored as objects, and therefore can take enums.
|
||||
// There is a key parser in SpriteComponent but it requires the qualified (i.e. full) enum name.
|
||||
// It feels like CopyToShaderParameters should be able to just take objects, but until then:
|
||||
displacementLayer.CopyToShaderParameters!.LayerKey = $"enum.{key.GetType().Name}.{key}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// This previously assigned a string reading "this is impossible" if key.ToString eval'd to false.
|
||||
// However, for the sake of sanity, we've changed this to assert non-null - !.
|
||||
// If this throws an error, we're not sorry. Nanotrasen thanks you for your service fixing this bug.
|
||||
displacementLayer.CopyToShaderParameters!.LayerKey = key.ToString()!;
|
||||
}
|
||||
|
||||
_sprite.AddLayer(sprite.AsNullable(), displacementLayer, index);
|
||||
_sprite.LayerMapSet(sprite.AsNullable(), displacementKey, index);
|
||||
|
|
@ -107,13 +119,13 @@ public sealed partial class DisplacementMapSystem : EntitySystem
|
|||
/// </summary>
|
||||
/// <param name="sprite">The sprite to remove the displacement layer from.</param>
|
||||
/// <param name="key">The key of the layer that is referenced by the displacement layer we want to remove.</param>
|
||||
/// <param name="logMissing">Whether to report an error if the displacement map isn't on the sprite.</param>
|
||||
public void EnsureDisplacementIsNotOnSprite(Entity<SpriteComponent> sprite, object key)
|
||||
/// <returns>Returns true if the displacement existed and was removed.</returns>
|
||||
public bool EnsureDisplacementIsNotOnSprite(Entity<SpriteComponent> sprite, object key)
|
||||
{
|
||||
var displacementLayerKey = BuildDisplacementLayerKey(key);
|
||||
if (displacementLayerKey is null)
|
||||
return;
|
||||
return false;
|
||||
|
||||
_sprite.RemoveLayer(sprite.AsNullable(), displacementLayerKey, false);
|
||||
return _sprite.RemoveLayer(sprite.AsNullable(), displacementLayerKey, false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ public sealed partial class RainbowOverlay : Overlay
|
|||
_statusEffects = _sysMan.GetEntitySystem<StatusEffectsSystem>();
|
||||
|
||||
_rainbowShader = _prototypeManager.Index(Shader).InstanceUnique();
|
||||
_config.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true);
|
||||
_config.OnValueChanged(CCVars.DisableRainbowOverlay, OnReducedMotionChanged, invokeImmediately: true);
|
||||
}
|
||||
|
||||
private void OnReducedMotionChanged(bool reducedMotion)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ public sealed partial class DrunkOverlay : Overlay
|
|||
IoCManager.InjectDependencies(this);
|
||||
_statusEffectsSystem = _entityManager.System<Shared.StatusEffectNew.StatusEffectsSystem>();
|
||||
_drunkShader = _prototypeManager.Index(DrunkShader).InstanceUnique();
|
||||
_configManager.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true);
|
||||
_configManager.OnValueChanged(CCVars.DisableDrunkOverlay, OnReducedMotionChanged, invokeImmediately: true);
|
||||
}
|
||||
|
||||
private void OnReducedMotionChanged(bool reducedMotion)
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ namespace Content.Client.Eye.Blinding
|
|||
|
||||
// While the cataracts shader is designed to be tame enough to keep motion sickness at bay, the general waviness means that those who are particularly sensitive to motion sickness will probably hurl.
|
||||
// So the reasonable alternative here is to replace it with a static effect! Specifically, one that replicates the blindness effect seen across most SS13 servers.
|
||||
if (_configManager.GetCVar(CCVars.ReducedMotion))
|
||||
if (_configManager.GetCVar(CCVars.DisableBlurryVision))
|
||||
{
|
||||
_circleMaskShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
|
||||
_circleMaskShader.SetParameter("Zoom", zoom);
|
||||
|
|
|
|||
|
|
@ -42,8 +42,7 @@ public sealed partial class FaxVisualsSystem : EntitySystem
|
|||
LayerKey = FaxMachineVisuals.VisualState,
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackSpriteFlick.KeyFrame(component.InsertingState, 0f),
|
||||
new AnimationTrackSpriteFlick.KeyFrame("icon", 2.4f),
|
||||
new AnimationTrackSpriteFlick.KeyFrame(component.InsertingState, 0f)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Flash;
|
||||
using Content.Shared.Flash.Components;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
|
|
@ -9,101 +9,100 @@ using Robust.Shared.Enums;
|
|||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Flash
|
||||
namespace Content.Client.Flash;
|
||||
|
||||
public sealed partial class FlashOverlay : Overlay
|
||||
{
|
||||
public sealed partial class FlashOverlay : Overlay
|
||||
private static readonly ProtoId<ShaderPrototype> FlashedEffectShader = "FlashedEffect";
|
||||
|
||||
[Dependency] private IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private IEntityManager _entityManager = default!;
|
||||
[Dependency] private IPlayerManager _playerManager = default!;
|
||||
[Dependency] private IGameTiming _timing = default!;
|
||||
[Dependency] private IConfigurationManager _configManager = default!;
|
||||
|
||||
private readonly StatusEffectsSystem _statusSys;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
private readonly ShaderInstance _shader;
|
||||
private bool _reducedMotion;
|
||||
public float PercentComplete;
|
||||
public Texture? ScreenshotTexture;
|
||||
|
||||
public FlashOverlay()
|
||||
{
|
||||
private static readonly ProtoId<ShaderPrototype> FlashedEffectShader = "FlashedEffect";
|
||||
IoCManager.InjectDependencies(this);
|
||||
_shader = _prototypeManager.Index(FlashedEffectShader).InstanceUnique();
|
||||
_statusSys = _entityManager.System<StatusEffectsSystem>();
|
||||
|
||||
[Dependency] private IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private IEntityManager _entityManager = default!;
|
||||
[Dependency] private IPlayerManager _playerManager = default!;
|
||||
[Dependency] private IGameTiming _timing = default!;
|
||||
[Dependency] private IConfigurationManager _configManager = default!;
|
||||
_configManager.OnValueChanged(CCVars.DisableFlashEffect, (b) => { _reducedMotion = b; }, invokeImmediately: true);
|
||||
}
|
||||
|
||||
private readonly SharedFlashSystem _flash;
|
||||
private readonly StatusEffectsSystem _statusSys;
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
var playerEntity = _playerManager.LocalEntity;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
private readonly ShaderInstance _shader;
|
||||
private bool _reducedMotion;
|
||||
public float PercentComplete;
|
||||
public Texture? ScreenshotTexture;
|
||||
if (playerEntity == null)
|
||||
return;
|
||||
|
||||
public FlashOverlay()
|
||||
if (!_statusSys.HasEffectComp<FlashedStatusEffectComponent>(playerEntity))
|
||||
return;
|
||||
|
||||
if (!_statusSys.TryGetTime(playerEntity.Value, SharedFlashSystem.FlashedKey, out var time))
|
||||
return;
|
||||
|
||||
var curTime = _timing.CurTime;
|
||||
if (time.StartEffectTime == null || time.EndEffectTime == null)
|
||||
return;
|
||||
|
||||
var lastsFor = (float)(time.EndEffectTime.Value - time.StartEffectTime.Value).TotalSeconds;
|
||||
var timeDone = (float)(curTime - time.StartEffectTime.Value).TotalSeconds;
|
||||
|
||||
PercentComplete = timeDone / lastsFor;
|
||||
}
|
||||
|
||||
protected override bool BeforeDraw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp))
|
||||
return false;
|
||||
if (args.Viewport.Eye != eyeComp.Eye)
|
||||
return false;
|
||||
|
||||
return PercentComplete < 1.0f;
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (RequestScreenTexture && ScreenTexture != null)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_shader = _prototypeManager.Index(FlashedEffectShader).InstanceUnique();
|
||||
_flash = _entityManager.System<SharedFlashSystem>();
|
||||
_statusSys = _entityManager.System<StatusEffectsSystem>();
|
||||
|
||||
_configManager.OnValueChanged(CCVars.ReducedMotion, (b) => { _reducedMotion = b; }, invokeImmediately: true);
|
||||
ScreenshotTexture = ScreenTexture;
|
||||
RequestScreenTexture = false; // we only need the first frame, so we can stop the request now for performance reasons
|
||||
}
|
||||
if (ScreenshotTexture == null)
|
||||
return;
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
var worldHandle = args.WorldHandle;
|
||||
if (_reducedMotion)
|
||||
{
|
||||
var playerEntity = _playerManager.LocalEntity;
|
||||
|
||||
if (playerEntity == null)
|
||||
return;
|
||||
|
||||
if (!_entityManager.HasComponent<FlashedComponent>(playerEntity)
|
||||
|| !_entityManager.TryGetComponent<StatusEffectsComponent>(playerEntity, out var status))
|
||||
return;
|
||||
|
||||
if (!_statusSys.TryGetTime(playerEntity.Value, _flash.FlashedKey, out var time, status))
|
||||
return;
|
||||
|
||||
var curTime = _timing.CurTime;
|
||||
var lastsFor = (float)(time.Value.Item2 - time.Value.Item1).TotalSeconds;
|
||||
var timeDone = (float)(curTime - time.Value.Item1).TotalSeconds;
|
||||
|
||||
PercentComplete = timeDone / lastsFor;
|
||||
// TODO: This is a very simple placeholder.
|
||||
// Replace it with a proper shader once we come up with something good.
|
||||
// Turns out making an effect that is supposed to be a bright, sudden, and disorienting flash
|
||||
// not do any of that while also being equivalent in terms of game balance is hard.
|
||||
var alpha = 1 - MathF.Pow(PercentComplete, 8f); // similar falloff curve to the flash shader
|
||||
worldHandle.DrawRect(args.WorldBounds, new Color(0f, 0f, 0f, alpha));
|
||||
}
|
||||
|
||||
protected override bool BeforeDraw(in OverlayDrawArgs args)
|
||||
else
|
||||
{
|
||||
if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp))
|
||||
return false;
|
||||
if (args.Viewport.Eye != eyeComp.Eye)
|
||||
return false;
|
||||
|
||||
return PercentComplete < 1.0f;
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (RequestScreenTexture && ScreenTexture != null)
|
||||
{
|
||||
ScreenshotTexture = ScreenTexture;
|
||||
RequestScreenTexture = false; // we only need the first frame, so we can stop the request now for performance reasons
|
||||
}
|
||||
if (ScreenshotTexture == null)
|
||||
return;
|
||||
|
||||
var worldHandle = args.WorldHandle;
|
||||
if (_reducedMotion)
|
||||
{
|
||||
// TODO: This is a very simple placeholder.
|
||||
// Replace it with a proper shader once we come up with something good.
|
||||
// Turns out making an effect that is supposed to be a bright, sudden, and disorienting flash
|
||||
// not do any of that while also being equivalent in terms of game balance is hard.
|
||||
var alpha = 1 - MathF.Pow(PercentComplete, 8f); // similar falloff curve to the flash shader
|
||||
worldHandle.DrawRect(args.WorldBounds, new Color(0f, 0f, 0f, alpha));
|
||||
}
|
||||
else
|
||||
{
|
||||
_shader.SetParameter("percentComplete", PercentComplete);
|
||||
worldHandle.UseShader(_shader);
|
||||
worldHandle.DrawTextureRectRegion(ScreenshotTexture, args.WorldBounds);
|
||||
worldHandle.UseShader(null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
base.DisposeBehavior();
|
||||
ScreenshotTexture = null;
|
||||
_shader.SetParameter("percentComplete", PercentComplete);
|
||||
worldHandle.UseShader(_shader);
|
||||
worldHandle.DrawTextureRectRegion(ScreenshotTexture, args.WorldBounds);
|
||||
worldHandle.UseShader(null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DisposeBehavior()
|
||||
{
|
||||
base.DisposeBehavior();
|
||||
ScreenshotTexture = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Content.Shared.Flash;
|
||||
using Content.Shared.Flash.Components;
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Player;
|
||||
|
|
@ -10,49 +11,55 @@ public sealed partial class FlashSystem : SharedFlashSystem
|
|||
{
|
||||
[Dependency] private IPlayerManager _player = default!;
|
||||
[Dependency] private IOverlayManager _overlayMan = default!;
|
||||
[Dependency] private StatusEffectsSystem _statusEffects = default!;
|
||||
|
||||
private FlashOverlay _overlay = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<FlashedComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<FlashedComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<FlashedComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<FlashedComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
|
||||
|
||||
_overlay = new();
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(EntityUid uid, FlashedComponent component, LocalPlayerAttachedEvent args)
|
||||
[SubscribeLocalEvent]
|
||||
private void OnApplied(Entity<FlashedStatusEffectComponent> ent, ref StatusEffectAppliedEvent args)
|
||||
{
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(EntityUid uid, FlashedComponent component, LocalPlayerDetachedEvent args)
|
||||
{
|
||||
_overlay.ScreenshotTexture = null;
|
||||
_overlay.RequestScreenTexture = false;
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, FlashedComponent component, ComponentInit args)
|
||||
{
|
||||
if (_player.LocalEntity == uid)
|
||||
if (_player.LocalEntity == args.Target)
|
||||
{
|
||||
_overlay.RequestScreenTexture = true;
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, FlashedComponent component, ComponentShutdown args)
|
||||
[SubscribeLocalEvent]
|
||||
private void OnRemoved(Entity<FlashedStatusEffectComponent> ent, ref StatusEffectRemovedEvent args)
|
||||
{
|
||||
if (_player.LocalEntity == uid)
|
||||
if (_player.LocalEntity != args.Target)
|
||||
return;
|
||||
|
||||
if (!_statusEffects.HasEffectComp<FlashedStatusEffectComponent>(args.Target))
|
||||
{
|
||||
_overlay.ScreenshotTexture = null;
|
||||
_overlay.RequestScreenTexture = false;
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
}
|
||||
}
|
||||
|
||||
[SubscribeLocalEvent]
|
||||
private void OnPlayerAttached(Entity<FlashedStatusEffectComponent> ent, ref StatusEffectRelayedEvent<LocalPlayerAttachedEvent> args)
|
||||
{
|
||||
_overlay.RequestScreenTexture = true;
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
[SubscribeLocalEvent]
|
||||
private void OnPlayerDetached(Entity<FlashedStatusEffectComponent> ent, ref StatusEffectRelayedEvent<LocalPlayerDetachedEvent> args)
|
||||
{
|
||||
if (_player.LocalEntity is null || _statusEffects.HasEffectComp<FlashedStatusEffectComponent>(_player.LocalEntity.Value))
|
||||
return;
|
||||
|
||||
_overlay.ScreenshotTexture = null;
|
||||
_overlay.RequestScreenTexture = false;
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using Content.Client.Movement.Systems;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.NightVision;
|
||||
using Content.Shared.Overlays;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
|
|
@ -13,9 +15,9 @@ namespace Content.Client.Ghost
|
|||
[Dependency] private IClientConsoleHost _console = default!;
|
||||
[Dependency] private IPlayerManager _playerManager = default!;
|
||||
[Dependency] private SharedActionsSystem _actions = default!;
|
||||
[Dependency] private PointLightSystem _pointLightSystem = default!;
|
||||
[Dependency] private ContentEyeSystem _contentEye = default!;
|
||||
[Dependency] private SpriteSystem _sprite = default!;
|
||||
[Dependency] private SharedNightVisionSystem _nv = default!;
|
||||
|
||||
public int AvailableGhostRoleCount { get; private set; }
|
||||
|
||||
|
|
@ -81,27 +83,25 @@ namespace Content.Client.Ghost
|
|||
if (args.Handled)
|
||||
return;
|
||||
|
||||
TryComp<PointLightComponent>(uid, out var light);
|
||||
|
||||
if (!component.DrawLight)
|
||||
{
|
||||
// normal lighting
|
||||
Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-lighting-manager-popup-normal"), args.Performer);
|
||||
_contentEye.RequestEye(component.DrawFov, true);
|
||||
}
|
||||
else if (!light?.Enabled ?? false) // skip this option if we have no PointLightComponent
|
||||
else if (TryComp<NightVisionComponent>(uid, out var nv) && !nv.Enabled)
|
||||
{
|
||||
// enable personal light
|
||||
Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-lighting-manager-popup-personal-light"), args.Performer);
|
||||
_pointLightSystem.SetEnabled(uid, true, light);
|
||||
Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-lighting-manager-popup-half-bright"), args.Performer);
|
||||
_nv.SetEnabled((uid, nv), true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// fullbright mode
|
||||
Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-lighting-manager-popup-fullbright"), args.Performer);
|
||||
_contentEye.RequestEye(component.DrawFov, false);
|
||||
_pointLightSystem.SetEnabled(uid, false, light);
|
||||
_nv.SetEnabled((uid, nv), false);
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont
|
|||
Container container = ReactantsContainer;
|
||||
SetReagents(prototype.Reactants, ref container, protoMan);
|
||||
Container productContainer = ProductsContainer;
|
||||
var products = new Dictionary<string, FixedPoint2>(prototype.Products);
|
||||
var products = new Dictionary<ProtoId<ReagentPrototype>, FixedPoint2>(prototype.Products);
|
||||
foreach (var (reagent, reactantProto) in prototype.Reactants)
|
||||
{
|
||||
if (reactantProto.Catalyst)
|
||||
|
|
@ -101,11 +101,11 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont
|
|||
ReactantsContainer.Visible = true;
|
||||
ReactantsContainer.AddChild(label);
|
||||
|
||||
if (prototype.Reagent != null)
|
||||
if (prototype.Reagent is {} reagent)
|
||||
{
|
||||
var quantity = new Dictionary<string, FixedPoint2>
|
||||
var quantity = new Dictionary<ProtoId<ReagentPrototype>, FixedPoint2>
|
||||
{
|
||||
{ prototype.Reagent, FixedPoint2.New(0.21f) }
|
||||
{ reagent, FixedPoint2.New(0.21f) }
|
||||
};
|
||||
Container productContainer = ProductsContainer;
|
||||
SetReagents(quantity, ref productContainer, protoMan, false);
|
||||
|
|
@ -113,9 +113,9 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont
|
|||
SetMixingCategory(categories, null, sysMan);
|
||||
}
|
||||
|
||||
private void SetReagents(List<ReagentQuantity> reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true)
|
||||
private static void SetReagents(List<ReagentQuantity> reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true)
|
||||
{
|
||||
var amounts = new Dictionary<string, FixedPoint2>();
|
||||
var amounts = new Dictionary<ProtoId<ReagentPrototype>, FixedPoint2>();
|
||||
foreach (var (reagent, quantity) in reagents)
|
||||
{
|
||||
amounts.Add(reagent.Prototype, quantity);
|
||||
|
|
@ -123,13 +123,13 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont
|
|||
SetReagents(amounts, ref container, protoMan, addLinks);
|
||||
}
|
||||
|
||||
private void SetReagents(
|
||||
Dictionary<string, ReactantPrototype> reactants,
|
||||
private static void SetReagents(
|
||||
Dictionary<ProtoId<ReagentPrototype>, ReactantInfo> reactants,
|
||||
ref Container container,
|
||||
IPrototypeManager protoMan,
|
||||
bool addLinks = true)
|
||||
{
|
||||
var amounts = new Dictionary<string, FixedPoint2>();
|
||||
var amounts = new Dictionary<ProtoId<ReagentPrototype>, FixedPoint2>();
|
||||
foreach (var (reagent, reactantPrototype) in reactants)
|
||||
{
|
||||
amounts.Add(reagent, reactantPrototype.Amount);
|
||||
|
|
@ -137,22 +137,7 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont
|
|||
SetReagents(amounts, ref container, protoMan, addLinks);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
private void SetReagents(
|
||||
Dictionary<ProtoId<MixingCategoryPrototype>, ReactantPrototype> reactants,
|
||||
ref Container container,
|
||||
IPrototypeManager protoMan,
|
||||
bool addLinks = true)
|
||||
{
|
||||
var amounts = new Dictionary<string, FixedPoint2>();
|
||||
foreach (var (reagent, reactantPrototype) in reactants)
|
||||
{
|
||||
amounts.Add(reagent, reactantPrototype.Amount);
|
||||
}
|
||||
SetReagents(amounts, ref container, protoMan, addLinks);
|
||||
}
|
||||
|
||||
private void SetReagents(Dictionary<string, FixedPoint2> reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true)
|
||||
private static void SetReagents(Dictionary<ProtoId<ReagentPrototype>, FixedPoint2> reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true)
|
||||
{
|
||||
foreach (var (product, amount) in reagents.OrderByDescending(p => p.Value))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<cc:VSeparator StyleClasses="LowDivider" Margin="0 -2"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<fancyTree:FancyTree Name="TableOfContents" HorizontalExpand="True"/>
|
||||
<BoxContainer Name="SearchContainer" Visible="False" HorizontalExpand="True">
|
||||
<LineEdit
|
||||
Name="SearchBar"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Content.Client.Guidebook.RichText;
|
||||
using Content.Client.UserInterface.ControlExtensions;
|
||||
|
|
@ -21,11 +20,12 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
|
|||
[Dependency] private DocumentParsingManager _parsingMan = default!;
|
||||
[Dependency] private IResourceManager _resourceManager = default!;
|
||||
|
||||
private Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry> _entries = new();
|
||||
private Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry> _entries = [];
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public ProtoId<GuideEntryPrototype> LastEntry;
|
||||
public ProtoId<GuideEntryPrototype>? Selected { get; private set; }
|
||||
|
||||
|
||||
public GuidebookWindow()
|
||||
{
|
||||
|
|
@ -34,6 +34,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
|
|||
_sawmill = Logger.GetSawmill("guidebook");
|
||||
|
||||
Tree.OnSelectedItemChanged += OnSelectionChanged;
|
||||
TableOfContents.OnSelectedItemChanged += OnTableOfContentsSelectionChanged;
|
||||
|
||||
SearchBar.OnTextChanged += _ =>
|
||||
{
|
||||
|
|
@ -76,7 +77,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
|
|||
|
||||
UserInterfaceManager.DeferAction(() =>
|
||||
{
|
||||
if (control.GetControlScrollPosition() is not {} position)
|
||||
if (control.GetControlScrollPosition() is not { } position)
|
||||
return;
|
||||
|
||||
Scroll.HScrollTarget = position.X;
|
||||
|
|
@ -91,6 +92,10 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
|
|||
{
|
||||
if (item != null && item.Metadata is GuideEntry entry)
|
||||
{
|
||||
// do nothing if the guide is the same as the currently selected one
|
||||
if (entry.Id == Selected)
|
||||
return;
|
||||
|
||||
ShowGuide(entry);
|
||||
|
||||
var isRulesEntry = entry.RuleEntry;
|
||||
|
|
@ -101,12 +106,29 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
|
|||
ClearSelectedGuide();
|
||||
}
|
||||
|
||||
private void OnTableOfContentsSelectionChanged(TreeItem? item)
|
||||
{
|
||||
if (item is null || item.Metadata is not Label entry)
|
||||
return;
|
||||
|
||||
UserInterfaceManager.DeferAction(() =>
|
||||
{
|
||||
if (entry.GetControlScrollPosition() is not { } position)
|
||||
return;
|
||||
|
||||
Scroll.HScrollTarget = position.X;
|
||||
Scroll.VScrollTarget = position.Y;
|
||||
});
|
||||
}
|
||||
|
||||
public void ClearSelectedGuide()
|
||||
{
|
||||
Placeholder.Visible = true;
|
||||
EntryContainer.Visible = false;
|
||||
SearchContainer.Visible = false;
|
||||
EntryContainer.RemoveAllChildren();
|
||||
|
||||
Selected = null;
|
||||
}
|
||||
|
||||
private void ShowGuide(GuideEntry entry)
|
||||
|
|
@ -127,11 +149,11 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
|
|||
_sawmill.Error($"Failed to parse contents of guide document {entry.Id}.");
|
||||
}
|
||||
|
||||
LastEntry = entry.Id;
|
||||
Selected = entry.Id;
|
||||
|
||||
var (linkableControls, linkControls) = GetLinkableControlsAndLinks(EntryContainer);
|
||||
|
||||
HashSet<IPrototype> availablePrototypeLinks = new();
|
||||
HashSet<IPrototype> availablePrototypeLinks = [];
|
||||
foreach (var linkableControl in linkableControls)
|
||||
{
|
||||
var prototype = linkableControl.RepresentedPrototype;
|
||||
|
|
@ -145,48 +167,108 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
|
|||
if (prototype != null && availablePrototypeLinks.Contains(prototype))
|
||||
linkControl.EnablePrototypeLink();
|
||||
}
|
||||
|
||||
RepopulateTableOfContents();
|
||||
}
|
||||
|
||||
public void UpdateGuides(
|
||||
private int? HeadingDepth(Label control)
|
||||
{
|
||||
if (control.StyleClasses.Contains("LabelHeadingBigger"))
|
||||
return 1;
|
||||
else if (control.StyleClasses.Contains("LabelHeading"))
|
||||
return 2;
|
||||
else if (control.StyleClasses.Contains("LabelKeyText"))
|
||||
return 3;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void RepopulateTableOfContents()
|
||||
{
|
||||
TableOfContents.Clear();
|
||||
|
||||
var firstEntry = TableOfContents.AddItem(null);
|
||||
firstEntry.Label.Text = Loc.GetString("guidebook-toc-header");
|
||||
|
||||
var labels = EntryContainer.GetControlOfType<Label>(true);
|
||||
var stack = new Stack<(TreeItem Item, int Depth)>();
|
||||
|
||||
foreach (var label in labels)
|
||||
{
|
||||
if (HeadingDepth(label) is not { } depth)
|
||||
continue;
|
||||
|
||||
while (stack.TryPeek(out var previous) && previous.Depth >= depth)
|
||||
{
|
||||
stack.Pop();
|
||||
}
|
||||
|
||||
var item = stack.TryPeek(out var parent)
|
||||
? TableOfContents.AddItem(parent.Item)
|
||||
: TableOfContents.AddItem(firstEntry);
|
||||
item.Label.Text = label.Text;
|
||||
item.Metadata = label;
|
||||
|
||||
stack.Push((item, depth));
|
||||
}
|
||||
|
||||
// Expand all entries, but collapse the first one
|
||||
TableOfContents.SetAllExpanded(true);
|
||||
firstEntry.SetExpanded(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the guides used in the window.
|
||||
/// Returns whether the guides changed.
|
||||
/// </summary>
|
||||
public bool UpdateGuides(
|
||||
Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry> entries,
|
||||
List<ProtoId<GuideEntryPrototype>>? rootEntries = null,
|
||||
ProtoId<GuideEntryPrototype>? forceRoot = null,
|
||||
ProtoId<GuideEntryPrototype>? selected = null)
|
||||
{
|
||||
_entries = entries;
|
||||
RepopulateTree(rootEntries, forceRoot);
|
||||
ClearSelectedGuide();
|
||||
|
||||
Split.State = SplitContainer.SplitState.Auto;
|
||||
if (entries.Count == 1)
|
||||
// check if old and new entries are equal
|
||||
var sameAsLastUpdate = entries.Count != _entries.Count || !entries.All(_entries.Contains);
|
||||
if (sameAsLastUpdate)
|
||||
{
|
||||
TreeBox.Visible = false;
|
||||
Split.ResizeMode = SplitContainer.SplitResizeMode.NotResizable;
|
||||
selected = entries.Keys.First();
|
||||
_entries = entries;
|
||||
RepopulateTree(rootEntries, forceRoot);
|
||||
Split.State = SplitContainer.SplitState.Auto;
|
||||
if (entries.Count == 1)
|
||||
{
|
||||
TreeBox.Visible = false;
|
||||
Split.ResizeMode = SplitContainer.SplitResizeMode.NotResizable;
|
||||
selected = entries.Keys.First();
|
||||
}
|
||||
else
|
||||
{
|
||||
TreeBox.Visible = true;
|
||||
Split.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (selected == null)
|
||||
ClearSelectedGuide();
|
||||
else
|
||||
{
|
||||
TreeBox.Visible = true;
|
||||
Split.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
|
||||
}
|
||||
|
||||
if (selected != null)
|
||||
{
|
||||
var item = Tree.Items.FirstOrDefault(x => x.Metadata is GuideEntry entry && entry.Id == selected);
|
||||
Tree.SetSelectedIndex(item?.Index);
|
||||
}
|
||||
|
||||
return sameAsLastUpdate;
|
||||
}
|
||||
|
||||
private IEnumerable<GuideEntry> GetSortedEntries(List<ProtoId<GuideEntryPrototype>>? rootEntries)
|
||||
{
|
||||
if (rootEntries == null)
|
||||
{
|
||||
HashSet<ProtoId<GuideEntryPrototype>> entries = new(_entries.Keys);
|
||||
HashSet<ProtoId<GuideEntryPrototype>> entries = [.. _entries.Keys];
|
||||
foreach (var entry in _entries.Values)
|
||||
{
|
||||
entries.ExceptWith(entry.Children);
|
||||
}
|
||||
rootEntries = entries.ToList();
|
||||
|
||||
rootEntries = [.. entries];
|
||||
}
|
||||
|
||||
// Only roots need to be sorted.
|
||||
|
|
@ -203,7 +285,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
|
|||
{
|
||||
Tree.Clear();
|
||||
|
||||
HashSet<ProtoId<GuideEntryPrototype>> addedEntries = new();
|
||||
HashSet<ProtoId<GuideEntryPrototype>> addedEntries = [];
|
||||
|
||||
var parent = forcedRoot == null ? null : AddEntry(forcedRoot.Value, null, addedEntries);
|
||||
foreach (var entry in GetSortedEntries(roots))
|
||||
|
|
@ -248,8 +330,6 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
|
|||
|
||||
private void HandleFilter()
|
||||
{
|
||||
var emptySearch = SearchBar.Text.Trim().Length == 0;
|
||||
|
||||
if (Tree.SelectedItem != null && Tree.SelectedItem.Metadata is GuideEntry entry && entry.FilterEnabled)
|
||||
{
|
||||
var foundElements = EntryContainer.GetSearchableControls();
|
||||
|
|
@ -263,8 +343,8 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
|
|||
|
||||
private static (List<IPrototypeRepresentationControl>, List<IPrototypeLinkControl>) GetLinkableControlsAndLinks(Control parent)
|
||||
{
|
||||
List<IPrototypeRepresentationControl> linkableList = new();
|
||||
List<IPrototypeLinkControl> linkList = new();
|
||||
List<IPrototypeRepresentationControl> linkableList = [];
|
||||
List<IPrototypeLinkControl> linkList = [];
|
||||
|
||||
foreach (var child in parent.Children)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ public sealed partial class HolopadWindow : FancyWindow
|
|||
|
||||
if (!_accessReaderSystem.IsAllowed(player.Value, _owner.Value))
|
||||
{
|
||||
_popupSystem.PopupClient(Loc.GetString("holopad-window-access-denied"), _owner.Value, player.Value);
|
||||
_popupSystem.PopupEntity(Loc.GetString("holopad-window-access-denied"), _owner.Value, player.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ public sealed partial class InstrumentComponent : SharedInstrumentComponent
|
|||
/// <summary>
|
||||
/// Whether this instrument is handheld or not.
|
||||
/// </summary>
|
||||
[DataField("handheld")]
|
||||
[DataField]
|
||||
public bool Handheld { get; set; } // TODO: Replace this by simply checking if the entity has an ItemComponent.
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Audio.Midi;
|
||||
using Content.Client.Changelog;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Clickable;
|
||||
|
|
@ -69,6 +70,7 @@ namespace Content.Client.IoC
|
|||
collection.Register<ClientsidePlaytimeTrackingManager>();
|
||||
collection.Register<ClientFeedbackManager>();
|
||||
collection.Register<ISharedFeedbackManager, ClientFeedbackManager>();
|
||||
collection.Register<MidiFileCollectionManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ namespace Content.Client.Lobby.UI
|
|||
|
||||
_createNewCharacterButton.OnPressed += args =>
|
||||
{
|
||||
_preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random());
|
||||
_preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random().WithJobFromCvar(_cfg));
|
||||
ReloadCharacterPickers();
|
||||
args.Event.Handle();
|
||||
};
|
||||
|
|
@ -66,6 +66,7 @@ namespace Content.Client.Lobby.UI
|
|||
StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered();
|
||||
|
||||
_cfg.OnValueChanged(CCVars.SeeOwnNotes, p => AdminRemarksButton.Visible = p, true);
|
||||
_cfg.OnValueChanged(CCVars.GameMaxCharacterSlots, _ => ReloadCharacterPickers());
|
||||
// Corvax-Sponsors-Start
|
||||
if (IoCManager.Instance!.TryResolveType<ISponsorWindowCreator>(out var creator))
|
||||
{
|
||||
|
|
@ -91,9 +92,11 @@ namespace Content.Client.Lobby.UI
|
|||
return;
|
||||
}
|
||||
|
||||
var maxCharactersSlots = _cfg.GetCVar(CCVars.GameMaxCharacterSlots);
|
||||
|
||||
_createNewCharacterButton.ToolTip =
|
||||
Loc.GetString("character-setup-gui-create-new-character-button-tooltip",
|
||||
("maxCharacters", _preferencesManager.Settings!.MaxCharacterSlots));
|
||||
("maxCharacters", maxCharactersSlots));
|
||||
|
||||
var selectedSlot = _preferencesManager.Preferences?.SelectedCharacterIndex;
|
||||
|
||||
|
|
@ -106,6 +109,14 @@ namespace Content.Client.Lobby.UI
|
|||
character,
|
||||
slot == selectedSlot);
|
||||
|
||||
if (slot >= maxCharactersSlots)
|
||||
{
|
||||
characterPickerButton.SetOnlyStyleClass(ContainerButton.StylePseudoClassDisabled);
|
||||
characterPickerButton.ToolTip =
|
||||
Loc.GetString("character-setup-gui-create-new-character-button-tooltip",
|
||||
("maxCharacters", maxCharactersSlots));
|
||||
}
|
||||
|
||||
Characters.AddChild(characterPickerButton);
|
||||
|
||||
characterPickerButton.OnPressed += args =>
|
||||
|
|
@ -119,7 +130,7 @@ namespace Content.Client.Lobby.UI
|
|||
};
|
||||
}
|
||||
|
||||
_createNewCharacterButton.Disabled = numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots;
|
||||
_createNewCharacterButton.Disabled = numberOfFullSlots >= maxCharactersSlots;
|
||||
Characters.AddChild(_createNewCharacterButton);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ public sealed partial class HumanoidProfileEditor
|
|||
private void SetVoice(ProtoId<EmoteSoundsPrototype> newVoice)
|
||||
{
|
||||
Profile = Profile?.WithVoice(newVoice);
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
private void SetGender(Gender newGender)
|
||||
|
|
|
|||
|
|
@ -21,18 +21,15 @@ public sealed partial class HumanoidProfileEditor
|
|||
NameEdit.Text = Profile?.Name ?? "";
|
||||
}
|
||||
|
||||
private void RandomizeEverything()
|
||||
/// <summary>
|
||||
/// Randomize values selectively while respecting locked values.
|
||||
/// </summary>
|
||||
private void RandomizeProfile()
|
||||
{
|
||||
Profile = HumanoidCharacterProfile.Random();
|
||||
Profile = Profile == null
|
||||
? HumanoidCharacterProfile.Random()
|
||||
: HumanoidCharacterProfile.Random(RandomizeLockButton.RandomizeCfg, Profile!);
|
||||
SetProfile(Profile, CharacterSlot);
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
private void RandomizeName()
|
||||
{
|
||||
if (Profile == null) return;
|
||||
var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
|
||||
SetName(name);
|
||||
UpdateNameEdit();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,21 +13,34 @@
|
|||
<BoxContainer Orientation="Vertical">
|
||||
<ui:HighlightedContainer>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<ui:HighlightedContainer>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-name-label'}" />
|
||||
<LineEdit Name="NameEdit" MinSize="270 0" VerticalAlignment="Center" Margin="5 0 0 0" />
|
||||
<Button Name="NameRandomize" Text="{Loc 'humanoid-profile-editor-name-random-button'}" />
|
||||
</BoxContainer>
|
||||
<Button Name="RandomizeEverythingButton" HorizontalAlignment="Center"
|
||||
HorizontalExpand="False" MaxWidth="256"
|
||||
Text="{Loc 'humanoid-profile-editor-randomize-everything-button'}" />
|
||||
<RichTextLabel Name="WarningLabel" HorizontalExpand="False"
|
||||
VerticalExpand="True" MaxWidth="425"
|
||||
HorizontalAlignment="Left" />
|
||||
</BoxContainer>
|
||||
</ui:HighlightedContainer>
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-name-label'}" />
|
||||
<LineEdit Name="NameEdit" MinSize="270 0" VerticalAlignment="Center" Margin="5 0 0 0" />
|
||||
</BoxContainer>
|
||||
<RichTextLabel Name="WarningLabel" HorizontalExpand="False"
|
||||
VerticalExpand="True" MaxWidth="425"
|
||||
HorizontalAlignment="Left" />
|
||||
</BoxContainer>
|
||||
</ui:HighlightedContainer>
|
||||
</BoxContainer>
|
||||
<!-- Randomizer -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<ui:HighlightedContainer>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-randomize'}" HorizontalAlignment="Center"/>
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 10"/>
|
||||
<GridContainer Columns="2" MinWidth="250" HorizontalExpand="True">
|
||||
<ui:RandomizeLockButton For="Name" LabelText="{Loc 'humanoid-profile-editor-randomize-name-button'}"/>
|
||||
<ui:RandomizeLockButton For="Species" LabelText="{Loc 'humanoid-profile-editor-randomize-species-button'}"/>
|
||||
<ui:RandomizeLockButton For="Age" LabelText="{Loc 'humanoid-profile-editor-randomize-age-button'}"/>
|
||||
<ui:RandomizeLockButton For="Sex" LabelText="{Loc 'humanoid-profile-editor-randomize-sex-button'}"/>
|
||||
<ui:RandomizeLockButton For="Skin" LabelText="{Loc 'humanoid-profile-editor-randomize-skin-button'}"/>
|
||||
<ui:RandomizeLockButton For="Eyes" LabelText="{Loc 'humanoid-profile-editor-randomize-eye-button'}"/>
|
||||
</GridContainer>
|
||||
<Button
|
||||
Name="RandomizeUnlockedButton"
|
||||
MaxWidth="256"
|
||||
Text="{Loc 'humanoid-profile-editor-randomize-unlocked-button'}"/>
|
||||
</BoxContainer>
|
||||
</ui:HighlightedContainer>
|
||||
</BoxContainer>
|
||||
|
|
|
|||
|
|
@ -144,8 +144,7 @@ namespace Content.Client.Lobby.UI
|
|||
|
||||
NameEdit.OnTextChanged += args => { SetName(args.Text); };
|
||||
NameEdit.IsValid = args => args.Length <= _maxNameLength;
|
||||
NameRandomize.OnPressed += args => RandomizeName();
|
||||
RandomizeEverythingButton.OnPressed += args => { RandomizeEverything(); };
|
||||
RandomizeUnlockedButton.OnPressed += args => { RandomizeProfile(); };
|
||||
WarningLabel.SetMarkup($"[color=red]{Loc.GetString("humanoid-profile-editor-naming-rules-warning")}[/color]");
|
||||
|
||||
#endregion Name
|
||||
|
|
|
|||
17
Content.Client/Lobby/UI/RandomizeLockButton.xaml
Normal file
17
Content.Client/Lobby/UI/RandomizeLockButton.xaml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<BoxContainer
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.Lobby.UI"
|
||||
Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
>
|
||||
|
||||
<TextureButton
|
||||
Name="LockButton"
|
||||
StyleIdentifier="RandomizerLockButton"
|
||||
ToggleMode="True"
|
||||
VerticalAlignment="Center"
|
||||
Scale="0.3 0.3"
|
||||
ToolTip="{Loc 'humanoid-profile-editor-randomizer-lock-tooltip'}"/>
|
||||
<Label Name="LockName"/>
|
||||
|
||||
</BoxContainer>
|
||||
91
Content.Client/Lobby/UI/RandomizeLockButton.xaml.cs
Normal file
91
Content.Client/Lobby/UI/RandomizeLockButton.xaml.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System.Numerics;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using static Content.Client.Stylesheets.StylesheetHelpers;
|
||||
|
||||
namespace Content.Client.Lobby.UI;
|
||||
|
||||
/// <summary>
|
||||
/// A toggle button with a lock icon meant to lock values for randomizing in the character editor screen.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RandomizeLockButton : BoxContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Which value this button locks.
|
||||
/// </summary>
|
||||
public HumanoidCharacterProfile.RandomizeCfg For { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the button
|
||||
/// </summary>
|
||||
public string? LabelText { get => LockName.Text; set => LockName.Text = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Tracks which values are allowed to be randomized.
|
||||
/// </summary>
|
||||
public static HumanoidCharacterProfile.RandomizeCfg RandomizeCfg = HumanoidCharacterProfile.RandomizeConfigAll;
|
||||
|
||||
public RandomizeLockButton()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
// Ensure we reset this value to default every time the button is created
|
||||
// Otherwise toggling, disconnecting and reconnecting will leave the cfg in an inconsistent state
|
||||
RandomizeCfg = HumanoidCharacterProfile.RandomizeConfigAll;
|
||||
|
||||
LockButton.OnToggled += args => HandleToggle(args.Pressed);
|
||||
}
|
||||
|
||||
private void HandleToggle(bool toggle)
|
||||
{
|
||||
if (For == default)
|
||||
return;
|
||||
if (toggle)
|
||||
{
|
||||
// if pressed, disable specific randomize by setting its bit to 0
|
||||
RandomizeCfg &= ~For;
|
||||
}
|
||||
else
|
||||
{
|
||||
// if not pressed, enable specific randomize by setting its bit to 1
|
||||
RandomizeCfg |= For;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[CommonSheetlet]
|
||||
public sealed class RandomizeLockButtonStyles : Sheetlet<PalettedStylesheet>
|
||||
{
|
||||
public override StyleRule[] GetRules(PalettedStylesheet sheet, object config)
|
||||
{
|
||||
var textureLocked = ResCache.GetTexture("/Textures/Interface/VerbIcons/lock.svg.192dpi.png");
|
||||
var textureUnlocked = ResCache.GetTexture("/Textures/Interface/VerbIcons/unlock.svg.192dpi.png");
|
||||
|
||||
return
|
||||
[
|
||||
E<TextureButton>()
|
||||
.Identifier("RandomizerLockButton")
|
||||
.Modulate(Color.FromHsl(new Vector4(0f, 0f, .65f, 1f)))
|
||||
.Prop(TextureButton.StylePropertyTexture, textureUnlocked)
|
||||
.Margin(new Thickness(0f, 0f, 3f, 0f)),
|
||||
|
||||
E<TextureButton>()
|
||||
.Identifier("RandomizerLockButton")
|
||||
.PseudoHovered()
|
||||
.Modulate(Color.WhiteSmoke)
|
||||
.Prop(TextureButton.StylePropertyTexture, textureLocked),
|
||||
|
||||
E<TextureButton>()
|
||||
.Identifier("RandomizerLockButton")
|
||||
.PseudoPressed()
|
||||
.Modulate(Color.WhiteSmoke)
|
||||
.Prop(TextureButton.StylePropertyTexture, textureLocked),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ using Content.Client.UserInterface.Screens;
|
|||
using Content.Client.UserInterface.Systems.Chat.Widgets;
|
||||
using Content.Shared.Decals;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
|
|
@ -18,6 +19,7 @@ namespace Content.Client.Mapping;
|
|||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MappingScreen : InGameScreen
|
||||
{
|
||||
[Dependency] private IPlacementManager _placement = default!;
|
||||
[Dependency] private IPrototypeManager _prototype = default!;
|
||||
|
||||
public DecalPlacementSystem DecalSystem = default!;
|
||||
|
|
@ -89,9 +91,10 @@ public sealed partial class MappingScreen : InGameScreen
|
|||
UpdateDecal();
|
||||
};
|
||||
|
||||
for (var i = 0; i < EntitySpawnWindow.InitOpts.Length; i++)
|
||||
var modes = _placement.AllModeNames;
|
||||
for (var i = 0; i < modes.Length; i++)
|
||||
{
|
||||
EntityPlacementMode.AddItem(EntitySpawnWindow.InitOpts[i], i);
|
||||
EntityPlacementMode.AddItem(modes[i], i);
|
||||
}
|
||||
|
||||
Pick.Texture.TexturePath = "/Textures/Interface/eyedropper.svg.png";
|
||||
|
|
|
|||
|
|
@ -560,7 +560,7 @@ public sealed partial class MappingState : GameplayStateBase
|
|||
|
||||
var placement = new PlacementInformation
|
||||
{
|
||||
PlacementOption = placementId > 0 ? EntitySpawnWindow.InitOpts[placementId] : entity.PlacementMode,
|
||||
PlacementOption = placementId > 0 ? _placement.AllModeNames[placementId] : entity.PlacementMode,
|
||||
EntityType = entity.ID,
|
||||
IsTile = false
|
||||
};
|
||||
|
|
@ -659,7 +659,7 @@ public sealed partial class MappingState : GameplayStateBase
|
|||
{
|
||||
var placement = new PlacementInformation
|
||||
{
|
||||
PlacementOption = EntitySpawnWindow.InitOpts[args.Id],
|
||||
PlacementOption = _placement.AllModeNames[args.Id],
|
||||
EntityType = _placement.CurrentPermission!.EntityType,
|
||||
TileType = _placement.CurrentPermission.TileType,
|
||||
Range = 2,
|
||||
|
|
|
|||
116
Content.Client/NightVision/NightVisionSystem.cs
Normal file
116
Content.Client/NightVision/NightVisionSystem.cs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
using Content.Client.Overlays;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.NightVision;
|
||||
using Content.Shared.Overlays;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client.NightVision;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed partial class NightVisionSystem : SharedNightVisionSystem
|
||||
{
|
||||
[Dependency] private IOverlayManager _overlayMan = default!;
|
||||
[Dependency] private IPlayerManager _player = default!;
|
||||
|
||||
private NightVisionOverlay _overlay = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_overlay = new NightVisionOverlay();
|
||||
}
|
||||
|
||||
[SubscribeLocalEvent]
|
||||
private void OnPlayerAttached(LocalPlayerAttachedEvent args)
|
||||
{
|
||||
RefreshOverlay(args.Entity);
|
||||
}
|
||||
|
||||
[SubscribeLocalEvent]
|
||||
private void OnPlayerDetached(LocalPlayerDetachedEvent args)
|
||||
{
|
||||
Deactivate(_player.LocalEntity);
|
||||
}
|
||||
|
||||
[SubscribeLocalEvent]
|
||||
private void OnHandleState(Entity<NightVisionComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
RefreshOverlay(ent);
|
||||
}
|
||||
|
||||
[SubscribeNetworkEvent]
|
||||
private void OnRoundRestart(RoundRestartCleanupEvent args)
|
||||
{
|
||||
var localPlayer = _player.LocalSession?.AttachedEntity;
|
||||
if (localPlayer != null)
|
||||
Deactivate(localPlayer.Value);
|
||||
}
|
||||
|
||||
private void Update(EntityUid entity, List<Entity<NightVisionComponent>> entities)
|
||||
{
|
||||
if (entity != _player.LocalSession?.AttachedEntity)
|
||||
return;
|
||||
|
||||
// Find the component with the lowest noise.
|
||||
NightVisionComponent? nvision = null;
|
||||
var bestNoise = float.MaxValue;
|
||||
foreach (var ent in entities)
|
||||
{
|
||||
if (!ent.Comp.Enabled)
|
||||
continue;
|
||||
|
||||
if (ent.Comp.RelayOverlay == (ent.Owner == entity))
|
||||
continue;
|
||||
|
||||
if (ent.Comp.Prioritized)
|
||||
{
|
||||
nvision = ent.Comp;
|
||||
break;
|
||||
}
|
||||
|
||||
var noise = ent.Comp.NoiseAmount * ent.Comp.NoiseMultiplier;
|
||||
if (noise < bestNoise)
|
||||
{
|
||||
nvision = ent.Comp;
|
||||
bestNoise = noise;
|
||||
}
|
||||
}
|
||||
|
||||
// There is no active night vision components, so we disable the overlay.
|
||||
if (nvision == null)
|
||||
{
|
||||
Deactivate(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
_overlay.SetParameters(nvision.OverlayColor, nvision.LightingColor, nvision.NoiseAmount, nvision.NoiseMultiplier);
|
||||
|
||||
if (!_overlayMan.HasOverlay<NightVisionOverlay>())
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
private void Deactivate(EntityUid? ent)
|
||||
{
|
||||
if (ent != _player.LocalSession?.AttachedEntity)
|
||||
return;
|
||||
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
}
|
||||
|
||||
protected override void RefreshOverlay(EntityUid target)
|
||||
{
|
||||
if (target != _player.LocalSession?.AttachedEntity)
|
||||
return;
|
||||
|
||||
var ev = new RefreshNightVisionEvent();
|
||||
RaiseLocalEvent(target, ref ev);
|
||||
|
||||
if (ev.Entities.Count > 0)
|
||||
Update(target, ev.Entities);
|
||||
else
|
||||
Deactivate(target);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using Content.Client.DisplacementMap;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Nutrition.EntitySystems;
|
||||
|
||||
|
|
@ -8,6 +10,7 @@ public sealed partial class CreamPieSystem : SharedCreamPieSystem
|
|||
{
|
||||
[Dependency] private SpriteSystem _sprite = default!;
|
||||
[Dependency] private AppearanceSystem _appearance = default!;
|
||||
[Dependency] private DisplacementMapSystem _displacement = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -27,6 +30,7 @@ public sealed partial class CreamPieSystem : SharedCreamPieSystem
|
|||
private void OnComponentShutdown(Entity<CreamPiedComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
_sprite.RemoveLayer(ent.Owner, CreamPiedVisualLayer.Key);
|
||||
_displacement.EnsureDisplacementIsNotOnSprite((ent, Comp<SpriteComponent>(ent)), CreamPiedVisualLayer.Key);
|
||||
}
|
||||
|
||||
private void OnAppearanceChange(Entity<CreamPiedComponent> ent, ref AppearanceChangeEvent args)
|
||||
|
|
@ -54,6 +58,7 @@ public sealed partial class CreamPieSystem : SharedCreamPieSystem
|
|||
if (creamPied.Sprite == null)
|
||||
{
|
||||
_sprite.RemoveLayer((ent.Owner, sprite), CreamPiedVisualLayer.Key);
|
||||
_displacement.EnsureDisplacementIsNotOnSprite((ent.Owner, sprite), CreamPiedVisualLayer.Key);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -62,5 +67,10 @@ public sealed partial class CreamPieSystem : SharedCreamPieSystem
|
|||
_appearance.TryGetData<bool>(ent.Owner, CreamPiedVisuals.Creamed, out var isCreamPied, appearance);
|
||||
_sprite.LayerSetSprite((ent.Owner, sprite), index, creamPied.Sprite);
|
||||
_sprite.LayerSetVisible((ent.Owner, sprite), index, isCreamPied);
|
||||
|
||||
if (ProtoMan.Resolve(ent.Comp1.Displacement, out var displacementProto))
|
||||
{
|
||||
_displacement.TryAddDisplacement(displacementProto.Displacement, (ent.Owner, sprite), index, CreamPiedVisualLayer.Key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,39 @@
|
|||
<BoxContainer Orientation="Vertical">
|
||||
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
|
||||
<BoxContainer Orientation="Vertical" Margin="8">
|
||||
<Label Text="{Loc 'ui-options-accessability-header-visuals'}"
|
||||
<Label Text="{Loc 'ui-options-accessibility-header-reduced-motion'}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<CheckBox Name="ReducedMotionCheckBox"
|
||||
Text="{Loc 'ui-options-reduced-motion'}"
|
||||
ToolTip="{Loc 'ui-options-reduced-motion-tooltip'}" />
|
||||
<GridContainer Columns="2" HorizontalExpand="True" Margin="8 8 8 8">
|
||||
<CheckBox Name="DisableAiStaticCheckBox"
|
||||
Text="{Loc 'ui-options-disable-ai-static'}"
|
||||
ToolTip="{Loc 'ui-options-disable-ai-static-tooltip'}" />
|
||||
<CheckBox Name="DisableBlurryVisionCheckBox"
|
||||
Text="{Loc 'ui-options-disable-blurry-vision'}"
|
||||
ToolTip="{Loc 'ui-options-disable-blurry-vision-tooltip'}" />
|
||||
<CheckBox Name="DisableDrunkOverlayCheckBox"
|
||||
Text="{Loc 'ui-options-disable-drunk-overlay'}"
|
||||
ToolTip="{Loc 'ui-options-disable-drunk-overlay-tooltip'}" />
|
||||
<CheckBox Name="DisableFlashEffectCheckBox"
|
||||
Text="{Loc 'ui-options-disable-flash-effect'}"
|
||||
ToolTip="{Loc 'ui-options-disable-flash-effect-tooltip'}" />
|
||||
<CheckBox Name="DisableHeatDistortionCheckBox"
|
||||
Text="{Loc 'ui-options-disable-heat-distortion'}"
|
||||
ToolTip="{Loc 'ui-options-disable-heat-distortion-tooltip'}" />
|
||||
<CheckBox Name="DisableNightVisionNoiseCheckBox"
|
||||
Text="{Loc 'ui-options-disable-nv-noise'}"
|
||||
ToolTip="{Loc 'ui-options-disable-nv-noise-tooltip'}" />
|
||||
<CheckBox Name="DisableRainbowOverlayCheckBox"
|
||||
Text="{Loc 'ui-options-disable-rainbow-overlay'}"
|
||||
ToolTip="{Loc 'ui-options-disable-rainbow-overlay-tooltip'}" />
|
||||
<CheckBox Name="DisableSinguloWarpCheckBox"
|
||||
Text="{Loc 'ui-options-disable-singulo-warp'}"
|
||||
ToolTip="{Loc 'ui-options-disable-singulo-warp-tooltip'}" />
|
||||
</GridContainer>
|
||||
<Label Text="{Loc 'ui-options-accessibility-header-visuals'}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<CheckBox Name="ReducedMotionCheckBox" Text="{Loc 'ui-options-reduced-motion'}" />
|
||||
<CheckBox Name="DisableAiStaticCheckBox" Text="{Loc 'ui-options-disable-ai-static'}" />
|
||||
<CheckBox Name="EnableColorNameCheckBox" Text="{Loc 'ui-options-enable-color-name'}" />
|
||||
<CheckBox Name="ColorblindFriendlyCheckBox" Text="{Loc 'ui-options-colorblind-friendly'}" />
|
||||
<ui:OptionSlider Name="ScreenShakeIntensitySlider" Title="{Loc 'ui-options-screen-shake-intensity'}" />
|
||||
|
|
@ -19,7 +48,7 @@
|
|||
<ui:OptionColorSlider Name="HighlightsColorSlider"
|
||||
Title="{Loc 'ui-options-highlights-color'}"
|
||||
Example="{Loc 'ui-options-highlights-color-example'}"/>
|
||||
<Label Text="{Loc 'ui-options-accessability-header-content'}"
|
||||
<Label Text="{Loc 'ui-options-accessibility-header-content'}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<CheckBox Name="CensorNudityCheckBox" Text="{Loc 'ui-options-censor-nudity'}" />
|
||||
</BoxContainer>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,17 @@ public sealed partial class AccessibilityTab : Control
|
|||
|
||||
Control.AddOptionCheckBox(CCVars.ChatEnableColorName, EnableColorNameCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.AccessibilityColorblindFriendly, ColorblindFriendlyCheckBox);
|
||||
|
||||
Control.AddOptionCheckBox(CCVars.ReducedMotion, ReducedMotionCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.DisableAiStatic, DisableAiStaticCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.DisableBlurryVision, DisableBlurryVisionCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.DisableDrunkOverlay, DisableDrunkOverlayCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.DisableFlashEffect, DisableFlashEffectCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.DisableHeatDistortion, DisableHeatDistortionCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.DisableNightVisionNoise, DisableNightVisionNoiseCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.DisableRainbowOverlay, DisableRainbowOverlayCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.DisableSinguloWarp, DisableSinguloWarpCheckBox);
|
||||
|
||||
Control.AddOptionPercentSlider(CCVars.ScreenShakeIntensity, ScreenShakeIntensitySlider);
|
||||
Control.AddOptionPercentSlider(CCVars.ChatWindowOpacity, ChatWindowOpacitySlider);
|
||||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleTextOpacity, SpeechBubbleTextOpacitySlider);
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ public sealed class EntityHealthBarOverlay : Overlay
|
|||
var yOffset = bounds.Height * EyeManager.PixelsPerMeter / 2 - 3f;
|
||||
var widthOfMob = bounds.Width * EyeManager.PixelsPerMeter;
|
||||
|
||||
var position = new Vector2(-widthOfMob / EyeManager.PixelsPerMeter / 2, yOffset / EyeManager.PixelsPerMeter);
|
||||
var position = new Vector2(sprite.Offset.X - widthOfMob / EyeManager.PixelsPerMeter / 2, sprite.Offset.Y + yOffset / EyeManager.PixelsPerMeter);
|
||||
var color = GetProgressColor(deathProgress.ratio, deathProgress.inCrit);
|
||||
|
||||
// Hardcoded width of the progress bar because it doesn't match the texture.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Content.Shared.CCVar;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
|
|
@ -10,6 +12,7 @@ namespace Content.Client.Overlays;
|
|||
public sealed partial class NightVisionOverlay : Overlay
|
||||
{
|
||||
[Dependency] private IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private IConfigurationManager _configManager = default!;
|
||||
|
||||
private static readonly ProtoId<ShaderPrototype> Shader = "NightVision";
|
||||
private readonly ShaderInstance _nightVisionShader;
|
||||
|
|
@ -19,6 +22,8 @@ public sealed partial class NightVisionOverlay : Overlay
|
|||
public float NoiseAmount { get; private set; }
|
||||
public float NoiseMultiplier { get; private set; }
|
||||
|
||||
public bool DisableNoise = false;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.BeforeLighting | OverlaySpace.WorldSpace;
|
||||
public override bool RequestScreenTexture => true;
|
||||
|
||||
|
|
@ -26,6 +31,12 @@ public sealed partial class NightVisionOverlay : Overlay
|
|||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_nightVisionShader = _prototypeManager.Index(Shader).InstanceUnique();
|
||||
_configManager.OnValueChanged(CCVars.DisableNightVisionNoise, OnNightVisionNoiseChanged, invokeImmediately: true);
|
||||
}
|
||||
|
||||
private void OnNightVisionNoiseChanged(bool toggle)
|
||||
{
|
||||
DisableNoise = toggle;
|
||||
}
|
||||
|
||||
public void SetParameters(Color overlayColor, Color lightingColor, float noiseAmount, float noiseMultiplier)
|
||||
|
|
@ -47,8 +58,8 @@ public sealed partial class NightVisionOverlay : Overlay
|
|||
if (isSpace)
|
||||
{
|
||||
_nightVisionShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
|
||||
_nightVisionShader.SetParameter("noise_amount", NoiseAmount);
|
||||
_nightVisionShader.SetParameter("noise_multiplier", NoiseMultiplier);
|
||||
_nightVisionShader.SetParameter("noise_amount", !DisableNoise ? NoiseAmount : 0);
|
||||
_nightVisionShader.SetParameter("noise_multiplier", !DisableNoise ? NoiseMultiplier : 0);
|
||||
handle.UseShader(_nightVisionShader);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Overlays;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
|
||||
/// <summary>
|
||||
/// Shows/hides the <see cref="NightVisionOverlay"/> based on whether the observed
|
||||
/// entity has a <see cref="NightVisionComponent"/> equipped.
|
||||
/// </summary>
|
||||
public sealed partial class NightVisionOverlaySystem : EquipmentHudSystem<NightVisionComponent>
|
||||
{
|
||||
[Dependency] private IOverlayManager _overlayMan = default!;
|
||||
|
||||
private NightVisionOverlay _overlay = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_overlay = new NightVisionOverlay();
|
||||
|
||||
SubscribeLocalEvent<NightVisionComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
||||
}
|
||||
|
||||
protected override void UpdateInternal(RefreshEquipmentHudEvent<NightVisionComponent> component)
|
||||
{
|
||||
base.UpdateInternal(component);
|
||||
|
||||
// Find the component with the lowest noise.
|
||||
NightVisionComponent? nvision = null;
|
||||
var bestNoise = float.MaxValue;
|
||||
foreach (var comp in component.Components)
|
||||
{
|
||||
if (!comp.Enabled)
|
||||
continue;
|
||||
|
||||
var noise = comp.NoiseAmount * comp.NoiseMultiplier;
|
||||
if (noise < bestNoise)
|
||||
{
|
||||
nvision = comp;
|
||||
bestNoise = noise;
|
||||
}
|
||||
}
|
||||
|
||||
// There is no active night vision components, so we disable the overlay.
|
||||
if (nvision == null)
|
||||
{
|
||||
DeactivateInternal();
|
||||
return;
|
||||
}
|
||||
|
||||
_overlay.SetParameters(nvision.OverlayColor, nvision.LightingColor, nvision.NoiseAmount, nvision.NoiseMultiplier);
|
||||
|
||||
if (!_overlayMan.HasOverlay<NightVisionOverlay>())
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
protected override void DeactivateInternal()
|
||||
{
|
||||
base.DeactivateInternal();
|
||||
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
}
|
||||
|
||||
private void OnHandleState(Entity<NightVisionComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
RefreshOverlay();
|
||||
}
|
||||
}
|
||||
|
|
@ -48,12 +48,12 @@
|
|||
</ContainerButton>
|
||||
</BoxContainer>
|
||||
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="True">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="50 50"
|
||||
Name="ProgramList"
|
||||
Margin="4"/>
|
||||
<GridContainer VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="50 50"
|
||||
Name="ProgramList"
|
||||
Margin="4"
|
||||
Columns="2" />
|
||||
</ScrollContainer>
|
||||
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="True">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
|
|
|
|||
|
|
@ -200,10 +200,10 @@ namespace Content.Client.PDA
|
|||
|
||||
public void UpdateAvailablePrograms(List<(EntityUid, CartridgeComponent)> programs)
|
||||
{
|
||||
ProgramList.RemoveAllChildren();
|
||||
|
||||
if (programs.Count == 0)
|
||||
{
|
||||
ProgramList.RemoveAllChildren();
|
||||
|
||||
ProgramList.AddChild(new Label()
|
||||
{
|
||||
Text = Loc.GetString("comp-pda-io-no-programs-available"),
|
||||
|
|
@ -215,50 +215,43 @@ namespace Content.Client.PDA
|
|||
return;
|
||||
}
|
||||
|
||||
var row = CreateProgramListRow();
|
||||
var itemCount = 1;
|
||||
ProgramList.AddChild(row);
|
||||
|
||||
foreach (var (uid, component) in programs)
|
||||
if (ProgramList.ChildCount >= 1 && ProgramList.Children[0] is Label label)
|
||||
{
|
||||
//Create a new row every second program item starting from the first
|
||||
if (itemCount % 2 != 0)
|
||||
label.Orphan();
|
||||
}
|
||||
|
||||
while (ProgramList.ChildCount > programs.Count)
|
||||
{
|
||||
ProgramList.Children[ProgramList.ChildCount - 1].Orphan();
|
||||
}
|
||||
|
||||
for (var i = 0; i < programs.Count; i++)
|
||||
{
|
||||
var cartridge = programs[i];
|
||||
|
||||
if (i < ProgramList.ChildCount)
|
||||
{
|
||||
row = CreateProgramListRow();
|
||||
ProgramList.AddChild(row);
|
||||
var currentItem = ProgramList.Children[i];
|
||||
if (currentItem is PdaProgramItem programItem)
|
||||
{
|
||||
programItem.SetCartridge(cartridge);
|
||||
continue;
|
||||
}
|
||||
|
||||
DebugTools.Assert(i == ProgramList.ChildCount-1);
|
||||
currentItem.Orphan();
|
||||
}
|
||||
|
||||
var item = new PdaProgramItem();
|
||||
|
||||
if (component.Icon is not null)
|
||||
item.Icon.SetFromSpriteSpecifier(component.Icon);
|
||||
|
||||
item.OnPressed += _ => OnProgramItemPressed?.Invoke(uid);
|
||||
|
||||
switch (component.InstallationStatus)
|
||||
{
|
||||
case InstallationStatus.Cartridge:
|
||||
item.InstallButton.Visible = true;
|
||||
item.InstallButton.Text = Loc.GetString("cartridge-bound-user-interface-install-button");
|
||||
item.InstallButton.OnPressed += _ => OnInstallButtonPressed?.Invoke(uid);
|
||||
break;
|
||||
case InstallationStatus.Installed:
|
||||
item.InstallButton.Visible = true;
|
||||
item.InstallButton.Text = Loc.GetString("cartridge-bound-user-interface-uninstall-button");
|
||||
item.InstallButton.OnPressed += _ => OnUninstallButtonPressed?.Invoke(uid);
|
||||
break;
|
||||
}
|
||||
|
||||
item.ProgramName.Text = Loc.GetString(component.ProgramName);
|
||||
item.SetHeight = 20;
|
||||
row.AddChild(item);
|
||||
|
||||
itemCount++;
|
||||
var item = new PdaProgramItem(cartridge);
|
||||
item.OnProgramItemPressed += uid => OnProgramItemPressed?.Invoke(uid);
|
||||
item.OnUninstallButtonPressed += uid => OnUninstallButtonPressed?.Invoke(uid);
|
||||
item.OnInstallButtonPressed += uid => OnInstallButtonPressed?.Invoke(uid);
|
||||
ProgramList.AddChild(item);
|
||||
}
|
||||
|
||||
//Add a filler item to the last row when it only contains one item
|
||||
if (itemCount % 2 == 0)
|
||||
row.AddChild(new Control() { HorizontalExpand = true });
|
||||
if (programs.Count % 2 == 0)
|
||||
ProgramList.AddChild(new Control() { HorizontalExpand = true });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -319,15 +312,6 @@ namespace Content.Client.PDA
|
|||
_currentView = view;
|
||||
}
|
||||
|
||||
private static BoxContainer CreateProgramListRow()
|
||||
{
|
||||
return new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
}
|
||||
|
||||
private void HideAllViews()
|
||||
{
|
||||
var views = ViewContainer.Children;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<pda:PdaProgramItem HorizontalExpand="True" MinHeight="60" Margin="4"
|
||||
xmlns:pda="clr-namespace:Content.Client.PDA"
|
||||
xmlns="https://spacestation14.io">
|
||||
<ContainerButton HorizontalExpand="True" MinHeight="60" Margin="4"
|
||||
xmlns:pda="clr-namespace:Content.Client.PDA"
|
||||
xmlns="https://spacestation14.io">
|
||||
<PanelContainer Name="Panel"/>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" MinWidth="60">
|
||||
<AnimatedTextureRect HorizontalAlignment="Center" VerticalAlignment="Center" VerticalExpand="True" Name="Icon" Access="Public"/>
|
||||
<AnimatedTextureRect HorizontalAlignment="Center" VerticalAlignment="Center" VerticalExpand="True" Name="Icon" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
|
||||
<Label Name="ProgramName" Access="Public" VerticalExpand="True"/>
|
||||
<Label Name="ProgramName" VerticalExpand="True" />
|
||||
<BoxContainer HorizontalExpand="True" SetHeight="28" Margin="0 0 4 4">
|
||||
<BoxContainer HorizontalExpand="True"/>
|
||||
<Button Name="InstallButton" Access="Public" Visible="False" SetWidth="90" HorizontalAlignment="Right" VerticalExpand="True"></Button>
|
||||
<BoxContainer HorizontalExpand="True" />
|
||||
<Button Name="InstallButton" Visible="False" SetWidth="90" HorizontalAlignment="Right" VerticalExpand="True"></Button>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</pda:PdaProgramItem>
|
||||
</ContainerButton>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
|
@ -12,6 +13,12 @@ public sealed partial class PdaProgramItem : ContainerButton
|
|||
public const string NormalBgColor = "#313138";
|
||||
public const string HoverColor = "#3E6C45";
|
||||
|
||||
private Entity<CartridgeComponent> _cartridge;
|
||||
|
||||
public event Action<EntityUid>? OnProgramItemPressed;
|
||||
public event Action<EntityUid>? OnUninstallButtonPressed;
|
||||
public event Action<EntityUid>? OnInstallButtonPressed;
|
||||
|
||||
private readonly StyleBoxFlat _styleBox = new()
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#25252a"),
|
||||
|
|
@ -23,10 +30,38 @@ public sealed partial class PdaProgramItem : ContainerButton
|
|||
set => _styleBox.BackgroundColor = value;
|
||||
}
|
||||
|
||||
public PdaProgramItem()
|
||||
public PdaProgramItem(Entity<CartridgeComponent> program)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
SetCartridge(program);
|
||||
Panel.PanelOverride = _styleBox;
|
||||
|
||||
OnPressed += OnSelfPressed;
|
||||
InstallButton.OnPressed += OnInstallPressed;
|
||||
}
|
||||
|
||||
private void OnSelfPressed(ButtonEventArgs args)
|
||||
{
|
||||
OnProgramItemPressed?.Invoke(_cartridge);
|
||||
}
|
||||
|
||||
private void OnInstallPressed(ButtonEventArgs args)
|
||||
{
|
||||
switch (_cartridge.Comp.InstallationStatus)
|
||||
{
|
||||
case InstallationStatus.Cartridge:
|
||||
OnInstallButtonPressed?.Invoke(_cartridge.Owner);
|
||||
break;
|
||||
|
||||
case InstallationStatus.Installed:
|
||||
OnUninstallButtonPressed?.Invoke(_cartridge.Owner);
|
||||
break;
|
||||
|
||||
case InstallationStatus.Readonly:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
|
|
@ -35,6 +70,29 @@ public sealed partial class PdaProgramItem : ContainerButton
|
|||
|
||||
if (TryGetStyleProperty<Color>(StylePropertyBgColor, out var bgColor))
|
||||
BackgroundColor = bgColor;
|
||||
}
|
||||
|
||||
public void SetCartridge(Entity<CartridgeComponent> program)
|
||||
{
|
||||
if (program.Comp.Icon is { } icon)
|
||||
Icon.SetFromSpriteSpecifier(icon);
|
||||
|
||||
_cartridge = program;
|
||||
ProgramName.Text = Loc.GetString(program.Comp.ProgramName);
|
||||
|
||||
switch (program.Comp.InstallationStatus)
|
||||
{
|
||||
case InstallationStatus.Cartridge:
|
||||
InstallButton.Visible = true;
|
||||
InstallButton.Text = Loc.GetString("cartridge-bound-user-interface-install-button");
|
||||
break;
|
||||
case InstallationStatus.Installed:
|
||||
InstallButton.Visible = true;
|
||||
InstallButton.Text = Loc.GetString("cartridge-bound-user-interface-uninstall-button");
|
||||
break;
|
||||
case InstallationStatus.Readonly:
|
||||
InstallButton.Visible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,10 +107,12 @@ public sealed partial class PaperVisualsComponent : Component
|
|||
public int ContentImageNumLines = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Modulate the style's font by this color
|
||||
/// Optional default color for text written on the paper. Useful to
|
||||
/// specify light color text for use on dark backgrounds. Paper text
|
||||
/// can still use markup to override color for subsections.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Color FontAccentColor = new Color(223, 223, 213);
|
||||
public Color? DefaultTextColor = null;
|
||||
|
||||
/// <summary>
|
||||
/// This can enforce that your paper has a limited area to write in.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ namespace Content.Client.Paper.UI
|
|||
|
||||
private static Color DefaultTextColor = new(25, 25, 25);
|
||||
|
||||
// <summary>
|
||||
// Default color for text which hasn't been changed using markup
|
||||
private Color _writtenTextColor = DefaultTextColor;
|
||||
|
||||
// Size of resize handles around the paper
|
||||
private const int DRAG_MARGIN_SIZE = 16;
|
||||
|
||||
|
|
@ -152,8 +154,7 @@ namespace Content.Client.Paper.UI
|
|||
visuals.FooterMargin.Right, visuals.FooterMargin.Bottom);
|
||||
|
||||
PaperContent.ModulateSelfOverride = visuals.ContentImageModulate;
|
||||
WrittenTextLabel.ModulateSelfOverride = visuals.FontAccentColor;
|
||||
FillStatus.ModulateSelfOverride = visuals.FontAccentColor;
|
||||
_writtenTextColor = visuals.DefaultTextColor ?? DefaultTextColor;
|
||||
|
||||
var contentImage = visuals.ContentImagePath != null ? _resCache.GetResource<TextureResource>(visuals.ContentImagePath) : null;
|
||||
if (contentImage != null)
|
||||
|
|
@ -271,7 +272,7 @@ namespace Content.Client.Paper.UI
|
|||
{
|
||||
msg.AddMarkupPermissive("\r\n");
|
||||
}
|
||||
WrittenTextLabel.SetMessage(msg, UserFormattableTags.BaseAllowedTags, DefaultTextColor);
|
||||
WrittenTextLabel.SetMessage(msg, UserFormattableTags.BaseAllowedTags, _writtenTextColor);
|
||||
|
||||
WrittenTextLabel.Visible = !isEditing && state.Text.Length > 0;
|
||||
BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0;
|
||||
|
|
|
|||
|
|
@ -12,358 +12,369 @@ using Robust.Shared.Configuration;
|
|||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Replays;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Popups
|
||||
namespace Content.Client.Popups;
|
||||
|
||||
public sealed partial class PopupSystem : SharedPopupSystem
|
||||
{
|
||||
public sealed partial class PopupSystem : SharedPopupSystem
|
||||
[Dependency] private IConfigurationManager _configManager = default!;
|
||||
[Dependency] private IInputManager _inputManager = default!;
|
||||
[Dependency] private IOverlayManager _overlay = default!;
|
||||
[Dependency] private IPlayerManager _playerManager = default!;
|
||||
[Dependency] private IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private IReplayRecordingManager _replayRecording = default!;
|
||||
[Dependency] private ExamineSystemShared _examine = default!;
|
||||
[Dependency] private SharedTransformSystem _transform = default!;
|
||||
|
||||
public IReadOnlyCollection<WorldPopupLabel> WorldLabels => _aliveWorldLabels.Values;
|
||||
public IReadOnlyCollection<CursorPopupLabel> CursorLabels => _aliveCursorLabels.Values;
|
||||
|
||||
private readonly Dictionary<WorldPopupData, WorldPopupLabel> _aliveWorldLabels = new();
|
||||
private readonly Dictionary<CursorPopupData, CursorPopupLabel> _aliveCursorLabels = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of popups that have been predicted by the client.
|
||||
/// If a popup is received from the server that matches one of these, it will be ignored to prevent duplicates.
|
||||
/// </summary>
|
||||
private readonly List<IPopupPredictionInstance> _predictionInstances = new();
|
||||
|
||||
public const float MinimumPopupLifetime = 0.7f;
|
||||
public const float MaximumPopupLifetime = 5f;
|
||||
public const float PopupLifetimePerCharacter = 0.04f;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private IConfigurationManager _configManager = default!;
|
||||
[Dependency] private IInputManager _inputManager = default!;
|
||||
[Dependency] private IOverlayManager _overlay = default!;
|
||||
[Dependency] private IPlayerManager _playerManager = default!;
|
||||
[Dependency] private IGameTiming _timing = default!;
|
||||
[Dependency] private IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private IReplayRecordingManager _replayRecording = default!;
|
||||
[Dependency] private ExamineSystemShared _examine = default!;
|
||||
[Dependency] private SharedTransformSystem _transform = default!;
|
||||
base.Initialize();
|
||||
|
||||
public IReadOnlyCollection<WorldPopupLabel> WorldLabels => _aliveWorldLabels.Values;
|
||||
public IReadOnlyCollection<CursorPopupLabel> CursorLabels => _aliveCursorLabels.Values;
|
||||
|
||||
private readonly Dictionary<WorldPopupData, WorldPopupLabel> _aliveWorldLabels = new();
|
||||
private readonly Dictionary<CursorPopupData, CursorPopupLabel> _aliveCursorLabels = new();
|
||||
|
||||
public const float MinimumPopupLifetime = 0.7f;
|
||||
public const float MaximumPopupLifetime = 5f;
|
||||
public const float PopupLifetimePerCharacter = 0.04f;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeNetworkEvent<PopupCursorEvent>(OnPopupCursorEvent);
|
||||
SubscribeNetworkEvent<PopupCoordinatesEvent>(OnPopupCoordinatesEvent);
|
||||
SubscribeNetworkEvent<PopupEntityEvent>(OnPopupEntityEvent);
|
||||
SubscribeNetworkEvent<RoundRestartCleanupEvent>(OnRoundRestart);
|
||||
_overlay
|
||||
.AddOverlay(new PopupOverlay(
|
||||
_configManager,
|
||||
EntityManager,
|
||||
_playerManager,
|
||||
ProtoMan,
|
||||
_uiManager,
|
||||
_uiManager.GetUIController<PopupUIController>(),
|
||||
_examine,
|
||||
_transform,
|
||||
this));
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_overlay
|
||||
.RemoveOverlay<PopupOverlay>();
|
||||
}
|
||||
|
||||
private void WrapAndRepeatPopup(PopupLabel existingLabel, string popupMessage)
|
||||
{
|
||||
existingLabel.TotalTime = 0;
|
||||
existingLabel.Repeats += 1;
|
||||
existingLabel.Text = Loc.GetString("popup-system-repeated-popup-stacking-wrap",
|
||||
("popup-message", popupMessage),
|
||||
("count", existingLabel.Repeats));
|
||||
}
|
||||
|
||||
private void PopupMessage(string? message, PopupType type, EntityCoordinates coordinates, EntityUid? entity, bool recordReplay)
|
||||
{
|
||||
if (message == null)
|
||||
return;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
if (entity != null)
|
||||
_replayRecording.RecordClientMessage(new PopupEntityEvent(message, type, GetNetEntity(entity.Value)));
|
||||
else
|
||||
_replayRecording.RecordClientMessage(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)));
|
||||
}
|
||||
|
||||
var popupData = new WorldPopupData(message, type, coordinates, entity);
|
||||
if (_aliveWorldLabels.TryGetValue(popupData, out var existingLabel))
|
||||
{
|
||||
WrapAndRepeatPopup(existingLabel, popupData.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
var label = new WorldPopupLabel(coordinates)
|
||||
{
|
||||
Text = message,
|
||||
Type = type,
|
||||
};
|
||||
|
||||
_aliveWorldLabels.Add(popupData, label);
|
||||
}
|
||||
|
||||
#region Abstract Method Implementations
|
||||
public override void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small)
|
||||
{
|
||||
PopupMessage(message, type, coordinates, null, true);
|
||||
}
|
||||
|
||||
public override void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (_playerManager.LocalSession == recipient)
|
||||
PopupMessage(message, type, coordinates, null, true);
|
||||
}
|
||||
|
||||
public override void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (_playerManager.LocalEntity == recipient)
|
||||
PopupMessage(message, type, coordinates, null, true);
|
||||
}
|
||||
|
||||
public override void PopupPredictedCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (recipient != null && _timing.IsFirstTimePredicted)
|
||||
PopupCoordinates(message, coordinates, recipient.Value, type);
|
||||
}
|
||||
|
||||
private void PopupCursorInternal(string? message, PopupType type, bool recordReplay)
|
||||
{
|
||||
if (message == null)
|
||||
return;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
_replayRecording.RecordClientMessage(new PopupCursorEvent(message, type));
|
||||
|
||||
var popupData = new CursorPopupData(message, type);
|
||||
if (_aliveCursorLabels.TryGetValue(popupData, out var existingLabel))
|
||||
{
|
||||
WrapAndRepeatPopup(existingLabel, popupData.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
var label = new CursorPopupLabel(_inputManager.MouseScreenPosition)
|
||||
{
|
||||
Text = message,
|
||||
Type = type,
|
||||
};
|
||||
|
||||
_aliveCursorLabels.Add(popupData, label);
|
||||
}
|
||||
|
||||
public override void PopupCursor(string? message, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (!_timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
PopupCursorInternal(message, type, true);
|
||||
}
|
||||
|
||||
public override void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (_playerManager.LocalSession == recipient)
|
||||
PopupCursor(message, type);
|
||||
}
|
||||
|
||||
public override void PopupCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (_playerManager.LocalEntity == recipient)
|
||||
PopupCursor(message, type);
|
||||
}
|
||||
|
||||
public override void PopupPredictedCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
PopupCursor(message, recipient, type);
|
||||
}
|
||||
|
||||
public override void PopupPredictedCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
PopupCursor(message, recipient, type);
|
||||
}
|
||||
|
||||
public override void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool replayRecord, PopupType type = PopupType.Small)
|
||||
{
|
||||
PopupCoordinates(message, coordinates, type);
|
||||
}
|
||||
|
||||
public override void PopupEntity(string? message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (_playerManager.LocalEntity == recipient)
|
||||
PopupEntity(message, uid, type);
|
||||
}
|
||||
|
||||
public override void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (_playerManager.LocalSession == recipient)
|
||||
PopupEntity(message, uid, type);
|
||||
}
|
||||
|
||||
public override void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (!filter.Recipients.Contains(_playerManager.LocalSession))
|
||||
return;
|
||||
|
||||
PopupEntity(message, uid, type);
|
||||
}
|
||||
|
||||
public override void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (recipient == null)
|
||||
return;
|
||||
|
||||
if (_timing.IsFirstTimePredicted)
|
||||
PopupCursor(message, recipient.Value, type);
|
||||
}
|
||||
|
||||
public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (recipient == null)
|
||||
return;
|
||||
|
||||
if (_timing.IsFirstTimePredicted)
|
||||
PopupEntity(message, uid, recipient.Value, type);
|
||||
}
|
||||
|
||||
public override void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (recipient == null)
|
||||
return;
|
||||
|
||||
if (_timing.IsFirstTimePredicted)
|
||||
PopupCoordinates(message, coordinates, recipient.Value, type);
|
||||
}
|
||||
|
||||
public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (TryComp(uid, out TransformComponent? transform))
|
||||
PopupMessage(message, type, transform.Coordinates, uid, true);
|
||||
}
|
||||
|
||||
public override void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (recipient != null && _timing.IsFirstTimePredicted)
|
||||
PopupEntity(message, uid, recipient.Value, type);
|
||||
}
|
||||
|
||||
public override void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, Filter filter, bool recordReplay, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (recipient != null && _timing.IsFirstTimePredicted)
|
||||
PopupEntity(message, uid, recipient.Value, type);
|
||||
}
|
||||
|
||||
public override void PopupPredicted(string? recipientMessage, string? othersMessage, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (recipient != null && _timing.IsFirstTimePredicted)
|
||||
PopupEntity(recipientMessage, uid, recipient.Value, type);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network Event Handlers
|
||||
|
||||
private void OnPopupCursorEvent(PopupCursorEvent ev)
|
||||
{
|
||||
PopupCursorInternal(ev.Message, ev.Type, false);
|
||||
}
|
||||
|
||||
private void OnPopupCoordinatesEvent(PopupCoordinatesEvent ev)
|
||||
{
|
||||
PopupMessage(ev.Message, ev.Type, GetCoordinates(ev.Coordinates), null, false);
|
||||
}
|
||||
|
||||
private void OnPopupEntityEvent(PopupEntityEvent ev)
|
||||
{
|
||||
var entity = GetEntity(ev.Uid);
|
||||
|
||||
if (TryComp(entity, out TransformComponent? transform))
|
||||
PopupMessage(ev.Message, ev.Type, transform.Coordinates, entity, false);
|
||||
}
|
||||
|
||||
private void OnRoundRestart(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
_aliveCursorLabels.Clear();
|
||||
_aliveWorldLabels.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static float GetPopupLifetime(PopupLabel label)
|
||||
{
|
||||
return Math.Clamp(PopupLifetimePerCharacter * label.Text.Length,
|
||||
MinimumPopupLifetime,
|
||||
MaximumPopupLifetime);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
if (_aliveWorldLabels.Count == 0 && _aliveCursorLabels.Count == 0)
|
||||
return;
|
||||
|
||||
if (_aliveWorldLabels.Count > 0)
|
||||
{
|
||||
var aliveWorldToRemove = new ValueList<WorldPopupData>();
|
||||
foreach (var (data, label) in _aliveWorldLabels)
|
||||
{
|
||||
label.TotalTime += frameTime;
|
||||
if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId))
|
||||
{
|
||||
aliveWorldToRemove.Add(data);
|
||||
}
|
||||
}
|
||||
foreach (var data in aliveWorldToRemove)
|
||||
{
|
||||
_aliveWorldLabels.Remove(data);
|
||||
}
|
||||
}
|
||||
|
||||
if (_aliveCursorLabels.Count > 0)
|
||||
{
|
||||
var aliveCursorToRemove = new ValueList<CursorPopupData>();
|
||||
foreach (var (data, label) in _aliveCursorLabels)
|
||||
{
|
||||
label.TotalTime += frameTime;
|
||||
if (label.TotalTime > GetPopupLifetime(label))
|
||||
{
|
||||
aliveCursorToRemove.Add(data);
|
||||
}
|
||||
}
|
||||
foreach (var data in aliveCursorToRemove)
|
||||
{
|
||||
_aliveCursorLabels.Remove(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class PopupLabel
|
||||
{
|
||||
public PopupType Type = PopupType.Small;
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public float TotalTime { get; set; }
|
||||
public int Repeats = 1;
|
||||
}
|
||||
|
||||
public sealed class WorldPopupLabel(EntityCoordinates coordinates) : PopupLabel
|
||||
{
|
||||
/// <summary>
|
||||
/// The original EntityCoordinates of the label.
|
||||
/// </summary>
|
||||
public EntityCoordinates InitialPos = coordinates;
|
||||
}
|
||||
|
||||
public sealed class CursorPopupLabel(ScreenCoordinates screenCoords) : PopupLabel
|
||||
{
|
||||
public ScreenCoordinates InitialPos = screenCoords;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
private record struct WorldPopupData(
|
||||
string Message,
|
||||
PopupType Type,
|
||||
EntityCoordinates Coordinates,
|
||||
EntityUid? Entity);
|
||||
|
||||
[UsedImplicitly]
|
||||
private record struct CursorPopupData(
|
||||
string Message,
|
||||
PopupType Type);
|
||||
_overlay.AddOverlay(new PopupOverlay(
|
||||
_configManager,
|
||||
EntityManager,
|
||||
_playerManager,
|
||||
ProtoMan,
|
||||
_uiManager,
|
||||
_uiManager.GetUIController<PopupUIController>(),
|
||||
_examine,
|
||||
_transform,
|
||||
this));
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
_overlay.RemoveOverlay<PopupOverlay>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the same popup is repeated, this will make show x2, x3, x4, ... at the end of the message instead of creating a new, overlapping popup.
|
||||
/// </summary>
|
||||
private void WrapAndRepeatPopup(PopupLabel existingLabel, string popupMessage)
|
||||
{
|
||||
existingLabel.TotalTime = 0;
|
||||
existingLabel.Repeats += 1;
|
||||
existingLabel.Text = Loc.GetString("popup-system-repeated-popup-stacking-wrap",
|
||||
("popup-message", popupMessage),
|
||||
("count", existingLabel.Repeats));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interal implementation for both coordinates and entity popups.
|
||||
/// </summary>
|
||||
private void PopupInternal(string? message, PopupType type, EntityCoordinates coordinates, EntityUid? entity, bool recordReplay)
|
||||
{
|
||||
if (message == null)
|
||||
return;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
{
|
||||
if (entity != null)
|
||||
_replayRecording.RecordClientMessage(new PopupEntityEvent(message, type, Timing.CurTick, GetNetEntity(entity.Value)));
|
||||
else
|
||||
_replayRecording.RecordClientMessage(new PopupCoordinatesEvent(message, type, Timing.CurTick, GetNetCoordinates(coordinates), 0));
|
||||
}
|
||||
|
||||
var popupData = new WorldPopupData(message, type, coordinates, entity);
|
||||
if (_aliveWorldLabels.TryGetValue(popupData, out var existingLabel))
|
||||
{
|
||||
WrapAndRepeatPopup(existingLabel, popupData.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
var label = new WorldPopupLabel(coordinates)
|
||||
{
|
||||
Text = message,
|
||||
Type = type,
|
||||
};
|
||||
|
||||
_aliveWorldLabels.Add(popupData, label);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal implementation for cursor popups.
|
||||
/// </summary>
|
||||
private void PopupCursorInternal(string? message, PopupType type, bool recordReplay)
|
||||
{
|
||||
if (message == null)
|
||||
return;
|
||||
|
||||
if (recordReplay && _replayRecording.IsRecording)
|
||||
_replayRecording.RecordClientMessage(new PopupCursorEvent(message, type, Timing.CurTick));
|
||||
|
||||
var popupData = new CursorPopupData(message, type);
|
||||
if (_aliveCursorLabels.TryGetValue(popupData, out var existingLabel))
|
||||
{
|
||||
WrapAndRepeatPopup(existingLabel, popupData.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
var label = new CursorPopupLabel(_inputManager.MouseScreenPosition)
|
||||
{
|
||||
Text = message,
|
||||
Type = type,
|
||||
};
|
||||
|
||||
_aliveCursorLabels.Add(popupData, label);
|
||||
}
|
||||
|
||||
#region Abstract Method Implementations
|
||||
|
||||
/// <summary>
|
||||
/// Shows a popup at the local user's cursor.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This overload only exists on the client. If you want to use this in Shared you should use the overload that takes a recipient, session or filter instead.
|
||||
/// We do not add a virtual method to the shared system because that will cause problems if the shared code is run in a non-predicted way.
|
||||
/// </remarks>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public void PopupCursor(string? message, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (!Timing.IsFirstTimePredicted || message is null)
|
||||
return;
|
||||
|
||||
_predictionInstances.Add(new PopupCursorEvent.PredictionInstance(message, type, Timing.CurTick));
|
||||
PopupCursorInternal(message, type, true);
|
||||
}
|
||||
|
||||
public override void PopupCursor(string? message, EntityUid? recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (_playerManager.LocalEntity == recipient)
|
||||
PopupCursor(message, type);
|
||||
}
|
||||
|
||||
public override void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (_playerManager.LocalSession == recipient)
|
||||
PopupCursor(message, type);
|
||||
}
|
||||
|
||||
public override void PopupCursor(string? message, Filter filter, bool recordReplay, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (filter.Recipients.Contains(_playerManager.LocalSession))
|
||||
PopupCursor(message, type);
|
||||
}
|
||||
|
||||
public override void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small, int predictionKey = 0)
|
||||
{
|
||||
if (!Timing.IsFirstTimePredicted || message is null)
|
||||
return;
|
||||
|
||||
_predictionInstances.Add(new PopupCoordinatesEvent.PredictionInstance(message, type, Timing.CurTick, predictionKey));
|
||||
PopupInternal(message, type, coordinates, null, true);
|
||||
}
|
||||
|
||||
public override void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small, int predictionKey = 0)
|
||||
{
|
||||
if (_playerManager.LocalSession == recipient)
|
||||
PopupCoordinates(message, coordinates, type, predictionKey);
|
||||
}
|
||||
|
||||
public override void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small, int predictionKey = 0)
|
||||
{
|
||||
if (_playerManager.LocalEntity == recipient)
|
||||
PopupCoordinates(message, coordinates, type, predictionKey);
|
||||
}
|
||||
|
||||
public override void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool replayRecord, PopupType type = PopupType.Small, int predictionKey = 0)
|
||||
{
|
||||
if (filter.Recipients.Contains(_playerManager.LocalSession))
|
||||
PopupCoordinates(message, coordinates, type, predictionKey);
|
||||
}
|
||||
|
||||
public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (!Timing.IsFirstTimePredicted || message is null)
|
||||
return;
|
||||
|
||||
if (!TryComp(uid, out TransformComponent? transform))
|
||||
return;
|
||||
|
||||
_predictionInstances.Add(new PopupEntityEvent.PredictionInstance(message, type, Timing.CurTick, GetNetEntity(uid)));
|
||||
PopupInternal(message, type, transform.Coordinates, uid, true);
|
||||
}
|
||||
|
||||
public override void PopupEntity(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (_playerManager.LocalEntity == recipient)
|
||||
PopupEntity(message, uid, type);
|
||||
}
|
||||
|
||||
public override void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (_playerManager.LocalSession == recipient)
|
||||
PopupEntity(message, uid, type);
|
||||
}
|
||||
|
||||
public override void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small)
|
||||
{
|
||||
if (filter.Recipients.Contains(_playerManager.LocalSession))
|
||||
PopupEntity(message, uid, type);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network Event Handlers
|
||||
|
||||
[SubscribeNetworkEvent]
|
||||
private void OnPopupCursorEvent(PopupCursorEvent ev)
|
||||
{
|
||||
var instance = new PopupCursorEvent.PredictionInstance(ev.Message, ev.Type, ev.Tick);
|
||||
if (_predictionInstances.Remove(instance))
|
||||
return;
|
||||
|
||||
PopupCursorInternal(ev.Message, ev.Type, false);
|
||||
}
|
||||
|
||||
[SubscribeNetworkEvent]
|
||||
private void OnPopupCoordinatesEvent(PopupCoordinatesEvent ev)
|
||||
{
|
||||
var instance = new PopupCoordinatesEvent.PredictionInstance(ev.Message, ev.Type, ev.Tick, ev.PredictionKey);
|
||||
if (_predictionInstances.Remove(instance))
|
||||
return;
|
||||
|
||||
PopupInternal(ev.Message, ev.Type, GetCoordinates(ev.Coordinates), null, false);
|
||||
}
|
||||
|
||||
[SubscribeNetworkEvent]
|
||||
private void OnPopupEntityEvent(PopupEntityEvent ev)
|
||||
{
|
||||
var instance = new PopupEntityEvent.PredictionInstance(ev.Message, ev.Type, ev.Tick, ev.Uid);
|
||||
if (_predictionInstances.Remove(instance))
|
||||
return;
|
||||
|
||||
var entity = GetEntity(ev.Uid);
|
||||
|
||||
if (TryComp(entity, out TransformComponent? transform))
|
||||
PopupInternal(ev.Message, ev.Type, transform.Coordinates, entity, false);
|
||||
}
|
||||
|
||||
[SubscribeNetworkEvent]
|
||||
private void OnRoundRestart(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
_aliveCursorLabels.Clear();
|
||||
_aliveWorldLabels.Clear();
|
||||
_predictionInstances.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the lifetime of a popup based on its text length.
|
||||
/// </summary>
|
||||
public static float GetPopupLifetime(PopupLabel label)
|
||||
{
|
||||
return Math.Clamp(PopupLifetimePerCharacter * label.Text.Length,
|
||||
MinimumPopupLifetime,
|
||||
MaximumPopupLifetime);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
if (!Timing.IsFirstTimePredicted)
|
||||
return; // We only need to clean up once per tick.
|
||||
|
||||
|
||||
// We only keep track of prediction instances for a short amount of time to prevent memory leaks.
|
||||
// We can safely assume that if a popup hasn't been received from the server within a 10 seconds, it will never be received.
|
||||
var deleteTickCount = Timing.TickRate * 10;
|
||||
if (_predictionInstances.Count != 0)
|
||||
{
|
||||
_predictionInstances.RemoveAll(p => (int)Timing.CurTick.Value - (int)p.Tick.Value > deleteTickCount);
|
||||
}
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
if (_aliveWorldLabels.Count == 0 && _aliveCursorLabels.Count == 0)
|
||||
return;
|
||||
|
||||
if (_aliveWorldLabels.Count > 0)
|
||||
{
|
||||
var aliveWorldToRemove = new ValueList<WorldPopupData>();
|
||||
foreach (var (data, label) in _aliveWorldLabels)
|
||||
{
|
||||
label.TotalTime += frameTime;
|
||||
if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId))
|
||||
{
|
||||
aliveWorldToRemove.Add(data);
|
||||
}
|
||||
}
|
||||
foreach (var data in aliveWorldToRemove)
|
||||
{
|
||||
_aliveWorldLabels.Remove(data);
|
||||
}
|
||||
}
|
||||
|
||||
if (_aliveCursorLabels.Count > 0)
|
||||
{
|
||||
var aliveCursorToRemove = new ValueList<CursorPopupData>();
|
||||
foreach (var (data, label) in _aliveCursorLabels)
|
||||
{
|
||||
label.TotalTime += frameTime;
|
||||
if (label.TotalTime > GetPopupLifetime(label))
|
||||
{
|
||||
aliveCursorToRemove.Add(data);
|
||||
}
|
||||
}
|
||||
foreach (var data in aliveCursorToRemove)
|
||||
{
|
||||
_aliveCursorLabels.Remove(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class PopupLabel
|
||||
{
|
||||
public PopupType Type = PopupType.Small;
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public float TotalTime { get; set; }
|
||||
public int Repeats = 1;
|
||||
}
|
||||
|
||||
public sealed class WorldPopupLabel(EntityCoordinates coordinates) : PopupLabel
|
||||
{
|
||||
/// <summary>
|
||||
/// The original EntityCoordinates of the label.
|
||||
/// </summary>
|
||||
public EntityCoordinates InitialPos = coordinates;
|
||||
}
|
||||
|
||||
public sealed class CursorPopupLabel(ScreenCoordinates screenCoords) : PopupLabel
|
||||
{
|
||||
public ScreenCoordinates InitialPos = screenCoords;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
private record struct WorldPopupData(
|
||||
string Message,
|
||||
PopupType Type,
|
||||
EntityCoordinates Coordinates,
|
||||
EntityUid? Entity);
|
||||
|
||||
[UsedImplicitly]
|
||||
private record struct CursorPopupData(
|
||||
string Message,
|
||||
PopupType Type);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,5 @@
|
|||
using Content.Client.Power.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Wires;
|
||||
|
||||
namespace Content.Client.Power;
|
||||
|
||||
public sealed partial class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequiresPowerSystem
|
||||
{
|
||||
[Dependency] private SharedPopupSystem _popup = default!;
|
||||
|
||||
protected override void OnActivate(Entity<ActivatableUIRequiresPowerComponent> ent, ref ActivatableUIOpenAttemptEvent args)
|
||||
{
|
||||
if (args.Cancelled || this.IsPowered(ent.Owner, EntityManager))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.Silent)
|
||||
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
|
||||
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
public sealed partial class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequiresPowerSystem;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ namespace Content.Client.RCD;
|
|||
[UsedImplicitly]
|
||||
public sealed partial class RCDMenuBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[Dependency] private IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private ISharedPlayerManager _playerManager = default!;
|
||||
[Dependency] private PopupSystem _popup = default!;
|
||||
|
||||
private const string TopLevelActionCategory = "Main";
|
||||
|
||||
private static readonly Dictionary<string, (string Tooltip, SpriteSpecifier Sprite)> PrototypesGroupingInfo
|
||||
|
|
@ -26,14 +30,10 @@ public sealed partial class RCDMenuBoundUserInterface : BoundUserInterface
|
|||
["Lighting"] = ("rcd-component-lighting", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/lighting.png"))),
|
||||
};
|
||||
|
||||
[Dependency] private IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private ISharedPlayerManager _playerManager = default!;
|
||||
|
||||
private SimpleRadialMenu? _menu;
|
||||
|
||||
public RCDMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
|
|
@ -134,8 +134,7 @@ public sealed partial class RCDMenuBoundUserInterface : BoundUserInterface
|
|||
}
|
||||
|
||||
// Popup message
|
||||
var popup = EntMan.System<PopupSystem>();
|
||||
popup.PopupClient(msg, Owner, _playerManager.LocalSession.AttachedEntity);
|
||||
_popup.PopupEntity(msg, Owner);
|
||||
}
|
||||
|
||||
private string GetTooltip(RCDPrototype proto)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ public sealed partial class RoundEndSummaryUIController : UIController,
|
|||
return;
|
||||
|
||||
_window = new RoundEndSummaryWindow(message.GamemodeTitle, message.RoundEndText,
|
||||
message.RoundDuration, message.RoundId, message.AllPlayersEndInfo, EntityManager);
|
||||
message.RoundDuration, message.RoundId, message.AllPlayersEndInfo);
|
||||
}
|
||||
|
||||
public void OnSystemLoaded(ClientGameTicker system)
|
||||
|
|
|
|||
|
|
@ -1,171 +1,496 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Message;
|
||||
using Content.Shared.GameTicking;
|
||||
using RoundEndPlayerInfo = Content.Shared.GameTicking.RoundEndMessageEvent.RoundEndPlayerInfo;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.RoundEnd
|
||||
namespace Content.Client.RoundEnd;
|
||||
|
||||
/// <summary>
|
||||
/// Window displaying round end information including player manifest.
|
||||
/// </summary>
|
||||
public sealed partial class RoundEndSummaryWindow : DefaultWindow
|
||||
{
|
||||
public sealed class RoundEndSummaryWindow : DefaultWindow
|
||||
[Dependency] private IEntityManager _entityManager = default!;
|
||||
|
||||
public int RoundId;
|
||||
private readonly RoundEndPlayerInfo[] _playersInfo;
|
||||
private GridContainer _playerGrid = null!;
|
||||
private readonly List<SortButton> _sortButtons = [];
|
||||
private string _searchText = string.Empty;
|
||||
|
||||
private enum SortField
|
||||
{
|
||||
private readonly IEntityManager _entityManager;
|
||||
public int RoundId;
|
||||
ICName,
|
||||
Role,
|
||||
PlayerType,
|
||||
OOCName
|
||||
}
|
||||
|
||||
public RoundEndSummaryWindow(string gm, string roundEnd, TimeSpan roundTimeSpan, int roundId,
|
||||
RoundEndMessageEvent.RoundEndPlayerInfo[] info, IEntityManager entityManager)
|
||||
private SortField _currentSortField = SortField.PlayerType;
|
||||
private bool _sortDescending;
|
||||
|
||||
public RoundEndSummaryWindow(string gm, string roundEnd, TimeSpan roundTimeSpan, int roundId, RoundEndPlayerInfo[] info)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_playersInfo = info;
|
||||
|
||||
MinSize = SetSize = new Vector2(720, 580);
|
||||
|
||||
Title = Loc.GetString("round-end-summary-window-title");
|
||||
|
||||
// The round end window is split into two tabs, one about the round stats
|
||||
// and the other is a list of RoundEndPlayerInfo for each player.
|
||||
// This tab would be a good place for things like: "x many people died.",
|
||||
// "clown slipped the crew x times.", "x shots were fired this round.", etc.
|
||||
// Also, good for serious info.
|
||||
|
||||
RoundId = roundId;
|
||||
var roundEndTabs = new TabContainer();
|
||||
roundEndTabs.AddChild(MakeRoundEndSummaryTab(gm, roundEnd, roundTimeSpan, roundId));
|
||||
roundEndTabs.AddChild(MakePlayerManifestTab());
|
||||
|
||||
ContentsContainer.AddChild(roundEndTabs);
|
||||
|
||||
OpenCenteredRight();
|
||||
MoveToFront();
|
||||
}
|
||||
|
||||
private static BoxContainer MakeRoundEndSummaryTab(string gamemode, string roundEnd, TimeSpan roundDuration, int roundId)
|
||||
{
|
||||
var roundEndSummaryTab = new BoxContainer
|
||||
{
|
||||
_entityManager = entityManager;
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
Name = Loc.GetString("round-end-summary-window-round-end-summary-tab-title")
|
||||
};
|
||||
|
||||
MinSize = SetSize = new Vector2(520, 580);
|
||||
var roundEndSummaryContainerScrollbox = new ScrollContainer
|
||||
{
|
||||
VerticalExpand = true,
|
||||
Margin = new Thickness(10)
|
||||
};
|
||||
var roundEndSummaryContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
};
|
||||
|
||||
Title = Loc.GetString("round-end-summary-window-title");
|
||||
//Gamemode Name
|
||||
var gamemodeLabel = new RichTextLabel();
|
||||
var gamemodeMessage = new FormattedMessage();
|
||||
gamemodeMessage.AddMarkupOrThrow(Loc.GetString("round-end-summary-window-round-id-label", ("roundId", roundId)));
|
||||
gamemodeMessage.AddText(" ");
|
||||
gamemodeMessage.AddMarkupOrThrow(Loc.GetString("round-end-summary-window-gamemode-name-label", ("gamemode", gamemode)));
|
||||
gamemodeLabel.SetMessage(gamemodeMessage);
|
||||
roundEndSummaryContainer.AddChild(gamemodeLabel);
|
||||
|
||||
// The round end window is split into two tabs, one about the round stats
|
||||
// and the other is a list of RoundEndPlayerInfo for each player.
|
||||
// This tab would be a good place for things like: "x many people died.",
|
||||
// "clown slipped the crew x times.", "x shots were fired this round.", etc.
|
||||
// Also good for serious info.
|
||||
//Duration
|
||||
var roundTimeLabel = new RichTextLabel();
|
||||
roundTimeLabel.SetMarkup(Loc.GetString("round-end-summary-window-duration-label",
|
||||
("hours", roundDuration.Hours),
|
||||
("minutes", roundDuration.Minutes),
|
||||
("seconds", roundDuration.Seconds)));
|
||||
roundEndSummaryContainer.AddChild(roundTimeLabel);
|
||||
|
||||
RoundId = roundId;
|
||||
var roundEndTabs = new TabContainer();
|
||||
roundEndTabs.AddChild(MakeRoundEndSummaryTab(gm, roundEnd, roundTimeSpan, roundId));
|
||||
roundEndTabs.AddChild(MakePlayerManifestTab(info));
|
||||
|
||||
ContentsContainer.AddChild(roundEndTabs);
|
||||
|
||||
OpenCenteredRight();
|
||||
MoveToFront();
|
||||
//Round end text
|
||||
if (!string.IsNullOrEmpty(roundEnd))
|
||||
{
|
||||
var roundEndLabel = new RichTextLabel();
|
||||
roundEndLabel.SetMarkup(roundEnd);
|
||||
roundEndSummaryContainer.AddChild(roundEndLabel);
|
||||
}
|
||||
|
||||
private BoxContainer MakeRoundEndSummaryTab(string gamemode, string roundEnd, TimeSpan roundDuration, int roundId)
|
||||
roundEndSummaryContainerScrollbox.AddChild(roundEndSummaryContainer);
|
||||
roundEndSummaryTab.AddChild(roundEndSummaryContainerScrollbox);
|
||||
|
||||
return roundEndSummaryTab;
|
||||
}
|
||||
|
||||
private BoxContainer MakePlayerManifestTab()
|
||||
{
|
||||
var playerManifestTab = new BoxContainer
|
||||
{
|
||||
var roundEndSummaryTab = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
Name = Loc.GetString("round-end-summary-window-round-end-summary-tab-title")
|
||||
};
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
Name = Loc.GetString("round-end-summary-window-player-manifest-tab-title")
|
||||
};
|
||||
|
||||
var roundEndSummaryContainerScrollbox = new ScrollContainer
|
||||
{
|
||||
VerticalExpand = true,
|
||||
Margin = new Thickness(10)
|
||||
};
|
||||
var roundEndSummaryContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
};
|
||||
// Search container
|
||||
var searchContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Margin = new Thickness(10, 10, 10, 5)
|
||||
};
|
||||
|
||||
//Gamemode Name
|
||||
var gamemodeLabel = new RichTextLabel();
|
||||
var gamemodeMessage = new FormattedMessage();
|
||||
gamemodeMessage.AddMarkupOrThrow(Loc.GetString("round-end-summary-window-round-id-label", ("roundId", roundId)));
|
||||
gamemodeMessage.AddText(" ");
|
||||
gamemodeMessage.AddMarkupOrThrow(Loc.GetString("round-end-summary-window-gamemode-name-label", ("gamemode", gamemode)));
|
||||
gamemodeLabel.SetMessage(gamemodeMessage);
|
||||
roundEndSummaryContainer.AddChild(gamemodeLabel);
|
||||
var searchLabel = new Label
|
||||
{
|
||||
Text = "Filter: ",
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
MinSize = new Vector2(40, 1)
|
||||
};
|
||||
|
||||
//Duration
|
||||
var roundTimeLabel = new RichTextLabel();
|
||||
roundTimeLabel.SetMarkup(Loc.GetString("round-end-summary-window-duration-label",
|
||||
("hours", roundDuration.Hours),
|
||||
("minutes", roundDuration.Minutes),
|
||||
("seconds", roundDuration.Seconds)));
|
||||
roundEndSummaryContainer.AddChild(roundTimeLabel);
|
||||
var searchBar = new LineEdit
|
||||
{
|
||||
PlaceHolder = Loc.GetString("round-end-summary-window-player-manifest-tab-search-placeholder"),
|
||||
HorizontalExpand = true,
|
||||
MinSize = new Vector2(200, 1)
|
||||
};
|
||||
|
||||
//Round end text
|
||||
if (!string.IsNullOrEmpty(roundEnd))
|
||||
{
|
||||
var roundEndLabel = new RichTextLabel();
|
||||
roundEndLabel.SetMarkup(roundEnd);
|
||||
roundEndSummaryContainer.AddChild(roundEndLabel);
|
||||
}
|
||||
searchBar.OnTextChanged += OnSearchTextChanged;
|
||||
|
||||
roundEndSummaryContainerScrollbox.AddChild(roundEndSummaryContainer);
|
||||
roundEndSummaryTab.AddChild(roundEndSummaryContainerScrollbox);
|
||||
searchContainer.AddChild(searchLabel);
|
||||
searchContainer.AddChild(searchBar);
|
||||
playerManifestTab.AddChild(searchContainer);
|
||||
|
||||
return roundEndSummaryTab;
|
||||
// Header with sort buttons
|
||||
var headerContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Margin = new Thickness(10, 5, 10, 5)
|
||||
};
|
||||
|
||||
var icNameButton = CreateSortButton("round-end-summary-window-player-manifest-tab-sort-character", SortField.ICName);
|
||||
var roleButton = CreateSortButton("round-end-summary-window-player-manifest-tab-sort-role", SortField.Role);
|
||||
var playerTypeButton = CreateSortButton("round-end-summary-window-player-manifest-tab-sort-player-type", SortField.PlayerType);
|
||||
var oocNameButton = CreateSortButton("round-end-summary-window-player-manifest-tab-sort-player", SortField.OOCName);
|
||||
|
||||
playerTypeButton.SetSortIndicator(true);
|
||||
headerContainer.AddChild(icNameButton);
|
||||
|
||||
// Add small spacer between buttons
|
||||
headerContainer.AddChild(new Control
|
||||
{
|
||||
MinSize = new Vector2(5, 1),
|
||||
HorizontalExpand = false
|
||||
});
|
||||
|
||||
headerContainer.AddChild(roleButton);
|
||||
|
||||
// Add small spacer between buttons
|
||||
headerContainer.AddChild(new Control
|
||||
{
|
||||
MinSize = new Vector2(5, 1),
|
||||
HorizontalExpand = false
|
||||
});
|
||||
|
||||
headerContainer.AddChild(playerTypeButton);
|
||||
|
||||
// Add small spacer between buttons
|
||||
headerContainer.AddChild(new Control
|
||||
{
|
||||
MinSize = new Vector2(5, 1),
|
||||
HorizontalExpand = false
|
||||
});
|
||||
|
||||
headerContainer.AddChild(oocNameButton);
|
||||
|
||||
playerManifestTab.AddChild(headerContainer);
|
||||
|
||||
var scrollContainer = new ScrollContainer
|
||||
{
|
||||
VerticalExpand = true,
|
||||
Margin = new Thickness(10, 0, 10, 10),
|
||||
};
|
||||
|
||||
_playerGrid = new GridContainer
|
||||
{
|
||||
Columns = 6, // Player Sprite,IC Name,Role,Player Type,OOC Name
|
||||
HorizontalExpand = true,
|
||||
};
|
||||
|
||||
RefreshPlayerList();
|
||||
|
||||
scrollContainer.AddChild(_playerGrid);
|
||||
playerManifestTab.AddChild(scrollContainer);
|
||||
|
||||
return playerManifestTab;
|
||||
}
|
||||
|
||||
private SortButton CreateSortButton(string text, SortField field)
|
||||
{
|
||||
var button = new SortButton(Loc.GetString(text), field);
|
||||
button.OnPressed += _ => SortBy(field);
|
||||
_sortButtons.Add(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles sorting by the specified field, toggling direction if the same field is clicked
|
||||
/// </summary>
|
||||
private void SortBy(SortField field)
|
||||
{
|
||||
if (_currentSortField == field)
|
||||
{
|
||||
_sortDescending = !_sortDescending;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentSortField = field;
|
||||
_sortDescending = false;
|
||||
}
|
||||
|
||||
private BoxContainer MakePlayerManifestTab(RoundEndMessageEvent.RoundEndPlayerInfo[] playersInfo)
|
||||
foreach (var button in _sortButtons)
|
||||
{
|
||||
var playerManifestTab = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
Name = Loc.GetString("round-end-summary-window-player-manifest-tab-title")
|
||||
};
|
||||
button.SetSortIndicator(button.Field == _currentSortField, _sortDescending);
|
||||
}
|
||||
|
||||
var playerInfoContainerScrollbox = new ScrollContainer
|
||||
{
|
||||
VerticalExpand = true,
|
||||
Margin = new Thickness(10)
|
||||
};
|
||||
var playerInfoContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
};
|
||||
RefreshPlayerList();
|
||||
}
|
||||
|
||||
//Put observers at the bottom of the list. Put antags on top.
|
||||
var sortedPlayersInfo = playersInfo.OrderBy(p => p.Observer).ThenBy(p => !p.Antag);
|
||||
/// <summary>
|
||||
/// Refreshes the player list grid by clearing it and repopulating with sorted player data
|
||||
/// </summary>
|
||||
private void RefreshPlayerList()
|
||||
{
|
||||
_playerGrid.RemoveAllChildren();
|
||||
|
||||
//Create labels for each player info.
|
||||
foreach (var playerInfo in sortedPlayersInfo)
|
||||
{
|
||||
var hBox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
};
|
||||
|
||||
var playerInfoText = new RichTextLabel
|
||||
{
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
VerticalExpand = true,
|
||||
};
|
||||
|
||||
if (playerInfo.PlayerNetEntity != null)
|
||||
{
|
||||
hBox.AddChild(new SpriteView(playerInfo.PlayerNetEntity.Value, _entityManager)
|
||||
{
|
||||
OverrideDirection = Direction.South,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SetSize = new Vector2(32, 32),
|
||||
VerticalExpand = true,
|
||||
});
|
||||
}
|
||||
|
||||
if (playerInfo.PlayerICName != null)
|
||||
{
|
||||
if (playerInfo.Observer)
|
||||
{
|
||||
playerInfoText.SetMarkup(
|
||||
Loc.GetString("round-end-summary-window-player-info-if-observer-text",
|
||||
("playerOOCName", playerInfo.PlayerOOCName),
|
||||
("playerICName", playerInfo.PlayerICName)));
|
||||
}
|
||||
else
|
||||
{
|
||||
//TODO: On Hover display a popup detailing more play info.
|
||||
//For example: their antag goals and if they completed them sucessfully.
|
||||
var icNameColor = playerInfo.Antag ? "red" : "white";
|
||||
playerInfoText.SetMarkup(
|
||||
Loc.GetString("round-end-summary-window-player-info-if-not-observer-text",
|
||||
("playerOOCName", playerInfo.PlayerOOCName),
|
||||
("icNameColor", icNameColor),
|
||||
("playerICName", playerInfo.PlayerICName),
|
||||
("playerRole", Loc.GetString(playerInfo.Role))));
|
||||
}
|
||||
}
|
||||
hBox.AddChild(playerInfoText);
|
||||
playerInfoContainer.AddChild(hBox);
|
||||
}
|
||||
|
||||
playerInfoContainerScrollbox.AddChild(playerInfoContainer);
|
||||
playerManifestTab.AddChild(playerInfoContainerScrollbox);
|
||||
|
||||
return playerManifestTab;
|
||||
var sortedPlayers = GetSortedPlayers();
|
||||
foreach (var playerInfo in sortedPlayers)
|
||||
{
|
||||
AddPlayerRow(playerInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a single player row to the grid with all columns (sprite, IC name, role, player type, OOC name)
|
||||
/// </summary>
|
||||
private void AddPlayerRow(RoundEndPlayerInfo playerInfo)
|
||||
{
|
||||
// Player Sprite column
|
||||
if (playerInfo.PlayerNetEntity != null)
|
||||
{
|
||||
_playerGrid.AddChild(new SpriteView(playerInfo.PlayerNetEntity.Value, _entityManager)
|
||||
{
|
||||
OverrideDirection = Direction.South,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SetSize = new Vector2(32, 32),
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_playerGrid.AddChild(new Control
|
||||
{
|
||||
SetSize = new Vector2(32, 32),
|
||||
});
|
||||
}
|
||||
|
||||
// IC Name column
|
||||
var icNameLabel = new Label
|
||||
{
|
||||
Text = playerInfo.PlayerICName ?? playerInfo.PlayerOOCName,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalExpand = true,
|
||||
ClipText = true
|
||||
};
|
||||
|
||||
// Apply color coding for antagonists
|
||||
if (playerInfo.Antag)
|
||||
{
|
||||
icNameLabel.FontColorOverride = Color.Red;
|
||||
}
|
||||
|
||||
_playerGrid.AddChild(icNameLabel);
|
||||
|
||||
_playerGrid.AddChild(new Control
|
||||
{
|
||||
SetSize = new Vector2(32, 32),
|
||||
});
|
||||
|
||||
// Role column
|
||||
var roleLabel = new Label
|
||||
{
|
||||
Text = playerInfo.Observer ? "-" : Loc.GetString(playerInfo.Role),
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalExpand = true,
|
||||
ClipText = true
|
||||
};
|
||||
_playerGrid.AddChild(roleLabel);
|
||||
|
||||
// Player Type column
|
||||
var playerTypeLabel = new Label
|
||||
{
|
||||
Text = GetPlayerTypeText(playerInfo),
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalExpand = true,
|
||||
ClipText = true
|
||||
};
|
||||
|
||||
// Apply color coding based on player type
|
||||
if (playerInfo.Antag)
|
||||
{
|
||||
playerTypeLabel.FontColorOverride = Color.Red;
|
||||
}
|
||||
else if (playerInfo.Observer)
|
||||
{
|
||||
playerTypeLabel.FontColorOverride = Color.Gray;
|
||||
}
|
||||
|
||||
_playerGrid.AddChild(playerTypeLabel);
|
||||
|
||||
// OOC Name column
|
||||
var oocNameLabel = new Label
|
||||
{
|
||||
Text = playerInfo.PlayerOOCName,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalExpand = true,
|
||||
ClipText = true
|
||||
};
|
||||
|
||||
_playerGrid.AddChild(oocNameLabel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the player type text for a player based on their observer and antagonist flags
|
||||
/// </summary>
|
||||
private static string GetPlayerTypeText(RoundEndPlayerInfo playerInfo)
|
||||
{
|
||||
if (playerInfo.Observer)
|
||||
return Loc.GetString("round-end-summary-window-player-manifest-tab-sort-player-type-observer");
|
||||
if (playerInfo.Antag)
|
||||
return Loc.GetString("round-end-summary-window-player-manifest-tab-sort-player-type-antag");
|
||||
|
||||
return Loc.GetString("round-end-summary-window-player-manifest-tab-sort-player-type-crew");
|
||||
}
|
||||
|
||||
private IEnumerable<RoundEndPlayerInfo> GetSortedPlayers()
|
||||
{
|
||||
// First filter players based on search text
|
||||
var filteredPlayers = string.IsNullOrEmpty(_searchText)
|
||||
? _playersInfo
|
||||
: _playersInfo.Where(PlayerMatchesSearch);
|
||||
|
||||
static string GetIcKey(RoundEndPlayerInfo p) =>
|
||||
(p.PlayerICName ?? p.PlayerOOCName).ToLowerInvariant();
|
||||
|
||||
static string GetOocKey(RoundEndPlayerInfo p) =>
|
||||
p.PlayerOOCName.ToLowerInvariant();
|
||||
|
||||
static string GetRoleKey(RoundEndPlayerInfo p) =>
|
||||
(p.Observer ? "zzz_observer" : p.Role).ToLowerInvariant();
|
||||
|
||||
static int GetPlayerTypeSortKey(RoundEndPlayerInfo p) =>
|
||||
p.Antag ? 1 : p.Observer ? 3 : 2;
|
||||
|
||||
return _currentSortField switch
|
||||
{
|
||||
SortField.ICName => ApplySort(filteredPlayers, GetIcKey, _sortDescending),
|
||||
SortField.OOCName => ApplySort(filteredPlayers, GetOocKey, _sortDescending),
|
||||
SortField.Role => ApplySort(filteredPlayers, GetRoleKey, _sortDescending),
|
||||
SortField.PlayerType => ApplySort(filteredPlayers, GetPlayerTypeSortKey, _sortDescending),
|
||||
_ => filteredPlayers
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<RoundEndPlayerInfo> ApplySort<TKey>(
|
||||
IEnumerable<RoundEndPlayerInfo> players,
|
||||
Func<RoundEndPlayerInfo, TKey> primaryKey,
|
||||
bool descending)
|
||||
{
|
||||
static string SecondaryKey(RoundEndPlayerInfo p) =>
|
||||
(p.PlayerICName ?? p.PlayerOOCName).ToLowerInvariant();
|
||||
|
||||
return descending
|
||||
? players.OrderByDescending(primaryKey).ThenByDescending(SecondaryKey)
|
||||
: players.OrderBy(primaryKey).ThenBy(SecondaryKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a sort key for player type to ensure consistent ordering: Antagonist -> Crew -> Observer
|
||||
/// </summary>
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a player matches the current search filter
|
||||
/// </summary>
|
||||
private bool PlayerMatchesSearch(RoundEndPlayerInfo playerInfo)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_searchText))
|
||||
return true;
|
||||
|
||||
// Search in character name (IC name)
|
||||
if (!string.IsNullOrEmpty(playerInfo.PlayerICName) &&
|
||||
playerInfo.PlayerICName.Contains(_searchText, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
// Search in player name (OOC name)
|
||||
if (!string.IsNullOrEmpty(playerInfo.PlayerOOCName) &&
|
||||
playerInfo.PlayerOOCName.Contains(_searchText, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
// Search in role
|
||||
if (!string.IsNullOrEmpty(playerInfo.Role))
|
||||
{
|
||||
if (playerInfo.Role.Contains(_searchText, StringComparison.OrdinalIgnoreCase) ||
|
||||
Loc.GetString(playerInfo.Role).Contains(_searchText, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
// Search in player type
|
||||
var playerType = GetPlayerTypeText(playerInfo);
|
||||
if (playerType.Contains(_searchText, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
// Search for "Observer" when they are observers
|
||||
if (playerInfo.Observer && "observer".Contains(_searchText, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles search text changes and refreshes the player list
|
||||
/// </summary>
|
||||
private void OnSearchTextChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
_searchText = args.Text;
|
||||
RefreshPlayerList();
|
||||
}
|
||||
|
||||
private sealed class SortButton : Button
|
||||
{
|
||||
public SortField Field { get; }
|
||||
private readonly Label _sortIndicator;
|
||||
|
||||
public SortButton(string text, SortField field)
|
||||
{
|
||||
Field = field;
|
||||
HorizontalExpand = true;
|
||||
|
||||
var container = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
var label = new Label
|
||||
{
|
||||
Text = text,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
_sortIndicator = new Label
|
||||
{
|
||||
Text = "",
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
MinSize = new Vector2(15, 1)
|
||||
};
|
||||
|
||||
container.AddChild(label);
|
||||
container.AddChild(_sortIndicator);
|
||||
|
||||
AddChild(container);
|
||||
}
|
||||
|
||||
public void SetSortIndicator(bool active, bool descending = false)
|
||||
{
|
||||
if (!active)
|
||||
{
|
||||
_sortIndicator.Text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
_sortIndicator.Text = descending ? "▼" : "▲";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,8 @@ namespace Content.Client.Sandbox
|
|||
if (_placement.Eraser)
|
||||
_placement.ToggleEraser();
|
||||
|
||||
_placement.Direction = _transform.GetWorldRotation(uid).GetCardinalDir();
|
||||
|
||||
_placement.BeginPlacing(new()
|
||||
{
|
||||
EntityType = comp.EntityPrototype.ID,
|
||||
|
|
@ -121,6 +123,9 @@ namespace Content.Client.Sandbox
|
|||
if (_placement.Eraser)
|
||||
_placement.ToggleEraser();
|
||||
|
||||
_placement.Direction = (Direction)(tileRef.Tile.RotationMirroring % 4 * 2);
|
||||
_placement.Mirrored = tileRef.Tile.RotationMirroring >= 4;
|
||||
|
||||
_placement.BeginPlacing(new()
|
||||
{
|
||||
EntityType = null,
|
||||
|
|
|
|||
|
|
@ -1,69 +1,65 @@
|
|||
<controls:BoxContainer Visible="False"
|
||||
HorizontalExpand="True"
|
||||
xmlns:controls="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.Shuttles.UI"
|
||||
xmlns:controls1="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<ui:ShuttleNavControl Name="NavRadar"
|
||||
MouseFilter="Stop"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="5 4 10 5"/>
|
||||
<!-- Nav controls -->
|
||||
<controls:BoxContainer Name="RightDisplayNav"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalExpand="True"
|
||||
MinWidth="256"
|
||||
MaxWidth="256"
|
||||
Margin="5 0 5 5"
|
||||
Orientation="Vertical">
|
||||
<controls1:StripeBack
|
||||
MinSize="48 48">
|
||||
<controls:Label Name="NavDisplayLabel" Text="{controls:Loc 'shuttle-console-display-label'}"
|
||||
VerticalExpand="True"
|
||||
HorizontalAlignment="Center"/>
|
||||
</controls1:StripeBack>
|
||||
<controls:GridContainer Columns="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalExpand="True"
|
||||
Margin="3"
|
||||
Name="ReadonlyDisplay">
|
||||
<controls:Label Text="{controls:Loc 'shuttle-console-position'}"/>
|
||||
<controls:Label Name="GridPosition"
|
||||
Text="0.0, 0.0"
|
||||
HorizontalExpand="True"
|
||||
Align="Right"/>
|
||||
<controls:Label Text="{controls:Loc 'shuttle-console-orientation'}"/>
|
||||
<controls:Label Name="GridOrientation"
|
||||
Text="0.0"
|
||||
HorizontalExpand="True"
|
||||
Align="Right"/>
|
||||
<controls:Label Text="{controls:Loc 'shuttle-console-linear-velocity'}"/>
|
||||
<controls:Label Name="GridLinearVelocity"
|
||||
Text="0.0, 0.0"
|
||||
HorizontalExpand="True"
|
||||
Align="Right"/>
|
||||
<controls:Label Text="{controls:Loc 'shuttle-console-angular-velocity'}"/>
|
||||
<controls:Label Name="GridAngularVelocity"
|
||||
Text="0.0"
|
||||
HorizontalExpand="True"
|
||||
Align="Right"/>
|
||||
</controls:GridContainer>
|
||||
<controls1:StripeBack
|
||||
MinSize="48 48">
|
||||
<controls:Label Name="NavSettingsLabel" Text="{controls:Loc 'shuttle-console-nav-settings'}"
|
||||
VerticalExpand="True"
|
||||
HorizontalAlignment="Center"/>
|
||||
</controls1:StripeBack>
|
||||
<controls:Button Name="IFFToggle"
|
||||
Text="{controls:Loc 'shuttle-console-iff-toggle'}"
|
||||
TextAlign="Center"
|
||||
ToggleMode="True"/>
|
||||
<controls:Button Name="DockToggle"
|
||||
Text="{controls:Loc 'shuttle-console-dock-toggle'}"
|
||||
TextAlign="Center"
|
||||
ToggleMode="True"/>
|
||||
</controls:BoxContainer>
|
||||
</controls:BoxContainer>
|
||||
<BoxContainer
|
||||
Visible="False"
|
||||
HorizontalExpand="True"
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.Shuttles.UI"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<ui:ShuttleNavControl
|
||||
Name="NavRadar"
|
||||
MouseFilter="Stop"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="5 4 10 5"/>
|
||||
<!-- Nav controls -->
|
||||
<BoxContainer
|
||||
Name="RightDisplayNav"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalExpand="True"
|
||||
MinWidth="256"
|
||||
MaxWidth="256"
|
||||
Margin="5 0 5 5"
|
||||
Orientation="Vertical">
|
||||
<controls:StripeBack MinSize="48 48">
|
||||
<Label
|
||||
Name="NavDisplayLabel"
|
||||
Text="{Loc 'shuttle-console-display-label'}"
|
||||
VerticalExpand="True"
|
||||
HorizontalAlignment="Center"/>
|
||||
</controls:StripeBack>
|
||||
<GridContainer
|
||||
Columns="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalExpand="True"
|
||||
Margin="3"
|
||||
Name="ReadonlyDisplay">
|
||||
<Label Text="{Loc 'shuttle-console-position'}" />
|
||||
<Label Name="GridPosition" Text="0.0, 0.0" HorizontalExpand="True" Align="Right" />
|
||||
<Label Text="{Loc 'shuttle-console-orientation'}" />
|
||||
<Label Name="GridOrientation" Text="0.0" HorizontalExpand="True" Align="Right" />
|
||||
<Label Text="{Loc 'shuttle-console-linear-velocity'}" />
|
||||
<Label Name="GridLinearVelocity" Text="0.0, 0.0" HorizontalExpand="True" Align="Right" />
|
||||
<Label Text="{Loc 'shuttle-console-angular-velocity'}" />
|
||||
<Label Name="GridAngularVelocity" Text="0.0" HorizontalExpand="True" Align="Right" />
|
||||
</GridContainer>
|
||||
<controls:StripeBack MinSize="48 48">
|
||||
<Label
|
||||
Name="NavSettingsLabel"
|
||||
Text="{Loc 'shuttle-console-nav-settings'}"
|
||||
VerticalExpand="True"
|
||||
HorizontalAlignment="Center"/>
|
||||
</controls:StripeBack>
|
||||
<Button
|
||||
Name="IFFToggle"
|
||||
Text="{Loc 'shuttle-console-iff-toggle'}"
|
||||
TextAlign="Center"
|
||||
ToggleMode="True"/>
|
||||
<Button
|
||||
Name="DockToggle"
|
||||
Text="{Loc 'shuttle-console-dock-toggle'}"
|
||||
TextAlign="Center"
|
||||
ToggleMode="True"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
|
|
|||
|
|
@ -298,11 +298,16 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
|
|||
if (!ShowDocks)
|
||||
return;
|
||||
|
||||
const float DockScale = 0.6f;
|
||||
const float dockScale = 0.6f;
|
||||
var topLeft = new Vector2(-dockScale, -dockScale);
|
||||
var topRight = new Vector2(dockScale, -dockScale);
|
||||
var bottomRight = new Vector2(dockScale, dockScale);
|
||||
var bottomLeft = new Vector2(-dockScale, dockScale);
|
||||
|
||||
var nent = EntManager.GetNetEntity(uid);
|
||||
|
||||
const float sqrt2 = 1.41421356f;
|
||||
const float dockRadius = DockScale * sqrt2;
|
||||
const float dockRadius = dockScale * sqrt2;
|
||||
// Worst-case bounds used to cull a dock:
|
||||
Box2 viewBounds = new Box2(
|
||||
-dockRadius * UIScale,
|
||||
|
|
@ -312,6 +317,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
|
|||
|
||||
if (_docks.TryGetValue(nent, out var docks))
|
||||
{
|
||||
Span<Vector2> verts = new Vector2[4];
|
||||
foreach (var state in docks)
|
||||
{
|
||||
var position = state.Coordinates.Position;
|
||||
|
|
@ -324,13 +330,10 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
|
|||
|
||||
var color = Color.ToSrgb(state.HighlightedColor);
|
||||
|
||||
var verts = new[]
|
||||
{
|
||||
Vector2.Transform(position + new Vector2(-DockScale, -DockScale), gridToView),
|
||||
Vector2.Transform(position + new Vector2(DockScale, -DockScale), gridToView),
|
||||
Vector2.Transform(position + new Vector2(DockScale, DockScale), gridToView),
|
||||
Vector2.Transform(position + new Vector2(-DockScale, DockScale), gridToView),
|
||||
};
|
||||
verts[0] = Vector2.Transform(position + topLeft, gridToView);
|
||||
verts[1] = Vector2.Transform(position + topRight, gridToView);
|
||||
verts[2] = Vector2.Transform(position + bottomRight, gridToView);
|
||||
verts[3] = Vector2.Transform(position + bottomLeft, gridToView);
|
||||
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, color.WithAlpha(0.8f));
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.LineStrip, verts, color);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ namespace Content.Client.Singularity
|
|||
_entMan.EventBus.SubscribeEvent<PixelToMapEvent>(EventSource.Local, this, OnProjectFromScreenToMap);
|
||||
ZIndex = 101; // Should be drawn after the placement overlay so admins placing items near the singularity can tell where they're going.
|
||||
|
||||
_configManager.OnValueChanged(CCVars.ReducedMotion, (b) => { _reducedMotion = b; }, invokeImmediately: true);
|
||||
_configManager.OnValueChanged(CCVars.DisableSinguloWarp, (b) => { _reducedMotion = b; }, invokeImmediately: true);
|
||||
}
|
||||
|
||||
private readonly Vector2[] _positions = new Vector2[MaxCount];
|
||||
|
|
|
|||
|
|
@ -95,8 +95,8 @@ public sealed partial class StatusIconOverlay : Overlay
|
|||
accOffsetL += texture.Height;
|
||||
countL++;
|
||||
}
|
||||
yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float)(accOffsetL - proto.Offset) / EyeManager.PixelsPerMeter;
|
||||
xOffset = -(bounds.Width + sprite.Offset.X) / 2f + (float)proto.OffsetHorizontal / EyeManager.PixelsPerMeter;
|
||||
yOffset = sprite.Offset.Y + bounds.Height / 2f - (float)(accOffsetL - proto.Offset) / EyeManager.PixelsPerMeter;
|
||||
xOffset = sprite.Offset.X - bounds.Width / 2f + (float)proto.OffsetHorizontal / EyeManager.PixelsPerMeter;
|
||||
|
||||
}
|
||||
else
|
||||
|
|
@ -108,8 +108,8 @@ public sealed partial class StatusIconOverlay : Overlay
|
|||
accOffsetR += texture.Height;
|
||||
countR++;
|
||||
}
|
||||
yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float)(accOffsetR - proto.Offset) / EyeManager.PixelsPerMeter;
|
||||
xOffset = (bounds.Width + sprite.Offset.X) / 2f - (float)(texture.Width - proto.OffsetHorizontal) / EyeManager.PixelsPerMeter;
|
||||
yOffset = sprite.Offset.Y + bounds.Height / 2f - (float)(accOffsetR - proto.Offset) / EyeManager.PixelsPerMeter;
|
||||
xOffset = sprite.Offset.X + bounds.Width / 2f - (float)(texture.Width - proto.OffsetHorizontal) / EyeManager.PixelsPerMeter;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
using Content.Shared.Stunnable;
|
||||
|
||||
namespace Content.Client.Stunnable;
|
||||
|
||||
public sealed class StunbatonSystem : SharedStunbatonSystem
|
||||
{
|
||||
}
|
||||
|
|
@ -23,7 +23,8 @@ public sealed partial class TippyUI : UIWidget
|
|||
if (visuals == null)
|
||||
return;
|
||||
|
||||
Label.ModulateSelfOverride = visuals.FontAccentColor;
|
||||
if (visuals.DefaultTextColor != null)
|
||||
Label.ModulateSelfOverride = visuals.DefaultTextColor;
|
||||
|
||||
if (visuals.BackgroundImagePath == null)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ public sealed partial class TippyUIController : UIController
|
|||
paper.BackgroundImagePath = "/Textures/Interface/Paper/paper_background_default.svg.96dpi.png";
|
||||
paper.BackgroundPatchMargin = new(16f, 16f, 16f, 16f);
|
||||
paper.BackgroundModulate = new(255, 255, 204);
|
||||
paper.FontAccentColor = new(0, 0, 0);
|
||||
paper.DefaultTextColor = new(0, 0, 0);
|
||||
}
|
||||
tippy.InitLabel(EntityManager.GetComponentOrNull<PaperVisualsComponent>(_entity), _resCache);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,15 @@ public sealed partial class ConfirmButton : Button
|
|||
|
||||
public const string ConfirmPrefix = "confirm-";
|
||||
|
||||
/// <summary>
|
||||
/// The time when the button will revert from confirming if left unpressed.
|
||||
/// </summary>
|
||||
private TimeSpan? _nextReset;
|
||||
/// <summary>
|
||||
/// The time when the button should re-enable itself (to avoid debouncing).
|
||||
/// </summary>
|
||||
private TimeSpan? _nextCooldown;
|
||||
private bool _isConfirming;
|
||||
private string? _confirmationText;
|
||||
private string? _text;
|
||||
|
||||
|
|
@ -30,6 +37,11 @@ public sealed partial class ConfirmButton : Button
|
|||
/// </summary>
|
||||
public new event Action<ButtonEventArgs>? OnPressed;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the button has started to confirm and is awaiting a second button press.
|
||||
/// </summary>
|
||||
public event Action<ButtonEventArgs>? OnConfirming;
|
||||
|
||||
/// <inheritdoc cref="Button.Text"/>
|
||||
/// <remarks>
|
||||
/// Hides the buttons text property to be able to sanely replace the button text with
|
||||
|
|
@ -41,7 +53,23 @@ public sealed partial class ConfirmButton : Button
|
|||
set
|
||||
{
|
||||
_text = value;
|
||||
base.Text = IsConfirming ? _confirmationText : value;
|
||||
UpdateText();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="BaseButton.Disabled"/>
|
||||
/// <remarks>
|
||||
/// Overrides the confirming state of the button when set.
|
||||
/// Intended for public use, should not be used inside the class due to IsConfirming side-effects.
|
||||
/// </remarks>
|
||||
public new bool Disabled
|
||||
{
|
||||
get => base.Disabled;
|
||||
set
|
||||
{
|
||||
// Force out of confirming status (may change base.Disabled), then write your new disabled value.
|
||||
SetIsConfirming(false);
|
||||
SetDisabled(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +80,11 @@ public sealed partial class ConfirmButton : Button
|
|||
public string ConfirmationText
|
||||
{
|
||||
get => _confirmationText ?? Loc.GetString("generic-confirm");
|
||||
set => _confirmationText = value;
|
||||
set
|
||||
{
|
||||
_confirmationText = value;
|
||||
UpdateText();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -67,8 +99,19 @@ public sealed partial class ConfirmButton : Button
|
|||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan CooldownTime { get; set; } = TimeSpan.FromSeconds(.5);
|
||||
|
||||
/// <summary>
|
||||
/// A property to get or change whether the button is confirming (awaiting a second press within a time limit) or not
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool IsConfirming = false;
|
||||
public bool IsConfirming
|
||||
{
|
||||
get => _isConfirming;
|
||||
set
|
||||
{
|
||||
if (_isConfirming != value)
|
||||
SetIsConfirming(value);
|
||||
}
|
||||
}
|
||||
|
||||
public ConfirmButton()
|
||||
{
|
||||
|
|
@ -79,15 +122,13 @@ public sealed partial class ConfirmButton : Button
|
|||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
if (IsConfirming && _gameTiming.CurTime > _nextReset)
|
||||
{
|
||||
IsConfirming = false;
|
||||
base.Text = Text;
|
||||
DrawModeChanged();
|
||||
}
|
||||
if (!IsConfirming)
|
||||
return;
|
||||
|
||||
if (Disabled && _gameTiming.CurTime > _nextCooldown)
|
||||
Disabled = false;
|
||||
if (_gameTiming.CurTime > _nextReset)
|
||||
SetIsConfirming(false);
|
||||
else if (Disabled && _gameTiming.CurTime > _nextCooldown)
|
||||
SetDisabled(false);
|
||||
}
|
||||
|
||||
protected override void DrawModeChanged()
|
||||
|
|
@ -111,10 +152,11 @@ public sealed partial class ConfirmButton : Button
|
|||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
base.DrawModeChanged();
|
||||
else
|
||||
{
|
||||
base.DrawModeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleOnPressed(ButtonEventArgs buttonEvent)
|
||||
|
|
@ -123,20 +165,62 @@ public sealed partial class ConfirmButton : Button
|
|||
if (IsConfirming && _nextCooldown > _gameTiming.CurTime)
|
||||
return;
|
||||
|
||||
// Update the state machine before invoking our events.
|
||||
SetIsConfirming(!IsConfirming);
|
||||
|
||||
switch (IsConfirming)
|
||||
{
|
||||
case false:
|
||||
_nextCooldown = _gameTiming.CurTime + CooldownTime;
|
||||
_nextReset = _gameTiming.CurTime + ResetTime;
|
||||
Disabled = true;
|
||||
break;
|
||||
case true:
|
||||
OnConfirming?.Invoke(buttonEvent);
|
||||
break;
|
||||
case false:
|
||||
OnPressed?.Invoke(buttonEvent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
base.Text = IsConfirming ? Text : ConfirmationText;
|
||||
/// <summary>
|
||||
/// Updates the text shown on the button depending on whether the button is confirming or not.
|
||||
/// </summary>
|
||||
private void UpdateText()
|
||||
{
|
||||
base.Text = IsConfirming ? ConfirmationText : Text;
|
||||
}
|
||||
|
||||
IsConfirming = !IsConfirming;
|
||||
/// <summary>
|
||||
/// Internal setter logi
|
||||
/// </summary>
|
||||
private void SetDisabled(bool value)
|
||||
{
|
||||
base.Disabled = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal setter logic for <see cref="IsConfirming"/>
|
||||
/// Sets the text, the timers, and the disabled state.
|
||||
/// </summary>
|
||||
private void SetIsConfirming(bool value)
|
||||
{
|
||||
_isConfirming = value;
|
||||
|
||||
// Update button visuals.
|
||||
UpdateText();
|
||||
DrawModeChanged();
|
||||
|
||||
if (value)
|
||||
{
|
||||
// Start our timers, disable button until cooldown.
|
||||
SetDisabled(true);
|
||||
_nextCooldown = _gameTiming.CurTime + CooldownTime;
|
||||
_nextReset = _gameTiming.CurTime + ResetTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clear timers, ensure button is enabled if it was disabled before (previous SetIsConfirming(true) call => valid reset timer)
|
||||
if (Disabled && _nextReset != null)
|
||||
SetDisabled(false);
|
||||
_nextReset = null;
|
||||
_nextCooldown = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,21 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io" Title="{Loc 'ghost-target-window-title'}" MinSize="450 450" SetSize="450 450">
|
||||
<DefaultWindow xmlns="https://spacestation14.io" Title="{Loc 'ghost-target-window-title'}" MinSize="450 450"
|
||||
SetSize="450 500">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="0.4">
|
||||
<Button Name="GhostnadoButton" Text="{Loc 'ghost-target-window-warp-to-most-followed'}" HorizontalAlignment="Center" Margin="0 4" />
|
||||
<BoxContainer Orientation="Vertical" Margin="0 4 0 4">
|
||||
<RichTextLabel Text="{Loc 'ghost-target-window-warp-to-title'}" HorizontalAlignment="Center" />
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 5" />
|
||||
<BoxContainer Orientation="Horizontal" Align="Center">
|
||||
<Button Name="GhostnadoButton" Text="{Loc 'ghost-target-window-warp-to-most-followed'}"
|
||||
ToolTip="{Loc 'ghost-target-window-warp-to-most-followed-tooltip'}"
|
||||
HorizontalAlignment="Center" StyleClasses="OpenRight" />
|
||||
<Button Name="WarpToRandomFollowedButton" Text="{Loc 'ghost-target-window-warp-to-random-followed'}"
|
||||
ToolTip="{Loc 'ghost-target-window-warp-to-random-followed-tooltip'}"
|
||||
HorizontalAlignment="Center" StyleClasses="OpenBoth" />
|
||||
<Button Name="WarpToRandomButton" Text="{Loc 'ghost-target-window-warp-to-random'}"
|
||||
ToolTip="{Loc 'ghost-target-window-warp-to-random-tooltip'}" HorizontalAlignment="Center"
|
||||
StyleClasses="OpenLeft" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<LineEdit Name="SearchBar" PlaceHolder="Search" HorizontalExpand="True" Margin="0 4" />
|
||||
<ScrollContainer Name="GhostScroll" VerticalExpand="True" HorizontalExpand="True" HScrollEnabled="False">
|
||||
<BoxContainer Name="ButtonContainer" Orientation="Vertical" VerticalExpand="True" SeparationOverride="5">
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls
|
|||
|
||||
public event Action<NetEntity>? WarpClicked;
|
||||
public event Action? OnGhostnadoClicked;
|
||||
public event Action? OnWarpToRandomFollowedClicked;
|
||||
public event Action? OnWarpToRandomClicked;
|
||||
|
||||
public GhostTargetWindow()
|
||||
{
|
||||
|
|
@ -23,6 +25,8 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls
|
|||
SearchBar.OnTextChanged += OnSearchTextChanged;
|
||||
|
||||
GhostnadoButton.OnPressed += _ => OnGhostnadoClicked?.Invoke();
|
||||
WarpToRandomFollowedButton.OnPressed += _ => OnWarpToRandomFollowedClicked?.Invoke();
|
||||
WarpToRandomButton.OnPressed += _ => OnWarpToRandomClicked?.Invoke();
|
||||
}
|
||||
|
||||
public void UpdateWarps(IEnumerable<GhostWarp> warps)
|
||||
|
|
|
|||
|
|
@ -117,6 +117,18 @@ public sealed partial class GhostUIController : UIController, IOnSystemChanged<G
|
|||
_net.SendSystemNetworkMessage(msg);
|
||||
}
|
||||
|
||||
private void OnWarpToRandomFollowedClicked()
|
||||
{
|
||||
var msg = new WarpToRandomFollowedRequestEvent();
|
||||
_net.SendSystemNetworkMessage(msg);
|
||||
}
|
||||
|
||||
private void OnWarpToRandomClicked()
|
||||
{
|
||||
var msg = new WarpToRandomRequestEvent();
|
||||
_net.SendSystemNetworkMessage(msg);
|
||||
}
|
||||
|
||||
public void LoadGui()
|
||||
{
|
||||
if (Gui == null)
|
||||
|
|
@ -127,6 +139,8 @@ public sealed partial class GhostUIController : UIController, IOnSystemChanged<G
|
|||
Gui.GhostRolesPressed += GhostRolesPressed;
|
||||
Gui.TargetWindow.WarpClicked += OnWarpClicked;
|
||||
Gui.TargetWindow.OnGhostnadoClicked += OnGhostnadoClicked;
|
||||
Gui.TargetWindow.OnWarpToRandomFollowedClicked += OnWarpToRandomFollowedClicked;
|
||||
Gui.TargetWindow.OnWarpToRandomClicked += OnWarpToRandomClicked;
|
||||
|
||||
UpdateGui();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ public sealed partial class GuidebookUIController : UIController, IOnStateEntere
|
|||
|
||||
private GuidebookWindow? _guideWindow;
|
||||
private MenuButton? GuidebookButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.GuidebookButton;
|
||||
private ProtoId<GuideEntryPrototype>? _lastEntry;
|
||||
|
||||
public void OnStateEntered(LobbyState state)
|
||||
{
|
||||
|
|
@ -145,7 +144,6 @@ public sealed partial class GuidebookUIController : UIController, IOnStateEntere
|
|||
if (_guideWindow != null)
|
||||
{
|
||||
_guideWindow.ReturnContainer.Visible = false;
|
||||
_lastEntry = _guideWindow.LastEntry;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +181,7 @@ public sealed partial class GuidebookUIController : UIController, IOnStateEntere
|
|||
if (guides == null)
|
||||
{
|
||||
guides = _prototypeManager.EnumeratePrototypes<GuideEntryPrototype>()
|
||||
.ToDictionary(x => new ProtoId<GuideEntryPrototype>(x.ID), x => (GuideEntry) x);
|
||||
.ToDictionary(x => new ProtoId<GuideEntryPrototype>(x.ID), x => (GuideEntry)x);
|
||||
}
|
||||
else if (includeChildren)
|
||||
{
|
||||
|
|
@ -197,20 +195,23 @@ public sealed partial class GuidebookUIController : UIController, IOnStateEntere
|
|||
|
||||
if (selected == null)
|
||||
{
|
||||
if (_lastEntry is { } lastEntry && guides.ContainsKey(lastEntry))
|
||||
if (_guideWindow.Selected is { } lastEntry && guides.ContainsKey(lastEntry))
|
||||
{
|
||||
selected = _lastEntry;
|
||||
selected = lastEntry;
|
||||
}
|
||||
else
|
||||
{
|
||||
selected = _configuration.GetCVar(CCVars.DefaultGuide);
|
||||
}
|
||||
}
|
||||
_guideWindow.UpdateGuides(guides, rootEntries, forceRoot, selected);
|
||||
var changed = _guideWindow.UpdateGuides(guides, rootEntries, forceRoot, selected);
|
||||
|
||||
// Expand up to depth-2.
|
||||
_guideWindow.Tree.SetAllExpanded(false);
|
||||
_guideWindow.Tree.SetAllExpanded(true, 1);
|
||||
if (changed)
|
||||
{
|
||||
_guideWindow.Tree.SetAllExpanded(false);
|
||||
_guideWindow.Tree.SetAllExpanded(true, 1);
|
||||
}
|
||||
|
||||
_guideWindow.OpenCenteredRight();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
</graphics:StyleBoxTexture>
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
<BoxContainer Name="Contents" Orientation="Vertical" Margin="0 6 0 4" RectClipContent="True">
|
||||
<BoxContainer Name="Contents" Orientation="Vertical" Margin="2 6 2 4" RectClipContent="True">
|
||||
<BoxContainer Name="StatusContents" Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Bottom" />
|
||||
<Control>
|
||||
<Label Name="NoItemLabel" ClipText="True" StyleClasses="ItemStatusNotHeld" Align="Left" Text="{Loc 'item-status-not-held'}" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Content.Client.Items;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Inventory.VirtualItem;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
|
|
@ -159,6 +160,13 @@ public sealed partial class ItemStatusPanel : Control
|
|||
return;
|
||||
}
|
||||
|
||||
if (_entityManager.HasComponent<BlockItemNameInHandComponent>(_entity))
|
||||
{
|
||||
StatusContents.VerticalAlignment = VAlignment.Center;
|
||||
ItemNameLabel.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entityManager.TryGetComponent(_entity, out VirtualItemComponent? virtualItem)
|
||||
&& _entityManager.EntityExists(virtualItem.BlockingEntity))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -261,3 +261,73 @@ public sealed class BatteryBulletRenderer : BaseBulletRenderer
|
|||
handle.DrawRect(UIBox2.FromDimensions(renderPos, new Vector2(SizeH, SizeV)), color);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CustomBulletRenderer : BaseBulletRenderer
|
||||
{
|
||||
private readonly Texture _loadedSprite;
|
||||
private readonly Texture _spentSprite;
|
||||
|
||||
private const int Separation = 2;
|
||||
|
||||
public CustomBulletRenderer(Texture loadedSprite, Texture spentSprite, int numberOfRows)
|
||||
{
|
||||
_loadedSprite = loadedSprite;
|
||||
_spentSprite = spentSprite;
|
||||
Rows = numberOfRows;
|
||||
|
||||
Parameters = new LayoutParameters
|
||||
{
|
||||
ItemWidth = _loadedSprite.Width,
|
||||
ItemHeight = _loadedSprite.Height,
|
||||
ItemSeparation = _loadedSprite.Width + Separation,
|
||||
MinCountPerRow = 3,
|
||||
VerticalSeparation = Separation,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
var itemSeparation = _loadedSprite.Width + Separation;
|
||||
var numberItemsPerRow = (int)Math.Ceiling((double)Capacity / Rows);
|
||||
|
||||
// if there is more than one item per row, then we need to recalculate the separation between them.
|
||||
if (numberItemsPerRow > 1)
|
||||
{
|
||||
// the item separation is the distance from the start of a item to the start of the next item,
|
||||
// so the total horizontal size will be the item separation times the number of items - 1 + item width.
|
||||
// the reason for the -1 and the + + item width. is that the last item doesn't have a item after it,
|
||||
// so instead of adding the last item separation we add the item width.
|
||||
|
||||
// converting into a equation:
|
||||
// total horizontal size = item separation * (number of items - 1) + width of the item
|
||||
|
||||
// converting to the variables:
|
||||
// Size.X = itemSeparation * (numberItemsPerRow - 1) + _loadedSprite.Width
|
||||
// Size.X - _loadedSprite.Width = itemSeparation * (numberItemsPerRow - 1)
|
||||
// (Size.X - _loadedSprite.Width) / (numberItemsPerRow - 1) = itemSeparation
|
||||
itemSeparation = (int)(Size.X - _loadedSprite.Width) / (numberItemsPerRow - 1);
|
||||
|
||||
// make sure the itemSeparation is less or equal to the standard item separation
|
||||
itemSeparation = int.Min(_loadedSprite.Width + Separation, itemSeparation);
|
||||
|
||||
// make sure the itemSeparation is greater or equal to 1
|
||||
itemSeparation = int.Max(1, itemSeparation);
|
||||
}
|
||||
|
||||
Parameters = new LayoutParameters()
|
||||
{
|
||||
ItemWidth = _loadedSprite.Width,
|
||||
ItemHeight = _loadedSprite.Height,
|
||||
ItemSeparation = itemSeparation,
|
||||
MinCountPerRow = 3,
|
||||
VerticalSeparation = Separation,
|
||||
};
|
||||
|
||||
base.Draw(handle);
|
||||
}
|
||||
|
||||
protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor)
|
||||
{
|
||||
handle.DrawTexture(spent ? _spentSprite : _loadedSprite, renderPos);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,30 @@ public sealed partial class GunSystem
|
|||
}
|
||||
}
|
||||
|
||||
public sealed class CustomIconStatusControl : Control
|
||||
{
|
||||
private readonly CustomBulletRenderer _customBulletRenderer;
|
||||
|
||||
public CustomIconStatusControl(Texture loadedSprite, Texture spentTexture, int numberOfRows)
|
||||
{
|
||||
MinHeight = 15;
|
||||
HorizontalExpand = true;
|
||||
VerticalAlignment = VAlignment.Center;
|
||||
|
||||
AddChild(_customBulletRenderer = new CustomBulletRenderer(loadedSprite, spentTexture, numberOfRows)
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
});
|
||||
}
|
||||
|
||||
public void Update(int count, int capacity)
|
||||
{
|
||||
_customBulletRenderer.Capacity = capacity;
|
||||
_customBulletRenderer.Count = count;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ChamberMagazineStatusControl : Control
|
||||
{
|
||||
private readonly BulletRender _bulletRender;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
using Content.Shared.Weapons.Ranged.Components;
|
||||
|
||||
namespace Content.Client.Weapons.Ranged.Systems;
|
||||
|
||||
public sealed partial class GunSystem
|
||||
{
|
||||
protected override void InitializeCustomAmmoCounter()
|
||||
{
|
||||
base.InitializeCustomAmmoCounter();
|
||||
|
||||
SubscribeLocalEvent<CustomSpriteAmmoCounterComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
|
||||
SubscribeLocalEvent<CustomSpriteAmmoCounterComponent, AmmoCounterControlEvent>(OnControl);
|
||||
}
|
||||
|
||||
private void OnAmmoCountUpdate(Entity<CustomSpriteAmmoCounterComponent> ent, ref UpdateAmmoCounterEvent args)
|
||||
{
|
||||
if (args.Control is not CustomIconStatusControl customIcon)
|
||||
return;
|
||||
|
||||
customIcon.Update(GetAmmoCount(ent), GetAmmoCapacity(ent));
|
||||
}
|
||||
|
||||
private void OnControl(Entity<CustomSpriteAmmoCounterComponent> ent, ref AmmoCounterControlEvent args)
|
||||
{
|
||||
var loadedTexture = _sprite.GetFrame(ent.Comp.LoadedAmmoSprite, TimeSpan.Zero);
|
||||
var spentTexture = _sprite.GetFrame(ent.Comp.SpentAmmoSprite, TimeSpan.Zero);
|
||||
|
||||
args.Control = new CustomIconStatusControl(loadedTexture, spentTexture, ent.Comp.NumberOfRows);
|
||||
}
|
||||
}
|
||||
|
|
@ -296,14 +296,6 @@ public sealed partial class GunSystem : SharedGunSystem
|
|||
_recoil.KickCamera(user.Value, recoil.Normalized() * 0.5f * recoilScalar);
|
||||
}
|
||||
|
||||
protected override void Popup(string message, EntityUid? uid, EntityUid? user)
|
||||
{
|
||||
if (uid == null || user == null || !Timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
PopupSystem.PopupEntity(message, uid.Value, user.Value);
|
||||
}
|
||||
|
||||
protected override void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? tracked = null)
|
||||
{
|
||||
if (!Timing.IsFirstTimePredicted)
|
||||
|
|
@ -483,14 +475,14 @@ public sealed partial class GunSystem : SharedGunSystem
|
|||
public int Compare((EntityUid clicked, bool alive, bool occluded, int depth, uint renderOrder, float bottom, float distance) x,
|
||||
(EntityUid clicked, bool alive, bool occluded, int depth, uint renderOrder, float bottom, float distance) y)
|
||||
{
|
||||
var cmp = y.alive.CompareTo(x.alive);
|
||||
var cmp = y.occluded.CompareTo(x.occluded);
|
||||
|
||||
if (cmp != 0)
|
||||
{
|
||||
return cmp;
|
||||
}
|
||||
|
||||
cmp = y.occluded.CompareTo(x.occluded);
|
||||
|
||||
cmp = y.alive.CompareTo(x.alive);
|
||||
if (cmp != 0)
|
||||
{
|
||||
return cmp;
|
||||
|
|
@ -532,6 +524,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
|||
if (!Resolve(entity, ref entity.Comp))
|
||||
return false;
|
||||
|
||||
// TODO: Maybe also check that our cursor is intersecting a valid fixture?
|
||||
foreach (var fix in entity.Comp.Fixtures)
|
||||
{
|
||||
if (!fix.Value.Hard || (fix.Value.CollisionLayer & (int)CollisionGroup.BulletImpassable) == 0)
|
||||
|
|
|
|||
|
|
@ -37,11 +37,22 @@ public sealed class LogWindowTest : InteractionTest
|
|||
var refresh = logWindow.Logs.RefreshButton;
|
||||
var cont = logWindow.Logs.LogsContainer;
|
||||
|
||||
async Task<AdminLogLabel[]> SearchForLog(Guid logGuid)
|
||||
{
|
||||
await Client.WaitPost(() => search.Text = logGuid.ToString());
|
||||
await ClickControl(refresh);
|
||||
|
||||
await RunUntilSynced();
|
||||
await RunTicks(10);
|
||||
|
||||
return cont.Children
|
||||
.Where(x => x.Visible && x is AdminLogLabel)
|
||||
.Cast<AdminLogLabel>()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
// Search for the log we added earlier.
|
||||
await Client.WaitPost(() => search.Text = guid.ToString());
|
||||
await ClickControl(refresh);
|
||||
await RunTicks(10);
|
||||
var searchResult = cont.Children.Where(x => x.Visible && x is AdminLogLabel).Cast<AdminLogLabel>().ToArray();
|
||||
var searchResult = await SearchForLog(guid);
|
||||
Assert.That(searchResult, Has.Length.EqualTo(1));
|
||||
Assert.That(searchResult[0].Log.Message, Contains.Substring($" test log 1: {guid}"));
|
||||
|
||||
|
|
@ -50,10 +61,7 @@ public sealed class LogWindowTest : InteractionTest
|
|||
await Server.WaitPost(() => log.Add(LogType.Unknown, $"{SPlayer} test log 2: {guid}"));
|
||||
|
||||
// Update the search and refresh
|
||||
await Client.WaitPost(() => search.Text = guid.ToString());
|
||||
await ClickControl(refresh);
|
||||
await RunTicks(10);
|
||||
searchResult = cont.Children.Where(x => x.Visible && x is AdminLogLabel).Cast<AdminLogLabel>().ToArray();
|
||||
searchResult = await SearchForLog(guid);
|
||||
Assert.That(searchResult, Has.Length.EqualTo(1));
|
||||
Assert.That(searchResult[0].Log.Message, Contains.Substring($" test log 2: {guid}"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
#nullable enable
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Client.Audio.Midi;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Audio.Midi;
|
||||
|
||||
[TestFixture]
|
||||
public sealed partial class MidiFileCollectionManagerTests : GameTest
|
||||
{
|
||||
private static readonly byte[] TestBytes = [1, 2, 3, 4, 5, 6];
|
||||
private static readonly ResPath TestFileName = new ResPath("unit_test.midi");
|
||||
private static ResPath TestUserDataDir => new ResPath("/UserMidis/");
|
||||
private static ResPath TestFullPath => TestUserDataDir / TestFileName;
|
||||
|
||||
private IResourceManager ResManager => Pair.Client.ResolveDependency<IResourceManager>();
|
||||
private MidiFileCollectionManager MidiLibManager => Pair.Client.ResolveDependency<MidiFileCollectionManager>();
|
||||
|
||||
[TearDown]
|
||||
public void CleanUserData()
|
||||
{
|
||||
foreach (var file in ResManager.UserData.DirectoryEntries(TestUserDataDir))
|
||||
{
|
||||
ResManager.UserData.Delete(new ResPath(TestUserDataDir + file));
|
||||
}
|
||||
|
||||
MidiLibManager.ReloadLibrary();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestAddMidiFile()
|
||||
{
|
||||
var addedFileName = new ResPath("");
|
||||
Stream stream = new MemoryStream(TestBytes);
|
||||
MidiLibManager.MidiFileAdded += s => { addedFileName = s; };
|
||||
|
||||
await MidiLibManager.AddMidiFile(TestFileName, stream);
|
||||
var outputBytes = ResManager.UserData.ReadAllBytes(TestFullPath);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(MidiLibManager.GetMidiFiles(), Contains.Item(TestFileName));
|
||||
Assert.That(outputBytes, Is.EqualTo(TestBytes));
|
||||
Assert.That(addedFileName, Is.EqualTo(TestFileName));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGetMidiData()
|
||||
{
|
||||
ResManager.UserData.WriteAllBytes(TestFullPath, TestBytes);
|
||||
var midiBytes = MidiLibManager.GetMidiData(TestFileName);
|
||||
|
||||
Assert.That(TestBytes, Is.EqualTo(midiBytes));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRemoveMidiFile()
|
||||
{
|
||||
var removedFileName = new ResPath("");
|
||||
MidiLibManager.MidiFileRemoved += s => { removedFileName = s; };
|
||||
|
||||
ResManager.UserData.WriteAllBytes(TestFullPath, TestBytes);
|
||||
Assert.That(ResManager.UserData.Exists(TestFullPath), Is.True);
|
||||
|
||||
MidiLibManager.RemoveMidiFile(TestFileName);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(ResManager.UserData.Exists(TestFullPath), Is.False);
|
||||
Assert.That(MidiLibManager.GetMidiFiles(), Is.Empty);
|
||||
Assert.That(removedFileName, Is.EqualTo(TestFileName));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestRemoveAllMidiFiles()
|
||||
{
|
||||
var resetFired = false;
|
||||
|
||||
MidiLibManager.MidiFilesReset += () => { resetFired = true; };
|
||||
await MidiLibManager.AddMidiFile(new ResPath("1_unit_test.midi"), TestBytes);
|
||||
await MidiLibManager.AddMidiFile(new ResPath("2_unit_test.midi"), TestBytes);
|
||||
await MidiLibManager.AddMidiFile(new ResPath("3_unit_test.midi"), TestBytes);
|
||||
|
||||
Assert.That(MidiLibManager.GetMidiFiles().Count(), Is.EqualTo(3));
|
||||
|
||||
MidiLibManager.RemoveAllMidiFiles();
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(MidiLibManager.GetMidiFiles(), Is.Empty);
|
||||
Assert.That(resetFired, Is.True);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRenameMidiFile()
|
||||
{
|
||||
var renamedFileName = new ResPath("unit_test_renamed.midi");
|
||||
var removedFileName = new ResPath("");
|
||||
var addedFileName = new ResPath("");
|
||||
|
||||
MidiLibManager.MidiFileRemoved += s => { removedFileName = s; };
|
||||
MidiLibManager.MidiFileAdded += s => { addedFileName = s; };
|
||||
|
||||
ResManager.UserData.WriteAllBytes(TestFullPath, TestBytes);
|
||||
Assert.That(ResManager.UserData.Exists(TestFullPath), Is.True);
|
||||
|
||||
MidiLibManager.RenameMidiFile(TestFileName, renamedFileName);
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(ResManager.UserData.Exists(TestUserDataDir / renamedFileName), Is.True);
|
||||
Assert.That(ResManager.UserData.Exists(TestFullPath), Is.False);
|
||||
Assert.That(removedFileName, Is.EqualTo(TestFileName));
|
||||
Assert.That(addedFileName, Is.EqualTo(renamedFileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
using Content.Client.UserInterface.Systems.Hotbar.Widgets;
|
||||
using Content.Client.UserInterface.Systems.Storage.Controls;
|
||||
using Content.IntegrationTests.NUnit.Constraints;
|
||||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Changeling.Components;
|
||||
using Content.Shared.Changeling.Systems;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Input;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Changeling;
|
||||
|
||||
[TestOf(typeof(ChangelingTransformSystem))]
|
||||
public sealed class ChangelingSlimeTests : InteractionTest
|
||||
{
|
||||
protected override string PlayerPrototype => "MobLing";
|
||||
private static readonly EntProtoId SlimeHumanoidProtoId = "MobSlimePerson";
|
||||
private static readonly EntProtoId AppleProtoId = "FoodApple";
|
||||
|
||||
[SidedDependency(Side.Server)] private SharedChangelingIdentitySystem _changelingIdentity = default!;
|
||||
[SidedDependency(Side.Server)] private ChangelingTransformSystem _changelingTransform = default!;
|
||||
[SidedDependency(Side.Server)] private SharedStorageSystem _sharedStorage = default!;
|
||||
[SidedDependency(Side.Server)] private SharedTransformSystem _transform = default!;
|
||||
[SidedDependency(Side.Server)] private SharedContainerSystem _container = default!;
|
||||
[SidedDependency(Side.Server)] private SharedHandsSystem _hands = default!;
|
||||
|
||||
public override async Task DoSetup()
|
||||
{
|
||||
await base.DoSetup();
|
||||
|
||||
// Set up the ling with a slime present and already consumed.
|
||||
var slime = await SpawnTarget(SlimeHumanoidProtoId);
|
||||
var slimeEntity = ToServer(slime);
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
// Just give the ling the identity of a slime, no need to mess around with devouring, that's on the devour test to handle.
|
||||
_changelingIdentity.GrantIdentity(SPlayer, slimeEntity);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description(
|
||||
"Test that a changeling transforming into a slime will gain the appropriate storage container and BUI associated with the species and lose them when transforming back into a human.")]
|
||||
public async Task TransformIntoSlimeTest()
|
||||
{
|
||||
Assume.That(_changelingIdentity.TryGetDataFromOriginal(SPlayer,
|
||||
SPlayer,
|
||||
out var humanIdentityData), "Failed to get the changeling's human identity data.");
|
||||
Assume.That(_changelingIdentity.TryGetDataFromOriginal(
|
||||
SPlayer,
|
||||
STarget!.Value,
|
||||
out var slimeIdentity), "Failed to get the changeling's slime identity data.");
|
||||
|
||||
// Assert that the player does not have a storage component or BUI before transforming into a slime.
|
||||
Assert.That(SPlayer, Has.No.Comp<StorageComponent>(Server), "Non-slime player spawned with a storage component.");
|
||||
Assert.That(CUiSys.TryGetInterfaceData(CPlayer, StorageComponent.StorageUiKey.Key, out _), Is.False, "Non-slime player spawned with a storage BUI.");
|
||||
Assert.That(CUiSys.TryGetInterfaceData(CPlayer, ChangelingTransformUiKey.Key, out _), Is.True, "Changeling did not spawn with a changeling transform BUI.");
|
||||
|
||||
// Transform the player into a slime.
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
_changelingTransform.TransformInto(SPlayer, slimeIdentity!.Identity!.Value);
|
||||
});
|
||||
await AwaitDoAfters();
|
||||
|
||||
// Check storage and BUI presence.
|
||||
Assert.That(SPlayer, Has.Comp<StorageComponent>(Server), "Changeling did not gain a storage component when transforming into a slime.");
|
||||
Assert.That(CUiSys.TryGetInterfaceData(CPlayer, StorageComponent.StorageUiKey.Key, out _), Is.True, "Changeling did not gain a storage BUI when transforming into a slime.");
|
||||
Assert.That(CUiSys.TryGetInterfaceData(CPlayer, ChangelingTransformUiKey.Key, out _), Is.True, "Changeling lost their changeling transform BUI when transforming into a slime.");
|
||||
|
||||
// Transform the player back into a human.
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
_changelingTransform.TransformInto(SPlayer, humanIdentityData!.Identity!.Value);
|
||||
|
||||
});
|
||||
await AwaitDoAfters();
|
||||
|
||||
// Reassert that the storage and BUI presence is back to the original state.
|
||||
Assert.That(SPlayer, Has.No.Comp<StorageComponent>(Server), "Changeling did not lose their storage component when transforming back into a human.");
|
||||
// Assert.That(CUiSys.TryGetInterfaceData(CPlayer, StorageComponent.StorageUiKey.Key, out _), Is.False, "Changeling did not lose their storage BUI when transforming back into a human."); // TODO: BUI is not being removed right now.
|
||||
Assert.That(CUiSys.TryGetInterfaceData(CPlayer, ChangelingTransformUiKey.Key, out _), Is.True, "Changeling lost their changeling transform BUI when transforming back into a human.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description(
|
||||
"Test that a changeling transforming between slimes wont lose a storage")]
|
||||
public async Task TransformPreserveStorage()
|
||||
{
|
||||
var lingIdentityComp = Comp<ChangelingIdentityComponent>(Player);
|
||||
Assume.That(_changelingIdentity.TryGetDataFromOriginal(SPlayer,
|
||||
STarget!.Value,
|
||||
out var slimeIdentity1), "Failed to get the changeling's slime identity data.");
|
||||
|
||||
// Spawn a second slime.
|
||||
var secondSlime = await Spawn(SlimeHumanoidProtoId);
|
||||
var secondSlimeEntity = ToServer(secondSlime);
|
||||
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
_changelingIdentity.GrantIdentity(SPlayer, secondSlimeEntity);
|
||||
Assume.That(lingIdentityComp.ConsumedIdentities, Has.Count.EqualTo(3), "Changeling did not gain the correct number of identities.");
|
||||
// Transform into the first slime.
|
||||
_changelingTransform.TransformInto(SPlayer, slimeIdentity1!.Identity!.Value);
|
||||
});
|
||||
await AwaitDoAfters();
|
||||
|
||||
Assume.That(_changelingIdentity.TryGetDataFromOriginal(SPlayer,
|
||||
secondSlimeEntity,
|
||||
out var slimeIdentity2), "Failed to get the changeling's second slime identity data.");
|
||||
|
||||
Assert.That(SPlayer, Has.Comp<StorageComponent>(Server), "Changeling did not gain a storage component when transforming into a slime.");
|
||||
// Spawn a test item in the players hand.
|
||||
var apple = await PlaceInHands(AppleProtoId);
|
||||
var appleEnt = ToServer(apple);
|
||||
// Now insert it into our slime storage.
|
||||
var storageComponent = Comp<StorageComponent>(Player);
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
_sharedStorage.PlayerInsertHeldEntity(SPlayer, SPlayer);
|
||||
});
|
||||
Assert.That(storageComponent.StoredItems, Has.Count.EqualTo(1));
|
||||
Assert.That(storageComponent.StoredItems.TryGetValue(appleEnt, out var appleStoredLocation), "Failed to get the stored location of the apple.");
|
||||
|
||||
// Transform into the second slime we added earlier.
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
_changelingTransform.TransformInto(SPlayer, slimeIdentity2!.Identity!.Value);
|
||||
});
|
||||
await AwaitDoAfters();
|
||||
|
||||
// Check that it's the same component from earlier and that the apple is in the same container.
|
||||
storageComponent = Comp<StorageComponent>(Player);
|
||||
Assert.That(storageComponent.StoredItems, Has.Count.EqualTo(1), "Changeling lost their storage contents when transforming between slimes.");
|
||||
Assert.That(_container.TryGetContainingContainer(appleEnt, out var container), "Failed to get the container for the stored item after transforming between slimes.");
|
||||
Assert.That(container, Is.EqualTo(storageComponent.Container), "The stored item is no longer in the same storage container after transforming between slimes.");
|
||||
Assert.That(storageComponent.StoredItems.TryGetValue(appleEnt, out var postTransformStoredLocation), "Failed to get the stored location of the apple after transforming between slimes.");
|
||||
Assert.That(appleStoredLocation, Is.EqualTo(postTransformStoredLocation), "The stored item is no longer in the same location within the storage container after transforming between slimes.");
|
||||
|
||||
// Actually pull the apple from the inventory.
|
||||
await Activate(Player);
|
||||
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), "Storage BUI did not open when activating the changeling.");
|
||||
var ctrl = GetStorageControl(apple);
|
||||
await ClickControl(ctrl, ContentKeyFunctions.MoveStoredItem);
|
||||
await RunUntilSynced();
|
||||
Assert.That(_hands.IsHolding((SPlayer, Hands), appleEnt), "Changeling did not successfully pull the stored item from their storage.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description(
|
||||
"Test that a changeling transforming out of a slime drops the item inside their storage onto the ground")]
|
||||
public async Task TransformDropStorage()
|
||||
{
|
||||
var transformComponent = Comp<TransformComponent>(Player);
|
||||
// Set up having an apple inside the slimes storage.
|
||||
Assume.That(_changelingIdentity.TryGetDataFromOriginal(SPlayer,
|
||||
STarget!.Value,
|
||||
out var slimeIdentity), "Failed to get slime identity.");
|
||||
Assume.That(_changelingIdentity.TryGetDataFromOriginal(SPlayer,
|
||||
SPlayer,
|
||||
out var humanIdentity), "Failed to get human identity.");
|
||||
|
||||
var apple = await PlaceInHands(AppleProtoId);
|
||||
var appleEnt = ToServer(apple);
|
||||
|
||||
// Transform into a slime.
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
_changelingTransform.TransformInto(SPlayer, slimeIdentity!.Identity!.Value);
|
||||
});
|
||||
await AwaitDoAfters();
|
||||
|
||||
// Insert the apple into the slime storage.
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
_sharedStorage.PlayerInsertHeldEntity(SPlayer, SPlayer);
|
||||
});
|
||||
var storageComponent = Comp<StorageComponent>(Player);
|
||||
Assert.That(_container.TryGetContainingContainer(appleEnt, out var container), "Failed to get the container for the stored item after inserting into slime storage.");
|
||||
Assert.That(container, Is.EqualTo(storageComponent.Container), "The stored item is not in the storage container after inserting into slime storage.");
|
||||
|
||||
// Now transform out.
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
_changelingTransform.TransformInto(SPlayer, humanIdentity!.Identity!.Value);
|
||||
});
|
||||
await AwaitDoAfters();
|
||||
|
||||
// Assert that the storage container has been properly removed from the player,
|
||||
// items have been dumped and the lifestage for the StorageComponent has become Deleted
|
||||
Assert.That(SPlayer, Has.No.Comp<StorageComponent>(Server));
|
||||
Assert.That(storageComponent.StoredItems, Has.Count.EqualTo(0));
|
||||
Assert.That(storageComponent.LifeStage, Is.EqualTo(ComponentLifeStage.Deleted));
|
||||
|
||||
// Does the apple still exist?
|
||||
Assert.That(_container.TryGetContainingContainer(appleEnt, out _), Is.False, "The apple is still in a container after the changeling transformed out of slime form.");
|
||||
Assert.That(_transform.InRange(SPlayer, appleEnt, 1), Is.True, "The apple is not within range after the changeling transformed out of slime form.");
|
||||
}
|
||||
|
||||
private ItemGridPiece GetStorageControl(NetEntity target)
|
||||
{
|
||||
var uid = ToClient(target);
|
||||
var hotbar = GetWidget<HotbarGui>();
|
||||
var storageContainer = GetControlFromField<Control>(nameof(HotbarGui.SingleStorageContainer), hotbar);
|
||||
return GetControlFromChildren<ItemGridPiece>(c => c.Entity == uid, storageContainer);
|
||||
}
|
||||
}
|
||||
123
Content.IntegrationTests/Tests/Chemistry/RiggableTest.cs
Normal file
123
Content.IntegrationTests/Tests/Chemistry/RiggableTest.cs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Item.ItemToggle.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Chemistry;
|
||||
|
||||
[TestOf(typeof(RiggableSystem))]
|
||||
public sealed class RiggableTest : InteractionTest
|
||||
{
|
||||
private static readonly EntProtoId HumanProtoId = "MobHuman";
|
||||
private static readonly EntProtoId BatteryProto = "PowerCellSmall";
|
||||
private static readonly EntProtoId FlashlightProto = "EmptyFlashlightLantern";
|
||||
private static readonly EntProtoId StunbatonProto = "Stunbaton";
|
||||
|
||||
[SidedDependency(Side.Server)] private readonly DamageableSystem _damageable = default!;
|
||||
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = @"
|
||||
- type: entity
|
||||
parent: PrefilledSyringe
|
||||
id: TestPlasmaSyringe
|
||||
components:
|
||||
- type: Solution
|
||||
solution:
|
||||
reagents:
|
||||
- ReagentId: Plasma
|
||||
Quantity: 15
|
||||
|
||||
- type: entity
|
||||
parent: PrefilledSyringe
|
||||
id: TestMilkSyringe
|
||||
components:
|
||||
- type: Solution
|
||||
solution:
|
||||
reagents:
|
||||
- ReagentId: Milk
|
||||
Quantity: 15
|
||||
";
|
||||
|
||||
[Test]
|
||||
[TestCase("TestPlasmaSyringe", ExpectedResult = true)]
|
||||
[TestCase("TestMilkSyringe", ExpectedResult = false)]
|
||||
[Description("Gives the player a power cell, injects it with different solutions and tests the rigged cell in a flashlight")]
|
||||
public async Task<bool> RigBatteryTest(string syringe)
|
||||
{
|
||||
await AddAtmosphere();
|
||||
await SpawnTarget(HumanProtoId);
|
||||
|
||||
Entity<DamageableComponent> mob = (STarget.Value, Comp<DamageableComponent>());
|
||||
Assert.That(_damageable.GetPositiveDamage(mob).GetTotal(), Is.EqualTo(FixedPoint2.Zero),
|
||||
"Player spawned with damage.");
|
||||
|
||||
var battery = await SpawnTarget(BatteryProto);
|
||||
|
||||
// Rig the cell
|
||||
await PlaceInHands(syringe);
|
||||
await Interact();
|
||||
|
||||
// Put it into a flashlight
|
||||
await Pickup(battery);
|
||||
await SpawnTarget(FlashlightProto);
|
||||
await Interact();
|
||||
await RunTicks(5);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(HandSys.GetActiveItem((SPlayer, Hands)), Is.Null,
|
||||
"Battery did not get inserted into the flashlight.");
|
||||
Assert.That(_damageable.GetPositiveDamage(mob).GetTotal(), Is.EqualTo(FixedPoint2.Zero),
|
||||
"Player received damage before flashlight activation.");
|
||||
}
|
||||
|
||||
// Turn the flashlight on and observe the result
|
||||
await Activate();
|
||||
await RunTicks(5);
|
||||
|
||||
if (_damageable.GetPositiveDamage(mob).GetTotal() > FixedPoint2.Zero)
|
||||
{
|
||||
// Flashlight exploded
|
||||
AssertDeleted(battery);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nothing happened
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("Gives the player an activated stunbaton and tests that it explodes immediately on plasma injection")]
|
||||
public async Task RigActivatedTest()
|
||||
{
|
||||
await AddAtmosphere();
|
||||
await SpawnTarget(HumanProtoId);
|
||||
|
||||
Entity<DamageableComponent> mob = (STarget.Value, Comp<DamageableComponent>());
|
||||
Assert.That(_damageable.GetPositiveDamage(mob).GetTotal(), Is.EqualTo(FixedPoint2.Zero),
|
||||
"Player spawned with damage.");
|
||||
|
||||
var baton = await PlaceInHands(StunbatonProto);
|
||||
|
||||
Assert.That(Comp<ItemToggleComponent>(baton).Activated, Is.True, "Stunbaton did not activate");
|
||||
|
||||
// Rig the baton
|
||||
await PlaceInHands("TestPlasmaSyringe");
|
||||
await Interact();
|
||||
|
||||
await RunTicks(5);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(_damageable.GetPositiveDamage(mob).GetTotal(), Is.GreaterThan(FixedPoint2.Zero), "Rigged stunbaton did not explode?");
|
||||
AssertDeleted(baton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,21 +2,21 @@ using Content.Shared.Chemistry.Reaction;
|
|||
using Content.Shared.Chemistry.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.IntegrationTests.Utility;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Chemistry
|
||||
namespace Content.IntegrationTests.Tests.Chemistry;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(typeof(ReactionPrototype))]
|
||||
public sealed class TryAllReactionsTest : GameTest
|
||||
{
|
||||
[TestFixture]
|
||||
[TestOf(typeof(ReactionPrototype))]
|
||||
public sealed class TryAllReactionsTest : GameTest
|
||||
{
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = @"
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = @"
|
||||
- type: entity
|
||||
id: TestSolutionContainer
|
||||
components:
|
||||
|
|
@ -25,115 +25,124 @@ namespace Content.IntegrationTests.Tests.Chemistry
|
|||
solution:
|
||||
maxVol: 120";
|
||||
|
||||
private static string[] _reactions = GameDataScrounger.PrototypesOfKind<ReactionPrototype>();
|
||||
private static readonly string[] Reactions = GameDataScrounger.PrototypesOfKind<ReactionPrototype>();
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(_reactions))]
|
||||
[TestOf(typeof(ReactionPrototype))]
|
||||
[Description("Tries an individual reaction to see if it succeeds.")]
|
||||
public async Task TryReaction(string reaction)
|
||||
[SidedDependency(Side.Server)] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
|
||||
[Test]
|
||||
[TestOf(typeof(ReactionPrototype))]
|
||||
[Description("Tries an individual reaction to see if it succeeds.")]
|
||||
public async Task TryReaction()
|
||||
{
|
||||
var testMap = await Pair.CreateTestMap();
|
||||
var coordinates = testMap.GridCoords;
|
||||
|
||||
// they call me a bird the way i be nesting
|
||||
try
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var testMap = await pair.CreateTestMap();
|
||||
var coordinates = testMap.GridCoords;
|
||||
var solutionContainerSystem = entityManager.System<SharedSolutionContainerSystem>();
|
||||
|
||||
var reactionPrototype = prototypeManager.Index<ReactionPrototype>(reaction);
|
||||
|
||||
EntityUid beaker = default;
|
||||
Solution solution = null;
|
||||
Entity<SolutionComponent>? solutionEnt = default!;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
beaker = entityManager.SpawnEntity("TestSolutionContainer", coordinates);
|
||||
Assert.That(solutionContainerSystem
|
||||
.TryGetSolution(beaker, "beaker", out solutionEnt, out solution));
|
||||
solutionContainerSystem.SetCanReact(solutionEnt!.Value, false);
|
||||
foreach (var (id, reactant) in reactionPrototype.Reactants)
|
||||
{
|
||||
#pragma warning disable NUnit2045
|
||||
Assert.That(solutionContainerSystem
|
||||
.TryAddReagent(solutionEnt.Value,
|
||||
id,
|
||||
reactant.Amount,
|
||||
out var quantity,
|
||||
reactionPrototype.MinimumTemperature));
|
||||
Assert.That(reactant.Amount, Is.EqualTo(quantity));
|
||||
#pragma warning restore NUnit2045
|
||||
}
|
||||
|
||||
//Get all possible reactions with the current reagents
|
||||
var possibleReactions = prototypeManager.EnumeratePrototypes<ReactionPrototype>()
|
||||
.Where(x => x.Reactants.All(id => solution.Contents.Any(s => s.Reagent.Prototype == id.Key)))
|
||||
.ToList();
|
||||
|
||||
//Check if the reaction is the first to occur when heated
|
||||
foreach (var possibleReaction in possibleReactions.OrderBy(r => r.MinimumTemperature))
|
||||
{
|
||||
if (possibleReaction.Priority >= reactionPrototype.Priority &&
|
||||
possibleReaction.MinimumTemperature < reactionPrototype.MinimumTemperature &&
|
||||
possibleReaction.MixingCategories == reactionPrototype.MixingCategories)
|
||||
{
|
||||
Assert.Fail(
|
||||
$"The {possibleReaction.ID} reaction may occur before {reactionPrototype.ID} when heated.");
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the reaction is the first to occur when freezing
|
||||
foreach (var possibleReaction in possibleReactions.OrderBy(r => r.MaximumTemperature))
|
||||
{
|
||||
if (possibleReaction.Priority >= reactionPrototype.Priority &&
|
||||
possibleReaction.MaximumTemperature > reactionPrototype.MaximumTemperature &&
|
||||
possibleReaction.MixingCategories == reactionPrototype.MixingCategories)
|
||||
{
|
||||
Assert.Fail(
|
||||
$"The {possibleReaction.ID} reaction may occur before {reactionPrototype.ID} when freezing.");
|
||||
}
|
||||
}
|
||||
|
||||
//Now safe set the temperature and mix the reagents
|
||||
solutionContainerSystem.SetTemperature(solutionEnt.Value, reactionPrototype.MinimumTemperature);
|
||||
solutionContainerSystem.SetCanReact(solutionEnt.Value, true);
|
||||
|
||||
if (reactionPrototype.MixingCategories != null)
|
||||
{
|
||||
var dummyEntity = entityManager.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
var mixerComponent = entityManager.AddComponent<ReactionMixerComponent>(dummyEntity);
|
||||
mixerComponent.ReactionTypes = reactionPrototype.MixingCategories;
|
||||
solutionContainerSystem.UpdateChemicals(solutionEnt.Value, true, mixerComponent);
|
||||
}
|
||||
});
|
||||
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
foreach (var reaction in Reactions)
|
||||
{
|
||||
//you just got linq'd fool
|
||||
//(i'm sorry)
|
||||
var foundProductsMap = reactionPrototype.Products
|
||||
.Concat(reactionPrototype.Reactants
|
||||
.Where(x => x.Value.Catalyst)
|
||||
.ToDictionary(x => x.Key, x => x.Value.Amount)
|
||||
)
|
||||
.ToDictionary(x => x, _ => false);
|
||||
var reactionPrototype = SProtoMan.Index<ReactionPrototype>(reaction);
|
||||
|
||||
foreach (var (reagent, quantity) in solution.Contents)
|
||||
EntityUid beaker = default;
|
||||
Solution solution = null;
|
||||
|
||||
try
|
||||
{
|
||||
Assert.That(foundProductsMap.TryFirstOrNull(
|
||||
x => x.Key.Key == reagent.Prototype && x.Key.Value == quantity,
|
||||
out var foundProduct));
|
||||
foundProductsMap[foundProduct!.Value.Key] = true;
|
||||
await Pair.Server.WaitAssertion(() =>
|
||||
{
|
||||
beaker = SEntMan.SpawnEntity("TestSolutionContainer", coordinates);
|
||||
Assert.That(_solutionContainerSystem
|
||||
.TryGetSolution(beaker, "beaker", out var solutionEnt, out solution));
|
||||
_solutionContainerSystem.SetCanReact(solutionEnt!.Value, false);
|
||||
foreach (var (id, reactant) in reactionPrototype.Reactants)
|
||||
{
|
||||
Assert.That(_solutionContainerSystem
|
||||
.TryAddReagent(solutionEnt.Value,
|
||||
id,
|
||||
reactant.Amount,
|
||||
out var quantity,
|
||||
reactionPrototype.MinimumTemperature));
|
||||
Assert.That(reactant.Amount, Is.EqualTo(quantity));
|
||||
}
|
||||
|
||||
//Get all possible reactions with the current reagents
|
||||
var possibleReactions = SProtoMan.EnumeratePrototypes<ReactionPrototype>()
|
||||
.Where(x => x.Reactants.All(id =>
|
||||
solution.Contents.Any(s => s.Reagent.Prototype == id.Key)))
|
||||
.ToList();
|
||||
|
||||
//Check if the reaction is the first to occur when heated
|
||||
foreach (var possibleReaction in possibleReactions.OrderBy(r => r.MinimumTemperature))
|
||||
{
|
||||
if (possibleReaction.Priority >= reactionPrototype.Priority &&
|
||||
possibleReaction.MinimumTemperature < reactionPrototype.MinimumTemperature &&
|
||||
possibleReaction.MixingCategories == reactionPrototype.MixingCategories)
|
||||
{
|
||||
Assert.Fail(
|
||||
$"The {possibleReaction.ID} reaction may occur before {reactionPrototype.ID} when heated.");
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the reaction is the first to occur when freezing
|
||||
foreach (var possibleReaction in possibleReactions.OrderBy(r => r.MaximumTemperature))
|
||||
{
|
||||
if (possibleReaction.Priority >= reactionPrototype.Priority &&
|
||||
possibleReaction.MaximumTemperature > reactionPrototype.MaximumTemperature &&
|
||||
possibleReaction.MixingCategories == reactionPrototype.MixingCategories)
|
||||
{
|
||||
Assert.Fail(
|
||||
$"The {possibleReaction.ID} reaction may occur before {reactionPrototype.ID} when freezing.");
|
||||
}
|
||||
}
|
||||
|
||||
//Now safe set the temperature and mix the reagents
|
||||
_solutionContainerSystem.SetTemperature(solutionEnt.Value,
|
||||
reactionPrototype.MinimumTemperature);
|
||||
_solutionContainerSystem.SetCanReact(solutionEnt.Value, true);
|
||||
|
||||
if (reactionPrototype.MixingCategories != null)
|
||||
{
|
||||
var dummyEntity = SEntMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
var mixerComponent = SEntMan.AddComponent<ReactionMixerComponent>(dummyEntity);
|
||||
mixerComponent.ReactionTypes = reactionPrototype.MixingCategories;
|
||||
_solutionContainerSystem.UpdateChemicals(solutionEnt.Value, true, mixerComponent);
|
||||
}
|
||||
});
|
||||
|
||||
await Pair.Server.WaitIdleAsync();
|
||||
|
||||
await Pair.Server.WaitAssertion(() =>
|
||||
{
|
||||
//you just got linq'd fool
|
||||
//(i'm sorry)
|
||||
var foundProductsMap = reactionPrototype.Products
|
||||
.Concat(reactionPrototype.Reactants
|
||||
.Where(x => x.Value.Catalyst)
|
||||
.ToDictionary(x => x.Key, x => x.Value.Amount)
|
||||
)
|
||||
.ToDictionary(x => x, _ => false);
|
||||
|
||||
foreach (var (reagent, quantity) in solution.Contents)
|
||||
{
|
||||
Assert.That(foundProductsMap.TryFirstOrNull(
|
||||
x => x.Key.Key == reagent.Prototype && x.Key.Value == quantity,
|
||||
out var foundProduct));
|
||||
foundProductsMap[foundProduct!.Value.Key] = true;
|
||||
}
|
||||
|
||||
Assert.That(foundProductsMap.All(x => x.Value));
|
||||
});
|
||||
}
|
||||
|
||||
Assert.That(foundProductsMap.All(x => x.Value));
|
||||
|
||||
server.EntMan.DeleteEntity(beaker);
|
||||
});
|
||||
finally
|
||||
{
|
||||
await Server.WaitPost(() => SEntMan.DeleteEntity(beaker));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Server.WaitPost(() => SEntMan.DeleteEntity(testMap.MapUid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.IntegrationTests.Utility;
|
||||
|
|
@ -8,7 +6,6 @@ using Content.Shared.Damage.Components;
|
|||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Damageable;
|
||||
|
||||
|
|
@ -19,42 +16,60 @@ public sealed class DamageAllPrototypesTest : GameTest
|
|||
{
|
||||
[SidedDependency(Side.Server)] private readonly DamageableSystem _damageableSystem = default!;
|
||||
|
||||
private static string[] _damageables = GameDataScrounger.EntitiesWithComponent("Damageable");
|
||||
|
||||
[Test]
|
||||
[TestOf(typeof(DamageableSystem))]
|
||||
[TestCaseSource(nameof(_damageables))]
|
||||
[Description("Ensures all Entity Prototypes with damageable can be damaged.")]
|
||||
public async Task TestDamageableComponents(string damageable)
|
||||
public async Task TestDamageableComponents()
|
||||
{
|
||||
var map = await Pair.CreateTestMap();
|
||||
|
||||
var entity = await SpawnAtPosition(damageable, map.GridCoords);
|
||||
|
||||
// Intentionally cannot take damage, ignore it.
|
||||
if (SEntMan.HasComponent<GodmodeComponent>(entity))
|
||||
return;
|
||||
|
||||
var canBeDamaged = false;
|
||||
|
||||
foreach (var type in SProtoMan.EnumeratePrototypes<DamageTypePrototype>())
|
||||
try
|
||||
{
|
||||
if (!_damageableSystem.CanBeDamagedBy(entity, type))
|
||||
continue;
|
||||
|
||||
canBeDamaged = true;
|
||||
|
||||
await Server.WaitPost(() =>
|
||||
foreach (var damageable in GameDataScrounger.EntitiesWithComponent("Damageable"))
|
||||
{
|
||||
var damage = new DamageSpecifier(type, FixedPoint2.Epsilon);
|
||||
var previousDamage = _damageableSystem.GetTotalDamage(entity);
|
||||
_damageableSystem.ChangeDamage(entity, damage, ignoreResistances: true);
|
||||
Assert.That(_damageableSystem.GetTotalDamage(entity) == FixedPoint2.Epsilon + previousDamage);
|
||||
_damageableSystem.ClearAllDamage(entity);
|
||||
});
|
||||
}
|
||||
var entity = await SpawnAtPosition(damageable, map.GridCoords);
|
||||
|
||||
// Ensure that this entity can actually be damaged.
|
||||
Assert.That(canBeDamaged);
|
||||
try
|
||||
{
|
||||
// Intentionally cannot take damage, ignore it.
|
||||
if (SEntMan.HasComponent<GodmodeComponent>(entity))
|
||||
continue;
|
||||
|
||||
var canBeDamaged = false;
|
||||
|
||||
foreach (var type in SProtoMan.EnumeratePrototypes<DamageTypePrototype>())
|
||||
{
|
||||
if (!_damageableSystem.CanBeDamagedBy(entity, type))
|
||||
continue;
|
||||
|
||||
canBeDamaged = true;
|
||||
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
var damage = new DamageSpecifier(type, FixedPoint2.Epsilon);
|
||||
var previousDamage = _damageableSystem.GetTotalDamage(entity);
|
||||
_damageableSystem.ChangeDamage(entity, damage, ignoreResistances: true);
|
||||
Assert.That(
|
||||
_damageableSystem.GetTotalDamage(entity),
|
||||
Is.EqualTo(FixedPoint2.Epsilon + previousDamage),
|
||||
$"{damageable} should take {type.ID} damage.");
|
||||
|
||||
_damageableSystem.ClearAllDamage(entity);
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure that this entity can actually be damaged.
|
||||
Assert.That(canBeDamaged, Is.True, $"{damageable} cannot be damaged by any damage type.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Server.WaitPost(() => SEntMan.DeleteEntity(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Server.WaitPost(() => SEntMan.DeleteEntity(map.MapUid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
69
Content.IntegrationTests/Tests/Eye/BlindnessTests.cs
Normal file
69
Content.IntegrationTests/Tests/Eye/BlindnessTests.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Eye.Blinding.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Eye;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for things that directly cause blindness unrelated to eye damage
|
||||
/// </summary>
|
||||
public sealed class BlindnessTests : InteractionTest
|
||||
{
|
||||
// We need eye slots (and eyeballs) to blind/damage
|
||||
protected override string PlayerPrototype => "MobHuman";
|
||||
private static readonly EntProtoId BlindfoldPrototype = "ClothingEyesBlindfold";
|
||||
|
||||
[SidedDependency(Side.Server)] private readonly SleepingSystem _sleeping = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that equipping and removing a blindfold adds and removes blindness, respectively
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task BlindfoldBlindnessTest()
|
||||
{
|
||||
var blindableComponent = Comp<BlindableComponent>(Player);
|
||||
|
||||
Assert.That(blindableComponent.IsBlind, Is.False, "Initial blind check failed");
|
||||
|
||||
// Grab blindfold, use it, and wait for the DoAfter
|
||||
var blindfold = await PlaceInHands(BlindfoldPrototype);
|
||||
await UseInHand();
|
||||
await AwaitDoAfters();
|
||||
|
||||
Assert.That(blindableComponent.IsBlind, Is.True, "Blindfold did not cause blindness");
|
||||
|
||||
//Now we remove the blindfold and test that IsBlind isn't set
|
||||
await Delete(blindfold);
|
||||
|
||||
Assert.That(blindableComponent.IsBlind, Is.False, "Removing blindfold did not remove blindness");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that falling asleep causes blindness
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task SleepBlindnessTest()
|
||||
{
|
||||
var blindableComponent = Comp<BlindableComponent>(Player);
|
||||
|
||||
// Sleeping inflicts blindness
|
||||
Assert.That(blindableComponent.IsBlind, Is.False, "Initial blind check failed");
|
||||
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
_sleeping.TrySleeping(SPlayer);
|
||||
});
|
||||
|
||||
Assert.That(blindableComponent.IsBlind, Is.True, "Sleeping did not cause blindness");
|
||||
|
||||
// Waking up removes blindness
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
_sleeping.TryWaking(SPlayer);
|
||||
});
|
||||
|
||||
Assert.That(blindableComponent.IsBlind, Is.False, "Waking up did not remove blindness");
|
||||
}
|
||||
}
|
||||
67
Content.IntegrationTests/Tests/Eye/EyeDamageTests.cs
Normal file
67
Content.IntegrationTests/Tests/Eye/EyeDamageTests.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Eye.Blinding.Components;
|
||||
using Content.Shared.Eye.Blinding.Systems;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Eye;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for eye damage
|
||||
/// </summary>
|
||||
public sealed class EyeDamageTests : InteractionTest
|
||||
{
|
||||
// We need eye slots (and eyeballs) to blind/damage
|
||||
protected override string PlayerPrototype => "MobHuman";
|
||||
private static readonly EntProtoId LockerPrototype = "LockerFreezer";
|
||||
private static readonly EntProtoId WeldingMaskPrototype = "ClothingHeadHatWelding";
|
||||
private const int ExpectedWelderEyeDamage = 1; // 1 is a magic number in EyeProtectionSystem.cs. Test will fail if this is changed at some point.
|
||||
|
||||
[SidedDependency(Side.Server)] private readonly BlindableSystem _blindable = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that applying maximum eye damage triggers blindness.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task EyeDamageBlindsTest()
|
||||
{
|
||||
var blindableComponent = Comp<BlindableComponent>(Player);
|
||||
|
||||
Assert.That(blindableComponent.IsBlind, Is.False, "Initial blind check failed");
|
||||
|
||||
// Max eye damage inflicts blindness
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
_blindable.AdjustEyeDamage(SPlayer, blindableComponent.MaxDamage);
|
||||
});
|
||||
|
||||
Assert.That(blindableComponent.IsBlind, Is.True, "Max eye damage did not inflict blindness");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that welding without protection causes eye damage, and welding with protection does not.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task WelderCausesEyeDamageTest()
|
||||
{
|
||||
var blindableComponent = Comp<BlindableComponent>(Player);
|
||||
await SpawnTarget(LockerPrototype);
|
||||
|
||||
// Welding without protection causes eye damage
|
||||
Assert.That(blindableComponent.EyeDamage, Is.Zero, "Initial eye damage >0");
|
||||
|
||||
await InteractUsing(Weld);
|
||||
|
||||
Assert.That(blindableComponent.EyeDamage, Is.EqualTo(ExpectedWelderEyeDamage), "Unexpected eye damage amount");
|
||||
|
||||
// Welding with protection prevents eye damage
|
||||
var initialEyeDamage = blindableComponent.EyeDamage;
|
||||
|
||||
await PlaceInHands(WeldingMaskPrototype);
|
||||
await UseInHand();
|
||||
await AwaitDoAfters();
|
||||
await InteractUsing(Weld);
|
||||
|
||||
Assert.That(blindableComponent.EyeDamage, Is.EqualTo(initialEyeDamage), "Eye damage inflicted despite protection");
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue