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:
Morbo
2023-01-17 22:57:53 +03:00
488 changed files with 24984 additions and 11253 deletions

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

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

View File

@@ -1,7 +0,0 @@
using Content.Shared.Doors.Components;
namespace Content.Client.Doors;
[RegisterComponent]
[ComponentReference(typeof(SharedAirlockComponent))]
public sealed class AirlockComponent : SharedAirlockComponent { }

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,10 @@
namespace Content.Client.Guidebook;
/// <summary>
/// This is used for the guidebook monkey.
/// </summary>
[RegisterComponent]
public sealed class GuidebookControlsTestComponent : Component
{
}

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

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

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

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

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

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

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

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

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

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

View File

@@ -96,6 +96,7 @@ namespace Content.Client.Input
common.AddFunction(ContentKeyFunctions.OpenTileSpawnWindow);
common.AddFunction(ContentKeyFunctions.OpenDecalSpawnWindow);
common.AddFunction(ContentKeyFunctions.OpenAdminMenu);
common.AddFunction(ContentKeyFunctions.OpenGuidebook);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -21,6 +21,7 @@ public sealed class ParallaxOverlay : Overlay
public ParallaxOverlay()
{
ZIndex = ParallaxSystem.ParallaxZIndex;
IoCManager.InjectDependencies(this);
_parallax = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ParallaxSystem>();

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
using Content.Shared.PneumaticCannon;
namespace Content.Client.PneumaticCannon;
public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem
{
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
namespace Content.Server.Construction.Components
using Content.Shared.Construction.Components;
namespace Content.Server.Construction.Components
{
[RequiresExplicitImplementation]
public interface IRefreshParts

View File

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

View File

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