mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-02-15 04:30:57 +01:00
Merge remote-tracking branch 'upstream/master' into upstream-sync
# Conflicts: # Content.Client/Entry/EntryPoint.cs # Content.Client/IoC/ClientContentIoC.cs # Content.Server/Doors/Components/AirlockComponent.cs # Content.Server/Fax/FaxMachineComponent.cs # Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json
This commit is contained in:
25
Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
Normal file
25
Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
Normal file
@@ -0,0 +1,25 @@
|
||||
<Control
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#25252A"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
<SplitContainer Orientation="Horizontal" VerticalExpand="True">
|
||||
<cc:PlayerListControl Access="Public" Name="ChannelSelector" HorizontalExpand="True" SizeFlagsStretchRatio="1" />
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2">
|
||||
<BoxContainer Access="Public" Name="BwoinkArea" VerticalExpand="True" />
|
||||
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Margin="0 0 10 0" Visible="True" Name="PopOut" Access="Public" Text="{Loc 'admin-logs-pop-out'}"/>
|
||||
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" />
|
||||
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" />
|
||||
<Button Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" />
|
||||
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" />
|
||||
<Button Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" />
|
||||
<Button Visible="False" Name="Teleport" Text="{Loc 'admin-player-actions-teleport'}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</SplitContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Administration.UI.CustomControls;
|
||||
@@ -16,27 +16,33 @@ using Robust.Shared.Network;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
namespace Content.Client.Administration.UI
|
||||
namespace Content.Client.Administration.UI.Bwoink
|
||||
{
|
||||
/// <summary>
|
||||
/// This window connects to a BwoinkSystem channel. BwoinkSystem manages the rest.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BwoinkWindow : DefaultWindow
|
||||
public sealed partial class BwoinkControl : Control
|
||||
{
|
||||
[Dependency] private readonly IClientAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _console = default!;
|
||||
private readonly AdminAHelpUIHandler _adminAHelpHelper;
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
public AdminAHelpUIHandler AHelpHelper = default!;
|
||||
|
||||
//private readonly BwoinkSystem _bwoinkSystem;
|
||||
private PlayerInfo? _currentPlayer = default;
|
||||
|
||||
public BwoinkWindow(AdminAHelpUIHandler adminAHelpHelper)
|
||||
public BwoinkControl()
|
||||
{
|
||||
_adminAHelpHelper = adminAHelpHelper;
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
var uiController = _ui.GetUIController<AHelpUIController>();
|
||||
if (uiController.UIHelper is not AdminAHelpUIHandler helper)
|
||||
return;
|
||||
|
||||
AHelpHelper = helper;
|
||||
|
||||
_adminManager.AdminStatusUpdated += FixButtons;
|
||||
FixButtons();
|
||||
|
||||
@@ -46,7 +52,6 @@ namespace Content.Client.Administration.UI
|
||||
if (sel is not null)
|
||||
{
|
||||
SwitchToChannel(sel.SessionId);
|
||||
Title = $"{sel.CharacterName} / {sel.Username}";
|
||||
}
|
||||
|
||||
ChannelSelector.PlayerListContainer.DirtyList();
|
||||
@@ -62,7 +67,7 @@ namespace Content.Client.Administration.UI
|
||||
sb.Append(info.ActiveThisRound ? '○' : '·');
|
||||
|
||||
sb.Append(' ');
|
||||
if (_adminAHelpHelper.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0)
|
||||
if (AHelpHelper.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0)
|
||||
{
|
||||
if (panel.Unread < 11)
|
||||
sb.Append(new Rune('➀' + (panel.Unread-1)));
|
||||
@@ -81,8 +86,8 @@ namespace Content.Client.Administration.UI
|
||||
|
||||
ChannelSelector.Comparison = (a, b) =>
|
||||
{
|
||||
var ach = _adminAHelpHelper.EnsurePanel(a.SessionId);
|
||||
var bch = _adminAHelpHelper.EnsurePanel(b.SessionId);
|
||||
var ach = AHelpHelper.EnsurePanel(a.SessionId);
|
||||
var bch = AHelpHelper.EnsurePanel(b.SessionId);
|
||||
|
||||
// First, sort by unread. Any chat with unread messages appears first. We just sort based on unread
|
||||
// status, not number of unread messages, so that more recent unread messages take priority.
|
||||
@@ -153,7 +158,10 @@ namespace Content.Client.Administration.UI
|
||||
_console.ExecuteCommand($"respawn \"{_currentPlayer.Username}\"");
|
||||
};
|
||||
|
||||
OnOpen += () => ChannelSelector.PopulateList();
|
||||
PopOut.OnPressed += _ =>
|
||||
{
|
||||
uiController.PopOut();
|
||||
};
|
||||
}
|
||||
|
||||
private Dictionary<Control, (CancellationTokenSource cancellation, string? originalText)> Confirmations { get; } = new();
|
||||
@@ -199,7 +207,7 @@ namespace Content.Client.Administration.UI
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(pl.Connected ? '●' : '○');
|
||||
sb.Append(' ');
|
||||
if (_adminAHelpHelper.TryGetChannel(pl.SessionId, out var panel) && panel.Unread > 0)
|
||||
if (AHelpHelper.TryGetChannel(pl.SessionId, out var panel) && panel.Unread > 0)
|
||||
{
|
||||
if (panel.Unread < 11)
|
||||
sb.Append(new Rune('➀' + (panel.Unread-1)));
|
||||
@@ -225,7 +233,7 @@ namespace Content.Client.Administration.UI
|
||||
{
|
||||
foreach (var bw in BwoinkArea.Children)
|
||||
bw.Visible = false;
|
||||
var panel = _adminAHelpHelper.EnsurePanel(ch);
|
||||
var panel = AHelpHelper.EnsurePanel(ch);
|
||||
panel.Visible = true;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.UserInterface.Systems.Bwoink;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Client.Administration.UI.CustomControls;
|
||||
|
||||
namespace Content.Client.Administration.UI.CustomControls
|
||||
namespace Content.Client.Administration.UI.Bwoink
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BwoinkPanel : BoxContainer
|
||||
@@ -0,0 +1,8 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.Bwoink"
|
||||
SetSize="900 500"
|
||||
HeaderClass="windowHeaderAlert"
|
||||
TitleClass="windowTitleAlert"
|
||||
Title="{Loc 'bwoink-user-title'}" >
|
||||
<cc:BwoinkControl Name="Bwoink" Access="Public"/>
|
||||
</DefaultWindow>
|
||||
42
Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs
Normal file
42
Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Administration.UI.CustomControls;
|
||||
using Content.Client.Administration.UI.Tabs.AdminTab;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Systems.Bwoink;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
namespace Content.Client.Administration.UI.Bwoink
|
||||
{
|
||||
/// <summary>
|
||||
/// This window connects to a BwoinkSystem channel. BwoinkSystem manages the rest.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BwoinkWindow : DefaultWindow
|
||||
{
|
||||
public BwoinkWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
Bwoink.ChannelSelector.OnSelectionChanged += sel =>
|
||||
{
|
||||
if (sel is not null)
|
||||
{
|
||||
Title = $"{sel.CharacterName} / {sel.Username}";
|
||||
}
|
||||
};
|
||||
|
||||
OnOpen += () => Bwoink.ChannelSelector.PopulateList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<DefaultWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
SetSize="900 500"
|
||||
HeaderClass="windowHeaderAlert"
|
||||
TitleClass="windowTitleAlert"
|
||||
Title="{Loc 'bwoink-user-title'}" >
|
||||
<SplitContainer Orientation="Horizontal">
|
||||
<cc:PlayerListControl Access="Public" Name="ChannelSelector" HorizontalExpand="True" SizeFlagsStretchRatio="1" />
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2">
|
||||
<BoxContainer Access="Public" Name="BwoinkArea" VerticalExpand="True" />
|
||||
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" />
|
||||
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" />
|
||||
<Button Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" />
|
||||
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" />
|
||||
<Button Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" />
|
||||
<Button Visible="False" Name="Teleport" Text="{Loc 'admin-player-actions-teleport'}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</SplitContainer>
|
||||
</DefaultWindow>
|
||||
@@ -1,11 +1,13 @@
|
||||
using Robust.Client.Console;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.Guidebook.Richtext;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Client.Administration.UI.CustomControls
|
||||
{
|
||||
[Virtual]
|
||||
public class CommandButton : Button
|
||||
public class CommandButton : Button, IDocumentTag
|
||||
{
|
||||
public string? Command { get; set; }
|
||||
|
||||
@@ -34,5 +36,20 @@ namespace Content.Client.Administration.UI.CustomControls
|
||||
if (!string.IsNullOrEmpty(Command))
|
||||
IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand(Command);
|
||||
}
|
||||
|
||||
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
if (args.Count != 2 || !args.TryGetValue("Text", out var text) || !args.TryGetValue("Command", out var command))
|
||||
{
|
||||
Logger.Error($"Invalid arguments passed to {nameof(CommandButton)}");
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Command = command;
|
||||
Text = Loc.GetString(text);
|
||||
control = this;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
Content.Client/Anomaly/AnomalySystem.cs
Normal file
59
Content.Client/Anomaly/AnomalySystem.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Anomaly;
|
||||
|
||||
public sealed class AnomalySystem : SharedAnomalySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AnomalyComponent, AppearanceChangeEvent>(OnAppearanceChanged);
|
||||
}
|
||||
|
||||
private void OnAppearanceChanged(EntityUid uid, AnomalyComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite is not { } sprite)
|
||||
return;
|
||||
|
||||
if (!Appearance.TryGetData(uid, AnomalyVisuals.IsPulsing, out bool pulsing, args.Component))
|
||||
pulsing = false;
|
||||
|
||||
if (Appearance.TryGetData(uid, AnomalyVisuals.IsPulsing, out bool super, args.Component) && super)
|
||||
pulsing = super;
|
||||
|
||||
if (HasComp<AnomalySupercriticalComponent>(uid))
|
||||
pulsing = true;
|
||||
|
||||
if (!sprite.LayerMapTryGet(AnomalyVisualLayers.Base, out var layer) ||
|
||||
!sprite.LayerMapTryGet(AnomalyVisualLayers.Animated, out var animatedLayer))
|
||||
return;
|
||||
|
||||
sprite.LayerSetVisible(layer, !pulsing);
|
||||
sprite.LayerSetVisible(animatedLayer, pulsing);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
foreach (var (super, sprite) in EntityQuery<AnomalySupercriticalComponent, SpriteComponent>())
|
||||
{
|
||||
var completion = 1f - (float) ((super.EndTime - _timing.CurTime) / super.SupercriticalDuration);
|
||||
var scale = completion * (super.MaxScaleAmount - 1f) + 1f;
|
||||
sprite.Scale = new Vector2(scale, scale);
|
||||
|
||||
var transparency = (byte) (65 * (1f - completion) + 190);
|
||||
if (transparency < sprite.Color.AByte)
|
||||
{
|
||||
sprite.Color = sprite.Color.WithAlpha(transparency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Content.Client/Anomaly/Effects/GravityAnomalySystem.cs
Normal file
8
Content.Client/Anomaly/Effects/GravityAnomalySystem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Anomaly.Effects;
|
||||
|
||||
namespace Content.Client.Anomaly.Effects;
|
||||
|
||||
public sealed class GravityAnomalySystem : SharedGravityAnomalySystem
|
||||
{
|
||||
// this is not the system you are looking for
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Gravity;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client. Anomaly.Ui;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class AnomalyGeneratorBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private AnomalyGeneratorWindow? _window;
|
||||
|
||||
public AnomalyGeneratorBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base (owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = new (Owner.Owner);
|
||||
|
||||
_window.OpenCentered();
|
||||
_window.OnClose += Close;
|
||||
|
||||
_window.OnGenerateButtonPressed += () =>
|
||||
{
|
||||
SendMessage(new AnomalyGeneratorGenerateButtonPressedEvent());
|
||||
};
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not AnomalyGeneratorUserInterfaceState msg)
|
||||
return;
|
||||
_window?.UpdateState(msg);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing) return;
|
||||
|
||||
_window?.Dispose();
|
||||
}
|
||||
|
||||
public void SetPowerSwitch(bool on)
|
||||
{
|
||||
SendMessage(new SharedGravityGeneratorComponent.SwitchGeneratorMessage(on));
|
||||
}
|
||||
}
|
||||
|
||||
48
Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml
Normal file
48
Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml
Normal file
@@ -0,0 +1,48 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'anomaly-generator-ui-title'}"
|
||||
MinSize="270 180"
|
||||
SetSize="360 180">
|
||||
<BoxContainer Margin="10 0 10 0"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Margin="0 0 0 0"
|
||||
VerticalAlignment="Center">
|
||||
<Label Text="{Loc 'anomaly-generator-fuel-display'}" StyleClasses="StatusFieldTitle" />
|
||||
<ProgressBar Name="FuelBar"
|
||||
HorizontalExpand="True"
|
||||
MaxValue="1"
|
||||
MinValue="0"
|
||||
SetHeight="25"
|
||||
Margin="10 0 10 0"
|
||||
VerticalAlignment="Center">
|
||||
<Label Name="FuelText"
|
||||
Margin="4 0"
|
||||
Text="0 %" />
|
||||
</ProgressBar>
|
||||
</BoxContainer>
|
||||
<RichTextLabel Name="CooldownLabel" StyleClasses="StatusFieldTitle" />
|
||||
<RichTextLabel Name="ReadyLabel" StyleClasses="StatusFieldTitle" />
|
||||
</BoxContainer>
|
||||
<PanelContainer Margin="12 0 0 0"
|
||||
StyleClasses="Inset"
|
||||
VerticalAlignment="Center">
|
||||
<SpriteView Name="EntityView"
|
||||
SetSize="96 96"
|
||||
OverrideDirection="South" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer VerticalExpand="True"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Button Name="GenerateButton"
|
||||
Text="{Loc 'anomaly-generator-generate'}"></Button>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
80
Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml.cs
Normal file
80
Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Content.Client.Message;
|
||||
using Content.Shared.Anomaly;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
|
||||
|
||||
namespace Content.Client.Anomaly.Ui;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AnomalyGeneratorWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private TimeSpan _cooldownEnd = TimeSpan.Zero;
|
||||
private bool _hasEnoughFuel;
|
||||
|
||||
public Action? OnGenerateButtonPressed;
|
||||
|
||||
public AnomalyGeneratorWindow(EntityUid gen)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
EntityView.Sprite = _entityManager.GetComponent<SpriteComponent>(gen);
|
||||
|
||||
GenerateButton.OnPressed += _ => OnGenerateButtonPressed?.Invoke();
|
||||
}
|
||||
|
||||
public void UpdateState(AnomalyGeneratorUserInterfaceState state)
|
||||
{
|
||||
_cooldownEnd = state.CooldownEndTime;
|
||||
_hasEnoughFuel = state.FuelCost <= state.FuelAmount;
|
||||
|
||||
var fuelCompletion = Math.Clamp((float) state.FuelAmount / state.FuelCost, 0f, 1f);
|
||||
|
||||
FuelBar.Value = fuelCompletion;
|
||||
FuelText.Text = $"{fuelCompletion:P}";
|
||||
|
||||
UpdateTimer();
|
||||
UpdateReady(); // yes this can trigger twice. no i don't care
|
||||
}
|
||||
|
||||
public void UpdateTimer()
|
||||
{
|
||||
if (_timing.CurTime > _cooldownEnd)
|
||||
{
|
||||
CooldownLabel.SetMarkup(Loc.GetString("anomaly-generator-no-cooldown"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var timeLeft = _cooldownEnd - _timing.CurTime;
|
||||
var timeString = $"{timeLeft.Minutes:0}:{timeLeft.Seconds:00}";
|
||||
CooldownLabel.SetMarkup(Loc.GetString("anomaly-generator-cooldown", ("time", timeString)));
|
||||
UpdateReady();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateReady()
|
||||
{
|
||||
var ready = _hasEnoughFuel && _timing.CurTime > _cooldownEnd;
|
||||
|
||||
var msg = ready
|
||||
? Loc.GetString("anomaly-generator-yes-fire")
|
||||
: Loc.GetString("anomaly-generator-no-fire");
|
||||
ReadyLabel.SetMarkup(msg);
|
||||
|
||||
GenerateButton.Disabled = !ready;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
UpdateTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using Content.Shared.Anomaly;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Anomaly.Ui;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class AnomalyScannerBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private AnomalyScannerMenu? _menu;
|
||||
|
||||
public AnomalyScannerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_menu = new AnomalyScannerMenu();
|
||||
_menu.OpenCentered();
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not AnomalyScannerUserInterfaceState msg)
|
||||
return;
|
||||
|
||||
if (_menu == null)
|
||||
return;
|
||||
|
||||
_menu.LastMessage = msg.Message;
|
||||
_menu.NextPulseTime = msg.NextPulseTime;
|
||||
_menu.UpdateMenu();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
return;
|
||||
_menu?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
12
Content.Client/Anomaly/Ui/AnomalyScannerMenu.xaml
Normal file
12
Content.Client/Anomaly/Ui/AnomalyScannerMenu.xaml
Normal file
@@ -0,0 +1,12 @@
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'anomaly-scanner-ui-title'}"
|
||||
MinSize="350 260"
|
||||
SetSize="350 260">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="10 0 10 10">
|
||||
<RichTextLabel Name="TextDisplay"></RichTextLabel>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
||||
|
||||
47
Content.Client/Anomaly/Ui/AnomalyScannerMenu.xaml.cs
Normal file
47
Content.Client/Anomaly/Ui/AnomalyScannerMenu.xaml.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Content.Client.Message;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Anomaly.Ui;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AnomalyScannerMenu : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public FormattedMessage LastMessage = new();
|
||||
public TimeSpan? NextPulseTime;
|
||||
|
||||
public AnomalyScannerMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
public void UpdateMenu()
|
||||
{
|
||||
var msg = new FormattedMessage(LastMessage);
|
||||
|
||||
if (NextPulseTime != null)
|
||||
{
|
||||
msg.PushNewline();
|
||||
msg.PushNewline();
|
||||
var time = NextPulseTime.Value - _timing.CurTime;
|
||||
var timestring = $"{time.Minutes:00}:{time.Seconds:00}";
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-pulse-timer", ("time", timestring)));
|
||||
}
|
||||
|
||||
TextDisplay.SetMarkup(msg.ToMarkup());
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (NextPulseTime != null)
|
||||
UpdateMenu();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
using Content.Client.Actions;
|
||||
using Content.Client.Decals.Overlays;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Decals;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
@@ -17,9 +18,11 @@ namespace Content.Client.Decals;
|
||||
public sealed class DecalPlacementSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly InputSystem _inputSystem = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlay = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly IMapManager _mapMan = default!;
|
||||
[Dependency] private readonly InputSystem _inputSystem = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
private string? _decalId;
|
||||
private Color _decalColor = Color.White;
|
||||
@@ -32,9 +35,17 @@ public sealed class DecalPlacementSystem : EntitySystem
|
||||
private bool _placing;
|
||||
private bool _erasing;
|
||||
|
||||
public (DecalPrototype? Decal, bool Snap, Angle Angle, Color Color) GetActiveDecal()
|
||||
{
|
||||
return _active && _decalId != null ?
|
||||
(_protoMan.Index<DecalPrototype>(_decalId), _snap, _decalAngle, _decalColor) :
|
||||
(null, false, Angle.Zero, Color.Wheat);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_overlay.AddOverlay(new DecalPlacementOverlay(this, _transform, _sprite));
|
||||
|
||||
CommandBinds.Builder.Bind(EngineKeyFunctions.EditorPlaceObject, new PointerStateInputCmdHandler(
|
||||
(session, coords, uid) =>
|
||||
@@ -158,6 +169,7 @@ public sealed class DecalPlacementSystem : EntitySystem
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
_overlay.RemoveOverlay<DecalPlacementOverlay>();
|
||||
CommandBinds.Unregister<DecalPlacementSystem>();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Client.Decals.Overlays;
|
||||
using Content.Shared.Decals;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -14,15 +15,11 @@ namespace Content.Client.Decals
|
||||
|
||||
private DecalOverlay _overlay = default!;
|
||||
|
||||
// TODO move this data to the component
|
||||
public readonly Dictionary<EntityUid, SortedDictionary<int, SortedDictionary<uint, Decal>>> DecalRenderIndex = new();
|
||||
private readonly Dictionary<EntityUid, Dictionary<uint, int>> _decalZIndexIndex = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_overlay = new DecalOverlay(this, _sprites, EntityManager, PrototypeManager);
|
||||
_overlay = new DecalOverlay(_sprites, EntityManager, PrototypeManager);
|
||||
_overlayManager.AddOverlay(_overlay);
|
||||
|
||||
SubscribeLocalEvent<DecalGridComponent, ComponentHandleState>(OnHandleState);
|
||||
@@ -41,41 +38,25 @@ namespace Content.Client.Decals
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnCompRemove(EntityUid uid, DecalGridComponent component, ComponentRemove args)
|
||||
{
|
||||
DecalRenderIndex.Remove(uid);
|
||||
_decalZIndexIndex.Remove(uid);
|
||||
base.OnCompRemove(uid, component, args);
|
||||
}
|
||||
|
||||
protected override void OnCompAdd(EntityUid uid, DecalGridComponent component, ComponentAdd args)
|
||||
{
|
||||
DecalRenderIndex[uid] = new();
|
||||
_decalZIndexIndex[uid] = new();
|
||||
base.OnCompAdd(uid, component, args);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_overlayManager.RemoveOverlay(_overlay);
|
||||
}
|
||||
|
||||
protected override bool RemoveDecalHook(EntityUid gridId, uint uid)
|
||||
protected override void OnDecalRemoved(EntityUid gridId, uint decalId, DecalGridComponent component, Vector2i indices, DecalChunk chunk)
|
||||
{
|
||||
RemoveDecalFromRenderIndex(gridId, uid);
|
||||
return base.RemoveDecalHook(gridId, uid);
|
||||
}
|
||||
base.OnDecalRemoved(gridId, decalId, component, indices, chunk);
|
||||
|
||||
private void RemoveDecalFromRenderIndex(EntityUid gridId, uint uid)
|
||||
{
|
||||
var zIndex = _decalZIndexIndex[gridId][uid];
|
||||
if (!component.DecalZIndexIndex.Remove(decalId, out var zIndex))
|
||||
return;
|
||||
|
||||
DecalRenderIndex[gridId][zIndex].Remove(uid);
|
||||
if (DecalRenderIndex[gridId][zIndex].Count == 0)
|
||||
DecalRenderIndex[gridId].Remove(zIndex);
|
||||
if (!component.DecalRenderIndex.TryGetValue(zIndex, out var renderIndex))
|
||||
return;
|
||||
|
||||
_decalZIndexIndex[gridId].Remove(uid);
|
||||
renderIndex.Remove(decalId);
|
||||
if (renderIndex.Count == 0)
|
||||
component.DecalRenderIndex.Remove(zIndex);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid gridUid, DecalGridComponent gridComp, ref ComponentHandleState args)
|
||||
@@ -142,14 +123,8 @@ namespace Content.Client.Decals
|
||||
private void UpdateChunks(EntityUid gridId, DecalGridComponent gridComp, Dictionary<Vector2i, DecalChunk> updatedGridChunks)
|
||||
{
|
||||
var chunkCollection = gridComp.ChunkCollection.ChunkCollection;
|
||||
|
||||
if (!ChunkIndex.TryGetValue(gridId, out var chunkIndex) ||
|
||||
!DecalRenderIndex.TryGetValue(gridId, out var renderIndex) ||
|
||||
!_decalZIndexIndex.TryGetValue(gridId, out var zIndexIndex))
|
||||
{
|
||||
Logger.Error($"Grid missing from dictionaries while updating decal chunks for grid {ToPrettyString(gridId)}");
|
||||
return;
|
||||
}
|
||||
var renderIndex = gridComp.DecalRenderIndex;
|
||||
var zIndexIndex = gridComp.DecalZIndexIndex;
|
||||
|
||||
// Update any existing data / remove decals we didn't receive data for.
|
||||
foreach (var (indices, newChunkData) in updatedGridChunks)
|
||||
@@ -160,8 +135,8 @@ namespace Content.Client.Decals
|
||||
removedUids.ExceptWith(newChunkData.Decals.Keys);
|
||||
foreach (var removedUid in removedUids)
|
||||
{
|
||||
RemoveDecalHook(gridId, removedUid);
|
||||
chunkIndex.Remove(removedUid);
|
||||
OnDecalRemoved(gridId, removedUid, gridComp, indices, chunk);
|
||||
gridComp.DecalIndex.Remove(removedUid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +149,7 @@ namespace Content.Client.Decals
|
||||
|
||||
renderIndex.GetOrNew(decal.ZIndex)[uid] = decal;
|
||||
zIndexIndex[uid] = decal.ZIndex;
|
||||
chunkIndex[uid] = indices;
|
||||
gridComp.DecalIndex[uid] = indices;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,20 +158,14 @@ namespace Content.Client.Decals
|
||||
{
|
||||
var chunkCollection = gridComp.ChunkCollection.ChunkCollection;
|
||||
|
||||
if (!ChunkIndex.TryGetValue(gridId, out var chunkIndex))
|
||||
{
|
||||
Logger.Error($"Missing grid in ChunkIndex dictionary while removing chunks from grid {ToPrettyString(gridId)}");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var index in chunks)
|
||||
{
|
||||
if (!chunkCollection.TryGetValue(index, out var chunk)) continue;
|
||||
|
||||
foreach (var uid in chunk.Decals.Keys)
|
||||
foreach (var decalId in chunk.Decals.Keys)
|
||||
{
|
||||
RemoveDecalHook(gridId, uid);
|
||||
chunkIndex.Remove(uid);
|
||||
OnDecalRemoved(gridId, decalId, gridComp, index, chunk);
|
||||
gridComp.DecalIndex.Remove(decalId);
|
||||
}
|
||||
|
||||
chunkCollection.Remove(index);
|
||||
|
||||
@@ -2,15 +2,12 @@ using Content.Shared.Decals;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Decals
|
||||
namespace Content.Client.Decals.Overlays
|
||||
{
|
||||
public sealed class DecalOverlay : Overlay
|
||||
{
|
||||
private readonly DecalSystem _decals;
|
||||
private readonly SpriteSystem _sprites;
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly IPrototypeManager _prototypeManager;
|
||||
@@ -20,12 +17,10 @@ namespace Content.Client.Decals
|
||||
private readonly Dictionary<string, (Texture Texture, bool SnapCardinals)> _cachedTextures = new(64);
|
||||
|
||||
public DecalOverlay(
|
||||
DecalSystem decals,
|
||||
SpriteSystem sprites,
|
||||
IEntityManager entManager,
|
||||
IPrototypeManager prototypeManager)
|
||||
{
|
||||
_decals = decals;
|
||||
_sprites = sprites;
|
||||
_entManager = entManager;
|
||||
_prototypeManager = prototypeManager;
|
||||
@@ -38,17 +33,14 @@ namespace Content.Client.Decals
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
var eyeAngle = args.Viewport.Eye?.Rotation ?? Angle.Zero;
|
||||
|
||||
foreach (var (gridId, zIndexDictionary) in _decals.DecalRenderIndex)
|
||||
foreach (var (decalGrid, xform) in _entManager.EntityQuery<DecalGridComponent, TransformComponent>(true))
|
||||
{
|
||||
var gridId = decalGrid.Owner;
|
||||
var zIndexDictionary = decalGrid.DecalRenderIndex;
|
||||
|
||||
if (zIndexDictionary.Count == 0)
|
||||
continue;
|
||||
|
||||
if (!xformQuery.TryGetComponent(gridId, out var xform))
|
||||
{
|
||||
Logger.Error($"Tried to draw decals on a non-existent grid. GridUid: {gridId}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (xform.MapID != args.MapId)
|
||||
continue;
|
||||
|
||||
@@ -62,8 +54,7 @@ namespace Content.Client.Decals
|
||||
{
|
||||
if (!_cachedTextures.TryGetValue(decal.Id, out var cache) && _prototypeManager.TryIndex<DecalPrototype>(decal.Id, out var decalProto))
|
||||
{
|
||||
var sprite = GetDecalSprite(decal.Id);
|
||||
cache = (_sprites.Frame0(sprite), decalProto.SnapCardinals);
|
||||
cache = (_sprites.Frame0(decalProto.Sprite), decalProto.SnapCardinals);
|
||||
_cachedTextures[decal.Id] = cache;
|
||||
}
|
||||
|
||||
@@ -87,14 +78,5 @@ namespace Content.Client.Decals
|
||||
|
||||
handle.SetTransform(Matrix3.Identity);
|
||||
}
|
||||
|
||||
public SpriteSpecifier GetDecalSprite(string id)
|
||||
{
|
||||
if (_prototypeManager.TryIndex<DecalPrototype>(id, out var proto))
|
||||
return proto.Sprite;
|
||||
|
||||
Logger.Error($"Unknown decal prototype: {id}");
|
||||
return new SpriteSpecifier.Texture(new ResourcePath("/Textures/noSprite.png"));
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Content.Client/Decals/Overlays/DecalPlacementOverlay.cs
Normal file
67
Content.Client/Decals/Overlays/DecalPlacementOverlay.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Client.Decals.Overlays;
|
||||
|
||||
public sealed class DecalPlacementOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
private readonly DecalPlacementSystem _placement;
|
||||
private readonly SharedTransformSystem _transform;
|
||||
private readonly SpriteSystem _sprite;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
public DecalPlacementOverlay(DecalPlacementSystem placement, SharedTransformSystem transform, SpriteSystem sprite)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_placement = placement;
|
||||
_transform = transform;
|
||||
_sprite = sprite;
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var (decal, snap, rotation, color) = _placement.GetActiveDecal();
|
||||
|
||||
if (decal == null)
|
||||
return;
|
||||
|
||||
var mouseScreenPos = _inputManager.MouseScreenPosition;
|
||||
var mousePos = _eyeManager.ScreenToMap(mouseScreenPos);
|
||||
|
||||
if (mousePos.MapId != args.MapId)
|
||||
return;
|
||||
|
||||
// No map support for decals
|
||||
if (!_mapManager.TryFindGridAt(mousePos, out var grid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var worldMatrix = _transform.GetWorldMatrix(grid.Owner);
|
||||
var invMatrix = _transform.GetInvWorldMatrix(grid.Owner);
|
||||
|
||||
var handle = args.WorldHandle;
|
||||
handle.SetTransform(worldMatrix);
|
||||
|
||||
var localPos = invMatrix.Transform(mousePos.Position);
|
||||
|
||||
if (snap)
|
||||
{
|
||||
localPos = (Vector2) localPos.Floored() + grid.TileSize / 2f;
|
||||
}
|
||||
|
||||
// Nothing uses snap cardinals so probably don't need preview?
|
||||
var aabb = Box2.UnitCentered.Translated(localPos);
|
||||
var box = new Box2Rotated(aabb, rotation, localPos);
|
||||
|
||||
handle.DrawTextureRect(_sprite.Frame0(decal.Sprite), box, color);
|
||||
handle.SetTransform(Matrix3.Identity);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
using Content.Shared.Doors.Components;
|
||||
|
||||
namespace Content.Client.Doors;
|
||||
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedAirlockComponent))]
|
||||
public sealed class AirlockComponent : SharedAirlockComponent { }
|
||||
@@ -8,6 +8,7 @@ using Content.Client.Options;
|
||||
using Content.Client.Eui;
|
||||
using Content.Client.Flash;
|
||||
using Content.Client.GhostKick;
|
||||
using Content.Client.Guidebook;
|
||||
using Content.Client.Info;
|
||||
using Content.Client.Input;
|
||||
using Content.Client.IoC;
|
||||
@@ -64,6 +65,7 @@ namespace Content.Client.Entry
|
||||
[Dependency] private readonly IVoteManager _voteManager = default!;
|
||||
[Dependency] private readonly IGamePrototypeLoadManager _gamePrototypeLoadManager = default!;
|
||||
[Dependency] private readonly NetworkResourceManager _networkResources = default!;
|
||||
[Dependency] private readonly DocumentParsingManager _documentParsingManager = default!;
|
||||
[Dependency] private readonly GhostKickManager _ghostKick = default!;
|
||||
[Dependency] private readonly ExtendedDisconnectInformationManager _extendedDisconnectInformation = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
@@ -168,6 +170,7 @@ namespace Content.Client.Entry
|
||||
_sponsorsManager.Initialize(); // Corvax-Sponsors
|
||||
_queueManager.Initialize(); // Corvax-Queue
|
||||
_ttsManager.Initialize(); // Corvax-TTS
|
||||
_documentParsingManager.Initialize();
|
||||
|
||||
_baseClient.RunLevelChanged += (_, args) =>
|
||||
{
|
||||
|
||||
@@ -40,7 +40,8 @@ public sealed class ExamineButton : ContainerButton
|
||||
Disabled = true;
|
||||
}
|
||||
|
||||
ToolTip = verb.Message;
|
||||
ToolTip = verb.Message ?? verb.Text;
|
||||
TooltipDelay = 0.3f; // if you're hovering over these icons, you probably want to know what they do.
|
||||
|
||||
Icon = new TextureRect
|
||||
{
|
||||
|
||||
@@ -306,6 +306,8 @@ namespace Content.Client.Examine
|
||||
if (obj.Button is ExamineButton button)
|
||||
{
|
||||
_verbSystem.ExecuteVerb(_examinedEntity, button.Verb);
|
||||
if (button.Verb.CloseMenu ?? button.Verb.CloseMenuDefault)
|
||||
CloseTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,8 @@ namespace Content.Client.Gameplay
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerMan = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
private FpsCounter _fpsCounter = default!;
|
||||
|
||||
|
||||
23
Content.Client/Guidebook/Components/GuideHelpComponent.cs
Normal file
23
Content.Client/Guidebook/Components/GuideHelpComponent.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
|
||||
namespace Content.Client.Guidebook;
|
||||
|
||||
/// <summary>
|
||||
/// This component stores a reference to a guidebook that contains information relevant to this entity.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class GuideHelpComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// What guides to include show when opening the guidebook. The first entry will be used to select the currently
|
||||
/// selected guidebook.
|
||||
/// </summary>
|
||||
[DataField("guides", customTypeSerializer: typeof(PrototypeIdListSerializer<GuideEntryPrototype>), required: true)]
|
||||
public List<string> Guides = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to automatically include the children of the given guides.
|
||||
/// </summary>
|
||||
[DataField("includeChildren")]
|
||||
public bool IncludeChildren = true;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Content.Client.Guidebook;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for the guidebook monkey.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class GuidebookControlsTestComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
6
Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml
Normal file
6
Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml
Normal file
@@ -0,0 +1,6 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical"
|
||||
Margin="5 5 5 5">
|
||||
<SpriteView Name="View"/>
|
||||
<Label Name="Caption" HorizontalAlignment="Center"/>
|
||||
</BoxContainer>
|
||||
172
Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs
Normal file
172
Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.ContextMenu.UI;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.Guidebook.Richtext;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Client.Verbs.UI;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Client.Guidebook.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Control for embedding an entity into a guidebook/document. This is effectively a sprite-view that supports
|
||||
/// examination, interactions, and captions.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
|
||||
private readonly TagSystem _tagSystem;
|
||||
private readonly ExamineSystem _examineSystem;
|
||||
private readonly GuidebookSystem _guidebookSystem;
|
||||
|
||||
public bool Interactive;
|
||||
|
||||
public SpriteComponent? Sprite
|
||||
{
|
||||
get => View.Sprite;
|
||||
set => View.Sprite = value;
|
||||
}
|
||||
|
||||
public Vector2 Scale
|
||||
{
|
||||
get => View.Scale;
|
||||
set => View.Scale = value;
|
||||
}
|
||||
|
||||
public GuideEntityEmbed()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_tagSystem = _systemManager.GetEntitySystem<TagSystem>();
|
||||
_examineSystem = _systemManager.GetEntitySystem<ExamineSystem>();
|
||||
_guidebookSystem = _systemManager.GetEntitySystem<GuidebookSystem>();
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
}
|
||||
|
||||
public GuideEntityEmbed(string proto, bool caption, bool interactive) : this()
|
||||
{
|
||||
Interactive = interactive;
|
||||
|
||||
var ent = _entityManager.SpawnEntity(proto, MapCoordinates.Nullspace);
|
||||
Sprite = _entityManager.GetComponent<SpriteComponent>(ent);
|
||||
|
||||
if (caption)
|
||||
Caption.Text = _entityManager.GetComponent<MetaDataComponent>(ent).EntityName;
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindDown(args);
|
||||
// get an entity associated with this element
|
||||
var entity = Sprite?.Owner;
|
||||
|
||||
// Deleted() automatically checks for null & existence.
|
||||
if (_entityManager.Deleted(entity))
|
||||
return;
|
||||
|
||||
// do examination?
|
||||
if (args.Function == ContentKeyFunctions.ExamineEntity)
|
||||
{
|
||||
_examineSystem.DoExamine(entity.Value);
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Interactive)
|
||||
return;
|
||||
|
||||
// open verb menu?
|
||||
if (args.Function == EngineKeyFunctions.UseSecondary)
|
||||
{
|
||||
_ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(entity.Value);
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
// from here out we're faking interactions! sue me. --moony
|
||||
|
||||
if (args.Function == ContentKeyFunctions.ActivateItemInWorld)
|
||||
{
|
||||
_guidebookSystem.FakeClientActivateInWorld(entity.Value);
|
||||
_ui.GetUIController<ContextMenuUIController>().Close();
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.AltActivateItemInWorld)
|
||||
{
|
||||
_guidebookSystem.FakeClientAltActivateInWorld(entity.Value);
|
||||
_ui.GetUIController<ContextMenuUIController>().Close();
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.AltActivateItemInWorld)
|
||||
{
|
||||
_guidebookSystem.FakeClientUse(entity.Value);
|
||||
_ui.GetUIController<ContextMenuUIController>().Close();
|
||||
args.Handle();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (Sprite is not null)
|
||||
_entityManager.DeleteEntity(Sprite.Owner);
|
||||
}
|
||||
|
||||
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
if (!args.TryGetValue("Entity", out var proto))
|
||||
{
|
||||
Logger.Error("Entity embed tag is missing entity prototype argument");
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var ent = _entityManager.SpawnEntity(proto, MapCoordinates.Nullspace);
|
||||
|
||||
_tagSystem.AddTag(ent, GuidebookSystem.GuideEmbedTag);
|
||||
Sprite = _entityManager.GetComponent<SpriteComponent>(ent);
|
||||
|
||||
if (!args.TryGetValue("Caption", out var caption))
|
||||
caption = _entityManager.GetComponent<MetaDataComponent>(ent).EntityName;
|
||||
|
||||
if (!string.IsNullOrEmpty(caption))
|
||||
Caption.Text = caption;
|
||||
// else:
|
||||
// caption text already defaults to null
|
||||
|
||||
if (args.TryGetValue("Scale", out var scaleStr))
|
||||
{
|
||||
var scale = float.Parse(scaleStr);
|
||||
Scale = new Vector2(scale, scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
Scale = (2, 2);
|
||||
}
|
||||
|
||||
if (args.TryGetValue("Interactive", out var interactive))
|
||||
Interactive = bool.Parse(interactive);
|
||||
|
||||
Margin = new Thickness(4, 8);
|
||||
|
||||
control = this;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
26
Content.Client/Guidebook/Controls/GuidebookWindow.xaml
Normal file
26
Content.Client/Guidebook/Controls/GuidebookWindow.xaml
Normal file
@@ -0,0 +1,26 @@
|
||||
<controls:FancyWindow xmlns:ui="clr-namespace:Content.Client.UserInterface"
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:fancyTree="clr-namespace:Content.Client.UserInterface.Controls.FancyTree"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
SetSize="750 700"
|
||||
MinSize="100 200"
|
||||
Resizable="True"
|
||||
Title="{Loc 'guidebook-window-title'}">
|
||||
<SplitContainer Orientation="Horizontal" HorizontalExpand="True" Name="Split">
|
||||
<!-- Guide select -->
|
||||
<BoxContainer Orientation="Horizontal" Name="TreeBox">
|
||||
<fancyTree:FancyTree Name="Tree" VerticalExpand="True" HorizontalExpand="True"/>
|
||||
<cc:VSeparator StyleClasses="LowDivider" Margin="0 -2"/>
|
||||
</BoxContainer>
|
||||
<ScrollContainer Name="Scroll" HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
|
||||
<Control>
|
||||
<BoxContainer Orientation="Vertical" Name="EntryContainer" Margin="5 5 5 5" Visible="False"/>
|
||||
<BoxContainer Orientation="Vertical" Name="Placeholder" Margin="5 5 5 5">
|
||||
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Loc 'guidebook-placeholder-text'}"/>
|
||||
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Loc 'guidebook-placeholder-text-2'}"/>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
</ScrollContainer>
|
||||
</SplitContainer>
|
||||
</controls:FancyWindow>
|
||||
142
Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
Normal file
142
Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Controls.FancyTree;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.ContentPack;
|
||||
|
||||
namespace Content.Client.Guidebook.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GuidebookWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly DocumentParsingManager _parsingMan = default!;
|
||||
|
||||
private Dictionary<string, GuideEntry> _entries = new();
|
||||
|
||||
public GuidebookWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
Tree.OnSelectedItemChanged += OnSelectionChanged;
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(TreeItem? item)
|
||||
{
|
||||
if (item != null && item.Metadata is GuideEntry entry)
|
||||
ShowGuide(entry);
|
||||
else
|
||||
ClearSelectedGuide();
|
||||
}
|
||||
|
||||
public void ClearSelectedGuide()
|
||||
{
|
||||
Placeholder.Visible = true;
|
||||
EntryContainer.Visible = false;
|
||||
EntryContainer.RemoveAllChildren();
|
||||
}
|
||||
|
||||
private void ShowGuide(GuideEntry entry)
|
||||
{
|
||||
Scroll.SetScrollValue(default);
|
||||
Placeholder.Visible = false;
|
||||
EntryContainer.Visible = true;
|
||||
EntryContainer.RemoveAllChildren();
|
||||
using var file = _resourceManager.ContentFileReadText(entry.Text);
|
||||
|
||||
if (!_parsingMan.TryAddMarkup(EntryContainer, file.ReadToEnd()))
|
||||
{
|
||||
EntryContainer.AddChild(new Label() { Text = "ERROR: Failed to parse document." });
|
||||
Logger.Error($"Failed to parse contents of guide document {entry.Id}.");
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateGuides(
|
||||
Dictionary<string, GuideEntry> entries,
|
||||
List<string>? rootEntries = null,
|
||||
string? forceRoot = null,
|
||||
string? selected = null)
|
||||
{
|
||||
_entries = entries;
|
||||
RepopulateTree(rootEntries, forceRoot);
|
||||
ClearSelectedGuide();
|
||||
|
||||
Split.State = SplitContainer.SplitState.Auto;
|
||||
if (entries.Count == 1)
|
||||
{
|
||||
TreeBox.Visible = false;
|
||||
Split.ResizeMode = SplitContainer.SplitResizeMode.NotResizable;
|
||||
selected = entries.Keys.First();
|
||||
}
|
||||
else
|
||||
{
|
||||
TreeBox.Visible = true;
|
||||
Split.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
|
||||
}
|
||||
|
||||
if (selected != null)
|
||||
{
|
||||
var item = Tree.Items.FirstOrDefault(x => x.Metadata is GuideEntry entry && entry.Id == selected);
|
||||
Tree.SetSelectedIndex(item?.Index);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<GuideEntry> GetSortedRootEntries(List<string>? rootEntries)
|
||||
{
|
||||
if (rootEntries == null)
|
||||
{
|
||||
HashSet<string> entries = new(_entries.Keys);
|
||||
foreach (var entry in _entries.Values)
|
||||
{
|
||||
entries.ExceptWith(entry.Children);
|
||||
}
|
||||
rootEntries = entries.ToList();
|
||||
}
|
||||
|
||||
return rootEntries
|
||||
.Select(x => _entries[x])
|
||||
.OrderBy(x => x.Priority)
|
||||
.ThenBy(x => Loc.GetString(x.Name));
|
||||
}
|
||||
|
||||
private void RepopulateTree(List<string>? roots = null, string? forcedRoot = null)
|
||||
{
|
||||
Tree.Clear();
|
||||
|
||||
HashSet<string> addedEntries = new();
|
||||
|
||||
TreeItem? parent = forcedRoot == null ? null : AddEntry(forcedRoot, null, addedEntries);
|
||||
foreach (var entry in GetSortedRootEntries(roots))
|
||||
{
|
||||
AddEntry(entry.Id, parent, addedEntries);
|
||||
}
|
||||
Tree.SetAllExpanded(true);
|
||||
}
|
||||
|
||||
private TreeItem? AddEntry(string id, TreeItem? parent, HashSet<string> addedEntries)
|
||||
{
|
||||
if (!_entries.TryGetValue(id, out var entry))
|
||||
return null;
|
||||
|
||||
if (!addedEntries.Add(id))
|
||||
{
|
||||
Logger.Error($"Adding duplicate guide entry: {id}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var item = Tree.AddItem(parent);
|
||||
item.Metadata = entry;
|
||||
var name = Loc.GetString(entry.Name);
|
||||
item.Label.Text = name;
|
||||
|
||||
foreach (var child in entry.Children)
|
||||
{
|
||||
AddEntry(child, item, addedEntries);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
84
Content.Client/Guidebook/DocumentParsingManager.cs
Normal file
84
Content.Client/Guidebook/DocumentParsingManager.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Guidebook.Richtext;
|
||||
using Pidgin;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Sandboxing;
|
||||
using static Pidgin.Parser;
|
||||
|
||||
namespace Content.Client.Guidebook;
|
||||
|
||||
/// <summary>
|
||||
/// This manager should be used to convert documents (shitty rich-text / pseudo-xaml) into UI Controls
|
||||
/// </summary>
|
||||
public sealed partial class DocumentParsingManager
|
||||
{
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly ISandboxHelper _sandboxHelper = default!;
|
||||
|
||||
private readonly Dictionary<string, Parser<char, Control>> _tagControlParsers = new();
|
||||
private Parser<char, Control> _tagParser = default!;
|
||||
private Parser<char, Control> _controlParser = default!;
|
||||
public Parser<char, IEnumerable<Control>> ControlParser = default!;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_tagParser = TryOpeningTag
|
||||
.Assert(_tagControlParsers.ContainsKey, tag => $"unknown tag: {tag}")
|
||||
.Bind(tag => _tagControlParsers[tag]);
|
||||
|
||||
_controlParser = OneOf(_tagParser, TryHeaderControl, ListControlParser, TextControlParser).Before(SkipWhitespaces);
|
||||
|
||||
foreach (var typ in _reflectionManager.GetAllChildren<IDocumentTag>())
|
||||
{
|
||||
_tagControlParsers.Add(typ.Name, CreateTagControlParser(typ.Name, typ, _sandboxHelper));
|
||||
}
|
||||
|
||||
ControlParser = SkipWhitespaces.Then(_controlParser.Many());
|
||||
}
|
||||
|
||||
public bool TryAddMarkup(Control control, string text, bool log = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var child in ControlParser.ParseOrThrow(text))
|
||||
{
|
||||
control.AddChild(child);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (log)
|
||||
Logger.Error($"Encountered error while generating markup controls: {e}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Parser<char, Control> CreateTagControlParser(string tagId, Type tagType, ISandboxHelper sandbox) => Map(
|
||||
(args, controls) =>
|
||||
{
|
||||
var tag = (IDocumentTag) sandbox.CreateInstance(tagType);
|
||||
if (!tag.TryParseTag(args, out var control))
|
||||
{
|
||||
Logger.Error($"Failed to parse {tagId} args");
|
||||
return new Control();
|
||||
}
|
||||
|
||||
foreach (var child in controls)
|
||||
{
|
||||
control.AddChild(child);
|
||||
}
|
||||
return control;
|
||||
},
|
||||
ParseTagArgs(tagId),
|
||||
TagContentParser(tagId)).Labelled($"{tagId} control");
|
||||
|
||||
// Parse a bunch of controls until we encounter a matching closing tag.
|
||||
private Parser<char, IEnumerable<Control>> TagContentParser(string tag) =>
|
||||
OneOf(
|
||||
Try(ImmediateTagEnd).ThenReturn(Enumerable.Empty<Control>()),
|
||||
TagEnd.Then(_controlParser.Until(TryTagTerminator(tag)).Labelled($"{tag} children"))
|
||||
);
|
||||
}
|
||||
142
Content.Client/Guidebook/DocumentParsingManager.static.cs
Normal file
142
Content.Client/Guidebook/DocumentParsingManager.static.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.Linq;
|
||||
using Pidgin;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Utility;
|
||||
using static Pidgin.Parser;
|
||||
using static Pidgin.Parser<char>;
|
||||
using static Robust.Client.UserInterface.Control;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Guidebook;
|
||||
|
||||
public sealed partial class DocumentParsingManager
|
||||
{
|
||||
private const string ListBullet = " › ";
|
||||
|
||||
#region Text Parsing
|
||||
#region Basic Text Parsing
|
||||
// Try look for an escaped character. If found, skip the escaping slash and return the character.
|
||||
private static readonly Parser<char, char> TryEscapedChar = Try(Char('\\').Then(OneOf(
|
||||
Try(Char('<')),
|
||||
Try(Char('>')),
|
||||
Try(Char('\\')),
|
||||
Try(Char('-')),
|
||||
Try(Char('=')),
|
||||
Try(Char('"')),
|
||||
Try(Char(' ')),
|
||||
Try(Char('n')).ThenReturn('\n'),
|
||||
Try(Char('t')).ThenReturn('\t')
|
||||
)));
|
||||
|
||||
private static readonly Parser<char, Unit> SkipNewline = Whitespace.SkipUntil(Char('\n'));
|
||||
|
||||
private static readonly Parser<char, char> TrySingleNewlineToSpace = Try(SkipNewline).Then(SkipWhitespaces).ThenReturn(' ');
|
||||
|
||||
private static readonly Parser<char, char> TextChar = OneOf(
|
||||
TryEscapedChar, // consume any backslashed being used to escape text
|
||||
TrySingleNewlineToSpace, // turn single newlines into spaces
|
||||
Any // just return the character.
|
||||
);
|
||||
|
||||
// like TextChar, but not skipping whitespace around newlines
|
||||
private static readonly Parser<char, char> QuotedTextChar = OneOf(TryEscapedChar, Any);
|
||||
|
||||
// Quoted text
|
||||
private static readonly Parser<char, string> QuotedText = Char('"').Then(QuotedTextChar.Until(Try(Char('"'))).Select(string.Concat)).Labelled("quoted text");
|
||||
#endregion
|
||||
|
||||
#region rich text-end markers
|
||||
private static readonly Parser<char, Unit> TryStartList = Try(SkipNewline.Then(SkipWhitespaces).Then(Char('-'))).Then(SkipWhitespaces);
|
||||
private static readonly Parser<char, Unit> TryStartTag = Try(Char('<')).Then(SkipWhitespaces);
|
||||
private static readonly Parser<char, Unit> TryStartParagraph = Try(SkipNewline.Then(SkipNewline)).Then(SkipWhitespaces);
|
||||
private static readonly Parser<char, Unit> TryLookTextEnd = Lookahead(OneOf(TryStartTag, TryStartList, TryStartParagraph, Try(Whitespace.SkipUntil(End))));
|
||||
#endregion
|
||||
|
||||
// parses text characters until it hits a text-end
|
||||
private static readonly Parser<char, string> TextParser = TextChar.AtLeastOnceUntil(TryLookTextEnd).Select(string.Concat);
|
||||
|
||||
private static readonly Parser<char, Control> TextControlParser = Try(Map(text =>
|
||||
{
|
||||
var rt = new RichTextLabel()
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 0, 0, 15.0f),
|
||||
};
|
||||
|
||||
var msg = new FormattedMessage();
|
||||
// THANK YOU RICHTEXT VERY COOL
|
||||
// (text doesn't default to white).
|
||||
msg.PushColor(Color.White);
|
||||
msg.AddMarkup(text);
|
||||
msg.Pop();
|
||||
rt.SetMessage(msg);
|
||||
return rt;
|
||||
}, TextParser).Cast<Control>()).Labelled("richtext");
|
||||
#endregion
|
||||
|
||||
#region Headers
|
||||
private static readonly Parser<char, Control> HeaderControlParser = Try(Char('#')).Then(SkipWhitespaces.Then(Map(text => new Label()
|
||||
{
|
||||
Text = text,
|
||||
StyleClasses = { "LabelHeadingBigger" }
|
||||
}, AnyCharExcept('\n').AtLeastOnceString()).Cast<Control>())).Labelled("header");
|
||||
|
||||
private static readonly Parser<char, Control> SubHeaderControlParser = Try(String("##")).Then(SkipWhitespaces.Then(Map(text => new Label()
|
||||
{
|
||||
Text = text,
|
||||
StyleClasses = { "LabelHeading" }
|
||||
}, AnyCharExcept('\n').AtLeastOnceString()).Cast<Control>())).Labelled("subheader");
|
||||
|
||||
private static readonly Parser<char, Control> TryHeaderControl = OneOf(SubHeaderControlParser, HeaderControlParser);
|
||||
#endregion
|
||||
|
||||
// Parser that consumes a - and then just parses normal rich text with some prefix text (a bullet point).
|
||||
private static readonly Parser<char, Control> ListControlParser = Try(Char('-')).Then(SkipWhitespaces).Then(Map(
|
||||
control => new BoxContainer()
|
||||
{
|
||||
Children = { new Label() { Text = ListBullet, VerticalAlignment = VAlignment.Top, }, control },
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
}, TextControlParser).Cast<Control>()).Labelled("list");
|
||||
|
||||
#region Tag Parsing
|
||||
// closing brackets for tags
|
||||
private static readonly Parser<char, Unit> TagEnd = Char('>').Then(SkipWhitespaces);
|
||||
private static readonly Parser<char, Unit> ImmediateTagEnd = String("/>").Then(SkipWhitespaces);
|
||||
|
||||
private static readonly Parser<char, Unit> TryLookTagEnd = Lookahead(OneOf(Try(TagEnd), Try(ImmediateTagEnd)));
|
||||
|
||||
//parse tag argument key. any normal text character up until we hit a "="
|
||||
private static readonly Parser<char, string> TagArgKey = LetterOrDigit.Until(Char('=')).Select(string.Concat).Labelled("tag argument key");
|
||||
|
||||
// parser for a singular tag argument. Note that each TryQuoteOrChar will consume a whole quoted block before the Until() looks for whitespace
|
||||
private static readonly Parser<char, (string, string)> TagArgParser = Map((key, value) => (key, value), TagArgKey, QuotedText).Before(SkipWhitespaces);
|
||||
|
||||
// parser for all tag arguments
|
||||
private static readonly Parser<char, IEnumerable<(string, string)>> TagArgsParser = TagArgParser.Until(TryLookTagEnd);
|
||||
|
||||
// parser for an opening tag.
|
||||
private static readonly Parser<char, string> TryOpeningTag =
|
||||
Try(Char('<'))
|
||||
.Then(SkipWhitespaces)
|
||||
.Then(TextChar.Until(OneOf(Whitespace.SkipAtLeastOnce(), TryLookTagEnd)))
|
||||
.Select(string.Concat).Labelled($"opening tag");
|
||||
|
||||
private static Parser<char, Dictionary<string, string>> ParseTagArgs(string tag)
|
||||
{
|
||||
return TagArgsParser.Labelled($"{tag} arguments")
|
||||
.Select(x => x.ToDictionary(y => y.Item1, y => y.Item2))
|
||||
.Before(SkipWhitespaces);
|
||||
}
|
||||
|
||||
private static Parser<char, Unit> TryTagTerminator(string tag)
|
||||
{
|
||||
return Try(String("</"))
|
||||
.Then(SkipWhitespaces)
|
||||
.Then(String(tag))
|
||||
.Then(SkipWhitespaces)
|
||||
.Then(TagEnd)
|
||||
.Labelled($"closing {tag} tag");
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
43
Content.Client/Guidebook/GuideEntry.cs
Normal file
43
Content.Client/Guidebook/GuideEntry.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Guidebook;
|
||||
|
||||
[Virtual]
|
||||
public class GuideEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The file containing the contents of this guide.
|
||||
/// </summary>
|
||||
[DataField("text", required: true)] public ResourcePath Text = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The unique id for this guide.
|
||||
/// </summary>
|
||||
[IdDataField]
|
||||
public string Id = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The name of this guide. This gets localized.
|
||||
/// </summary>
|
||||
[DataField("name", required: true)] public string Name = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The "children" of this guide for when guides are shown in a tree / table of contents.
|
||||
/// </summary>
|
||||
[DataField("children", customTypeSerializer:typeof(PrototypeIdListSerializer<GuideEntryPrototype>))]
|
||||
public List<string> Children = new();
|
||||
|
||||
/// <summary>
|
||||
/// Priority for sorting top-level guides when shown in a tree / table of contents.
|
||||
/// If the guide is the child of some other guide, the order simply determined by the order of children in <see cref="Children"/>.
|
||||
/// </summary>
|
||||
[DataField("priority")] public int Priority = 0;
|
||||
}
|
||||
|
||||
[Prototype("guideEntry")]
|
||||
public sealed class GuideEntryPrototype : GuideEntry, IPrototype
|
||||
{
|
||||
public string ID => Id;
|
||||
}
|
||||
244
Content.Client/Guidebook/GuidebookSystem.cs
Normal file
244
Content.Client/Guidebook/GuidebookSystem.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Guidebook.Controls;
|
||||
using Content.Client.Light;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Light.Component;
|
||||
using Content.Shared.Speech;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Guidebook;
|
||||
|
||||
/// <summary>
|
||||
/// This system handles the help-verb and interactions with various client-side entities that are embedded into guidebooks.
|
||||
/// </summary>
|
||||
public sealed class GuidebookSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly VerbSystem _verbSystem = default!;
|
||||
[Dependency] private readonly RgbLightControllerSystem _rgbLightControllerSystem = default!;
|
||||
[Dependency] private readonly TagSystem _tags = default!;
|
||||
private GuidebookWindow _guideWindow = default!;
|
||||
|
||||
public const string GuideEmbedTag = "GuideEmbeded";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenGuidebook,
|
||||
new PointerInputCmdHandler(HandleOpenGuidebook))
|
||||
.Register<GuidebookSystem>();
|
||||
_guideWindow = new GuidebookWindow();
|
||||
|
||||
SubscribeLocalEvent<GuideHelpComponent, GetVerbsEvent<ExamineVerb>>(OnGetVerbs);
|
||||
SubscribeLocalEvent<GuidebookControlsTestComponent, InteractHandEvent>(OnGuidebookControlsTestInteractHand);
|
||||
SubscribeLocalEvent<GuidebookControlsTestComponent, ActivateInWorldEvent>(OnGuidebookControlsTestActivateInWorld);
|
||||
SubscribeLocalEvent<GuidebookControlsTestComponent, GetVerbsEvent<AlternativeVerb>>(
|
||||
OnGuidebookControlsTestGetAlternateVerbs);
|
||||
}
|
||||
|
||||
private void OnGetVerbs(EntityUid uid, GuideHelpComponent component, GetVerbsEvent<ExamineVerb> args)
|
||||
{
|
||||
if (component.Guides.Count == 0 || _tags.HasTag(uid, GuideEmbedTag))
|
||||
return;
|
||||
|
||||
args.Verbs.Add(new()
|
||||
{
|
||||
Text = Loc.GetString("guide-help-verb"),
|
||||
IconTexture = "/Textures/Interface/VerbIcons/information.svg.192dpi.png",
|
||||
Act = () => OpenGuidebook(component.Guides, includeChildren: component.IncludeChildren, selected: component.Guides[0]),
|
||||
ClientExclusive = true,
|
||||
CloseMenu = true
|
||||
});
|
||||
}
|
||||
|
||||
private void OnGuidebookControlsTestGetAlternateVerbs(EntityUid uid, GuidebookControlsTestComponent component, GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
args.Verbs.Add(new AlternativeVerb()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
if (Transform(uid).LocalRotation != Angle.Zero)
|
||||
Transform(uid).LocalRotation -= Angle.FromDegrees(90);
|
||||
},
|
||||
Text = Loc.GetString("guidebook-monkey-unspin"),
|
||||
Priority = -9999,
|
||||
});
|
||||
|
||||
args.Verbs.Add(new AlternativeVerb()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
var light = EnsureComp<PointLightComponent>(uid); // RGB demands this.
|
||||
light.Enabled = false;
|
||||
var rgb = EnsureComp<RgbLightControllerComponent>(uid);
|
||||
|
||||
var sprite = EnsureComp<SpriteComponent>(uid);
|
||||
var layers = new List<int>();
|
||||
|
||||
for (var i = 0; i < sprite.AllLayers.Count(); i++)
|
||||
{
|
||||
layers.Add(i);
|
||||
}
|
||||
|
||||
_rgbLightControllerSystem.SetLayers(uid, layers, rgb);
|
||||
},
|
||||
Text = Loc.GetString("guidebook-monkey-disco"),
|
||||
Priority = -9998,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnGuidebookControlsTestActivateInWorld(EntityUid uid, GuidebookControlsTestComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
Transform(uid).LocalRotation += Angle.FromDegrees(90);
|
||||
}
|
||||
|
||||
private void OnGuidebookControlsTestInteractHand(EntityUid uid, GuidebookControlsTestComponent component, InteractHandEvent args)
|
||||
{
|
||||
if (!TryComp<SpeechComponent>(uid, out var speech) || speech.SpeechSounds is null)
|
||||
return;
|
||||
|
||||
_audioSystem.PlayGlobal(speech.SpeechSounds, Filter.Local(), false, speech.AudioParams);
|
||||
}
|
||||
|
||||
|
||||
public void FakeClientActivateInWorld(EntityUid activated)
|
||||
{
|
||||
var user = _playerManager.LocalPlayer!.ControlledEntity;
|
||||
if (user is null)
|
||||
return;
|
||||
var activateMsg = new ActivateInWorldEvent(user.Value, activated);
|
||||
RaiseLocalEvent(activated, activateMsg, true);
|
||||
}
|
||||
|
||||
public void FakeClientAltActivateInWorld(EntityUid activated)
|
||||
{
|
||||
var user = _playerManager.LocalPlayer!.ControlledEntity;
|
||||
if (user is null)
|
||||
return;
|
||||
// Get list of alt-interact verbs
|
||||
var verbs = _verbSystem.GetLocalVerbs(activated, user.Value, typeof(AlternativeVerb));
|
||||
|
||||
if (!verbs.Any())
|
||||
return;
|
||||
|
||||
_verbSystem.ExecuteVerb(verbs.First(), user.Value, activated);
|
||||
}
|
||||
|
||||
public void FakeClientUse(EntityUid activated)
|
||||
{
|
||||
var user = _playerManager.LocalPlayer!.ControlledEntity ?? EntityUid.Invalid;
|
||||
var activateMsg = new InteractHandEvent(user, activated);
|
||||
RaiseLocalEvent(activated, activateMsg, true);
|
||||
}
|
||||
|
||||
private bool HandleOpenGuidebook(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
if (args.State != BoundKeyState.Down)
|
||||
return false;
|
||||
|
||||
OpenGuidebook();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the guidebook.
|
||||
/// </summary>
|
||||
/// <param name="guides">What guides should be shown. If not specified, this will instead raise a <see
|
||||
/// cref="GetGuidesEvent"/> and automatically include all guide prototypes.</param>
|
||||
/// <param name="rootEntries">A list of guides that should form the base of the table of contents. If not specified,
|
||||
/// this will automatically simply be a list of all guides that have no parent.</param>
|
||||
/// <param name="forceRoot">This forces a singular guide to contain all other guides. This guide will
|
||||
/// contain its own children, in addition to what would normally be the root guides if this were not
|
||||
/// specified.</param>
|
||||
/// <param name="includeChildren">Whether or not to automatically include child entries. If false, this will ONLY
|
||||
/// show the specified entries</param>
|
||||
/// <param name="selected">The guide whose contents should be displayed when the guidebook is opened</param>
|
||||
public bool OpenGuidebook(
|
||||
Dictionary<string, GuideEntry>? guides = null,
|
||||
List<string>? rootEntries = null,
|
||||
string? forceRoot = null,
|
||||
bool includeChildren = true,
|
||||
string? selected = null)
|
||||
{
|
||||
_guideWindow.OpenCenteredRight();
|
||||
|
||||
if (guides == null)
|
||||
{
|
||||
var ev = new GetGuidesEvent()
|
||||
{
|
||||
Guides = _prototypeManager.EnumeratePrototypes<GuideEntryPrototype>().ToDictionary(x => x.ID, x => (GuideEntry) x)
|
||||
};
|
||||
RaiseLocalEvent(ev);
|
||||
guides = ev.Guides;
|
||||
}
|
||||
else if (includeChildren)
|
||||
{
|
||||
var oldGuides = guides;
|
||||
guides = new(oldGuides);
|
||||
foreach (var guide in oldGuides.Values)
|
||||
{
|
||||
RecursivelyAddChildren(guide, guides);
|
||||
}
|
||||
}
|
||||
|
||||
_guideWindow.UpdateGuides(guides, rootEntries, forceRoot, selected);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool OpenGuidebook(
|
||||
List<string> guideList,
|
||||
List<string>? rootEntries = null,
|
||||
string? forceRoot = null,
|
||||
bool includeChildren = true,
|
||||
string? selected = null)
|
||||
{
|
||||
Dictionary<string, GuideEntry>? guides = new();
|
||||
foreach (var guideId in guideList)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<GuideEntryPrototype>(guideId, out var guide))
|
||||
{
|
||||
Logger.Error($"Encountered unknown guide prototype: {guideId}");
|
||||
continue;
|
||||
}
|
||||
guides.Add(guideId, guide);
|
||||
}
|
||||
|
||||
return OpenGuidebook(guides, rootEntries, forceRoot, includeChildren, selected);
|
||||
}
|
||||
|
||||
private void RecursivelyAddChildren(GuideEntry guide, Dictionary<string, GuideEntry> guides)
|
||||
{
|
||||
foreach (var childId in guide.Children)
|
||||
{
|
||||
if (guides.ContainsKey(childId))
|
||||
continue;
|
||||
|
||||
if (!_prototypeManager.TryIndex<GuideEntryPrototype>(childId, out var child))
|
||||
{
|
||||
Logger.Error($"Encountered unknown guide prototype: {childId} as a child of {guide.Id}. If the child is not a prototype, it must be directly provided.");
|
||||
continue;
|
||||
}
|
||||
|
||||
guides.Add(childId, child);
|
||||
RecursivelyAddChildren(child, guides);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GetGuidesEvent : EntityEventArgs
|
||||
{
|
||||
public Dictionary<string, GuideEntry> Guides { get; init; } = new();
|
||||
}
|
||||
29
Content.Client/Guidebook/Richtext/Box.cs
Normal file
29
Content.Client/Guidebook/Richtext/Box.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.Guidebook.Richtext;
|
||||
|
||||
public sealed class Box : BoxContainer, IDocumentTag
|
||||
{
|
||||
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
HorizontalExpand = true;
|
||||
control = this;
|
||||
|
||||
if (args.TryGetValue("Orientation", out var orientation))
|
||||
Orientation = Enum.Parse<LayoutOrientation>(orientation);
|
||||
else
|
||||
Orientation = LayoutOrientation.Horizontal;
|
||||
|
||||
if (args.TryGetValue("HorizontalAlignment", out var halign))
|
||||
HorizontalAlignment = Enum.Parse<HAlignment>(halign);
|
||||
else
|
||||
HorizontalAlignment = HAlignment.Center;
|
||||
|
||||
if (args.TryGetValue("VerticalAlignment", out var valign))
|
||||
VerticalAlignment = Enum.Parse<VAlignment>(valign);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
29
Content.Client/Guidebook/Richtext/Document.cs
Normal file
29
Content.Client/Guidebook/Richtext/Document.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Client.Guidebook.Richtext;
|
||||
|
||||
/// <summary>
|
||||
/// A document, containing arbitrary text and UI elements.
|
||||
/// </summary>
|
||||
public sealed class Document : BoxContainer, IDocumentTag
|
||||
{
|
||||
public Document()
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical;
|
||||
}
|
||||
|
||||
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
DebugTools.Assert(args.Count == 0);
|
||||
control = this;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IDocumentTag
|
||||
{
|
||||
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control);
|
||||
}
|
||||
@@ -96,6 +96,7 @@ namespace Content.Client.Input
|
||||
common.AddFunction(ContentKeyFunctions.OpenTileSpawnWindow);
|
||||
common.AddFunction(ContentKeyFunctions.OpenDecalSpawnWindow);
|
||||
common.AddFunction(ContentKeyFunctions.OpenAdminMenu);
|
||||
common.AddFunction(ContentKeyFunctions.OpenGuidebook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ using Content.Client.Voting;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Module;
|
||||
using Content.Client.Guidebook;
|
||||
|
||||
namespace Content.Client.IoC
|
||||
{
|
||||
@@ -48,6 +49,7 @@ namespace Content.Client.IoC
|
||||
IoCManager.Register<SponsorsManager>(); // Corvax-Sponsors
|
||||
IoCManager.Register<JoinQueueManager>(); // Corvax-Queue
|
||||
IoCManager.Register<TTSManager>(); // Corvax-TTS
|
||||
IoCManager.Register<DocumentParsingManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,25 @@ namespace Content.Client.Light.Visualizers
|
||||
[UsedImplicitly]
|
||||
public sealed class ExpendableLightVisualizer : AppearanceVisualizer
|
||||
{
|
||||
[DataField("iconStateSpent")]
|
||||
public string? IconStateSpent { get; set; }
|
||||
|
||||
[DataField("iconStateOn")]
|
||||
public string? IconStateLit { get; set; }
|
||||
|
||||
[Obsolete("Subscribe to AppearanceChangeEvent instead.")]
|
||||
public override void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
base.OnChangeData(component);
|
||||
|
||||
var entities = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
if (!entities.TryGetComponent(component.Owner, out ExpendableLightComponent? expendableLight))
|
||||
return;
|
||||
|
||||
if (!entities.TryGetComponent(component.Owner, out SpriteComponent? sprite))
|
||||
return;
|
||||
|
||||
if (component.TryGetData(ExpendableLightVisuals.Behavior, out string lightBehaviourID))
|
||||
{
|
||||
if (entities.TryGetComponent(component.Owner, out LightBehaviourComponent? lightBehaviour))
|
||||
@@ -31,22 +44,36 @@ namespace Content.Client.Light.Visualizers
|
||||
}
|
||||
}
|
||||
|
||||
if (component.TryGetData(ExpendableLightVisuals.State, out ExpendableLightState state)
|
||||
&& entities.TryGetComponent(component.Owner, out ExpendableLightComponent? expendableLight))
|
||||
if (!component.TryGetData(ExpendableLightVisuals.State, out ExpendableLightState state))
|
||||
return;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case ExpendableLightState.Lit:
|
||||
expendableLight.PlayingStream?.Stop();
|
||||
expendableLight.PlayingStream = entities.EntitySysManager.GetEntitySystem<SharedAudioSystem>().PlayPvs(
|
||||
expendableLight.LoopedSound,
|
||||
expendableLight.Owner,
|
||||
SharedExpendableLightComponent.LoopedSoundParams);
|
||||
break;
|
||||
case ExpendableLightState.Dead:
|
||||
expendableLight.PlayingStream?.Stop();
|
||||
break;
|
||||
}
|
||||
case ExpendableLightState.Lit:
|
||||
expendableLight.PlayingStream?.Stop();
|
||||
expendableLight.PlayingStream = entities.EntitySysManager.GetEntitySystem<SharedAudioSystem>().PlayPvs(
|
||||
expendableLight.LoopedSound,
|
||||
expendableLight.Owner,
|
||||
SharedExpendableLightComponent.LoopedSoundParams);
|
||||
if (!string.IsNullOrWhiteSpace(IconStateLit))
|
||||
{
|
||||
sprite.LayerSetState(2, IconStateLit);
|
||||
sprite.LayerSetShader(2, "shaded");
|
||||
}
|
||||
|
||||
sprite.LayerSetVisible(1, true);
|
||||
|
||||
break;
|
||||
case ExpendableLightState.Dead:
|
||||
expendableLight.PlayingStream?.Stop();
|
||||
if (!string.IsNullOrWhiteSpace(IconStateSpent))
|
||||
{
|
||||
sprite.LayerSetState(0, IconStateSpent);
|
||||
sprite.LayerSetShader(0, "shaded");
|
||||
}
|
||||
|
||||
sprite.LayerSetVisible(1, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<ui:VoteCallMenuButton />
|
||||
<Button Access="Public" Name="OptionsButton" Text="{Loc 'ui-escape-options'}" />
|
||||
<Button Access="Public" Name="RulesButton" Text="{Loc 'ui-escape-rules'}" />
|
||||
<Button Access="Public" Name="GuidebookButton" Text="{Loc 'ui-escape-guidebook'}" />
|
||||
<Button Access="Public" Name="WikiButton" Text="{Loc 'ui-escape-wiki'}" />
|
||||
<Button Access="Public" Name="DisconnectButton" Text="{Loc 'ui-escape-disconnect'}" />
|
||||
<Button Access="Public" Name="QuitButton" Text="{Loc 'ui-escape-quit'}" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
xmlns:tabs="clr-namespace:Content.Client.Options.UI.Tabs"
|
||||
Title="{Loc 'ui-options-title'}"
|
||||
MinSize="800 450">
|
||||
<TabContainer Name="Tabs">
|
||||
<TabContainer Name="Tabs" Access="Public">
|
||||
<tabs:GraphicsTab />
|
||||
<tabs:KeyRebindTab />
|
||||
<tabs:AudioTab />
|
||||
|
||||
@@ -136,6 +136,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
AddButton(ContentKeyFunctions.CycleChatChannelBackward);
|
||||
AddButton(ContentKeyFunctions.OpenCharacterMenu);
|
||||
AddButton(ContentKeyFunctions.OpenCraftingMenu);
|
||||
AddButton(ContentKeyFunctions.OpenGuidebook);
|
||||
AddButton(ContentKeyFunctions.OpenInventoryMenu);
|
||||
AddButton(ContentKeyFunctions.OpenAHelp);
|
||||
AddButton(ContentKeyFunctions.OpenActionsMenu);
|
||||
|
||||
@@ -19,14 +19,18 @@ namespace Content.Client.Paper.UI
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_window = new PaperWindow
|
||||
{
|
||||
Title = IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(Owner.Owner).EntityName,
|
||||
};
|
||||
var entityMgr = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
_window = new PaperWindow();
|
||||
_window.OnClose += Close;
|
||||
_window.Input.OnTextEntered += Input_OnTextEntered;
|
||||
_window.OpenCentered();
|
||||
|
||||
if (entityMgr.TryGetComponent<PaperVisualsComponent>(Owner.Owner, out var visuals))
|
||||
{
|
||||
_window.InitVisuals(visuals);
|
||||
}
|
||||
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
|
||||
@@ -1,6 +1,99 @@
|
||||
namespace Content.Client.Paper;
|
||||
namespace Content.Client.Paper;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class PaperVisualsComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The path to the image which will be used as a background for the paper itself
|
||||
/// </summary>
|
||||
[DataField("backgroundImagePath")]
|
||||
public string? BackgroundImagePath;
|
||||
|
||||
/// <summary>
|
||||
/// An optional patch to configure tiling stretching of the background. Used to set
|
||||
/// the PatchMargin in a <code>StyleBoxTexture</code>
|
||||
/// </summary>
|
||||
[DataField("backgroundPatchMargin")]
|
||||
public Box2 BackgroundPatchMargin = default;
|
||||
|
||||
/// <summary>
|
||||
/// Modulate the background image by this color. Can be used to add colorful
|
||||
/// variants of images, without having to create new textures.
|
||||
/// </summary>
|
||||
[DataField("backgroundModulate")]
|
||||
public Color BackgroundModulate = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// Should the background image tile, or be streched? Sets <code>StyleBoxTexture.StrechMode</code>
|
||||
/// </summary>
|
||||
[DataField("backgroundImageTile")]
|
||||
public bool BackgroundImageTile = false;
|
||||
|
||||
/// <summary>
|
||||
/// An additional scale to apply to the background image
|
||||
/// </summary>
|
||||
[DataField("backgroundScale")]
|
||||
public Vector2 BackgroundScale = Vector2.One;
|
||||
|
||||
/// <summary>
|
||||
/// A path to an image which will be used as a header on the paper
|
||||
/// </summary>
|
||||
[DataField("headerImagePath")]
|
||||
public string? HeaderImagePath;
|
||||
|
||||
/// <summary>
|
||||
/// Modulate the header image by this color
|
||||
/// </summary>
|
||||
[DataField("headerImageModulate")]
|
||||
public Color HeaderImageModulate = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// Any additional margin to add around the header
|
||||
/// </summary>
|
||||
[DataField("headerMargin")]
|
||||
public Box2 HeaderMargin = default;
|
||||
|
||||
/// <summary>
|
||||
/// Path to an image to use as the background to the "content" of the paper
|
||||
/// The header and actual written text will use this as a background. The
|
||||
/// image will be tiled vertically with the property that the bottom of the
|
||||
/// written text will line up with the bottom of this image.
|
||||
/// </summary>
|
||||
[DataField("contentImagePath")]
|
||||
public string? ContentImagePath;
|
||||
|
||||
/// <summary>
|
||||
/// Modulate the content image by this color
|
||||
/// </summary>
|
||||
[DataField("contentImageModulate")]
|
||||
public Color ContentImageModulate = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// An additional margin around the content (including header)
|
||||
/// </summary>
|
||||
[DataField("contentMargin")]
|
||||
public Box2 ContentMargin = default;
|
||||
|
||||
/// <summary>
|
||||
/// The number of lines that the content image represents. The
|
||||
/// content image will be vertically tiled after this many lines
|
||||
/// of text.
|
||||
/// </summary>
|
||||
[DataField("contentImageNumLines")]
|
||||
public int ContentImageNumLines = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Modulate the style's font by this color
|
||||
/// </summary>
|
||||
[DataField("fontAccentColor")]
|
||||
public Color FontAccentColor = new Color(0x25, 0x25, 0x2a);
|
||||
|
||||
/// <summary>
|
||||
/// This can enforce that your paper has a limited area to write in.
|
||||
/// If you wish to constrain only one direction, the other direction
|
||||
/// can be unlimited by specifying a value of zero.
|
||||
/// This will be scaled according to UI scale.
|
||||
/// </summary>
|
||||
[DataField("maxWritableArea")]
|
||||
public Vector2? MaxWritableArea = null;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
MinSize="300 300"
|
||||
SetSize="300 300">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<RichTextLabel Name="Label" />
|
||||
<LineEdit Name="Input"
|
||||
Access="Public"
|
||||
Visible="False" />
|
||||
<paper:PaperWindow xmlns="https://spacestation14.io"
|
||||
xmlns:paper="clr-namespace:Content.Client.Paper.UI"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
MouseFilter="Stop" Resizable="True" MinSize="150 150"
|
||||
SetSize="300 400"> <!-- Provide some reasonable sizes by default. Can be changed by the component -->
|
||||
|
||||
<BoxContainer Name="ContentsRoot" Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="AngleRect" VerticalAlignment="Top" HorizontalAlignment="Right" Margin="6">
|
||||
<TextureButton Name="CloseButton" StyleClasses="windowCloseButton"/>
|
||||
</PanelContainer>
|
||||
<PanelContainer Name="PaperBackground" StyleClasses="PaperDefaultBorder" VerticalExpand="True" HorizontalExpand="True">
|
||||
<ScrollContainer Name="ScrollingContents" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="False">
|
||||
<PanelContainer Name="PaperContent" VerticalExpand="True" HorizontalExpand="True" MaxWidth="600">
|
||||
<BoxContainer Orientation="Vertical" VerticalAlignment="Top">
|
||||
<TextureButton Name="HeaderImage" HorizontalAlignment="Center" VerticalAlignment="Top" MouseFilter="Ignore"/>
|
||||
<Control Name="TextAlignmentPadding" VerticalAlignment="Top" />
|
||||
<RichTextLabel Name="BlankPaperIndicator" StyleClasses="LabelSecondaryColor"
|
||||
VerticalAlignment="Top" HorizontalAlignment="Center"/>
|
||||
<RichTextLabel StyleClasses="PaperWrittenText" Name="WrittenTextLabel" VerticalAlignment="Top"/>
|
||||
<PanelContainer Name="InputContainer" StyleClasses="TransparentBorderedWindowPanel"
|
||||
VerticalAlignment="Top" HorizontalExpand="True">
|
||||
<LineEdit Name="Input" StyleClasses="PaperLineEdit" Access="Public" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Name="StampDisplay" Orientation="Vertical" VerticalAlignment="Bottom" Margin="6"/>
|
||||
</PanelContainer>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
</paper:PaperWindow>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Paper;
|
||||
using Content.Shared.Paper;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -8,22 +9,223 @@ using Robust.Shared.Utility;
|
||||
namespace Content.Client.Paper.UI
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class PaperWindow : DefaultWindow
|
||||
public sealed partial class PaperWindow : BaseWindow
|
||||
{
|
||||
// <summary>
|
||||
// Size of resize handles around the paper
|
||||
private const int DRAG_MARGIN_SIZE = 16;
|
||||
|
||||
// We keep a reference to the paper content texture that we create
|
||||
// so that we can modify it later.
|
||||
private StyleBoxTexture _paperContentTex = new();
|
||||
|
||||
// The number of lines that the content image represents.
|
||||
// See PaperVisualsComponent.ContentImageNumLines.
|
||||
private float _paperContentLineScale = 1.0f;
|
||||
|
||||
// If paper limits the size in one or both axes, it'll affect whether
|
||||
// we're able to resize this UI or not. Default to everything enabled:
|
||||
private DragMode _allowedResizeModes = ~DragMode.None;
|
||||
|
||||
public PaperWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
// We can't configure the RichTextLabel contents from xaml, so do it here:
|
||||
BlankPaperIndicator.SetMessage(Loc.GetString("paper-ui-blank-page-message"));
|
||||
|
||||
// Hook up the close button:
|
||||
CloseButton.OnPressed += _ => Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize this UI according to <code>visuals</code> Initializes
|
||||
/// textures, recalculates sizes, and applies some layout rules.
|
||||
/// </summary>
|
||||
public void InitVisuals(PaperVisualsComponent visuals)
|
||||
{
|
||||
var resCache = IoCManager.Resolve<IResourceCache>();
|
||||
|
||||
/// Initialize the background:
|
||||
PaperBackground.ModulateSelfOverride = visuals.BackgroundModulate;
|
||||
var backgroundImage = visuals.BackgroundImagePath != null? resCache.GetResource<TextureResource>(visuals.BackgroundImagePath) : null;
|
||||
if (backgroundImage != null)
|
||||
{
|
||||
var backgroundImageMode = visuals.BackgroundImageTile ? StyleBoxTexture.StretchMode.Tile : StyleBoxTexture.StretchMode.Stretch;
|
||||
var backgroundPatchMargin = visuals.BackgroundPatchMargin;
|
||||
PaperBackground.PanelOverride = new StyleBoxTexture
|
||||
{
|
||||
Texture = backgroundImage,
|
||||
TextureScale = visuals.BackgroundScale,
|
||||
Mode = backgroundImageMode,
|
||||
PatchMarginLeft = backgroundPatchMargin.Left,
|
||||
PatchMarginBottom = backgroundPatchMargin.Bottom,
|
||||
PatchMarginRight = backgroundPatchMargin.Right,
|
||||
PatchMarginTop = backgroundPatchMargin.Top
|
||||
};
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
PaperBackground.PanelOverride = null;
|
||||
}
|
||||
|
||||
|
||||
// Then the header:
|
||||
if (visuals.HeaderImagePath != null)
|
||||
{
|
||||
HeaderImage.TexturePath = visuals.HeaderImagePath;
|
||||
HeaderImage.MinSize = HeaderImage.TextureNormal?.Size ?? Vector2.Zero;
|
||||
}
|
||||
|
||||
HeaderImage.ModulateSelfOverride = visuals.HeaderImageModulate;
|
||||
HeaderImage.Margin = new Thickness(visuals.HeaderMargin.Left, visuals.HeaderMargin.Top,
|
||||
visuals.HeaderMargin.Right, visuals.HeaderMargin.Bottom);
|
||||
|
||||
|
||||
PaperContent.ModulateSelfOverride = visuals.ContentImageModulate;
|
||||
WrittenTextLabel.ModulateSelfOverride = visuals.FontAccentColor;
|
||||
|
||||
var contentImage = visuals.ContentImagePath != null ? resCache.GetResource<TextureResource>(visuals.ContentImagePath) : null;
|
||||
if (contentImage != null)
|
||||
{
|
||||
// Setup the paper content texture, but keep a reference to it, as we can't set
|
||||
// some font-related properties here. We'll fix those up later, in Draw()
|
||||
_paperContentTex = new StyleBoxTexture
|
||||
{
|
||||
Texture = contentImage,
|
||||
Mode = StyleBoxTexture.StretchMode.Tile,
|
||||
};
|
||||
PaperContent.PanelOverride = _paperContentTex;
|
||||
_paperContentLineScale = visuals.ContentImageNumLines;
|
||||
}
|
||||
|
||||
PaperContent.Margin = new Thickness(
|
||||
visuals.ContentMargin.Left, visuals.ContentMargin.Top,
|
||||
visuals.ContentMargin.Right, visuals.ContentMargin.Bottom);
|
||||
|
||||
if (visuals.MaxWritableArea != null)
|
||||
{
|
||||
var a = (Vector2)visuals.MaxWritableArea;
|
||||
// Paper has requested that this has a maximum area that you can write on.
|
||||
// So, we'll make the window non-resizable and fix the size of the content.
|
||||
// Ideally, would like to be able to allow resizing only one direction.
|
||||
ScrollingContents.MinSize = Vector2.Zero;
|
||||
ScrollingContents.MinSize = (Vector2)(a);
|
||||
|
||||
if (a.X > 0.0f)
|
||||
{
|
||||
ScrollingContents.MaxWidth = a.X;
|
||||
_allowedResizeModes &= ~(DragMode.Left | DragMode.Right);
|
||||
|
||||
// Since this dimension has been specified by the user, we
|
||||
// need to undo the SetSize which was configured in the xaml.
|
||||
// Controls use NaNs to indicate unset for this value.
|
||||
// This is leaky - there should be a method for this
|
||||
SetWidth = float.NaN;
|
||||
}
|
||||
|
||||
if (a.Y > 0.0f)
|
||||
{
|
||||
ScrollingContents.MaxHeight = a.Y;
|
||||
_allowedResizeModes &= ~(DragMode.Top | DragMode.Bottom);
|
||||
SetHeight = float.NaN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Control interface. We'll mostly rely on the children to do the drawing
|
||||
/// but in order to get lines on paper to match up with the rich text labels,
|
||||
/// we need to do a small calculation to sync them up.
|
||||
/// </summary>
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
// Now do the deferred setup of the written area. At the point
|
||||
// that InitVisuals runs, the label hasn't had it's style initialized
|
||||
// so we need to get some info out now:
|
||||
if (WrittenTextLabel.TryGetStyleProperty<Font>("font", out var font))
|
||||
{
|
||||
float fontLineHeight = font.GetLineHeight(UIScale);
|
||||
// This positions the texture so the font baseline is on the bottom:
|
||||
_paperContentTex.ExpandMarginTop = font.GetDescent(UIScale);
|
||||
// And this scales the texture so that it's a single text line:
|
||||
var scaleY = (_paperContentLineScale * fontLineHeight) / _paperContentTex.Texture?.Height ?? fontLineHeight;
|
||||
_paperContentTex.TextureScale = new Vector2(1, scaleY);
|
||||
|
||||
// Now, we might need to add some padding to the text to ensure
|
||||
// that, even if a header is specified, the text will line up with
|
||||
// where the content image expects the font to be rendered (i.e.,
|
||||
// adjusting the height of the header image shouldn't cause the
|
||||
// text to be offset from a line)
|
||||
{
|
||||
var headerHeight = HeaderImage.Size.Y + HeaderImage.Margin.Top + HeaderImage.Margin.Bottom;
|
||||
var headerInLines = headerHeight / (fontLineHeight * _paperContentLineScale);
|
||||
var paddingRequiredInLines = (float)Math.Ceiling(headerInLines) - headerInLines;
|
||||
var verticalMargin = fontLineHeight * paddingRequiredInLines * _paperContentLineScale;
|
||||
TextAlignmentPadding.Margin = new Thickness(0.0f, verticalMargin, 0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
base.Draw(handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the paper contents, i.e. the text typed by the
|
||||
/// user and any stamps that have peen put on the page.
|
||||
/// </summary>
|
||||
public void Populate(SharedPaperComponent.PaperBoundUserInterfaceState state)
|
||||
{
|
||||
if (state.Mode == SharedPaperComponent.PaperAction.Write)
|
||||
{
|
||||
Input.Visible = true;
|
||||
}
|
||||
bool isEditing = state.Mode == SharedPaperComponent.PaperAction.Write;
|
||||
InputContainer.Visible = isEditing;
|
||||
|
||||
var msg = new FormattedMessage();
|
||||
msg.AddMarkupPermissive(state.Text);
|
||||
Label.SetMessage(msg);
|
||||
// Remove any newlines from the end of the message. There can be a trailing
|
||||
// new line at the end of user input, and we would like to display the input
|
||||
// box immediately on the next line.
|
||||
msg.AddMarkupPermissive(state.Text.TrimEnd('\r', '\n'));
|
||||
WrittenTextLabel.SetMessage(msg);
|
||||
WrittenTextLabel.Visible = state.Text.Length > 0;
|
||||
|
||||
BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0;
|
||||
|
||||
StampDisplay.RemoveAllChildren();
|
||||
foreach(var stamper in state.StampedBy)
|
||||
{
|
||||
StampDisplay.AddChild(new StampWidget{ Stamper = stamper });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BaseWindow interface. Allow users to drag UI around by grabbing
|
||||
/// anywhere on the page (like FancyWindow) but try to calculate
|
||||
/// reasonable dragging bounds because this UI can have round corners,
|
||||
/// and it can be hard to judge where to click to resize.
|
||||
/// </summary>
|
||||
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
|
||||
{
|
||||
var mode = DragMode.Move;
|
||||
|
||||
// Be quite generous with resize margins:
|
||||
if (relativeMousePos.Y < DRAG_MARGIN_SIZE)
|
||||
{
|
||||
mode |= DragMode.Top;
|
||||
}
|
||||
else if (relativeMousePos.Y > Size.Y - DRAG_MARGIN_SIZE)
|
||||
{
|
||||
mode |= DragMode.Bottom;
|
||||
}
|
||||
|
||||
if (relativeMousePos.X < DRAG_MARGIN_SIZE)
|
||||
{
|
||||
mode |= DragMode.Left;
|
||||
}
|
||||
else if (relativeMousePos.X > Size.X - DRAG_MARGIN_SIZE)
|
||||
{
|
||||
mode |= DragMode.Right;
|
||||
}
|
||||
|
||||
return mode & _allowedResizeModes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
Content.Client/Paper/UI/StampWidget.xaml
Normal file
31
Content.Client/Paper/UI/StampWidget.xaml
Normal file
@@ -0,0 +1,31 @@
|
||||
<paper:StampWidget xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:style="clr-namespace:Content.Client.Stylesheets"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:paper="clr-namespace:Content.Client.Paper.UI" HorizontalAlignment="Center" Margin="6">
|
||||
<!--
|
||||
<TextureButton Margin="6 6 6 2" MinSize="24 12"
|
||||
TexturePath="/Textures/Interface/Nano/nano_stamp.192dpi.png">
|
||||
-->
|
||||
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.DangerousRedFore}" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<Control MinSize="3 3" />
|
||||
</PanelContainer>
|
||||
|
||||
<Label Name="StampedByLabel" StyleClasses="LabelHeadingBigger" FontColorOverride="{x:Static style:StyleNano.DangerousRedFore}" Margin="12 6 12 6"/>
|
||||
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.DangerousRedFore}" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<Control MinSize="3 3" />
|
||||
</PanelContainer>
|
||||
|
||||
</BoxContainer>
|
||||
</paper:StampWidget>
|
||||
23
Content.Client/Paper/UI/StampWidget.xaml.cs
Normal file
23
Content.Client/Paper/UI/StampWidget.xaml.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Content.Shared.Paper;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Paper.UI
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StampWidget : Container
|
||||
{
|
||||
public string? Stamper {
|
||||
get => StampedByLabel.Text;
|
||||
set => StampedByLabel.Text = value;
|
||||
}
|
||||
|
||||
public StampWidget()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public sealed class ParallaxOverlay : Overlay
|
||||
|
||||
public ParallaxOverlay()
|
||||
{
|
||||
ZIndex = ParallaxSystem.ParallaxZIndex;
|
||||
IoCManager.InjectDependencies(this);
|
||||
_parallax = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ParallaxSystem>();
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ public sealed class ParallaxSystem : SharedParallaxSystem
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
|
||||
private const string Fallback = "Default";
|
||||
public const int ParallaxZIndex = 0;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using Robust.Client.Player;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Physics.Controllers
|
||||
{
|
||||
@@ -52,21 +53,11 @@ namespace Content.Client.Physics.Controllers
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity is not {Valid: true} player)
|
||||
return;
|
||||
|
||||
if (TryComp<RelayInputMoverComponent>(player, out var relayMover))
|
||||
if (TryComp<RelayInputMoverComponent>(player, out var relayMover)
|
||||
&& TryComp(relayMover.RelayEntity, out MovementRelayTargetComponent? targetComp))
|
||||
{
|
||||
if (relayMover.RelayEntity != null)
|
||||
{
|
||||
if (TryComp<InputMoverComponent>(player, out var mover) &&
|
||||
TryComp<InputMoverComponent>(relayMover.RelayEntity, out var relayed))
|
||||
{
|
||||
relayed.CanMove = mover.CanMove;
|
||||
relayed.RelativeEntity = mover.RelativeEntity;
|
||||
relayed.RelativeRotation = mover.RelativeRotation;
|
||||
relayed.TargetRelativeRotation = mover.RelativeRotation;
|
||||
}
|
||||
|
||||
HandleClientsideMovement(relayMover.RelayEntity.Value, frameTime);
|
||||
}
|
||||
DebugTools.Assert(targetComp.Entities.Count <= 1, "Multiple relayed movers are not supported at the moment");
|
||||
HandleClientsideMovement(relayMover.RelayEntity.Value, frameTime);
|
||||
}
|
||||
|
||||
HandleClientsideMovement(player, frameTime);
|
||||
@@ -75,6 +66,8 @@ namespace Content.Client.Physics.Controllers
|
||||
private void HandleClientsideMovement(EntityUid player, float frameTime)
|
||||
{
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var moverQuery = GetEntityQuery<InputMoverComponent>();
|
||||
var relayTargetQuery = GetEntityQuery<MovementRelayTargetComponent>();
|
||||
|
||||
if (!TryComp(player, out InputMoverComponent? mover) ||
|
||||
!xformQuery.TryGetComponent(player, out var xform))
|
||||
@@ -82,7 +75,7 @@ namespace Content.Client.Physics.Controllers
|
||||
return;
|
||||
}
|
||||
|
||||
PhysicsComponent? body = null;
|
||||
PhysicsComponent? body;
|
||||
TransformComponent? xformMover = xform;
|
||||
|
||||
if (mover.ToParent && HasComp<RelayInputMoverComponent>(xform.ParentUid))
|
||||
@@ -135,7 +128,7 @@ namespace Content.Client.Physics.Controllers
|
||||
}
|
||||
|
||||
// Server-side should just be handled on its own so we'll just do this shizznit
|
||||
HandleMobMovement(mover, body, xformMover, frameTime, xformQuery);
|
||||
HandleMobMovement(player, mover, body, xformMover, frameTime, xformQuery, moverQuery, relayTargetQuery);
|
||||
}
|
||||
|
||||
protected override bool CanSound()
|
||||
|
||||
7
Content.Client/PneumaticCannon/PneumaticCannonSystem.cs
Normal file
7
Content.Client/PneumaticCannon/PneumaticCannonSystem.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Content.Shared.PneumaticCannon;
|
||||
|
||||
namespace Content.Client.PneumaticCannon;
|
||||
|
||||
public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem
|
||||
{
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Content.Shared.PneumaticCannon;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Client.PneumaticCannon
|
||||
{
|
||||
public sealed class PneumaticCannonVisualizer : AppearanceVisualizer
|
||||
{
|
||||
[Obsolete("Subscribe to AppearanceChangeEvent instead.")]
|
||||
public override void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
base.OnChangeData(component);
|
||||
|
||||
var entities = IoCManager.Resolve<IEntityManager>();
|
||||
if (!entities.TryGetComponent(component.Owner, out SpriteComponent? sprite))
|
||||
return;
|
||||
|
||||
if (component.TryGetData(PneumaticCannonVisuals.Tank, out bool tank))
|
||||
{
|
||||
sprite.LayerSetVisible(PneumaticCannonVisualLayers.Tank, tank);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,9 @@ public sealed class PointingSystem : SharedPointingSystem
|
||||
|
||||
private void AddPointingVerb(GetVerbsEvent<Verb> args)
|
||||
{
|
||||
if (args.Target.IsClientSide())
|
||||
return;
|
||||
|
||||
// Really this could probably be a properly predicted event, but that requires reworking pointing. For now
|
||||
// I'm just adding this verb exclusively to clients so that the verb-loading pop-in on the verb menu isn't
|
||||
// as bad. Important for this verb seeing as its usually an option on just about any entity.
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared;
|
||||
@@ -21,6 +22,7 @@ public sealed class PopupOverlay : Overlay
|
||||
{
|
||||
private readonly IConfigurationManager _configManager;
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly IPlayerManager _playerMgr;
|
||||
private readonly IUserInterfaceManager _uiManager;
|
||||
private readonly PopupSystem _popup;
|
||||
|
||||
@@ -34,6 +36,7 @@ public sealed class PopupOverlay : Overlay
|
||||
public PopupOverlay(
|
||||
IConfigurationManager configManager,
|
||||
IEntityManager entManager,
|
||||
IPlayerManager playerMgr,
|
||||
IPrototypeManager protoManager,
|
||||
IResourceCache cache,
|
||||
IUserInterfaceManager uiManager,
|
||||
@@ -41,6 +44,7 @@ public sealed class PopupOverlay : Overlay
|
||||
{
|
||||
_configManager = configManager;
|
||||
_entManager = entManager;
|
||||
_playerMgr = playerMgr;
|
||||
_uiManager = uiManager;
|
||||
_popup = popup;
|
||||
|
||||
@@ -75,6 +79,7 @@ public sealed class PopupOverlay : Overlay
|
||||
|
||||
var matrix = args.ViewportControl!.GetWorldToScreenMatrix();
|
||||
var viewPos = new MapCoordinates(args.WorldAABB.Center, args.MapId);
|
||||
var ourEntity = _playerMgr.LocalPlayer?.ControlledEntity;
|
||||
|
||||
foreach (var popup in _popup.WorldLabels)
|
||||
{
|
||||
@@ -87,7 +92,7 @@ public sealed class PopupOverlay : Overlay
|
||||
|
||||
// Should handle fade here too wyci.
|
||||
if (!args.WorldAABB.Contains(mapPos.Position) || !ExamineSystemShared.InRangeUnOccluded(viewPos, mapPos, distance,
|
||||
e => e == popup.InitialPos.EntityId, entMan: _entManager))
|
||||
e => e == popup.InitialPos.EntityId || e == ourEntity, entMan: _entManager))
|
||||
continue;
|
||||
|
||||
var pos = matrix.Transform(mapPos.Position);
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Content.Client.Popups
|
||||
SubscribeNetworkEvent<PopupEntityEvent>(OnPopupEntityEvent);
|
||||
SubscribeNetworkEvent<RoundRestartCleanupEvent>(OnRoundRestart);
|
||||
_overlay
|
||||
.AddOverlay(new PopupOverlay(_configManager, EntityManager, _prototype, _resource, _uiManager, this));
|
||||
.AddOverlay(new PopupOverlay(_configManager, EntityManager, _playerManager, _prototype, _resource, _uiManager, this));
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
|
||||
@@ -5,6 +5,7 @@ using Content.Client.PDA;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Targeting.UI;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Controls.FancyTree;
|
||||
using Content.Client.Verbs.UI;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -113,6 +114,11 @@ namespace Content.Client.Stylesheets
|
||||
public static readonly Color ExamineButtonColorContextPressed = Color.LightSlateGray;
|
||||
public static readonly Color ExamineButtonColorContextDisabled = Color.FromHex("#5A5A5A");
|
||||
|
||||
// Fancy Tree elements
|
||||
public static readonly Color FancyTreeEvenRowColor = Color.FromHex("#25252A");
|
||||
public static readonly Color FancyTreeOddRowColor = FancyTreeEvenRowColor * new Color(0.8f, 0.8f, 0.8f);
|
||||
public static readonly Color FancyTreeSelectedRowColor = new Color(55, 55, 68);
|
||||
|
||||
//Used by the APC and SMES menus
|
||||
public const string StyleClassPowerStateNone = "PowerStateNone";
|
||||
public const string StyleClassPowerStateLow = "PowerStateLow";
|
||||
@@ -479,6 +485,14 @@ namespace Content.Client.Stylesheets
|
||||
};
|
||||
insetBack.SetPatchMargin(StyleBox.Margin.All, 10);
|
||||
|
||||
// Default paper background:
|
||||
var paperBackground = new StyleBoxTexture
|
||||
{
|
||||
Texture = resCache.GetTexture("/Textures/Interface/Paper/paper_background_default.svg.96dpi.png"),
|
||||
Modulate = Color.FromHex("#eaedde"), // A light cream
|
||||
};
|
||||
paperBackground.SetPatchMargin(StyleBox.Margin.All, 16.0f);
|
||||
|
||||
var contextMenuExpansionTexture = resCache.GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png");
|
||||
var verbMenuConfirmationTexture = resCache.GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png");
|
||||
|
||||
@@ -1317,6 +1331,16 @@ namespace Content.Client.Stylesheets
|
||||
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#753131")),
|
||||
// ---
|
||||
|
||||
// The default look of paper in UIs. Pages can have components which override this
|
||||
Element<PanelContainer>().Class("PaperDefaultBorder")
|
||||
.Prop(PanelContainer.StylePropertyPanel, paperBackground),
|
||||
Element<RichTextLabel>().Class("PaperWrittenText")
|
||||
.Prop(Label.StylePropertyFont, notoSans12)
|
||||
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#111111")),
|
||||
|
||||
Element<LineEdit>().Class("PaperLineEdit")
|
||||
.Prop(LineEdit.StylePropertyStyleBox, new StyleBoxEmpty()),
|
||||
|
||||
// Red Button ---
|
||||
Element<Button>().Class("ButtonColorRed")
|
||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorDefaultRed),
|
||||
@@ -1408,6 +1432,35 @@ namespace Content.Client.Stylesheets
|
||||
.Prop(Label.StylePropertyFont, notoSans10)
|
||||
.Prop(Label.StylePropertyFontColor, Color.FromHex("#333d3b")),
|
||||
|
||||
// Fancy Tree
|
||||
Element<ContainerButton>().Identifier(TreeItem.StyleIdentifierTreeButton)
|
||||
.Class(TreeItem.StyleClassEvenRow)
|
||||
.Prop(ContainerButton.StylePropertyStyleBox, new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = FancyTreeEvenRowColor,
|
||||
}),
|
||||
|
||||
Element<ContainerButton>().Identifier(TreeItem.StyleIdentifierTreeButton)
|
||||
.Class(TreeItem.StyleClassOddRow)
|
||||
.Prop(ContainerButton.StylePropertyStyleBox, new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = FancyTreeOddRowColor,
|
||||
}),
|
||||
|
||||
Element<ContainerButton>().Identifier(TreeItem.StyleIdentifierTreeButton)
|
||||
.Class(TreeItem.StyleClassSelected)
|
||||
.Prop(ContainerButton.StylePropertyStyleBox, new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = FancyTreeSelectedRowColor,
|
||||
}),
|
||||
|
||||
Element<ContainerButton>().Identifier(TreeItem.StyleIdentifierTreeButton)
|
||||
.Pseudo(ContainerButton.StylePseudoClassHover)
|
||||
.Prop(ContainerButton.StylePropertyStyleBox, new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = FancyTreeSelectedRowColor,
|
||||
}),
|
||||
|
||||
}).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<controls:FancyTree xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls.FancyTree">
|
||||
<ScrollContainer ReturnMeasure="True">
|
||||
<BoxContainer Orientation="Vertical" Name="Body" Access="Public" Margin="2"/>
|
||||
</ScrollContainer>
|
||||
</controls:FancyTree>
|
||||
@@ -0,0 +1,282 @@
|
||||
using Content.Client.Resources;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls.FancyTree;
|
||||
|
||||
/// <summary>
|
||||
/// Functionally similar to <see cref="Tree"/>, but with collapsible sections,
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class FancyTree : Control
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _resCache = default!;
|
||||
|
||||
public const string StylePropertyLineWidth = "LineWidth";
|
||||
public const string StylePropertyLineColor = "LineColor";
|
||||
public const string StylePropertyIconColor = "IconColor";
|
||||
public const string StylePropertyIconExpanded = "IconExpanded";
|
||||
public const string StylePropertyIconCollapsed = "IconCollapsed";
|
||||
public const string StylePropertyIconNoChildren = "IconNoChildren";
|
||||
|
||||
public readonly List<TreeItem> Items = new();
|
||||
|
||||
public event Action<TreeItem?>? OnSelectedItemChanged;
|
||||
|
||||
public int? SelectedIndex { get; private set; }
|
||||
|
||||
private bool _rowStyleUpdateQueued = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to draw the lines connecting parents & children.
|
||||
/// </summary>
|
||||
public bool DrawLines = true;
|
||||
|
||||
/// <summary>
|
||||
/// Colour of the lines connecting parents & their child entries.
|
||||
/// </summary>
|
||||
public Color LineColor = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// Color used to modulate the icon textures.
|
||||
/// </summary>
|
||||
public Color IconColor = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// Width of the lines connecting parents & their child entries.
|
||||
/// </summary>
|
||||
public int LineWidth = 2;
|
||||
|
||||
// If people ever want to customize this, this should be a style parameter/
|
||||
public const int Indentation = 16;
|
||||
|
||||
public const string DefaultIconExpanded = "/Textures/Interface/Nano/inverted_triangle.svg.png";
|
||||
public const string DefaultIconCollapsed = "/Textures/Interface/Nano/triangle_right.png";
|
||||
public const string DefaultIconNoChildren = "/Textures/Interface/Nano/triangle_right_hollow.svg.png";
|
||||
|
||||
public Texture? IconExpanded;
|
||||
public Texture? IconCollapsed;
|
||||
public Texture? IconNoChildren;
|
||||
|
||||
/// <summary>
|
||||
/// If true, tree entries will hide their icon if the texture is set to null. If the icon is hidden then the
|
||||
/// text of that entry will no longer be aligned with sibling entries that do have an icon.
|
||||
/// </summary>
|
||||
public bool HideEmptyIcon
|
||||
{
|
||||
get => _hideEmptyIcon;
|
||||
set => SetHideEmptyIcon(value);
|
||||
}
|
||||
private bool _hideEmptyIcon;
|
||||
|
||||
public TreeItem? SelectedItem => SelectedIndex == null ? null : Items[SelectedIndex.Value];
|
||||
|
||||
/// <summary>
|
||||
/// If true, a collapsed item will automatically expand when first selected. If false, it has to be manually expanded by
|
||||
/// clicking on it a second time.
|
||||
/// </summary>
|
||||
public bool AutoExpand = true;
|
||||
|
||||
public FancyTree()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
LoadIcons();
|
||||
}
|
||||
|
||||
private void LoadIcons()
|
||||
{
|
||||
IconColor = TryGetStyleProperty(StylePropertyIconColor, out Color color) ? color : Color.White;
|
||||
string? path;
|
||||
|
||||
if (!TryGetStyleProperty(StylePropertyIconExpanded, out IconExpanded))
|
||||
IconExpanded = _resCache.GetTexture(DefaultIconExpanded);
|
||||
|
||||
if (!TryGetStyleProperty(StylePropertyIconCollapsed, out IconCollapsed))
|
||||
IconCollapsed = _resCache.GetTexture(DefaultIconCollapsed);
|
||||
|
||||
if (!TryGetStyleProperty(StylePropertyIconNoChildren, out IconNoChildren))
|
||||
IconNoChildren = _resCache.GetTexture(DefaultIconNoChildren);
|
||||
|
||||
foreach (var item in Body.Children)
|
||||
{
|
||||
RecursiveUpdateIcon((TreeItem) item);
|
||||
}
|
||||
}
|
||||
|
||||
public TreeItem AddItem(TreeItem? parent = null)
|
||||
{
|
||||
if (parent != null)
|
||||
{
|
||||
if (parent.Tree != this)
|
||||
throw new ArgumentException("Parent must be owned by this tree.", nameof(parent));
|
||||
|
||||
DebugTools.Assert(Items[parent.Index] == parent);
|
||||
}
|
||||
|
||||
var item = new TreeItem()
|
||||
{
|
||||
Tree = this,
|
||||
Index = Items.Count,
|
||||
};
|
||||
|
||||
Items.Add(item);
|
||||
item.Icon.SetSize = (Indentation, Indentation);
|
||||
item.Button.OnPressed += (_) => OnPressed(item);
|
||||
|
||||
if (parent == null)
|
||||
Body.AddChild(item);
|
||||
else
|
||||
{
|
||||
item.Padding.MinWidth = parent.Padding.MinWidth + Indentation;
|
||||
parent.Body.AddChild(item);
|
||||
}
|
||||
|
||||
item.UpdateIcon();
|
||||
QueueRowStyleUpdate();
|
||||
return item;
|
||||
}
|
||||
|
||||
private void OnPressed(TreeItem item)
|
||||
{
|
||||
if (SelectedIndex == item.Index)
|
||||
{
|
||||
item.SetExpanded(!item.Expanded);
|
||||
return;
|
||||
}
|
||||
|
||||
SetSelectedIndex(item.Index);
|
||||
}
|
||||
|
||||
public void SetSelectedIndex(int? value)
|
||||
{
|
||||
if (value == null || value < 0 || value >= Items.Count)
|
||||
value = null;
|
||||
|
||||
if (SelectedIndex == value)
|
||||
return;
|
||||
|
||||
SelectedItem?.SetSelected(false);
|
||||
SelectedIndex = value;
|
||||
|
||||
var newSelection = SelectedItem;
|
||||
if (newSelection != null)
|
||||
{
|
||||
newSelection.SetSelected(true);
|
||||
if (AutoExpand && !newSelection.Expanded)
|
||||
newSelection.SetExpanded(true);
|
||||
}
|
||||
|
||||
OnSelectedItemChanged?.Invoke(newSelection);
|
||||
}
|
||||
|
||||
public void SetAllExpanded(bool value)
|
||||
{
|
||||
foreach (var item in Body.Children)
|
||||
{
|
||||
RecursiveSetExpanded((TreeItem) item, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void RecursiveSetExpanded(TreeItem item, bool value)
|
||||
{
|
||||
item.SetExpanded(value);
|
||||
foreach (var child in item.Body.Children)
|
||||
{
|
||||
RecursiveSetExpanded((TreeItem) child, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var item in Items)
|
||||
{
|
||||
item.Dispose();
|
||||
}
|
||||
|
||||
Items.Clear();
|
||||
Body.Children.Clear();
|
||||
SelectedIndex = null;
|
||||
}
|
||||
|
||||
public void QueueRowStyleUpdate()
|
||||
{
|
||||
_rowStyleUpdateQueued = true;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
if (!_rowStyleUpdateQueued)
|
||||
return;
|
||||
|
||||
_rowStyleUpdateQueued = false;
|
||||
|
||||
int index = 0;
|
||||
|
||||
foreach (var item in Body.Children)
|
||||
{
|
||||
RecursivelyUpdateRowStyle((TreeItem) item, ref index);
|
||||
}
|
||||
}
|
||||
|
||||
private void RecursivelyUpdateRowStyle(TreeItem item, ref int index)
|
||||
{
|
||||
if (int.IsOddInteger(index))
|
||||
{
|
||||
item.Button.RemoveStyleClass(TreeItem.StyleClassEvenRow);
|
||||
item.Button.AddStyleClass(TreeItem.StyleClassOddRow);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Button.AddStyleClass(TreeItem.StyleClassEvenRow);
|
||||
item.Button.RemoveStyleClass(TreeItem.StyleClassOddRow);
|
||||
}
|
||||
|
||||
index++;
|
||||
|
||||
if (!item.Expanded)
|
||||
return;
|
||||
|
||||
foreach (var child in item.Body.Children)
|
||||
{
|
||||
RecursivelyUpdateRowStyle((TreeItem) child, ref index);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetHideEmptyIcon(bool value)
|
||||
{
|
||||
if (value == _hideEmptyIcon)
|
||||
return;
|
||||
|
||||
_hideEmptyIcon = value;
|
||||
|
||||
foreach (var item in Body.Children)
|
||||
{
|
||||
RecursiveUpdateIcon((TreeItem) item);
|
||||
}
|
||||
}
|
||||
|
||||
private void RecursiveUpdateIcon(TreeItem item)
|
||||
{
|
||||
item.UpdateIcon();
|
||||
|
||||
foreach (var child in item.Body.Children)
|
||||
{
|
||||
RecursiveUpdateIcon((TreeItem) child);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void StylePropertiesChanged()
|
||||
{
|
||||
LoadIcons();
|
||||
LineColor = TryGetStyleProperty(StylePropertyLineColor, out Color color) ? color: Color.White;
|
||||
LineWidth = TryGetStyleProperty(StylePropertyLineWidth, out int width) ? width : 2;
|
||||
base.StylePropertiesChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<controls:TreeItem xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls.FancyTree">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<ContainerButton Name="Button" Access="Public">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Control Name="Padding" Access="Public"/>
|
||||
<TextureRect Name="Icon" Access="Public" Stretch="KeepCentered" Visible="False"/>
|
||||
<Label Margin="2 0 2 0" Name="Label" Access="Public"/>
|
||||
</BoxContainer>
|
||||
</ContainerButton>
|
||||
<BoxContainer Name="Body" Access="Public" Orientation="Vertical" Visible="False"/>
|
||||
</BoxContainer>
|
||||
<controls:TreeLine/>
|
||||
</controls:TreeItem>
|
||||
@@ -0,0 +1,92 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls.FancyTree;
|
||||
|
||||
/// <summary>
|
||||
/// Element of a <see cref="FancyTree"/>
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TreeItem : PanelContainer
|
||||
{
|
||||
public const string StyleClassSelected = "selected";
|
||||
public const string StyleIdentifierTreeButton = "tree-button";
|
||||
public const string StyleClassEvenRow = "even-row";
|
||||
public const string StyleClassOddRow = "odd-row";
|
||||
|
||||
public object? Metadata;
|
||||
public int Index;
|
||||
public FancyTree Tree = default!;
|
||||
public event Action<TreeItem>? OnSelected;
|
||||
public event Action<TreeItem>? OnDeselected;
|
||||
|
||||
public bool Expanded { get; private set; } = false;
|
||||
|
||||
public TreeItem()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
Button.StyleIdentifier = StyleIdentifierTreeButton;
|
||||
Body.OnChildAdded += OnItemAdded;
|
||||
Body.OnChildRemoved += OnItemRemoved;
|
||||
}
|
||||
|
||||
private void OnItemRemoved(Control obj)
|
||||
{
|
||||
Tree.QueueRowStyleUpdate();
|
||||
|
||||
if (Body.ChildCount == 0)
|
||||
{
|
||||
Body.Visible = false;
|
||||
UpdateIcon();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnItemAdded(Control obj)
|
||||
{
|
||||
Tree.QueueRowStyleUpdate();
|
||||
|
||||
if (Body.ChildCount == 1)
|
||||
{
|
||||
Body.Visible = Expanded && Body.ChildCount != 0;
|
||||
UpdateIcon();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetExpanded(bool value)
|
||||
{
|
||||
if (Expanded == value)
|
||||
return;
|
||||
|
||||
Expanded = value;
|
||||
Body.Visible = Expanded && Body.ChildCount > 0;
|
||||
UpdateIcon();
|
||||
Tree.QueueRowStyleUpdate();
|
||||
}
|
||||
|
||||
public void SetSelected(bool value)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
OnSelected?.Invoke(this);
|
||||
Button.AddStyleClass(StyleClassSelected);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnDeselected?.Invoke(this);
|
||||
Button.RemoveStyleClass(StyleClassSelected);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateIcon()
|
||||
{
|
||||
if (Body.ChildCount == 0)
|
||||
Icon.Texture = Tree.IconNoChildren;
|
||||
else
|
||||
Icon.Texture = Expanded ? Tree.IconExpanded : Tree.IconCollapsed;
|
||||
|
||||
Icon.Modulate = Tree.IconColor;
|
||||
Icon.Visible = Icon.Texture != null || !Tree.HideEmptyIcon;
|
||||
}
|
||||
}
|
||||
61
Content.Client/UserInterface/Controls/FancyTree/TreeLine.cs
Normal file
61
Content.Client/UserInterface/Controls/FancyTree/TreeLine.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls.FancyTree;
|
||||
|
||||
/// <summary>
|
||||
/// This is a basic control that draws the lines connecting parents & children in a tree.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Ideally this would just be a draw method in <see cref="TreeItem"/>, but sadly the draw override gets called BEFORE children are drawn.
|
||||
/// </remarks>
|
||||
public sealed class TreeLine : Control
|
||||
{
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
// This is basically just a shitty hack to call Draw() after children get drawn.
|
||||
if (Parent is not TreeItem parent)
|
||||
return;
|
||||
|
||||
if (!parent.Expanded || !parent.Tree.DrawLines || parent.Body.ChildCount == 0)
|
||||
return;
|
||||
|
||||
var width = Math.Max(1, (int) (parent.Tree.LineWidth * UIScale));
|
||||
var w1 = width / 2;
|
||||
var w2 = width - w1;
|
||||
|
||||
var global = parent.GlobalPixelPosition;
|
||||
|
||||
var iconPos = parent.Icon.GlobalPixelPosition - global;
|
||||
var iconSize = parent.Icon.PixelSize;
|
||||
var x = iconPos.X + iconSize.X / 2;
|
||||
DebugTools.Assert(parent.Icon.Visible);
|
||||
|
||||
var buttonPos = parent.Button.GlobalPixelPosition - global;
|
||||
var buttonSize = parent.Button.PixelSize;
|
||||
var y1 = buttonPos.Y + buttonSize.Y;
|
||||
|
||||
var lastItem = (TreeItem) parent.Body.GetChild(parent.Body.ChildCount - 1);
|
||||
|
||||
var childPos = lastItem.Button.GlobalPixelPosition - global;
|
||||
var y2 = childPos.Y + lastItem.Button.PixelSize.Y / 2;
|
||||
|
||||
// Vertical line
|
||||
var rect = new UIBox2i((x - w1, y1), (x + w2, y2));
|
||||
handle.DrawRect(rect, parent.Tree.LineColor);
|
||||
|
||||
// Horizontal lines
|
||||
var dx = Math.Max(1, (int) (FancyTree.Indentation * UIScale / 2));
|
||||
foreach (var child in parent.Body.Children)
|
||||
{
|
||||
var item = (TreeItem) child;
|
||||
var pos = item.Button.GlobalPixelPosition - global;
|
||||
var y = pos.Y + item.Button.PixelSize.Y / 2;
|
||||
rect = new UIBox2i((x - w1, y - w1), (x + dx, y + w2));
|
||||
handle.DrawRect(rect, parent.Tree.LineColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.Administration.UI;
|
||||
using Content.Client.Administration.UI.Bwoink;
|
||||
using Content.Client.Administration.UI.CustomControls;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
@@ -11,6 +13,7 @@ using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
@@ -19,6 +22,7 @@ using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
using BwoinkPanel = Content.Client.Administration.UI.Bwoink.BwoinkPanel;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Bwoink;
|
||||
|
||||
@@ -28,13 +32,14 @@ public sealed class AHelpUIController: UIController, IOnStateChanged<GameplaySta
|
||||
[Dependency] private readonly IClientAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
private BwoinkSystem? _bwoinkSystem;
|
||||
private MenuButton? AhelpButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.AHelpButton;
|
||||
private IAHelpUIHandler? _uiHelper;
|
||||
public IAHelpUIHandler? UIHelper;
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_uiHelper == null);
|
||||
DebugTools.Assert(UIHelper == null);
|
||||
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
|
||||
|
||||
CommandBinds.Builder
|
||||
@@ -65,23 +70,24 @@ public sealed class AHelpUIController: UIController, IOnStateChanged<GameplaySta
|
||||
|
||||
private void OnAdminStatusUpdated()
|
||||
{
|
||||
if (_uiHelper is not { IsOpen: true })
|
||||
if (UIHelper is not { IsOpen: true })
|
||||
return;
|
||||
EnsureUIHelper();
|
||||
}
|
||||
|
||||
|
||||
private void AHelpButtonPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
_uiHelper!.ToggleWindow();
|
||||
UIHelper!.ToggleWindow();
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
SetAHelpPressed(false);
|
||||
_adminManager.AdminStatusUpdated -= OnAdminStatusUpdated;
|
||||
_uiHelper?.Dispose();
|
||||
_uiHelper = null;
|
||||
UIHelper?.Dispose();
|
||||
UIHelper = null;
|
||||
CommandBinds.Unregister<AHelpUIController>();
|
||||
}
|
||||
public void OnSystemLoaded(BwoinkSystem system)
|
||||
@@ -120,33 +126,33 @@ public sealed class AHelpUIController: UIController, IOnStateChanged<GameplaySta
|
||||
}
|
||||
|
||||
EnsureUIHelper();
|
||||
if (!_uiHelper!.IsOpen)
|
||||
if (!UIHelper!.IsOpen)
|
||||
{
|
||||
AhelpButton?.StyleClasses.Add(MenuButton.StyleClassRedTopButton);
|
||||
}
|
||||
_uiHelper!.Receive(message);
|
||||
UIHelper!.Receive(message);
|
||||
}
|
||||
|
||||
public void EnsureUIHelper()
|
||||
{
|
||||
var isAdmin = _adminManager.HasFlag(AdminFlags.Adminhelp);
|
||||
|
||||
if (_uiHelper != null && _uiHelper.IsAdmin == isAdmin)
|
||||
if (UIHelper != null && UIHelper.IsAdmin == isAdmin)
|
||||
return;
|
||||
|
||||
_uiHelper?.Dispose();
|
||||
UIHelper?.Dispose();
|
||||
var ownerUserId = _playerManager!.LocalPlayer!.UserId;
|
||||
_uiHelper = isAdmin ? new AdminAHelpUIHandler(ownerUserId) : new UserAHelpUIHandler(ownerUserId);
|
||||
UIHelper = isAdmin ? new AdminAHelpUIHandler(ownerUserId) : new UserAHelpUIHandler(ownerUserId);
|
||||
|
||||
_uiHelper.SendMessageAction = (userId, textMessage) => _bwoinkSystem?.Send(userId, textMessage);
|
||||
_uiHelper.OnClose += () => { SetAHelpPressed(false); };
|
||||
_uiHelper.OnOpen += () => { SetAHelpPressed(true); };
|
||||
SetAHelpPressed(_uiHelper.IsOpen);
|
||||
UIHelper.SendMessageAction = (userId, textMessage) => _bwoinkSystem?.Send(userId, textMessage);
|
||||
UIHelper.OnClose += () => { SetAHelpPressed(false); };
|
||||
UIHelper.OnOpen += () => { SetAHelpPressed(true); };
|
||||
SetAHelpPressed(UIHelper.IsOpen);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_uiHelper?.Close();
|
||||
UIHelper?.Close();
|
||||
}
|
||||
|
||||
public void Open()
|
||||
@@ -157,24 +163,63 @@ public sealed class AHelpUIController: UIController, IOnStateChanged<GameplaySta
|
||||
return;
|
||||
}
|
||||
EnsureUIHelper();
|
||||
if (_uiHelper!.IsOpen)
|
||||
if (UIHelper!.IsOpen)
|
||||
return;
|
||||
_uiHelper!.Open(localPlayer.UserId);
|
||||
UIHelper!.Open(localPlayer.UserId);
|
||||
}
|
||||
public void Open(NetUserId userId)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
if (!_uiHelper!.IsAdmin)
|
||||
if (!UIHelper!.IsAdmin)
|
||||
return;
|
||||
_uiHelper?.Open(userId);
|
||||
UIHelper?.Open(userId);
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
EnsureUIHelper();
|
||||
_uiHelper?.ToggleWindow();
|
||||
UIHelper?.ToggleWindow();
|
||||
}
|
||||
|
||||
|
||||
public void PopOut()
|
||||
{
|
||||
EnsureUIHelper();
|
||||
if (UIHelper is not AdminAHelpUIHandler helper)
|
||||
return;
|
||||
|
||||
if (helper.Window == null || helper.Control == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
helper.Control.Orphan();
|
||||
helper.Window.Dispose();
|
||||
helper.Window = null;
|
||||
|
||||
var monitor = _clyde.EnumerateMonitors().First();
|
||||
|
||||
helper.ClydeWindow = _clyde.CreateWindow(new WindowCreateParameters
|
||||
{
|
||||
Maximized = false,
|
||||
Title = "Admin Help",
|
||||
Monitor = monitor,
|
||||
Width = 900,
|
||||
Height = 500
|
||||
});
|
||||
|
||||
helper.ClydeWindow.RequestClosed += helper.OnRequestClosed;
|
||||
helper.ClydeWindow.DisposeOnClose = true;
|
||||
|
||||
helper.WindowRoot = _uiManager.CreateWindowRoot(helper.ClydeWindow);
|
||||
helper.WindowRoot.AddChild(helper.Control);
|
||||
|
||||
helper.Control.PopOut.Disabled = true;
|
||||
helper.Control.PopOut.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// please kill all this indirection
|
||||
public interface IAHelpUIHandler: IDisposable
|
||||
{
|
||||
public bool IsAdmin { get; }
|
||||
@@ -196,31 +241,54 @@ public sealed class AdminAHelpUIHandler : IAHelpUIHandler
|
||||
}
|
||||
private readonly Dictionary<NetUserId, BwoinkPanel> _activePanelMap = new();
|
||||
public bool IsAdmin => true;
|
||||
public bool IsOpen => _window is { Disposed: false, IsOpen: true };
|
||||
private BwoinkWindow? _window;
|
||||
public bool IsOpen => Window is { Disposed: false, IsOpen: true } || ClydeWindow is { IsDisposed: false };
|
||||
|
||||
public BwoinkWindow? Window;
|
||||
public WindowRoot? WindowRoot;
|
||||
public IClydeWindow? ClydeWindow;
|
||||
public BwoinkControl? Control;
|
||||
|
||||
public void Receive(SharedBwoinkSystem.BwoinkTextMessage message)
|
||||
{
|
||||
var window = EnsurePanel(message.UserId);
|
||||
window.ReceiveLine(message);
|
||||
_window?.OnBwoink(message.UserId);
|
||||
var panel = EnsurePanel(message.UserId);
|
||||
panel.ReceiveLine(message);
|
||||
Control?.OnBwoink(message.UserId);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_window?.Close();
|
||||
Window?.Close();
|
||||
|
||||
// popped-out window is being closed
|
||||
if (ClydeWindow != null)
|
||||
{
|
||||
ClydeWindow.RequestClosed -= OnRequestClosed;
|
||||
ClydeWindow.Dispose();
|
||||
// need to dispose control cause we cant reattach it directly back to the window
|
||||
// but orphan panels first so -they- can get readded when the window is opened again
|
||||
if (Control != null)
|
||||
{
|
||||
foreach (var (_, panel) in _activePanelMap)
|
||||
{
|
||||
panel.Orphan();
|
||||
}
|
||||
Control?.Dispose();
|
||||
}
|
||||
// window wont be closed here so we will invoke ourselves
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
EnsurePanel(_ownerId);
|
||||
if (_window!.IsOpen)
|
||||
if (IsOpen)
|
||||
{
|
||||
_window.Close();
|
||||
Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_window.OpenCentered();
|
||||
Window!.OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,28 +299,46 @@ public sealed class AdminAHelpUIHandler : IAHelpUIHandler
|
||||
public void Open(NetUserId channelId)
|
||||
{
|
||||
SelectChannel(channelId);
|
||||
_window?.OpenCentered();
|
||||
Window?.OpenCentered();
|
||||
}
|
||||
|
||||
private void EnsureWindow()
|
||||
public void OnRequestClosed(WindowRequestClosedEventArgs args)
|
||||
{
|
||||
if (_window is { Disposed: false })
|
||||
return;
|
||||
_window = new BwoinkWindow(this);
|
||||
_window.OnClose += () => { OnClose?.Invoke(); };
|
||||
_window.OnOpen += () => { OnOpen?.Invoke(); };
|
||||
Close();
|
||||
}
|
||||
|
||||
private void EnsureControl()
|
||||
{
|
||||
if (Control is { Disposed: false })
|
||||
return;
|
||||
|
||||
Window = new BwoinkWindow();
|
||||
Control = Window.Bwoink;
|
||||
Window.OnClose += () => { OnClose?.Invoke(); };
|
||||
Window.OnOpen += () => { OnOpen?.Invoke(); };
|
||||
|
||||
// need to readd any unattached panels..
|
||||
foreach (var (_, panel) in _activePanelMap)
|
||||
{
|
||||
if (!Control!.BwoinkArea.Children.Contains(panel))
|
||||
{
|
||||
Control!.BwoinkArea.AddChild(panel);
|
||||
}
|
||||
panel.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
public BwoinkPanel EnsurePanel(NetUserId channelId)
|
||||
{
|
||||
EnsureWindow();
|
||||
EnsureControl();
|
||||
|
||||
if (_activePanelMap.TryGetValue(channelId, out var existingPanel))
|
||||
return existingPanel;
|
||||
|
||||
_activePanelMap[channelId] = existingPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(channelId, text));
|
||||
existingPanel.Visible = false;
|
||||
if (!_window!.BwoinkArea.Children.Contains(existingPanel))
|
||||
_window.BwoinkArea.AddChild(existingPanel);
|
||||
if (!Control!.BwoinkArea.Children.Contains(existingPanel))
|
||||
Control.BwoinkArea.AddChild(existingPanel);
|
||||
|
||||
return existingPanel;
|
||||
}
|
||||
@@ -261,13 +347,14 @@ public sealed class AdminAHelpUIHandler : IAHelpUIHandler
|
||||
private void SelectChannel(NetUserId uid)
|
||||
{
|
||||
EnsurePanel(uid);
|
||||
_window!.SelectChannel(uid);
|
||||
Control!.SelectChannel(uid);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_window?.Dispose();
|
||||
_window = null;
|
||||
Window?.Dispose();
|
||||
Window = null;
|
||||
Control = null;
|
||||
_activePanelMap.Clear();
|
||||
}
|
||||
}
|
||||
@@ -309,6 +396,11 @@ public sealed class UserAHelpUIHandler : IAHelpUIHandler
|
||||
}
|
||||
}
|
||||
|
||||
// user can't pop out their window.
|
||||
public void PopOut()
|
||||
{
|
||||
}
|
||||
|
||||
public event Action? OnClose;
|
||||
public event Action? OnOpen;
|
||||
public Action<NetUserId, string>? SendMessageAction { get; set; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Guidebook;
|
||||
using Content.Client.Info;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Info;
|
||||
@@ -25,6 +26,7 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
||||
[Dependency] private readonly ChangelogUIController _changelog = default!;
|
||||
[Dependency] private readonly InfoUIController _info = default!;
|
||||
[Dependency] private readonly OptionsUIController _options = default!;
|
||||
[UISystemDependency] private readonly GuidebookSystem? _guidebook = default!;
|
||||
|
||||
private Options.UI.EscapeMenu? _escapeWindow;
|
||||
|
||||
@@ -98,6 +100,11 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
||||
_uri.OpenUri(_cfg.GetCVar(CCVars.InfoLinksWiki));
|
||||
};
|
||||
|
||||
_escapeWindow.GuidebookButton.OnPressed += _ =>
|
||||
{
|
||||
_guidebook?.OpenGuidebook();
|
||||
};
|
||||
|
||||
// Hide wiki button if we don't have a link for it.
|
||||
_escapeWindow.WikiButton.Visible = _cfg.GetCVar(CCVars.InfoLinksWiki) != "";
|
||||
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
using Content.Client.Options.UI;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.EscapeMenu;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class OptionsUIController : UIController
|
||||
{
|
||||
[Dependency] private readonly IConsoleHost _con = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
_con.RegisterCommand("options", Loc.GetString("cmd-options-desc"), Loc.GetString("cmd-options-help"), OptionsCommand);
|
||||
}
|
||||
|
||||
private void OptionsCommand(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
ToggleWindow();
|
||||
return;
|
||||
}
|
||||
OpenWindow();
|
||||
|
||||
if (!int.TryParse(args[0], out var tab))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-int", ("arg", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
_optionsWindow.Tabs.CurrentTab = tab;
|
||||
}
|
||||
|
||||
private OptionsMenu _optionsWindow = default!;
|
||||
|
||||
private void EnsureWindow()
|
||||
|
||||
@@ -259,7 +259,8 @@ namespace Content.Client.Verbs.UI
|
||||
private void ExecuteVerb(Verb verb)
|
||||
{
|
||||
_verbSystem.ExecuteVerb(CurrentTarget, verb);
|
||||
if (verb.CloseMenu)
|
||||
|
||||
if (verb.CloseMenu ?? verb.CloseMenuDefault)
|
||||
_context.Close();
|
||||
}
|
||||
}
|
||||
|
||||
207
Content.Client/Weather/WeatherOverlay.cs
Normal file
207
Content.Client/Weather/WeatherOverlay.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Parallax;
|
||||
using Content.Shared.Weather;
|
||||
using OpenToolkit.Graphics.ES11;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Weather;
|
||||
|
||||
public sealed class WeatherOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IResourceCache _cache = default!;
|
||||
private readonly SharedTransformSystem _transform;
|
||||
private readonly SpriteSystem _sprite;
|
||||
private readonly WeatherSystem _weather;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
|
||||
|
||||
private IRenderTexture? _blep;
|
||||
|
||||
public WeatherOverlay(SharedTransformSystem transform, SpriteSystem sprite, WeatherSystem weather)
|
||||
{
|
||||
ZIndex = ParallaxSystem.ParallaxZIndex + 1;
|
||||
_transform = transform;
|
||||
_weather = weather;
|
||||
_sprite = sprite;
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
protected override bool BeforeDraw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (args.MapId == MapId.Nullspace)
|
||||
return false;
|
||||
|
||||
if (!_entManager.TryGetComponent<WeatherComponent>(_mapManager.GetMapEntityId(args.MapId), out var weather) ||
|
||||
weather.Weather == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.BeforeDraw(in args);
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var mapUid = _mapManager.GetMapEntityId(args.MapId);
|
||||
|
||||
if (!_entManager.TryGetComponent<WeatherComponent>(mapUid, out var weather) ||
|
||||
weather.Weather == null ||
|
||||
!_protoManager.TryIndex<WeatherPrototype>(weather.Weather, out var weatherProto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var alpha = _weather.GetPercent(weather, mapUid, weatherProto);
|
||||
DrawWorld(args, weatherProto, alpha);
|
||||
}
|
||||
|
||||
private void DrawWorld(in OverlayDrawArgs args, WeatherPrototype weatherProto, float alpha)
|
||||
{
|
||||
var worldHandle = args.WorldHandle;
|
||||
var mapId = args.MapId;
|
||||
var worldAABB = args.WorldAABB;
|
||||
var worldBounds = args.WorldBounds;
|
||||
var invMatrix = args.Viewport.GetWorldToLocalMatrix();
|
||||
var position = args.Viewport.Eye?.Position.Position ?? Vector2.Zero;
|
||||
|
||||
if (_blep?.Texture.Size != args.Viewport.Size)
|
||||
{
|
||||
_blep?.Dispose();
|
||||
_blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil");
|
||||
}
|
||||
|
||||
// Cut out the irrelevant bits via stencil
|
||||
// This is why we don't just use parallax; we might want specific tiles to get drawn over
|
||||
// particularly for planet maps or stations.
|
||||
worldHandle.RenderInRenderTarget(_blep, () =>
|
||||
{
|
||||
var bodyQuery = _entManager.GetEntityQuery<PhysicsComponent>();
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB))
|
||||
{
|
||||
var matrix = _transform.GetWorldMatrix(grid.Owner, xformQuery);
|
||||
Matrix3.Multiply(in matrix, in invMatrix, out var matty);
|
||||
worldHandle.SetTransform(matty);
|
||||
|
||||
foreach (var tile in grid.GetTilesIntersecting(worldAABB))
|
||||
{
|
||||
// Ignored tiles for stencil
|
||||
if (_weather.CanWeatherAffect(grid, tile, bodyQuery))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var gridTile = new Box2(tile.GridIndices * grid.TileSize,
|
||||
(tile.GridIndices + Vector2i.One) * grid.TileSize);
|
||||
|
||||
worldHandle.DrawRect(gridTile, Color.White);
|
||||
}
|
||||
}
|
||||
|
||||
}, Color.Transparent);
|
||||
|
||||
worldHandle.SetTransform(Matrix3.Identity);
|
||||
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("StencilMask").Instance());
|
||||
worldHandle.DrawTextureRect(_blep.Texture, worldBounds);
|
||||
Texture? sprite = null;
|
||||
var curTime = _timing.RealTime;
|
||||
|
||||
switch (weatherProto.Sprite)
|
||||
{
|
||||
case SpriteSpecifier.Rsi rsi:
|
||||
var rsiActual = _cache.GetResource<RSIResource>(rsi.RsiPath).RSI;
|
||||
rsiActual.TryGetState(rsi.RsiState, out var state);
|
||||
var frames = state!.GetFrames(RSI.State.Direction.South);
|
||||
var delays = state.GetDelays();
|
||||
var totalDelay = delays.Sum();
|
||||
var time = curTime.TotalSeconds % totalDelay;
|
||||
var delaySum = 0f;
|
||||
|
||||
for (var i = 0; i < delays.Length; i++)
|
||||
{
|
||||
var delay = delays[i];
|
||||
delaySum += delay;
|
||||
|
||||
if (time > delaySum)
|
||||
continue;
|
||||
|
||||
sprite = frames[i];
|
||||
break;
|
||||
}
|
||||
|
||||
sprite ??= _sprite.Frame0(weatherProto.Sprite);
|
||||
break;
|
||||
case SpriteSpecifier.Texture texture:
|
||||
sprite = texture.GetTexture(_cache);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// Draw the rain
|
||||
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("StencilDraw").Instance());
|
||||
|
||||
// TODO: This is very similar to parallax but we need stencil support but we can probably combine these somehow
|
||||
// and not make it spaghetti, while getting the advantages of not-duped code?
|
||||
|
||||
|
||||
// Okay I have spent like 5 hours on this at this point and afaict you have one of the following comprises:
|
||||
// - No scrolling so the weather is always centered on the player
|
||||
// - Crappy looking rotation but strafing looks okay and scrolls
|
||||
// - Crappy looking strafing but rotation looks okay.
|
||||
// - No rotation
|
||||
// - Storing state across frames to do scrolling and just having it always do topdown.
|
||||
|
||||
// I have chosen no rotation.
|
||||
|
||||
const float scale = 1f;
|
||||
const float slowness = 0f;
|
||||
var scrolling = Vector2.Zero;
|
||||
|
||||
// Size of the texture in world units.
|
||||
var size = (sprite.Size / (float) EyeManager.PixelsPerMeter) * scale;
|
||||
var scrolled = scrolling * (float) curTime.TotalSeconds;
|
||||
|
||||
// Origin - start with the parallax shift itself.
|
||||
var originBL = position * slowness + scrolled;
|
||||
|
||||
// Centre the image.
|
||||
originBL -= size / 2;
|
||||
|
||||
// Remove offset so we can floor.
|
||||
var flooredBL = args.WorldAABB.BottomLeft - originBL;
|
||||
|
||||
// Floor to background size.
|
||||
flooredBL = (flooredBL / size).Floored() * size;
|
||||
|
||||
// Re-offset.
|
||||
flooredBL += originBL;
|
||||
|
||||
for (var x = flooredBL.X; x < args.WorldAABB.Right; x += size.X)
|
||||
{
|
||||
for (var y = flooredBL.Y; y < args.WorldAABB.Top; y += size.Y)
|
||||
{
|
||||
var box = Box2.FromDimensions((x, y), size);
|
||||
worldHandle.DrawTextureRect(sprite, box, (weatherProto.Color ?? Color.White).WithAlpha(alpha));
|
||||
}
|
||||
}
|
||||
|
||||
worldHandle.SetTransform(Matrix3.Identity);
|
||||
worldHandle.UseShader(null);
|
||||
}
|
||||
}
|
||||
218
Content.Client/Weather/WeatherSystem.cs
Normal file
218
Content.Client/Weather/WeatherSystem.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
using Content.Shared.Weather;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client.Weather;
|
||||
|
||||
public sealed class WeatherSystem : SharedWeatherSystem
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
// Consistency isn't really important, just want to avoid sharp changes and there's no way to lerp on engine nicely atm.
|
||||
private float _lastAlpha;
|
||||
private float _lastOcclusion;
|
||||
|
||||
private const float OcclusionLerpRate = 4f;
|
||||
private const float AlphaLerpRate = 4f;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_overlayManager.AddOverlay(new WeatherOverlay(_transform, EntityManager.System<SpriteSystem>(), this));
|
||||
SubscribeLocalEvent<WeatherComponent, ComponentHandleState>(OnWeatherHandleState);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_overlayManager.RemoveOverlay<WeatherOverlay>();
|
||||
}
|
||||
|
||||
protected override void Run(EntityUid uid, WeatherComponent component, WeatherPrototype weather, WeatherState state, float frameTime)
|
||||
{
|
||||
base.Run(uid, component, weather, state, frameTime);
|
||||
|
||||
var ent = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
|
||||
if (ent == null)
|
||||
return;
|
||||
|
||||
var mapUid = Transform(uid).MapUid;
|
||||
var entXform = Transform(ent.Value);
|
||||
|
||||
// Maybe have the viewports manage this?
|
||||
if (mapUid == null || entXform.MapUid != mapUid)
|
||||
{
|
||||
_lastOcclusion = 0f;
|
||||
_lastAlpha = 0f;
|
||||
component.Stream?.Stop();
|
||||
component.Stream = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Timing.IsFirstTimePredicted || weather.Sound == null)
|
||||
return;
|
||||
|
||||
component.Stream ??= _audio.PlayGlobal(weather.Sound, Filter.Local(), true);
|
||||
var volumeMod = MathF.Pow(10, weather.Sound.Params.Volume / 10f);
|
||||
|
||||
var stream = (AudioSystem.PlayingStream) component.Stream!;
|
||||
var alpha = GetPercent(component, mapUid.Value, weather);
|
||||
alpha = MathF.Pow(alpha, 2f) * volumeMod;
|
||||
// TODO: Lerp this occlusion.
|
||||
var occlusion = 0f;
|
||||
// TODO: Fade-out needs to be slower
|
||||
// TODO: HELPER PLZ
|
||||
|
||||
// Work out tiles nearby to determine volume.
|
||||
if (TryComp<MapGridComponent>(entXform.GridUid, out var grid))
|
||||
{
|
||||
// Floodfill to the nearest tile and use that for audio.
|
||||
var seed = grid.GetTileRef(entXform.Coordinates);
|
||||
var frontier = new Queue<TileRef>();
|
||||
frontier.Enqueue(seed);
|
||||
// If we don't have a nearest node don't play any sound.
|
||||
EntityCoordinates? nearestNode = null;
|
||||
var bodyQuery = GetEntityQuery<PhysicsComponent>();
|
||||
var visited = new HashSet<Vector2i>();
|
||||
|
||||
while (frontier.TryDequeue(out var node))
|
||||
{
|
||||
if (!visited.Add(node.GridIndices))
|
||||
continue;
|
||||
|
||||
if (!CanWeatherAffect(grid, node, bodyQuery))
|
||||
{
|
||||
// Add neighbors
|
||||
// TODO: Ideally we pick some deterministically random direction and use that
|
||||
// We can't just do that naively here because it will flicker between nearby tiles.
|
||||
for (var x = -1; x <= 1; x++)
|
||||
{
|
||||
for (var y = -1; y <= 1; y++)
|
||||
{
|
||||
if (Math.Abs(x) == 1 && Math.Abs(y) == 1 ||
|
||||
x == 0 && y == 0 ||
|
||||
(new Vector2(x, y) + node.GridIndices - seed.GridIndices).Length > 3)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
frontier.Enqueue(grid.GetTileRef(new Vector2i(x, y) + node.GridIndices));
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
nearestNode = new EntityCoordinates(entXform.GridUid.Value,
|
||||
(Vector2) node.GridIndices + (grid.TileSize / 2f));
|
||||
break;
|
||||
}
|
||||
|
||||
if (nearestNode == null)
|
||||
alpha = 0f;
|
||||
else
|
||||
{
|
||||
var entPos = _transform.GetWorldPosition(entXform);
|
||||
var sourceRelative = nearestNode.Value.ToMap(EntityManager).Position - entPos;
|
||||
|
||||
if (sourceRelative.LengthSquared > 1f)
|
||||
{
|
||||
occlusion = _physics.IntersectRayPenetration(entXform.MapID,
|
||||
new CollisionRay(entPos, sourceRelative.Normalized, _audio.OcclusionCollisionMask),
|
||||
sourceRelative.Length, stream.TrackingEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (MathHelper.CloseTo(_lastOcclusion, occlusion, 0.01f))
|
||||
_lastOcclusion = occlusion;
|
||||
else
|
||||
_lastOcclusion += (occlusion - _lastOcclusion) * OcclusionLerpRate * frameTime;
|
||||
|
||||
if (MathHelper.CloseTo(_lastAlpha, alpha, 0.01f))
|
||||
_lastAlpha = alpha;
|
||||
else
|
||||
_lastAlpha += (alpha - _lastAlpha) * AlphaLerpRate * frameTime;
|
||||
|
||||
// Full volume if not on grid
|
||||
stream.Source.SetVolumeDirect(_lastAlpha);
|
||||
stream.Source.SetOcclusion(_lastOcclusion);
|
||||
}
|
||||
|
||||
public float GetPercent(WeatherComponent component, EntityUid mapUid, WeatherPrototype weatherProto)
|
||||
{
|
||||
var pauseTime = _metadata.GetPauseTime(mapUid);
|
||||
var elapsed = Timing.CurTime - (component.StartTime + pauseTime);
|
||||
var duration = component.Duration;
|
||||
var remaining = duration - elapsed;
|
||||
float alpha;
|
||||
|
||||
if (elapsed < weatherProto.StartupTime)
|
||||
{
|
||||
alpha = (float) (elapsed / weatherProto.StartupTime);
|
||||
}
|
||||
else if (remaining < weatherProto.ShutdownTime)
|
||||
{
|
||||
alpha = (float) (remaining / weatherProto.ShutdownTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
alpha = 1f;
|
||||
}
|
||||
|
||||
return alpha;
|
||||
}
|
||||
|
||||
protected override bool SetState(EntityUid uid, WeatherComponent component, WeatherState state, WeatherPrototype prototype)
|
||||
{
|
||||
if (!base.SetState(uid, component, state, prototype))
|
||||
return false;
|
||||
|
||||
if (!Timing.IsFirstTimePredicted)
|
||||
return true;
|
||||
|
||||
// TODO: Fades
|
||||
component.Stream?.Stop();
|
||||
component.Stream = null;
|
||||
component.Stream = _audio.PlayGlobal(prototype.Sound, Filter.Local(), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void EndWeather(WeatherComponent component)
|
||||
{
|
||||
_lastOcclusion = 0f;
|
||||
_lastAlpha = 0f;
|
||||
base.EndWeather(component);
|
||||
}
|
||||
|
||||
private void OnWeatherHandleState(EntityUid uid, WeatherComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not WeatherComponentState state)
|
||||
return;
|
||||
|
||||
if (component.Weather != state.Weather || !component.EndTime.Equals(state.EndTime) || !component.StartTime.Equals(state.StartTime))
|
||||
{
|
||||
EndWeather(component);
|
||||
|
||||
if (state.Weather != null)
|
||||
StartWeather(component, ProtoMan.Index<WeatherPrototype>(state.Weather));
|
||||
}
|
||||
|
||||
component.EndTime = state.EndTime;
|
||||
component.StartTime = state.StartTime;
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ namespace Content.IntegrationTests.Tests.Doors
|
||||
components:
|
||||
- type: Door
|
||||
- type: Airlock
|
||||
- type: ApcPowerReceiver
|
||||
needsPower: false
|
||||
- type: Physics
|
||||
bodyType: Static
|
||||
- type: Fixtures
|
||||
@@ -54,7 +56,6 @@ namespace Content.IntegrationTests.Tests.Doors
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var doors = entityManager.EntitySysManager.GetEntitySystem<DoorSystem>();
|
||||
|
||||
|
||||
139
Content.IntegrationTests/Tests/Guidebook/DocumentParsingTest.cs
Normal file
139
Content.IntegrationTests/Tests/Guidebook/DocumentParsingTest.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Client.Guidebook;
|
||||
using Content.Client.Guidebook.Richtext;
|
||||
using NUnit.Framework;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Guidebook;
|
||||
|
||||
/// <summary>
|
||||
/// This test checks that an example document string properly gets parsed by the <see cref="DocumentParsingManager"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[TestOf(typeof(DocumentParsingManager))]
|
||||
public sealed class DocumentParsingTest
|
||||
{
|
||||
|
||||
public string TestDocument = @"multiple
|
||||
lines
|
||||
separated by
|
||||
only single newlines
|
||||
make a single rich text control
|
||||
|
||||
unless there is a double newline. Also
|
||||
whitespace before newlines are ignored.
|
||||
|
||||
<TestControl/>
|
||||
|
||||
< TestControl />
|
||||
|
||||
<TestControl>
|
||||
some text with a nested control
|
||||
<TestControl/>
|
||||
</TestControl>
|
||||
|
||||
<TestControl key1=""value1"" key2=""value2 with spaces"" key3=""value3 with a
|
||||
newline""/>
|
||||
|
||||
<TestControl >
|
||||
<TestControl k=""<\>\\>=\""=<-_?*3.0//"">
|
||||
</TestControl>
|
||||
</TestControl>";
|
||||
|
||||
[Test]
|
||||
public async Task ParseTestDocument()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient();
|
||||
var client = pairTracker.Pair.Client;
|
||||
await client.WaitIdleAsync();
|
||||
var parser = client.ResolveDependency<DocumentParsingManager>();
|
||||
|
||||
Control ctrl = default!;
|
||||
await client.WaitPost(() =>
|
||||
{
|
||||
ctrl = new Control();
|
||||
Assert.That(parser.TryAddMarkup(ctrl, TestDocument));
|
||||
});
|
||||
|
||||
Assert.That(ctrl.ChildCount, Is.EqualTo(7));
|
||||
|
||||
var richText1 = ctrl.GetChild(0) as RichTextLabel;
|
||||
var richText2 = ctrl.GetChild(1) as RichTextLabel;
|
||||
|
||||
Assert.NotNull(richText1);
|
||||
Assert.NotNull(richText2);
|
||||
|
||||
// uhh.. WTF. rich text has no means of getting the contents!?!?
|
||||
// TODO assert text content is correct after fixing that bullshit.
|
||||
//Assert.That(richText1?.Text, Is.EqualTo("multiple lines separated by only single newlines make a single rich text control"));
|
||||
// Assert.That(richText2?.Text, Is.EqualTo("unless there is a double newline. Also whitespace before newlines are ignored."));
|
||||
|
||||
var test1 = ctrl.GetChild(2) as TestControl;
|
||||
var test2 = ctrl.GetChild(3) as TestControl;
|
||||
var test3 = ctrl.GetChild(4) as TestControl;
|
||||
var test4 = ctrl.GetChild(5) as TestControl;
|
||||
var test5 = ctrl.GetChild(6) as TestControl;
|
||||
|
||||
Assert.NotNull(test1);
|
||||
Assert.NotNull(test2);
|
||||
Assert.NotNull(test3);
|
||||
Assert.NotNull(test4);
|
||||
Assert.NotNull(test5);
|
||||
|
||||
Assert.That(test1?.ChildCount, Is.EqualTo(0));
|
||||
Assert.That(test2?.ChildCount, Is.EqualTo(0));
|
||||
Assert.That(test3?.ChildCount, Is.EqualTo(2));
|
||||
Assert.That(test4?.ChildCount, Is.EqualTo(0));
|
||||
Assert.That(test5?.ChildCount, Is.EqualTo(1));
|
||||
|
||||
var subText = test3.GetChild(0) as RichTextLabel;
|
||||
var subTest = test3.GetChild(1) as TestControl;
|
||||
Assert.NotNull(subText);
|
||||
//Assert.That(subText?.Text, Is.EqualTo("some text with a nested control"));
|
||||
Assert.NotNull(subTest);
|
||||
Assert.That(subTest?.ChildCount, Is.EqualTo(0));
|
||||
|
||||
var subTest2 = test5.GetChild(0) as TestControl;
|
||||
Assert.NotNull(subTest2);
|
||||
Assert.That(subTest2?.ChildCount, Is.EqualTo(0));
|
||||
|
||||
Assert.That(test1?.Params?.Count, Is.EqualTo(0));
|
||||
Assert.That(test2?.Params?.Count, Is.EqualTo(0));
|
||||
Assert.That(test3?.Params?.Count, Is.EqualTo(0));
|
||||
Assert.That(test4?.Params?.Count, Is.EqualTo(3));
|
||||
Assert.That(test5?.Params?.Count, Is.EqualTo(0));
|
||||
Assert.That(subTest2?.Params?.Count, Is.EqualTo(1));
|
||||
|
||||
string? val = null;
|
||||
test4?.Params?.TryGetValue("key1", out val);
|
||||
Assert.That(val, Is.EqualTo("value1"));
|
||||
|
||||
test4?.Params?.TryGetValue("key2", out val);
|
||||
Assert.That(val, Is.EqualTo("value2 with spaces"));
|
||||
|
||||
test4?.Params?.TryGetValue("key3", out val);
|
||||
Assert.That(val, Is.EqualTo(@"value3 with a
|
||||
newline"));
|
||||
|
||||
subTest2?.Params?.TryGetValue("k", out val);
|
||||
Assert.That(val, Is.EqualTo(@"<>\>=""=<-_?*3.0//"));
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
public sealed class TestControl : Control, IDocumentTag
|
||||
{
|
||||
public Dictionary<string, string> Params = default!;
|
||||
|
||||
public bool TryParseTag(Dictionary<string, string> param, [NotNullWhen(true)] out Control control)
|
||||
{
|
||||
Params = param;
|
||||
control = this;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Content.Client.Guidebook;
|
||||
using Content.Client.Guidebook.Richtext;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Guidebook;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(typeof(GuidebookSystem))]
|
||||
[TestOf(typeof(GuideEntryPrototype))]
|
||||
[TestOf(typeof(DocumentParsingManager))]
|
||||
public sealed class GuideEntryPrototypeTests
|
||||
{
|
||||
[Test]
|
||||
public async Task ValidatePrototypeContents()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient();
|
||||
var client = pairTracker.Pair.Client;
|
||||
await client.WaitIdleAsync();
|
||||
var protoMan = client.ResolveDependency<IPrototypeManager>();
|
||||
var resMan = client.ResolveDependency<IResourceManager>();
|
||||
var parser = client.ResolveDependency<DocumentParsingManager>();
|
||||
var prototypes = protoMan.EnumeratePrototypes<GuideEntryPrototype>().ToList();
|
||||
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var proto in prototypes)
|
||||
{
|
||||
var text = resMan.ContentFileReadText(proto.Text).ReadToEnd();
|
||||
Assert.That(parser.TryAddMarkup(new Document(), text), $"Failed to parse guidebook: {proto.Id}");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Construction.Components;
|
||||
using Content.Shared.Construction.Components;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.Construction.Components;
|
||||
|
||||
namespace Content.IntegrationTests.Tests;
|
||||
|
||||
@@ -148,7 +149,7 @@ public sealed class MaterialArbitrageTest
|
||||
spawnedOnDestroy.Add(proto.ID, (spawnedEnts, spawnedMats));
|
||||
}
|
||||
|
||||
// This is the main loop where we actually check for destruction arbitrage
|
||||
// This is the main loop where we actually check for destruction arbitrage
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
foreach (var (id, (spawnedEnts, spawnedMats)) in spawnedOnDestroy)
|
||||
@@ -183,7 +184,7 @@ public sealed class MaterialArbitrageTest
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Finally, lets also check for deconstruction arbitrage.
|
||||
// Get ingredients returned when deconstructing an entity
|
||||
Dictionary<string, Dictionary<string, int>> deconstructionMaterials = new();
|
||||
|
||||
@@ -13,6 +13,11 @@ public sealed class AccessWireAction : BaseWireAction
|
||||
|
||||
[DataField("name")]
|
||||
private string _text = "ACC";
|
||||
protected override string Text
|
||||
{
|
||||
get => _text;
|
||||
set => _text = value;
|
||||
}
|
||||
|
||||
[DataField("pulseTimeout")]
|
||||
private int _pulseTimeout = 30;
|
||||
@@ -39,6 +44,7 @@ public sealed class AccessWireAction : BaseWireAction
|
||||
|
||||
public override bool Cut(EntityUid user, Wire wire)
|
||||
{
|
||||
base.Cut(user, wire);
|
||||
if (EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var access))
|
||||
{
|
||||
WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key);
|
||||
@@ -50,6 +56,7 @@ public sealed class AccessWireAction : BaseWireAction
|
||||
|
||||
public override bool Mend(EntityUid user, Wire wire)
|
||||
{
|
||||
base.Mend(user, wire);
|
||||
if (EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var access))
|
||||
{
|
||||
access.Enabled = true;
|
||||
@@ -60,6 +67,7 @@ public sealed class AccessWireAction : BaseWireAction
|
||||
|
||||
public override bool Pulse(EntityUid user, Wire wire)
|
||||
{
|
||||
base.Pulse(user, wire);
|
||||
if (EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var access))
|
||||
{
|
||||
access.Enabled = false;
|
||||
|
||||
@@ -23,6 +23,7 @@ using Content.Shared.Construction.Components;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Stacks;
|
||||
@@ -72,7 +73,7 @@ public sealed partial class AdminVerbSystem
|
||||
: "/Textures/Interface/AdminActions/bolt.png",
|
||||
Act = () =>
|
||||
{
|
||||
airlock.SetBoltsWithAudio(!airlock.BoltsDown);
|
||||
_airlockSystem.SetBoltsWithAudio(args.Target, airlock, !airlock.BoltsDown);
|
||||
},
|
||||
Impact = LogImpact.Medium,
|
||||
Message = Loc.GetString(airlock.BoltsDown
|
||||
@@ -90,7 +91,7 @@ public sealed partial class AdminVerbSystem
|
||||
IconTexture = "/Textures/Interface/AdminActions/emergency_access.png",
|
||||
Act = () =>
|
||||
{
|
||||
_airlockSystem.ToggleEmergencyAccess(airlock);
|
||||
_airlockSystem.ToggleEmergencyAccess(args.Target, airlock);
|
||||
},
|
||||
Impact = LogImpact.Medium,
|
||||
Message = Loc.GetString(airlock.EmergencyAccess
|
||||
|
||||
114
Content.Server/Anomaly/AnomalySystem.Generator.cs
Normal file
114
Content.Server/Anomaly/AnomalySystem.Generator.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Materials;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.Server.Anomaly;
|
||||
|
||||
/// <summary>
|
||||
/// This handles anomalous vessel as well as
|
||||
/// the calculations for how many points they
|
||||
/// should produce.
|
||||
/// </summary>
|
||||
public sealed partial class AnomalySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// A multiplier applied to the grid bounds
|
||||
/// to make the likelihood of it spawning outside
|
||||
/// of the main station less likely.
|
||||
///
|
||||
/// tl;dr anomalies only generate on the inner __% of the station.
|
||||
/// </summary>
|
||||
public const float GridBoundsMultiplier = 0.6f;
|
||||
|
||||
private void InitializeGenerator()
|
||||
{
|
||||
SubscribeLocalEvent<AnomalyGeneratorComponent, BoundUIOpenedEvent>(OnGeneratorBUIOpened);
|
||||
SubscribeLocalEvent<AnomalyGeneratorComponent, MaterialAmountChangedEvent>(OnGeneratorMaterialAmountChanged);
|
||||
SubscribeLocalEvent<AnomalyGeneratorComponent, AnomalyGeneratorGenerateButtonPressedEvent>(OnGenerateButtonPressed);
|
||||
SubscribeLocalEvent<AnomalyGeneratorComponent, PowerChangedEvent>(OnGeneratorPowerChanged);
|
||||
}
|
||||
|
||||
private void OnGeneratorPowerChanged(EntityUid uid, AnomalyGeneratorComponent component, ref PowerChangedEvent args)
|
||||
{
|
||||
_ambient.SetAmbience(uid, args.Powered);
|
||||
}
|
||||
|
||||
private void OnGeneratorBUIOpened(EntityUid uid, AnomalyGeneratorComponent component, BoundUIOpenedEvent args)
|
||||
{
|
||||
UpdateGeneratorUi(uid, component);
|
||||
}
|
||||
|
||||
private void OnGeneratorMaterialAmountChanged(EntityUid uid, AnomalyGeneratorComponent component, ref MaterialAmountChangedEvent args)
|
||||
{
|
||||
UpdateGeneratorUi(uid, component);
|
||||
}
|
||||
|
||||
private void OnGenerateButtonPressed(EntityUid uid, AnomalyGeneratorComponent component, AnomalyGeneratorGenerateButtonPressedEvent args)
|
||||
{
|
||||
TryGeneratorCreateAnomaly(uid, component);
|
||||
}
|
||||
|
||||
public void UpdateGeneratorUi(EntityUid uid, AnomalyGeneratorComponent component)
|
||||
{
|
||||
var materialAmount = _material.GetMaterialAmount(uid, component.RequiredMaterial);
|
||||
|
||||
var state = new AnomalyGeneratorUserInterfaceState(component.CooldownEndTime, materialAmount, component.MaterialPerAnomaly);
|
||||
_ui.TrySetUiState(uid, AnomalyGeneratorUiKey.Key, state);
|
||||
}
|
||||
|
||||
public void TryGeneratorCreateAnomaly(EntityUid uid, AnomalyGeneratorComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
return;
|
||||
|
||||
if (Timing.CurTime < component.CooldownEndTime)
|
||||
return;
|
||||
|
||||
var grid = Transform(uid).GridUid;
|
||||
if (grid == null)
|
||||
return;
|
||||
|
||||
if (!_material.TryChangeMaterialAmount(uid, component.RequiredMaterial, -component.MaterialPerAnomaly))
|
||||
return;
|
||||
|
||||
SpawnOnRandomGridLocation(grid.Value, component.SpawnerPrototype);
|
||||
component.CooldownEndTime = Timing.CurTime + component.CooldownLength;
|
||||
UpdateGeneratorUi(uid, component);
|
||||
}
|
||||
|
||||
private void SpawnOnRandomGridLocation(EntityUid grid, string toSpawn)
|
||||
{
|
||||
if (!TryComp<MapGridComponent>(grid, out var gridComp))
|
||||
return;
|
||||
|
||||
var xform = Transform(grid);
|
||||
|
||||
var targetCoords = xform.Coordinates;
|
||||
var (gridPos, _, gridMatrix) = _transform.GetWorldPositionRotationMatrix(xform);
|
||||
var gridBounds = gridMatrix.TransformBox(gridComp.LocalAABB);
|
||||
|
||||
for (var i = 0; i < 25; i++)
|
||||
{
|
||||
var randomX = Random.Next((int) (gridBounds.Left * GridBoundsMultiplier), (int) (gridBounds.Right * GridBoundsMultiplier));
|
||||
var randomY = Random.Next((int) (gridBounds.Bottom * GridBoundsMultiplier), (int) (gridBounds.Top * GridBoundsMultiplier));
|
||||
|
||||
var tile = new Vector2i(randomX - (int) gridPos.X, randomY - (int) gridPos.Y);
|
||||
if (_atmosphere.IsTileSpace(grid, Transform(grid).MapUid, tile,
|
||||
mapGridComp: gridComp) || _atmosphere.IsTileAirBlocked(grid, tile, mapGridComp: gridComp))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
targetCoords = gridComp.GridTileToLocal(tile);
|
||||
break;
|
||||
}
|
||||
|
||||
Spawn(toSpawn, targetCoords);
|
||||
}
|
||||
}
|
||||
169
Content.Server/Anomaly/AnomalySystem.Scanner.cs
Normal file
169
Content.Server/Anomaly/AnomalySystem.Scanner.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Anomaly;
|
||||
|
||||
/// <summary>
|
||||
/// This handles the anomaly scanner and it's UI updates.
|
||||
/// </summary>
|
||||
public sealed partial class AnomalySystem
|
||||
{
|
||||
private void InitializeScanner()
|
||||
{
|
||||
SubscribeLocalEvent<AnomalyScannerComponent, BoundUIOpenedEvent>(OnScannerUiOpened);
|
||||
SubscribeLocalEvent<AnomalyScannerComponent, AfterInteractEvent>(OnScannerAfterInteract);
|
||||
SubscribeLocalEvent<AnomalyScannerComponent, AnomalyScanFinishedEvent>(OnScannerDoAfterFinished);
|
||||
SubscribeLocalEvent<AnomalyScannerComponent, AnomalyScanCancelledEvent>(OnScannerDoAfterCancelled);
|
||||
|
||||
SubscribeLocalEvent<AnomalyShutdownEvent>(OnScannerAnomalyShutdown);
|
||||
SubscribeLocalEvent<AnomalySeverityChangedEvent>(OnScannerAnomalySeverityChanged);
|
||||
SubscribeLocalEvent<AnomalyStabilityChangedEvent>(OnScannerAnomalyStabilityChanged);
|
||||
SubscribeLocalEvent<AnomalyHealthChangedEvent>(OnScannerAnomalyHealthChanged);
|
||||
}
|
||||
|
||||
private void OnScannerAnomalyShutdown(ref AnomalyShutdownEvent args)
|
||||
{
|
||||
foreach (var component in EntityQuery<AnomalyScannerComponent>())
|
||||
{
|
||||
if (component.ScannedAnomaly != args.Anomaly)
|
||||
continue;
|
||||
_ui.TryCloseAll(component.Owner, AnomalyScannerUiKey.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScannerAnomalySeverityChanged(ref AnomalySeverityChangedEvent args)
|
||||
{
|
||||
foreach (var component in EntityQuery<AnomalyScannerComponent>())
|
||||
{
|
||||
if (component.ScannedAnomaly != args.Anomaly)
|
||||
continue;
|
||||
UpdateScannerUi(component.Owner, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScannerAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args)
|
||||
{
|
||||
foreach (var component in EntityQuery<AnomalyScannerComponent>())
|
||||
{
|
||||
if (component.ScannedAnomaly != args.Anomaly)
|
||||
continue;
|
||||
UpdateScannerUi(component.Owner, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScannerAnomalyHealthChanged(ref AnomalyHealthChangedEvent args)
|
||||
{
|
||||
foreach (var component in EntityQuery<AnomalyScannerComponent>())
|
||||
{
|
||||
if (component.ScannedAnomaly != args.Anomaly)
|
||||
continue;
|
||||
UpdateScannerUi(component.Owner, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScannerUiOpened(EntityUid uid, AnomalyScannerComponent component, BoundUIOpenedEvent args)
|
||||
{
|
||||
UpdateScannerUi(uid, component);
|
||||
}
|
||||
|
||||
private void OnScannerAfterInteract(EntityUid uid, AnomalyScannerComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (component.TokenSource != null)
|
||||
return;
|
||||
|
||||
if (args.Target is not { } target)
|
||||
return;
|
||||
if (!HasComp<AnomalyComponent>(target))
|
||||
return;
|
||||
|
||||
component.TokenSource = new();
|
||||
_doAfter.DoAfter(new DoAfterEventArgs(args.User, component.ScanDoAfterDuration, component.TokenSource.Token, target, uid)
|
||||
{
|
||||
DistanceThreshold = 2f,
|
||||
UsedFinishedEvent = new AnomalyScanFinishedEvent(target, args.User),
|
||||
UsedCancelledEvent = new AnomalyScanCancelledEvent()
|
||||
});
|
||||
}
|
||||
|
||||
private void OnScannerDoAfterFinished(EntityUid uid, AnomalyScannerComponent component, AnomalyScanFinishedEvent args)
|
||||
{
|
||||
component.TokenSource = null;
|
||||
|
||||
Audio.PlayPvs(component.CompleteSound, uid);
|
||||
_popup.PopupEntity(Loc.GetString("anomaly-scanner-component-scan-complete"), uid);
|
||||
UpdateScannerWithNewAnomaly(uid, args.Anomaly, component);
|
||||
|
||||
if (TryComp<ActorComponent>(args.User, out var actor))
|
||||
_ui.TryOpen(uid, AnomalyScannerUiKey.Key, actor.PlayerSession);
|
||||
}
|
||||
|
||||
private void OnScannerDoAfterCancelled(EntityUid uid, AnomalyScannerComponent component, AnomalyScanCancelledEvent args)
|
||||
{
|
||||
component.TokenSource = null;
|
||||
}
|
||||
|
||||
public void UpdateScannerUi(EntityUid uid, AnomalyScannerComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
TimeSpan? nextPulse = null;
|
||||
if (TryComp<AnomalyComponent>(component.ScannedAnomaly, out var anomalyComponent))
|
||||
nextPulse = anomalyComponent.NextPulseTime;
|
||||
|
||||
var state = new AnomalyScannerUserInterfaceState(GetScannerMessage(component), nextPulse);
|
||||
_ui.TrySetUiState(uid, AnomalyScannerUiKey.Key, state);
|
||||
}
|
||||
|
||||
public void UpdateScannerWithNewAnomaly(EntityUid scanner, EntityUid anomaly, AnomalyScannerComponent? scannerComp = null, AnomalyComponent? anomalyComp = null)
|
||||
{
|
||||
if (!Resolve(scanner, ref scannerComp) || !Resolve(anomaly, ref anomalyComp))
|
||||
return;
|
||||
|
||||
scannerComp.ScannedAnomaly = anomaly;
|
||||
UpdateScannerUi(scanner, scannerComp);
|
||||
}
|
||||
|
||||
public FormattedMessage GetScannerMessage(AnomalyScannerComponent component)
|
||||
{
|
||||
var msg = new FormattedMessage();
|
||||
if (component.ScannedAnomaly is not { } anomaly || !TryComp<AnomalyComponent>(anomaly, out var anomalyComp))
|
||||
{
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-no-anomaly"));
|
||||
return msg;
|
||||
}
|
||||
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P"))));
|
||||
msg.PushNewline();
|
||||
string stateLoc;
|
||||
if (anomalyComp.Stability < anomalyComp.DecayThreshold)
|
||||
stateLoc = Loc.GetString("anomaly-scanner-stability-low");
|
||||
else if (anomalyComp.Stability > anomalyComp.GrowthThreshold)
|
||||
stateLoc = Loc.GetString("anomaly-scanner-stability-high");
|
||||
else
|
||||
stateLoc = Loc.GetString("anomaly-scanner-stability-medium");
|
||||
msg.AddMarkup(stateLoc);
|
||||
msg.PushNewline();
|
||||
|
||||
var points = GetAnomalyPointValue(anomaly, anomalyComp) / 10 * 10; //round to tens place
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-point-output", ("point", points)));
|
||||
msg.PushNewline();
|
||||
msg.PushNewline();
|
||||
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-readout"));
|
||||
msg.PushNewline();
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType))));
|
||||
msg.PushNewline();
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType))));
|
||||
msg.PushNewline();
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType))));
|
||||
|
||||
//The timer at the end here is actually added in the ui itself.
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
129
Content.Server/Anomaly/AnomalySystem.Vessel.cs
Normal file
129
Content.Server/Anomaly/AnomalySystem.Vessel.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Server.Construction;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Research.Components;
|
||||
|
||||
namespace Content.Server.Anomaly;
|
||||
|
||||
/// <summary>
|
||||
/// This handles anomalous vessel as well as
|
||||
/// the calculations for how many points they
|
||||
/// should produce.
|
||||
/// </summary>
|
||||
public sealed partial class AnomalySystem
|
||||
{
|
||||
private void InitializeVessel()
|
||||
{
|
||||
SubscribeLocalEvent<AnomalyVesselComponent, ComponentShutdown>(OnVesselShutdown);
|
||||
SubscribeLocalEvent<AnomalyVesselComponent, MapInitEvent>(OnVesselMapInit);
|
||||
SubscribeLocalEvent<AnomalyVesselComponent, RefreshPartsEvent>(OnRefreshParts);
|
||||
SubscribeLocalEvent<AnomalyVesselComponent, InteractUsingEvent>(OnVesselInteractUsing);
|
||||
SubscribeLocalEvent<AnomalyVesselComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<AnomalyVesselComponent, ResearchServerGetPointsPerSecondEvent>(OnVesselGetPointsPerSecond);
|
||||
SubscribeLocalEvent<AnomalyShutdownEvent>(OnVesselAnomalyShutdown);
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, AnomalyVesselComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange)
|
||||
return;
|
||||
|
||||
args.PushText(component.Anomaly == null
|
||||
? Loc.GetString("anomaly-vessel-component-not-assigned")
|
||||
: Loc.GetString("anomaly-vessel-component-assigned"));
|
||||
}
|
||||
|
||||
private void OnVesselShutdown(EntityUid uid, AnomalyVesselComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (component.Anomaly is not { } anomaly)
|
||||
return;
|
||||
|
||||
if (!TryComp<AnomalyComponent>(anomaly, out var anomalyComp))
|
||||
return;
|
||||
|
||||
anomalyComp.ConnectedVessel = null;
|
||||
}
|
||||
|
||||
private void OnVesselMapInit(EntityUid uid, AnomalyVesselComponent component, MapInitEvent args)
|
||||
{
|
||||
UpdateVesselAppearance(uid, component);
|
||||
}
|
||||
|
||||
private void OnRefreshParts(EntityUid uid, AnomalyVesselComponent component, RefreshPartsEvent args)
|
||||
{
|
||||
var modifierRating = args.PartRatings[component.MachinePartPointModifier] - 1;
|
||||
component.PointMultiplier = MathF.Pow(component.PartRatingPointModifier, modifierRating);
|
||||
}
|
||||
|
||||
private void OnVesselInteractUsing(EntityUid uid, AnomalyVesselComponent component, InteractUsingEvent args)
|
||||
{
|
||||
if (component.Anomaly != null ||
|
||||
!TryComp<AnomalyScannerComponent>(args.Used, out var scanner) ||
|
||||
scanner.ScannedAnomaly is not {} anomaly)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryComp<AnomalyComponent>(anomaly, out var anomalyComponent) || anomalyComponent.ConnectedVessel != null)
|
||||
return;
|
||||
|
||||
component.Anomaly = scanner.ScannedAnomaly;
|
||||
anomalyComponent.ConnectedVessel = uid;
|
||||
UpdateVesselAppearance(uid, component);
|
||||
_popup.PopupEntity(Loc.GetString("anomaly-vessel-component-anomaly-assigned"), uid);
|
||||
}
|
||||
|
||||
private void OnVesselGetPointsPerSecond(EntityUid uid, AnomalyVesselComponent component, ref ResearchServerGetPointsPerSecondEvent args)
|
||||
{
|
||||
if (!this.IsPowered(uid, EntityManager) || component.Anomaly is not {} anomaly)
|
||||
{
|
||||
args.Points = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
args.Points += (int) (GetAnomalyPointValue(anomaly) * component.PointMultiplier);
|
||||
}
|
||||
|
||||
private void OnVesselAnomalyShutdown(ref AnomalyShutdownEvent args)
|
||||
{
|
||||
foreach (var component in EntityQuery<AnomalyVesselComponent>())
|
||||
{
|
||||
var ent = component.Owner;
|
||||
|
||||
if (args.Anomaly != component.Anomaly)
|
||||
continue;
|
||||
|
||||
component.Anomaly = null;
|
||||
UpdateVesselAppearance(ent, component);
|
||||
|
||||
if (!args.Supercritical)
|
||||
continue;
|
||||
_explosion.TriggerExplosive(ent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the appearance of an anomaly vessel
|
||||
/// based on whether or not it has an anomaly
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="component"></param>
|
||||
public void UpdateVesselAppearance(EntityUid uid, AnomalyVesselComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
var on = component.Anomaly != null;
|
||||
|
||||
Appearance.SetData(uid, AnomalyVesselVisuals.HasAnomaly, on);
|
||||
if (TryComp<SharedPointLightComponent>(uid, out var pointLightComponent))
|
||||
{
|
||||
pointLightComponent.Enabled = on;
|
||||
}
|
||||
_ambient.SetAmbience(uid, on);
|
||||
}
|
||||
}
|
||||
127
Content.Server/Anomaly/AnomalySystem.cs
Normal file
127
Content.Server/Anomaly/AnomalySystem.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Audio;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Server.Materials;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Random;
|
||||
namespace Content.Server.Anomaly;
|
||||
|
||||
/// <summary>
|
||||
/// This handles logic and interactions relating to <see cref="AnomalyComponent"/>
|
||||
/// </summary>
|
||||
public sealed partial class AnomalySystem : SharedAnomalySystem
|
||||
{
|
||||
[Dependency] private readonly AmbientSoundSystem _ambient = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly ExplosionSystem _explosion = default!;
|
||||
[Dependency] private readonly MaterialStorageSystem _material = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
public const float MinParticleVariation = 0.8f;
|
||||
public const float MaxParticleVariation = 1.2f;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<AnomalyComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<AnomalyComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<AnomalyComponent, StartCollideEvent>(OnStartCollide);
|
||||
|
||||
InitializeGenerator();
|
||||
InitializeScanner();
|
||||
InitializeVessel();
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, AnomalyComponent component, MapInitEvent args)
|
||||
{
|
||||
component.NextPulseTime = Timing.CurTime + GetPulseLength(component) * 3; // longer the first time
|
||||
ChangeAnomalyStability(uid, Random.NextFloat(component.InitialStabilityRange.Item1 , component.InitialStabilityRange.Item2), component);
|
||||
ChangeAnomalySeverity(uid, Random.NextFloat(component.InitialSeverityRange.Item1, component.InitialSeverityRange.Item2), component);
|
||||
|
||||
var particles = new List<AnomalousParticleType>
|
||||
{ AnomalousParticleType.Delta, AnomalousParticleType.Epsilon, AnomalousParticleType.Zeta };
|
||||
component.SeverityParticleType = Random.PickAndTake(particles);
|
||||
component.DestabilizingParticleType = Random.PickAndTake(particles);
|
||||
component.WeakeningParticleType = Random.PickAndTake(particles);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, AnomalyComponent component, ComponentShutdown args)
|
||||
{
|
||||
EndAnomaly(uid, component);
|
||||
}
|
||||
|
||||
private void OnStartCollide(EntityUid uid, AnomalyComponent component, ref StartCollideEvent args)
|
||||
{
|
||||
if (!TryComp<AnomalousParticleComponent>(args.OtherFixture.Body.Owner, out var particleComponent))
|
||||
return;
|
||||
|
||||
if (args.OtherFixture.ID != particleComponent.FixtureId)
|
||||
return;
|
||||
|
||||
// small function to randomize because it's easier to read like this
|
||||
float VaryValue(float v) => v * Random.NextFloat(MinParticleVariation, MaxParticleVariation);
|
||||
|
||||
if (particleComponent.ParticleType == component.DestabilizingParticleType)
|
||||
{
|
||||
ChangeAnomalyStability(uid, VaryValue(component.StabilityPerDestabilizingHit), component);
|
||||
}
|
||||
else if (particleComponent.ParticleType == component.SeverityParticleType)
|
||||
{
|
||||
ChangeAnomalySeverity(uid, VaryValue(component.SeverityPerSeverityHit), component);
|
||||
}
|
||||
else if (particleComponent.ParticleType == component.WeakeningParticleType)
|
||||
{
|
||||
ChangeAnomalyHealth(uid, VaryValue(component.HealthPerWeakeningeHit), component);
|
||||
ChangeAnomalyStability(uid, VaryValue(component.StabilityPerWeakeningeHit), component);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of research points generated per second for an anomaly.
|
||||
/// </summary>
|
||||
/// <param name="anomaly"></param>
|
||||
/// <param name="component"></param>
|
||||
/// <returns>The amount of points</returns>
|
||||
public int GetAnomalyPointValue(EntityUid anomaly, AnomalyComponent? component = null)
|
||||
{
|
||||
if (!Resolve(anomaly, ref component, false))
|
||||
return 0;
|
||||
|
||||
var multiplier = 1f;
|
||||
if (component.Stability > component.GrowthThreshold)
|
||||
multiplier = 1.25f; //more points for unstable
|
||||
else if (component.Stability < component.DecayThreshold)
|
||||
multiplier = 0.75f; //less points if it's dying
|
||||
|
||||
//penalty of up to 50% based on health
|
||||
multiplier *= MathF.Pow(1.5f, component.Health) - 0.5f;
|
||||
|
||||
return (int) ((component.MaxPointsPerSecond - component.MinPointsPerSecond) * component.Severity * multiplier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized name of a particle.
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public string GetParticleLocale(AnomalousParticleType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
AnomalousParticleType.Delta => Loc.GetString("anomaly-particles-delta"),
|
||||
AnomalousParticleType.Epsilon => Loc.GetString("anomaly-particles-epsilon"),
|
||||
AnomalousParticleType.Zeta => Loc.GetString("anomaly-particles-zeta"),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Content.Shared.Anomaly;
|
||||
|
||||
namespace Content.Server.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for projectiles which affect anomalies through colliding with them.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class AnomalousParticleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of particle that the projectile
|
||||
/// imbues onto the anomaly on contact.
|
||||
/// </summary>
|
||||
[DataField("particleType", required: true)]
|
||||
public AnomalousParticleType ParticleType;
|
||||
|
||||
/// <summary>
|
||||
/// The fixture that's checked on collision.
|
||||
/// </summary>
|
||||
[DataField("fixtureId")]
|
||||
public string FixtureId = "projectile";
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Content.Shared.Materials;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for a machine that is able to generate
|
||||
/// anomalies randomly on the station.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class AnomalyGeneratorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The time at which the cooldown for generating another anomaly will be over
|
||||
/// </summary>
|
||||
[DataField("cooldownEndTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan CooldownEndTime = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The cooldown between generating anomalies.
|
||||
/// </summary>
|
||||
[DataField("cooldownLength"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan CooldownLength = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// The material needed to generate an anomaly
|
||||
/// </summary>
|
||||
[DataField("requiredMaterial", customTypeSerializer: typeof(PrototypeIdSerializer<MaterialPrototype>)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public string RequiredMaterial = "Plasma";
|
||||
|
||||
/// <summary>
|
||||
/// The amount of material needed to generate a single anomaly
|
||||
/// </summary>
|
||||
[DataField("materialPerAnomaly"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public int MaterialPerAnomaly = 1500; // a bit less than a stack of plasma
|
||||
|
||||
/// <summary>
|
||||
/// The random anomaly spawner entity
|
||||
/// </summary>
|
||||
[DataField("spawnerPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public string SpawnerPrototype = "RandomAnomalySpawner";
|
||||
}
|
||||
49
Content.Server/Anomaly/Components/AnomalyScannerComponent.cs
Normal file
49
Content.Server/Anomaly/Components/AnomalyScannerComponent.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.Threading;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for scanning anomalies and
|
||||
/// displaying information about them in the ui
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class AnomalyScannerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The anomaly that was last scanned by this scanner.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public EntityUid? ScannedAnomaly;
|
||||
|
||||
/// <summary>
|
||||
/// How long the scan takes
|
||||
/// </summary>
|
||||
[DataField("scanDoAfterDuration")]
|
||||
public float ScanDoAfterDuration = 5;
|
||||
|
||||
public CancellationTokenSource? TokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// The sound plays when the scan finished
|
||||
/// </summary>
|
||||
[DataField("completeSound")]
|
||||
public SoundSpecifier? CompleteSound = new SoundPathSpecifier("/Audio/Items/beep.ogg");
|
||||
}
|
||||
|
||||
public sealed class AnomalyScanFinishedEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Anomaly;
|
||||
|
||||
public EntityUid User;
|
||||
|
||||
public AnomalyScanFinishedEvent(EntityUid anomaly, EntityUid user)
|
||||
{
|
||||
Anomaly = anomaly;
|
||||
User = user;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AnomalyScanCancelledEvent : EntityEventArgs
|
||||
{
|
||||
}
|
||||
40
Content.Server/Anomaly/Components/AnomalyVesselComponent.cs
Normal file
40
Content.Server/Anomaly/Components/AnomalyVesselComponent.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Anomaly Vessels can have an anomaly "stored" in them
|
||||
/// by interacting on them with an anomaly scanner. Then,
|
||||
/// they generate points for the selected server based on
|
||||
/// the anomaly's stability and severity.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class AnomalyVesselComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The anomaly that the vessel is storing.
|
||||
/// Can be null.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public EntityUid? Anomaly;
|
||||
|
||||
/// <summary>
|
||||
/// A multiplier applied to the amount of points generated.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float PointMultiplier = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The machine part that affects the point multiplier of the vessel
|
||||
/// </summary>
|
||||
[DataField("machinePartPointModifier", customTypeSerializer: typeof(PrototypeIdSerializer<MachinePartPrototype>))]
|
||||
public string MachinePartPointModifier = "ScanningModule";
|
||||
|
||||
/// <summary>
|
||||
/// A value used to scale the point multiplier
|
||||
/// with the corresponding part rating.
|
||||
/// </summary>
|
||||
[DataField("partRatingPointModifier")]
|
||||
public float PartRatingPointModifier = 1.5f;
|
||||
}
|
||||
61
Content.Server/Anomaly/Effects/ElectricityAnomalySystem.cs
Normal file
61
Content.Server/Anomaly/Effects/ElectricityAnomalySystem.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Content.Server.Electrocution;
|
||||
using Content.Server.Lightning;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Anomaly.Effects.Components;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Anomaly.Effects;
|
||||
|
||||
public sealed class ElectricityAnomalySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly LightningSystem _lightning = default!;
|
||||
[Dependency] private readonly ElectrocutionSystem _electrocution = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<ElectricityAnomalyComponent, AnomalyPulseEvent>(OnPulse);
|
||||
SubscribeLocalEvent<ElectricityAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical);
|
||||
}
|
||||
|
||||
private void OnPulse(EntityUid uid, ElectricityAnomalyComponent component, ref AnomalyPulseEvent args)
|
||||
{
|
||||
var range = component.MaxElectrocuteRange * args.Stabiltiy;
|
||||
var damage = (int) (component.MaxElectrocuteDamage * args.Severity);
|
||||
var duration = component.MaxElectrocuteDuration * args.Severity;
|
||||
|
||||
var xform = Transform(uid);
|
||||
foreach (var comp in _lookup.GetComponentsInRange<StatusEffectsComponent>(xform.MapPosition, range))
|
||||
{
|
||||
var ent = comp.Owner;
|
||||
|
||||
_electrocution.TryDoElectrocution(ent, uid, damage, duration, true, statusEffects: comp, ignoreInsulation: true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSupercritical(EntityUid uid, ElectricityAnomalyComponent component, ref AnomalySupercriticalEvent args)
|
||||
{
|
||||
var poweredQuery = GetEntityQuery<ApcPowerReceiverComponent>();
|
||||
var mobQuery = GetEntityQuery<MobThresholdsComponent>();
|
||||
var validEnts = new HashSet<EntityUid>();
|
||||
foreach (var ent in _lookup.GetEntitiesInRange(uid, component.MaxElectrocuteRange * 2))
|
||||
{
|
||||
if (mobQuery.HasComponent(ent))
|
||||
validEnts.Add(ent);
|
||||
|
||||
if (_random.Prob(0.1f) && poweredQuery.HasComponent(ent))
|
||||
validEnts.Add(ent);
|
||||
}
|
||||
|
||||
// goodbye, sweet perf
|
||||
foreach (var ent in validEnts)
|
||||
{
|
||||
_lightning.ShootLightning(uid, ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Content.Server/Anomaly/Effects/GravityAnomalySystem.cs
Normal file
39
Content.Server/Anomaly/Effects/GravityAnomalySystem.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Content.Server.Singularity.Components;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Anomaly.Effects;
|
||||
using Content.Shared.Anomaly.Effects.Components;
|
||||
using Content.Shared.Radiation.Components;
|
||||
|
||||
namespace Content.Server.Anomaly.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// This handles logic and events relating to <see cref="GravityAnomalyComponent"/> and <seealso cref="AnomalySystem"/>
|
||||
/// </summary>
|
||||
public sealed class GravityAnomalySystem : SharedGravityAnomalySystem
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<GravityAnomalyComponent, AnomalySeverityChangedEvent>(OnSeverityChanged);
|
||||
SubscribeLocalEvent<GravityAnomalyComponent, AnomalyStabilityChangedEvent>(OnStabilityChanged);
|
||||
}
|
||||
|
||||
private void OnSeverityChanged(EntityUid uid, GravityAnomalyComponent component, ref AnomalySeverityChangedEvent args)
|
||||
{
|
||||
if (TryComp<RadiationSourceComponent>(uid, out var radSource))
|
||||
radSource.Intensity = component.MaxRadiationIntensity * args.Severity;
|
||||
|
||||
if (!TryComp<GravityWellComponent>(uid, out var gravityWell))
|
||||
return;
|
||||
var accel = (component.MaxAccel - component.MinAccel) * args.Severity + component.MinAccel;
|
||||
gravityWell.BaseRadialAcceleration = accel;
|
||||
gravityWell.BaseTangentialAcceleration = accel * 0.2f;
|
||||
}
|
||||
|
||||
private void OnStabilityChanged(EntityUid uid, GravityAnomalyComponent component, ref AnomalyStabilityChangedEvent args)
|
||||
{
|
||||
if (TryComp<GravityWellComponent>(uid, out var gravityWell))
|
||||
gravityWell.MaxRange = component.MaxGravityWellRange * args.Stability;
|
||||
}
|
||||
}
|
||||
100
Content.Server/Anomaly/Effects/PyroclasticAnomalySystem.cs
Normal file
100
Content.Server/Anomaly/Effects/PyroclasticAnomalySystem.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Interaction;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Anomaly.Effects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Anomaly.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// This handles <see cref="PyroclasticAnomalyComponent"/> and the events from <seealso cref="AnomalySystem"/>
|
||||
/// </summary>
|
||||
public sealed class PyroclasticAnomalySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly FlammableSystem _flammable = default!;
|
||||
[Dependency] private readonly InteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly TransformSystem _xform = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<PyroclasticAnomalyComponent, AnomalyPulseEvent>(OnPulse);
|
||||
SubscribeLocalEvent<PyroclasticAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical);
|
||||
}
|
||||
|
||||
private void OnPulse(EntityUid uid, PyroclasticAnomalyComponent component, ref AnomalyPulseEvent args)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
var ignitionRadius = component.MaximumIgnitionRadius * args.Stabiltiy;
|
||||
IgniteNearby(xform.Coordinates, args.Severity, ignitionRadius);
|
||||
}
|
||||
|
||||
private void OnSupercritical(EntityUid uid, PyroclasticAnomalyComponent component, ref AnomalySupercriticalEvent args)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
var grid = xform.GridUid;
|
||||
var map = xform.MapUid;
|
||||
|
||||
var indices = _xform.GetGridOrMapTilePosition(uid, xform);
|
||||
var mixture = _atmosphere.GetTileMixture(grid, map, indices, true);
|
||||
|
||||
if (mixture == null)
|
||||
return;
|
||||
mixture.AdjustMoles(component.SupercriticalGas, component.SupercriticalMoleAmount);
|
||||
if (grid is { })
|
||||
{
|
||||
foreach (var ind in _atmosphere.GetAdjacentTiles(grid.Value, indices))
|
||||
{
|
||||
var mix = _atmosphere.GetTileMixture(grid, map, indices, true);
|
||||
if (mix is not {})
|
||||
continue;
|
||||
mix.AdjustMoles(component.SupercriticalGas, component.SupercriticalMoleAmount);
|
||||
mix.Temperature += component.HotspotExposeTemperature;
|
||||
_atmosphere.HotspotExpose(grid.Value, indices, component.HotspotExposeTemperature, mix?.Volume ?? component.SupercriticalMoleAmount, true);
|
||||
}
|
||||
}
|
||||
IgniteNearby(xform.Coordinates, 1, component.MaximumIgnitionRadius * 2);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
foreach (var (pyro, anom, xform) in EntityQuery<PyroclasticAnomalyComponent, AnomalyComponent, TransformComponent>())
|
||||
{
|
||||
var ent = pyro.Owner;
|
||||
|
||||
var grid = xform.GridUid;
|
||||
var map = xform.MapUid;
|
||||
var indices = _xform.GetGridOrMapTilePosition(ent, xform);
|
||||
var mixture = _atmosphere.GetTileMixture(grid, map, indices, true);
|
||||
if (mixture is { })
|
||||
{
|
||||
mixture.Temperature += pyro.HeatPerSecond * anom.Severity * frameTime;
|
||||
}
|
||||
|
||||
if (grid != null && anom.Severity > pyro.AnomalyHotspotThreshold)
|
||||
{
|
||||
_atmosphere.HotspotExpose(grid.Value, indices, pyro.HotspotExposeTemperature, pyro.HotspotExposeVolume, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void IgniteNearby(EntityCoordinates coordinates, float severity, float radius)
|
||||
{
|
||||
foreach (var flammable in _lookup.GetComponentsInRange<FlammableComponent>(coordinates, radius))
|
||||
{
|
||||
var ent = flammable.Owner;
|
||||
if (!_interaction.InRangeUnobstructed(coordinates.ToMap(EntityManager), ent, -1))
|
||||
continue;
|
||||
|
||||
var stackAmount = 1 + (int) (severity / 0.25f);
|
||||
_flammable.AdjustFireStacks(ent, stackAmount, flammable);
|
||||
_flammable.Ignite(ent, flammable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,11 @@ namespace Content.Server.Arcade;
|
||||
public sealed class ArcadePlayerInvincibleWireAction : BaseToggleWireAction
|
||||
{
|
||||
private string _text = "MNGR";
|
||||
protected override string Text
|
||||
{
|
||||
get => _text;
|
||||
set => _text = value;
|
||||
}
|
||||
private Color _color = Color.Purple;
|
||||
|
||||
public override object? StatusKey { get; } = SharedSpaceVillainArcadeComponent.Indicators.HealthManager;
|
||||
|
||||
@@ -10,6 +10,11 @@ public sealed class ArcadeOverflowWireAction : BaseToggleWireAction
|
||||
{
|
||||
private Color _color = Color.Red;
|
||||
private string _text = "LMTR";
|
||||
protected override string Text
|
||||
{
|
||||
get => _text;
|
||||
set => _text = value;
|
||||
}
|
||||
|
||||
public override object? StatusKey { get; } = SharedSpaceVillainArcadeComponent.Indicators.HealthLimiter;
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ namespace Content.Server.Atmos.Monitor;
|
||||
public sealed class AirAlarmPanicWire : BaseWireAction
|
||||
{
|
||||
private string _text = "PANC";
|
||||
protected override string Text
|
||||
{
|
||||
get => _text;
|
||||
set => _text = value;
|
||||
}
|
||||
private Color _color = Color.Red;
|
||||
|
||||
private AirAlarmSystem _airAlarmSystem = default!;
|
||||
@@ -42,6 +47,7 @@ public sealed class AirAlarmPanicWire : BaseWireAction
|
||||
|
||||
public override bool Cut(EntityUid user, Wire wire)
|
||||
{
|
||||
base.Cut(user, wire);
|
||||
if (EntityManager.TryGetComponent<DeviceNetworkComponent>(wire.Owner, out var devNet))
|
||||
{
|
||||
_airAlarmSystem.SetMode(wire.Owner, devNet.Address, AirAlarmMode.Panic, false);
|
||||
@@ -52,6 +58,7 @@ public sealed class AirAlarmPanicWire : BaseWireAction
|
||||
|
||||
public override bool Mend(EntityUid user, Wire wire)
|
||||
{
|
||||
base.Mend(user, wire);
|
||||
if (EntityManager.TryGetComponent<DeviceNetworkComponent>(wire.Owner, out var devNet)
|
||||
&& EntityManager.TryGetComponent<AirAlarmComponent>(wire.Owner, out var alarm)
|
||||
&& alarm.CurrentMode == AirAlarmMode.Panic)
|
||||
@@ -65,6 +72,7 @@ public sealed class AirAlarmPanicWire : BaseWireAction
|
||||
|
||||
public override bool Pulse(EntityUid user, Wire wire)
|
||||
{
|
||||
base.Pulse(user, wire);
|
||||
if (EntityManager.TryGetComponent<DeviceNetworkComponent>(wire.Owner, out var devNet))
|
||||
{
|
||||
_airAlarmSystem.SetMode(wire.Owner, devNet.Address, AirAlarmMode.Panic, false);
|
||||
|
||||
@@ -15,6 +15,11 @@ public sealed class AtmosMonitorDeviceNetWire : BaseWireAction
|
||||
private bool _alarmOnPulse = false;
|
||||
|
||||
private string _text = "NETW";
|
||||
protected override string Text
|
||||
{
|
||||
get => _text;
|
||||
set => _text = value;
|
||||
}
|
||||
private Color _color = Color.Orange;
|
||||
|
||||
private AtmosAlarmableSystem _atmosAlarmableSystem = default!;
|
||||
@@ -52,6 +57,7 @@ public sealed class AtmosMonitorDeviceNetWire : BaseWireAction
|
||||
|
||||
public override bool Cut(EntityUid user, Wire wire)
|
||||
{
|
||||
base.Cut(user, wire);
|
||||
if (EntityManager.TryGetComponent<AtmosAlarmableComponent>(wire.Owner, out var monitor))
|
||||
{
|
||||
monitor.IgnoreAlarms = true;
|
||||
@@ -62,6 +68,7 @@ public sealed class AtmosMonitorDeviceNetWire : BaseWireAction
|
||||
|
||||
public override bool Mend(EntityUid user, Wire wire)
|
||||
{
|
||||
base.Mend(user, wire);
|
||||
if (EntityManager.TryGetComponent<AtmosAlarmableComponent>(wire.Owner, out var monitor))
|
||||
{
|
||||
monitor.IgnoreAlarms = false;
|
||||
@@ -72,6 +79,7 @@ public sealed class AtmosMonitorDeviceNetWire : BaseWireAction
|
||||
|
||||
public override bool Pulse(EntityUid user, Wire wire)
|
||||
{
|
||||
base.Pulse(user, wire);
|
||||
if (_alarmOnPulse)
|
||||
{
|
||||
_atmosAlarmableSystem.ForceAlert(wire.Owner, AtmosAlarmType.Danger);
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
using Content.Server.Beam.Components;
|
||||
using Content.Server.Lightning;
|
||||
using Content.Shared.Beam;
|
||||
using Content.Shared.Beam.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
|
||||
namespace Content.Server.Beam;
|
||||
@@ -17,8 +14,8 @@ public sealed class BeamSystem : SharedBeamSystem
|
||||
{
|
||||
[Dependency] private readonly FixtureSystem _fixture = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -79,52 +76,52 @@ public sealed class BeamSystem : SharedBeamSystem
|
||||
var shape = new EdgeShape(distanceCorrection, new Vector2(0,0));
|
||||
var distanceLength = distanceCorrection.Length;
|
||||
|
||||
if (TryComp<PhysicsComponent>(ent, out var physics) && TryComp<BeamComponent>(ent, out var beam))
|
||||
if (!TryComp<PhysicsComponent>(ent, out var physics) || !TryComp<BeamComponent>(ent, out var beam))
|
||||
return;
|
||||
FixturesComponent? manager = null;
|
||||
_fixture.TryCreateFixture(
|
||||
ent,
|
||||
shape,
|
||||
"BeamBody",
|
||||
hard: false,
|
||||
collisionMask: (int)CollisionGroup.ItemMask,
|
||||
collisionLayer: (int)CollisionGroup.MobLayer,
|
||||
manager: manager,
|
||||
body: physics);
|
||||
|
||||
_physics.SetBodyType(ent, BodyType.Dynamic, manager: manager, body: physics);
|
||||
_physics.SetCanCollide(ent, true, manager: manager, body: physics);
|
||||
_broadphase.RegenerateContacts(physics, manager);
|
||||
|
||||
var beamVisualizerEvent = new BeamVisualizerEvent(ent, distanceLength, userAngle, bodyState, shader);
|
||||
RaiseNetworkEvent(beamVisualizerEvent);
|
||||
|
||||
if (controller != null)
|
||||
beam.VirtualBeamController = controller;
|
||||
|
||||
else
|
||||
{
|
||||
FixturesComponent? manager = null;
|
||||
_fixture.TryCreateFixture(
|
||||
ent,
|
||||
shape,
|
||||
"BeamBody",
|
||||
hard: false,
|
||||
collisionMask: (int)CollisionGroup.ItemMask,
|
||||
collisionLayer: (int)CollisionGroup.MobLayer,
|
||||
manager: manager,
|
||||
body: physics);
|
||||
var controllerEnt = Spawn("VirtualBeamEntityController", beamSpawnPos);
|
||||
beam.VirtualBeamController = controllerEnt;
|
||||
|
||||
_physics.SetBodyType(ent, BodyType.Dynamic, manager: manager, body: physics);
|
||||
_physics.SetCanCollide(ent, true, manager: manager, body: physics);
|
||||
_audio.PlayPvs(beam.Sound, beam.Owner);
|
||||
|
||||
var beamVisualizerEvent = new BeamVisualizerEvent(ent, distanceLength, userAngle, bodyState, shader);
|
||||
RaiseNetworkEvent(beamVisualizerEvent);
|
||||
|
||||
if (controller != null)
|
||||
beam.VirtualBeamController = controller;
|
||||
|
||||
else
|
||||
{
|
||||
var controllerEnt = Spawn("VirtualBeamEntityController", beamSpawnPos);
|
||||
beam.VirtualBeamController = controllerEnt;
|
||||
|
||||
_audio.PlayPvs(beam.Sound, beam.Owner);
|
||||
|
||||
var beamControllerCreatedEvent = new BeamControllerCreatedEvent(ent, controllerEnt);
|
||||
RaiseLocalEvent(controllerEnt, beamControllerCreatedEvent);
|
||||
}
|
||||
|
||||
//Create the rest of the beam, sprites handled through the BeamVisualizerEvent
|
||||
for (int i = 0; i < distanceLength-1; i++)
|
||||
{
|
||||
beamSpawnPos = beamSpawnPos.Offset(calculatedDistance.Normalized);
|
||||
var newEnt = Spawn(prototype, beamSpawnPos);
|
||||
|
||||
var ev = new BeamVisualizerEvent(newEnt, distanceLength, userAngle, bodyState, shader);
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
|
||||
var beamFiredEvent = new BeamFiredEvent(ent);
|
||||
RaiseLocalEvent(beam.VirtualBeamController.Value, beamFiredEvent);
|
||||
var beamControllerCreatedEvent = new BeamControllerCreatedEvent(ent, controllerEnt);
|
||||
RaiseLocalEvent(controllerEnt, beamControllerCreatedEvent);
|
||||
}
|
||||
|
||||
//Create the rest of the beam, sprites handled through the BeamVisualizerEvent
|
||||
for (var i = 0; i < distanceLength-1; i++)
|
||||
{
|
||||
beamSpawnPos = beamSpawnPos.Offset(calculatedDistance.Normalized);
|
||||
var newEnt = Spawn(prototype, beamSpawnPos);
|
||||
|
||||
var ev = new BeamVisualizerEvent(newEnt, distanceLength, userAngle, bodyState, shader);
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
|
||||
var beamFiredEvent = new BeamFiredEvent(ent);
|
||||
RaiseLocalEvent(beam.VirtualBeamController.Value, beamFiredEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Content.Server.Cargo.Components
|
||||
/// The paper-type prototype to spawn with the order information.
|
||||
/// </summary>
|
||||
[DataField("printerOutput", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string PrinterOutput = "Paper";
|
||||
public string PrinterOutput = "PaperCargoInvoice";
|
||||
|
||||
[DataField("receiverPort", customTypeSerializer: typeof(PrototypeIdSerializer<ReceiverPortPrototype>))]
|
||||
public string ReceiverPort = "OrderReceiver";
|
||||
|
||||
@@ -465,7 +465,7 @@ public sealed partial class CargoSystem
|
||||
return;
|
||||
|
||||
// spawn a piece of paper.
|
||||
var printed = EntityManager.SpawnEntity("Paper", coordinates);
|
||||
var printed = EntityManager.SpawnEntity(component.PrinterOutput, coordinates);
|
||||
|
||||
if (!TryComp<PaperComponent>(printed, out var paper))
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
|
||||
namespace Content.Server.Chemistry.ReagentEffectConditions
|
||||
{
|
||||
public sealed class MobStateCondition : ReagentEffectCondition
|
||||
{
|
||||
|
||||
|
||||
[DataField("mobstate")]
|
||||
public MobState mobstate = MobState.Alive;
|
||||
|
||||
public override bool Condition(ReagentEffectArgs args)
|
||||
{
|
||||
if (args.EntityManager.TryGetComponent(args.SolutionEntity, out MobStateComponent? mobState))
|
||||
{
|
||||
if (mobState.CurrentState == mobstate)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Shared.Body.Part;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Climbing;
|
||||
using Content.Shared.Climbing.Events;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.DragDrop;
|
||||
using Content.Shared.GameTicking;
|
||||
@@ -280,6 +281,8 @@ public sealed class ClimbSystem : SharedClimbSystem
|
||||
|
||||
climbing.IsClimbing = false;
|
||||
climbing.OwnerIsTransitioning = false;
|
||||
var ev = new EndClimbEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
Dirty(climbing);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Construction.Components;
|
||||
using Content.Shared.Construction;
|
||||
using Content.Shared.Construction.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Content.Server.Construction.Components
|
||||
using Content.Shared.Construction.Components;
|
||||
|
||||
namespace Content.Server.Construction.Components
|
||||
{
|
||||
[RequiresExplicitImplementation]
|
||||
public interface IRefreshParts
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Content.Shared.Construction.Components;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Construction;
|
||||
using JetBrains.Annotations;
|
||||
using Content.Server.Doors.Components;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Examine;
|
||||
|
||||
namespace Content.Server.Construction.Conditions
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user