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:
faint
2023-06-26 17:34:49 +03:00
375 changed files with 19692 additions and 98927 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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();
}
}

View 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>

View 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"))));
}
}

View 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>

View 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
});
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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>

View File

@@ -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)

View File

@@ -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);

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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" />

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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"

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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()

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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();
}
}
}

View 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();
}
}

View 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;
}
}

View 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();
}
}

View File

@@ -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()

View File

@@ -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();
}
}
}

View 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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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));
}
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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));
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
{

View File

@@ -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;

View File

@@ -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));
}
}
}

View File

@@ -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>

View File

@@ -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
{

View File

@@ -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

View File

@@ -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;
}
}
}

View 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;
}

View File

@@ -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)
};
}

View 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);
}
}
}
}

View File

@@ -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);

View File

@@ -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]

View File

@@ -3,5 +3,4 @@
[RegisterComponent]
public sealed class ActiveSolutionHeaterComponent : Component
{
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}
}

View 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);
}
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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";
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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!;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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
{

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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));
}

View File

@@ -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())
{

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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);
}

View 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;
}
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using YamlDotNet.Core.Tokens;
namespace Content.Server.Mind.Components
{

View File

@@ -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>

View File

@@ -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>

View File

@@ -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]

View File

@@ -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);

View File

@@ -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;
}
}
}

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)
{

View File

@@ -9,6 +9,6 @@
/// <summary>
/// The magnet that spawned this grid.
/// </summary>
public SalvageMagnetComponent? SpawnerMagnet;
public EntityUid? SpawnerMagnet;
}
}

View File

@@ -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]

View File

@@ -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;
}

View File

@@ -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"));
}

View File

@@ -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