mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-02-15 04:30:57 +01:00
Merge remote-tracking branch 'wizards/master' into upstream-sync
# Conflicts: # Resources/Prototypes/Catalog/VendingMachines/Inventories/lawdrobe.yml # Resources/Prototypes/Roles/Jobs/Cargo/cargo_technician.yml # Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml # Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml # Resources/ServerInfo/Guidebook/Controls/Controls.xml # Resources/ServerInfo/Guidebook/Controls/Radio.xml # Resources/ServerInfo/Guidebook/Engineering/AME.xml # Resources/ServerInfo/Guidebook/Engineering/Fires.xml # Resources/ServerInfo/Guidebook/Engineering/Singularity.xml # Resources/ServerInfo/Guidebook/Jobs.xml # Resources/ServerInfo/Guidebook/Science/Science.xml # Resources/ServerInfo/Guidebook/Security/DNA.xml # Resources/ServerInfo/Guidebook/Security/Security.xml # Resources/ServerInfo/Guidebook/SpaceStation14.xml # Resources/ServerInfo/Guidebook/Survival.xml # Resources/Textures/Clothing/Uniforms/Jumpskirt/cargotech.rsi/equipped-INNERCLOTHING.png # Resources/Textures/Clothing/Uniforms/Jumpskirt/cargotech.rsi/icon.png # Resources/Textures/Clothing/Uniforms/Jumpskirt/qm.rsi/equipped-INNERCLOTHING.png # Resources/Textures/Clothing/Uniforms/Jumpskirt/qm.rsi/icon.png # Resources/Textures/Clothing/Uniforms/Jumpsuit/cargotech.rsi/equipped-INNERCLOTHING.png # Resources/Textures/Clothing/Uniforms/Jumpsuit/cargotech.rsi/icon.png # Resources/Textures/Clothing/Uniforms/Jumpsuit/qm.rsi/equipped-INNERCLOTHING.png # Resources/Textures/Clothing/Uniforms/Jumpsuit/qm.rsi/icon.png # Resources/Textures/Clothing/Uniforms/Jumpsuit/salvage.rsi/equipped-INNERCLOTHING.png # Resources/Textures/Clothing/Uniforms/Jumpsuit/salvage.rsi/icon.png
This commit is contained in:
@@ -25,8 +25,8 @@ namespace Content.Client.Access.UI
|
||||
_window.OpenCentered();
|
||||
|
||||
_window.OnClose += Close;
|
||||
_window.OnNameEntered += OnNameChanged;
|
||||
_window.OnJobEntered += OnJobChanged;
|
||||
_window.OnNameChanged += OnNameChanged;
|
||||
_window.OnJobChanged += OnJobChanged;
|
||||
}
|
||||
|
||||
private void OnNameChanged(string newName)
|
||||
|
||||
@@ -7,16 +7,18 @@ namespace Content.Client.Access.UI
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AgentIDCardWindow : DefaultWindow
|
||||
{
|
||||
public event Action<string>? OnNameEntered;
|
||||
|
||||
public event Action<string>? OnJobEntered;
|
||||
public event Action<string>? OnNameChanged;
|
||||
public event Action<string>? OnJobChanged;
|
||||
|
||||
public AgentIDCardWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
NameLineEdit.OnTextEntered += e => OnNameEntered?.Invoke(e.Text);
|
||||
JobLineEdit.OnTextEntered += e => OnJobEntered?.Invoke(e.Text);
|
||||
NameLineEdit.OnTextEntered += e => OnNameChanged?.Invoke(e.Text);
|
||||
NameLineEdit.OnFocusExit += e => OnNameChanged?.Invoke(e.Text);
|
||||
|
||||
JobLineEdit.OnTextEntered += e => OnJobChanged?.Invoke(e.Text);
|
||||
JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
|
||||
}
|
||||
|
||||
public void SetCurrentName(string name)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using Content.Client.Cargo.UI;
|
||||
using Content.Shared.Cargo.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Cargo.BUI;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class CargoBountyConsoleBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[ViewVariables]
|
||||
private CargoBountyMenu? _menu;
|
||||
|
||||
public CargoBountyConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_menu = new();
|
||||
|
||||
_menu.OnClose += Close;
|
||||
|
||||
_menu.OnLabelButtonPressed += id =>
|
||||
{
|
||||
SendMessage(new BountyPrintLabelMessage(id));
|
||||
};
|
||||
|
||||
_menu.OpenCentered();
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState message)
|
||||
{
|
||||
base.UpdateState(message);
|
||||
|
||||
if (message is not CargoBountyConsoleState state)
|
||||
return;
|
||||
|
||||
_menu?.UpdateEntries(state.Bounties);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
_menu?.Dispose();
|
||||
}
|
||||
}
|
||||
27
Content.Client/Cargo/UI/BountyEntry.xaml
Normal file
27
Content.Client/Cargo/UI/BountyEntry.xaml
Normal file
@@ -0,0 +1,27 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
Margin="10 10 10 0"
|
||||
HorizontalExpand="True"
|
||||
Visible="True">
|
||||
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<RichTextLabel Name="TimeLabel"/>
|
||||
<RichTextLabel Name="RewardLabel"/>
|
||||
<RichTextLabel Name="ManifestLabel"/>
|
||||
</BoxContainer>
|
||||
<Control MinWidth="10"/>
|
||||
<BoxContainer Orientation="Vertical" MinWidth="120">
|
||||
<Button Name="PrintButton" Text="{Loc 'bounty-console-label-button-text'}" HorizontalExpand="False" HorizontalAlignment="Right"/>
|
||||
<Label Name="IdLabel" HorizontalAlignment="Right" Margin="0 0 5 0"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<customControls:HSeparator Margin="5 10 5 10"/>
|
||||
<BoxContainer>
|
||||
<RichTextLabel Name="DescriptionLabel"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
54
Content.Client/Cargo/UI/BountyEntry.xaml.cs
Normal file
54
Content.Client/Cargo/UI/BountyEntry.xaml.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Content.Client.Message;
|
||||
using Content.Shared.Cargo;
|
||||
using Content.Shared.Cargo.Prototypes;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Cargo.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BountyEntry : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
public Action? OnButtonPressed;
|
||||
|
||||
public TimeSpan EndTime;
|
||||
|
||||
public BountyEntry(CargoBountyData bounty)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
if (!_prototype.TryIndex<CargoBountyPrototype>(bounty.Bounty, out var bountyPrototype))
|
||||
return;
|
||||
|
||||
EndTime = bounty.EndTime;
|
||||
|
||||
var items = new List<string>();
|
||||
foreach (var entry in bountyPrototype.Entries)
|
||||
{
|
||||
items.Add(Loc.GetString("bounty-console-manifest-entry",
|
||||
("amount", entry.Amount),
|
||||
("item", Loc.GetString(entry.Name))));
|
||||
}
|
||||
ManifestLabel.SetMarkup(Loc.GetString("bounty-console-manifest-label", ("item", string.Join(", ", items))));
|
||||
RewardLabel.SetMarkup(Loc.GetString("bounty-console-reward-label", ("reward", bountyPrototype.Reward)));
|
||||
DescriptionLabel.SetMarkup(Loc.GetString("bounty-console-description-label", ("description", Loc.GetString(bountyPrototype.Description))));
|
||||
IdLabel.Text = Loc.GetString("bounty-console-id-label", ("id", bounty.Id));
|
||||
|
||||
PrintButton.OnPressed += _ => OnButtonPressed?.Invoke();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
var remaining = TimeSpan.FromSeconds(Math.Max((EndTime - _timing.CurTime).TotalSeconds, 0));
|
||||
TimeLabel.SetMarkup(Loc.GetString("bounty-console-time-label", ("time", remaining.ToString("mm':'ss"))));
|
||||
}
|
||||
}
|
||||
36
Content.Client/Cargo/UI/CargoBountyMenu.xaml
Normal file
36
Content.Client/Cargo/UI/CargoBountyMenu.xaml
Normal file
@@ -0,0 +1,36 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'bounty-console-menu-title'}"
|
||||
SetSize="550 420"
|
||||
MinSize="400 350">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer VerticalExpand="True" HorizontalExpand="True" Margin="10">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<ScrollContainer HScrollEnabled="False"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer Name="BountyEntriesContainer"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True">
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
|
||||
<Label Text="{Loc 'bounty-console-flavor-left'}" StyleClasses="WindowFooterText" />
|
||||
<Label Text="{Loc 'bounty-console-flavor-right'}" StyleClasses="WindowFooterText"
|
||||
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
|
||||
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
34
Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs
Normal file
34
Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Cargo;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Cargo.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CargoBountyMenu : FancyWindow
|
||||
{
|
||||
public Action<int>? OnLabelButtonPressed;
|
||||
|
||||
public CargoBountyMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void UpdateEntries(List<CargoBountyData> bounties)
|
||||
{
|
||||
BountyEntriesContainer.Children.Clear();
|
||||
foreach (var b in bounties)
|
||||
{
|
||||
var entry = new BountyEntry(b);
|
||||
entry.OnButtonPressed += () => OnLabelButtonPressed?.Invoke(b.Id);
|
||||
|
||||
BountyEntriesContainer.AddChild(entry);
|
||||
}
|
||||
BountyEntriesContainer.AddChild(new Control
|
||||
{
|
||||
MinHeight = 10
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@ namespace Content.Client.Disposal.Systems;
|
||||
|
||||
public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
{
|
||||
[Dependency] private readonly AppearanceSystem AppearanceSystem = default!;
|
||||
[Dependency] private readonly AnimationPlayerSystem AnimationSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem SoundSystem = default!;
|
||||
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
|
||||
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
private const string AnimationKey = "disposal_unit_animation";
|
||||
|
||||
private List<EntityUid> PressuringDisposals = new();
|
||||
private readonly List<EntityUid> _pressuringDisposals = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -28,25 +28,25 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
if (!PressuringDisposals.Contains(disposalEntity))
|
||||
PressuringDisposals.Add(disposalEntity);
|
||||
if (!_pressuringDisposals.Contains(disposalEntity))
|
||||
_pressuringDisposals.Add(disposalEntity);
|
||||
}
|
||||
else
|
||||
{
|
||||
PressuringDisposals.Remove(disposalEntity);
|
||||
_pressuringDisposals.Remove(disposalEntity);
|
||||
}
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
for (var i = PressuringDisposals.Count - 1; i >= 0; i--)
|
||||
for (var i = _pressuringDisposals.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var disposal = PressuringDisposals[i];
|
||||
var disposal = _pressuringDisposals[i];
|
||||
if (!UpdateInterface(disposal))
|
||||
continue;
|
||||
|
||||
PressuringDisposals.RemoveAt(i);
|
||||
_pressuringDisposals.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,17 +79,18 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||
return;
|
||||
|
||||
if(!sprite.LayerMapTryGet(DisposalUnitVisualLayers.Base, out var baseLayerIdx))
|
||||
if (!sprite.LayerMapTryGet(DisposalUnitVisualLayers.Base, out var baseLayerIdx))
|
||||
return; // Couldn't find the "normal" layer to return to after flush animation
|
||||
|
||||
if(!sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseFlush, out var flushLayerIdx))
|
||||
if (!sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseFlush, out var flushLayerIdx))
|
||||
return; // Couldn't find the flush animation layer
|
||||
|
||||
var originalBaseState = sprite.LayerGetState(baseLayerIdx);
|
||||
var flushState = sprite.LayerGetState(flushLayerIdx);
|
||||
|
||||
// Setup the flush animation to play
|
||||
disposalUnit.FlushAnimation = new Animation {
|
||||
disposalUnit.FlushAnimation = new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(disposalUnit.FlushTime),
|
||||
AnimationTracks = {
|
||||
new AnimationTrackSpriteFlick {
|
||||
@@ -109,9 +110,10 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
if (disposalUnit.FlushSound != null)
|
||||
{
|
||||
disposalUnit.FlushAnimation.AnimationTracks.Add(
|
||||
new AnimationTrackPlaySound {
|
||||
new AnimationTrackPlaySound
|
||||
{
|
||||
KeyFrames = {
|
||||
new AnimationTrackPlaySound.KeyFrame(SoundSystem.GetSound(disposalUnit.FlushSound), 0)
|
||||
new AnimationTrackPlaySound.KeyFrame(_audioSystem.GetSound(disposalUnit.FlushSound), 0)
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -134,7 +136,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
// Update visuals and tick animation
|
||||
private void UpdateState(EntityUid uid, DisposalUnitComponent unit, SpriteComponent sprite)
|
||||
{
|
||||
if (!AppearanceSystem.TryGetData<VisualState>(uid, Visuals.VisualState, out var state))
|
||||
if (!_appearanceSystem.TryGetData<VisualState>(uid, Visuals.VisualState, out var state))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -146,20 +148,20 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
|
||||
|
||||
if (state == VisualState.Flushing)
|
||||
{
|
||||
if (!AnimationSystem.HasRunningAnimation(uid, AnimationKey))
|
||||
if (!_animationSystem.HasRunningAnimation(uid, AnimationKey))
|
||||
{
|
||||
AnimationSystem.Play(uid, unit.FlushAnimation, AnimationKey);
|
||||
_animationSystem.Play(uid, unit.FlushAnimation, AnimationKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (!AppearanceSystem.TryGetData<HandleState>(uid, Visuals.Handle, out var handleState))
|
||||
if (!_appearanceSystem.TryGetData<HandleState>(uid, Visuals.Handle, out var handleState))
|
||||
{
|
||||
handleState = HandleState.Normal;
|
||||
}
|
||||
|
||||
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayEngaged, handleState != HandleState.Normal);
|
||||
|
||||
if (!AppearanceSystem.TryGetData<LightStates>(uid, Visuals.Light, out var lightState))
|
||||
if (!_appearanceSystem.TryGetData<LightStates>(uid, Visuals.Light, out var lightState))
|
||||
{
|
||||
lightState = LightStates.Off;
|
||||
}
|
||||
|
||||
@@ -32,12 +32,16 @@ using Content.Shared.Localizations;
|
||||
using Robust.Client;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Replays.Loading;
|
||||
using Robust.Client.Replays.Playback;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Replays;
|
||||
|
||||
namespace Content.Client.Entry
|
||||
{
|
||||
@@ -73,6 +77,9 @@ namespace Content.Client.Entry
|
||||
[Dependency] private readonly TTSManager _ttsManager = default!; // Corvax-TTS
|
||||
[Dependency] private readonly DiscordAuthManager _discordAuthManager = default!; // Corvax-DiscordAuth
|
||||
[Dependency] private readonly ContentReplayPlaybackManager _playbackMan = default!;
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly IReplayLoadManager _replayLoad = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
@@ -198,7 +205,20 @@ namespace Content.Client.Entry
|
||||
{
|
||||
// Fire off into state dependent on launcher or not.
|
||||
|
||||
if (_gameController.LaunchState.FromLauncher)
|
||||
// Check if we're loading a replay via content bundle!
|
||||
if (_configManager.GetCVar(CVars.LaunchContentBundle)
|
||||
&& _resourceManager.ContentFileExists(
|
||||
ReplayConstants.ReplayZipFolder.ToRootedPath() / ReplayConstants.FileMeta))
|
||||
{
|
||||
_logManager.GetSawmill("entry").Info("Loading content bundle replay from VFS!");
|
||||
|
||||
var reader = new ReplayFileReaderResources(
|
||||
_resourceManager,
|
||||
ReplayConstants.ReplayZipFolder.ToRootedPath());
|
||||
|
||||
_replayLoad.LoadAndStartReplay(reader);
|
||||
}
|
||||
else if (_gameController.LaunchState.FromLauncher)
|
||||
{
|
||||
_stateManager.RequestStateChange<LauncherConnecting>();
|
||||
var state = (LauncherConnecting) _stateManager.CurrentState;
|
||||
|
||||
@@ -49,7 +49,6 @@ namespace Content.Client.GameTicking.Managers
|
||||
|
||||
public event Action? InfoBlobUpdated;
|
||||
public event Action? LobbyStatusUpdated;
|
||||
public event Action? LobbyReadyUpdated;
|
||||
public event Action? LobbyLateJoinStatusUpdated;
|
||||
public event Action<IReadOnlyDictionary<EntityUid, Dictionary<string, uint?>>>? LobbyJobsAvailableUpdated;
|
||||
|
||||
@@ -62,7 +61,6 @@ namespace Content.Client.GameTicking.Managers
|
||||
SubscribeNetworkEvent<TickerLobbyStatusEvent>(LobbyStatus);
|
||||
SubscribeNetworkEvent<TickerLobbyInfoEvent>(LobbyInfo);
|
||||
SubscribeNetworkEvent<TickerLobbyCountdownEvent>(LobbyCountdown);
|
||||
SubscribeNetworkEvent<TickerLobbyReadyEvent>(LobbyReady);
|
||||
SubscribeNetworkEvent<RoundEndMessageEvent>(RoundEnd);
|
||||
SubscribeNetworkEvent<RequestWindowAttentionEvent>(msg =>
|
||||
{
|
||||
@@ -124,11 +122,6 @@ namespace Content.Client.GameTicking.Managers
|
||||
Paused = message.Paused;
|
||||
}
|
||||
|
||||
private void LobbyReady(TickerLobbyReadyEvent message)
|
||||
{
|
||||
LobbyReadyUpdated?.Invoke();
|
||||
}
|
||||
|
||||
private void RoundEnd(RoundEndMessageEvent message)
|
||||
{
|
||||
if (message.LobbySong != null)
|
||||
|
||||
@@ -26,14 +26,12 @@ namespace Content.Client.Labels.UI
|
||||
_window.OpenCentered();
|
||||
|
||||
_window.OnClose += Close;
|
||||
_window.OnLabelEntered += OnLabelChanged;
|
||||
|
||||
_window.OnLabelChanged += OnLabelChanged;
|
||||
}
|
||||
|
||||
private void OnLabelChanged(string newLabel)
|
||||
{
|
||||
SendMessage(new HandLabelerLabelChangedMessage(newLabel));
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Labels.UI
|
||||
@@ -9,13 +7,14 @@ namespace Content.Client.Labels.UI
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HandLabelerWindow : DefaultWindow
|
||||
{
|
||||
public event Action<string>? OnLabelEntered;
|
||||
public event Action<string>? OnLabelChanged;
|
||||
|
||||
public HandLabelerWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
LabelLineEdit.OnTextEntered += e => OnLabelEntered?.Invoke(e.Text);
|
||||
LabelLineEdit.OnTextEntered += e => OnLabelChanged?.Invoke(e.Text);
|
||||
LabelLineEdit.OnFocusExit += e => OnLabelChanged?.Invoke(e.Text);
|
||||
}
|
||||
|
||||
public void SetCurrentLabel(string label)
|
||||
|
||||
@@ -29,16 +29,15 @@ namespace Content.Client.Lathe.UI
|
||||
|
||||
_menu.OnQueueButtonPressed += _ =>
|
||||
{
|
||||
_queueMenu.OpenCenteredLeft();
|
||||
if (_queueMenu.IsOpen)
|
||||
_queueMenu.Close();
|
||||
else
|
||||
_queueMenu.OpenCenteredLeft();
|
||||
};
|
||||
_menu.OnServerListButtonPressed += _ =>
|
||||
{
|
||||
SendMessage(new ConsoleServerSelectionMessage());
|
||||
};
|
||||
_menu.OnServerSyncButtonPressed += _ =>
|
||||
{
|
||||
SendMessage(new ConsoleServerSyncMessage());
|
||||
};
|
||||
_menu.RecipeQueueAction += (recipe, amount) =>
|
||||
{
|
||||
SendMessage(new LatheQueueRecipeMessage(recipe, amount));
|
||||
@@ -56,7 +55,7 @@ namespace Content.Client.Lathe.UI
|
||||
case LatheUpdateState msg:
|
||||
if (_menu != null)
|
||||
_menu.Recipes = msg.Recipes;
|
||||
_menu?.PopulateRecipes(Owner.Owner);
|
||||
_menu?.PopulateRecipes(Lathe);
|
||||
_menu?.PopulateMaterials(Lathe);
|
||||
_queueMenu?.PopulateList(msg.Queue);
|
||||
_queueMenu?.SetInfo(msg.CurrentlyProducing);
|
||||
|
||||
@@ -1,85 +1,84 @@
|
||||
<DefaultWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Title="{Loc 'lathe-menu-title'}"
|
||||
MinSize="300 450"
|
||||
SetSize="300 450">
|
||||
<BoxContainer
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
SeparationOverride="5">
|
||||
<BoxContainer
|
||||
Orientation="Horizontal"
|
||||
Align="End"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="1">
|
||||
HorizontalExpand="True">
|
||||
<Button
|
||||
Name="QueueButton"
|
||||
Text="{Loc 'lathe-menu-queue'}"
|
||||
TextAlign="Center"
|
||||
Mode="Press"
|
||||
SizeFlagsStretchRatio="1">
|
||||
StyleClasses="OpenRight">
|
||||
</Button>
|
||||
<Button
|
||||
Name="ServerListButton"
|
||||
Text="{Loc 'lathe-menu-server-list'}"
|
||||
TextAlign="Center"
|
||||
Mode="Press"
|
||||
SizeFlagsStretchRatio="1">
|
||||
</Button>
|
||||
<Button
|
||||
Name="ServerSyncButton"
|
||||
Text="{Loc 'lathe-menu-sync'}"
|
||||
TextAlign="Center"
|
||||
Mode="Press"
|
||||
SizeFlagsStretchRatio="1">
|
||||
StyleClasses="OpenLeft">
|
||||
</Button>
|
||||
</BoxContainer>
|
||||
<BoxContainer
|
||||
Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="1">
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True">
|
||||
<LineEdit
|
||||
Name="SearchBar"
|
||||
PlaceHolder="{Loc 'lathe-menu-search-designs'}"
|
||||
HorizontalExpand="True"
|
||||
SizeFlagsStretchRatio="1">
|
||||
HorizontalExpand="True">
|
||||
</LineEdit>
|
||||
<Button
|
||||
Name="FilterButton"
|
||||
Text="{Loc 'lathe-menu-search-filter'}"
|
||||
TextAlign="Center"
|
||||
SizeFlagsStretchRatio="1"
|
||||
Disabled="True">
|
||||
Margin="5 0 0 0"
|
||||
Disabled="True"
|
||||
StyleClasses="ButtonSquare">
|
||||
</Button>
|
||||
</BoxContainer>
|
||||
<ScrollContainer MinHeight="225">
|
||||
<BoxContainer
|
||||
Name="RecipeList"
|
||||
Orientation="Vertical"
|
||||
SizeFlagsStretchRatio="8"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
<BoxContainer
|
||||
Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="1">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
MinHeight="225"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
SizeFlagsStretchRatio="4">
|
||||
<PanelContainer VerticalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
|
||||
<BoxContainer
|
||||
Name="RecipeList"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
RectClipContent="True">
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True">
|
||||
<Label Margin="8 0 8 0" Text="{Loc 'lathe-menu-amount'}"/>
|
||||
<LineEdit
|
||||
Name="AmountLineEdit"
|
||||
PlaceHolder="0"
|
||||
Text="1"
|
||||
HorizontalExpand="True" />
|
||||
HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
|
||||
<ItemList
|
||||
Name="Materials"
|
||||
VerticalExpand="True">
|
||||
</ItemList>
|
||||
</BoxContainer>
|
||||
<ItemList
|
||||
Name="Materials"
|
||||
VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="3">
|
||||
</ItemList>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Lathe;
|
||||
using Content.Shared.Materials;
|
||||
using Content.Shared.Research.Prototypes;
|
||||
@@ -22,7 +23,6 @@ public sealed partial class LatheMenu : DefaultWindow
|
||||
|
||||
public event Action<BaseButton.ButtonEventArgs>? OnQueueButtonPressed;
|
||||
public event Action<BaseButton.ButtonEventArgs>? OnServerListButtonPressed;
|
||||
public event Action<BaseButton.ButtonEventArgs>? OnServerSyncButtonPressed;
|
||||
public event Action<string, int>? RecipeQueueAction;
|
||||
|
||||
public List<string> Recipes = new();
|
||||
@@ -49,15 +49,13 @@ public sealed partial class LatheMenu : DefaultWindow
|
||||
QueueButton.OnPressed += a => OnQueueButtonPressed?.Invoke(a);
|
||||
ServerListButton.OnPressed += a => OnServerListButtonPressed?.Invoke(a);
|
||||
|
||||
//refresh the bui state
|
||||
ServerSyncButton.OnPressed += a => OnServerSyncButtonPressed?.Invoke(a);
|
||||
|
||||
if (_entityManager.TryGetComponent<LatheComponent>(owner.Lathe, out var latheComponent))
|
||||
{
|
||||
if (!latheComponent.DynamicRecipes.Any())
|
||||
{
|
||||
ServerListButton.Visible = false;
|
||||
ServerSyncButton.Visible = false;
|
||||
QueueButton.RemoveStyleClass(StyleBase.ButtonOpenRight);
|
||||
//QueueButton.AddStyleClass(StyleBase.ButtonSquare);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
<Button
|
||||
Name="Button"
|
||||
HorizontalExpand="True"
|
||||
TooltipDelay="0.5">
|
||||
TooltipDelay="0.5"
|
||||
Margin="0"
|
||||
StyleClasses="ButtonSquare">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<TextureRect
|
||||
Name="RecipeTexture"
|
||||
Margin="0,0,4,0"
|
||||
Margin="0 0 4 0"
|
||||
MinSize="32 32"
|
||||
Stretch="KeepAspectCentered" />
|
||||
<Label Name="RecipeName" HorizontalExpand="True" />
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Launcher;
|
||||
using Content.Client.MainMenu;
|
||||
using Content.Client.Replay.Spectator;
|
||||
using Content.Client.Replay.UI.Loading;
|
||||
using Content.Client.UserInterface.Systems.Chat;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.GameWindow;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Instruments;
|
||||
using Content.Shared.Popups;
|
||||
@@ -16,6 +18,7 @@ using Content.Shared.Weapons.Ranged.Systems;
|
||||
using Robust.Client;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.Replays.Loading;
|
||||
using Robust.Client.Replays.Playback;
|
||||
using Robust.Client.State;
|
||||
@@ -38,6 +41,7 @@ public sealed class ContentReplayPlaybackManager
|
||||
[Dependency] private readonly IReplayPlaybackManager _playback = default!;
|
||||
[Dependency] private readonly IClientConGroupController _conGrp = default!;
|
||||
[Dependency] private readonly IClientAdminManager _adminMan = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
|
||||
/// <summary>
|
||||
/// UI state to return to when stopping a replay or loading fails.
|
||||
@@ -59,10 +63,10 @@ public sealed class ContentReplayPlaybackManager
|
||||
_loadMan.LoadOverride += LoadOverride;
|
||||
}
|
||||
|
||||
private void LoadOverride(IWritableDirProvider dir, ResPath resPath)
|
||||
private void LoadOverride(IReplayFileReader fileReader)
|
||||
{
|
||||
var screen = _stateMan.RequestStateChange<LoadingScreen<bool>>();
|
||||
screen.Job = new ContentLoadReplayJob(1/60f, dir, resPath, _loadMan, screen);
|
||||
screen.Job = new ContentLoadReplayJob(1/60f, fileReader, _loadMan, screen);
|
||||
screen.OnJobFinished += (_, e) => OnFinishedLoading(e);
|
||||
}
|
||||
|
||||
@@ -98,36 +102,53 @@ public sealed class ContentReplayPlaybackManager
|
||||
|
||||
private bool OnHandleReplayMessage(object message, bool skipEffects)
|
||||
{
|
||||
// TODO REPLAYS figure out a cleaner way of doing this. This sucks.
|
||||
// Maybe wrap the event in another cancellable event and raise that?
|
||||
|
||||
// This is where replays filter through networked messages and can choose to ignore or give them special treatment.
|
||||
// In particular, we want to avoid spamming pop-ups, sounds, and visual effect entities while fast forwarding.
|
||||
// E.g., when rewinding 1 tick, we really rewind back to the last checkpoint and then fast forward. Currently, this is
|
||||
// effectively an EntityEvent blacklist.
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case BoundUserInterfaceMessage:
|
||||
break; // TODO REPLAYS refactor BUIs
|
||||
case ChatMessage chat:
|
||||
// Just pass on the chat message to the UI controller, but skip speech-bubbles if we are fast-forwarding.
|
||||
_uiMan.GetUIController<ChatUIController>().ProcessChatMessage(chat, speechBubble: !skipEffects);
|
||||
return true;
|
||||
// TODO REPLAYS figure out a cleaner way of doing this. This sucks.
|
||||
// Next: we want to avoid spamming animations, sounds, and pop-ups while scrubbing or rewinding time
|
||||
// (e.g., to rewind 1 tick, we really rewind ~60 and then fast forward 59). Currently, this is
|
||||
// effectively an EntityEvent blacklist. But this is kinda shit and should be done differently somehow.
|
||||
// The unifying aspect of these events is that they trigger pop-ups, UI changes, spawn client-side
|
||||
// entities or start animations.
|
||||
case RoundEndMessageEvent:
|
||||
case PopupEvent:
|
||||
case AudioMessage:
|
||||
case PickupAnimationEvent:
|
||||
case MeleeLungeEvent:
|
||||
case SharedGunSystem.HitscanEvent:
|
||||
case ImpactEffectEvent:
|
||||
case MuzzleFlashEvent:
|
||||
case DamageEffectEvent:
|
||||
case InstrumentStartMidiEvent:
|
||||
case InstrumentMidiEventEvent:
|
||||
case InstrumentStopMidiEvent:
|
||||
if (!skipEffects)
|
||||
_entMan.DispatchReceivedNetworkMsg((EntityEventArgs)message);
|
||||
return true;
|
||||
}
|
||||
{
|
||||
case BoundUserInterfaceMessage: // TODO REPLAYS refactor BUIs
|
||||
case RequestWindowAttentionEvent:
|
||||
// Mark as handled -- the event won't get raised.
|
||||
return true;
|
||||
case TickerJoinGameEvent:
|
||||
if (!_entMan.EntityExists(_player.LocalPlayer?.ControlledEntity))
|
||||
_entMan.System<ReplaySpectatorSystem>().SetSpectatorPosition(default);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!skipEffects)
|
||||
{
|
||||
// Don't mark as handled -- the event get raised as a normal networked event.
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case ChatMessage chat:
|
||||
// Pass the chat message to the UI controller, skip the speech-bubble / pop-up.
|
||||
_uiMan.GetUIController<ChatUIController>().ProcessChatMessage(chat, speechBubble: false);
|
||||
return true;
|
||||
case RoundEndMessageEvent:
|
||||
case PopupEvent:
|
||||
case AudioMessage:
|
||||
case PickupAnimationEvent:
|
||||
case MeleeLungeEvent:
|
||||
case SharedGunSystem.HitscanEvent:
|
||||
case ImpactEffectEvent:
|
||||
case MuzzleFlashEvent:
|
||||
case DamageEffectEvent:
|
||||
case InstrumentStartMidiEvent:
|
||||
case InstrumentMidiEventEvent:
|
||||
case InstrumentStopMidiEvent:
|
||||
// Block visual effects, pop-ups, and sounds
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,11 +12,10 @@ public sealed class ContentLoadReplayJob : LoadReplayJob
|
||||
|
||||
public ContentLoadReplayJob(
|
||||
float maxTime,
|
||||
IWritableDirProvider dir,
|
||||
ResPath path,
|
||||
IReplayFileReader fileReader,
|
||||
IReplayLoadManager loadMan,
|
||||
LoadingScreen<bool> screen)
|
||||
: base(maxTime, dir, path, loadMan)
|
||||
: base(maxTime, fileReader, loadMan)
|
||||
{
|
||||
_screen = screen;
|
||||
}
|
||||
|
||||
@@ -85,8 +85,13 @@ public sealed partial class ReplaySpectatorSystem
|
||||
}
|
||||
|
||||
// A poor mans grid-traversal system. Should also interrupt ghost-following.
|
||||
// This is very hacky and has already caused bugs.
|
||||
// This is done the way it is because grid traversal gets processed in physics' SimulateWorld() update.
|
||||
// TODO do this properly somehow.
|
||||
_transform.SetGridId(player, xform, null);
|
||||
_transform.AttachToGridOrMap(player);
|
||||
if (xform.ParentUid.IsValid())
|
||||
_transform.SetGridId(player, xform, Transform(xform.ParentUid).GridUid);
|
||||
|
||||
var parentRotation = _mover.GetParentGridAngle(mover, query);
|
||||
var localVec = effectiveDir.AsDir().ToAngle().ToWorldVec();
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Content.Shared.Research.Components;
|
||||
using Content.Shared.Research.Prototypes;
|
||||
using Content.Shared.Research.Systems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
@@ -15,7 +13,7 @@ public sealed class ResearchConsoleBoundUserInterface : BoundUserInterface
|
||||
|
||||
public ResearchConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
SendMessage(new ConsoleServerSyncMessage());
|
||||
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
@@ -36,11 +34,6 @@ public sealed class ResearchConsoleBoundUserInterface : BoundUserInterface
|
||||
SendMessage(new ConsoleServerSelectionMessage());
|
||||
};
|
||||
|
||||
_consoleMenu.OnSyncButtonPressed += () =>
|
||||
{
|
||||
SendMessage(new ConsoleServerSyncMessage());
|
||||
};
|
||||
|
||||
_consoleMenu.OnClose += Close;
|
||||
|
||||
_consoleMenu.OpenCentered();
|
||||
|
||||
@@ -21,10 +21,7 @@
|
||||
<!-- This is where we put all of the little graphics that display discipline tiers!-->
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalAlignment="Right">
|
||||
<Button Name="ServerButton" Text="{Loc 'research-console-menu-server-selection-button'}" VerticalExpand="True"/>
|
||||
<Control MinHeight="5"/>
|
||||
<!--todo is this button even necessary?!-->
|
||||
<Button Name="SyncButton" Text="{Loc 'research-console-menu-server-sync-button'}" VerticalExpand="True"/>
|
||||
<Button Name="ServerButton" Text="{Loc 'research-console-menu-server-selection-button'}" MinHeight="40"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
|
||||
@@ -16,7 +16,6 @@ public sealed partial class ResearchConsoleMenu : FancyWindow
|
||||
{
|
||||
public Action<string>? OnTechnologyCardPressed;
|
||||
public Action? OnServerButtonPressed;
|
||||
public Action? OnSyncButtonPressed;
|
||||
|
||||
[Dependency] private readonly IEntityManager _entity = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
@@ -36,7 +35,6 @@ public sealed partial class ResearchConsoleMenu : FancyWindow
|
||||
Entity = entity;
|
||||
|
||||
ServerButton.OnPressed += _ => OnServerButtonPressed?.Invoke();
|
||||
SyncButton.OnPressed += _ => OnSyncButtonPressed?.Invoke();
|
||||
|
||||
_entity.TryGetComponent(entity, out _technologyDatabase);
|
||||
}
|
||||
|
||||
@@ -38,9 +38,7 @@ namespace Content.Client.Stack
|
||||
return;
|
||||
}
|
||||
|
||||
// Dirty the UI now that the stack count has changed.
|
||||
if (component is StackComponent clientComp)
|
||||
clientComp.UiUpdateNeeded = true;
|
||||
component.UiUpdateNeeded = true;
|
||||
}
|
||||
|
||||
private void OnAppearanceChange(EntityUid uid, StackComponent comp, ref AppearanceChangeEvent args)
|
||||
|
||||
@@ -51,6 +51,46 @@ public sealed class CargoTest
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
[Test]
|
||||
public async Task NoCargoBountyArbitageTest()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings() {NoClient = true});
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
||||
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var cargo = entManager.System<CargoSystem>();
|
||||
|
||||
var bounties = protoManager.EnumeratePrototypes<CargoBountyPrototype>().ToList();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var mapId = testMap.MapId;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<CargoProductPrototype>())
|
||||
{
|
||||
var ent = entManager.SpawnEntity(proto.Product, new MapCoordinates(Vector2.Zero, mapId));
|
||||
|
||||
foreach (var bounty in bounties)
|
||||
{
|
||||
if (cargo.IsBountyComplete(ent, bounty))
|
||||
Assert.That(proto.PointCost, Is.GreaterThan(bounty.Reward), $"Found arbitrage on {bounty.ID} cargo bounty! Product {proto.ID} costs {proto.PointCost} but fulfills bounty {bounty.ID} with reward {bounty.Reward}!");
|
||||
}
|
||||
|
||||
entManager.DeleteEntity(ent);
|
||||
}
|
||||
});
|
||||
|
||||
mapManager.DeleteMap(mapId);
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NoStaticPriceAndStackPrice()
|
||||
|
||||
@@ -8,7 +8,6 @@ using Content.Server.Power.Components;
|
||||
using Content.Shared.Disposal;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Reflection;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Disposal
|
||||
@@ -33,22 +32,20 @@ namespace Content.IntegrationTests.Tests.Disposal
|
||||
var unitTransform = EntityManager.GetComponent<TransformComponent>(unit);
|
||||
// Not in a tube yet
|
||||
Assert.That(insertTransform.ParentUid, Is.EqualTo(unit));
|
||||
}, after: new[] {typeof(SharedDisposalUnitSystem)});
|
||||
}, after: new[] { typeof(SharedDisposalUnitSystem) });
|
||||
}
|
||||
}
|
||||
|
||||
private void UnitInsert(DisposalUnitComponent unit, bool result, params EntityUid[] entities)
|
||||
private static void UnitInsert(EntityUid uid, DisposalUnitComponent unit, bool result, DisposalUnitSystem disposalSystem, params EntityUid[] entities)
|
||||
{
|
||||
var system = EntitySystem.Get<DisposalUnitSystem>();
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
Assert.That(system.CanInsert(unit, entity), Is.EqualTo(result));
|
||||
system.TryInsert(unit.Owner, entity, null);
|
||||
Assert.That(disposalSystem.CanInsert(uid, unit, entity), Is.EqualTo(result));
|
||||
disposalSystem.TryInsert(uid, entity, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void UnitContains(DisposalUnitComponent unit, bool result, params EntityUid[] entities)
|
||||
private static void UnitContains(DisposalUnitComponent unit, bool result, params EntityUid[] entities)
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
@@ -56,19 +53,22 @@ namespace Content.IntegrationTests.Tests.Disposal
|
||||
}
|
||||
}
|
||||
|
||||
private void UnitInsertContains(DisposalUnitComponent unit, bool result, params EntityUid[] entities)
|
||||
private static void UnitInsertContains(EntityUid uid, DisposalUnitComponent unit, bool result, DisposalUnitSystem disposalSystem, params EntityUid[] entities)
|
||||
{
|
||||
UnitInsert(unit, result, entities);
|
||||
UnitInsert(uid, unit, result, disposalSystem, entities);
|
||||
UnitContains(unit, result, entities);
|
||||
}
|
||||
|
||||
private void Flush(EntityUid unitEntity, DisposalUnitComponent unit, bool result, params EntityUid[] entities)
|
||||
private static void Flush(EntityUid unitEntity, DisposalUnitComponent unit, bool result, DisposalUnitSystem disposalSystem, params EntityUid[] entities)
|
||||
{
|
||||
Assert.That(unit.Container.ContainedEntities, Is.SupersetOf(entities));
|
||||
Assert.That(entities.Length, Is.EqualTo(unit.Container.ContainedEntities.Count));
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(unit.Container.ContainedEntities, Is.SupersetOf(entities));
|
||||
Assert.That(entities, Has.Length.EqualTo(unit.Container.ContainedEntities.Count));
|
||||
|
||||
Assert.That(result, Is.EqualTo(EntitySystem.Get<DisposalUnitSystem>().TryFlush(unitEntity, unit)));
|
||||
Assert.That(result || entities.Length == 0, Is.EqualTo(unit.Container.ContainedEntities.Count == 0));
|
||||
Assert.That(result, Is.EqualTo(disposalSystem.TryFlush(unitEntity, unit)));
|
||||
Assert.That(result || entities.Length == 0, Is.EqualTo(unit.Container.ContainedEntities.Count == 0));
|
||||
});
|
||||
}
|
||||
|
||||
private const string Prototypes = @"
|
||||
@@ -147,7 +147,10 @@ namespace Content.IntegrationTests.Tests.Disposal
|
||||
public async Task Test()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings
|
||||
{NoClient = true, ExtraPrototypes = Prototypes});
|
||||
{
|
||||
NoClient = true,
|
||||
ExtraPrototypes = Prototypes
|
||||
});
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
||||
@@ -161,6 +164,8 @@ namespace Content.IntegrationTests.Tests.Disposal
|
||||
DisposalUnitComponent unitComponent = default!;
|
||||
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var xformSystem = entityManager.System<SharedTransformSystem>();
|
||||
var disposalSystem = entityManager.System<DisposalUnitSystem>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
@@ -174,62 +179,69 @@ namespace Content.IntegrationTests.Tests.Disposal
|
||||
|
||||
// Test for components existing
|
||||
unitUid = disposalUnit;
|
||||
Assert.True(entityManager.TryGetComponent(disposalUnit, out unitComponent));
|
||||
Assert.True(entityManager.HasComponent<DisposalEntryComponent>(disposalTrunk));
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(entityManager.TryGetComponent(disposalUnit, out unitComponent));
|
||||
Assert.That(entityManager.HasComponent<DisposalEntryComponent>(disposalTrunk));
|
||||
});
|
||||
|
||||
// Can't insert, unanchored and unpowered
|
||||
entityManager.GetComponent<TransformComponent>(unitUid).Anchored = false;
|
||||
UnitInsertContains(unitComponent, false, human, wrench, disposalUnit, disposalTrunk);
|
||||
xformSystem.Unanchor(unitUid, entityManager.GetComponent<TransformComponent>(unitUid));
|
||||
UnitInsertContains(disposalUnit, unitComponent, false, disposalSystem, human, wrench, disposalUnit, disposalTrunk);
|
||||
});
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
// Anchor the disposal unit
|
||||
entityManager.GetComponent<TransformComponent>(unitUid).Anchored = true;
|
||||
xformSystem.AnchorEntity(unitUid, entityManager.GetComponent<TransformComponent>(unitUid));
|
||||
|
||||
// No power
|
||||
Assert.False(unitComponent.Powered);
|
||||
Assert.That(unitComponent.Powered, Is.False);
|
||||
|
||||
// Can't insert the trunk or the unit into itself
|
||||
UnitInsertContains(unitComponent, false, disposalUnit, disposalTrunk);
|
||||
UnitInsertContains(unitUid, unitComponent, false, disposalSystem, disposalUnit, disposalTrunk);
|
||||
|
||||
// Can insert mobs and items
|
||||
UnitInsertContains(unitComponent, true, human, wrench);
|
||||
UnitInsertContains(unitUid, unitComponent, true, disposalSystem, human, wrench);
|
||||
});
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
// Move the disposal trunk away
|
||||
entityManager.GetComponent<TransformComponent>(disposalTrunk).WorldPosition += (1, 0);
|
||||
var xform = entityManager.GetComponent<TransformComponent>(disposalTrunk);
|
||||
var worldPos = xformSystem.GetWorldPosition(disposalTrunk);
|
||||
xformSystem.SetWorldPosition(xform, worldPos + (1, 0));
|
||||
|
||||
// Fail to flush with a mob and an item
|
||||
Flush(disposalUnit, unitComponent, false, human, wrench);
|
||||
Flush(disposalUnit, unitComponent, false, disposalSystem, human, wrench);
|
||||
});
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
// Move the disposal trunk back
|
||||
entityManager.GetComponent<TransformComponent>(disposalTrunk).WorldPosition -= (1, 0);
|
||||
var xform = entityManager.GetComponent<TransformComponent>(disposalTrunk);
|
||||
var worldPos = xformSystem.GetWorldPosition(disposalTrunk);
|
||||
xformSystem.SetWorldPosition(xform, worldPos - (1, 0));
|
||||
|
||||
// Fail to flush with a mob and an item, no power
|
||||
Flush(disposalUnit, unitComponent, false, human, wrench);
|
||||
Flush(disposalUnit, unitComponent, false, disposalSystem, human, wrench);
|
||||
});
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
// Remove power need
|
||||
Assert.True(entityManager.TryGetComponent(disposalUnit, out ApcPowerReceiverComponent power));
|
||||
Assert.That(entityManager.TryGetComponent(disposalUnit, out ApcPowerReceiverComponent power));
|
||||
power!.NeedsPower = false;
|
||||
unitComponent.Powered = true; //Power state changed event doesn't get fired smh
|
||||
|
||||
// Flush with a mob and an item
|
||||
Flush(disposalUnit, unitComponent, true, human, wrench);
|
||||
Flush(disposalUnit, unitComponent, true, disposalSystem, human, wrench);
|
||||
});
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
// Re-pressurizing
|
||||
Flush(disposalUnit, unitComponent, false);
|
||||
Flush(disposalUnit, unitComponent, false, disposalSystem);
|
||||
});
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||
using Content.Client.Construction;
|
||||
using Content.Client.Examine;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Mind.Components;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Stack;
|
||||
@@ -184,7 +185,7 @@ public abstract partial class InteractionTest
|
||||
{
|
||||
// Fuck you mind system I want an hour of my life back
|
||||
// Mind system is a time vampire
|
||||
ServerSession.ContentData()?.WipeMind();
|
||||
SEntMan.System<MindSystem>().WipeMind(ServerSession.ContentData()?.Mind);
|
||||
|
||||
old = cPlayerMan.LocalPlayer.ControlledEntity;
|
||||
Player = SEntMan.SpawnEntity(PlayerPrototype, PlayerCoords);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Ghost.Roles;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
@@ -16,12 +17,7 @@ public sealed class GhostRoleTests
|
||||
{
|
||||
private const string Prototypes = @"
|
||||
- type: entity
|
||||
id: GhostRoleTestEntity_Player
|
||||
components:
|
||||
- type: MindContainer
|
||||
|
||||
- type: entity
|
||||
id: GhostRoleTestEntity_Role
|
||||
id: GhostRoleTestEntity
|
||||
components:
|
||||
- type: MindContainer
|
||||
- type: GhostRole
|
||||
@@ -42,36 +38,29 @@ public sealed class GhostRoleTests
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var sPlayerMan = server.ResolveDependency<Robust.Server.Player.IPlayerManager>();
|
||||
var conHost = client.ResolveDependency<IConsoleHost>();
|
||||
var cPlayerMan = client.ResolveDependency<Robust.Client.Player.IPlayerManager>();
|
||||
var mindSystem = entMan.System<MindSystem>();
|
||||
|
||||
// Get player data
|
||||
if (cPlayerMan.LocalPlayer?.Session == null)
|
||||
Assert.Fail("No player");
|
||||
|
||||
var clientSession = cPlayerMan.LocalPlayer!.Session!;
|
||||
var session = sPlayerMan.GetSessionByUserId(clientSession.UserId);
|
||||
var session = sPlayerMan.ServerSessions.Single();
|
||||
|
||||
// Spawn player entity & attach
|
||||
EntityUid originalMob = default;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
originalMob = entMan.SpawnEntity("GhostRoleTestEntity_Player", MapCoordinates.Nullspace);
|
||||
originalMob = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
mindSystem.TransferTo(session.ContentData()!.Mind!, originalMob, true);
|
||||
});
|
||||
|
||||
// Check player got attached.
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 10);
|
||||
Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.EqualTo(originalMob));
|
||||
Assert.That(session.AttachedEntity, Is.EqualTo(originalMob));
|
||||
|
||||
// Use the ghost command
|
||||
conHost.ExecuteCommand("ghost");
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 10);
|
||||
Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.Not.EqualTo(originalMob));
|
||||
Assert.That(session.AttachedEntity, Is.Not.EqualTo(originalMob));
|
||||
|
||||
// Spawn ghost takeover entity.
|
||||
EntityUid ghostRole = default;
|
||||
await server.WaitPost(() => ghostRole = entMan.SpawnEntity("GhostRoleTestEntity_Role", MapCoordinates.Nullspace));
|
||||
await server.WaitPost(() => ghostRole = entMan.SpawnEntity("GhostRoleTestEntity", MapCoordinates.Nullspace));
|
||||
|
||||
// Take the ghost role
|
||||
await server.WaitPost(() =>
|
||||
@@ -82,13 +71,13 @@ public sealed class GhostRoleTests
|
||||
|
||||
// Check player got attached to ghost role.
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 10);
|
||||
Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.EqualTo(ghostRole));
|
||||
Assert.That(session.AttachedEntity, Is.EqualTo(ghostRole));
|
||||
|
||||
// Ghost again.
|
||||
conHost.ExecuteCommand("ghost");
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 10);
|
||||
Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.Not.EqualTo(originalMob));
|
||||
Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.Not.EqualTo(ghostRole));
|
||||
Assert.That(session.AttachedEntity, Is.Not.EqualTo(originalMob));
|
||||
Assert.That(session.AttachedEntity, Is.Not.EqualTo(ghostRole));
|
||||
|
||||
// Next, control the original entity again:
|
||||
await server.WaitPost(() =>
|
||||
@@ -96,7 +85,7 @@ public sealed class GhostRoleTests
|
||||
mindSystem.TransferTo(session.ContentData()!.Mind!, originalMob, true);
|
||||
});
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 10);
|
||||
Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.EqualTo(originalMob));
|
||||
Assert.That(session.AttachedEntity, Is.EqualTo(originalMob));
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Mind;
|
||||
using NUnit.Framework;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Minds
|
||||
{
|
||||
// Tests various scenarios of deleting the entity that a player's mind is connected to.
|
||||
[TestFixture]
|
||||
public sealed class MindEntityDeletionTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestDeleteVisiting()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient();
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IServerEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
|
||||
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
|
||||
|
||||
EntityUid playerEnt = default;
|
||||
EntityUid visitEnt = default;
|
||||
Mind mind = default!;
|
||||
var map = await PoolManager.CreateTestMap(pairTracker);
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = playerMan.ServerSessions.Single();
|
||||
var pos = new MapCoordinates(Vector2.Zero, map.MapId);
|
||||
|
||||
playerEnt = entMan.SpawnEntity(null, pos);
|
||||
visitEnt = entMan.SpawnEntity(null, pos);
|
||||
|
||||
mind = mindSystem.CreateMind(player.UserId);
|
||||
mindSystem.TransferTo(mind, playerEnt);
|
||||
mindSystem.Visit(mind, visitEnt);
|
||||
|
||||
Assert.That(player.AttachedEntity, Is.EqualTo(visitEnt));
|
||||
Assert.That(mind.VisitingEntity, Is.EqualTo(visitEnt));
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
entMan.DeleteEntity(visitEnt);
|
||||
|
||||
if (mind.VisitingEntity != null)
|
||||
{
|
||||
Assert.Fail("Mind VisitingEntity was not null");
|
||||
return;
|
||||
}
|
||||
|
||||
// This used to throw so make sure it doesn't.
|
||||
entMan.DeleteEntity(playerEnt);
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
mapManager.DeleteMap(map.MapId);
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestGhostOnDelete()
|
||||
{
|
||||
// Has to be a non-dummy ticker so we have a proper map.
|
||||
|
||||
await using var pairTracker = await PoolManager.GetServerClient();
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IServerEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
|
||||
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
|
||||
|
||||
var map = await PoolManager.CreateTestMap(pairTracker);
|
||||
|
||||
EntityUid playerEnt = default;
|
||||
Mind mind = default!;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = playerMan.ServerSessions.Single();
|
||||
|
||||
var pos = new MapCoordinates(Vector2.Zero, map.MapId);
|
||||
|
||||
playerEnt = entMan.SpawnEntity(null, pos);
|
||||
|
||||
mind = mindSystem.CreateMind(player.UserId);
|
||||
mindSystem.TransferTo(mind, playerEnt);
|
||||
|
||||
Assert.That(mind.CurrentEntity, Is.EqualTo(playerEnt));
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
entMan.DeleteEntity(playerEnt);
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(entMan.EntityExists(mind.CurrentEntity!.Value), Is.True);
|
||||
});
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
mapManager.DeleteMap(map.MapId);
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestGhostOnDeleteMap()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true });
|
||||
var server = pairTracker.Pair.Server;
|
||||
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
||||
var coordinates = testMap.GridCoords;
|
||||
|
||||
var entMan = server.ResolveDependency<IServerEntityManager>();
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
|
||||
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
|
||||
|
||||
var map = await PoolManager.CreateTestMap(pairTracker);
|
||||
|
||||
EntityUid playerEnt = default;
|
||||
Mind mind = default!;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
playerEnt = entMan.SpawnEntity(null, coordinates);
|
||||
|
||||
mind = mindSystem.CreateMind(null);
|
||||
mindSystem.TransferTo(mind, playerEnt);
|
||||
|
||||
Assert.That(mind.CurrentEntity, Is.EqualTo(playerEnt));
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
mapManager.DeleteMap(testMap.MapId);
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(entMan.EntityExists(mind.CurrentEntity!.Value), Is.True);
|
||||
Assert.That(mind.CurrentEntity, Is.Not.EqualTo(playerEnt));
|
||||
});
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
mapManager.DeleteMap(map.MapId);
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
282
Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs
Normal file
282
Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Players;
|
||||
using NUnit.Framework;
|
||||
using Robust.Server.Console;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Minds;
|
||||
|
||||
// Tests various scenarios where an entity that is associated with a player's mind is deleted.
|
||||
public sealed partial class MindTests
|
||||
{
|
||||
// This test will do the following:
|
||||
// - spawn a player
|
||||
// - visit some entity
|
||||
// - delete the entity being visited
|
||||
// - assert that player returns to original entity
|
||||
[Test]
|
||||
public async Task TestDeleteVisiting()
|
||||
{
|
||||
await using var pairTracker = await SetupPair();
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IServerEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
|
||||
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
|
||||
|
||||
EntityUid playerEnt = default;
|
||||
EntityUid visitEnt = default;
|
||||
Mind mind = default!;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = playerMan.ServerSessions.Single();
|
||||
|
||||
playerEnt = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
visitEnt = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
mind = mindSystem.CreateMind(player.UserId);
|
||||
mindSystem.TransferTo(mind, playerEnt);
|
||||
mindSystem.Visit(mind, visitEnt);
|
||||
|
||||
Assert.That(player.AttachedEntity, Is.EqualTo(visitEnt));
|
||||
Assert.That(mind.VisitingEntity, Is.EqualTo(visitEnt));
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
await server.WaitPost(() => entMan.DeleteEntity(visitEnt));
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
Assert.IsNull(mind.VisitingEntity);
|
||||
Assert.That(entMan.EntityExists(mind.OwnedEntity));
|
||||
Assert.That(mind.OwnedEntity, Is.EqualTo(playerEnt));
|
||||
|
||||
// This used to throw so make sure it doesn't.
|
||||
await server.WaitPost(() => entMan.DeleteEntity(mind.OwnedEntity!.Value));
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
// this is a variant of TestGhostOnDelete that just deletes the whole map.
|
||||
[Test]
|
||||
public async Task TestGhostOnDeleteMap()
|
||||
{
|
||||
await using var pairTracker = await SetupPair();
|
||||
var server = pairTracker.Pair.Server;
|
||||
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
||||
var coordinates = testMap.GridCoords;
|
||||
|
||||
var entMan = server.ResolveDependency<IServerEntityManager>();
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
var player = playerMan.ServerSessions.Single();
|
||||
|
||||
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
|
||||
|
||||
EntityUid playerEnt = default;
|
||||
Mind mind = default!;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
playerEnt = entMan.SpawnEntity(null, coordinates);
|
||||
mind = player.ContentData()!.Mind!;
|
||||
mindSystem.TransferTo(mind, playerEnt);
|
||||
|
||||
Assert.That(mind.CurrentEntity, Is.EqualTo(playerEnt));
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
await server.WaitPost(() => mapManager.DeleteMap(testMap.MapId));
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(entMan.EntityExists(mind.CurrentEntity!.Value), Is.True);
|
||||
Assert.That(mind.CurrentEntity, Is.Not.EqualTo(playerEnt));
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that a ghost gets created when the player entity is deleted.
|
||||
/// 1. Delete mob
|
||||
/// 2. Assert is ghost
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestGhostOnDelete()
|
||||
{
|
||||
// Client is needed to spawn session
|
||||
await using var pairTracker = await SetupPair();
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IServerEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
|
||||
IPlayerSession player = playerMan.ServerSessions.Single();
|
||||
|
||||
Assert.That(!entMan.HasComponent<GhostComponent>(player.AttachedEntity), "Player was initially a ghost?");
|
||||
|
||||
// Delete entity
|
||||
await server.WaitPost(() => entMan.DeleteEntity(player.AttachedEntity!.Value));
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
Assert.That(entMan.HasComponent<GhostComponent>(player.AttachedEntity), "Player did not become a ghost");
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that when the original mob gets deleted, the visited ghost does not get deleted.
|
||||
/// And that the visited ghost becomes the main mob.
|
||||
/// 1. Visit ghost
|
||||
/// 2. Delete original mob
|
||||
/// 3. Assert is ghost
|
||||
/// 4. Assert was not deleted
|
||||
/// 5. Assert is main mob
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestOriginalDeletedWhileGhostingKeepsGhost()
|
||||
{
|
||||
// Client is needed to spawn session
|
||||
await using var pairTracker = await SetupPair();
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IServerEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
|
||||
var mind = GetMind(pairTracker.Pair);
|
||||
|
||||
var player = playerMan.ServerSessions.Single();
|
||||
Assert.NotNull(player.AttachedEntity);
|
||||
Assert.That(entMan.EntityExists(player.AttachedEntity));
|
||||
var originalEntity = player.AttachedEntity.Value;
|
||||
|
||||
EntityUid ghost = default!;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
ghost = entMan.SpawnEntity("MobObserver", MapCoordinates.Nullspace);
|
||||
mindSystem.Visit(mind, ghost);
|
||||
});
|
||||
|
||||
Assert.That(player.AttachedEntity, Is.EqualTo(ghost));
|
||||
Assert.That(entMan.HasComponent<GhostComponent>(player.AttachedEntity), "player is not a ghost");
|
||||
Assert.That(mind.VisitingEntity, Is.EqualTo(player.AttachedEntity));
|
||||
Assert.That(mind.OwnedEntity, Is.EqualTo(originalEntity));
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
await server.WaitAssertion(() => entMan.DeleteEntity(originalEntity));
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
Assert.That(entMan.Deleted(originalEntity));
|
||||
|
||||
// Check that the player is still in control of the ghost
|
||||
mind = GetMind(pairTracker.Pair);
|
||||
Assert.That(!entMan.Deleted(ghost), "ghost has been deleted");
|
||||
Assert.That(player.AttachedEntity, Is.EqualTo(ghost));
|
||||
Assert.That(entMan.HasComponent<GhostComponent>(player.AttachedEntity));
|
||||
Assert.IsNull(mind.VisitingEntity);
|
||||
Assert.That(mind.OwnedEntity, Is.EqualTo(ghost));
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that ghosts can become admin ghosts without issue
|
||||
/// 1. Become a ghost
|
||||
/// 2. visit an admin ghost
|
||||
/// 3. original ghost is deleted, player is an admin ghost.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestGhostToAghost()
|
||||
{
|
||||
await using var pairTracker = await SetupPair();
|
||||
var server = pairTracker.Pair.Server;
|
||||
var entMan = server.ResolveDependency<IServerEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
var serverConsole = server.ResolveDependency<IServerConsoleHost>();
|
||||
|
||||
var player = playerMan.ServerSessions.Single();
|
||||
|
||||
var ghost = await BecomeGhost(pairTracker.Pair);
|
||||
|
||||
// Player is a normal ghost (not admin ghost).
|
||||
Assert.That(entMan.GetComponent<MetaDataComponent>(player.AttachedEntity!.Value).EntityPrototype?.ID, Is.Not.EqualTo("AdminObserver"));
|
||||
|
||||
// Try to become an admin ghost
|
||||
await server.WaitAssertion(() => serverConsole.ExecuteCommand(player, "aghost"));
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
Assert.That(entMan.Deleted(ghost), "old ghost was not deleted");
|
||||
Assert.That(player.AttachedEntity, Is.Not.EqualTo(ghost), "Player is still attached to the old ghost");
|
||||
Assert.That(entMan.HasComponent<GhostComponent>(player.AttachedEntity!.Value), "Player did not become a new ghost");
|
||||
Assert.That(entMan.GetComponent<MetaDataComponent>(player.AttachedEntity.Value).EntityPrototype?.ID, Is.EqualTo("AdminObserver"));
|
||||
|
||||
var mind = player.ContentData()?.Mind;
|
||||
Assert.NotNull(mind);
|
||||
Assert.Null(mind.VisitingEntity);
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test ghost getting deleted while player is connected spawns another ghost
|
||||
/// 1. become ghost
|
||||
/// 2. delete ghost
|
||||
/// 3. new ghost is spawned
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestGhostDeletedSpawnsNewGhost()
|
||||
{
|
||||
// Client is needed to spawn session
|
||||
await using var pairTracker = await SetupPair();
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IServerEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
var serverConsole = server.ResolveDependency<IServerConsoleHost>();
|
||||
|
||||
IPlayerSession player = playerMan.ServerSessions.Single();
|
||||
|
||||
EntityUid ghost = default!;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(player.AttachedEntity, Is.Not.EqualTo(null));
|
||||
entMan.DeleteEntity(player.AttachedEntity!.Value);
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
// Is player a ghost?
|
||||
Assert.That(player.AttachedEntity, Is.Not.EqualTo(null));
|
||||
ghost = player.AttachedEntity!.Value;
|
||||
Assert.That(entMan.HasComponent<GhostComponent>(ghost));
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
serverConsole.ExecuteCommand(player, "aghost");
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(entMan.Deleted(ghost));
|
||||
Assert.That(player.AttachedEntity, Is.Not.EqualTo(ghost));
|
||||
Assert.That(entMan.HasComponent<GhostComponent>(player.AttachedEntity!.Value));
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
171
Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs
Normal file
171
Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Players;
|
||||
using NUnit.Framework;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using IPlayerManager = Robust.Server.Player.IPlayerManager;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Minds;
|
||||
|
||||
// This partial class contains misc helper functions for other tests.
|
||||
public sealed partial class MindTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a server-client pair and ensures that the client is attached to a simple mind test entity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Without this, it may be possible that a tests starts with the client attached to an entity that does not match
|
||||
/// the player's mind's current entity, likely because some previous test directly changed the players attached
|
||||
/// entity.
|
||||
/// </remarks>
|
||||
public async Task<PairTracker> SetupPair()
|
||||
{
|
||||
var pairTracker = await PoolManager.GetServerClient();
|
||||
var pair = pairTracker.Pair;
|
||||
|
||||
var entMan = pair.Server.ResolveDependency<IServerEntityManager>();
|
||||
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
|
||||
var mindSys = entMan.System<MindSystem>();
|
||||
|
||||
var player = playerMan.ServerSessions.Single();
|
||||
|
||||
EntityUid entity = default;
|
||||
Mind mind = default!;
|
||||
await pair.Server.WaitPost(() =>
|
||||
{
|
||||
entity = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
mind = mindSys.CreateMind(player.UserId);
|
||||
mindSys.TransferTo(mind, entity);
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pair, 5);
|
||||
|
||||
Assert.That(player.ContentData()?.Mind, Is.EqualTo(mind));
|
||||
Assert.That(player.AttachedEntity, Is.EqualTo(entity));
|
||||
Assert.That(player.AttachedEntity, Is.EqualTo(mind.CurrentEntity), "Player is not attached to the mind's current entity.");
|
||||
Assert.That(entMan.EntityExists(mind.OwnedEntity), "The mind's current entity does not exist");
|
||||
Assert.That(mind.VisitingEntity == null || entMan.EntityExists(mind.VisitingEntity), "The minds visited entity does not exist.");
|
||||
return pairTracker;
|
||||
}
|
||||
|
||||
public async Task<EntityUid> BecomeGhost(Pair pair, bool visit = false)
|
||||
{
|
||||
var entMan = pair.Server.ResolveDependency<IServerEntityManager>();
|
||||
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
|
||||
var mindSys = entMan.System<MindSystem>();
|
||||
EntityUid ghostUid = default;
|
||||
Mind mind = default!;
|
||||
|
||||
var player = playerMan.ServerSessions.Single();
|
||||
await pair.Server.WaitAssertion(() =>
|
||||
{
|
||||
var oldUid = player.AttachedEntity;
|
||||
ghostUid = entMan.SpawnEntity("MobObserver", MapCoordinates.Nullspace);
|
||||
mind = mindSys.GetMind(player.UserId);
|
||||
Assert.NotNull(mind);
|
||||
|
||||
if (visit)
|
||||
{
|
||||
mindSys.Visit(mind, ghostUid);
|
||||
return;
|
||||
}
|
||||
|
||||
mindSys.TransferTo(mind, ghostUid);
|
||||
if (oldUid != null)
|
||||
entMan.DeleteEntity(oldUid.Value);
|
||||
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pair, 5);
|
||||
Assert.That(entMan.HasComponent<GhostComponent>(ghostUid));
|
||||
Assert.That(player.AttachedEntity == ghostUid);
|
||||
Assert.That(mind.CurrentEntity == ghostUid);
|
||||
|
||||
if (!visit)
|
||||
Assert.Null(mind.VisitingEntity);
|
||||
|
||||
return ghostUid;
|
||||
}
|
||||
|
||||
public async Task<EntityUid> VisitGhost(Pair pair, bool visit = false)
|
||||
{
|
||||
return await BecomeGhost(pair, visit: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the player's current mind and check that the entities exists.
|
||||
/// </summary>
|
||||
public Mind GetMind(Pair pair)
|
||||
{
|
||||
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
|
||||
var entMan = pair.Server.ResolveDependency<IEntityManager>();
|
||||
var player = playerMan.ServerSessions.SingleOrDefault();
|
||||
Assert.NotNull(player);
|
||||
|
||||
var mind = player.ContentData()!.Mind;
|
||||
Assert.NotNull(mind);
|
||||
Assert.That(player.AttachedEntity, Is.EqualTo(mind.CurrentEntity), "Player is not attached to the mind's current entity.");
|
||||
Assert.That(entMan.EntityExists(mind.OwnedEntity), "The mind's current entity does not exist");
|
||||
Assert.That(mind.VisitingEntity == null || entMan.EntityExists(mind.VisitingEntity), "The minds visited entity does not exist.");
|
||||
|
||||
return mind;
|
||||
}
|
||||
|
||||
public async Task Disconnect(Pair pair)
|
||||
{
|
||||
var netManager = pair.Client.ResolveDependency<IClientNetManager>();
|
||||
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
|
||||
var player = playerMan.ServerSessions.Single();
|
||||
var mind = player.ContentData()!.Mind;
|
||||
|
||||
await pair.Client.WaitAssertion(() =>
|
||||
{
|
||||
netManager.ClientDisconnect("Disconnect command used.");
|
||||
});
|
||||
await PoolManager.RunTicksSync(pair, 5);
|
||||
|
||||
Assert.That(player.Status == SessionStatus.Disconnected);
|
||||
Assert.NotNull(mind.UserId);
|
||||
Assert.Null(mind.Session);
|
||||
}
|
||||
|
||||
public async Task Connect(Pair pair, string username)
|
||||
{
|
||||
var netManager = pair.Client.ResolveDependency<IClientNetManager>();
|
||||
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
|
||||
Assert.That(!playerMan.ServerSessions.Any());
|
||||
|
||||
await Task.WhenAll(pair.Client.WaitIdleAsync(), pair.Client.WaitIdleAsync());
|
||||
pair.Client.SetConnectTarget(pair.Server);
|
||||
await pair.Client.WaitPost(() => netManager.ClientConnect(null!, 0, username));
|
||||
await PoolManager.RunTicksSync(pair, 5);
|
||||
|
||||
var player = playerMan.ServerSessions.Single();
|
||||
Assert.That(player.Status == SessionStatus.InGame);
|
||||
}
|
||||
|
||||
public async Task<IPlayerSession> DisconnectReconnect(Pair pair)
|
||||
{
|
||||
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
|
||||
var player = playerMan.ServerSessions.Single();
|
||||
var name = player.Name;
|
||||
var id = player.UserId;
|
||||
|
||||
await Disconnect(pair);
|
||||
await Connect(pair, name);
|
||||
|
||||
// Session has changed
|
||||
var newSession = playerMan.ServerSessions.Single();
|
||||
Assert.That(newSession != player);
|
||||
Assert.That(newSession.UserId == id);
|
||||
|
||||
return newSession;
|
||||
}
|
||||
}
|
||||
145
Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs
Normal file
145
Content.IntegrationTests/Tests/Minds/MindTests.ReconnectTests.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Server.Mind;
|
||||
using NUnit.Framework;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Minds;
|
||||
|
||||
public sealed partial class MindTests
|
||||
{
|
||||
// This test will do the following:
|
||||
// - attach a player to a ghost (not visiting)
|
||||
// - disconnect
|
||||
// - reconnect
|
||||
// - assert that they spawned in as a new entity
|
||||
[Test]
|
||||
public async Task TestGhostsCanReconnect()
|
||||
{
|
||||
await using var pairTracker = await SetupPair();
|
||||
var pair = pairTracker.Pair;
|
||||
var entMan = pair.Server.ResolveDependency<IEntityManager>();
|
||||
var mind = GetMind(pair);
|
||||
|
||||
var ghost = await BecomeGhost(pair);
|
||||
await DisconnectReconnect(pair);
|
||||
|
||||
// Player in control of a new ghost, but with the same mind
|
||||
Assert.That(GetMind(pair) == mind);
|
||||
Assert.That(entMan.Deleted(ghost));
|
||||
Assert.That(entMan.HasComponent<GhostComponent>(mind.OwnedEntity));
|
||||
Assert.Null(mind.VisitingEntity);
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
// This test will do the following:
|
||||
// - disconnect a player
|
||||
// - delete their original entity
|
||||
// - reconnect
|
||||
// - assert that they spawned in as a new entity
|
||||
[Test]
|
||||
public async Task TestDeletedCanReconnect()
|
||||
{
|
||||
await using var pairTracker = await SetupPair();
|
||||
var pair = pairTracker.Pair;
|
||||
var entMan = pair.Server.ResolveDependency<IEntityManager>();
|
||||
var mind = GetMind(pair);
|
||||
|
||||
var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
|
||||
var player = playerMan.ServerSessions.Single();
|
||||
var name = player.Name;
|
||||
var user = player.UserId;
|
||||
Assert.NotNull(mind.OwnedEntity);
|
||||
var entity = mind.OwnedEntity.Value;
|
||||
|
||||
// Player is not a ghost
|
||||
Assert.That(!entMan.HasComponent<GhostComponent>(mind.CurrentEntity));
|
||||
|
||||
// Disconnect
|
||||
await Disconnect(pair);
|
||||
|
||||
// Delete entity
|
||||
Assert.That(entMan.EntityExists(entity));
|
||||
await pair.Server.WaitPost(() => entMan.DeleteEntity(entity));
|
||||
Assert.That(entMan.Deleted(entity));
|
||||
Assert.IsNull(mind.OwnedEntity);
|
||||
|
||||
// Reconnect
|
||||
await Connect(pair, name);
|
||||
player = playerMan.ServerSessions.Single();
|
||||
Assert.That(user, Is.EqualTo(player.UserId));
|
||||
|
||||
// Player is now a new ghost entity
|
||||
Assert.That(GetMind(pair), Is.EqualTo(mind));
|
||||
Assert.That(mind.OwnedEntity, Is.Not.EqualTo(entity));
|
||||
Assert.That(entMan.HasComponent<GhostComponent>(mind.OwnedEntity));
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
// This test will do the following:
|
||||
// - visit a ghost
|
||||
// - disconnect
|
||||
// - reconnect
|
||||
// - assert that they return to their original entity
|
||||
[Test]
|
||||
public async Task TestVisitingGhostReconnect()
|
||||
{
|
||||
await using var pairTracker = await SetupPair();
|
||||
var pair = pairTracker.Pair;
|
||||
var entMan = pair.Server.ResolveDependency<IEntityManager>();
|
||||
var mind = GetMind(pair);
|
||||
|
||||
var original = mind.CurrentEntity;
|
||||
var ghost = await VisitGhost(pair);
|
||||
await DisconnectReconnect(pair);
|
||||
|
||||
// Player now controls their original mob, mind was preserved
|
||||
Assert.That(mind, Is.EqualTo(GetMind(pair)));
|
||||
Assert.That(mind.CurrentEntity, Is.EqualTo(original));
|
||||
Assert.That(!entMan.Deleted(original));
|
||||
Assert.That(entMan.Deleted(ghost));
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
// This test will do the following:
|
||||
// - visit a normal (non-ghost) entity,
|
||||
// - disconnect
|
||||
// - reconnect
|
||||
// - assert that they return to the visited entity.
|
||||
[Test]
|
||||
public async Task TestVisitingReconnect()
|
||||
{
|
||||
await using var pairTracker = await SetupPair();
|
||||
var pair = pairTracker.Pair;
|
||||
var entMan = pair.Server.ResolveDependency<IEntityManager>();
|
||||
var mindSys = entMan.System<MindSystem>();
|
||||
var mind = GetMind(pair);
|
||||
|
||||
// Make player visit a new mob
|
||||
var original = mind.CurrentEntity;
|
||||
EntityUid visiting = default;
|
||||
await pair.Server.WaitAssertion(() =>
|
||||
{
|
||||
visiting = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
mindSys.Visit(mind, visiting);
|
||||
});
|
||||
await PoolManager.RunTicksSync(pair, 5);
|
||||
|
||||
await DisconnectReconnect(pair);
|
||||
|
||||
// Player is back in control of the visited mob, mind was preserved
|
||||
Assert.That(mind == GetMind(pair));
|
||||
Assert.That(!entMan.Deleted(original));
|
||||
Assert.That(!entMan.Deleted(visiting));
|
||||
Assert.That(mind.CurrentEntity == visiting);
|
||||
Assert.That(mind.CurrentEntity == visiting);
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -28,18 +28,13 @@ using IPlayerManager = Robust.Server.Player.IPlayerManager;
|
||||
namespace Content.IntegrationTests.Tests.Minds;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class MindTests
|
||||
public sealed partial class MindTests
|
||||
{
|
||||
private const string Prototypes = @"
|
||||
- type: entity
|
||||
id: MindTestEntity
|
||||
components:
|
||||
- type: MindContainer
|
||||
|
||||
- type: entity
|
||||
parent: MindTestEntity
|
||||
id: MindTestEntityDamageable
|
||||
components:
|
||||
- type: MindContainer
|
||||
- type: Damageable
|
||||
damageContainer: Biological
|
||||
- type: Body
|
||||
@@ -61,26 +56,6 @@ public sealed class MindTests
|
||||
- !type:GibBehavior { }
|
||||
";
|
||||
|
||||
/// <summary>
|
||||
/// Exception handling for PlayerData and NetUserId invalid due to testing.
|
||||
/// Can be removed when Players can be mocked.
|
||||
/// </summary>
|
||||
/// <param name="func"></param>
|
||||
private void CatchPlayerDataException(Action func)
|
||||
{
|
||||
try
|
||||
{
|
||||
func();
|
||||
}
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
// Prevent exiting due to PlayerData not being initialized.
|
||||
if (e.Message == "New owner must have previously logged into the server. (Parameter 'newOwner')")
|
||||
return;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestCreateAndTransferMindToNewEntity()
|
||||
{
|
||||
@@ -125,7 +100,7 @@ public sealed class MindTests
|
||||
var mind = mindSystem.CreateMind(null);
|
||||
mindSystem.TransferTo(mind, entity);
|
||||
Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind));
|
||||
|
||||
|
||||
var mind2 = mindSystem.CreateMind(null);
|
||||
mindSystem.TransferTo(mind2, entity);
|
||||
Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind2));
|
||||
@@ -220,32 +195,44 @@ public sealed class MindTests
|
||||
[Test]
|
||||
public async Task TestOwningPlayerCanBeChanged()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{ NoClient = true });
|
||||
await using var pairTracker = await PoolManager.GetServerClient();
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IServerEntityManager>();
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
|
||||
var originalMind = GetMind(pairTracker.Pair);
|
||||
var userId = originalMind.UserId;
|
||||
|
||||
Mind mind = default!;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
|
||||
|
||||
var entity = entMan.SpawnEntity(null, new MapCoordinates());
|
||||
var mindComp = entMan.EnsureComponent<MindContainerComponent>(entity);
|
||||
entMan.DirtyEntity(entity);
|
||||
|
||||
var mind = mindSystem.CreateMind(null);
|
||||
|
||||
mind = mindSystem.CreateMind(null);
|
||||
mindSystem.TransferTo(mind, entity);
|
||||
|
||||
Assert.That(mindSystem.GetMind(entity, mindComp), Is.EqualTo(mind));
|
||||
|
||||
var newUserId = new NetUserId(Guid.NewGuid());
|
||||
Assert.That(mindComp.HasMind);
|
||||
CatchPlayerDataException(() =>
|
||||
mindSystem.ChangeOwningPlayer(mindComp.Mind!, newUserId));
|
||||
|
||||
Assert.That(mind.UserId, Is.EqualTo(newUserId));
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
mindSystem.SetUserId(mind, userId);
|
||||
Assert.That(mind.UserId, Is.EqualTo(userId));
|
||||
Assert.That(originalMind.UserId, Is.EqualTo(null));
|
||||
|
||||
mindSystem.SetUserId(originalMind, userId);
|
||||
Assert.That(mind.UserId, Is.EqualTo(null));
|
||||
Assert.That(originalMind.UserId, Is.EqualTo(userId));
|
||||
});
|
||||
|
||||
await PoolManager.RunTicksSync(pairTracker.Pair, 5);
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
@@ -275,26 +262,26 @@ public sealed class MindTests
|
||||
Assert.That(!mindSystem.HasRole<Job>(mind));
|
||||
|
||||
var traitorRole = new TraitorRole(mind, new AntagPrototype());
|
||||
|
||||
|
||||
mindSystem.AddRole(mind, traitorRole);
|
||||
|
||||
|
||||
Assert.That(mindSystem.HasRole<TraitorRole>(mind));
|
||||
Assert.That(!mindSystem.HasRole<Job>(mind));
|
||||
|
||||
var jobRole = new Job(mind, new JobPrototype());
|
||||
|
||||
|
||||
mindSystem.AddRole(mind, jobRole);
|
||||
|
||||
|
||||
Assert.That(mindSystem.HasRole<TraitorRole>(mind));
|
||||
Assert.That(mindSystem.HasRole<Job>(mind));
|
||||
|
||||
|
||||
mindSystem.RemoveRole(mind, traitorRole);
|
||||
|
||||
|
||||
Assert.That(!mindSystem.HasRole<TraitorRole>(mind));
|
||||
Assert.That(mindSystem.HasRole<Job>(mind));
|
||||
|
||||
|
||||
mindSystem.RemoveRole(mind, jobRole);
|
||||
|
||||
|
||||
Assert.That(!mindSystem.HasRole<TraitorRole>(mind));
|
||||
Assert.That(!mindSystem.HasRole<Job>(mind));
|
||||
});
|
||||
@@ -353,7 +340,7 @@ public sealed class MindTests
|
||||
MakeSentientCommand.MakeSentient(mob, IoCManager.Resolve<IEntityManager>());
|
||||
mobMind = mindSystem.CreateMind(player.UserId, "Mindy McThinker the Second");
|
||||
|
||||
mindSystem.ChangeOwningPlayer(mobMind, player.UserId);
|
||||
mindSystem.SetUserId(mobMind, player.UserId);
|
||||
mindSystem.TransferTo(mobMind, mob);
|
||||
});
|
||||
|
||||
@@ -370,11 +357,11 @@ public sealed class MindTests
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
// TODO Implement
|
||||
/*[Test]
|
||||
public async Task TestPlayerCanReturnFromGhostWhenDead()
|
||||
{
|
||||
// TODO Implement
|
||||
}
|
||||
}*/
|
||||
|
||||
[Test]
|
||||
public async Task TestGhostDoesNotInfiniteLoop()
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Salvage;
|
||||
using NUnit.Framework;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public sealed class SalvageTest
|
||||
{
|
||||
[Test]
|
||||
public async Task SalvageGridBoundsTest()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
|
||||
var server = pairTracker.Pair.Server;
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
var mapMan = server.ResolveDependency<IMapManager>();
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var mapLoader = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<MapLoaderSystem>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var salvage in protoManager.EnumeratePrototypes<SalvageMapPrototype>())
|
||||
{
|
||||
var mapId = mapMan.CreateMap();
|
||||
mapLoader.TryLoad(mapId, salvage.MapPath.ToString(), out var rootUids);
|
||||
Assert.That(rootUids is { Count: 1 }, $"Salvage map {salvage.ID} does not have a single grid");
|
||||
var grid = rootUids[0];
|
||||
Assert.That(entManager.TryGetComponent<MapGridComponent>(grid, out var gridComp), $"Salvage {salvage.ID}'s grid does not have GridComponent.");
|
||||
Assert.That(gridComp.LocalAABB, Is.EqualTo(salvage.Bounds), $"Salvage {salvage.ID}'s bounds {gridComp.LocalAABB} are not equal to the bounds on the prototype {salvage.Bounds}");
|
||||
}
|
||||
});
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Content.IntegrationTests/Tests/StackTest.cs
Normal file
42
Content.IntegrationTests/Tests/StackTest.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Stacks;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class StackTest
|
||||
{
|
||||
[Test]
|
||||
public async Task StackCorrectItemSize()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var entity in PoolManager.GetEntityPrototypes<StackComponent>(server))
|
||||
{
|
||||
if (!entity.TryGetComponent<StackComponent>(out var stackComponent, compFact) ||
|
||||
!entity.TryGetComponent<ItemComponent>(out var itemComponent, compFact))
|
||||
continue;
|
||||
|
||||
if (!protoManager.TryIndex<StackPrototype>(stackComponent.StackTypeId, out var stackProto) ||
|
||||
stackProto.ItemSize == null)
|
||||
continue;
|
||||
|
||||
var expectedSize = stackProto.ItemSize * stackComponent.Count;
|
||||
Assert.That(itemComponent.Size, Is.EqualTo(expectedSize), $"Prototype id: {entity.ID} has an item size of {itemComponent.Size} but expected size of {expectedSize}.");
|
||||
}
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,15 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.MapRenderer.Painters
|
||||
namespace Content.MapRenderer.Painters;
|
||||
|
||||
public readonly record struct EntityData(EntityUid Owner, SpriteComponent Sprite, float X, float Y)
|
||||
{
|
||||
public sealed class EntityData
|
||||
{
|
||||
public EntityData(SpriteComponent sprite, float x, float y)
|
||||
{
|
||||
Sprite = sprite;
|
||||
X = x;
|
||||
Y = y;
|
||||
}
|
||||
public readonly EntityUid Owner = Owner;
|
||||
|
||||
public SpriteComponent Sprite { get; }
|
||||
public readonly SpriteComponent Sprite = Sprite;
|
||||
|
||||
public float X { get; }
|
||||
public readonly float X = X;
|
||||
|
||||
public float Y { get; }
|
||||
}
|
||||
public readonly float Y = Y;
|
||||
}
|
||||
|
||||
@@ -38,23 +38,24 @@ public sealed class EntityPainter
|
||||
|
||||
// TODO cache this shit what are we insane
|
||||
entities.Sort(Comparer<EntityData>.Create((x, y) => x.Sprite.DrawDepth.CompareTo(y.Sprite.DrawDepth)));
|
||||
var xformSystem = _sEntityManager.System<SharedTransformSystem>();
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
Run(canvas, entity);
|
||||
Run(canvas, entity, xformSystem);
|
||||
}
|
||||
|
||||
Console.WriteLine($"{nameof(EntityPainter)} painted {entities.Count} entities in {(int) stopwatch.Elapsed.TotalMilliseconds} ms");
|
||||
}
|
||||
|
||||
public void Run(Image canvas, EntityData entity)
|
||||
public void Run(Image canvas, EntityData entity, SharedTransformSystem xformSystem)
|
||||
{
|
||||
if (!entity.Sprite.Visible || entity.Sprite.ContainerOccluded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var worldRotation = _sEntityManager.GetComponent<TransformComponent>(entity.Sprite.Owner).WorldRotation;
|
||||
var worldRotation = xformSystem.GetWorldRotation(entity.Owner);
|
||||
foreach (var layer in entity.Sprite.AllLayers)
|
||||
{
|
||||
if (!layer.Visible)
|
||||
@@ -70,7 +71,7 @@ public sealed class EntityPainter
|
||||
var rsi = layer.ActualRsi;
|
||||
Image image;
|
||||
|
||||
if (rsi == null || rsi.Path == null || !rsi.TryGetState(layer.RsiState, out var state))
|
||||
if (rsi == null || !rsi.TryGetState(layer.RsiState, out var state))
|
||||
{
|
||||
image = _errorImage;
|
||||
}
|
||||
@@ -89,7 +90,7 @@ public sealed class EntityPainter
|
||||
|
||||
image = image.CloneAs<Rgba32>();
|
||||
|
||||
(int, int, int, int) GetRsiFrame(RSI? rsi, Image image, EntityData entity, ISpriteLayer layer, int direction)
|
||||
static (int, int, int, int) GetRsiFrame(RSI? rsi, Image image, EntityData entity, ISpriteLayer layer, int direction)
|
||||
{
|
||||
if (rsi is null)
|
||||
return (0, 0, EyeManager.PixelsPerMeter, EyeManager.PixelsPerMeter);
|
||||
@@ -115,7 +116,7 @@ public sealed class EntityPainter
|
||||
var rect = new Rectangle(x, y, width, height);
|
||||
if (!new Rectangle(Point.Empty, image.Size()).Contains(rect))
|
||||
{
|
||||
Console.WriteLine($"Invalid layer {rsi!.Path}/{layer.RsiState.Name}.png for entity {_sEntityManager.ToPrettyString(entity.Sprite.Owner)} at ({entity.X}, {entity.Y})");
|
||||
Console.WriteLine($"Invalid layer {rsi!.Path}/{layer.RsiState.Name}.png for entity {_sEntityManager.ToPrettyString(entity.Owner)} at ({entity.X}, {entity.Y})");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,24 +45,24 @@ namespace Content.MapRenderer.Painters
|
||||
_decals = GetDecals();
|
||||
}
|
||||
|
||||
public void Run(Image gridCanvas, MapGridComponent grid)
|
||||
public void Run(Image gridCanvas, EntityUid gridUid, MapGridComponent grid)
|
||||
{
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
if (!_entities.TryGetValue(grid.Owner, out var entities))
|
||||
if (!_entities.TryGetValue(gridUid, out var entities))
|
||||
{
|
||||
Console.WriteLine($"No entities found on grid {grid.Owner}");
|
||||
Console.WriteLine($"No entities found on grid {gridUid}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Decals are always painted before entities, and are also optional.
|
||||
if (_decals.TryGetValue(grid.Owner, out var decals))
|
||||
if (_decals.TryGetValue(gridUid, out var decals))
|
||||
_decalPainter.Run(gridCanvas, CollectionsMarshal.AsSpan(decals));
|
||||
|
||||
|
||||
_entityPainter.Run(gridCanvas, entities);
|
||||
Console.WriteLine($"{nameof(GridPainter)} painted grid {grid.Owner} in {(int) stopwatch.Elapsed.TotalMilliseconds} ms");
|
||||
Console.WriteLine($"{nameof(GridPainter)} painted grid {gridUid} in {(int) stopwatch.Elapsed.TotalMilliseconds} ms");
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<EntityUid, List<EntityData>> GetEntities()
|
||||
@@ -91,7 +91,7 @@ namespace Content.MapRenderer.Painters
|
||||
var position = transform.LocalPosition;
|
||||
|
||||
var (x, y) = TransformLocalPosition(position, grid);
|
||||
var data = new EntityData(sprite, x, y);
|
||||
var data = new EntityData(entity, sprite, x, y);
|
||||
|
||||
components.GetOrAdd(transform.GridUid.Value, _ => new List<EntityData>()).Add(data);
|
||||
}
|
||||
@@ -108,21 +108,22 @@ namespace Content.MapRenderer.Painters
|
||||
stopwatch.Start();
|
||||
|
||||
var decals = new Dictionary<EntityUid, List<DecalData>>();
|
||||
var query = _sEntityManager.AllEntityQueryEnumerator<MapGridComponent>();
|
||||
|
||||
foreach (var grid in _sMapManager.GetAllGrids())
|
||||
while (query.MoveNext(out var uid, out var grid))
|
||||
{
|
||||
// TODO this needs to use the client entity manager because the client
|
||||
// actually has the correct z-indices for decals for some reason when the server doesn't,
|
||||
// BUT can't do that yet because the client hasn't actually received everything yet
|
||||
// for some reason decal moment i guess.
|
||||
if (_sEntityManager.TryGetComponent<DecalGridComponent>(grid.Owner, out var comp))
|
||||
if (_sEntityManager.TryGetComponent<DecalGridComponent>(uid, out var comp))
|
||||
{
|
||||
foreach (var chunk in comp.ChunkCollection.ChunkCollection.Values)
|
||||
{
|
||||
foreach (var decal in chunk.Decals.Values)
|
||||
{
|
||||
var (x, y) = TransformLocalPosition(decal.Coordinates, grid);
|
||||
decals.GetOrNew(grid.Owner).Add(new DecalData(decal, x, y));
|
||||
decals.GetOrNew(uid).Add(new DecalData(decal, x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +60,9 @@ namespace Content.MapRenderer.Painters
|
||||
|
||||
var tilePainter = new TilePainter(client, server);
|
||||
var entityPainter = new GridPainter(client, server);
|
||||
MapGridComponent[] grids = null!;
|
||||
(EntityUid Uid, MapGridComponent Grid)[] grids = null!;
|
||||
var xformQuery = sEntityManager.GetEntityQuery<TransformComponent>();
|
||||
var xformSystem = sEntityManager.System<SharedTransformSystem>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
@@ -73,12 +74,12 @@ namespace Content.MapRenderer.Painters
|
||||
}
|
||||
|
||||
var mapId = sMapManager.GetAllMapIds().Last();
|
||||
grids = sMapManager.GetAllMapGrids(mapId).ToArray();
|
||||
grids = sMapManager.GetAllMapGrids(mapId).Select(o => (o.Owner, o)).ToArray();
|
||||
|
||||
foreach (var grid in grids)
|
||||
{
|
||||
var gridXform = xformQuery.GetComponent(grid.Owner);
|
||||
gridXform.WorldRotation = Angle.Zero;
|
||||
var gridXform = xformQuery.GetComponent(grid.Uid);
|
||||
xformSystem.SetWorldRotation(gridXform, Angle.Zero);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,16 +89,16 @@ namespace Content.MapRenderer.Painters
|
||||
foreach (var grid in grids)
|
||||
{
|
||||
// Skip empty grids
|
||||
if (grid.LocalAABB.IsEmpty())
|
||||
if (grid.Grid.LocalAABB.IsEmpty())
|
||||
{
|
||||
Console.WriteLine($"Warning: Grid {grid.Owner} was empty. Skipping image rendering.");
|
||||
Console.WriteLine($"Warning: Grid {grid.Uid} was empty. Skipping image rendering.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var tileXSize = grid.TileSize * TilePainter.TileImageSize;
|
||||
var tileYSize = grid.TileSize * TilePainter.TileImageSize;
|
||||
var tileXSize = grid.Grid.TileSize * TilePainter.TileImageSize;
|
||||
var tileYSize = grid.Grid.TileSize * TilePainter.TileImageSize;
|
||||
|
||||
var bounds = grid.LocalAABB;
|
||||
var bounds = grid.Grid.LocalAABB;
|
||||
|
||||
var left = bounds.Left;
|
||||
var right = bounds.Right;
|
||||
@@ -111,16 +112,16 @@ namespace Content.MapRenderer.Painters
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
tilePainter.Run(gridCanvas, grid);
|
||||
entityPainter.Run(gridCanvas, grid);
|
||||
tilePainter.Run(gridCanvas, grid.Uid, grid.Grid);
|
||||
entityPainter.Run(gridCanvas, grid.Uid, grid.Grid);
|
||||
|
||||
gridCanvas.Mutate(e => e.Flip(FlipMode.Vertical));
|
||||
});
|
||||
|
||||
var renderedImage = new RenderedGridImage<Rgba32>(gridCanvas)
|
||||
{
|
||||
GridUid = grid.Owner,
|
||||
Offset = xformQuery.GetComponent(grid.Owner).WorldPosition
|
||||
GridUid = grid.Uid,
|
||||
Offset = xformSystem.GetWorldPosition(grid.Uid),
|
||||
};
|
||||
|
||||
yield return renderedImage;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -26,7 +27,7 @@ namespace Content.MapRenderer.Painters
|
||||
_cResourceCache = client.ResolveDependency<IResourceCache>();
|
||||
}
|
||||
|
||||
public void Run(Image gridCanvas, MapGridComponent grid)
|
||||
public void Run(Image gridCanvas, EntityUid gridUid, MapGridComponent grid)
|
||||
{
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
@@ -55,7 +56,7 @@ namespace Content.MapRenderer.Painters
|
||||
i++;
|
||||
});
|
||||
|
||||
Console.WriteLine($"{nameof(TilePainter)} painted {i} tiles on grid {grid.Owner} in {(int) stopwatch.Elapsed.TotalMilliseconds} ms");
|
||||
Console.WriteLine($"{nameof(TilePainter)} painted {i} tiles on grid {gridUid} in {(int) stopwatch.Elapsed.TotalMilliseconds} ms");
|
||||
}
|
||||
|
||||
private Dictionary<string, List<Image>> GetTileImages(
|
||||
@@ -87,7 +88,8 @@ namespace Content.MapRenderer.Painters
|
||||
|
||||
for (var i = 0; i < definition.Variants; i++)
|
||||
{
|
||||
var tileImage = tileSheet.Clone(o => o.Crop(new Rectangle(tileSize * i, 0, 32, 32)));
|
||||
var index = i;
|
||||
var tileImage = tileSheet.Clone(o => o.Crop(new Rectangle(tileSize * index, 0, 32, 32)));
|
||||
images[path].Add(tileImage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.UserInterface.Systems.EscapeMenu;
|
||||
@@ -13,7 +14,7 @@ using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Shared.Replays.IReplayRecordingManager;
|
||||
using static Robust.Shared.Replays.ReplayConstants;
|
||||
|
||||
namespace Content.Replay.Menu;
|
||||
|
||||
@@ -71,8 +72,10 @@ public sealed class ReplayMainScreen : State
|
||||
return;
|
||||
}
|
||||
|
||||
using var fileReader = new ReplayFileReaderZip(
|
||||
new ZipArchive(_resMan.UserData.OpenRead(replay)), ReplayZipFolder);
|
||||
if (!_resMan.UserData.Exists(replay)
|
||||
|| _loadMan.LoadYamlMetadata(_resMan.UserData, replay) is not { } data)
|
||||
|| _loadMan.LoadYamlMetadata(fileReader) is not { } data)
|
||||
{
|
||||
info.SetMarkup(Loc.GetString("replay-info-invalid"));
|
||||
info.HorizontalAlignment = Control.HAlignment.Center;
|
||||
@@ -82,16 +85,16 @@ public sealed class ReplayMainScreen : State
|
||||
}
|
||||
|
||||
var file = replay.ToRelativePath().ToString();
|
||||
data.TryGet<ValueDataNode>(Time, out var timeNode);
|
||||
data.TryGet<ValueDataNode>(Duration, out var durationNode);
|
||||
data.TryGet<ValueDataNode>(MetaKeyTime, out var timeNode);
|
||||
data.TryGet<ValueDataNode>(MetaFinalKeyDuration, out var durationNode);
|
||||
data.TryGet<ValueDataNode>("roundId", out var roundIdNode);
|
||||
data.TryGet<ValueDataNode>(Hash, out var hashNode);
|
||||
data.TryGet<ValueDataNode>(CompHash, out var compHashNode);
|
||||
data.TryGet<ValueDataNode>(MetaKeyTypeHash, out var hashNode);
|
||||
data.TryGet<ValueDataNode>(MetaKeyComponentHash, out var compHashNode);
|
||||
DateTime.TryParse(timeNode?.Value, out var time);
|
||||
TimeSpan.TryParse(durationNode?.Value, out var duration);
|
||||
|
||||
var forkId = string.Empty;
|
||||
if (data.TryGet<ValueDataNode>(Fork, out var forkNode))
|
||||
if (data.TryGet<ValueDataNode>(MetaKeyForkId, out var forkNode))
|
||||
{
|
||||
// TODO Replay client build info.
|
||||
// When distributing the client we need to distribute a build.json or provide these cvars some other way?
|
||||
@@ -105,7 +108,7 @@ public sealed class ReplayMainScreen : State
|
||||
}
|
||||
|
||||
var forkVersion = string.Empty;
|
||||
if (data.TryGet<ValueDataNode>(ForkVersion, out var versionNode))
|
||||
if (data.TryGet<ValueDataNode>(MetaKeyForkVersion, out var versionNode))
|
||||
{
|
||||
forkVersion = versionNode.Value;
|
||||
// Why does this not have a try-convert function? I just want to check if it looks like a hash code.
|
||||
@@ -162,7 +165,7 @@ public sealed class ReplayMainScreen : State
|
||||
}
|
||||
|
||||
var engineVersion = string.Empty;
|
||||
if (data.TryGet<ValueDataNode>(Engine, out var engineNode))
|
||||
if (data.TryGet<ValueDataNode>(MetaKeyEngineVersion, out var engineNode))
|
||||
{
|
||||
var clientVer = _cfg.GetCVar(CVars.BuildEngineVersion);
|
||||
if (string.IsNullOrWhiteSpace(clientVer))
|
||||
@@ -176,7 +179,7 @@ public sealed class ReplayMainScreen : State
|
||||
// Strip milliseconds. Apparently there is no general format string that suppresses milliseconds.
|
||||
duration = new((int)Math.Floor(duration.TotalDays), duration.Hours, duration.Minutes, duration.Seconds);
|
||||
|
||||
data.TryGet<ValueDataNode>(Name, out var nameNode);
|
||||
data.TryGet<ValueDataNode>(MetaKeyName, out var nameNode);
|
||||
var name = nameNode?.Value ?? string.Empty;
|
||||
|
||||
info.HorizontalAlignment = Control.HAlignment.Left;
|
||||
@@ -205,7 +208,11 @@ public sealed class ReplayMainScreen : State
|
||||
private void OnLoadpressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
if (_selected.HasValue)
|
||||
_loadMan.LoadAndStartReplay(_resMan.UserData, _selected.Value);
|
||||
{
|
||||
var fileReader = new ReplayFileReaderZip(
|
||||
new ZipArchive(_resMan.UserData.OpenRead(_selected.Value)), ReplayZipFolder);
|
||||
_loadMan.LoadAndStartReplay(fileReader);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshReplays()
|
||||
@@ -217,11 +224,14 @@ public sealed class ReplayMainScreen : State
|
||||
var file = _directory / entry;
|
||||
try
|
||||
{
|
||||
var data = _loadMan.LoadYamlMetadata(_resMan.UserData, file);
|
||||
using var fileReader = new ReplayFileReaderZip(
|
||||
new ZipArchive(_resMan.UserData.OpenRead(file)), ReplayZipFolder);
|
||||
|
||||
var data = _loadMan.LoadYamlMetadata(fileReader);
|
||||
if (data == null)
|
||||
continue;
|
||||
|
||||
var name = data.Get<ValueDataNode>(Name).Value;
|
||||
var name = data.Get<ValueDataNode>(MetaKeyName).Value;
|
||||
_replays.Add((name, file));
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.Commands;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Administration;
|
||||
@@ -58,4 +59,26 @@ public sealed class AdjustStationJobCommand : IConsoleCommand
|
||||
|
||||
stationJobs.TrySetJobSlot(station, job, amount, true);
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
var options = ContentCompletionHelper.StationIds(_entityManager);
|
||||
return CompletionResult.FromHintOptions(options, "<station id>");
|
||||
}
|
||||
|
||||
if (args.Length == 2)
|
||||
{
|
||||
var options = CompletionHelper.PrototypeIDs<JobPrototype>();
|
||||
return CompletionResult.FromHintOptions(options, "<job id>");
|
||||
}
|
||||
|
||||
if (args.Length == 3)
|
||||
{
|
||||
return CompletionResult.FromHint("<amount>");
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.Commands;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Administration;
|
||||
@@ -40,4 +41,15 @@ public sealed class ListStationJobsCommand : IConsoleCommand
|
||||
shell.WriteLine($"{job}: {amountText}");
|
||||
}
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
var options = ContentCompletionHelper.StationIds(_entityManager);
|
||||
return CompletionResult.FromHintOptions(options, "<station id>");
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.Commands;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Administration;
|
||||
@@ -27,7 +28,7 @@ public sealed class RenameStationCommand : IConsoleCommand
|
||||
|
||||
var stationSystem = _entSysManager.GetEntitySystem<StationSystem>();
|
||||
|
||||
if (!EntityUid.TryParse(args[0], out var station) || _entityManager.HasComponent<StationDataComponent>(station))
|
||||
if (!EntityUid.TryParse(args[0], out var station) || !_entityManager.HasComponent<StationDataComponent>(station))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-argument-station-id-invalid", ("index", 1)));
|
||||
return;
|
||||
@@ -35,4 +36,20 @@ public sealed class RenameStationCommand : IConsoleCommand
|
||||
|
||||
stationSystem.RenameStation(station, args[1]);
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
var options = ContentCompletionHelper.StationIds(_entityManager);
|
||||
return CompletionResult.FromHintOptions(options, "<station id>");
|
||||
}
|
||||
|
||||
if (args.Length == 2)
|
||||
{
|
||||
return CompletionResult.FromHint("<name>");
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +292,11 @@ namespace Content.Server.Administration.Managers
|
||||
|
||||
private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminData(IPlayerSession session)
|
||||
{
|
||||
if (IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal) || _promotedPlayers.Contains(session.UserId))
|
||||
var promoteHost = IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal)
|
||||
|| _promotedPlayers.Contains(session.UserId)
|
||||
|| session.Name == _cfg.GetCVar(CCVars.ConsoleLoginHostUser);
|
||||
|
||||
if (promoteHost)
|
||||
{
|
||||
var data = new AdminData
|
||||
{
|
||||
|
||||
@@ -29,8 +29,6 @@ public sealed class AlertLevelComponent : Component
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] public bool IsLevelLocked = false;
|
||||
|
||||
[ViewVariables] public const float Delay = 30;
|
||||
|
||||
[ViewVariables] public float CurrentDelay = 0;
|
||||
[ViewVariables] public bool ActiveDelay;
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.PDA;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.AlertLevel;
|
||||
@@ -12,6 +14,7 @@ public sealed class AlertLevelSystem : EntitySystem
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
// Until stations are a prototype, this is how it's going to have to be.
|
||||
public const string DefaultAlertLevelSet = "stationAlerts";
|
||||
@@ -138,7 +141,7 @@ public sealed class AlertLevelSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
component.CurrentDelay = AlertLevelComponent.Delay;
|
||||
component.CurrentDelay = _cfg.GetCVar(CCVars.GameAlertLevelChangeDelay);
|
||||
component.ActiveDelay = true;
|
||||
}
|
||||
|
||||
@@ -186,12 +189,6 @@ public sealed class AlertLevelSystem : EntitySystem
|
||||
}
|
||||
|
||||
RaiseLocalEvent(new AlertLevelChangedEvent(station, level));
|
||||
|
||||
var pdas = EntityQueryEnumerator<PdaComponent>();
|
||||
while (pdas.MoveNext(out var ent, out var comp))
|
||||
{
|
||||
RaiseLocalEvent(ent,new AlertLevelChangedEvent(station, level));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ public sealed class SpaceVillainArcadeComponent : SharedSpaceVillainArcadeCompon
|
||||
"FoamCrossbow", "RevolverCapGun", "PlushieHampter", "PlushieLizard", "PlushieAtmosian", "PlushieSpaceLizard",
|
||||
"PlushieNuke", "PlushieCarp", "PlushieRatvar", "PlushieNar", "PlushieSnake", "Basketball", "Football",
|
||||
"PlushieRouny", "PlushieBee", "PlushieSlime", "BalloonCorgi", "ToySword", "CrayonBox", "BoxDonkSoftBox", "BoxCartridgeCap",
|
||||
"HarmonicaInstrument", "OcarinaInstrument", "RecorderInstrument", "GunpetInstrument", "BirdToyInstrument", "PlushieXeno"
|
||||
"HarmonicaInstrument", "OcarinaInstrument", "RecorderInstrument", "GunpetInstrument", "BirdToyInstrument", "PlushieXeno", "BeachBall"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,7 +7,6 @@ using Content.Shared.Maps;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Timing;
|
||||
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
|
||||
|
||||
namespace Content.Server.Atmos.EntitySystems
|
||||
{
|
||||
|
||||
@@ -35,13 +35,13 @@ namespace Content.Server.Body.Components
|
||||
/// How much should bleeding should be reduced every update interval?
|
||||
/// </summary>
|
||||
[DataField("bleedReductionAmount")]
|
||||
public float BleedReductionAmount = 0.5f;
|
||||
public float BleedReductionAmount = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// How high can <see cref="BleedAmount"/> go?
|
||||
/// </summary>
|
||||
[DataField("maxBleedAmount")]
|
||||
public float MaxBleedAmount = 20.0f;
|
||||
public float MaxBleedAmount = 10.0f;
|
||||
|
||||
/// <summary>
|
||||
/// What percentage of current blood is necessary to avoid dealing blood loss damage?
|
||||
@@ -67,7 +67,7 @@ namespace Content.Server.Body.Components
|
||||
/// How frequently should this bloodstream update, in seconds?
|
||||
/// </summary>
|
||||
[DataField("updateInterval")]
|
||||
public float UpdateInterval = 5.0f;
|
||||
public float UpdateInterval = 3.0f;
|
||||
|
||||
// TODO shouldn't be hardcoded, should just use some organ simulation like bone marrow or smth.
|
||||
/// <summary>
|
||||
@@ -80,7 +80,7 @@ namespace Content.Server.Body.Components
|
||||
/// How much blood needs to be in the temporary solution in order to create a puddle?
|
||||
/// </summary>
|
||||
[DataField("bleedPuddleThreshold")]
|
||||
public FixedPoint2 BleedPuddleThreshold = 5.0f;
|
||||
public FixedPoint2 BleedPuddleThreshold = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// A modifier set prototype ID corresponding to how damage should be modified
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
|
||||
|
||||
SubscribeLocalEvent<CardboardBoxComponent, DamageChangedEvent>(OnDamage);
|
||||
}
|
||||
|
||||
|
||||
private void OnInteracted(EntityUid uid, CardboardBoxComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (!TryComp<EntityStorageComponent>(uid, out var box))
|
||||
@@ -75,7 +75,7 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
|
||||
{
|
||||
RaiseNetworkEvent(new PlayBoxEffectMessage(uid, component.Mover.Value));
|
||||
_audio.PlayPvs(component.EffectSound, uid);
|
||||
component.EffectCooldown = _timing.CurTime + CardboardBoxComponent.MaxEffectCooldown;
|
||||
component.EffectCooldown = _timing.CurTime + component.CooldownDuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
Content.Server/Cargo/Components/CargoBountyLabelComponent.cs
Normal file
20
Content.Server/Cargo/Components/CargoBountyLabelComponent.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Content.Server.Cargo.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for marking containers as
|
||||
/// containing goods for fulfilling bounties.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class CargoBountyLabelComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID for the bounty this label corresponds to.
|
||||
/// </summary>
|
||||
[DataField("id"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Id;
|
||||
|
||||
/// <summary>
|
||||
/// Used to prevent recursion in calculating the price.
|
||||
/// </summary>
|
||||
public bool Calculating;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Content.Shared.Cargo;
|
||||
|
||||
namespace Content.Server.Cargo.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Stores all active cargo bounties for a particular station.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class StationCargoBountyDatabaseComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum amount of bounties a station can have.
|
||||
/// </summary>
|
||||
[DataField("maxBounties"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public int MaxBounties = 3;
|
||||
|
||||
/// <summary>
|
||||
/// A list of all the bounties currently active for a station.
|
||||
/// </summary>
|
||||
[DataField("bounties"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<CargoBountyData> Bounties = new();
|
||||
|
||||
/// <summary>
|
||||
/// Used to determine unique order IDs
|
||||
/// </summary>
|
||||
[DataField("totalBounties")]
|
||||
public int TotalBounties;
|
||||
|
||||
/// <summary>
|
||||
/// A poor-man's weighted list of the durations for how long
|
||||
/// each bounty will last.
|
||||
/// </summary>
|
||||
[DataField("bountyDurations")]
|
||||
public List<TimeSpan> BountyDurations = new()
|
||||
{
|
||||
TimeSpan.FromMinutes(5),
|
||||
TimeSpan.FromMinutes(7.5f),
|
||||
TimeSpan.FromMinutes(7.5f),
|
||||
TimeSpan.FromMinutes(7.5f),
|
||||
TimeSpan.FromMinutes(10),
|
||||
TimeSpan.FromMinutes(10),
|
||||
TimeSpan.FromMinutes(10),
|
||||
TimeSpan.FromMinutes(10),
|
||||
TimeSpan.FromMinutes(10),
|
||||
TimeSpan.FromMinutes(15)
|
||||
};
|
||||
}
|
||||
341
Content.Server/Cargo/Systems/CargoSystem.Bounty.cs
Normal file
341
Content.Server/Cargo/Systems/CargoSystem.Bounty.cs
Normal file
@@ -0,0 +1,341 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Cargo.Components;
|
||||
using Content.Server.Labels;
|
||||
using Content.Server.Paper;
|
||||
using Content.Shared.Cargo;
|
||||
using Content.Shared.Cargo.Components;
|
||||
using Content.Shared.Cargo.Prototypes;
|
||||
using Content.Shared.Database;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Cargo.Systems;
|
||||
|
||||
public sealed partial class CargoSystem
|
||||
{
|
||||
[Dependency] private readonly ContainerSystem _container = default!;
|
||||
|
||||
private void InitializeBounty()
|
||||
{
|
||||
SubscribeLocalEvent<CargoBountyConsoleComponent, BoundUIOpenedEvent>(OnBountyConsoleOpened);
|
||||
SubscribeLocalEvent<CargoBountyConsoleComponent, BountyPrintLabelMessage>(OnPrintLabelMessage);
|
||||
SubscribeLocalEvent<CargoBountyLabelComponent, PriceCalculationEvent>(OnGetBountyPrice);
|
||||
SubscribeLocalEvent<EntitySoldEvent>(OnSold);
|
||||
SubscribeLocalEvent<StationCargoBountyDatabaseComponent, MapInitEvent>(OnMapInit);
|
||||
}
|
||||
|
||||
private void OnBountyConsoleOpened(EntityUid uid, CargoBountyConsoleComponent component, BoundUIOpenedEvent args)
|
||||
{
|
||||
if (_station.GetOwningStation(uid) is not { } station ||
|
||||
!TryComp<StationCargoBountyDatabaseComponent>(station, out var bountyDb))
|
||||
return;
|
||||
|
||||
_uiSystem.TrySetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties));
|
||||
}
|
||||
|
||||
private void OnPrintLabelMessage(EntityUid uid, CargoBountyConsoleComponent component, BountyPrintLabelMessage args)
|
||||
{
|
||||
if (_timing.CurTime < component.NextPrintTime)
|
||||
return;
|
||||
|
||||
if (_station.GetOwningStation(uid) is not { } station)
|
||||
return;
|
||||
|
||||
if (!TryGetBountyFromId(station, args.BountyId, out var bounty))
|
||||
return;
|
||||
|
||||
var label = Spawn(component.BountyLabelId, Transform(uid).Coordinates);
|
||||
component.NextPrintTime = _timing.CurTime + component.PrintDelay;
|
||||
SetupBountyLabel(label, bounty.Value);
|
||||
_audio.PlayPvs(component.PrintSound, uid);
|
||||
}
|
||||
|
||||
public void SetupBountyLabel(EntityUid uid, CargoBountyData bounty, PaperComponent? paper = null, CargoBountyLabelComponent? label = null)
|
||||
{
|
||||
if (!Resolve(uid, ref paper, ref label) || !_protoMan.TryIndex<CargoBountyPrototype>(bounty.Bounty, out var prototype))
|
||||
return;
|
||||
|
||||
label.Id = bounty.Id;
|
||||
var msg = new FormattedMessage();
|
||||
msg.AddText(Loc.GetString("bounty-manifest-header", ("id", bounty.Id)));
|
||||
msg.PushNewline();
|
||||
msg.AddText(Loc.GetString("bounty-manifest-list-start"));
|
||||
msg.PushNewline();
|
||||
foreach (var entry in prototype.Entries)
|
||||
{
|
||||
msg.AddMarkup($"- {Loc.GetString("bounty-console-manifest-entry",
|
||||
("amount", entry.Amount),
|
||||
("item", Loc.GetString(entry.Name)))}");
|
||||
msg.PushNewline();
|
||||
}
|
||||
_paperSystem.SetContent(uid, msg.ToMarkup(), paper);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bounties do not sell for any currency. The reward for a bounty is
|
||||
/// calculated after it is sold separately from the selling system.
|
||||
/// </summary>
|
||||
private void OnGetBountyPrice(EntityUid uid, CargoBountyLabelComponent component, ref PriceCalculationEvent args)
|
||||
{
|
||||
if (args.Handled || component.Calculating)
|
||||
return;
|
||||
|
||||
// make sure this label was actually applied to a crate.
|
||||
if (!_container.TryGetContainingContainer(uid, out var container) || container.ID != LabelSystem.ContainerName)
|
||||
return;
|
||||
|
||||
if (_station.GetOwningStation(uid) is not { } station)
|
||||
return;
|
||||
|
||||
if (!TryGetBountyFromId(station, component.Id, out var bounty))
|
||||
return;
|
||||
|
||||
if (!_protoMan.TryIndex<CargoBountyPrototype>(bounty.Value.Bounty, out var bountyProtoype) ||!IsBountyComplete(container.Owner, bountyProtoype))
|
||||
return;
|
||||
args.Handled = true;
|
||||
|
||||
component.Calculating = true;
|
||||
args.Price = bountyProtoype.Reward - _pricing.GetPrice(container.Owner);
|
||||
component.Calculating = false;
|
||||
}
|
||||
|
||||
private void OnSold(ref EntitySoldEvent args)
|
||||
{
|
||||
var containerQuery = GetEntityQuery<ContainerManagerComponent>();
|
||||
var labelQuery = GetEntityQuery<CargoBountyLabelComponent>();
|
||||
foreach (var sold in args.Sold)
|
||||
{
|
||||
if (!containerQuery.TryGetComponent(sold, out var containerMan))
|
||||
continue;
|
||||
|
||||
// make sure this label was actually applied to a crate.
|
||||
if (!_container.TryGetContainer(sold, LabelSystem.ContainerName, out var container, containerMan))
|
||||
continue;
|
||||
|
||||
if (container.ContainedEntities.FirstOrNull() is not { } label ||
|
||||
!labelQuery.TryGetComponent(label, out var component))
|
||||
continue;
|
||||
|
||||
if (!TryGetBountyFromId(args.Station, component.Id, out var bounty))
|
||||
continue;
|
||||
|
||||
if (!IsBountyComplete(container.Owner, bounty.Value))
|
||||
continue;
|
||||
|
||||
TryRemoveBounty(args.Station, bounty.Value);
|
||||
FillBountyDatabase(args.Station);
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"Bounty \"{bounty.Value.Bounty}\" (id:{bounty.Value.Id}) was fulfilled");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, StationCargoBountyDatabaseComponent component, MapInitEvent args)
|
||||
{
|
||||
FillBountyDatabase(uid, component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills up the bounty database with random bounties.
|
||||
/// </summary>
|
||||
public void FillBountyDatabase(EntityUid uid, StationCargoBountyDatabaseComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
while (component.Bounties.Count < component.MaxBounties)
|
||||
{
|
||||
if (!TryAddBounty(uid, component))
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateBountyConsoles();
|
||||
}
|
||||
|
||||
public bool IsBountyComplete(EntityUid container, CargoBountyData data)
|
||||
{
|
||||
if (!_protoMan.TryIndex<CargoBountyPrototype>(data.Bounty, out var proto))
|
||||
return false;
|
||||
|
||||
return IsBountyComplete(container, proto.Entries);
|
||||
}
|
||||
|
||||
public bool IsBountyComplete(EntityUid container, string id)
|
||||
{
|
||||
if (!_protoMan.TryIndex<CargoBountyPrototype>(id, out var proto))
|
||||
return false;
|
||||
|
||||
return IsBountyComplete(container, proto.Entries);
|
||||
}
|
||||
|
||||
public bool IsBountyComplete(EntityUid container, CargoBountyPrototype prototype)
|
||||
{
|
||||
return IsBountyComplete(container, prototype.Entries);
|
||||
}
|
||||
|
||||
public bool IsBountyComplete(EntityUid container, IEnumerable<CargoBountyItemEntry> entries)
|
||||
{
|
||||
var contained = new HashSet<EntityUid>();
|
||||
if (TryComp<ContainerManagerComponent>(container, out var containers))
|
||||
{
|
||||
foreach (var con in containers.Containers.Values)
|
||||
{
|
||||
if (con.ID == LabelSystem.ContainerName)
|
||||
continue;
|
||||
|
||||
foreach (var ent in con.ContainedEntities)
|
||||
{
|
||||
contained.Add(ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return IsBountyComplete(contained, entries);
|
||||
}
|
||||
|
||||
public bool IsBountyComplete(HashSet<EntityUid> entities, IEnumerable<CargoBountyItemEntry> entries)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var count = 0;
|
||||
|
||||
// store entities that already satisfied an
|
||||
// entry so we don't double-count them.
|
||||
var temp = new HashSet<EntityUid>();
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (!entry.Whitelist.IsValid(entity, EntityManager))
|
||||
continue;
|
||||
count++;
|
||||
temp.Add(entity);
|
||||
|
||||
if (count >= entry.Amount)
|
||||
break;
|
||||
}
|
||||
|
||||
if (count < entry.Amount)
|
||||
return false;
|
||||
|
||||
foreach (var ent in temp)
|
||||
{
|
||||
entities.Remove(ent);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public bool TryAddBounty(EntityUid uid, StationCargoBountyDatabaseComponent? component = null)
|
||||
{
|
||||
// todo: consider making the cargo bounties weighted.
|
||||
var bounty = _random.Pick(_protoMan.EnumeratePrototypes<CargoBountyPrototype>().ToList());
|
||||
return TryAddBounty(uid, bounty, component);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public bool TryAddBounty(EntityUid uid, string bountyId, StationCargoBountyDatabaseComponent? component = null)
|
||||
{
|
||||
if (!_protoMan.TryIndex<CargoBountyPrototype>(bountyId, out var bounty))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryAddBounty(uid, bounty, component);
|
||||
}
|
||||
|
||||
public bool TryAddBounty(EntityUid uid, CargoBountyPrototype bounty, StationCargoBountyDatabaseComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return false;
|
||||
|
||||
if (component.Bounties.Count >= component.MaxBounties)
|
||||
return false;
|
||||
|
||||
var endTime = _timing.CurTime + _random.Pick(component.BountyDurations) + TimeSpan.FromSeconds(_random.Next(-10, 10));
|
||||
component.Bounties.Add(new CargoBountyData(component.TotalBounties, bounty.ID, endTime));
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"Added bounty \"{bounty.ID}\" (id:{component.TotalBounties}) to station {ToPrettyString(uid)}");
|
||||
component.TotalBounties++;
|
||||
return true;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public bool TryRemoveBounty(EntityUid uid, int dataId, StationCargoBountyDatabaseComponent? component = null)
|
||||
{
|
||||
if (!TryGetBountyFromId(uid, dataId, out var data, component))
|
||||
return false;
|
||||
|
||||
return TryRemoveBounty(uid, data.Value, component);
|
||||
}
|
||||
|
||||
public bool TryRemoveBounty(EntityUid uid, CargoBountyData data, StationCargoBountyDatabaseComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < component.Bounties.Count; i++)
|
||||
{
|
||||
if (component.Bounties[i].Id == data.Id)
|
||||
{
|
||||
component.Bounties.RemoveAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetBountyFromId(
|
||||
EntityUid uid,
|
||||
int id,
|
||||
[NotNullWhen(true)] out CargoBountyData? bounty,
|
||||
StationCargoBountyDatabaseComponent? component = null)
|
||||
{
|
||||
bounty = null;
|
||||
if (!Resolve(uid, ref component))
|
||||
return false;
|
||||
|
||||
foreach (var bountyData in component.Bounties)
|
||||
{
|
||||
if (bountyData.Id != id)
|
||||
continue;
|
||||
bounty = bountyData;
|
||||
break;
|
||||
}
|
||||
|
||||
return bounty != null;
|
||||
}
|
||||
|
||||
public void UpdateBountyConsoles()
|
||||
{
|
||||
var query = EntityQueryEnumerator<CargoBountyConsoleComponent, ServerUserInterfaceComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var ui))
|
||||
{
|
||||
if (_station.GetOwningStation(uid) is not { } station ||
|
||||
!TryComp<StationCargoBountyDatabaseComponent>(station, out var db))
|
||||
continue;
|
||||
|
||||
_uiSystem.TrySetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties), ui: ui);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateBounty()
|
||||
{
|
||||
var query = EntityQueryEnumerator<StationCargoBountyDatabaseComponent>();
|
||||
while (query.MoveNext(out var uid, out var bountyDatabase))
|
||||
{
|
||||
var bounties = new ValueList<CargoBountyData>(bountyDatabase.Bounties);
|
||||
foreach (var bounty in bounties)
|
||||
{
|
||||
if (_timing.CurTime < bounty.EndTime)
|
||||
continue;
|
||||
TryRemoveBounty(uid, bounty, bountyDatabase);
|
||||
FillBountyDatabase(uid, bountyDatabase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Coordinates;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Server.Cargo.Systems;
|
||||
|
||||
@@ -228,14 +229,21 @@ public sealed partial class CargoSystem
|
||||
|
||||
#region Station
|
||||
|
||||
private void SellPallets(EntityUid gridUid, out double amount)
|
||||
private void SellPallets(EntityUid gridUid, EntityUid? station, out double amount)
|
||||
{
|
||||
station ??= _station.GetOwningStation(gridUid);
|
||||
GetPalletGoods(gridUid, out var toSell, out amount);
|
||||
|
||||
_sawmill.Debug($"Cargo sold {toSell.Count} entities for {amount}");
|
||||
|
||||
foreach (var ent in toSell)
|
||||
{
|
||||
if (station != null)
|
||||
{
|
||||
var ev = new EntitySoldEvent(station.Value, toSell);
|
||||
RaiseLocalEvent(ref ev);
|
||||
}
|
||||
|
||||
Del(ent);
|
||||
}
|
||||
}
|
||||
@@ -325,7 +333,7 @@ public sealed partial class CargoSystem
|
||||
return;
|
||||
}
|
||||
|
||||
SellPallets(gridUid, out var price);
|
||||
SellPallets(gridUid, null, out var price);
|
||||
var stackPrototype = _protoMan.Index<StackPrototype>(component.CashType);
|
||||
_stack.Spawn((int)price, stackPrototype, uid.ToCoordinates());
|
||||
UpdatePalletConsoleInterface(uid);
|
||||
@@ -359,7 +367,7 @@ public sealed partial class CargoSystem
|
||||
|
||||
if (TryComp<StationBankAccountComponent>(stationUid, out var bank))
|
||||
{
|
||||
SellPallets(uid, out var amount);
|
||||
SellPallets(uid, stationUid, out var amount);
|
||||
bank.Balance += (int) amount;
|
||||
}
|
||||
}
|
||||
@@ -424,3 +432,10 @@ public sealed partial class CargoSystem
|
||||
_console.RefreshShuttleConsoles();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event broadcast raised by-ref before it is sold and
|
||||
/// deleted but after the price has been calculated.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct EntitySoldEvent(EntityUid Station, HashSet<EntityUid> Sold);
|
||||
|
||||
@@ -15,12 +15,14 @@ using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Cargo.Systems;
|
||||
|
||||
public sealed partial class CargoSystem : SharedCargoSystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IComponentFactory _factory = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
@@ -51,6 +53,7 @@ public sealed partial class CargoSystem : SharedCargoSystem
|
||||
InitializeConsole();
|
||||
InitializeShuttle();
|
||||
InitializeTelepad();
|
||||
InitializeBounty();
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
@@ -64,6 +67,7 @@ public sealed partial class CargoSystem : SharedCargoSystem
|
||||
base.Update(frameTime);
|
||||
UpdateConsole(frameTime);
|
||||
UpdateTelepad(frameTime);
|
||||
UpdateBounty();
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
|
||||
@@ -3,5 +3,4 @@
|
||||
[RegisterComponent]
|
||||
public sealed class ActiveSolutionHeaterComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,50 @@
|
||||
namespace Content.Server.Chemistry.Components;
|
||||
using Content.Shared.Whitelist;
|
||||
|
||||
namespace Content.Server.Chemistry.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class SolutionHeaterComponent : Component
|
||||
{
|
||||
public readonly string BeakerSlotId = "beakerSlot";
|
||||
|
||||
[DataField("heatPerSecond")]
|
||||
public float HeatPerSecond = 120;
|
||||
/// <summary>
|
||||
/// How much heat is added per second to the solution, with no upgrades.
|
||||
/// </summary>
|
||||
[DataField("baseHeatPerSecond")]
|
||||
public float BaseHeatPerSecond = 120;
|
||||
|
||||
/// <summary>
|
||||
/// How much heat is added per second to the solution, taking upgrades into account.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float HeatMultiplier = 1;
|
||||
public float HeatPerSecond;
|
||||
|
||||
[DataField("machinePartHeatPerSecond")]
|
||||
public string MachinePartHeatPerSecond = "Capacitor";
|
||||
/// <summary>
|
||||
/// The machine part that affects the heat multiplier.
|
||||
/// </summary>
|
||||
[DataField("machinePartHeatMultiplier")]
|
||||
public string MachinePartHeatMultiplier = "Capacitor";
|
||||
|
||||
/// <summary>
|
||||
/// How much each upgrade multiplies the heat by.
|
||||
/// </summary>
|
||||
[DataField("partRatingHeatMultiplier")]
|
||||
public float PartRatingHeatMultiplier = 1.5f;
|
||||
|
||||
/// <summary>
|
||||
/// The entities that are placed on the heater.
|
||||
/// <summary>
|
||||
[DataField("placedEntities")]
|
||||
public HashSet<EntityUid> PlacedEntities = new();
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of entities that can be heated at the same time.
|
||||
/// </summary>
|
||||
[DataField("maxEntities")]
|
||||
public uint MaxEntities = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Whitelist for entities that can be placed on the heater.
|
||||
/// </summary>
|
||||
[DataField("whitelist")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public EntityWhitelist? Whitelist;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
using Content.Server.Chemistry.Components;
|
||||
using System.Linq;
|
||||
using Content.Server.Chemistry.Components;
|
||||
using Content.Server.Chemistry.Components.SolutionManager;
|
||||
using Content.Server.Construction;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Placeable;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
|
||||
namespace Content.Server.Chemistry.EntitySystems;
|
||||
|
||||
public sealed class SolutionHeaterSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly PlaceableSurfaceSystem _placeableSurface = default!;
|
||||
[Dependency] private readonly PowerReceiverSystem _powerReceiver = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solution = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -17,48 +25,101 @@ public sealed class SolutionHeaterSystem : EntitySystem
|
||||
SubscribeLocalEvent<SolutionHeaterComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
SubscribeLocalEvent<SolutionHeaterComponent, RefreshPartsEvent>(OnRefreshParts);
|
||||
SubscribeLocalEvent<SolutionHeaterComponent, UpgradeExamineEvent>(OnUpgradeExamine);
|
||||
SubscribeLocalEvent<SolutionHeaterComponent, StartCollideEvent>(OnStartCollide);
|
||||
SubscribeLocalEvent<SolutionHeaterComponent, EndCollideEvent>(OnEndCollide);
|
||||
}
|
||||
|
||||
private void TurnOn(EntityUid uid)
|
||||
{
|
||||
_appearance.SetData(uid, SolutionHeaterVisuals.IsOn, true);
|
||||
EnsureComp<ActiveSolutionHeaterComponent>(uid);
|
||||
}
|
||||
|
||||
public bool TryTurnOn(EntityUid uid, SolutionHeaterComponent component)
|
||||
{
|
||||
if (component.PlacedEntities.Count <= 0 || !_powerReceiver.IsPowered(uid))
|
||||
return false;
|
||||
|
||||
TurnOn(uid);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void TurnOff(EntityUid uid)
|
||||
{
|
||||
_appearance.SetData(uid, SolutionHeaterVisuals.IsOn, false);
|
||||
RemComp<ActiveSolutionHeaterComponent>(uid);
|
||||
}
|
||||
|
||||
private void OnPowerChanged(EntityUid uid, SolutionHeaterComponent component, ref PowerChangedEvent args)
|
||||
{
|
||||
if (args.Powered)
|
||||
if (args.Powered && component.PlacedEntities.Count > 0)
|
||||
{
|
||||
EnsureComp<ActiveSolutionHeaterComponent>(uid);
|
||||
TurnOn(uid);
|
||||
}
|
||||
else
|
||||
{
|
||||
RemComp<ActiveSolutionHeaterComponent>(uid);
|
||||
TurnOff(uid);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRefreshParts(EntityUid uid, SolutionHeaterComponent component, RefreshPartsEvent args)
|
||||
{
|
||||
var heatRating = args.PartRatings[component.MachinePartHeatPerSecond] - 1;
|
||||
var heatRating = args.PartRatings[component.MachinePartHeatMultiplier] - 1;
|
||||
|
||||
component.HeatMultiplier = MathF.Pow(component.PartRatingHeatMultiplier, heatRating);
|
||||
component.HeatPerSecond = component.BaseHeatPerSecond * MathF.Pow(component.PartRatingHeatMultiplier, heatRating);
|
||||
}
|
||||
|
||||
private void OnUpgradeExamine(EntityUid uid, SolutionHeaterComponent component, UpgradeExamineEvent args)
|
||||
{
|
||||
args.AddPercentageUpgrade("solution-heater-upgrade-heat", component.HeatMultiplier);
|
||||
args.AddPercentageUpgrade("solution-heater-upgrade-heat", component.HeatPerSecond / component.BaseHeatPerSecond);
|
||||
}
|
||||
|
||||
private void OnStartCollide(EntityUid uid, SolutionHeaterComponent component, ref StartCollideEvent args)
|
||||
{
|
||||
if (component.Whitelist is not null && !component.Whitelist.IsValid(args.OtherEntity))
|
||||
return;
|
||||
|
||||
// Disallow sleeping so we can detect when entity is removed from the heater.
|
||||
_physics.SetSleepingAllowed(args.OtherEntity, args.OtherBody, false);
|
||||
|
||||
component.PlacedEntities.Add(args.OtherEntity);
|
||||
|
||||
TryTurnOn(uid, component);
|
||||
|
||||
if (component.PlacedEntities.Count >= component.MaxEntities)
|
||||
_placeableSurface.SetPlaceable(uid, false);
|
||||
}
|
||||
|
||||
private void OnEndCollide(EntityUid uid, SolutionHeaterComponent component, ref EndCollideEvent args)
|
||||
{
|
||||
// Re-allow sleeping.
|
||||
_physics.SetSleepingAllowed(args.OtherEntity, args.OtherBody, true);
|
||||
|
||||
component.PlacedEntities.Remove(args.OtherEntity);
|
||||
|
||||
if (component.PlacedEntities.Count == 0) // Last entity was removed
|
||||
TurnOff(uid);
|
||||
|
||||
_placeableSurface.SetPlaceable(uid, true);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
foreach (var (_, heater) in EntityQuery<ActiveSolutionHeaterComponent, SolutionHeaterComponent>())
|
||||
var query = EntityQueryEnumerator<ActiveSolutionHeaterComponent, SolutionHeaterComponent>();
|
||||
while (query.MoveNext(out _, out _, out var heater))
|
||||
{
|
||||
if (_itemSlots.GetItemOrNull(heater.Owner, heater.BeakerSlotId) is not { } item)
|
||||
continue;
|
||||
|
||||
if (!TryComp<SolutionContainerManagerComponent>(item, out var solution))
|
||||
continue;
|
||||
|
||||
var energy = heater.HeatPerSecond * heater.HeatMultiplier * frameTime;
|
||||
foreach (var s in solution.Solutions.Values)
|
||||
foreach (var heatingEntity in heater.PlacedEntities.Take((int) heater.MaxEntities))
|
||||
{
|
||||
_solution.AddThermalEnergy(solution.Owner, s, energy);
|
||||
if (!TryComp<SolutionContainerManagerComponent>(heatingEntity, out var solution))
|
||||
continue;
|
||||
|
||||
var energy = heater.HeatPerSecond * frameTime;
|
||||
foreach (var s in solution.Solutions.Values)
|
||||
{
|
||||
_solution.AddThermalEnergy(heatingEntity, s, energy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
Content.Server/Commands/ContentCompletionHelper.cs
Normal file
22
Content.Server/Commands/ContentCompletionHelper.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Content.Server.Station.Components;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Helper functions for programming console command completions.
|
||||
/// </summary>
|
||||
public static class ContentCompletionHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Return all stations, with their ID as value and name as hint.
|
||||
/// </summary>
|
||||
public static IEnumerable<CompletionOption> StationIds(IEntityManager entityManager)
|
||||
{
|
||||
var query = entityManager.EntityQueryEnumerator<StationDataComponent, MetaDataComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var metaData))
|
||||
{
|
||||
yield return new CompletionOption(uid.ToString(), metaData.EntityName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.DeviceLinking.Components;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Timing;
|
||||
|
||||
@@ -9,6 +11,7 @@ public sealed class SignallerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DeviceLinkSystem _link = default!;
|
||||
[Dependency] private readonly UseDelaySystem _useDelay = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -28,6 +31,8 @@ public sealed class SignallerSystem : EntitySystem
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):actor} triggered signaler {ToPrettyString(uid):tool}");
|
||||
_link.InvokePort(uid, component.Port);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public sealed class MailingUnitSystem : EntitySystem
|
||||
case NetCmdResponse when args.Data.TryGetValue(NetTag, out string? tag):
|
||||
//Add the received tag request response to the list of targets
|
||||
component.TargetList.Add(tag);
|
||||
UpdateUserInterface(component);
|
||||
UpdateUserInterface(uid, component);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ public sealed class MailingUnitSystem : EntitySystem
|
||||
}
|
||||
|
||||
component.Tag = configuration[TagConfigurationKey];
|
||||
UpdateUserInterface(component);
|
||||
UpdateUserInterface(uid, component);
|
||||
}
|
||||
|
||||
private void HandleActivate(EntityUid uid, MailingUnitComponent component, ActivateInWorldEvent args)
|
||||
@@ -158,7 +158,8 @@ public sealed class MailingUnitSystem : EntitySystem
|
||||
|
||||
args.Handled = true;
|
||||
UpdateTargetList(uid, component);
|
||||
_userInterfaceSystem.GetUiOrNull(uid, MailingUnitUiKey.Key)?.Open(actor.PlayerSession);
|
||||
if (_userInterfaceSystem.TryGetUi(uid, MailingUnitUiKey.Key, out var bui))
|
||||
_userInterfaceSystem.OpenUi(bui, actor.PlayerSession);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -167,28 +168,23 @@ public sealed class MailingUnitSystem : EntitySystem
|
||||
private void OnDisposalUnitUIStateChange(EntityUid uid, MailingUnitComponent component, DisposalUnitUIStateUpdatedEvent args)
|
||||
{
|
||||
component.DisposalUnitInterfaceState = args.State;
|
||||
UpdateUserInterface(component);
|
||||
UpdateUserInterface(uid, component);
|
||||
}
|
||||
|
||||
private void UpdateUserInterface(MailingUnitComponent component)
|
||||
private void UpdateUserInterface(EntityUid uid, MailingUnitComponent component)
|
||||
{
|
||||
if (component.DisposalUnitInterfaceState == null)
|
||||
return;
|
||||
|
||||
var state = new MailingUnitBoundUserInterfaceState(component.DisposalUnitInterfaceState, component.Target, component.TargetList, component.Tag);
|
||||
component.Owner.GetUIOrNull(MailingUnitUiKey.Key)?.SetState(state);
|
||||
if (_userInterfaceSystem.TryGetUi(uid, MailingUnitUiKey.Key, out var bui))
|
||||
_userInterfaceSystem.SetUiState(bui, state);
|
||||
}
|
||||
|
||||
private void OnTargetSelected(EntityUid uid, MailingUnitComponent component, TargetSelectedMessage args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(args.target))
|
||||
{
|
||||
component.Target = null;
|
||||
}
|
||||
|
||||
component.Target = args.target;
|
||||
UpdateUserInterface(component);
|
||||
|
||||
component.Target = args.Target;
|
||||
UpdateUserInterface(uid, component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Disposal.Unit.Components;
|
||||
using Content.Server.Disposal.Unit.EntitySystems;
|
||||
|
||||
namespace Content.Server.Disposal.Tube.Components
|
||||
@@ -9,27 +6,6 @@ namespace Content.Server.Disposal.Tube.Components
|
||||
[Access(typeof(DisposalTubeSystem), typeof(DisposalUnitSystem))]
|
||||
public sealed class DisposalEntryComponent : Component
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
private const string HolderPrototypeId = "DisposalHolder";
|
||||
|
||||
public bool TryInsert(DisposalUnitComponent from, IEnumerable<string>? tags = default)
|
||||
{
|
||||
var holder = _entMan.SpawnEntity(HolderPrototypeId, _entMan.GetComponent<TransformComponent>(Owner).MapPosition);
|
||||
var holderComponent = _entMan.GetComponent<DisposalHolderComponent>(holder);
|
||||
|
||||
foreach (var entity in from.Container.ContainedEntities.ToArray())
|
||||
{
|
||||
holderComponent.TryInsert(entity);
|
||||
}
|
||||
|
||||
EntitySystem.Get<AtmosphereSystem>().Merge(holderComponent.Air, from.Air);
|
||||
from.Air.Clear();
|
||||
|
||||
if (tags != default)
|
||||
holderComponent.Tags.UnionWith(tags);
|
||||
|
||||
return EntitySystem.Get<DisposableSystem>().EnterTube((holderComponent).Owner, Owner, holderComponent);
|
||||
}
|
||||
public const string HolderPrototypeId = "DisposalHolder";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,63 +17,7 @@ namespace Content.Server.Disposal.Tube.Components
|
||||
[DataField("tags")]
|
||||
public HashSet<string> Tags = new();
|
||||
|
||||
[ViewVariables]
|
||||
public bool Anchored =>
|
||||
!_entMan.TryGetComponent(Owner, out PhysicsComponent? physics) ||
|
||||
physics.BodyType == BodyType.Static;
|
||||
|
||||
[ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(DisposalRouterUiKey.Key);
|
||||
|
||||
[DataField("clickSound")] private SoundSpecifier _clickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles ui messages from the client. For things such as button presses
|
||||
/// which interact with the world and require server action.
|
||||
/// </summary>
|
||||
/// <param name="obj">A user interface message from the client.</param>
|
||||
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
|
||||
{
|
||||
if (obj.Session.AttachedEntity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = (UiActionMessage) obj.Message;
|
||||
|
||||
if (!Anchored)
|
||||
return;
|
||||
|
||||
//Check for correct message and ignore maleformed strings
|
||||
if (msg.Action == UiAction.Ok && TagRegex.IsMatch(msg.Tags))
|
||||
{
|
||||
Tags.Clear();
|
||||
foreach (var tag in msg.Tags.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
Tags.Add(tag.Trim());
|
||||
ClickSound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClickSound()
|
||||
{
|
||||
SoundSystem.Play(_clickSound.GetSound(), Filter.Pvs(Owner), Owner, AudioParams.Default.WithVolume(-2f));
|
||||
}
|
||||
|
||||
protected override void OnRemove()
|
||||
{
|
||||
UserInterface?.CloseAll();
|
||||
base.OnRemove();
|
||||
}
|
||||
[DataField("clickSound")]
|
||||
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,60 +12,11 @@ namespace Content.Server.Disposal.Tube.Components
|
||||
[RegisterComponent]
|
||||
public sealed class DisposalTaggerComponent : DisposalTransitComponent
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("tag")]
|
||||
public string Tag = "";
|
||||
|
||||
[ViewVariables]
|
||||
public bool Anchored =>
|
||||
!_entMan.TryGetComponent(Owner, out PhysicsComponent? physics) ||
|
||||
physics.BodyType == BodyType.Static;
|
||||
|
||||
[ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(DisposalTaggerUiKey.Key);
|
||||
|
||||
[DataField("clickSound")] private SoundSpecifier _clickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles ui messages from the client. For things such as button presses
|
||||
/// which interact with the world and require server action.
|
||||
/// </summary>
|
||||
/// <param name="obj">A user interface message from the client.</param>
|
||||
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
|
||||
{
|
||||
var msg = (UiActionMessage) obj.Message;
|
||||
|
||||
if (!Anchored)
|
||||
return;
|
||||
|
||||
//Check for correct message and ignore maleformed strings
|
||||
if (msg.Action == UiAction.Ok && TagRegex.IsMatch(msg.Tag))
|
||||
{
|
||||
Tag = msg.Tag;
|
||||
ClickSound();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClickSound()
|
||||
{
|
||||
SoundSystem.Play(_clickSound.GetSound(), Filter.Pvs(Owner), Owner, AudioParams.Default.WithVolume(-2f));
|
||||
}
|
||||
|
||||
protected override void OnRemove()
|
||||
{
|
||||
base.OnRemove();
|
||||
UserInterface?.CloseAll();
|
||||
}
|
||||
[DataField("clickSound")]
|
||||
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,53 +26,7 @@ namespace Content.Server.Disposal.Tube.Components
|
||||
/// Container of entities that are currently inside this tube
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Container Contents { get; private set; } = default!;
|
||||
|
||||
// TODO: Make disposal pipes extend the grid
|
||||
// ???
|
||||
public void Connect()
|
||||
{
|
||||
if (Connected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Connected = true;
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
if (!Connected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Connected = false;
|
||||
|
||||
foreach (var entity in Contents.ContainedEntities.ToArray())
|
||||
{
|
||||
if (!_entMan.TryGetComponent(entity, out DisposalHolderComponent? holder))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EntitySystem.Get<DisposableSystem>().ExitDisposals((holder).Owner);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Contents = ContainerHelpers.EnsureContainer<Container>(Owner, ContainerId);
|
||||
Owner.EnsureComponent<AnchorableComponent>();
|
||||
}
|
||||
|
||||
protected override void OnRemove()
|
||||
{
|
||||
base.OnRemove();
|
||||
|
||||
Disconnect();
|
||||
}
|
||||
[Access(typeof(DisposalTubeSystem), typeof(DisposableSystem))]
|
||||
public Container Contents { get; set; } = default!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Construction.Completions;
|
||||
using Content.Server.Disposal.Tube.Components;
|
||||
using Content.Server.Disposal.Unit.Components;
|
||||
using Content.Server.Disposal.Unit.EntitySystems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.Disposal.Components;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent;
|
||||
using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent;
|
||||
|
||||
namespace Content.Server.Disposal.Tube
|
||||
{
|
||||
@@ -24,11 +31,18 @@ namespace Content.Server.Disposal.Tube
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popups = default!;
|
||||
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly DisposableSystem _disposableSystem = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosSystem = default!;
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DisposalTubeComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<DisposalTubeComponent, ComponentRemove>(OnComponentRemove);
|
||||
|
||||
SubscribeLocalEvent<DisposalTubeComponent, AnchorStateChangedEvent>(OnAnchorChange);
|
||||
SubscribeLocalEvent<DisposalTubeComponent, ContainerRelayMovementEntityEvent>(OnRelayMovement);
|
||||
SubscribeLocalEvent<DisposalTubeComponent, BreakageEventArgs>(OnBreak);
|
||||
@@ -44,17 +58,90 @@ namespace Content.Server.Disposal.Tube
|
||||
SubscribeLocalEvent<DisposalJunctionComponent, GetDisposalsConnectableDirectionsEvent>(OnGetJunctionConnectableDirections);
|
||||
SubscribeLocalEvent<DisposalJunctionComponent, GetDisposalsNextDirectionEvent>(OnGetJunctionNextDirection);
|
||||
|
||||
SubscribeLocalEvent<DisposalRouterComponent, ComponentRemove>(OnComponentRemove);
|
||||
SubscribeLocalEvent<DisposalRouterComponent, GetDisposalsConnectableDirectionsEvent>(OnGetRouterConnectableDirections);
|
||||
SubscribeLocalEvent<DisposalRouterComponent, GetDisposalsNextDirectionEvent>(OnGetRouterNextDirection);
|
||||
|
||||
SubscribeLocalEvent<DisposalTransitComponent, GetDisposalsConnectableDirectionsEvent>(OnGetTransitConnectableDirections);
|
||||
SubscribeLocalEvent<DisposalTransitComponent, GetDisposalsNextDirectionEvent>(OnGetTransitNextDirection);
|
||||
|
||||
SubscribeLocalEvent<DisposalTaggerComponent, ComponentRemove>(OnComponentRemove);
|
||||
SubscribeLocalEvent<DisposalTaggerComponent, GetDisposalsConnectableDirectionsEvent>(OnGetTaggerConnectableDirections);
|
||||
SubscribeLocalEvent<DisposalTaggerComponent, GetDisposalsNextDirectionEvent>(OnGetTaggerNextDirection);
|
||||
|
||||
SubscribeLocalEvent<DisposalRouterComponent, ActivatableUIOpenAttemptEvent>(OnOpenRouterUIAttempt);
|
||||
SubscribeLocalEvent<DisposalTaggerComponent, ActivatableUIOpenAttemptEvent>(OnOpenTaggerUIAttempt);
|
||||
|
||||
SubscribeLocalEvent<DisposalRouterComponent, SharedDisposalRouterComponent.UiActionMessage>(OnUiAction);
|
||||
SubscribeLocalEvent<DisposalTaggerComponent, SharedDisposalTaggerComponent.UiActionMessage>(OnUiAction);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Handles ui messages from the client. For things such as button presses
|
||||
/// which interact with the world and require server action.
|
||||
/// </summary>
|
||||
/// <param name="msg">A user interface message from the client.</param>
|
||||
private void OnUiAction(EntityUid uid, DisposalTaggerComponent tagger, SharedDisposalTaggerComponent.UiActionMessage msg)
|
||||
{
|
||||
if (!DisposalTaggerUiKey.Key.Equals(msg.UiKey))
|
||||
return;
|
||||
if (TryComp<PhysicsComponent>(uid, out var physBody) && physBody.BodyType != BodyType.Static)
|
||||
return;
|
||||
|
||||
//Check for correct message and ignore maleformed strings
|
||||
if (msg.Action == SharedDisposalTaggerComponent.UiAction.Ok && SharedDisposalTaggerComponent.TagRegex.IsMatch(msg.Tag))
|
||||
{
|
||||
tagger.Tag = msg.Tag;
|
||||
_audioSystem.PlayPvs(tagger.ClickSound, uid, AudioParams.Default.WithVolume(-2f));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Handles ui messages from the client. For things such as button presses
|
||||
/// which interact with the world and require server action.
|
||||
/// </summary>
|
||||
/// <param name="msg">A user interface message from the client.</param>
|
||||
private void OnUiAction(EntityUid uid, DisposalRouterComponent router, SharedDisposalRouterComponent.UiActionMessage msg)
|
||||
{
|
||||
if (!DisposalRouterUiKey.Key.Equals(msg.UiKey))
|
||||
return;
|
||||
if (!EntityManager.EntityExists(msg.Session.AttachedEntity))
|
||||
return;
|
||||
if (TryComp<PhysicsComponent>(uid, out var physBody) && physBody.BodyType != BodyType.Static)
|
||||
return;
|
||||
|
||||
//Check for correct message and ignore maleformed strings
|
||||
if (msg.Action == SharedDisposalRouterComponent.UiAction.Ok && SharedDisposalRouterComponent.TagRegex.IsMatch(msg.Tags))
|
||||
{
|
||||
router.Tags.Clear();
|
||||
foreach (var tag in msg.Tags.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
router.Tags.Add(tag.Trim());
|
||||
_audioSystem.PlayPvs(router.ClickSound, uid, AudioParams.Default.WithVolume(-2f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnComponentInit(EntityUid uid, DisposalTubeComponent tube, ComponentInit args)
|
||||
{
|
||||
tube.Contents = _containerSystem.EnsureContainer<Container>(uid, tube.ContainerId);
|
||||
}
|
||||
|
||||
private void OnComponentRemove(EntityUid uid, DisposalTubeComponent tube, ComponentRemove args)
|
||||
{
|
||||
DisconnectTube(uid, tube);
|
||||
}
|
||||
|
||||
private void OnComponentRemove(EntityUid uid, DisposalTaggerComponent tagger, ComponentRemove args)
|
||||
{
|
||||
_uiSystem.TryCloseAll(uid, DisposalTaggerUiKey.Key);
|
||||
}
|
||||
|
||||
private void OnComponentRemove(EntityUid uid, DisposalRouterComponent tagger, ComponentRemove args)
|
||||
{
|
||||
_uiSystem.TryCloseAll(uid, DisposalRouterUiKey.Key);
|
||||
}
|
||||
|
||||
private void OnGetBendConnectableDirections(EntityUid uid, DisposalBendComponent component, ref GetDisposalsConnectableDirectionsEvent args)
|
||||
@@ -62,7 +149,7 @@ namespace Content.Server.Disposal.Tube
|
||||
var direction = Transform(uid).LocalRotation;
|
||||
var side = new Angle(MathHelper.DegreesToRadians(direction.Degrees - 90));
|
||||
|
||||
args.Connectable = new[] {direction.GetDir(), side.GetDir()};
|
||||
args.Connectable = new[] { direction.GetDir(), side.GetDir() };
|
||||
}
|
||||
|
||||
private void OnGetBendNextDirection(EntityUid uid, DisposalBendComponent component, ref GetDisposalsNextDirectionEvent args)
|
||||
@@ -83,7 +170,7 @@ namespace Content.Server.Disposal.Tube
|
||||
|
||||
private void OnGetEntryConnectableDirections(EntityUid uid, DisposalEntryComponent component, ref GetDisposalsConnectableDirectionsEvent args)
|
||||
{
|
||||
args.Connectable = new[] {Transform(uid).LocalRotation.GetDir()};
|
||||
args.Connectable = new[] { Transform(uid).LocalRotation.GetDir() };
|
||||
}
|
||||
|
||||
private void OnGetEntryNextDirection(EntityUid uid, DisposalEntryComponent component, ref GetDisposalsNextDirectionEvent args)
|
||||
@@ -150,7 +237,7 @@ namespace Content.Server.Disposal.Tube
|
||||
var rotation = Transform(uid).LocalRotation;
|
||||
var opposite = new Angle(rotation.Theta + Math.PI);
|
||||
|
||||
args.Connectable = new[] {rotation.GetDir(), opposite.GetDir()};
|
||||
args.Connectable = new[] { rotation.GetDir(), opposite.GetDir() };
|
||||
}
|
||||
|
||||
private void OnGetTransitNextDirection(EntityUid uid, DisposalTransitComponent component, ref GetDisposalsNextDirectionEvent args)
|
||||
@@ -183,7 +270,7 @@ namespace Content.Server.Disposal.Tube
|
||||
|
||||
private void OnDeconstruct(EntityUid uid, DisposalTubeComponent component, ConstructionBeforeDeleteEvent args)
|
||||
{
|
||||
component.Disconnect();
|
||||
DisconnectTube(uid, component);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, DisposalTubeComponent component, ComponentStartup args)
|
||||
@@ -199,19 +286,19 @@ namespace Content.Server.Disposal.Tube
|
||||
}
|
||||
|
||||
component.LastClang = _gameTiming.CurTime;
|
||||
SoundSystem.Play(component.ClangSound.GetSound(), Filter.Pvs(uid), uid);
|
||||
_audioSystem.PlayPvs(component.ClangSound, uid);
|
||||
}
|
||||
|
||||
private void OnBreak(EntityUid uid, DisposalTubeComponent component, BreakageEventArgs args)
|
||||
{
|
||||
component.Disconnect();
|
||||
DisconnectTube(uid, component);
|
||||
}
|
||||
|
||||
private void OnOpenRouterUIAttempt(EntityUid uid, DisposalRouterComponent router, ActivatableUIOpenAttemptEvent args)
|
||||
{
|
||||
if (!TryComp<HandsComponent>(args.User, out var hands))
|
||||
{
|
||||
uid.PopupMessage(args.User, Loc.GetString("disposal-router-window-tag-input-activate-no-hands"));
|
||||
_popups.PopupClient(Loc.GetString("disposal-router-window-tag-input-activate-no-hands"), uid, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -221,14 +308,14 @@ namespace Content.Server.Disposal.Tube
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
UpdateRouterUserInterface(router);
|
||||
UpdateRouterUserInterface(uid, router);
|
||||
}
|
||||
|
||||
private void OnOpenTaggerUIAttempt(EntityUid uid, DisposalTaggerComponent tagger, ActivatableUIOpenAttemptEvent args)
|
||||
{
|
||||
if (!TryComp<HandsComponent>(args.User, out var hands))
|
||||
{
|
||||
uid.PopupMessage(args.User, Loc.GetString("disposal-tagger-window-activate-no-hands"));
|
||||
_popups.PopupClient(Loc.GetString("disposal-tagger-window-activate-no-hands"), uid, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -238,18 +325,21 @@ namespace Content.Server.Disposal.Tube
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
tagger.UserInterface?.SetState(new SharedDisposalTaggerComponent.DisposalTaggerUserInterfaceState(tagger.Tag));
|
||||
if (_uiSystem.TryGetUi(uid, SharedDisposalTaggerComponent.DisposalTaggerUiKey.Key, out var bui))
|
||||
_uiSystem.SetUiState(bui, new SharedDisposalTaggerComponent.DisposalTaggerUserInterfaceState(tagger.Tag));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets component data to be used to update the user interface client-side.
|
||||
/// </summary>
|
||||
/// <returns>Returns a <see cref="SharedDisposalRouterComponent.DisposalRouterUserInterfaceState"/></returns>
|
||||
private void UpdateRouterUserInterface(DisposalRouterComponent router)
|
||||
private void UpdateRouterUserInterface(EntityUid uid, DisposalRouterComponent router)
|
||||
{
|
||||
var bui = _uiSystem.GetUiOrNull(uid, SharedDisposalTaggerComponent.DisposalTaggerUiKey.Key);
|
||||
if (router.Tags.Count <= 0)
|
||||
{
|
||||
router.UserInterface?.SetState(new SharedDisposalRouterComponent.DisposalRouterUserInterfaceState(""));
|
||||
if (bui is not null)
|
||||
_uiSystem.SetUiState(bui, new SharedDisposalTaggerComponent.DisposalTaggerUserInterfaceState(""));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -263,7 +353,8 @@ namespace Content.Server.Disposal.Tube
|
||||
|
||||
taglist.Remove(taglist.Length - 2, 2);
|
||||
|
||||
router.UserInterface?.SetState(new SharedDisposalRouterComponent.DisposalRouterUserInterfaceState(taglist.ToString()));
|
||||
if (bui is not null)
|
||||
_uiSystem.SetUiState(bui, new SharedDisposalTaggerComponent.DisposalTaggerUserInterfaceState(taglist.ToString()));
|
||||
}
|
||||
|
||||
private void OnAnchorChange(EntityUid uid, DisposalTubeComponent component, ref AnchorStateChangedEvent args)
|
||||
@@ -275,25 +366,25 @@ namespace Content.Server.Disposal.Tube
|
||||
{
|
||||
if (anchored)
|
||||
{
|
||||
component.Connect();
|
||||
ConnectTube(uid, component);
|
||||
|
||||
// TODO this visual data should just generalized into some anchored-visuals system/comp, this has nothing to do with disposal tubes.
|
||||
_appearanceSystem.SetData(uid, DisposalTubeVisuals.VisualState, DisposalTubeVisualState.Anchored);
|
||||
}
|
||||
else
|
||||
{
|
||||
component.Disconnect();
|
||||
DisconnectTube(uid, component);
|
||||
_appearanceSystem.SetData(uid, DisposalTubeVisuals.VisualState, DisposalTubeVisualState.Free);
|
||||
}
|
||||
}
|
||||
|
||||
public DisposalTubeComponent? NextTubeFor(EntityUid target, Direction nextDirection, DisposalTubeComponent? targetTube = null)
|
||||
public EntityUid? NextTubeFor(EntityUid target, Direction nextDirection, DisposalTubeComponent? targetTube = null)
|
||||
{
|
||||
if (!Resolve(target, ref targetTube))
|
||||
return null;
|
||||
var oppositeDirection = nextDirection.GetOpposite();
|
||||
|
||||
var xform = Transform(targetTube.Owner);
|
||||
var xform = Transform(target);
|
||||
if (!_mapManager.TryGetGrid(xform.GridUid, out var grid))
|
||||
return null;
|
||||
|
||||
@@ -315,12 +406,39 @@ namespace Content.Server.Disposal.Tube
|
||||
continue;
|
||||
}
|
||||
|
||||
return tube;
|
||||
return entity;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void ConnectTube(EntityUid _, DisposalTubeComponent tube)
|
||||
{
|
||||
if (tube.Connected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tube.Connected = true;
|
||||
}
|
||||
|
||||
|
||||
public void DisconnectTube(EntityUid _, DisposalTubeComponent tube)
|
||||
{
|
||||
if (!tube.Connected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tube.Connected = false;
|
||||
|
||||
var query = GetEntityQuery<DisposalHolderComponent>();
|
||||
foreach (var entity in tube.Contents.ContainedEntities.ToArray())
|
||||
{
|
||||
if (query.TryGetComponent(entity, out var holder))
|
||||
_disposableSystem.ExitDisposals(entity, holder);
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanConnect(EntityUid tubeId, DisposalTubeComponent tube, Direction direction)
|
||||
{
|
||||
@@ -334,7 +452,7 @@ namespace Content.Server.Disposal.Tube
|
||||
return ev.Connectable.Contains(direction);
|
||||
}
|
||||
|
||||
public void PopupDirections(EntityUid tubeId, DisposalTubeComponent tube, EntityUid recipient)
|
||||
public void PopupDirections(EntityUid tubeId, DisposalTubeComponent _, EntityUid recipient)
|
||||
{
|
||||
var ev = new GetDisposalsConnectableDirectionsEvent();
|
||||
RaiseLocalEvent(tubeId, ref ev);
|
||||
@@ -342,5 +460,28 @@ namespace Content.Server.Disposal.Tube
|
||||
|
||||
_popups.PopupEntity(Loc.GetString("disposal-tube-component-popup-directions-text", ("directions", directions)), tubeId, recipient);
|
||||
}
|
||||
|
||||
public bool TryInsert(EntityUid uid, DisposalUnitComponent from, IEnumerable<string>? tags = default, DisposalEntryComponent? entry = null)
|
||||
{
|
||||
if (!Resolve(uid, ref entry))
|
||||
return false;
|
||||
|
||||
var xform = Transform(uid);
|
||||
var holder = Spawn(DisposalEntryComponent.HolderPrototypeId, xform.MapPosition);
|
||||
var holderComponent = Comp<DisposalHolderComponent>(holder);
|
||||
|
||||
foreach (var entity in from.Container.ContainedEntities.ToArray())
|
||||
{
|
||||
_disposableSystem.TryInsert(holder, entity, holderComponent);
|
||||
}
|
||||
|
||||
_atmosSystem.Merge(holderComponent.Air, from.Air);
|
||||
from.Air.Clear();
|
||||
|
||||
if (tags != default)
|
||||
holderComponent.Tags.UnionWith(tags);
|
||||
|
||||
return _disposableSystem.EnterTube(holder, uid, holderComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Content.Server.Disposal.Unit.Components;
|
||||
|
||||
namespace Content.Server.Disposal.Tube;
|
||||
namespace Content.Server.Disposal.Tube;
|
||||
|
||||
[ByRefEvent]
|
||||
public record struct GetDisposalsConnectableDirectionsEvent
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.Disposal.Tube.Components;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Item;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
|
||||
namespace Content.Server.Disposal.Unit.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class DisposalHolderComponent : Component, IGasMixtureHolder
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
public Container Container = null!;
|
||||
|
||||
/// <summary>
|
||||
@@ -29,7 +23,7 @@ namespace Content.Server.Disposal.Unit.Components
|
||||
public float TimeLeft { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public DisposalTubeComponent? PreviousTube { get; set; }
|
||||
public EntityUid? PreviousTube { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public Direction PreviousDirection { get; set; } = Direction.Invalid;
|
||||
@@ -38,7 +32,7 @@ namespace Content.Server.Disposal.Unit.Components
|
||||
public Direction PreviousDirectionFrom => (PreviousDirection == Direction.Invalid) ? Direction.Invalid : PreviousDirection.GetOpposite();
|
||||
|
||||
[ViewVariables]
|
||||
public DisposalTubeComponent? CurrentTube { get; set; }
|
||||
public EntityUid? CurrentTube { get; set; }
|
||||
|
||||
// CurrentDirection is not null when CurrentTube isn't null.
|
||||
[ViewVariables]
|
||||
@@ -55,39 +49,6 @@ namespace Content.Server.Disposal.Unit.Components
|
||||
public HashSet<string> Tags { get; set; } = new();
|
||||
|
||||
[DataField("air")]
|
||||
public GasMixture Air { get; set; } = new (70);
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Container = ContainerHelpers.EnsureContainer<Container>(Owner, nameof(DisposalHolderComponent));
|
||||
}
|
||||
|
||||
private bool CanInsert(EntityUid entity)
|
||||
{
|
||||
if (!Container.CanInsert(entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _entMan.HasComponent<ItemComponent>(entity) ||
|
||||
_entMan.HasComponent<BodyComponent>(entity);
|
||||
}
|
||||
|
||||
public bool TryInsert(EntityUid entity)
|
||||
{
|
||||
if (!CanInsert(entity) || !Container.Insert(entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_entMan.TryGetComponent(entity, out PhysicsComponent? physics))
|
||||
{
|
||||
_entMan.System<SharedPhysicsSystem>().SetCanCollide(entity, false, body: physics);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
public GasMixture Air { get; set; } = new(70);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,5 +67,11 @@ namespace Content.Server.Disposal.Unit.Components
|
||||
|
||||
[DataField("air")]
|
||||
public GasMixture Air { get; set; } = new(Atmospherics.CellVolume);
|
||||
|
||||
[ViewVariables]
|
||||
public TimeSpan NextFlush = TimeSpan.MaxValue;
|
||||
|
||||
[ViewVariables]
|
||||
public bool AutoFlushing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Disposal.Tube;
|
||||
using Content.Server.Disposal.Tube.Components;
|
||||
using Content.Server.Disposal.Unit.Components;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Item;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
@@ -18,6 +21,50 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
[Dependency] private readonly DisposalTubeSystem _disposalTubeSystem = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DisposalHolderComponent, ComponentStartup>(OnComponentStartup);
|
||||
}
|
||||
|
||||
private void OnComponentStartup(EntityUid uid, DisposalHolderComponent holder, ComponentStartup args)
|
||||
{
|
||||
holder.Container = _containerSystem.EnsureContainer<Container>(uid, nameof(DisposalHolderComponent));
|
||||
}
|
||||
|
||||
public bool TryInsert(EntityUid uid, EntityUid toInsert, DisposalHolderComponent? holder = null)
|
||||
{
|
||||
if (!Resolve(uid, ref holder))
|
||||
return false;
|
||||
if (!CanInsert(uid, toInsert, holder))
|
||||
return false;
|
||||
|
||||
if (!holder.Container.Insert(toInsert, EntityManager))
|
||||
return false;
|
||||
|
||||
if (TryComp<PhysicsComponent>(toInsert, out var physBody))
|
||||
_physicsSystem.SetCanCollide(toInsert, false, body: physBody);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CanInsert(EntityUid uid, EntityUid toInsert, DisposalHolderComponent? holder = null)
|
||||
{
|
||||
if (!Resolve(uid, ref holder))
|
||||
return false;
|
||||
|
||||
if (!holder.Container.CanInsert(toInsert))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return HasComp<ItemComponent>(toInsert) ||
|
||||
HasComp<BodyComponent>(toInsert);
|
||||
}
|
||||
|
||||
public void ExitDisposals(EntityUid uid, DisposalHolderComponent? holder = null, TransformComponent? holderTransform = null)
|
||||
{
|
||||
@@ -28,7 +75,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
return;
|
||||
if (holder.IsExitingDisposals)
|
||||
{
|
||||
Logger.ErrorS("c.s.disposal.holder", "Tried exiting disposals twice. This should never happen.");
|
||||
Log.Error("Tried exiting disposals twice. This should never happen.");
|
||||
return;
|
||||
}
|
||||
holder.IsExitingDisposals = true;
|
||||
@@ -65,7 +112,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
if (duc != null)
|
||||
duc.Container.Insert(entity, EntityManager, xform, meta: meta);
|
||||
else
|
||||
xform.AttachToGridOrMap();
|
||||
_xformSystem.AttachToGridOrMap(entity, xform);
|
||||
|
||||
if (EntityManager.TryGetComponent(entity, out PhysicsComponent? physics))
|
||||
{
|
||||
@@ -78,7 +125,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
_disposalUnitSystem.TryEjectContents(disposalId.Value, duc);
|
||||
}
|
||||
|
||||
if (_atmosphereSystem.GetContainingMixture(uid, false, true) is {} environment)
|
||||
if (_atmosphereSystem.GetContainingMixture(uid, false, true) is { } environment)
|
||||
{
|
||||
_atmosphereSystem.Merge(environment, holder.Air);
|
||||
holder.Air.Clear();
|
||||
@@ -94,7 +141,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
return false;
|
||||
if (holder.IsExitingDisposals)
|
||||
{
|
||||
Logger.ErrorS("c.s.disposal.holder", "Tried entering tube after exiting disposals. This should never happen.");
|
||||
Log.Error("Tried entering tube after exiting disposals. This should never happen.");
|
||||
return false;
|
||||
}
|
||||
if (!Resolve(toUid, ref to, ref toTransform))
|
||||
@@ -106,11 +153,11 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
foreach (var ent in holder.Container.ContainedEntities)
|
||||
{
|
||||
var comp = EnsureComp<BeingDisposedComponent>(ent);
|
||||
comp.Holder = holder.Owner;
|
||||
comp.Holder = holderUid;
|
||||
}
|
||||
|
||||
// Insert into next tube
|
||||
if (!to.Contents.Insert(holder.Owner))
|
||||
if (!to.Contents.Insert(holderUid))
|
||||
{
|
||||
ExitDisposals(holderUid, holder, holderTransform);
|
||||
return false;
|
||||
@@ -121,7 +168,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
holder.PreviousTube = holder.CurrentTube;
|
||||
holder.PreviousDirection = holder.CurrentDirection;
|
||||
}
|
||||
holder.CurrentTube = to;
|
||||
holder.CurrentTube = toUid;
|
||||
var ev = new GetDisposalsNextDirectionEvent(holder);
|
||||
RaiseLocalEvent(toUid, ref ev);
|
||||
holder.CurrentDirection = ev.Next;
|
||||
@@ -140,13 +187,14 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
foreach (var comp in EntityManager.EntityQuery<DisposalHolderComponent>())
|
||||
var query = EntityQueryEnumerator<DisposalHolderComponent>();
|
||||
while (query.MoveNext(out var uid, out var holder))
|
||||
{
|
||||
UpdateComp(comp, frameTime);
|
||||
UpdateComp(uid, holder, frameTime);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateComp(DisposalHolderComponent holder, float frameTime)
|
||||
private void UpdateComp(EntityUid uid, DisposalHolderComponent holder, float frameTime)
|
||||
{
|
||||
while (frameTime > 0)
|
||||
{
|
||||
@@ -159,40 +207,39 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
holder.TimeLeft -= time;
|
||||
frameTime -= time;
|
||||
|
||||
var currentTube = holder.CurrentTube;
|
||||
if (currentTube == null || currentTube.Deleted)
|
||||
if (!EntityManager.EntityExists(holder.CurrentTube))
|
||||
{
|
||||
ExitDisposals((holder).Owner);
|
||||
ExitDisposals(uid, holder);
|
||||
break;
|
||||
}
|
||||
|
||||
var currentTube = holder.CurrentTube!.Value;
|
||||
if (holder.TimeLeft > 0)
|
||||
{
|
||||
var progress = 1 - holder.TimeLeft / holder.StartingTime;
|
||||
var origin = EntityManager.GetComponent<TransformComponent>(currentTube.Owner).Coordinates;
|
||||
var origin = Transform(currentTube).Coordinates;
|
||||
var destination = holder.CurrentDirection.ToVec();
|
||||
var newPosition = destination * progress;
|
||||
|
||||
// This is some supreme shit code.
|
||||
EntityManager.GetComponent<TransformComponent>(holder.Owner).Coordinates = origin.Offset(newPosition).WithEntityId(currentTube.Owner);
|
||||
|
||||
_xformSystem.SetCoordinates(uid, origin.Offset(newPosition).WithEntityId(currentTube));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Past this point, we are performing inter-tube transfer!
|
||||
// Remove current tube content
|
||||
currentTube.Contents.Remove(holder.Owner, reparent: false, force: true);
|
||||
Comp<DisposalTubeComponent>(currentTube).Contents.Remove(uid, reparent: false, force: true);
|
||||
|
||||
// Find next tube
|
||||
var nextTube = _disposalTubeSystem.NextTubeFor(currentTube.Owner, holder.CurrentDirection);
|
||||
if (nextTube == null || nextTube.Deleted)
|
||||
var nextTube = _disposalTubeSystem.NextTubeFor(currentTube, holder.CurrentDirection);
|
||||
if (!EntityManager.EntityExists(nextTube))
|
||||
{
|
||||
ExitDisposals((holder).Owner);
|
||||
ExitDisposals(uid, holder);
|
||||
break;
|
||||
}
|
||||
|
||||
// Perform remainder of entry process
|
||||
if (!EnterTube((holder).Owner, nextTube.Owner, holder))
|
||||
if (!EnterTube(uid, nextTube!.Value, holder))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Disposal.Tube;
|
||||
using Content.Server.Disposal.Tube.Components;
|
||||
using Content.Server.Disposal.Unit.Components;
|
||||
using Content.Server.Popups;
|
||||
@@ -29,6 +30,7 @@ using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
@@ -48,6 +50,9 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly PowerReceiverSystem _power = default!;
|
||||
[Dependency] private readonly DisposalTubeSystem _disposalTubeSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -92,11 +97,13 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
if (component.Container.ContainedEntities.Count > 0)
|
||||
{
|
||||
// Verbs to flush the unit
|
||||
AlternativeVerb flushVerb = new();
|
||||
flushVerb.Act = () => Engage(uid, component);
|
||||
flushVerb.Text = Loc.GetString("disposal-flush-verb-get-data-text");
|
||||
flushVerb.Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png"));
|
||||
flushVerb.Priority = 1;
|
||||
AlternativeVerb flushVerb = new()
|
||||
{
|
||||
Act = () => Engage(uid, component),
|
||||
Text = Loc.GetString("disposal-flush-verb-get-data-text"),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png")),
|
||||
Priority = 1,
|
||||
};
|
||||
args.Verbs.Add(flushVerb);
|
||||
|
||||
// Verb to eject the contents
|
||||
@@ -143,7 +150,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
if (!_actionBlockerSystem.CanDrop(args.User))
|
||||
return;
|
||||
|
||||
if (!CanInsert(component, args.Using.Value))
|
||||
if (!CanInsert(uid, component, args.Using.Value))
|
||||
return;
|
||||
|
||||
InteractionVerb insertVerb = new()
|
||||
@@ -186,10 +193,11 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
foreach (var (_, comp) in EntityQuery<ActiveDisposalUnitComponent, DisposalUnitComponent>())
|
||||
|
||||
var query = EntityQueryEnumerator<ActiveDisposalUnitComponent, DisposalUnitComponent>();
|
||||
while (query.MoveNext(out var uid, out var _, out var unit))
|
||||
{
|
||||
var uid = comp.Owner;
|
||||
if (!Update(uid, comp, frameTime))
|
||||
if (!Update(uid, unit, frameTime))
|
||||
continue;
|
||||
|
||||
RemComp<ActiveDisposalUnitComponent>(uid);
|
||||
@@ -199,7 +207,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
#region UI Handlers
|
||||
private void OnUiButtonPressed(EntityUid uid, DisposalUnitComponent component, SharedDisposalUnitComponent.UiButtonPressedMessage args)
|
||||
{
|
||||
if (args.Session.AttachedEntity is not {Valid: true} player)
|
||||
if (args.Session.AttachedEntity is not { Valid: true } player)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -218,7 +226,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
_power.TogglePower(uid, user: args.Session.AttachedEntity);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
throw new ArgumentOutOfRangeException($"{ToPrettyString(player):player} attempted to hit a nonexistant button on {ToPrettyString(uid)}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +267,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CanInsert(component, args.Used) || !_handsSystem.TryDropIntoContainer(args.User, args.Used, component.Container))
|
||||
if (!CanInsert(uid, component, args.Used) || !_handsSystem.TryDropIntoContainer(args.User, args.Used, component.Container))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -274,7 +282,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
/// </summary>
|
||||
private void HandleThrowCollide(EntityUid uid, DisposalUnitComponent component, ThrowHitByEvent args)
|
||||
{
|
||||
if (!CanInsert(component, args.Thrown) ||
|
||||
if (!CanInsert(uid, component, args.Thrown) ||
|
||||
_robustRandom.NextDouble() > 0.75 ||
|
||||
!component.Container.Insert(args.Thrown))
|
||||
{
|
||||
@@ -295,7 +303,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
|
||||
if (!HasComp<AnchorableComponent>(uid))
|
||||
{
|
||||
Logger.WarningS("VitalComponentMissing", $"Disposal unit {uid} is missing an {nameof(AnchorableComponent)}");
|
||||
Log.Warning($"Disposal unit {uid} is missing an {nameof(AnchorableComponent)}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,8 +315,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
}
|
||||
|
||||
_ui.TryCloseAll(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key);
|
||||
component.AutomaticEngageToken?.Cancel();
|
||||
component.AutomaticEngageToken = null;
|
||||
component.NextFlush = TimeSpan.MaxValue;
|
||||
|
||||
component.Container = null!;
|
||||
RemComp<ActiveDisposalUnitComponent>(uid);
|
||||
@@ -324,11 +331,10 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
// TODO: Need to check the other stuff.
|
||||
if (!args.Powered)
|
||||
{
|
||||
component.AutomaticEngageToken?.Cancel();
|
||||
component.AutomaticEngageToken = null;
|
||||
component.NextFlush = TimeSpan.MaxValue;
|
||||
}
|
||||
|
||||
HandleStateChange(uid, component, args.Powered && component.State == SharedDisposalUnitComponent.PressureState.Pressurizing);
|
||||
HandleStateChange(uid, component, args.Powered && (component.State == SharedDisposalUnitComponent.PressureState.Pressurizing || component.NextFlush != TimeSpan.MaxValue));
|
||||
UpdateVisualState(uid, component);
|
||||
UpdateInterface(uid, component, args.Powered);
|
||||
|
||||
@@ -341,7 +347,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
/// <summary>
|
||||
/// Add or remove this disposal from the active ones for updating.
|
||||
/// </summary>
|
||||
public void HandleStateChange(EntityUid uid, DisposalUnitComponent component, bool active)
|
||||
public void HandleStateChange(EntityUid uid, DisposalUnitComponent _, bool active)
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
@@ -416,20 +422,27 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
if (component.State == SharedDisposalUnitComponent.PressureState.Pressurizing)
|
||||
{
|
||||
var oldTimeElapsed = oldPressure / PressurePerSecond;
|
||||
if (oldTimeElapsed < component.FlushTime && (oldTimeElapsed + frameTime) >= component.FlushTime)
|
||||
if (oldTimeElapsed < component.FlushTime && oldTimeElapsed + frameTime >= component.FlushTime)
|
||||
{
|
||||
// We've crossed over the amount of time it takes to flush. This will switch the
|
||||
// visuals over to a 'Charging' state.
|
||||
UpdateVisualState(uid, component);
|
||||
}
|
||||
}
|
||||
else if (component.State == SharedDisposalUnitComponent.PressureState.Ready && component.NextFlush < _gameTiming.CurTime)
|
||||
{
|
||||
if (!TryFlush(uid, component) && component.AutoFlushing)
|
||||
TryQueueEngage(uid, component);
|
||||
else
|
||||
component.AutoFlushing = false;
|
||||
}
|
||||
|
||||
Box2? disposalsBounds = null;
|
||||
var count = component.RecentlyEjected.Count;
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
if (!TryComp(uid, out PhysicsComponent? disposalsBody))
|
||||
if (!HasComp<PhysicsComponent>(uid))
|
||||
{
|
||||
component.RecentlyEjected.Clear();
|
||||
}
|
||||
@@ -442,8 +455,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
for (var i = component.RecentlyEjected.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var ejectedId = component.RecentlyEjected[i];
|
||||
if (Exists(ejectedId) &&
|
||||
TryComp(ejectedId, out PhysicsComponent? body))
|
||||
if (HasComp<PhysicsComponent>(ejectedId))
|
||||
{
|
||||
// TODO: We need to use a specific collision method (which sloth hasn't coded yet) for actual bounds overlaps.
|
||||
// TODO: Come do this sloth :^)
|
||||
@@ -460,7 +472,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
if (count != component.RecentlyEjected.Count)
|
||||
Dirty(component);
|
||||
|
||||
return state == SharedDisposalUnitComponent.PressureState.Ready && component.RecentlyEjected.Count == 0;
|
||||
return state == SharedDisposalUnitComponent.PressureState.Ready && component.NextFlush == TimeSpan.MaxValue && component.RecentlyEjected.Count == 0;
|
||||
}
|
||||
|
||||
public bool TryInsert(EntityUid unitId, EntityUid toInsertId, EntityUid? userId, DisposalUnitComponent? unit = null)
|
||||
@@ -474,7 +486,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CanInsert(unit, toInsertId))
|
||||
if (!CanInsert(unitId, unit, toInsertId))
|
||||
return false;
|
||||
|
||||
var delay = userId == toInsertId ? unit.EntryDelay : unit.DraggedEntryDelay;
|
||||
@@ -507,6 +519,8 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
return false;
|
||||
}
|
||||
|
||||
component.NextFlush = TimeSpan.MaxValue;
|
||||
|
||||
//Allows the MailingUnitSystem to add tags or prevent flushing
|
||||
var beforeFlushArgs = new BeforeDisposalFlushEvent();
|
||||
RaiseLocalEvent(uid, beforeFlushArgs);
|
||||
@@ -534,17 +548,16 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
var entryComponent = Comp<DisposalEntryComponent>(entry);
|
||||
var indices = _transformSystem.GetGridOrMapTilePosition(uid, xform);
|
||||
|
||||
if (_atmosSystem.GetTileMixture(xform.GridUid, xform.MapUid, indices, true) is {Temperature: > 0} environment)
|
||||
if (_atmosSystem.GetTileMixture(xform.GridUid, xform.MapUid, indices, true) is { Temperature: > 0f } environment)
|
||||
{
|
||||
var transferMoles = 0.1f * (0.25f * Atmospherics.OneAtmosphere * 1.01f - air.Pressure) * air.Volume / (environment.Temperature * Atmospherics.R);
|
||||
|
||||
component.Air = environment.Remove(transferMoles);
|
||||
}
|
||||
|
||||
entryComponent.TryInsert(component, beforeFlushArgs.Tags);
|
||||
_disposalTubeSystem.TryInsert(entry, component, beforeFlushArgs.Tags);
|
||||
|
||||
component.AutomaticEngageToken?.Cancel();
|
||||
component.AutomaticEngageToken = null;
|
||||
component.NextFlush = TimeSpan.MaxValue;
|
||||
|
||||
if (!component.DisablePressure)
|
||||
{
|
||||
@@ -642,8 +655,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
|
||||
if (component.Container.ContainedEntities.Count == 0)
|
||||
{
|
||||
component.AutomaticEngageToken?.Cancel();
|
||||
component.AutomaticEngageToken = null;
|
||||
component.NextFlush = TimeSpan.MaxValue;
|
||||
}
|
||||
|
||||
if (!component.RecentlyEjected.Contains(toRemove))
|
||||
@@ -669,7 +681,8 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
|
||||
if (CanFlush(uid, component))
|
||||
{
|
||||
uid.SpawnTimer(component.FlushDelay, () => TryFlush(uid, component));
|
||||
component.NextFlush = _gameTiming.CurTime + component.FlushDelay;
|
||||
EnsureComp<ActiveDisposalUnitComponent>(uid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,9 +704,9 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanInsert(SharedDisposalUnitComponent component, EntityUid entity)
|
||||
public override bool CanInsert(EntityUid uid, SharedDisposalUnitComponent component, EntityUid entity)
|
||||
{
|
||||
if (!base.CanInsert(component, entity) || component is not DisposalUnitComponent serverComp)
|
||||
if (!base.CanInsert(uid, component, entity) || component is not DisposalUnitComponent serverComp)
|
||||
return false;
|
||||
|
||||
return serverComp.Container.CanInsert(entity);
|
||||
@@ -709,15 +722,10 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
||||
return;
|
||||
}
|
||||
|
||||
component.AutomaticEngageToken = new CancellationTokenSource();
|
||||
component.NextFlush = _gameTiming.CurTime + component.AutomaticEngageTime;
|
||||
component.AutoFlushing = true;
|
||||
|
||||
uid.SpawnTimer(component.AutomaticEngageTime, () =>
|
||||
{
|
||||
if (!TryFlush(uid, component))
|
||||
{
|
||||
TryQueueEngage(uid, component);
|
||||
}
|
||||
}, component.AutomaticEngageToken.Token);
|
||||
EnsureComp<ActiveDisposalUnitComponent>(uid);
|
||||
}
|
||||
|
||||
public void AfterInsert(EntityUid uid, DisposalUnitComponent component, EntityUid inserted, EntityUid? user = null)
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Content.Server.GameTicking.Commands
|
||||
if (ticker.PlayerGameStatuses.TryGetValue(player.UserId, out var status) &&
|
||||
status != PlayerGameStatus.JoinedGame)
|
||||
{
|
||||
ticker.MakeObserve(player);
|
||||
ticker.JoinAsObserver(player);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Players;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
@@ -21,7 +22,9 @@ namespace Content.Server.GameTicking.Commands
|
||||
}
|
||||
|
||||
var playerMgr = IoCManager.Resolve<IPlayerManager>();
|
||||
var ticker = EntitySystem.Get<GameTicker>();
|
||||
var sysMan = IoCManager.Resolve<IEntitySystemManager>();
|
||||
var ticker = sysMan.GetEntitySystem<GameTicker>();
|
||||
var mind = sysMan.GetEntitySystem<MindSystem>();
|
||||
|
||||
NetUserId userId;
|
||||
if (args.Length == 0)
|
||||
@@ -48,7 +51,7 @@ namespace Content.Server.GameTicking.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
data.ContentData()?.WipeMind();
|
||||
mind.WipeMind(data.ContentData()?.Mind);
|
||||
shell.WriteLine("Player is not currently online, but they will respawn if they come back online");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,11 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameTicking.Presets;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
|
||||
@@ -19,9 +17,6 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
public const float PresetFailedCooldownIncrease = 30f;
|
||||
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
||||
|
||||
public GamePresetPrototype? Preset { get; private set; }
|
||||
|
||||
private bool StartPreset(IPlayerSession[] origReadyPlayers, bool force)
|
||||
@@ -190,7 +185,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
if (mind.VisitingEntity != default)
|
||||
{
|
||||
_mindSystem.UnVisit(mind);
|
||||
_mind.UnVisit(mind);
|
||||
}
|
||||
|
||||
var position = Exists(playerEntity)
|
||||
@@ -208,11 +203,11 @@ namespace Content.Server.GameTicking
|
||||
// + If we're in a mob that is critical, and we're supposed to be able to return if possible,
|
||||
// we're succumbing - the mob is killed. Therefore, character is dead. Ghosting OK.
|
||||
// (If the mob survives, that's a bug. Ghosting is kept regardless.)
|
||||
var canReturn = canReturnGlobal && _mindSystem.IsCharacterDeadPhysically(mind);
|
||||
var canReturn = canReturnGlobal && _mind.IsCharacterDeadPhysically(mind);
|
||||
|
||||
if (canReturnGlobal && TryComp(playerEntity, out MobStateComponent? mobState))
|
||||
{
|
||||
if (_mobStateSystem.IsCritical(playerEntity.Value, mobState))
|
||||
if (_mobState.IsCritical(playerEntity.Value, mobState))
|
||||
{
|
||||
canReturn = true;
|
||||
|
||||
@@ -250,9 +245,9 @@ namespace Content.Server.GameTicking
|
||||
_ghosts.SetCanReturnToBody(ghostComponent, canReturn);
|
||||
|
||||
if (canReturn)
|
||||
_mindSystem.Visit(mind, ghost);
|
||||
_mind.Visit(mind, ghost);
|
||||
else
|
||||
_mindSystem.TransferTo(mind, ghost);
|
||||
_mind.TransferTo(mind, ghost);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,22 +78,6 @@ namespace Content.Server.GameTicking
|
||||
("roundId", RoundId), ("playerCount", playerCount), ("readyCount", readyCount), ("mapName", stationNames.ToString()),("gmTitle", gmTitle),("desc", desc));
|
||||
}
|
||||
|
||||
private TickerLobbyReadyEvent GetStatusSingle(ICommonSession player, PlayerGameStatus gameStatus)
|
||||
{
|
||||
return new (new Dictionary<NetUserId, PlayerGameStatus> { { player.UserId, gameStatus } });
|
||||
}
|
||||
|
||||
private TickerLobbyReadyEvent GetPlayerStatus()
|
||||
{
|
||||
var players = new Dictionary<NetUserId, PlayerGameStatus>();
|
||||
foreach (var player in _playerGameStatuses.Keys)
|
||||
{
|
||||
_playerGameStatuses.TryGetValue(player, out var status);
|
||||
players.Add(player, status);
|
||||
}
|
||||
return new TickerLobbyReadyEvent(players);
|
||||
}
|
||||
|
||||
private TickerLobbyStatusEvent GetStatusMsg(IPlayerSession session)
|
||||
{
|
||||
_playerGameStatuses.TryGetValue(session.UserId, out var status);
|
||||
@@ -160,7 +144,6 @@ namespace Content.Server.GameTicking
|
||||
if (!_playerManager.TryGetSessionById(playerUserId, out var playerSession))
|
||||
continue;
|
||||
RaiseNetworkEvent(GetStatusMsg(playerSession), playerSession.ConnectedClient);
|
||||
RaiseNetworkEvent(GetStatusSingle(playerSession, status));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +163,6 @@ namespace Content.Server.GameTicking
|
||||
var status = ready ? PlayerGameStatus.ReadyToPlay : PlayerGameStatus.NotReadyToPlay;
|
||||
_playerGameStatuses[player.UserId] = ready ? PlayerGameStatus.ReadyToPlay : PlayerGameStatus.NotReadyToPlay;
|
||||
RaiseNetworkEvent(GetStatusMsg(player), player.ConnectedClient);
|
||||
RaiseNetworkEvent(GetStatusSingle(player, status));
|
||||
// update server info to reflect new ready count
|
||||
UpdateInfoText();
|
||||
}
|
||||
|
||||
@@ -27,15 +27,29 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
var session = args.Session;
|
||||
|
||||
if (_mind.TryGetMind(session.UserId, out var mind))
|
||||
{
|
||||
if (args.OldStatus == SessionStatus.Connecting && args.NewStatus == SessionStatus.Connected)
|
||||
mind.Session = session;
|
||||
|
||||
DebugTools.Assert(mind.Session == session);
|
||||
}
|
||||
|
||||
DebugTools.Assert(session.GetMind() == mind);
|
||||
|
||||
switch (args.NewStatus)
|
||||
{
|
||||
case SessionStatus.Connected:
|
||||
{
|
||||
AddPlayerToDb(args.Session.UserId.UserId);
|
||||
|
||||
// Always make sure the client has player data. Mind gets assigned on spawn.
|
||||
// Always make sure the client has player data.
|
||||
if (session.Data.ContentDataUncast == null)
|
||||
session.Data.ContentDataUncast = new PlayerData(session.UserId, args.Session.Name);
|
||||
{
|
||||
var data = new PlayerData(session.UserId, args.Session.Name);
|
||||
data.Mind = mind;
|
||||
session.Data.ContentDataUncast = data;
|
||||
}
|
||||
|
||||
// Make the player actually join the game.
|
||||
// timer time must be > tick length
|
||||
@@ -62,32 +76,30 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
_userDb.ClientConnected(session);
|
||||
|
||||
var data = session.ContentData();
|
||||
|
||||
DebugTools.AssertNotNull(data);
|
||||
|
||||
if (data!.Mind == null)
|
||||
if (mind == null)
|
||||
{
|
||||
if (LobbyEnabled)
|
||||
{
|
||||
PlayerJoinLobby(session);
|
||||
return;
|
||||
}
|
||||
else
|
||||
SpawnWaitDb();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
SpawnWaitDb();
|
||||
if (mind.CurrentEntity == null || Deleted(mind.CurrentEntity))
|
||||
{
|
||||
DebugTools.Assert(mind.CurrentEntity == null, "a mind's current entity was deleted without updating the mind");
|
||||
|
||||
// This player is joining the game with an existing mind, but the mind has no entity.
|
||||
// Their entity was probably deleted sometime while they were disconnected, or they were an observer.
|
||||
// Instead of allowing them to spawn in, we will dump and their existing mind in an observer ghost.
|
||||
SpawnObserverWaitDb();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (data.Mind.CurrentEntity == null)
|
||||
{
|
||||
SpawnWaitDb();
|
||||
}
|
||||
else
|
||||
{
|
||||
session.AttachToEntity(data.Mind.CurrentEntity);
|
||||
PlayerJoinGame(session);
|
||||
}
|
||||
// Simply re-attach to existing entity.
|
||||
session.AttachToEntity(mind.CurrentEntity);
|
||||
PlayerJoinGame(session);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -96,6 +108,8 @@ namespace Content.Server.GameTicking
|
||||
case SessionStatus.Disconnected:
|
||||
{
|
||||
_chatManager.SendAdminAnnouncement(Loc.GetString("player-leave-message", ("name", args.Session.Name)));
|
||||
if (mind != null)
|
||||
mind.Session = null;
|
||||
|
||||
if (_playerGameStatuses.ContainsKey(args.Session.UserId)) // Corvax-Queue: Delete data only if player was in game
|
||||
_userDb.ClientDisconnected(session);
|
||||
@@ -111,6 +125,12 @@ namespace Content.Server.GameTicking
|
||||
SpawnPlayer(session, EntityUid.Invalid);
|
||||
}
|
||||
|
||||
async void SpawnObserverWaitDb()
|
||||
{
|
||||
await _userDb.WaitLoadComplete(session);
|
||||
JoinAsObserver(session);
|
||||
}
|
||||
|
||||
async void AddPlayerToDb(Guid id)
|
||||
{
|
||||
if (RoundId != 0 && _runLevel != GameRunLevel.PreRoundLobby)
|
||||
@@ -144,7 +164,6 @@ namespace Content.Server.GameTicking
|
||||
RaiseNetworkEvent(new TickerJoinLobbyEvent(), client);
|
||||
RaiseNetworkEvent(GetStatusMsg(session), client);
|
||||
RaiseNetworkEvent(GetInfoMsg(), client);
|
||||
RaiseNetworkEvent(GetPlayerStatus(), client);
|
||||
RaiseLocalEvent(new PlayerJoinedLobbyEvent(session));
|
||||
}
|
||||
|
||||
|
||||
@@ -308,12 +308,11 @@ namespace Content.Server.GameTicking
|
||||
var allMinds = Get<MindTrackerSystem>().AllMinds;
|
||||
foreach (var mind in allMinds)
|
||||
{
|
||||
if (mind == null)
|
||||
continue;
|
||||
// TODO don't list redundant observer roles?
|
||||
// I.e., if a player was an observer ghost, then a hamster ghost role, maybe just list hamster and not
|
||||
// the observer role?
|
||||
var userId = mind.UserId ?? mind.OriginalOwnerUserId;
|
||||
|
||||
// Some basics assuming things fail
|
||||
var userId = mind.OriginalOwnerUserId;
|
||||
var playerOOCName = userId.ToString();
|
||||
var connected = false;
|
||||
var observer = mind.AllRoles.Any(role => role is ObserverRole);
|
||||
// Continuing
|
||||
@@ -416,13 +415,6 @@ namespace Content.Server.GameTicking
|
||||
PlayerJoinLobby(player);
|
||||
}
|
||||
|
||||
// Delete the minds of everybody.
|
||||
// TODO: Maybe move this into a separate manager?
|
||||
foreach (var unCastData in _playerManager.GetAllPlayerData())
|
||||
{
|
||||
unCastData.ContentData()?.WipeMind();
|
||||
}
|
||||
|
||||
// Delete all entities.
|
||||
foreach (var entity in EntityManager.GetEntities().ToArray())
|
||||
{
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using JetBrains.Annotations;
|
||||
@@ -131,7 +128,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
if (lateJoin && DisallowLateJoin)
|
||||
{
|
||||
MakeObserve(player);
|
||||
JoinAsObserver(player);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -163,7 +160,7 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
if (!LobbyEnabled)
|
||||
{
|
||||
MakeObserve(player);
|
||||
JoinAsObserver(player);
|
||||
}
|
||||
_chatManager.DispatchServerMessage(player, Loc.GetString("game-ticker-player-no-jobs-available-when-joining"));
|
||||
return;
|
||||
@@ -175,13 +172,12 @@ namespace Content.Server.GameTicking
|
||||
|
||||
DebugTools.AssertNotNull(data);
|
||||
|
||||
data!.WipeMind();
|
||||
var newMind = _mindSystem.CreateMind(data.UserId, character.Name);
|
||||
_mindSystem.ChangeOwningPlayer(newMind, data.UserId);
|
||||
var newMind = _mind.CreateMind(data!.UserId, character.Name);
|
||||
_mind.SetUserId(newMind, data.UserId);
|
||||
|
||||
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
|
||||
var job = new Job(newMind, jobPrototype);
|
||||
_mindSystem.AddRole(newMind, job);
|
||||
_mind.AddRole(newMind, job);
|
||||
|
||||
_playTimeTrackings.PlayerRolesChanged(player);
|
||||
|
||||
@@ -190,7 +186,7 @@ namespace Content.Server.GameTicking
|
||||
DebugTools.AssertNotNull(mobMaybe);
|
||||
var mob = mobMaybe!.Value;
|
||||
|
||||
_mindSystem.TransferTo(newMind, mob);
|
||||
_mind.TransferTo(newMind, mob);
|
||||
|
||||
if (lateJoin)
|
||||
{
|
||||
@@ -245,7 +241,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
public void Respawn(IPlayerSession player)
|
||||
{
|
||||
player.ContentData()?.WipeMind();
|
||||
_mind.WipeMind(player);
|
||||
_adminLogger.Add(LogType.Respawn, LogImpact.Medium, $"Player {player} was respawned.");
|
||||
|
||||
if (LobbyEnabled)
|
||||
@@ -265,33 +261,41 @@ namespace Content.Server.GameTicking
|
||||
SpawnPlayer(player, station, jobId);
|
||||
}
|
||||
|
||||
public void MakeObserve(IPlayerSession player)
|
||||
/// <summary>
|
||||
/// Causes the given player to join the current game as observer ghost. See also <see cref="SpawnObserver"/>
|
||||
/// </summary>
|
||||
public void JoinAsObserver(IPlayerSession player)
|
||||
{
|
||||
// Can't spawn players with a dummy ticker!
|
||||
if (DummyTicker)
|
||||
return;
|
||||
|
||||
PlayerJoinGame(player);
|
||||
SpawnObserver(player);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns an observer ghost and attaches the given player to it. If the player does not yet have a mind, the
|
||||
/// player is given a new mind with the observer role. Otherwise, the current mind is transferred to the ghost.
|
||||
/// </summary>
|
||||
public void SpawnObserver(IPlayerSession player)
|
||||
{
|
||||
if (DummyTicker)
|
||||
return;
|
||||
|
||||
var mind = player.GetMind();
|
||||
if (mind == null)
|
||||
{
|
||||
mind = _mind.CreateMind(player.UserId);
|
||||
_mind.SetUserId(mind, player.UserId);
|
||||
_mind.AddRole(mind, new ObserverRole(mind));
|
||||
}
|
||||
|
||||
var name = GetPlayerProfile(player).Name;
|
||||
|
||||
var data = player.ContentData();
|
||||
|
||||
DebugTools.AssertNotNull(data);
|
||||
|
||||
data!.WipeMind();
|
||||
var newMind = _mindSystem.CreateMind(data.UserId);
|
||||
_mindSystem.ChangeOwningPlayer(newMind, data.UserId);
|
||||
_mindSystem.AddRole(newMind, new ObserverRole(newMind));
|
||||
|
||||
var mob = SpawnObserverMob();
|
||||
EntityManager.GetComponent<MetaDataComponent>(mob).EntityName = name;
|
||||
var ghost = EntityManager.GetComponent<GhostComponent>(mob);
|
||||
EntitySystem.Get<SharedGhostSystem>().SetCanReturnToBody(ghost, false);
|
||||
_mindSystem.TransferTo(newMind, mob);
|
||||
|
||||
_playerGameStatuses[player.UserId] = PlayerGameStatus.JoinedGame;
|
||||
RaiseNetworkEvent(GetStatusSingle(player, PlayerGameStatus.JoinedGame));
|
||||
var ghost = SpawnObserverMob();
|
||||
MetaData(ghost).EntityName = name;
|
||||
_ghost.SetCanReturnToBody(ghost, false);
|
||||
_mind.TransferTo(mind, ghost);
|
||||
}
|
||||
|
||||
#region Mob Spawning Helpers
|
||||
|
||||
@@ -5,6 +5,7 @@ using Content.Server.Chat.Systems;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.ServerUpdates;
|
||||
@@ -13,6 +14,8 @@ using Content.Server.Station.Systems;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server;
|
||||
using Robust.Server.GameObjects;
|
||||
@@ -34,6 +37,9 @@ namespace Content.Server.GameTicking
|
||||
[Dependency] private readonly ArrivalsSystem _arrivals = default!;
|
||||
[Dependency] private readonly MapLoaderSystem _map = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly GhostSystem _ghost = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
|
||||
[ViewVariables] private bool _initialized;
|
||||
[ViewVariables] private bool _postInitialized;
|
||||
|
||||
@@ -765,7 +765,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
var mob = EntityManager.SpawnEntity(species.Prototype, _random.Pick(spawns));
|
||||
SetupOperativeEntity(mob, spawnDetails.Name, spawnDetails.Gear, profile, component);
|
||||
var newMind = _mindSystem.CreateMind(session.UserId, spawnDetails.Name);
|
||||
_mindSystem.ChangeOwningPlayer(newMind, session.UserId);
|
||||
_mindSystem.SetUserId(newMind, session.UserId);
|
||||
_mindSystem.AddRole(newMind, new NukeopsRole(newMind, nukeOpsAntag));
|
||||
|
||||
_mindSystem.TransferTo(newMind, mob);
|
||||
|
||||
@@ -208,7 +208,7 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
|
||||
|
||||
var session = ops[i];
|
||||
var newMind = _mindSystem.CreateMind(session.UserId, name);
|
||||
_mindSystem.ChangeOwningPlayer(newMind, session.UserId);
|
||||
_mindSystem.SetUserId(newMind, session.UserId);
|
||||
|
||||
var mob = Spawn("MobHuman", _random.Pick(spawns));
|
||||
MetaData(mob).EntityName = name;
|
||||
|
||||
@@ -236,8 +236,6 @@ namespace Content.Server.Ghost
|
||||
if (Deleted(uid) || Terminating(uid))
|
||||
return;
|
||||
|
||||
if (EntityManager.TryGetComponent<MindContainerComponent?>(uid, out var mind))
|
||||
_mindSystem.SetGhostOnShutdown(uid, false, mind);
|
||||
QueueDel(uid);
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ namespace Content.Server.Ghost.Roles
|
||||
EntityManager.GetComponent<MetaDataComponent>(mob).EntityName);
|
||||
_mindSystem.AddRole(newMind, new GhostRoleMarkerRole(newMind, role.RoleName));
|
||||
|
||||
_mindSystem.ChangeOwningPlayer(newMind, player.UserId);
|
||||
_mindSystem.SetUserId(newMind, player.UserId);
|
||||
_mindSystem.TransferTo(newMind, mob);
|
||||
}
|
||||
|
||||
|
||||
92
Content.Server/Item/ItemToggleSystem.cs
Normal file
92
Content.Server/Item/ItemToggleSystem.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using Content.Server.CombatMode.Disarm;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Toggleable;
|
||||
using Content.Shared.Tools.Components;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Weapons.Melee.ItemToggle;
|
||||
|
||||
public sealed class ItemToggleSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedItemSystem _item = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ItemToggleComponent, UseInHandEvent>(OnUseInHand);
|
||||
SubscribeLocalEvent<ItemToggleComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
SubscribeLocalEvent<ItemToggleComponent, ItemToggleDeactivatedEvent>(TurnOff);
|
||||
SubscribeLocalEvent<ItemToggleComponent, ItemToggleActivatedEvent>(TurnOn);
|
||||
}
|
||||
|
||||
private void OnUseInHand(EntityUid uid, ItemToggleComponent comp, UseInHandEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
if (comp.Activated)
|
||||
{
|
||||
var ev = new ItemToggleDeactivatedEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
else
|
||||
{
|
||||
var ev = new ItemToggleActivatedEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
|
||||
UpdateAppearance(uid, comp);
|
||||
}
|
||||
|
||||
private void TurnOff(EntityUid uid, ItemToggleComponent comp, ref ItemToggleDeactivatedEvent args)
|
||||
{
|
||||
if (TryComp(uid, out ItemComponent? item))
|
||||
_item.SetSize(uid, comp.OffSize, item);
|
||||
|
||||
if (TryComp<DisarmMalusComponent>(uid, out var malus))
|
||||
malus.Malus -= comp.ActivatedDisarmMalus;
|
||||
|
||||
_audio.Play(comp.DeActivateSound, Filter.Pvs(uid, entityManager: EntityManager), uid, true, comp.DeActivateSound.Params);
|
||||
|
||||
comp.Activated = false;
|
||||
}
|
||||
|
||||
private void TurnOn(EntityUid uid, ItemToggleComponent comp, ref ItemToggleActivatedEvent args)
|
||||
{
|
||||
if (TryComp(uid, out ItemComponent? item))
|
||||
_item.SetSize(uid, comp.OnSize, item);
|
||||
|
||||
if (TryComp<DisarmMalusComponent>(uid, out var malus))
|
||||
malus.Malus += comp.ActivatedDisarmMalus;
|
||||
|
||||
_audio.Play(comp.ActivateSound, Filter.Pvs(uid, entityManager: EntityManager), uid, true, comp.ActivateSound.Params);
|
||||
|
||||
comp.Activated = true;
|
||||
}
|
||||
|
||||
private void UpdateAppearance(EntityUid uid, ItemToggleComponent component)
|
||||
{
|
||||
if (!TryComp(uid, out AppearanceComponent? appearanceComponent))
|
||||
return;
|
||||
|
||||
_appearance.SetData(uid, ToggleableLightVisuals.Enabled, component.Activated, appearanceComponent);
|
||||
}
|
||||
|
||||
private void OnInteractUsing(EntityUid uid, ItemToggleComponent comp, InteractUsingEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!TryComp(args.Used, out ToolComponent? tool) || !tool.Qualities.ContainsAny("Pulsing"))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ namespace Content.Server.Labels
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HandLabelerComponent, AfterInteractEvent>(AfterInteractOn);
|
||||
SubscribeLocalEvent<HandLabelerComponent, ActivateInWorldEvent>(OnActivate);
|
||||
SubscribeLocalEvent<HandLabelerComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
|
||||
// Bound UI subscriptions
|
||||
SubscribeLocalEvent<HandLabelerComponent, HandLabelerLabelChangedMessage>(OnHandLabelerLabelChanged);
|
||||
@@ -54,6 +53,7 @@ namespace Content.Server.Labels
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private void AfterInteractOn(EntityUid uid, HandLabelerComponent handLabeler, AfterInteractEvent args)
|
||||
{
|
||||
if (args.Target is not {Valid: true} target || !handLabeler.Whitelist.IsValid(target) || !args.CanReach)
|
||||
@@ -88,15 +88,6 @@ namespace Content.Server.Labels
|
||||
result = Loc.GetString("hand-labeler-successfully-applied");
|
||||
}
|
||||
|
||||
private void OnActivate(EntityUid uid, HandLabelerComponent handLabeler, ActivateInWorldEvent args)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
handLabeler.Owner.GetUIOrNull(HandLabelerUiKey.Key)?.Open(actor.PlayerSession);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnHandLabelerLabelChanged(EntityUid uid, HandLabelerComponent handLabeler, HandLabelerLabelChangedMessage args)
|
||||
{
|
||||
if (args.Session.AttachedEntity is not {Valid: true} player)
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace Content.Server.Lathe
|
||||
SubscribeLocalEvent<LatheComponent, RefreshPartsEvent>(OnPartsRefresh);
|
||||
SubscribeLocalEvent<LatheComponent, UpgradeExamineEvent>(OnUpgradeExamine);
|
||||
SubscribeLocalEvent<LatheComponent, TechnologyDatabaseModifiedEvent>(OnDatabaseModified);
|
||||
SubscribeLocalEvent<LatheComponent, ResearchRegistrationChangedEvent>(OnResearchRegistrationChanged);
|
||||
|
||||
SubscribeLocalEvent<LatheComponent, LatheQueueRecipeMessage>(OnLatheQueueRecipeMessage);
|
||||
SubscribeLocalEvent<LatheComponent, LatheSyncRequestMessage>(OnLatheSyncRequestMessage);
|
||||
@@ -101,7 +102,7 @@ namespace Content.Server.Lathe
|
||||
{
|
||||
var ev = new LatheGetRecipesEvent(uid)
|
||||
{
|
||||
Recipes = component.StaticRecipes
|
||||
Recipes = new List<string>(component.StaticRecipes)
|
||||
};
|
||||
RaiseLocalEvent(uid, ev);
|
||||
return ev.Recipes;
|
||||
@@ -195,7 +196,7 @@ namespace Content.Server.Lathe
|
||||
|
||||
foreach (var recipe in latheComponent.DynamicRecipes)
|
||||
{
|
||||
if (!component.UnlockedRecipes.Contains(recipe))
|
||||
if (!component.UnlockedRecipes.Contains(recipe) || args.Recipes.Contains(recipe))
|
||||
continue;
|
||||
args.Recipes.Add(recipe);
|
||||
}
|
||||
@@ -262,6 +263,11 @@ namespace Content.Server.Lathe
|
||||
UpdateUserInterfaceState(uid, component);
|
||||
}
|
||||
|
||||
private void OnResearchRegistrationChanged(EntityUid uid, LatheComponent component, ref ResearchRegistrationChangedEvent args)
|
||||
{
|
||||
UpdateUserInterfaceState(uid, component);
|
||||
}
|
||||
|
||||
protected override bool HasRecipe(EntityUid uid, LatheRecipePrototype recipe, LatheComponent component)
|
||||
{
|
||||
return GetAvailableRecipes(uid, component).Contains(recipe.ID);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using YamlDotNet.Core.Tokens;
|
||||
|
||||
namespace Content.Server.Mind.Components
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Mind.Components;
|
||||
using Content.Server.Objectives;
|
||||
using Content.Server.Roles;
|
||||
@@ -30,29 +31,27 @@ namespace Content.Server.Mind
|
||||
/// Note: the Mind is NOT initially attached!
|
||||
/// The provided UserId is solely for tracking of intended owner.
|
||||
/// </summary>
|
||||
/// <param name="userId">The session ID of the original owner (may get credited).</param>
|
||||
public Mind(NetUserId? userId)
|
||||
public Mind()
|
||||
{
|
||||
OriginalOwnerUserId = userId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The session ID of the player owning this mind.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public NetUserId? UserId { get; internal set; }
|
||||
[ViewVariables, Access(typeof(MindSystem))]
|
||||
public NetUserId? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The session ID of the original owner, if any.
|
||||
/// May end up used for round-end information (as the owner may have abandoned Mind since)
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public NetUserId? OriginalOwnerUserId { get; }
|
||||
[ViewVariables, Access(typeof(MindSystem))]
|
||||
public NetUserId? OriginalOwnerUserId { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public bool IsVisitingEntity => VisitingEntity != null;
|
||||
|
||||
[ViewVariables]
|
||||
[ViewVariables, Access(typeof(MindSystem))]
|
||||
public EntityUid? VisitingEntity { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
@@ -79,8 +78,8 @@ namespace Content.Server.Mind
|
||||
/// The entity currently owned by this mind.
|
||||
/// Can be null.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public EntityUid? OwnedEntity { get; internal set; }
|
||||
[ViewVariables, Access(typeof(MindSystem))]
|
||||
public EntityUid? OwnedEntity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An enumerable over all the roles this mind has.
|
||||
@@ -112,7 +111,7 @@ namespace Content.Server.Mind
|
||||
/// The session of the player owning this mind.
|
||||
/// Can be null, in which case the player is currently not logged in.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[ViewVariables, Access(typeof(MindSystem), typeof(GameTicker))]
|
||||
public IPlayerSession? Session { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Server.Players;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Mobs.Components;
|
||||
@@ -30,29 +31,25 @@ public sealed class MindSystem : EntitySystem
|
||||
[Dependency] private readonly GhostSystem _ghostSystem = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly ActorSystem _actor = default!;
|
||||
|
||||
// This is dictionary is required to track the minds of disconnected players that may have had their entity deleted.
|
||||
private readonly Dictionary<NetUserId, Mind> _userMinds = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<MindContainerComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<MindContainerComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<MindContainerComponent, SuicideEvent>(OnSuicide);
|
||||
SubscribeLocalEvent<VisitingMindComponent, EntityTerminatingEvent>(OnTerminating);
|
||||
SubscribeLocalEvent<VisitingMindComponent, PlayerDetachedEvent>(OnDetached);
|
||||
SubscribeLocalEvent<MindContainerComponent, EntityTerminatingEvent>(OnMindContainerTerminating);
|
||||
SubscribeLocalEvent<VisitingMindComponent, EntityTerminatingEvent>(OnVisitingTerminating);
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnReset);
|
||||
}
|
||||
|
||||
private void OnDetached(EntityUid uid, VisitingMindComponent component, PlayerDetachedEvent args)
|
||||
public override void Shutdown()
|
||||
{
|
||||
component.Mind = null;
|
||||
RemCompDeferred(uid, component);
|
||||
}
|
||||
|
||||
private void OnTerminating(EntityUid uid, VisitingMindComponent component, ref EntityTerminatingEvent args)
|
||||
{
|
||||
if (component.Mind?.Session?.AttachedEntity == uid)
|
||||
UnVisit(component.Mind);
|
||||
base.Shutdown();
|
||||
WipeAllMinds();
|
||||
}
|
||||
|
||||
public void SetGhostOnShutdown(EntityUid uid, bool value, MindContainerComponent? mind = null)
|
||||
@@ -63,6 +60,49 @@ public sealed class MindSystem : EntitySystem
|
||||
mind.GhostOnShutdown = value;
|
||||
}
|
||||
|
||||
private void OnReset(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
WipeAllMinds();
|
||||
}
|
||||
|
||||
public void WipeAllMinds()
|
||||
{
|
||||
foreach (var mind in _userMinds.Values)
|
||||
{
|
||||
WipeMind(mind);
|
||||
}
|
||||
DebugTools.Assert(_userMinds.Count == 0);
|
||||
|
||||
foreach (var unCastData in _playerManager.GetAllPlayerData())
|
||||
{
|
||||
if (unCastData.ContentData()?.Mind is not { } mind)
|
||||
continue;
|
||||
|
||||
Log.Error("Player mind was missing from MindSystem dictionary.");
|
||||
WipeMind(mind);
|
||||
}
|
||||
}
|
||||
|
||||
public Mind? GetMind(NetUserId user)
|
||||
{
|
||||
TryGetMind(user, out var mind);
|
||||
return mind;
|
||||
}
|
||||
|
||||
public bool TryGetMind(NetUserId user, [NotNullWhen(true)] out Mind? mind)
|
||||
{
|
||||
if (_userMinds.TryGetValue(user, out mind))
|
||||
{
|
||||
DebugTools.Assert(mind.UserId == user);
|
||||
DebugTools.Assert(_playerManager.GetPlayerData(user).ContentData() is not {} data
|
||||
|| data.Mind == mind);
|
||||
return true;
|
||||
}
|
||||
|
||||
DebugTools.Assert(_playerManager.GetPlayerData(user).ContentData()?.Mind == null);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Don't call this unless you know what the hell you're doing.
|
||||
/// Use <see cref="MindSystem.TransferTo(Mind,System.Nullable{Robust.Shared.GameObjects.EntityUid},bool)"/> instead.
|
||||
@@ -91,29 +131,39 @@ public sealed class MindSystem : EntitySystem
|
||||
mind.Mind = null;
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, MindContainerComponent mindContainerComp, ComponentShutdown args)
|
||||
private void OnVisitingTerminating(EntityUid uid, VisitingMindComponent component, ref EntityTerminatingEvent args)
|
||||
{
|
||||
if (component.Mind != null)
|
||||
UnVisit(component.Mind);
|
||||
}
|
||||
|
||||
private void OnMindContainerTerminating(EntityUid uid, MindContainerComponent component, ref EntityTerminatingEvent args)
|
||||
{
|
||||
// Let's not create ghosts if not in the middle of the round.
|
||||
if (_gameTicker.RunLevel != GameRunLevel.InRound)
|
||||
return;
|
||||
|
||||
if (!TryGetMind(uid, out var mind, mindContainerComp))
|
||||
if (component.Mind is not { } mind)
|
||||
return;
|
||||
|
||||
if (mind.VisitingEntity is {Valid: true} visiting)
|
||||
// If the player is currently visiting some other entity, simply attach to that entity.
|
||||
if (mind.VisitingEntity is {Valid: true} visiting
|
||||
&& visiting != uid
|
||||
&& !Deleted(visiting)
|
||||
&& !Terminating(visiting))
|
||||
{
|
||||
if (TryComp(visiting, out GhostComponent? ghost))
|
||||
{
|
||||
_ghostSystem.SetCanReturnToBody(ghost, false);
|
||||
}
|
||||
|
||||
TransferTo(mind, visiting);
|
||||
if (TryComp(visiting, out GhostComponent? ghost))
|
||||
_ghostSystem.SetCanReturnToBody(ghost, false);
|
||||
return;
|
||||
}
|
||||
else if (mindContainerComp.GhostOnShutdown)
|
||||
|
||||
TransferTo(mind, null);
|
||||
|
||||
if (component.GhostOnShutdown && mind.Session != null)
|
||||
{
|
||||
// Changing an entities parents while deleting is VERY sus. This WILL throw exceptions.
|
||||
// TODO: just find the applicable spawn position directly without actually updating the transform's parent.
|
||||
Transform(uid).AttachToGridOrMap();
|
||||
var xform = Transform(uid);
|
||||
var gridId = xform.GridUid;
|
||||
var spawnPosition = Transform(uid).Coordinates;
|
||||
|
||||
// Use a regular timer here because the entity has probably been deleted.
|
||||
@@ -124,11 +174,8 @@ public sealed class MindSystem : EntitySystem
|
||||
return;
|
||||
|
||||
// Async this so that we don't throw if the grid we're on is being deleted.
|
||||
var gridId = spawnPosition.GetGridUid(EntityManager);
|
||||
if (!spawnPosition.IsValid(EntityManager) || gridId == EntityUid.Invalid || !_mapManager.GridExists(gridId))
|
||||
{
|
||||
if (!_mapManager.GridExists(gridId))
|
||||
spawnPosition = _gameTicker.GetObserverSpawnPoint();
|
||||
}
|
||||
|
||||
// TODO refactor observer spawning.
|
||||
// please.
|
||||
@@ -195,9 +242,10 @@ public sealed class MindSystem : EntitySystem
|
||||
|
||||
public Mind CreateMind(NetUserId? userId, string? name = null)
|
||||
{
|
||||
var mind = new Mind(userId);
|
||||
var mind = new Mind();
|
||||
mind.CharacterName = name;
|
||||
ChangeOwningPlayer(mind, userId);
|
||||
SetUserId(mind, userId);
|
||||
|
||||
return mind;
|
||||
}
|
||||
|
||||
@@ -262,7 +310,6 @@ public sealed class MindSystem : EntitySystem
|
||||
if (mind == null || mind.VisitingEntity == null)
|
||||
return;
|
||||
|
||||
DebugTools.Assert(mind.VisitingEntity != mind.OwnedEntity);
|
||||
RemoveVisitingEntity(mind);
|
||||
|
||||
if (mind.Session == null || mind.Session.AttachedEntity == mind.VisitingEntity)
|
||||
@@ -300,6 +347,25 @@ public sealed class MindSystem : EntitySystem
|
||||
RaiseLocalEvent(oldVisitingEnt, new MindUnvisitedMessage(), true);
|
||||
}
|
||||
|
||||
public void WipeMind(IPlayerSession player)
|
||||
{
|
||||
var mind = player.ContentData()?.Mind;
|
||||
DebugTools.Assert(GetMind(player.UserId) == mind);
|
||||
WipeMind(mind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches a mind from all entities and clears the user ID.
|
||||
/// </summary>
|
||||
public void WipeMind(Mind? mind)
|
||||
{
|
||||
if (mind == null)
|
||||
return;
|
||||
|
||||
TransferTo(mind, null);
|
||||
SetUserId(mind, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transfer this mind's control over to a new entity.
|
||||
/// </summary>
|
||||
@@ -316,26 +382,18 @@ public sealed class MindSystem : EntitySystem
|
||||
/// </exception>
|
||||
public void TransferTo(Mind mind, EntityUid? entity, bool ghostCheckOverride = false)
|
||||
{
|
||||
// Looks like caller just wants us to go back to normal.
|
||||
if (entity == mind.OwnedEntity)
|
||||
{
|
||||
UnVisit(mind);
|
||||
return;
|
||||
}
|
||||
|
||||
MindContainerComponent? component = null;
|
||||
var alreadyAttached = false;
|
||||
|
||||
if (entity != null)
|
||||
{
|
||||
if (!TryComp(entity.Value, out component))
|
||||
{
|
||||
component = AddComp<MindContainerComponent>(entity.Value);
|
||||
}
|
||||
else if (component.HasMind)
|
||||
{
|
||||
component = EnsureComp<MindContainerComponent>(entity.Value);
|
||||
|
||||
if (component.HasMind)
|
||||
_gameTicker.OnGhostAttempt(component.Mind, false);
|
||||
}
|
||||
|
||||
if (TryComp<ActorComponent>(entity.Value, out var actor))
|
||||
{
|
||||
@@ -382,51 +440,6 @@ public sealed class MindSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
public void ChangeOwningPlayer(Mind mind, NetUserId? newOwner)
|
||||
{
|
||||
// Make sure to remove control from our old owner if they're logged in.
|
||||
var oldSession = mind.Session;
|
||||
oldSession?.AttachToEntity(null);
|
||||
|
||||
if (mind.UserId.HasValue)
|
||||
{
|
||||
if (_playerManager.TryGetPlayerData(mind.UserId.Value, out var oldUncast))
|
||||
{
|
||||
var data = oldUncast.ContentData();
|
||||
DebugTools.AssertNotNull(data);
|
||||
data!.UpdateMindFromMindChangeOwningPlayer(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"Mind UserId {newOwner} is does not exist in PlayerManager");
|
||||
}
|
||||
}
|
||||
|
||||
SetUserId(mind, newOwner);
|
||||
if (!newOwner.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_playerManager.TryGetPlayerData(newOwner.Value, out var uncast))
|
||||
{
|
||||
// This restriction is because I'm too lazy to initialize the player data
|
||||
// for a client that hasn't logged in yet.
|
||||
// Go ahead and remove it if you need.
|
||||
throw new ArgumentException("New owner must have previously logged into the server.", nameof(newOwner));
|
||||
}
|
||||
|
||||
// PlayerData? newOwnerData = null;
|
||||
var newOwnerData = uncast.ContentData();
|
||||
|
||||
// Yank new owner out of their old mind too.
|
||||
// Can I mention how much I love the word yank?
|
||||
DebugTools.AssertNotNull(newOwnerData);
|
||||
if (newOwnerData!.Mind != null)
|
||||
ChangeOwningPlayer(newOwnerData.Mind, null);
|
||||
newOwnerData.UpdateMindFromMindChangeOwningPlayer(mind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an objective to this mind.
|
||||
/// </summary>
|
||||
@@ -569,19 +582,57 @@ public sealed class MindSystem : EntitySystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Mind's UserId and Session
|
||||
/// Sets the Mind's UserId, Session, and updates the player's PlayerData.
|
||||
/// This should have no direct effect on the entity that any mind is connected to, but it may change a player's attached entity.
|
||||
/// </summary>
|
||||
/// <param name="mind"></param>
|
||||
/// <param name="userId"></param>
|
||||
private void SetUserId(Mind mind, NetUserId? userId)
|
||||
public void SetUserId(Mind mind, NetUserId? userId)
|
||||
{
|
||||
mind.UserId = userId;
|
||||
|
||||
if (!userId.HasValue)
|
||||
if (mind.UserId == userId)
|
||||
return;
|
||||
|
||||
if (userId != null && !_playerManager.TryGetPlayerData(userId.Value, out _))
|
||||
{
|
||||
Log.Error($"Attempted to set mind user to invalid value {userId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mind.Session != null)
|
||||
{
|
||||
mind.Session.AttachToEntity(null);
|
||||
mind.Session = null;
|
||||
}
|
||||
|
||||
if (mind.UserId != null)
|
||||
{
|
||||
_userMinds.Remove(mind.UserId.Value);
|
||||
if (_playerManager.GetPlayerData(mind.UserId.Value).ContentData() is { } oldData)
|
||||
oldData.Mind = null;
|
||||
mind.UserId = null;
|
||||
}
|
||||
|
||||
if (userId == null)
|
||||
{
|
||||
DebugTools.AssertNull(mind.Session);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_userMinds.TryGetValue(userId.Value, out var oldMind))
|
||||
SetUserId(oldMind, null);
|
||||
|
||||
DebugTools.AssertNull(_playerManager.GetPlayerData(userId.Value).ContentData()?.Mind);
|
||||
|
||||
_userMinds[userId.Value] = mind;
|
||||
mind.UserId = userId;
|
||||
mind.OriginalOwnerUserId ??= userId;
|
||||
|
||||
_playerManager.TryGetSessionById(userId.Value, out var ret);
|
||||
mind.Session = ret;
|
||||
|
||||
// session may be null, but user data may still exist for disconnected players.
|
||||
if (_playerManager.GetPlayerData(userId.Value).ContentData() is { } data)
|
||||
data.Mind = mind;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -14,7 +14,7 @@ public sealed class GridPathfindingComponent : Component
|
||||
/// <summary>
|
||||
/// Next time the graph is allowed to update.
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("nextUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))]
|
||||
/// Removing this datafield is the lazy fix HOWEVER I want to purge this anyway and do pathfinding at runtime.
|
||||
public TimeSpan NextUpdate;
|
||||
|
||||
[ViewVariables]
|
||||
|
||||
@@ -33,8 +33,6 @@ namespace Content.Server.PDA
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PdaComponent, LightToggleEvent>(OnLightToggle);
|
||||
SubscribeLocalEvent<PdaComponent, GridModifiedEvent>(OnGridChanged);
|
||||
SubscribeLocalEvent<PdaComponent, AlertLevelChangedEvent>(OnAlertLevelChanged);
|
||||
|
||||
// UI Events:
|
||||
SubscribeLocalEvent<PdaComponent, PdaRequestUpdateInterfaceMessage>(OnUiMessage);
|
||||
@@ -43,6 +41,9 @@ namespace Content.Server.PDA
|
||||
SubscribeLocalEvent<PdaComponent, PdaShowMusicMessage>(OnUiMessage);
|
||||
SubscribeLocalEvent<PdaComponent, PdaShowUplinkMessage>(OnUiMessage);
|
||||
SubscribeLocalEvent<PdaComponent, PdaLockUplinkMessage>(OnUiMessage);
|
||||
|
||||
SubscribeLocalEvent<StationRenamedEvent>(OnStationRenamed);
|
||||
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
|
||||
}
|
||||
|
||||
protected override void OnComponentInit(EntityUid uid, PdaComponent pda, ComponentInit args)
|
||||
@@ -80,10 +81,23 @@ namespace Content.Server.PDA
|
||||
UpdatePdaUi(uid, pda);
|
||||
}
|
||||
|
||||
private void OnGridChanged(EntityUid uid, PdaComponent pda, GridModifiedEvent args)
|
||||
private void OnStationRenamed(StationRenamedEvent ev)
|
||||
{
|
||||
UpdateStationName(uid, pda);
|
||||
UpdatePdaUi(uid, pda);
|
||||
UpdateAllPdaUisOnStation();
|
||||
}
|
||||
|
||||
private void OnAlertLevelChanged(AlertLevelChangedEvent args)
|
||||
{
|
||||
UpdateAllPdaUisOnStation();
|
||||
}
|
||||
|
||||
private void UpdateAllPdaUisOnStation()
|
||||
{
|
||||
var query = EntityQueryEnumerator<PdaComponent>();
|
||||
while (query.MoveNext(out var ent, out var comp))
|
||||
{
|
||||
UpdatePdaUi(ent, comp);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -91,16 +105,7 @@ namespace Content.Server.PDA
|
||||
/// </summary>
|
||||
public void UpdatePdaUi(EntityUid uid, PdaComponent pda)
|
||||
{
|
||||
var ownerInfo = new PdaIdInfoText
|
||||
{
|
||||
ActualOwnerName = pda.OwnerName,
|
||||
IdOwner = pda.ContainedId?.FullName,
|
||||
JobTitle = pda.ContainedId?.JobTitle,
|
||||
StationAlertLevel = pda.StationAlertLevel,
|
||||
StationAlertColor = pda.StationAlertColor
|
||||
};
|
||||
|
||||
if (!_ui.TryGetUi(uid, PdaUiKey.Key, out var ui))
|
||||
if (!_ui.TryGetUi(uid, PdaUiKey.Key, out _))
|
||||
return;
|
||||
|
||||
var address = GetDeviceNetAddress(uid);
|
||||
@@ -115,7 +120,14 @@ namespace Content.Server.PDA
|
||||
var state = new PdaUpdateState(
|
||||
pda.FlashlightOn,
|
||||
pda.PenSlot.HasItem,
|
||||
ownerInfo,
|
||||
new PdaIdInfoText
|
||||
{
|
||||
ActualOwnerName = pda.OwnerName,
|
||||
IdOwner = pda.ContainedId?.FullName,
|
||||
JobTitle = pda.ContainedId?.JobTitle,
|
||||
StationAlertLevel = pda.StationAlertLevel,
|
||||
StationAlertColor = pda.StationAlertColor
|
||||
},
|
||||
pda.StationName,
|
||||
showUplink,
|
||||
hasInstrument,
|
||||
@@ -192,12 +204,6 @@ namespace Content.Server.PDA
|
||||
pda.StationName = station is null ? null : Name(station.Value);
|
||||
}
|
||||
|
||||
private void OnAlertLevelChanged(EntityUid uid, PdaComponent pda, AlertLevelChangedEvent args)
|
||||
{
|
||||
UpdateAlertLevel(uid, pda);
|
||||
UpdatePdaUi(uid, pda);
|
||||
}
|
||||
|
||||
private void UpdateAlertLevel(EntityUid uid, PdaComponent pda)
|
||||
{
|
||||
var station = _station.GetOwningStation(uid);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Mind;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Network;
|
||||
@@ -27,8 +28,8 @@ namespace Content.Server.Players
|
||||
/// The currently occupied mind of the player owning this data.
|
||||
/// DO NOT DIRECTLY SET THIS UNLESS YOU KNOW WHAT YOU'RE DOING.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Mind.Mind? Mind { get; private set; }
|
||||
[ViewVariables, Access(typeof(MindSystem), typeof(GameTicker))]
|
||||
public Mind.Mind? Mind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the player is an admin and they explicitly de-adminned mid-game,
|
||||
@@ -36,27 +37,6 @@ namespace Content.Server.Players
|
||||
/// </summary>
|
||||
public bool ExplicitlyDeadminned { get; set; }
|
||||
|
||||
public void WipeMind()
|
||||
{
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var mindSystem = entityManager.System<MindSystem>();
|
||||
|
||||
// This will ensure Mind == null
|
||||
if (Mind == null)
|
||||
return;
|
||||
|
||||
mindSystem.TransferTo(Mind, null);
|
||||
mindSystem.ChangeOwningPlayer(Mind, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called from Mind.ChangeOwningPlayer *and nowhere else.*
|
||||
/// </summary>
|
||||
public void UpdateMindFromMindChangeOwningPlayer(Mind.Mind? mind)
|
||||
{
|
||||
Mind = mind;
|
||||
}
|
||||
|
||||
public PlayerData(NetUserId userId, string name)
|
||||
{
|
||||
UserId = userId;
|
||||
@@ -81,5 +61,13 @@ namespace Content.Server.Players
|
||||
{
|
||||
return session.Data.ContentData();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mind that is associated with this player.
|
||||
/// </summary>
|
||||
public static Mind.Mind? GetMind(this IPlayerSession session)
|
||||
{
|
||||
return session.Data.ContentData()?.Mind;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ public sealed partial class ResearchSystem
|
||||
SubscribeLocalEvent<ResearchClientComponent, MapInitEvent>(OnClientMapInit);
|
||||
SubscribeLocalEvent<ResearchClientComponent, ComponentShutdown>(OnClientShutdown);
|
||||
SubscribeLocalEvent<ResearchClientComponent, BoundUIOpenedEvent>(OnClientUIOpen);
|
||||
SubscribeLocalEvent<ResearchClientComponent, ConsoleServerSyncMessage>(OnConsoleSync);
|
||||
SubscribeLocalEvent<ResearchClientComponent, ConsoleServerSelectionMessage>(OnConsoleSelect);
|
||||
|
||||
SubscribeLocalEvent<ResearchClientComponent, ResearchClientSyncMessage>(OnClientSyncMessage);
|
||||
@@ -50,14 +49,6 @@ public sealed partial class ResearchSystem
|
||||
|
||||
_uiSystem.TryToggleUi(uid, ResearchClientUiKey.Key, (IPlayerSession) args.Session);
|
||||
}
|
||||
|
||||
private void OnConsoleSync(EntityUid uid, ResearchClientComponent component, ConsoleServerSyncMessage args)
|
||||
{
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
return;
|
||||
|
||||
SyncClientWithServer(uid);
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void OnClientRegistrationChanged(EntityUid uid, ResearchClientComponent component, ref ResearchRegistrationChangedEvent args)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Research.Components;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.Research.Components;
|
||||
|
||||
namespace Content.Server.Research.Systems;
|
||||
@@ -9,6 +10,7 @@ public sealed partial class ResearchSystem
|
||||
private void InitializeConsole()
|
||||
{
|
||||
SubscribeLocalEvent<ResearchConsoleComponent, ConsoleUnlockTechnologyMessage>(OnConsoleUnlock);
|
||||
SubscribeLocalEvent<ResearchConsoleComponent, BeforeActivatableUIOpenEvent>(OnConsoleBeforeUiOpened);
|
||||
SubscribeLocalEvent<ResearchConsoleComponent, ResearchServerPointsChangedEvent>(OnPointsChanged);
|
||||
SubscribeLocalEvent<ResearchConsoleComponent, ResearchRegistrationChangedEvent>(OnConsoleRegistrationChanged);
|
||||
SubscribeLocalEvent<ResearchConsoleComponent, TechnologyDatabaseModifiedEvent>(OnConsoleDatabaseModified);
|
||||
@@ -26,6 +28,11 @@ public sealed partial class ResearchSystem
|
||||
UpdateConsoleInterface(uid, component);
|
||||
}
|
||||
|
||||
private void OnConsoleBeforeUiOpened(EntityUid uid, ResearchConsoleComponent component, BeforeActivatableUIOpenEvent args)
|
||||
{
|
||||
SyncClientWithServer(uid);
|
||||
}
|
||||
|
||||
private void UpdateConsoleInterface(EntityUid uid, ResearchConsoleComponent? component = null, ResearchClientComponent? clientComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component, ref clientComponent, false))
|
||||
|
||||
@@ -71,6 +71,7 @@ public sealed partial class ResearchSystem
|
||||
|
||||
serverComponent.Clients.Add(client);
|
||||
clientComponent.Server = server;
|
||||
SyncClientWithServer(client, clientComponent: clientComponent);
|
||||
|
||||
if (dirtyServer)
|
||||
Dirty(serverComponent);
|
||||
@@ -112,6 +113,7 @@ public sealed partial class ResearchSystem
|
||||
|
||||
serverComponent.Clients.Remove(client);
|
||||
clientComponent.Server = null;
|
||||
SyncClientWithServer(client, clientComponent: clientComponent);
|
||||
|
||||
if (dirtyServer)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
/// <summary>
|
||||
/// The magnet that spawned this grid.
|
||||
/// </summary>
|
||||
public SalvageMagnetComponent? SpawnerMagnet;
|
||||
public EntityUid? SpawnerMagnet;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Radio;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Salvage;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
@@ -12,33 +13,19 @@ namespace Content.Server.Salvage
|
||||
[Access(typeof(SalvageSystem))]
|
||||
public sealed class SalvageMagnetComponent : SharedSalvageMagnetComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Offset relative to magnet used as centre of the placement circle.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("offset")]
|
||||
public Vector2 Offset = Vector2.Zero; // TODO: Maybe specify a direction, and find the nearest edge of the magnets grid the salvage can fit at
|
||||
|
||||
/// <summary>
|
||||
/// Minimum distance from the offset position that will be used as a salvage's spawnpoint.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("offsetRadiusMin")]
|
||||
public float OffsetRadiusMin = 24f;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum distance from the offset position that will be used as a salvage's spawnpoint.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("offsetRadiusMax")]
|
||||
public float OffsetRadiusMax = 48f;
|
||||
public float OffsetRadiusMax = 32;
|
||||
|
||||
/// <summary>
|
||||
/// The entity attached to the magnet
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField("attachedEntity")]
|
||||
public EntityUid? AttachedEntity = null;
|
||||
public EntityUid? AttachedEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Current state of this magnet
|
||||
@@ -95,18 +82,34 @@ namespace Content.Server.Salvage
|
||||
/// <summary>
|
||||
/// Current how much charge the magnet currently has
|
||||
/// </summary>
|
||||
[DataField("chargeRemaining")]
|
||||
public int ChargeRemaining = 5;
|
||||
|
||||
/// <summary>
|
||||
/// How much capacity the magnet can hold
|
||||
/// </summary>
|
||||
[DataField("chargeCapacity")]
|
||||
public int ChargeCapacity = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Used as a guard to prevent spamming the appearance system
|
||||
/// </summary>
|
||||
[DataField("previousCharge")]
|
||||
public int PreviousCharge = 5;
|
||||
|
||||
/// <summary>
|
||||
/// The chance that a random procgen asteroid will be
|
||||
/// generated rather than a static salvage prototype.
|
||||
/// </summary>
|
||||
[DataField("asteroidChance"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float AsteroidChance = 0.6f;
|
||||
|
||||
/// <summary>
|
||||
/// A weighted random prototype corresponding to
|
||||
/// what asteroid entities will be generated.
|
||||
/// </summary>
|
||||
[DataField("asteroidPool", customTypeSerializer: typeof(PrototypeIdSerializer<WeightedRandomPrototype>)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public string AsteroidPool = "RandomAsteroidPool";
|
||||
}
|
||||
|
||||
[CopyByRef, DataRecord]
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Salvage
|
||||
namespace Content.Server.Salvage;
|
||||
|
||||
[Prototype("salvageMap")]
|
||||
public sealed class SalvageMapPrototype : IPrototype
|
||||
{
|
||||
[Prototype("salvageMap")]
|
||||
public sealed class SalvageMapPrototype : IPrototype
|
||||
{
|
||||
[ViewVariables]
|
||||
[IdDataField]
|
||||
public string ID { get; } = default!;
|
||||
[ViewVariables] [IdDataField] public string ID { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Relative directory path to the given map, i.e. `Maps/Salvage/template.yml`
|
||||
/// </summary>
|
||||
[DataField("mapPath", required: true)]
|
||||
public ResPath MapPath { get; } = default!;
|
||||
/// <summary>
|
||||
/// Relative directory path to the given map, i.e. `Maps/Salvage/template.yml`
|
||||
/// </summary>
|
||||
[DataField("mapPath", required: true)] public ResPath MapPath;
|
||||
|
||||
/// <summary>
|
||||
/// Map rectangle in world coordinates (to check if it fits)
|
||||
/// </summary>
|
||||
[DataField("bounds", required: true)]
|
||||
public Box2 Bounds { get; } = Box2.UnitCentered;
|
||||
|
||||
/// <summary>
|
||||
/// Name for admin use
|
||||
/// </summary>
|
||||
[DataField("name")]
|
||||
public string Name { get; } = "";
|
||||
}
|
||||
/// <summary>
|
||||
/// Name for admin use
|
||||
/// </summary>
|
||||
[DataField("name")] public string Name = string.Empty;
|
||||
}
|
||||
|
||||
@@ -178,14 +178,14 @@ public sealed partial class SalvageSystem
|
||||
// Handle payout after expedition has finished
|
||||
if (expedition.Completed)
|
||||
{
|
||||
_sawmill.Debug($"Completed mission {expedition.MissionParams.MissionType} with seed {expedition.MissionParams.Seed}");
|
||||
Log.Debug($"Completed mission {expedition.MissionParams.MissionType} with seed {expedition.MissionParams.Seed}");
|
||||
component.NextOffer = _timing.CurTime + TimeSpan.FromSeconds(_cooldown);
|
||||
Announce(uid, Loc.GetString("salvage-expedition-mission-completed"));
|
||||
GiveRewards(expedition);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sawmill.Debug($"Failed mission {expedition.MissionParams.MissionType} with seed {expedition.MissionParams.Seed}");
|
||||
Log.Debug($"Failed mission {expedition.MissionParams.MissionType} with seed {expedition.MissionParams.Seed}");
|
||||
component.NextOffer = _timing.CurTime + TimeSpan.FromSeconds(_failedCooldown);
|
||||
Announce(uid, Loc.GetString("salvage-expedition-mission-failed"));
|
||||
}
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Construction;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Radio.Components;
|
||||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Radio;
|
||||
using Content.Shared.Salvage;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Maps;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Parallax;
|
||||
using Content.Server.Procedural;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.Worldgen.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Robust.Server.Maps;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Salvage
|
||||
@@ -49,18 +51,15 @@ namespace Content.Server.Salvage
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
private static readonly int SalvageLocationPlaceAttempts = 16;
|
||||
private const int SalvageLocationPlaceAttempts = 16;
|
||||
|
||||
// TODO: This is probably not compatible with multi-station
|
||||
private readonly Dictionary<EntityUid, SalvageGridState> _salvageGridStates = new();
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_sawmill = Logger.GetSawmill("salvage");
|
||||
SubscribeLocalEvent<SalvageMagnetComponent, InteractHandEvent>(OnInteractHand);
|
||||
SubscribeLocalEvent<SalvageMagnetComponent, RefreshPartsEvent>(OnRefreshParts);
|
||||
SubscribeLocalEvent<SalvageMagnetComponent, UpgradeExamineEvent>(OnUpgradeExamine);
|
||||
@@ -105,24 +104,21 @@ namespace Content.Server.Salvage
|
||||
if (!Resolve(uid, ref component, false))
|
||||
return;
|
||||
|
||||
int timeLeft = Convert.ToInt32(component.MagnetState.Until.TotalSeconds - currentTime.TotalSeconds);
|
||||
if (component.MagnetState.StateType == MagnetStateType.Inactive)
|
||||
component.ChargeRemaining = 5;
|
||||
else if (component.MagnetState.StateType == MagnetStateType.Holding)
|
||||
var timeLeft = Convert.ToInt32(component.MagnetState.Until.TotalSeconds - currentTime.TotalSeconds);
|
||||
|
||||
component.ChargeRemaining = component.MagnetState.StateType switch
|
||||
{
|
||||
component.ChargeRemaining = (timeLeft / (Convert.ToInt32(component.HoldTime.TotalSeconds) / component.ChargeCapacity)) + 1;
|
||||
}
|
||||
else if (component.MagnetState.StateType == MagnetStateType.Detaching)
|
||||
component.ChargeRemaining = 0;
|
||||
else if (component.MagnetState.StateType == MagnetStateType.CoolingDown)
|
||||
{
|
||||
component.ChargeRemaining = component.ChargeCapacity - (timeLeft / (Convert.ToInt32(component.CooldownTime.TotalSeconds) / component.ChargeCapacity)) - 1;
|
||||
}
|
||||
if (component.PreviousCharge != component.ChargeRemaining)
|
||||
{
|
||||
_appearanceSystem.SetData(uid, SalvageMagnetVisuals.ChargeState, component.ChargeRemaining);
|
||||
component.PreviousCharge = component.ChargeRemaining;
|
||||
}
|
||||
MagnetStateType.Inactive => 5,
|
||||
MagnetStateType.Holding => timeLeft / (Convert.ToInt32(component.HoldTime.TotalSeconds) / component.ChargeCapacity) + 1,
|
||||
MagnetStateType.Detaching => 0,
|
||||
MagnetStateType.CoolingDown => component.ChargeCapacity - timeLeft / (Convert.ToInt32(component.CooldownTime.TotalSeconds) / component.ChargeCapacity) - 1,
|
||||
_ => component.ChargeRemaining
|
||||
};
|
||||
|
||||
if (component.PreviousCharge == component.ChargeRemaining)
|
||||
return;
|
||||
_appearanceSystem.SetData(uid, SalvageMagnetVisuals.ChargeState, component.ChargeRemaining);
|
||||
component.PreviousCharge = component.ChargeRemaining;
|
||||
}
|
||||
|
||||
private void OnGridRemoval(GridRemovalEvent ev)
|
||||
@@ -130,34 +126,37 @@ namespace Content.Server.Salvage
|
||||
// If we ever want to give magnets names, and announce them individually, we would need to loop this, before removing it.
|
||||
if (_salvageGridStates.Remove(ev.EntityUid))
|
||||
{
|
||||
if (EntityManager.TryGetComponent<SalvageGridComponent>(ev.EntityUid, out var salvComp) && salvComp.SpawnerMagnet != null)
|
||||
Report(salvComp.SpawnerMagnet.Owner, salvComp.SpawnerMagnet.SalvageChannel, "salvage-system-announcement-spawn-magnet-lost");
|
||||
if (TryComp<SalvageGridComponent>(ev.EntityUid, out var salvComp) &&
|
||||
TryComp<SalvageMagnetComponent>(salvComp.SpawnerMagnet, out var magnet))
|
||||
Report(salvComp.SpawnerMagnet.Value, magnet.SalvageChannel, "salvage-system-announcement-spawn-magnet-lost");
|
||||
// For the very unlikely possibility that the salvage magnet was on a salvage, we will not return here
|
||||
}
|
||||
foreach(var gridState in _salvageGridStates)
|
||||
{
|
||||
foreach(var magnet in gridState.Value.ActiveMagnets)
|
||||
{
|
||||
if (magnet.AttachedEntity == ev.EntityUid)
|
||||
{
|
||||
magnet.AttachedEntity = null;
|
||||
magnet.MagnetState = MagnetState.Inactive;
|
||||
return;
|
||||
}
|
||||
if (!TryComp<SalvageMagnetComponent>(magnet, out var magnetComponent))
|
||||
continue;
|
||||
|
||||
if (magnetComponent.AttachedEntity != ev.EntityUid)
|
||||
continue;
|
||||
magnetComponent.AttachedEntity = null;
|
||||
magnetComponent.MagnetState = MagnetState.Inactive;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMagnetRemoval(EntityUid uid, SalvageMagnetComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (component.MagnetState.StateType == MagnetStateType.Inactive) return;
|
||||
|
||||
var magnetTranform = EntityManager.GetComponent<TransformComponent>(component.Owner);
|
||||
if (!(magnetTranform.GridUid is EntityUid gridId) || !_salvageGridStates.TryGetValue(gridId, out var salvageGridState))
|
||||
{
|
||||
if (component.MagnetState.StateType == MagnetStateType.Inactive)
|
||||
return;
|
||||
}
|
||||
salvageGridState.ActiveMagnets.Remove(component);
|
||||
|
||||
var magnetTranform = Transform(uid);
|
||||
if (magnetTranform.GridUid is not { } gridId || !_salvageGridStates.TryGetValue(gridId, out var salvageGridState))
|
||||
return;
|
||||
|
||||
salvageGridState.ActiveMagnets.Remove(uid);
|
||||
Report(uid, component.SalvageChannel, "salvage-system-announcement-spawn-magnet-lost");
|
||||
if (component.AttachedEntity.HasValue)
|
||||
{
|
||||
@@ -169,6 +168,7 @@ namespace Content.Server.Salvage
|
||||
{
|
||||
Report(uid, component.SalvageChannel, "salvage-system-announcement-spawn-no-debris-available");
|
||||
}
|
||||
|
||||
component.MagnetState = MagnetState.Inactive;
|
||||
}
|
||||
|
||||
@@ -187,13 +187,13 @@ namespace Content.Server.Salvage
|
||||
|
||||
private void OnExamined(EntityUid uid, SalvageMagnetComponent component, ExaminedEvent args)
|
||||
{
|
||||
var gotGrid = false;
|
||||
var remainingTime = TimeSpan.Zero;
|
||||
|
||||
if (!args.IsInDetailsRange)
|
||||
return;
|
||||
|
||||
if (Transform(uid).GridUid is EntityUid gridId &&
|
||||
var gotGrid = false;
|
||||
var remainingTime = TimeSpan.Zero;
|
||||
|
||||
if (Transform(uid).GridUid is { } gridId &&
|
||||
_salvageGridStates.TryGetValue(gridId, out var salvageGridState))
|
||||
{
|
||||
remainingTime = component.MagnetState.Until - salvageGridState.CurrentTime;
|
||||
@@ -201,8 +201,9 @@ namespace Content.Server.Salvage
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.WarningS("salvage", "Failed to load salvage grid state, can't display remaining time");
|
||||
Log.Warning("Failed to load salvage grid state, can't display remaining time");
|
||||
}
|
||||
|
||||
switch (component.MagnetState.StateType)
|
||||
{
|
||||
case MagnetStateType.Inactive:
|
||||
@@ -223,7 +224,7 @@ namespace Content.Server.Salvage
|
||||
args.PushMarkup(Loc.GetString("salvage-system-magnet-examined-active", ("timeLeft", Math.Ceiling(remainingTime.TotalSeconds))));
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException("Unexpected magnet state type");
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,57 +233,56 @@ namespace Content.Server.Salvage
|
||||
if (args.Handled)
|
||||
return;
|
||||
args.Handled = true;
|
||||
StartMagnet(component, args.User);
|
||||
StartMagnet(uid, component, args.User);
|
||||
UpdateAppearance(uid, component);
|
||||
}
|
||||
|
||||
private void StartMagnet(SalvageMagnetComponent component, EntityUid user)
|
||||
private void StartMagnet(EntityUid uid, SalvageMagnetComponent component, EntityUid user)
|
||||
{
|
||||
switch (component.MagnetState.StateType)
|
||||
{
|
||||
case MagnetStateType.Inactive:
|
||||
ShowPopup("salvage-system-report-activate-success", component, user);
|
||||
SalvageGridState? gridState;
|
||||
var magnetTransform = EntityManager.GetComponent<TransformComponent>(component.Owner);
|
||||
EntityUid gridId = magnetTransform.GridUid ?? throw new InvalidOperationException("Magnet had no grid associated");
|
||||
if (!_salvageGridStates.TryGetValue(gridId, out gridState))
|
||||
ShowPopup(uid, "salvage-system-report-activate-success", user);
|
||||
var magnetTransform = Transform(uid);
|
||||
var gridId = magnetTransform.GridUid ?? throw new InvalidOperationException("Magnet had no grid associated");
|
||||
if (!_salvageGridStates.TryGetValue(gridId, out var gridState))
|
||||
{
|
||||
gridState = new SalvageGridState();
|
||||
_salvageGridStates[gridId] = gridState;
|
||||
}
|
||||
gridState.ActiveMagnets.Add(component);
|
||||
gridState.ActiveMagnets.Add(uid);
|
||||
component.MagnetState = new MagnetState(MagnetStateType.Attaching, gridState.CurrentTime + component.AttachingTime);
|
||||
RaiseLocalEvent(new SalvageMagnetActivatedEvent(component.Owner));
|
||||
Report(component.Owner, component.SalvageChannel, "salvage-system-report-activate-success");
|
||||
RaiseLocalEvent(new SalvageMagnetActivatedEvent(uid));
|
||||
Report(uid, component.SalvageChannel, "salvage-system-report-activate-success");
|
||||
break;
|
||||
case MagnetStateType.Attaching:
|
||||
case MagnetStateType.Holding:
|
||||
ShowPopup("salvage-system-report-already-active", component, user);
|
||||
ShowPopup(uid, "salvage-system-report-already-active", user);
|
||||
break;
|
||||
case MagnetStateType.Detaching:
|
||||
case MagnetStateType.CoolingDown:
|
||||
ShowPopup("salvage-system-report-cooling-down", component, user);
|
||||
ShowPopup(uid, "salvage-system-report-cooling-down", user);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException("Unexpected magnet state type");
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
private void ShowPopup(string messageKey, SalvageMagnetComponent component, EntityUid user)
|
||||
private void ShowPopup(EntityUid uid, string messageKey, EntityUid user)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString(messageKey), component.Owner, user);
|
||||
_popupSystem.PopupEntity(Loc.GetString(messageKey), uid, user);
|
||||
}
|
||||
|
||||
private void SafeDeleteSalvage(EntityUid salvage)
|
||||
{
|
||||
if(!EntityManager.TryGetComponent<TransformComponent>(salvage, out var salvageTransform))
|
||||
{
|
||||
Logger.ErrorS("salvage", "Salvage entity was missing transform component");
|
||||
Log.Error("Salvage entity was missing transform component");
|
||||
return;
|
||||
}
|
||||
|
||||
if (salvageTransform.GridUid == null)
|
||||
{
|
||||
Logger.ErrorS("salvage", "Salvage entity has no associated grid?");
|
||||
Log.Error( "Salvage entity has no associated grid?");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -296,125 +296,110 @@ namespace Content.Server.Salvage
|
||||
// Salvage mobs are NEVER immune (even if they're from a different salvage, they shouldn't be here)
|
||||
continue;
|
||||
}
|
||||
Transform(playerEntityUid).AttachParent(salvageTransform.ParentUid);
|
||||
_transform.SetParent(playerEntityUid, salvageTransform.ParentUid);
|
||||
}
|
||||
}
|
||||
|
||||
// Deletion has to happen before grid traversal re-parents players.
|
||||
EntityManager.DeleteEntity(salvage);
|
||||
Del(salvage);
|
||||
}
|
||||
|
||||
private void TryGetSalvagePlacementLocation(SalvageMagnetComponent component, out MapCoordinates coords, out Angle angle)
|
||||
private bool TryGetSalvagePlacementLocation(EntityUid uid, SalvageMagnetComponent component, Box2 bounds, out MapCoordinates coords, out Angle angle)
|
||||
{
|
||||
coords = MapCoordinates.Nullspace;
|
||||
var xform = Transform(uid);
|
||||
angle = Angle.Zero;
|
||||
var tsc = Transform(component.Owner);
|
||||
coords = new EntityCoordinates(component.Owner, component.Offset).ToMap(EntityManager);
|
||||
coords = new EntityCoordinates(uid, new Vector2(0, -component.OffsetRadiusMax)).ToMap(EntityManager, _transform);
|
||||
|
||||
if (_mapManager.TryGetGrid(tsc.GridUid, out var magnetGrid) && TryComp<TransformComponent>(magnetGrid.Owner, out var gridXform))
|
||||
if (xform.GridUid is not null)
|
||||
angle = _transform.GetWorldRotation(Transform(xform.GridUid.Value));
|
||||
|
||||
for (var i = 0; i < SalvageLocationPlaceAttempts; i++)
|
||||
{
|
||||
angle = gridXform.WorldRotation;
|
||||
var randomRadius = _random.NextFloat(component.OffsetRadiusMax);
|
||||
var randomOffset = _random.NextAngle().ToWorldVec() * randomRadius;
|
||||
var finalCoords = coords.Offset(randomOffset);
|
||||
|
||||
var box2 = Box2.CenteredAround(finalCoords.Position, bounds.Size);
|
||||
var box2Rot = new Box2Rotated(box2, angle, finalCoords.Position);
|
||||
|
||||
// This doesn't stop it from spawning on top of random things in space
|
||||
// Might be better like this, ghosts could stop it before
|
||||
if (_mapManager.FindGridsIntersecting(finalCoords.MapId, box2Rot).Any())
|
||||
continue;
|
||||
coords = finalCoords;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<SalvageMapPrototype> GetAllSalvageMaps() =>
|
||||
_prototypeManager.EnumeratePrototypes<SalvageMapPrototype>();
|
||||
|
||||
private bool SpawnSalvage(SalvageMagnetComponent component)
|
||||
private bool SpawnSalvage(EntityUid uid, SalvageMagnetComponent component)
|
||||
{
|
||||
TryGetSalvagePlacementLocation(component, out var spl, out var spAngle);
|
||||
var salvMap = _mapManager.CreateMap();
|
||||
|
||||
var forcedSalvage = _configurationManager.GetCVar(CCVars.SalvageForced);
|
||||
List<SalvageMapPrototype> allSalvageMaps;
|
||||
if (string.IsNullOrWhiteSpace(forcedSalvage))
|
||||
EntityUid? salvageEnt;
|
||||
if (_random.Prob(component.AsteroidChance))
|
||||
{
|
||||
allSalvageMaps = GetAllSalvageMaps().ToList();
|
||||
var asteroidProto = _prototypeManager.Index<WeightedRandomPrototype>(component.AsteroidPool).Pick(_random);
|
||||
salvageEnt = Spawn(asteroidProto, new MapCoordinates(0, 0, salvMap));
|
||||
}
|
||||
else
|
||||
{
|
||||
allSalvageMaps = new();
|
||||
if (_prototypeManager.TryIndex<SalvageMapPrototype>(forcedSalvage, out var forcedMap))
|
||||
var forcedSalvage = _configurationManager.GetCVar(CCVars.SalvageForced);
|
||||
var salvageProto = string.IsNullOrWhiteSpace(forcedSalvage)
|
||||
? _random.Pick(_prototypeManager.EnumeratePrototypes<SalvageMapPrototype>().ToList())
|
||||
: _prototypeManager.Index<SalvageMapPrototype>(forcedSalvage);
|
||||
|
||||
var opts = new MapLoadOptions
|
||||
{
|
||||
allSalvageMaps.Add(forcedMap);
|
||||
}
|
||||
else
|
||||
Offset = new Vector2(0, 0)
|
||||
};
|
||||
|
||||
if (!_map.TryLoad(salvMap, salvageProto.MapPath.ToString(), out var roots, opts) ||
|
||||
roots.FirstOrNull() is not { } root)
|
||||
{
|
||||
Logger.ErrorS("c.s.salvage", $"Unable to get forced salvage map prototype {forcedSalvage}");
|
||||
Report(uid, component.SalvageChannel, "salvage-system-announcement-spawn-debris-disintegrated");
|
||||
_mapManager.DeleteMap(salvMap);
|
||||
return false;
|
||||
}
|
||||
|
||||
salvageEnt = root;
|
||||
}
|
||||
|
||||
SalvageMapPrototype? map = null;
|
||||
Vector2 spawnLocation = Vector2.Zero;
|
||||
|
||||
for (var i = 0; i < allSalvageMaps.Count; i++)
|
||||
var bounds = Comp<MapGridComponent>(salvageEnt.Value).LocalAABB;
|
||||
if (!TryGetSalvagePlacementLocation(uid, component, bounds, out var spawnLocation, out var spawnAngle))
|
||||
{
|
||||
SalvageMapPrototype attemptedMap = _random.PickAndTake(allSalvageMaps);
|
||||
for (var attempt = 0; attempt < SalvageLocationPlaceAttempts; attempt++)
|
||||
{
|
||||
var randomRadius = _random.NextFloat(component.OffsetRadiusMin, component.OffsetRadiusMax);
|
||||
var randomOffset = _random.NextAngle().ToWorldVec() * randomRadius;
|
||||
spawnLocation = spl.Position + randomOffset;
|
||||
|
||||
var box2 = Box2.CenteredAround(spawnLocation + attemptedMap.Bounds.Center, attemptedMap.Bounds.Size);
|
||||
var box2rot = new Box2Rotated(box2, spAngle, spawnLocation);
|
||||
|
||||
// This doesn't stop it from spawning on top of random things in space
|
||||
// Might be better like this, ghosts could stop it before
|
||||
if (!_mapManager.FindGridsIntersecting(spl.MapId, box2rot).Any())
|
||||
{
|
||||
map = attemptedMap;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (map != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (map == null)
|
||||
{
|
||||
Report(component.Owner, component.SalvageChannel, "salvage-system-announcement-spawn-no-debris-available");
|
||||
Report(uid, component.SalvageChannel, "salvage-system-announcement-spawn-no-debris-available");
|
||||
_mapManager.DeleteMap(salvMap);
|
||||
return false;
|
||||
}
|
||||
|
||||
var opts = new MapLoadOptions
|
||||
{
|
||||
Offset = spawnLocation
|
||||
};
|
||||
var salvXForm = Transform(salvageEnt.Value);
|
||||
_transform.SetParent(salvageEnt.Value, salvXForm, _mapManager.GetMapEntityId(spawnLocation.MapId));
|
||||
_transform.SetWorldPosition(salvXForm, spawnLocation.Position);
|
||||
|
||||
var salvageEntityId = _map.LoadGrid(spl.MapId, map.MapPath.ToString(), opts);
|
||||
if (salvageEntityId == null)
|
||||
{
|
||||
Report(component.Owner, component.SalvageChannel, "salvage-system-announcement-spawn-debris-disintegrated");
|
||||
return false;
|
||||
}
|
||||
component.AttachedEntity = salvageEntityId;
|
||||
var gridcomp = EntityManager.EnsureComponent<SalvageGridComponent>(salvageEntityId.Value);
|
||||
gridcomp.SpawnerMagnet = component;
|
||||
component.AttachedEntity = salvageEnt;
|
||||
var gridcomp = EnsureComp<SalvageGridComponent>(salvageEnt.Value);
|
||||
gridcomp.SpawnerMagnet = uid;
|
||||
_transform.SetWorldRotation(salvageEnt.Value, spawnAngle);
|
||||
|
||||
var pulledTransform = EntityManager.GetComponent<TransformComponent>(salvageEntityId.Value);
|
||||
pulledTransform.WorldRotation = spAngle;
|
||||
|
||||
Report(component.Owner, component.SalvageChannel, "salvage-system-announcement-arrived", ("timeLeft", component.HoldTime.TotalSeconds));
|
||||
Report(uid, component.SalvageChannel, "salvage-system-announcement-arrived", ("timeLeft", component.HoldTime.TotalSeconds));
|
||||
_mapManager.DeleteMap(salvMap);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Report(EntityUid source, string channelName, string messageKey, params (string, object)[] args)
|
||||
{
|
||||
if (!TryComp<IntrinsicRadioReceiverComponent>(source, out var radio)) return;
|
||||
|
||||
var message = args.Length == 0 ? Loc.GetString(messageKey) : Loc.GetString(messageKey, args);
|
||||
var channel = _prototypeManager.Index<RadioChannelPrototype>(channelName);
|
||||
_radioSystem.SendRadioMessage(source, message, channel, source);
|
||||
}
|
||||
|
||||
private void Transition(SalvageMagnetComponent magnet, TimeSpan currentTime)
|
||||
private void Transition(EntityUid uid, SalvageMagnetComponent magnet, TimeSpan currentTime)
|
||||
{
|
||||
switch (magnet.MagnetState.StateType)
|
||||
{
|
||||
case MagnetStateType.Attaching:
|
||||
if (SpawnSalvage(magnet))
|
||||
if (SpawnSalvage(uid, magnet))
|
||||
{
|
||||
magnet.MagnetState = new MagnetState(MagnetStateType.Holding, currentTime + magnet.HoldTime);
|
||||
}
|
||||
@@ -424,7 +409,7 @@ namespace Content.Server.Salvage
|
||||
}
|
||||
break;
|
||||
case MagnetStateType.Holding:
|
||||
Report(magnet.Owner, magnet.SalvageChannel, "salvage-system-announcement-losing", ("timeLeft", magnet.DetachingTime.TotalSeconds));
|
||||
Report(uid, magnet.SalvageChannel, "salvage-system-announcement-losing", ("timeLeft", magnet.DetachingTime.TotalSeconds));
|
||||
magnet.MagnetState = new MagnetState(MagnetStateType.Detaching, currentTime + magnet.DetachingTime);
|
||||
break;
|
||||
case MagnetStateType.Detaching:
|
||||
@@ -434,41 +419,42 @@ namespace Content.Server.Salvage
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("salvage", "Salvage detaching was expecting attached entity but it was null");
|
||||
Log.Error("Salvage detaching was expecting attached entity but it was null");
|
||||
}
|
||||
Report(magnet.Owner, magnet.SalvageChannel, "salvage-system-announcement-lost");
|
||||
Report(uid, magnet.SalvageChannel, "salvage-system-announcement-lost");
|
||||
magnet.MagnetState = new MagnetState(MagnetStateType.CoolingDown, currentTime + magnet.CooldownTime);
|
||||
break;
|
||||
case MagnetStateType.CoolingDown:
|
||||
magnet.MagnetState = MagnetState.Inactive;
|
||||
break;
|
||||
}
|
||||
UpdateAppearance(magnet.Owner, magnet);
|
||||
UpdateChargeStateAppearance(magnet.Owner, currentTime, magnet);
|
||||
UpdateAppearance(uid, magnet);
|
||||
UpdateChargeStateAppearance(uid, currentTime, magnet);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var secondsPassed = TimeSpan.FromSeconds(frameTime);
|
||||
// Keep track of time, and state per grid
|
||||
foreach (var gridIdAndState in _salvageGridStates)
|
||||
foreach (var (uid, state) in _salvageGridStates)
|
||||
{
|
||||
var state = gridIdAndState.Value;
|
||||
if (state.ActiveMagnets.Count == 0) continue;
|
||||
var gridId = gridIdAndState.Key;
|
||||
// Not handling the case where the salvage we spawned got paused
|
||||
// They both need to be paused, or it doesn't make sense
|
||||
if (MetaData(gridId).EntityPaused) continue;
|
||||
if (MetaData(uid).EntityPaused) continue;
|
||||
state.CurrentTime += secondsPassed;
|
||||
|
||||
var deleteQueue = new RemQueue<SalvageMagnetComponent>();
|
||||
var deleteQueue = new RemQueue<EntityUid>();
|
||||
|
||||
foreach(var magnet in state.ActiveMagnets)
|
||||
{
|
||||
UpdateChargeStateAppearance(magnet.Owner, state.CurrentTime, magnet);
|
||||
if (magnet.MagnetState.Until > state.CurrentTime) continue;
|
||||
Transition(magnet, state.CurrentTime);
|
||||
if (magnet.MagnetState.StateType == MagnetStateType.Inactive)
|
||||
if (!TryComp<SalvageMagnetComponent>(magnet, out var magnetComp))
|
||||
continue;
|
||||
|
||||
UpdateChargeStateAppearance(magnet, state.CurrentTime, magnetComp);
|
||||
if (magnetComp.MagnetState.Until > state.CurrentTime) continue;
|
||||
Transition(magnet, magnetComp, state.CurrentTime);
|
||||
if (magnetComp.MagnetState.StateType == MagnetStateType.Inactive)
|
||||
{
|
||||
deleteQueue.Add(magnet);
|
||||
}
|
||||
@@ -488,7 +474,7 @@ namespace Content.Server.Salvage
|
||||
public sealed class SalvageGridState
|
||||
{
|
||||
public TimeSpan CurrentTime { get; set; }
|
||||
public List<SalvageMagnetComponent> ActiveMagnets { get; } = new();
|
||||
public List<EntityUid> ActiveMagnets { get; } = new();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user