Merge remote-tracking branch 'upstream/master' into upstream-sync

# Conflicts:
#	Content.Server/Communications/CommunicationsConsoleSystem.cs
#	README.md
#	Resources/Prototypes/Entities/Mobs/Species/reptilian.yml
#	Resources/Prototypes/Roles/Jobs/Civilian/librarian.yml
#	Resources/Prototypes/secret_weights.yml
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/librarian.rsi/equipped-INNERCLOTHING.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/librarian.rsi/icon.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/librarian.rsi/inhand-left.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/librarian.rsi/inhand-right.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/librarian.rsi/meta.json
This commit is contained in:
Morb0
2023-10-11 16:56:19 +03:00
531 changed files with 7710 additions and 18981 deletions

32
.github/workflows/update-credits.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Update Contrib and Patreons in credits
on:
workflow_dispatch:
schedule:
- cron: 0 0 * * 0
jobs:
get_credits:
runs-on: ubuntu-latest
# Hey there fork dev! If you like to include your own contributors in this then you can probably just change this to your own repo
# Do this in dump_github_contributors.ps1 too into your own repo
if: github.repository == 'space-wizards/space-station-14'
steps:
- uses: actions/checkout@v3.6.0
with:
ref: master
- name: Get this week's Contributors
shell: pwsh
run: Tools/dump_github_contributors.ps1 > Resources/Credits/GitHub.txt
# TODO
#- name: Get this week's Patreons
# run: Tools/script2dumppatreons > Resources/Credits/Patrons.yml
- name: Commit new credit files
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Update Credits
commit_author: PJBot <pieterjan.briers+bot@gmail.com>

1
.gitignore vendored
View File

@@ -168,6 +168,7 @@ PublishScripts/
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
.nuget/
# Microsoft Azure Build Output
csx/

View File

@@ -29,7 +29,6 @@ namespace Content.Client.Actions
public event Action<EntityUid>? OnActionAdded;
public event Action<EntityUid>? OnActionRemoved;
public event OnActionReplaced? ActionReplaced;
public event Action? ActionsUpdated;
public event Action<ActionsComponent>? LinkActions;
public event Action? UnlinkActions;

View File

@@ -1,11 +1,4 @@
using System;
using Content.Shared.Chemistry;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Content.Client.Chemistry.Visualizers
@@ -13,40 +6,40 @@ namespace Content.Client.Chemistry.Visualizers
[RegisterComponent]
public sealed partial class SolutionContainerVisualsComponent : Component
{
[DataField("maxFillLevels")]
[DataField]
public int MaxFillLevels = 0;
[DataField("fillBaseName")]
[DataField]
public string? FillBaseName = null;
[DataField("layer")]
public SolutionContainerLayers FillLayer = SolutionContainerLayers.Fill;
[DataField("baseLayer")]
[DataField]
public SolutionContainerLayers Layer = SolutionContainerLayers.Fill;
[DataField]
public SolutionContainerLayers BaseLayer = SolutionContainerLayers.Base;
[DataField("overlayLayer")]
[DataField]
public SolutionContainerLayers OverlayLayer = SolutionContainerLayers.Overlay;
[DataField("changeColor")]
[DataField]
public bool ChangeColor = true;
[DataField("emptySpriteName")]
[DataField]
public string? EmptySpriteName = null;
[DataField("emptySpriteColor")]
[DataField]
public Color EmptySpriteColor = Color.White;
[DataField("metamorphic")]
[DataField]
public bool Metamorphic = false;
[DataField("metamorphicDefaultSprite")]
[DataField]
public SpriteSpecifier? MetamorphicDefaultSprite;
[DataField("metamorphicNameFull")]
public string MetamorphicNameFull = "transformable-container-component-glass";
[DataField]
public LocId MetamorphicNameFull = "transformable-container-component-glass";
/// <summary>
/// Which solution of the SolutionContainerManagerComponent to represent.
/// If not set, will work as default.
/// </summary>
[DataField("solutionName")]
[DataField]
public string? SolutionName;
[DataField("initialName")]
[DataField]
public string InitialName = string.Empty;
[DataField("initialDescription")]
[DataField]
public string InitialDescription = string.Empty;
}
}

View File

@@ -41,7 +41,7 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
if (args.Sprite == null)
return;
if (!args.Sprite.LayerMapTryGet(component.FillLayer, out var fillLayer))
if (!args.Sprite.LayerMapTryGet(component.Layer, out var fillLayer))
return;
// Currently some solution methods such as overflowing will try to update appearance with a

View File

@@ -1,7 +1,6 @@
using Content.Client.Hands.Systems;
using Content.Shared.CCVar;
using Content.Shared.CombatMode;
using Content.Shared.Targeting;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
@@ -44,11 +43,6 @@ public sealed class CombatModeSystem : SharedCombatModeSystem
base.Shutdown();
}
private void OnTargetingZoneChanged(TargetingZone obj)
{
EntityManager.RaisePredictiveEvent(new CombatModeSystemMessages.SetTargetZoneMessage(obj));
}
public bool IsInCombatMode()
{
var entity = _playerManager.LocalPlayer?.ControlledEntity;
@@ -65,12 +59,6 @@ public sealed class CombatModeSystem : SharedCombatModeSystem
UpdateHud(entity);
}
public override void SetActiveZone(EntityUid entity, TargetingZone zone, CombatModeComponent? component = null)
{
base.SetActiveZone(entity, zone, component);
UpdateHud(entity);
}
private void UpdateHud(EntityUid entity)
{
if (entity != _playerManager.LocalPlayer?.ControlledEntity || !Timing.IsFirstTimePredicted)

View File

@@ -15,6 +15,7 @@ namespace Content.Client.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var containerSys = entityManager.System<SharedContainerSystem>();
var organs = entityManager.EntityQuery<OrganComponent>(true);
foreach (var part in organs)
@@ -27,7 +28,7 @@ namespace Content.Client.Commands
sprite.ContainerOccluded = false;
var tempParent = part.Owner;
while (tempParent.TryGetContainer(out var container))
while (containerSys.TryGetContainingContainer(tempParent, out var container))
{
if (!container.ShowContents)
{

View File

@@ -0,0 +1,30 @@
using Content.Shared.Ghost;
using Robust.Client.GameObjects;
using Robust.Shared.Console;
namespace Content.Client.Ghost;
public sealed class GhostToggleSelfVisibility : IConsoleCommand
{
public string Command => "toggleselfghost";
public string Description => "Toggles seeing your own ghost.";
public string Help => "toggleselfghost";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var attachedEntity = shell.Player?.AttachedEntity;
if (!attachedEntity.HasValue)
return;
var entityManager = IoCManager.Resolve<IEntityManager>();
if (!entityManager.HasComponent<GhostComponent>(attachedEntity))
{
shell.WriteError("Entity must be a ghost.");
return;
}
if (!entityManager.TryGetComponent(attachedEntity, out SpriteComponent? spriteComponent))
return;
spriteComponent.Visible = !spriteComponent.Visible;
}
}

View File

@@ -174,8 +174,9 @@ namespace Content.Client.Instruments.UI
if (localPlayer.ControlledEntity == instrumentEnt)
return true;
var container = _owner.Entities.System<SharedContainerSystem>();
// If we're a handheld instrument, we might be in a container. Get it just in case.
instrumentEnt.TryGetContainerMan(out var conMan);
container.TryGetContainingContainer(instrumentEnt, out var conMan);
// If the instrument is handheld and we're not holding it, we return.
if ((instrument.Handheld && (conMan == null || conMan.Owner != localPlayer.ControlledEntity)))

View File

@@ -6,15 +6,17 @@ namespace Content.Client.Interactable
{
public sealed class InteractionSystem : SharedInteractionSystem
{
[Dependency] private readonly SharedContainerSystem _container = default!;
public override bool CanAccessViaStorage(EntityUid user, EntityUid target)
{
if (!EntityManager.EntityExists(target))
return false;
if (!target.TryGetContainer(out var container))
if (!_container.TryGetContainingContainer(target, out var container))
return false;
if (!TryComp(container.Owner, out StorageComponent? storage))
if (!HasComp<StorageComponent>(container.Owner))
return false;
// we don't check if the user can access the storage entity itself. This should be handed by the UI system.

View File

@@ -1,7 +1,7 @@
using Robust.Client.Input;
using Robust.Shared.Map;
namespace Content.Client.DragDrop;
namespace Content.Client.Interaction;
/// <summary>
/// Helper for implementing drag and drop interactions.

View File

@@ -1,3 +1,4 @@
using System.Numerics;
using Content.Client.CombatMode;
using Content.Client.Gameplay;
using Content.Client.Outline;
@@ -7,7 +8,6 @@ using Content.Shared.DragDrop;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
@@ -20,15 +20,13 @@ using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Numerics;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client.DragDrop;
namespace Content.Client.Interaction;
/// <summary>
/// Handles clientside drag and drop logic
/// </summary>
[UsedImplicitly]
public sealed class DragDropSystem : SharedDragDropSystem
{
[Dependency] private readonly IStateManager _stateManager = default!;
@@ -45,8 +43,6 @@ public sealed class DragDropSystem : SharedDragDropSystem
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
private ISawmill _sawmill = default!;
// how often to recheck possible targets (prevents calling expensive
// check logic each update)
private const float TargetRecheckInterval = 0.25f;
@@ -110,7 +106,6 @@ public sealed class DragDropSystem : SharedDragDropSystem
public override void Initialize()
{
base.Initialize();
_sawmill = Logger.GetSawmill("drag_drop");
UpdatesOutsidePrediction = true;
UpdatesAfter.Add(typeof(SharedEyeSystem));
@@ -263,7 +258,7 @@ public sealed class DragDropSystem : SharedDragDropSystem
return;
}
_sawmill.Warning($"Unable to display drag shadow for {ToPrettyString(_draggedEntity.Value)} because it has no sprite component.");
Log.Warning($"Unable to display drag shadow for {ToPrettyString(_draggedEntity.Value)} because it has no sprite component.");
}
private bool UpdateDrag(float frameTime)
@@ -392,7 +387,7 @@ public sealed class DragDropSystem : SharedDragDropSystem
}
// tell the server about the drop attempt
RaiseNetworkEvent(new DragDropRequestEvent(GetNetEntity(_draggedEntity.Value), GetNetEntity(entity)));
RaisePredictiveEvent(new DragDropRequestEvent(GetNetEntity(_draggedEntity.Value), GetNetEntity(entity)));
EndDrag();
return true;
}

View File

@@ -1,33 +0,0 @@
using Content.Client.Interactable;
using Content.Shared.Climbing;
using Content.Shared.DragDrop;
namespace Content.Client.Movement.Systems;
public sealed class ClimbSystem : SharedClimbSystem
{
[Dependency] private readonly InteractionSystem _interactionSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ClimbableComponent, CanDropTargetEvent>(OnCanDragDropOn);
}
protected override void OnCanDragDropOn(EntityUid uid, ClimbableComponent component, ref CanDropTargetEvent args)
{
base.OnCanDragDropOn(uid, component, ref args);
if (!args.CanDrop)
return;
var user = args.User;
var target = uid;
var dragged = args.Dragged;
bool Ignored(EntityUid entity) => entity == target || entity == user || entity == dragged;
args.CanDrop = _interactionSystem.InRangeUnobstructed(user, target, component.Range, predicate: Ignored)
&& _interactionSystem.InRangeUnobstructed(user, dragged, component.Range, predicate: Ignored);
args.Handled = true;
}
}

View File

@@ -1,7 +1,7 @@
using Content.Shared.PDA;
using Content.Shared.PDA.Ringer;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;
namespace Content.Client.PDA.Ringer
{
@@ -29,9 +29,17 @@ namespace Content.Client.PDA.Ringer
_menu.SetRingerButton.OnPressed += _ =>
{
if (!TryGetRingtone(out var ringtone)) return;
if (!TryGetRingtone(out var ringtone))
return;
SendMessage(new RingerSetRingtoneMessage(ringtone));
_menu.SetRingerButton.Disabled = true;
Timer.Spawn(333, () =>
{
if (_menu is { Disposed: false, SetRingerButton: { Disposed: false } ringer})
ringer.Disabled = false;
});
};
}
@@ -74,7 +82,7 @@ namespace Content.Client.PDA.Ringer
}
_menu.TestRingerButton.Visible = !msg.IsPlaying;
_menu.TestRingerButton.Disabled = msg.IsPlaying;
}

View File

@@ -79,12 +79,14 @@
Access="Public"
Text="{Loc 'comp-ringer-ui-test-ringtone-button'}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
VerticalAlignment="Center"
StyleClasses="OpenRight" />
<Button Name = "SetRingerButton"
Access="Public"
Text="{Loc 'comp-ringer-ui-set-ringtone-button'}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
VerticalAlignment="Center"
StyleClasses="OpenLeft" />
</BoxContainer>
</PanelContainer>
</BoxContainer>

View File

@@ -46,6 +46,8 @@ public sealed class ContentReplayPlaybackManager
/// </summary>
public Type? DefaultState;
public bool IsScreenshotMode = false;
private bool _initialized;
public void Initialize()

View File

@@ -0,0 +1,35 @@
using Content.Client.UserInterface.Systems.Chat;
using Content.Shared.Chat;
using Robust.Client.Replays.Commands;
using Robust.Client.Replays.UI;
using Robust.Client.UserInterface;
using Robust.Shared.Console;
namespace Content.Client.Replay;
public sealed class ReplayToggleScreenshotModeCommand : BaseReplayCommand
{
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly ContentReplayPlaybackManager _replayManager = default!;
public override string Command => "replay_toggle_screenshot_mode";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var screen = _userInterfaceManager.ActiveScreen;
if (screen == null)
return;
_replayManager.IsScreenshotMode = !_replayManager.IsScreenshotMode;
var showReplayWidget = _replayManager.IsScreenshotMode;
screen.ShowWidget<ReplayControlWidget>(showReplayWidget);
foreach (var chatBox in _userInterfaceManager.GetUIController<ChatUIController>().Chats)
{
chatBox.ChatInput.Visible = !showReplayWidget;
if (!showReplayWidget)
chatBox.ChatInput.ChannelSelector.Select(ChatSelectChannel.Local);
}
}
}

View File

@@ -12,6 +12,8 @@ namespace Content.Client.Replay.UI;
[Virtual]
public class ReplaySpectateEntityState : GameplayState
{
[Dependency] private readonly ContentReplayPlaybackManager _replayManager = default!;
protected override void Startup()
{
base.Startup();
@@ -21,11 +23,13 @@ public class ReplaySpectateEntityState : GameplayState
return;
screen.ShowWidget<GameTopMenuBar>(false);
SetAnchorAndMarginPreset(screen.GetOrAddWidget<ReplayControlWidget>(), LayoutPreset.TopLeft, margin: 10);
var replayWidget = screen.GetOrAddWidget<ReplayControlWidget>();
SetAnchorAndMarginPreset(replayWidget, LayoutPreset.TopLeft, margin: 10);
replayWidget.Visible = !_replayManager.IsScreenshotMode;
foreach (var chatbox in UserInterfaceManager.GetUIController<ChatUIController>().Chats)
{
chatbox.ChatInput.Visible = false;
chatbox.ChatInput.Visible = _replayManager.IsScreenshotMode;
}
}

View File

@@ -4,7 +4,6 @@ using Content.Client.ContextMenu.UI;
using Content.Client.Examine;
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;
@@ -1148,29 +1147,6 @@ namespace Content.Client.Stylesheets
new StyleProperty(Label.StylePropertyFont, notoSansDisplayBold14),
}),
// Targeting doll
new StyleRule(
new SelectorElement(typeof(TextureButton), new[] {TargetingDoll.StyleClassTargetDollZone}, null,
new[] {TextureButton.StylePseudoClassNormal}), new[]
{
new StyleProperty(Control.StylePropertyModulateSelf, ButtonColorDefault),
}),
new StyleRule(
new SelectorElement(typeof(TextureButton), new[] {TargetingDoll.StyleClassTargetDollZone}, null,
new[] {TextureButton.StylePseudoClassHover}), new[]
{
new StyleProperty(Control.StylePropertyModulateSelf, ButtonColorHovered),
}),
new StyleRule(
new SelectorElement(typeof(TextureButton), new[] {TargetingDoll.StyleClassTargetDollZone}, null,
new[] {TextureButton.StylePseudoClassPressed}), new[]
{
new StyleProperty(Control.StylePropertyModulateSelf, ButtonColorPressed),
}),
// NanoHeading
new StyleRule(

View File

@@ -1,8 +0,0 @@
<targeting:TargetingDoll xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:targeting="clr-namespace:Content.Client.Targeting.UI"
Orientation="Vertical">
<TextureButton Name = "ButtonHigh" TexturePath="/Textures/Interface/target-doll-high.svg.96dpi.png" HorizontalAlignment="Center" StyleIdentifier="target-doll-zone"/>
<TextureButton Name = "ButtonMedium" TexturePath="/Textures/Interface/target-doll-middle.svg.96dpi.png" HorizontalAlignment="Center" StyleIdentifier="target-doll-zone"/>
<TextureButton Name = "ButtonLow" TexturePath="/Textures/Interface/target-doll-low.svg.96dpi.png" HorizontalAlignment="Center" StyleIdentifier="target-doll-zone"/>
</targeting:TargetingDoll>

View File

@@ -1,44 +0,0 @@
using Content.Shared.Targeting;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Targeting.UI;
[GenerateTypedNameReferences]
public sealed partial class TargetingDoll : BoxContainer
{
public static readonly string StyleClassTargetDollZone = "target-doll-zone";
private TargetingZone _activeZone = TargetingZone.Middle;
public event Action<TargetingZone>? OnZoneChanged;
public TargetingDoll()
{
RobustXamlLoader.Load(this);
}
public TargetingZone ActiveZone
{
get => _activeZone;
set
{
if (_activeZone == value)
{
return;
}
_activeZone = value;
OnZoneChanged?.Invoke(value);
UpdateButtons();
}
}
private void UpdateButtons()
{
ButtonHigh.Pressed = _activeZone == TargetingZone.High;
ButtonMedium.Pressed = _activeZone == TargetingZone.Middle;
ButtonLow.Pressed = _activeZone == TargetingZone.Low;
}
}

View File

@@ -1,7 +1,5 @@
using System.Numerics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
namespace Content.Client.UserInterface.Controls;
@@ -10,8 +8,6 @@ namespace Content.Client.UserInterface.Controls;
/// </summary>
public sealed class RecordedSplitContainer : SplitContainer
{
public Action<Vector2, Vector2>? OnSplitResizeFinish;
public double? DesiredSplitCenter;
protected override Vector2 ArrangeOverride(Vector2 finalSize)
@@ -30,24 +26,4 @@ public sealed class RecordedSplitContainer : SplitContainer
return base.ArrangeOverride(finalSize);
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
if (args.Function != EngineKeyFunctions.UIClick)
{
return;
}
if (ChildCount != 2)
{
return;
}
var first = GetChild(0);
var second = GetChild(1);
OnSplitResizeFinish?.Invoke(first.Size, second.Size);
}
}

View File

@@ -24,7 +24,7 @@ public sealed partial class SeparatedChatGameScreen : InGameScreen
SetAnchorAndMarginPreset(Hotbar, LayoutPreset.BottomWide, margin: 5);
SetAnchorAndMarginPreset(Alerts, LayoutPreset.CenterRight, margin: 10);
ScreenContainer.OnSplitResizeFinish += (first, second) =>
ScreenContainer.OnSplitResizeFinished += () =>
OnChatResized?.Invoke(new Vector2(ScreenContainer.SplitFraction, 0));
}

View File

@@ -3,9 +3,9 @@ using System.Numerics;
using System.Runtime.InteropServices;
using Content.Client.Actions;
using Content.Client.Construction;
using Content.Client.DragDrop;
using Content.Client.Gameplay;
using Content.Client.Hands;
using Content.Client.Interaction;
using Content.Client.Outline;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Actions.Controls;
@@ -42,6 +42,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
[Dependency] private readonly IOverlayManager _overlays = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[UISystemDependency] private readonly ActionsSystem? _actionsSystem = default;
[UISystemDependency] private readonly InteractionOutlineSystem? _interactionOutline = default;
@@ -113,12 +114,11 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
{
_actionsSystem.OnActionAdded += OnActionAdded;
_actionsSystem.OnActionRemoved += OnActionRemoved;
_actionsSystem.ActionReplaced += OnActionReplaced;
_actionsSystem.ActionsUpdated += OnActionsUpdated;
}
UpdateFilterLabel();
SearchAndDisplay();
QueueWindowUpdate();
_dragShadow.Orphan();
UIManager.PopupRoot.AddChild(_dragShadow);
@@ -307,6 +307,8 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
{
if (ActionButton != null)
ActionButton.Pressed = true;
SearchAndDisplay();
}
private void OnWindowClosed()
@@ -321,7 +323,6 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
{
_actionsSystem.OnActionAdded -= OnActionAdded;
_actionsSystem.OnActionRemoved -= OnActionRemoved;
_actionsSystem.ActionReplaced -= OnActionReplaced;
_actionsSystem.ActionsUpdated -= OnActionsUpdated;
}
@@ -345,6 +346,9 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void ChangePage(int index)
{
if (_actionsSystem == null)
return;
var lastPage = _pages.Count - 1;
if (index < 0)
{
@@ -357,7 +361,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
_currentPageIndex = index;
var page = _pages[_currentPageIndex];
_container?.SetActionData(page);
_container?.SetActionData(_actionsSystem, page);
ActionsBar!.PageButtons.Label.Text = $"{_currentPageIndex + 1}";
}
@@ -424,7 +428,6 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
}
AppendAction(actionId);
SearchAndDisplay();
}
private void OnActionRemoved(EntityUid actionId)
@@ -454,24 +457,11 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
}
}
}
SearchAndDisplay();
}
private void OnActionReplaced(EntityUid actionId)
{
if (_container == null)
return;
foreach (var button in _container.GetButtons())
{
if (button.ActionId == actionId)
button.UpdateData(actionId);
}
}
private void OnActionsUpdated()
{
QueueWindowUpdate();
if (_container == null)
return;
@@ -538,27 +528,56 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void PopulateActions(IEnumerable<(EntityUid Id, BaseActionComponent Comp)> actions)
{
if (_window == null)
if (_window is not { Disposed: false, IsOpen: true })
return;
ClearList();
if (_actionsSystem == null)
return;
_window.UpdateNeeded = false;
List<ActionButton> existing = new(_window.ResultsGrid.ChildCount);
foreach (var child in _window.ResultsGrid.Children)
{
if (child is ActionButton button)
existing.Add(button);
}
int i = 0;
foreach (var action in actions)
{
var button = new ActionButton {Locked = true};
if (i < existing.Count)
{
existing[i++].UpdateData(action.Id, _actionsSystem);
continue;
}
button.UpdateData(action.Id);
var button = new ActionButton(_entMan, _spriteSystem, this) {Locked = true};
button.ActionPressed += OnWindowActionPressed;
button.ActionUnpressed += OnWindowActionUnPressed;
button.ActionFocusExited += OnWindowActionFocusExisted;
button.UpdateData(action.Id, _actionsSystem);
_window.ResultsGrid.AddChild(button);
}
for (; i < existing.Count; i++)
{
existing[i].Dispose();
}
}
public void QueueWindowUpdate()
{
if (_window != null)
_window.UpdateNeeded = true;
}
private void SearchAndDisplay()
{
if (_window is not { Disposed: false } || _actionsSystem == null)
if (_window is not { Disposed: false, IsOpen: true })
return;
if (_actionsSystem == null)
return;
if (_playerManager.LocalPlayer?.ControlledEntity is not { } player)
@@ -598,6 +617,9 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void SetAction(ActionButton button, EntityUid? actionId)
{
if (_actionsSystem == null)
return;
int position;
if (actionId == null)
@@ -611,7 +633,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
return;
}
if (button.TryReplaceWith(actionId.Value) &&
if (button.TryReplaceWith(actionId.Value, _actionsSystem) &&
_container != null &&
_container.TryGetButtonIndex(button, out position))
{
@@ -648,18 +670,18 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
_window.SearchBar.Clear();
_window.FilterButton.DeselectAll();
UpdateFilterLabel();
SearchAndDisplay();
QueueWindowUpdate();
}
private void OnSearchChanged(LineEditEventArgs args)
{
SearchAndDisplay();
QueueWindowUpdate();
}
private void OnFilterSelected(ItemPressedEventArgs args)
{
UpdateFilterLabel();
SearchAndDisplay();
QueueWindowUpdate();
}
private void OnWindowActionPressed(GUIBoundKeyEventArgs args, ActionButton action)
@@ -849,12 +871,15 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void AssignSlots(List<SlotAssignment> assignments)
{
if (_actionsSystem == null)
return;
foreach (ref var assignment in CollectionsMarshal.AsSpan(assignments))
{
_pages[assignment.Hotbar][assignment.Slot] = assignment.ActionId;
}
_container?.SetActionData(_pages[_currentPageIndex]);
_container?.SetActionData(_actionsSystem, _pages[_currentPageIndex]);
}
public void RemoveActionContainer()
@@ -881,19 +906,24 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
public override void FrameUpdate(FrameEventArgs args)
{
_menuDragHelper.Update(args.DeltaSeconds);
if (_window is {UpdateNeeded: true})
SearchAndDisplay();
}
private void OnComponentLinked(ActionsComponent component)
{
if (_actionsSystem == null)
return;
LoadDefaultActions(component);
_container?.SetActionData(_pages[DefaultPageIndex]);
SearchAndDisplay();
_container?.SetActionData(_actionsSystem, _pages[DefaultPageIndex]);
QueueWindowUpdate();
}
private void OnComponentUnlinked()
{
_container?.ClearActionData();
SearchAndDisplay();
QueueWindowUpdate();
StopTargeting();
}

View File

@@ -8,8 +8,6 @@ using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -21,11 +19,9 @@ namespace Content.Client.UserInterface.Systems.Actions.Controls;
public sealed class ActionButton : Control, IEntityControl
{
private IEntityManager? _entities;
private ActionUIController Controller => UserInterfaceManager.GetUIController<ActionUIController>();
private IEntityManager Entities => _entities ??= IoCManager.Resolve<IEntityManager>();
private ActionsSystem Actions => Entities.System<ActionsSystem>();
private IEntityManager _entities;
private SpriteSystem? _spriteSys;
private ActionUIController? _controller;
private bool _beingHovered;
private bool _depressed;
private bool _toggled;
@@ -54,14 +50,21 @@ public sealed class ActionButton : Control, IEntityControl
private readonly SpriteView _bigItemSpriteView;
public EntityUid? ActionId { get; private set; }
private BaseActionComponent? _action;
public bool Locked { get; set; }
public event Action<GUIBoundKeyEventArgs, ActionButton>? ActionPressed;
public event Action<GUIBoundKeyEventArgs, ActionButton>? ActionUnpressed;
public event Action<ActionButton>? ActionFocusExited;
public ActionButton()
public ActionButton(IEntityManager entities, SpriteSystem? spriteSys = null, ActionUIController? controller = null)
{
// TODO why is this constructor so slooooow. The rest of the code is fine
_entities = entities;
_spriteSys = spriteSys;
_controller = controller;
MouseFilter = MouseFilterMode.Pass;
Button = new TextureRect
{
@@ -180,7 +183,7 @@ public sealed class ActionButton : Control, IEntityControl
private Control? SupplyTooltip(Control sender)
{
if (!Entities.TryGetComponent(ActionId, out MetaDataComponent? metadata))
if (!_entities.TryGetComponent(ActionId, out MetaDataComponent? metadata))
return null;
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
@@ -196,9 +199,8 @@ public sealed class ActionButton : Control, IEntityControl
private void UpdateItemIcon()
{
if (!Actions.TryGetActionData(ActionId, out var action) ||
action is not {EntityIcon: { } entity} ||
!Entities.HasComponent<SpriteComponent>(entity))
if (_action is not {EntityIcon: { } entity} ||
!_entities.HasComponent<SpriteComponent>(entity))
{
_bigItemSpriteView.Visible = false;
_bigItemSpriteView.SetEntity(null);
@@ -207,7 +209,7 @@ public sealed class ActionButton : Control, IEntityControl
}
else
{
switch (action.ItemIconStyle)
switch (_action.ItemIconStyle)
{
case ItemActionIconStyle.BigItem:
_bigItemSpriteView.Visible = true;
@@ -233,17 +235,17 @@ public sealed class ActionButton : Control, IEntityControl
private void SetActionIcon(Texture? texture)
{
if (!Actions.TryGetActionData(ActionId, out var action) || texture == null)
if (_action == null || texture == null)
{
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
}
else if (action.EntityIcon != null && action.ItemIconStyle == ItemActionIconStyle.BigItem)
else if (_action.EntityIcon != null && _action.ItemIconStyle == ItemActionIconStyle.BigItem)
{
_smallActionIcon.Texture = texture;
_smallActionIcon.Modulate = action.IconColor;
_smallActionIcon.Modulate = _action.IconColor;
_smallActionIcon.Visible = true;
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
@@ -251,7 +253,7 @@ public sealed class ActionButton : Control, IEntityControl
else
{
_bigActionIcon.Texture = texture;
_bigActionIcon.Modulate = action.IconColor;
_bigActionIcon.Modulate = _action.IconColor;
_bigActionIcon.Visible = true;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
@@ -262,39 +264,43 @@ public sealed class ActionButton : Control, IEntityControl
{
UpdateItemIcon();
if (!Actions.TryGetActionData(ActionId, out var action))
if (_action == null)
{
SetActionIcon(null);
return;
}
if ((Controller.SelectingTargetFor == ActionId || action.Toggled) && action.IconOn != null)
SetActionIcon(action.IconOn.Frame0());
_controller ??= UserInterfaceManager.GetUIController<ActionUIController>();
_spriteSys ??= _entities.System<SpriteSystem>();
if ((_controller.SelectingTargetFor == ActionId || _action.Toggled) && _action.IconOn != null)
SetActionIcon(_spriteSys.Frame0(_action.IconOn));
else
SetActionIcon(action.Icon?.Frame0());
SetActionIcon(_action.Icon != null ? _spriteSys.Frame0(_action.Icon) : null);
}
public bool TryReplaceWith(EntityUid actionId)
public bool TryReplaceWith(EntityUid actionId, ActionsSystem system)
{
if (Locked)
{
return false;
}
UpdateData(actionId);
UpdateData(actionId, system);
return true;
}
public void UpdateData(EntityUid actionId)
public void UpdateData(EntityUid? actionId, ActionsSystem system)
{
ActionId = actionId;
Label.Visible = true;
system.TryGetActionData(actionId, out _action);
Label.Visible = actionId != null;
UpdateIcons();
}
public void ClearData()
{
ActionId = null;
_action = null;
Cooldown.Visible = false;
Cooldown.Progress = 1;
Label.Visible = false;
@@ -305,19 +311,17 @@ public sealed class ActionButton : Control, IEntityControl
{
base.FrameUpdate(args);
if (!Actions.TryGetActionData(ActionId, out var action))
{
if (_action == null)
return;
if (_action.Cooldown != null)
{
Cooldown.FromTime(_action.Cooldown.Value.Start, _action.Cooldown.Value.End);
}
if (action.Cooldown != null)
if (ActionId != null && _toggled != _action.Toggled)
{
Cooldown.FromTime(action.Cooldown.Value.Start, action.Cooldown.Value.End);
}
if (ActionId != null && _toggled != action.Toggled)
{
_toggled = action.Toggled;
_toggled = _action.Toggled;
}
}
@@ -344,7 +348,7 @@ public sealed class ActionButton : Control, IEntityControl
public void Depress(GUIBoundKeyEventArgs args, bool depress)
{
// action can still be toggled if it's allowed to stay selected
if (!Actions.TryGetActionData(ActionId, out var action) || action is not {Enabled: true})
if (_action is not {Enabled: true})
return;
if (_depressed && !depress)
@@ -362,14 +366,15 @@ public sealed class ActionButton : Control, IEntityControl
HighlightRect.Visible = _beingHovered;
// always show the normal empty button style if no action in this slot
if (!Actions.TryGetActionData(ActionId, out var action))
if (_action == null)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
return;
}
// show a hover only if the action is usable or another action is being dragged on top of this
if (_beingHovered && (Controller.IsDragging || action.Enabled))
_controller ??= UserInterfaceManager.GetUIController<ActionUIController>();
if (_beingHovered && (_controller.IsDragging || _action.Enabled))
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
}
@@ -384,16 +389,16 @@ public sealed class ActionButton : Control, IEntityControl
}
// if it's toggled on, always show the toggled on style (currently same as depressed style)
if (action.Toggled || Controller.SelectingTargetFor == ActionId)
if (_action.Toggled || _controller.SelectingTargetFor == ActionId)
{
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
SetOnlyStylePseudoClass(action.IconOn != null
SetOnlyStylePseudoClass(_action.IconOn != null
? ContainerButton.StylePseudoClassNormal
: ContainerButton.StylePseudoClassPressed);
return;
}
if (!action.Enabled)
if (!_action.Enabled)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
return;

View File

@@ -1,3 +1,4 @@
using Content.Client.Actions;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -21,7 +22,7 @@ public class ActionButtonContainer : GridContainer
get => (ActionButton) GetChild(index);
}
public void SetActionData(params EntityUid?[] actionTypes)
public void SetActionData(ActionsSystem system, params EntityUid?[] actionTypes)
{
ClearActionData();
@@ -31,7 +32,7 @@ public class ActionButtonContainer : GridContainer
if (action == null)
continue;
((ActionButton) GetChild(i)).UpdateData(action.Value);
((ActionButton) GetChild(i)).UpdateData(action.Value, system);
}
}

View File

@@ -3,16 +3,18 @@ using Content.Shared.Input;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input;
namespace Content.Client.UserInterface.Systems.Actions.Widgets;
[GenerateTypedNameReferences]
public sealed partial class ActionsBar : UIWidget
{
[Dependency] private readonly IEntityManager _entity = default!;
public ActionsBar()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
var keys = ContentKeyFunctions.GetHotbarBoundKeys();
for (var index = 1; index < keys.Length; index++)
@@ -24,7 +26,7 @@ public sealed partial class ActionsBar : UIWidget
ActionButton MakeButton(int index)
{
var boundKey = keys[index];
var button = new ActionButton();
var button = new ActionButton(_entity);
button.KeyBind = boundKey;
button.Label.Text = index.ToString();
return button;

View File

@@ -10,6 +10,11 @@ public sealed partial class ActionsWindow : DefaultWindow
{
public MultiselectOptionButton<Filters> FilterButton { get; private set; }
/// <summary>
/// Whether the displayed actions or search filter needs updating.
/// </summary>
public bool UpdateNeeded;
public ActionsWindow()
{
RobustXamlLoader.Load(this);

View File

@@ -703,7 +703,7 @@ public sealed class ChatUIController : UIController
public void UpdateSelectedChannel(ChatBox box)
{
var (prefixChannel, _, radioChannel) = SplitInputContents(box.ChatInput.Input.Text);
var (prefixChannel, _, radioChannel) = SplitInputContents(box.ChatInput.Input.Text.ToLower());
if (prefixChannel == ChatSelectChannel.None)
box.ChatInput.ChannelSelector.UpdateChannelSelectButton(box.SelectedChannel, null);

View File

@@ -1,7 +1,12 @@
#nullable enable
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Players;
using Content.Shared.Mind;
using Content.Shared.Players;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
@@ -25,6 +30,9 @@ public sealed partial class TestPair
public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
public IPlayerSession? Player => (IPlayerSession?) Server.PlayerMan.Sessions.FirstOrDefault();
public PlayerData? PlayerData => Player?.Data.ContentData();
public PoolTestLogHandler ServerLogHandler { get; private set; } = default!;
public PoolTestLogHandler ClientLogHandler { get; private set; } = default!;

View File

@@ -1,9 +1,12 @@
using JetBrains.Annotations;
namespace Content.IntegrationTests;
/// <summary>
/// Attribute that indicates that a string contains yaml prototype data that should be loaded by integration tests.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
[MeansImplicitUse]
public sealed class TestPrototypesAttribute : Attribute
{
}

View File

@@ -29,6 +29,7 @@ namespace Content.IntegrationTests.Tests.Buckle
components:
- type: Buckle
- type: Hands
- type: InputMover
- type: Body
prototype: Human
- type: StandingState

View File

@@ -1,8 +1,8 @@
#nullable enable
using Content.IntegrationTests.Tests.Interaction;
using Content.Server.Climbing;
using Content.Shared.Climbing;
using Robust.Shared.Maths;
using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
using ClimbSystem = Content.Shared.Climbing.Systems.ClimbSystem;
namespace Content.IntegrationTests.Tests.Climbing;

View File

@@ -0,0 +1,71 @@
using System.Collections.Generic;
using System.IO;
using Content.Server.Entry;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
namespace Content.IntegrationTests.Tests;
[TestFixture]
public sealed class ConfigPresetTests
{
[Test]
public async Task TestLoadAll()
{
var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var resources = server.ResolveDependency<IResourceManager>();
var config = server.ResolveDependency<IConfigurationManager>();
await server.WaitPost(() =>
{
var originalCVars = new List<(string, object)>();
foreach (var cvar in config.GetRegisteredCVars())
{
var value = config.GetCVar<object>(cvar);
originalCVars.Add((cvar, value));
}
var originalCvarsStream = new MemoryStream();
config.SaveToTomlStream(originalCvarsStream, config.GetRegisteredCVars());
originalCvarsStream.Position = 0;
var presets = resources.ContentFindFiles(EntryPoint.ConfigPresetsDir);
Assert.Multiple(() =>
{
foreach (var preset in presets)
{
var stream = resources.ContentFileRead(preset);
Assert.DoesNotThrow(() => config.LoadDefaultsFromTomlStream(stream));
}
});
config.LoadDefaultsFromTomlStream(originalCvarsStream);
foreach (var originalCVar in originalCVars)
{
var (name, originalValue) = originalCVar;
var newValue = config.GetCVar<object>(name);
var originalValueType = originalValue.GetType();
var newValueType = newValue.GetType();
if (originalValueType.IsEnum || newValueType.IsEnum)
{
originalValue = Enum.ToObject(originalValueType, originalValue);
newValue = Enum.ToObject(originalValueType, newValue);
}
if (originalValueType == typeof(float) || newValueType == typeof(float))
{
originalValue = Convert.ToSingle(originalValue);
newValue = Convert.ToSingle(newValue);
}
if (!Equals(newValue, originalValue))
Assert.Fail($"CVar {name} was not reset to its original value.");
}
});
await pair.CleanReturnAsync();
}
}

View File

@@ -63,11 +63,11 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
await server.WaitAssertion(() =>
{
user = sEntities.SpawnEntity(null, coords);
user.EnsureComponent<HandsComponent>();
sEntities.EnsureComponent<HandsComponent>(user);
handSys.AddHand(user, "hand", HandLocation.Left);
target = sEntities.SpawnEntity(null, coords);
item = sEntities.SpawnEntity(null, coords);
item.EnsureComponent<ItemComponent>();
sEntities.EnsureComponent<ItemComponent>(item);
});
await server.WaitRunTicks(1);
@@ -134,11 +134,11 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
await server.WaitAssertion(() =>
{
user = sEntities.SpawnEntity(null, coords);
user.EnsureComponent<HandsComponent>();
sEntities.EnsureComponent<HandsComponent>(user);
handSys.AddHand(user, "hand", HandLocation.Left);
target = sEntities.SpawnEntity(null, new MapCoordinates(new Vector2(1.9f, 0), mapId));
item = sEntities.SpawnEntity(null, coords);
item.EnsureComponent<ItemComponent>();
sEntities.EnsureComponent<ItemComponent>(item);
wall = sEntities.SpawnEntity("DummyDebugWall", new MapCoordinates(new Vector2(1, 0), sEntities.GetComponent<TransformComponent>(user).MapID));
});
@@ -204,11 +204,11 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
await server.WaitAssertion(() =>
{
user = sEntities.SpawnEntity(null, coords);
user.EnsureComponent<HandsComponent>();
sEntities.EnsureComponent<HandsComponent>(user);
handSys.AddHand(user, "hand", HandLocation.Left);
target = sEntities.SpawnEntity(null, new MapCoordinates(new Vector2(SharedInteractionSystem.InteractionRange - 0.1f, 0), mapId));
item = sEntities.SpawnEntity(null, coords);
item.EnsureComponent<ItemComponent>();
sEntities.EnsureComponent<ItemComponent>(item);
});
await server.WaitRunTicks(1);
@@ -274,11 +274,11 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
await server.WaitAssertion(() =>
{
user = sEntities.SpawnEntity(null, coords);
user.EnsureComponent<HandsComponent>();
sEntities.EnsureComponent<HandsComponent>(user);
handSys.AddHand(user, "hand", HandLocation.Left);
target = sEntities.SpawnEntity(null, new MapCoordinates(new Vector2(SharedInteractionSystem.InteractionRange + 0.01f, 0), mapId));
item = sEntities.SpawnEntity(null, coords);
item.EnsureComponent<ItemComponent>();
sEntities.EnsureComponent<ItemComponent>(item);
});
await server.WaitRunTicks(1);
@@ -346,11 +346,11 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
await server.WaitAssertion(() =>
{
user = sEntities.SpawnEntity(null, coords);
user.EnsureComponent<HandsComponent>();
sEntities.EnsureComponent<HandsComponent>(user);
handSys.AddHand(user, "hand", HandLocation.Left);
target = sEntities.SpawnEntity(null, coords);
item = sEntities.SpawnEntity(null, coords);
item.EnsureComponent<ItemComponent>();
sEntities.EnsureComponent<ItemComponent>(item);
containerEntity = sEntities.SpawnEntity(null, coords);
container = conSystem.EnsureContainer<Container>(containerEntity, "InteractionTestContainer");
});

View File

@@ -0,0 +1,55 @@
#nullable enable
using Robust.Shared.Console;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Minds;
[TestFixture]
public sealed partial class MindTests
{
[Test]
public async Task DeleteAllThenGhost()
{
var settings = new PoolSettings
{
Dirty = true,
DummyTicker = false,
Connected = true
};
await using var pair = await PoolManager.GetServerClient(settings);
// Client is connected with a valid entity & mind
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
// Delete **everything**
var conHost = pair.Server.ResolveDependency<IConsoleHost>();
await pair.Server.WaitPost(() => conHost.ExecuteCommand("entities delete"));
await pair.RunTicksSync(5);
Assert.That(pair.Server.EntMan.EntityCount, Is.EqualTo(0));
Assert.That(pair.Client.EntMan.EntityCount, Is.EqualTo(0));
// Create a new map.
int mapId = 1;
await pair.Server.WaitPost(() => conHost.ExecuteCommand($"addmap {mapId}"));
await pair.RunTicksSync(5);
// Client is not attached to anything
Assert.Null(pair.Client.Player?.ControlledEntity);
Assert.Null(pair.PlayerData?.Mind);
// Attempt to ghost
var cConHost = pair.Client.ResolveDependency<IConsoleHost>();
await pair.Client.WaitPost(() => cConHost.ExecuteCommand("ghost"));
await pair.RunTicksSync(10);
// Client should be attached to a ghost placed on the new map.
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
var xform = pair.Client.Transform(pair.Client.Player!.ControlledEntity!.Value);
Assert.That(xform.MapID, Is.EqualTo(new MapId(mapId)));
await pair.CleanReturnAsync();
}
}

View File

@@ -1,18 +1,17 @@
#nullable enable
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Content.Server.Storage.Components;
using Content.Server.VendingMachines;
using Content.Server.Wires;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.VendingMachines;
using Content.Shared.Wires;
using Content.Server.Wires;
using Content.Shared.Prototypes;
using Content.Shared.Storage.Components;
using Content.Shared.VendingMachines;
using Content.Shared.Wires;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests
{
@@ -96,7 +95,7 @@ namespace Content.IntegrationTests.Tests
name: Test Ramen
components:
- type: Wires
LayoutId: Vending
layoutId: Vending
- type: VendingMachine
pack: TestInventory
- type: Sprite

View File

@@ -0,0 +1,47 @@
using Content.Server.Anomaly.Effects;
using Robust.Shared.Prototypes;
namespace Content.Server.Anomaly.Components;
/// <summary>
/// This component allows the anomaly to inject liquid from the SolutionContainer
/// into the surrounding entities with the InjectionSolution component
/// </summary>
[RegisterComponent, Access(typeof(InjectionAnomalySystem))]
public sealed partial class InjectionAnomalyComponent : Component
{
/// <summary>
/// the maximum amount of injection of a substance into an entity per pulsation
/// scales with Severity
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float MaxSolutionInjection = 15;
/// <summary>
/// the maximum amount of injection of a substance into an entity in the supercritical phase
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float SuperCriticalSolutionInjection = 50;
/// <summary>
/// The maximum radius in which the anomaly injects reagents into the surrounding containers.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float InjectRadius = 3;
/// <summary>
/// The maximum radius in which the anomaly injects reagents into the surrounding containers.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float SuperCriticalInjectRadius = 15;
/// <summary>
/// The name of the prototype of the special effect that appears above the entities into which the injection was carried out
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public EntProtoId VisualEffectPrototype = "PuddleSparkle";
/// <summary>
/// Solution name that can be drained.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public string Solution { get; set; } = "default";
}

View File

@@ -0,0 +1,29 @@
using Content.Server.Anomaly.Effects;
namespace Content.Server.Anomaly.Components;
/// <summary>
/// This component allows the anomaly to create puddles from the solutionContainer
/// </summary>
[RegisterComponent, Access(typeof(PuddleCreateAnomalySystem))]
public sealed partial class PuddleCreateAnomalyComponent : Component
{
/// <summary>
/// The maximum amount of solution that an anomaly can splash out of the storage on the floor during pulsation.
/// Scales with Severity.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float MaxPuddleSize = 100;
/// <summary>
/// The maximum amount of solution that an anomaly can splash out of the storage on the floor during supercritical event
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float SuperCriticalPuddleSize = 1000;
/// <summary>
/// Solution name that can be drained.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public string Solution { get; set; } = "default";
}

View File

@@ -0,0 +1,94 @@
using Content.Server.Anomaly.Effects;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using System.Numerics;
namespace Content.Server.Anomaly.Components;
/// <summary>
/// This component allows the anomaly to generate a random type of reagent in the specified SolutionContainer.
/// With the increasing severity of the anomaly, the type of reagent produced may change.
/// The higher the severity of the anomaly, the higher the chance of dangerous or useful reagents.
/// </summary>
[RegisterComponent, Access(typeof(ReagentProducerAnomalySystem))]
public sealed partial class ReagentProducerAnomalyComponent : Component
{
//the addition of the reagent will occur instantly when an anomaly appears,
//and there will not be the first three seconds of a white empty anomaly.
public float AccumulatedFrametime = 3.0f;
/// <summary>
/// How frequently should this reagent generation update, in seconds?
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float UpdateInterval = 3.0f;
/// <summary>
/// The spread of the random weight of the choice of this category, depending on the severity.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public Vector2 WeightSpreadDangerous = new(5.0f, 9.0f);
/// <summary>
/// The spread of the random weight of the choice of this category, depending on the severity.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public Vector2 WeightSpreadFun = new(3.0f, 0.0f);
/// <summary>
/// The spread of the random weight of the choice of this category, depending on the severity.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public Vector2 WeightSpreadUseful = new(1.0f, 1.0f);
/// <summary>
/// Category of dangerous reagents for injection. Various toxins and poisons
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public List<ProtoId<ReagentPrototype>> DangerousChemicals = new();
/// <summary>
/// Category of useful reagents for injection. Medicine and other things that players WANT to get
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public List<ProtoId<ReagentPrototype>> UsefulChemicals = new();
/// <summary>
/// Category of fun reagents for injection. Glue, drugs, beer. Something that will bring fun.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public List<ProtoId<ReagentPrototype>> FunChemicals = new();
/// <summary>
/// Noise made when anomaly pulse.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier ChangeSound = new SoundPathSpecifier("/Audio/Effects/waterswirl.ogg");
/// <summary>
/// The component will repaint the sprites of the object to match the current color of the solution,
/// if the RandomSprite component is hung correctly.
/// Ideally, this should be put into a separate component, but I suffered for 4 hours,
/// and nothing worked out for me. So for now it will be like this.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public bool NeedRecolor = false;
/// <summary>
/// the maximum amount of reagent produced per second
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float MaxReagentProducing = 1.5f;
/// <summary>
/// how much does the reagent production increase before entering the supercritical state
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float SupercriticalReagentProducingModifier = 100f;
/// <summary>
/// The name of the reagent that the anomaly produces.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public ProtoId<ReagentPrototype> ProducingReagent = "Water";
/// <summary>
/// Solution name where the substance is generated
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("solution")]
public string Solution = "default";
}

View File

@@ -48,7 +48,7 @@ public sealed class ElectricityAnomalySystem : EntitySystem
if (mobQuery.HasComponent(ent))
validEnts.Add(ent);
if (_random.Prob(0.2f) && poweredQuery.HasComponent(ent))
if (_random.Prob(0.01f) && poweredQuery.HasComponent(ent))
validEnts.Add(ent);
}

View File

@@ -1,13 +1,12 @@
using System.Linq;
using System.Linq;
using System.Numerics;
using Content.Server.Maps;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects.Components;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Anomaly.Effects;
@@ -39,11 +38,10 @@ public sealed class EntityAnomalySystem : EntitySystem
// A cluster of monsters
SpawnMonstersOnOpenTiles(component, xform, component.MaxSpawnAmount, component.SpawnRange, component.Spawns);
// And so much meat (for the meat anomaly at least)
Spawn(component.SupercriticalSpawn, xform.Coordinates);
SpawnMonstersOnOpenTiles(component, xform, component.MaxSpawnAmount, component.SpawnRange, new List<string>(){component.SupercriticalSpawn});
SpawnMonstersOnOpenTiles(component, xform, component.MaxSpawnAmount, component.SpawnRange, component.SuperCriticalSpawns);
}
private void SpawnMonstersOnOpenTiles(EntitySpawnAnomalyComponent component, TransformComponent xform, int amount, float radius, List<string> spawns)
private void SpawnMonstersOnOpenTiles(EntitySpawnAnomalyComponent component, TransformComponent xform, int amount, float radius, List<EntProtoId> spawns)
{
if (!component.Spawns.Any())
return;
@@ -68,10 +66,12 @@ public sealed class EntityAnomalySystem : EntitySystem
{
if (!physQuery.TryGetComponent(ent, out var body))
continue;
if (body.BodyType != BodyType.Static ||
!body.Hard ||
(body.CollisionLayer & (int) CollisionGroup.Impassable) == 0)
continue;
valid = false;
break;
}

View File

@@ -0,0 +1,67 @@
using System.Linq;
using Content.Server.Anomaly.Components;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Anomaly.Components;
namespace Content.Server.Anomaly.Effects;
/// <summary>
/// This component allows the anomaly to inject liquid from the SolutionContainer
/// into the surrounding entities with the InjectionSolution component
/// </summary>
///
/// <see cref="InjectionAnomalyComponent"/>
public sealed class InjectionAnomalySystem : EntitySystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
private EntityQuery<InjectableSolutionComponent> _injectableQuery;
public override void Initialize()
{
SubscribeLocalEvent<InjectionAnomalyComponent, AnomalyPulseEvent>(OnPulse);
SubscribeLocalEvent<InjectionAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical, before: new[] { typeof(SolutionContainerSystem) });
_injectableQuery = GetEntityQuery<InjectableSolutionComponent>();
}
private void OnPulse(EntityUid uid, InjectionAnomalyComponent component, ref AnomalyPulseEvent args)
{
PulseScalableEffect(uid, component, component.InjectRadius, component.MaxSolutionInjection * args.Severity);
}
private void OnSupercritical(EntityUid uid, InjectionAnomalyComponent component, ref AnomalySupercriticalEvent args)
{
PulseScalableEffect(uid, component, component.SuperCriticalInjectRadius, component.SuperCriticalSolutionInjection);
}
private void PulseScalableEffect(EntityUid uid, InjectionAnomalyComponent component, float injectRadius, float maxInject)
{
if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var sol))
return;
//We get all the entity in the radius into which the reagent will be injected.
var xformQuery = GetEntityQuery<TransformComponent>();
var xform = xformQuery.GetComponent(uid);
var allEnts = _lookup.GetComponentsInRange<InjectableSolutionComponent>(xform.MapPosition, injectRadius)
.Select(x => x.Owner).ToList();
//for each matching entity found
foreach (var ent in allEnts)
{
if (!_solutionContainer.TryGetInjectableSolution(ent, out var injectable))
continue;
if (_injectableQuery.TryGetComponent(ent, out var injEnt))
{
var buffer = sol;
_solutionContainer.TryTransferSolution(ent, injectable, buffer, maxInject);
//Spawn Effect
var uidXform = Transform(ent);
Spawn(component.VisualEffectPrototype, uidXform.Coordinates);
}
}
}
}

View File

@@ -0,0 +1,39 @@
using Content.Server.Anomaly.Components;
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Anomaly.Components;
using Content.Server.Fluids.EntitySystems;
namespace Content.Server.Anomaly.Effects;
/// <summary>
/// This component allows the anomaly to create puddles from SolutionContainer.
/// </summary>
public sealed class PuddleCreateAnomalySystem : EntitySystem
{
[Dependency] private readonly PuddleSystem _puddle = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
public override void Initialize()
{
SubscribeLocalEvent<PuddleCreateAnomalyComponent, AnomalyPulseEvent>(OnPulse);
SubscribeLocalEvent<PuddleCreateAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical, before: new[] { typeof(InjectionAnomalySystem) });
}
private void OnPulse(EntityUid uid, PuddleCreateAnomalyComponent component, ref AnomalyPulseEvent args)
{
if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var sol))
return;
var xform = Transform(uid);
var puddleSol = _solutionContainer.SplitSolution(uid, sol, component.MaxPuddleSize * args.Severity);
_puddle.TrySplashSpillAt(uid, xform.Coordinates, puddleSol, out _);
}
private void OnSupercritical(EntityUid uid, PuddleCreateAnomalyComponent component, ref AnomalySupercriticalEvent args)
{
if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var sol))
return;
var buffer = sol;
var xform = Transform(uid);
_puddle.TrySpillAt(xform.Coordinates, buffer, out _);
}
}

View File

@@ -0,0 +1,152 @@
using Content.Server.Anomaly.Components;
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Anomaly.Components;
using Robust.Shared.Random;
using Content.Shared.Chemistry.Components;
using Robust.Shared.Prototypes;
using Content.Shared.Sprite;
using Robust.Server.GameObjects;
namespace Content.Server.Anomaly.Effects;
/// <see cref="ReagentProducerAnomalyComponent"/>
public sealed class ReagentProducerAnomalySystem : EntitySystem
{
//The idea is to divide substances into several categories.
//The anomaly will choose one of the categories with a given chance based on severity.
//Then a random substance will be selected from the selected category.
//There are the following categories:
//Dangerous:
//selected most often. A list of substances that are extremely unpleasant for injection.
//Fun:
//Funny things have an increased chance of appearing in an anomaly.
//Useful:
//Those reagents that the players are hunting for. Very low percentage of loss.
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PointLightSystem _light = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
public const string FallbackReagent = "Water";
public override void Initialize()
{
SubscribeLocalEvent<ReagentProducerAnomalyComponent, AnomalyPulseEvent>(OnPulse);
SubscribeLocalEvent<ReagentProducerAnomalyComponent, MapInitEvent>(OnMapInit);
}
private void OnPulse(EntityUid uid, ReagentProducerAnomalyComponent component, ref AnomalyPulseEvent args)
{
if (_random.NextFloat(0.0f, 1.0f) > args.Stability)
ChangeReagent(uid, component, args.Severity);
}
private void ChangeReagent(EntityUid uid, ReagentProducerAnomalyComponent component, float severity)
{
var reagent = GetRandomReagentType(uid, component, severity);
component.ProducingReagent = reagent;
_audio.PlayPvs(component.ChangeSound, uid);
}
//reagent realtime generation
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<ReagentProducerAnomalyComponent, AnomalyComponent>();
while (query.MoveNext(out var uid, out var component, out var anomaly))
{
component.AccumulatedFrametime += frameTime;
if (component.AccumulatedFrametime < component.UpdateInterval)
continue;
if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var producerSol))
continue;
Solution newSol = new();
var reagentProducingAmount = anomaly.Stability * component.MaxReagentProducing * component.AccumulatedFrametime;
if (anomaly.Severity >= 0.97) reagentProducingAmount *= component.SupercriticalReagentProducingModifier;
newSol.AddReagent(component.ProducingReagent, reagentProducingAmount);
_solutionContainer.TryAddSolution(uid, producerSol, newSol); //TO DO - the container is not fully filled.
component.AccumulatedFrametime = 0;
// The component will repaint the sprites of the object to match the current color of the solution,
// if the RandomSprite component is hung correctly.
// Ideally, this should be put into a separate component, but I suffered for 4 hours,
// and nothing worked out for me. So for now it will be like this.
if (component.NeedRecolor)
{
var color = producerSol.GetColor(_prototypeManager);
_light.SetColor(uid, color);
if (TryComp<RandomSpriteComponent>(uid, out var randomSprite))
{
foreach (var ent in randomSprite.Selected)
{
var state = randomSprite.Selected[ent.Key];
state.Color = color;
randomSprite.Selected[ent.Key] = state;
}
Dirty(uid, randomSprite);
}
}
}
}
private void OnMapInit(EntityUid uid, ReagentProducerAnomalyComponent component, MapInitEvent args)
{
ChangeReagent(uid, component, 0.1f); //MapInit Reagent 100% change
}
// returns a random reagent based on a system of random weights.
// First, the category is selected: The category has a minimum and maximum weight,
// the current value depends on severity.
// Accordingly, with the strengthening of the anomaly,
// the chances of falling out of some categories grow, and some fall.
//
// After that, a random reagent in the selected category is selected.
//
// Such a system is made to control the danger and interest of the anomaly more.
private string GetRandomReagentType(EntityUid uid, ReagentProducerAnomalyComponent component, float severity)
{
//Category Weight Randomization
var currentWeightDangerous = MathHelper.Lerp(component.WeightSpreadDangerous.X, component.WeightSpreadDangerous.Y, severity);
var currentWeightFun = MathHelper.Lerp(component.WeightSpreadFun.X, component.WeightSpreadFun.Y, severity);
var currentWeightUseful = MathHelper.Lerp(component.WeightSpreadUseful.X, component.WeightSpreadUseful.Y, severity);
var sumWeight = currentWeightDangerous + currentWeightFun + currentWeightUseful;
var rnd = _random.NextFloat(0f, sumWeight);
//Dangerous
if (rnd <= currentWeightDangerous && component.DangerousChemicals.Count > 0)
{
var reagent = _random.Pick(component.DangerousChemicals);
return reagent;
}
else rnd -= currentWeightDangerous;
//Fun
if (rnd <= currentWeightFun && component.FunChemicals.Count > 0)
{
var reagent = _random.Pick(component.FunChemicals);
return reagent;
}
else rnd -= currentWeightFun;
//Useful
if (rnd <= currentWeightUseful && component.UsefulChemicals.Count > 0)
{
var reagent = _random.Pick(component.UsefulChemicals);
return reagent;
}
//We should never end up here.
//Maybe Log Error?
return FallbackReagent;
}
}

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Numerics;
using Content.Server.Maps;
using Content.Shared.Anomaly.Components;
@@ -38,7 +38,7 @@ public sealed class TileAnomalySystem : EntitySystem
new Box2(localpos + new Vector2(-radius, -radius), localpos + new Vector2(radius, radius)));
foreach (var tileref in tilerefs)
{
if (!_random.Prob(0.33f))
if (!_random.Prob(component.SpawnChance))
continue;
_tile.ReplaceTile(tileref, fleshTile);
}

View File

@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Content.Tests")]
[assembly: InternalsVisibleTo("Content.IntegrationTests")]

View File

@@ -44,6 +44,7 @@ namespace Content.Server.Bed
AddComp<HealOnBuckleHealingComponent>(uid);
component.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(component.HealTime);
_actionsSystem.AddAction(args.BuckledEntity, ref component.SleepAction, SleepingSystem.SleepActionId, uid);
Dirty(uid, component);
return;
}

View File

@@ -1,6 +1,6 @@
using Content.Server.Actions;
using Content.Server.Popups;
using Content.Server.Sound.Components;
using Content.Shared.Actions;
using Content.Shared.Audio;
using Content.Shared.Bed.Sleep;
using Content.Shared.Damage;
@@ -36,6 +36,7 @@ namespace Content.Server.Bed.Sleep
SubscribeLocalEvent<MobStateComponent, SleepStateChangedEvent>(OnSleepStateChanged);
SubscribeLocalEvent<SleepingComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<MobStateComponent, SleepActionEvent>(OnSleepAction);
SubscribeLocalEvent<ActionsContainerComponent, SleepActionEvent>(OnBedSleepAction);
SubscribeLocalEvent<MobStateComponent, WakeActionEvent>(OnWakeAction);
SubscribeLocalEvent<SleepingComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<SleepingComponent, GetVerbsEvent<AlternativeVerb>>(AddWakeVerb);
@@ -93,6 +94,11 @@ namespace Content.Server.Bed.Sleep
TrySleeping(uid);
}
private void OnBedSleepAction(EntityUid uid, ActionsContainerComponent component, SleepActionEvent args)
{
TrySleeping(args.Performer);
}
private void OnWakeAction(EntityUid uid, MobStateComponent component, WakeActionEvent args)
{
if (!TryWakeCooldown(uid))

View File

@@ -133,7 +133,7 @@ namespace Content.Server.Bible
var damage = _damageableSystem.TryChangeDamage(args.Target.Value, component.Damage, true, origin: uid);
if (damage == null || damage.Total == 0)
if (damage == null || damage.Empty)
{
var othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-others", ("user", Identity.Entity(args.User, EntityManager)),("target", Identity.Entity(args.Target.Value, EntityManager)),("bible", uid));
_popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium);

View File

@@ -2,17 +2,13 @@ using Content.Server.Body.Components;
using Content.Server.Ghost.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Body.Organ;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
namespace Content.Server.Body.Systems
{
public sealed class BrainSystem : EntitySystem
{
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
public override void Initialize()
@@ -20,11 +16,14 @@ namespace Content.Server.Body.Systems
base.Initialize();
SubscribeLocalEvent<BrainComponent, AddedToPartInBodyEvent>((uid, _, args) => HandleMind(args.Body, uid));
SubscribeLocalEvent<BrainComponent, RemovedFromPartInBodyEvent>((uid, _, args) => HandleMind(args.OldBody, uid));
SubscribeLocalEvent<BrainComponent, RemovedFromPartInBodyEvent>((uid, _, args) => HandleMind(uid, args.OldBody));
}
private void HandleMind(EntityUid newEntity, EntityUid oldEntity)
{
if (TerminatingOrDeleted(newEntity) || TerminatingOrDeleted(oldEntity))
return;
EnsureComp<MindContainerComponent>(newEntity);
EnsureComp<MindContainerComponent>(oldEntity);
@@ -32,16 +31,6 @@ namespace Content.Server.Body.Systems
if (HasComp<BodyComponent>(newEntity))
ghostOnMove.MustBeDead = true;
// TODO: This is an awful solution.
// Our greatest minds still can't figure out how to allow brains/heads to ghost without giving them the
// ability to move first. I hate this with a passion.
if (!HasComp<InputMoverComponent>(newEntity))
{
AddComp<InputMoverComponent>(newEntity);
var move = EnsureComp<MovementSpeedModifierComponent>(newEntity);
_movementSpeed.ChangeBaseSpeed(newEntity, 0, 0 , 0, move);
}
if (!_mindSystem.TryGetMind(oldEntity, out var mindId, out var mind))
return;

View File

@@ -37,7 +37,7 @@ public sealed class MutationSystem : EntitySystem
}
// Add up everything in the bits column and put the number here.
const int totalbits = 265;
const int totalbits = 270;
// Tolerances (55)
MutateFloat(ref seed.NutrientConsumption , 0.05f, 1.2f, 5, totalbits, severity);
@@ -69,8 +69,7 @@ public sealed class MutationSystem : EntitySystem
MutateBool(ref seed.Sentient , true , 10, totalbits, severity);
MutateBool(ref seed.Ligneous , true , 10, totalbits, severity);
MutateBool(ref seed.Bioluminescent, true , 10, totalbits, severity);
// Kudzu disabled until superkudzu bug is fixed
// MutateBool(ref seed.TurnIntoKudzu , true , 10, totalbits, severity);
MutateBool(ref seed.TurnIntoKudzu , true , 10, totalbits, severity);
MutateBool(ref seed.CanScream , true , 10, totalbits, severity);
seed.BioluminescentColor = RandomColor(seed.BioluminescentColor, 10, totalbits, severity);
@@ -119,7 +118,7 @@ public sealed class MutationSystem : EntitySystem
CrossBool(ref result.Sentient, a.Sentient);
CrossBool(ref result.Ligneous, a.Ligneous);
CrossBool(ref result.Bioluminescent, a.Bioluminescent);
// CrossBool(ref result.TurnIntoKudzu, a.TurnIntoKudzu);
CrossBool(ref result.TurnIntoKudzu, a.TurnIntoKudzu);
CrossBool(ref result.CanScream, a.CanScream);
CrossGasses(ref result.ExudeGasses, a.ExudeGasses);

View File

@@ -1,16 +0,0 @@
using Content.Server.Fluids.EntitySystems;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Chemistry.Components;
/// <summary>
/// When a <see cref="SmokeComponent"/> despawns this will spawn another entity in its place.
/// </summary>
[RegisterComponent, Access(typeof(SmokeSystem))]
public sealed partial class SmokeDissipateSpawnComponent : Component
{
[DataField("prototype", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Prototype = string.Empty;
}

View File

@@ -7,24 +7,24 @@ public sealed partial class SolutionSpikerComponent : Component
/// The source solution to take the reagents from in order
/// to spike the other solution container.
/// </summary>
[DataField("sourceSolution")]
[DataField]
public string SourceSolution { get; private set; } = string.Empty;
/// <summary>
/// If spiking with this entity should ignore empty containers or not.
/// </summary>
[DataField("ignoreEmpty")]
[DataField]
public bool IgnoreEmpty { get; private set; }
/// <summary>
/// What should pop up when spiking with this entity.
/// </summary>
[DataField("popup")]
public string Popup { get; private set; } = "spike-solution-generic";
[DataField]
public LocId Popup { get; private set; } = "spike-solution-generic";
/// <summary>
/// What should pop up when spiking fails because the container was empty.
/// </summary>
[DataField("popupEmpty")]
public string PopupEmpty { get; private set; } = "spike-solution-empty-generic";
[DataField]
public LocId PopupEmpty { get; private set; } = "spike-solution-empty-generic";
}

View File

@@ -22,16 +22,9 @@ namespace Content.Server.Chemistry.EntitySystems
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SolutionInjectOnCollideComponent, ComponentInit>(HandleInit);
SubscribeLocalEvent<SolutionInjectOnCollideComponent, StartCollideEvent>(HandleInjection);
}
private void HandleInit(EntityUid uid, SolutionInjectOnCollideComponent component, ComponentInit args)
{
component.Owner
.EnsureComponentWarn<SolutionContainerManagerComponent>($"{nameof(SolutionInjectOnCollideComponent)} requires a SolutionContainerManager on {component.Owner}!");
}
private void HandleInjection(EntityUid uid, SolutionInjectOnCollideComponent component, ref StartCollideEvent args)
{
var target = args.OtherEntity;

View File

@@ -1,476 +0,0 @@
using System.Numerics;
using Content.Server.Body.Systems;
using Content.Server.Climbing.Components;
using Content.Server.Interaction;
using Content.Server.Popups;
using Content.Server.Stunnable;
using Content.Shared.ActionBlocker;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Buckle.Components;
using Content.Shared.Climbing;
using Content.Shared.Climbing.Events;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
namespace Content.Server.Climbing;
[UsedImplicitly]
public sealed class ClimbSystem : SharedClimbSystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly BodySystem _bodySystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly FixtureSystem _fixtureSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly InteractionSystem _interactionSystem = default!;
[Dependency] private readonly StunSystem _stunSystem = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
private const string ClimbingFixtureName = "climb";
private const int ClimbingCollisionGroup = (int) (CollisionGroup.TableLayer | CollisionGroup.LowImpassable);
private readonly Dictionary<EntityUid, Dictionary<string, Fixture>> _fixtureRemoveQueue = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<ClimbableComponent, GetVerbsEvent<AlternativeVerb>>(AddClimbableVerb);
SubscribeLocalEvent<ClimbableComponent, DragDropTargetEvent>(OnClimbableDragDrop);
SubscribeLocalEvent<ClimbingComponent, ClimbDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<ClimbingComponent, EndCollideEvent>(OnClimbEndCollide);
SubscribeLocalEvent<ClimbingComponent, BuckleChangeEvent>(OnBuckleChange);
SubscribeLocalEvent<GlassTableComponent, ClimbedOnEvent>(OnGlassClimbed);
}
protected override void OnCanDragDropOn(EntityUid uid, ClimbableComponent component, ref CanDropTargetEvent args)
{
base.OnCanDragDropOn(uid, component, ref args);
if (!args.CanDrop)
return;
string reason;
var canVault = args.User == args.Dragged
? CanVault(component, args.User, uid, out reason)
: CanVault(component, args.User, args.Dragged, uid, out reason);
if (!canVault)
_popupSystem.PopupEntity(reason, args.User, args.User);
args.CanDrop = canVault;
args.Handled = true;
}
private void AddClimbableVerb(EntityUid uid, ClimbableComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract || !_actionBlockerSystem.CanMove(args.User))
return;
if (!TryComp(args.User, out ClimbingComponent? climbingComponent) || climbingComponent.IsClimbing)
return;
// TODO VERBS ICON add a climbing icon?
args.Verbs.Add(new AlternativeVerb
{
Act = () => TryClimb(args.User, args.User, args.Target, out _, component),
Text = Loc.GetString("comp-climbable-verb-climb")
});
}
private void OnClimbableDragDrop(EntityUid uid, ClimbableComponent component, ref DragDropTargetEvent args)
{
// definitely a better way to check if two entities are equal
// but don't have computer access and i have to do this without syntax
if (args.Handled || args.User != args.Dragged && !HasComp<HandsComponent>(args.User))
return;
TryClimb(args.User, args.Dragged, uid, out _, component);
}
public bool TryClimb(EntityUid user,
EntityUid entityToMove,
EntityUid climbable,
out DoAfterId? id,
ClimbableComponent? comp = null,
ClimbingComponent? climbing = null)
{
id = null;
if (!Resolve(climbable, ref comp) || !Resolve(entityToMove, ref climbing))
return false;
// Note, IsClimbing does not mean a DoAfter is active, it means the target has already finished a DoAfter and
// is currently on top of something..
if (climbing.IsClimbing)
return true;
var args = new DoAfterArgs(EntityManager, user, comp.ClimbDelay, new ClimbDoAfterEvent(), entityToMove, target: climbable, used: entityToMove)
{
BreakOnTargetMove = true,
BreakOnUserMove = true,
BreakOnDamage = true
};
_audio.PlayPvs(comp.StartClimbSound, climbable);
_doAfterSystem.TryStartDoAfter(args, out id);
return true;
}
private void OnDoAfter(EntityUid uid, ClimbingComponent component, ClimbDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Target == null || args.Args.Used == null)
return;
Climb(uid, args.Args.User, args.Args.Used.Value, args.Args.Target.Value, climbing: component);
args.Handled = true;
}
private void Climb(EntityUid uid, EntityUid user, EntityUid instigator, EntityUid climbable, bool silent = false, ClimbingComponent? climbing = null,
PhysicsComponent? physics = null, FixturesComponent? fixtures = null, ClimbableComponent? comp = null)
{
if (!Resolve(uid, ref climbing, ref physics, ref fixtures, false))
return;
if (!Resolve(climbable, ref comp))
return;
if (!ReplaceFixtures(climbing, fixtures))
return;
climbing.IsClimbing = true;
Dirty(climbing);
_audio.PlayPvs(comp.FinishClimbSound, climbable);
MoveEntityToward(uid, climbable, physics, climbing);
// we may potentially need additional logic since we're forcing a player onto a climbable
// there's also the cases where the user might collide with the person they are forcing onto the climbable that i haven't accounted for
RaiseLocalEvent(uid, new StartClimbEvent(climbable), false);
RaiseLocalEvent(climbable, new ClimbedOnEvent(uid, user), false);
if (silent)
return;
if (user == uid)
{
var othersMessage = Loc.GetString("comp-climbable-user-climbs-other", ("user", Identity.Entity(uid, EntityManager)),
("climbable", climbable));
uid.PopupMessageOtherClients(othersMessage);
var selfMessage = Loc.GetString("comp-climbable-user-climbs", ("climbable", climbable));
uid.PopupMessage(selfMessage);
}
else
{
var othersMessage = Loc.GetString("comp-climbable-user-climbs-force-other", ("user", Identity.Entity(user, EntityManager)),
("moved-user", Identity.Entity(uid, EntityManager)), ("climbable", climbable));
user.PopupMessageOtherClients(othersMessage);
var selfMessage = Loc.GetString("comp-climbable-user-climbs-force", ("moved-user", Identity.Entity(uid, EntityManager)),
("climbable", climbable));
user.PopupMessage(selfMessage);
}
}
/// <summary>
/// Replaces the current fixtures with non-climbing collidable versions so that climb end can be detected
/// </summary>
/// <returns>Returns whether adding the new fixtures was successful</returns>
private bool ReplaceFixtures(ClimbingComponent climbingComp, FixturesComponent fixturesComp)
{
var uid = climbingComp.Owner;
// Swap fixtures
foreach (var (name, fixture) in fixturesComp.Fixtures)
{
if (climbingComp.DisabledFixtureMasks.ContainsKey(name)
|| fixture.Hard == false
|| (fixture.CollisionMask & ClimbingCollisionGroup) == 0)
continue;
climbingComp.DisabledFixtureMasks.Add(name, fixture.CollisionMask & ClimbingCollisionGroup);
_physics.SetCollisionMask(uid, name, fixture, fixture.CollisionMask & ~ClimbingCollisionGroup, fixturesComp);
}
if (!_fixtureSystem.TryCreateFixture(
uid,
new PhysShapeCircle(0.35f),
ClimbingFixtureName,
collisionLayer: (int) CollisionGroup.None,
collisionMask: ClimbingCollisionGroup,
hard: false,
manager: fixturesComp))
{
return false;
}
return true;
}
private void OnClimbEndCollide(EntityUid uid, ClimbingComponent component, ref EndCollideEvent args)
{
if (args.OurFixtureId != ClimbingFixtureName
|| !component.IsClimbing
|| component.OwnerIsTransitioning)
return;
foreach (var fixture in args.OurFixture.Contacts.Keys)
{
if (fixture == args.OtherFixture)
continue;
// If still colliding with a climbable, do not stop climbing
if (HasComp<ClimbableComponent>(args.OtherEntity))
return;
}
StopClimb(uid, component);
}
private void StopClimb(EntityUid uid, ClimbingComponent? climbing = null, FixturesComponent? fixtures = null)
{
if (!Resolve(uid, ref climbing, ref fixtures, false))
return;
foreach (var (name, fixtureMask) in climbing.DisabledFixtureMasks)
{
if (!fixtures.Fixtures.TryGetValue(name, out var fixture))
{
continue;
}
_physics.SetCollisionMask(uid, name, fixture, fixture.CollisionMask | fixtureMask, fixtures);
}
climbing.DisabledFixtureMasks.Clear();
if (!_fixtureRemoveQueue.TryGetValue(uid, out var removeQueue))
{
removeQueue = new Dictionary<string, Fixture>();
_fixtureRemoveQueue.Add(uid, removeQueue);
}
if (fixtures.Fixtures.TryGetValue(ClimbingFixtureName, out var climbingFixture))
removeQueue.Add(ClimbingFixtureName, climbingFixture);
climbing.IsClimbing = false;
climbing.OwnerIsTransitioning = false;
var ev = new EndClimbEvent();
RaiseLocalEvent(uid, ref ev);
Dirty(climbing);
}
/// <summary>
/// Checks if the user can vault the target
/// </summary>
/// <param name="component">The component of the entity that is being vaulted</param>
/// <param name="user">The entity that wants to vault</param>
/// <param name="target">The object that is being vaulted</param>
/// <param name="reason">The reason why it cant be dropped</param>
/// <returns></returns>
public bool CanVault(ClimbableComponent component, EntityUid user, EntityUid target, out string reason)
{
if (!_actionBlockerSystem.CanInteract(user, target))
{
reason = Loc.GetString("comp-climbable-cant-interact");
return false;
}
if (!HasComp<ClimbingComponent>(user)
|| !TryComp(user, out BodyComponent? body)
|| !_bodySystem.BodyHasPartType(user, BodyPartType.Leg, body)
|| !_bodySystem.BodyHasPartType(user, BodyPartType.Foot, body))
{
reason = Loc.GetString("comp-climbable-cant-climb");
return false;
}
if (!_interactionSystem.InRangeUnobstructed(user, target, component.Range))
{
reason = Loc.GetString("comp-climbable-cant-reach");
return false;
}
reason = string.Empty;
return true;
}
/// <summary>
/// Checks if the user can vault the dragged entity onto the the target
/// </summary>
/// <param name="component">The climbable component of the object being vaulted onto</param>
/// <param name="user">The user that wants to vault the entity</param>
/// <param name="dragged">The entity that is being vaulted</param>
/// <param name="target">The object that is being vaulted onto</param>
/// <param name="reason">The reason why it cant be dropped</param>
/// <returns></returns>
public bool CanVault(ClimbableComponent component, EntityUid user, EntityUid dragged, EntityUid target,
out string reason)
{
if (!_actionBlockerSystem.CanInteract(user, dragged) || !_actionBlockerSystem.CanInteract(user, target))
{
reason = Loc.GetString("comp-climbable-cant-interact");
return false;
}
if (!HasComp<ClimbingComponent>(dragged))
{
reason = Loc.GetString("comp-climbable-cant-climb");
return false;
}
bool Ignored(EntityUid entity) => entity == target || entity == user || entity == dragged;
if (!_interactionSystem.InRangeUnobstructed(user, target, component.Range, predicate: Ignored)
|| !_interactionSystem.InRangeUnobstructed(user, dragged, component.Range, predicate: Ignored))
{
reason = Loc.GetString("comp-climbable-cant-reach");
return false;
}
reason = string.Empty;
return true;
}
public void ForciblySetClimbing(EntityUid uid, EntityUid climbable, ClimbingComponent? component = null)
{
Climb(uid, uid, uid, climbable, true, component);
}
private void OnBuckleChange(EntityUid uid, ClimbingComponent component, ref BuckleChangeEvent args)
{
if (!args.Buckling)
return;
StopClimb(uid, component);
}
private void OnGlassClimbed(EntityUid uid, GlassTableComponent component, ClimbedOnEvent args)
{
if (TryComp<PhysicsComponent>(args.Climber, out var physics) && physics.Mass <= component.MassLimit)
return;
_damageableSystem.TryChangeDamage(args.Climber, component.ClimberDamage, origin: args.Climber);
_damageableSystem.TryChangeDamage(uid, component.TableDamage, origin: args.Climber);
_stunSystem.TryParalyze(args.Climber, TimeSpan.FromSeconds(component.StunTime), true);
// Not shown to the user, since they already get a 'you climb on the glass table' popup
_popupSystem.PopupEntity(
Loc.GetString("glass-table-shattered-others", ("table", uid), ("climber", Identity.Entity(args.Climber, EntityManager))), args.Climber,
Filter.PvsExcept(args.Climber), true);
}
/// <summary>
/// Moves the entity toward the target climbed entity
/// </summary>
public void MoveEntityToward(EntityUid uid, EntityUid target, PhysicsComponent? physics = null, ClimbingComponent? climbing = null)
{
if (!Resolve(uid, ref physics, ref climbing, false))
return;
var from = Transform(uid).WorldPosition;
var to = Transform(target).WorldPosition;
var (x, y) = (to - from).Normalized();
if (MathF.Abs(x) < 0.6f) // user climbed mostly vertically so lets make it a clean straight line
to = new Vector2(from.X, to.Y);
else if (MathF.Abs(y) < 0.6f) // user climbed mostly horizontally so lets make it a clean straight line
to = new Vector2(to.X, from.Y);
var velocity = (to - from).Length();
if (velocity <= 0.0f)
return;
// Since there are bodies with different masses:
// mass * 10 seems enough to move entity
// instead of launching cats like rockets against the walls with constant impulse value.
_physics.ApplyLinearImpulse(uid, (to - from).Normalized() * velocity * physics.Mass * 10, body: physics);
_physics.SetBodyType(uid, BodyType.Dynamic, body: physics);
climbing.OwnerIsTransitioning = true;
_actionBlockerSystem.UpdateCanMove(uid);
// Transition back to KinematicController after BufferTime
climbing.Owner.SpawnTimer((int) (ClimbingComponent.BufferTime * 1000), () =>
{
if (climbing.Deleted)
return;
_physics.SetBodyType(uid, BodyType.KinematicController);
climbing.OwnerIsTransitioning = false;
_actionBlockerSystem.UpdateCanMove(uid);
});
}
public override void Update(float frameTime)
{
foreach (var (uid, fixtures) in _fixtureRemoveQueue)
{
if (!TryComp<PhysicsComponent>(uid, out var physicsComp)
|| !TryComp<FixturesComponent>(uid, out var fixturesComp))
{
continue;
}
foreach (var fixture in fixtures)
{
_fixtureSystem.DestroyFixture(uid, fixture.Key, fixture.Value, body: physicsComp, manager: fixturesComp);
}
}
_fixtureRemoveQueue.Clear();
}
private void Reset(RoundRestartCleanupEvent ev)
{
_fixtureRemoveQueue.Clear();
}
}
/// <summary>
/// Raised on an entity when it is climbed on.
/// </summary>
public sealed class ClimbedOnEvent : EntityEventArgs
{
public EntityUid Climber;
public EntityUid Instigator;
public ClimbedOnEvent(EntityUid climber, EntityUid instigator)
{
Climber = climber;
Instigator = instigator;
}
}
/// <summary>
/// Raised on an entity when it successfully climbs on something.
/// </summary>
public sealed class StartClimbEvent : EntityEventArgs
{
public EntityUid Climbable;
public StartClimbEvent(EntityUid climbable)
{
Climbable = climbable;
}
}

View File

@@ -1,6 +1,5 @@
using Content.Server.UserInterface;
using Content.Shared.Communications;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
namespace Content.Server.Communications
@@ -21,42 +20,40 @@ namespace Content.Server.Communications
/// If a Fluent ID isn't found, just uses the raw string
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("title", required: true)]
public string AnnouncementDisplayName = "comms-console-announcement-title-station";
[DataField(required: true)]
public LocId Title = "comms-console-announcement-title-station";
/// <summary>
/// Announcement color
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("color")]
public Color AnnouncementColor = Color.Gold;
[DataField]
public Color Color = Color.Gold;
/// <summary>
/// Time in seconds between announcement delays on a per-console basis
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("delay")]
public int DelayBetweenAnnouncements = 90;
[DataField]
public int Delay = 90;
/// <summary>
/// Can call or recall the shuttle
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("canShuttle")]
public bool CanCallShuttle = true;
[DataField]
public bool CanShuttle = true;
/// <summary>
/// Announce on all grids (for nukies)
/// </summary>
[DataField("global")]
public bool AnnounceGlobal = false;
[DataField]
public bool Global = false;
/// <summary>
/// Announce sound file path
/// </summary>
[DataField("sound")]
public SoundSpecifier AnnouncementSound = new SoundPathSpecifier("/Audio/Announcements/announce.ogg");
public PlayerBoundUserInterface? UserInterface => Owner.GetUIOrNull(CommunicationsConsoleUiKey.Key);
[DataField]
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Announcements/announce.ogg");
}
}

View File

@@ -72,8 +72,8 @@ namespace Content.Server.Communications
comp.UIUpdateAccumulator -= UIUpdateInterval;
if (comp.UserInterface is { } ui && ui.SubscribedSessions.Count > 0)
UpdateCommsConsoleInterface(uid, comp);
if (_uiSystem.TryGetUi(uid, CommunicationsConsoleUiKey.Key, out var ui) && ui.SubscribedSessions.Count > 0)
UpdateCommsConsoleInterface(uid, comp, ui);
}
base.Update(frameTime);
@@ -121,9 +121,11 @@ namespace Content.Server.Communications
/// <summary>
/// Updates the UI for a particular comms console.
/// </summary>
/// <param name="comp"></param>
public void UpdateCommsConsoleInterface(EntityUid uid, CommunicationsConsoleComponent comp)
public void UpdateCommsConsoleInterface(EntityUid uid, CommunicationsConsoleComponent comp, PlayerBoundUserInterface? ui = null)
{
if (ui == null && !_uiSystem.TryGetUi(uid, CommunicationsConsoleUiKey.Key, out ui))
return;
var stationUid = _stationSystem.GetOwningStation(uid);
List<string>? levels = null;
string currentLevel = default!;
@@ -151,15 +153,14 @@ namespace Content.Server.Communications
}
}
if (comp.UserInterface is not null)
_uiSystem.SetUiState(comp.UserInterface, new CommunicationsConsoleInterfaceState(
CanAnnounce(comp),
CanCallOrRecall(comp),
levels,
currentLevel,
currentDelay,
_roundEndSystem.ExpectedCountdownEnd
));
_uiSystem.SetUiState(ui, new CommunicationsConsoleInterfaceState(
CanAnnounce(comp),
CanCallOrRecall(comp),
levels,
currentLevel,
currentDelay,
_roundEndSystem.ExpectedCountdownEnd
));
}
private static bool CanAnnounce(CommunicationsConsoleComponent comp)
@@ -188,7 +189,7 @@ namespace Content.Server.Communications
// Calling shuttle checks
if (_roundEndSystem.ExpectedCountdownEnd is null)
return comp.CanCallShuttle;
return comp.CanShuttle;
// Recalling shuttle checks
var recallThreshold = _cfg.GetCVar(CCVars.EmergencyRecallTurningPoint);
@@ -203,7 +204,9 @@ namespace Content.Server.Communications
private void OnSelectAlertLevelMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleSelectAlertLevelMessage message)
{
if (message.Session.AttachedEntity is not { Valid: true } mob) return;
if (message.Session.AttachedEntity is not { Valid: true } mob)
return;
if (!CanUse(mob, uid))
{
_popupSystem.PopupCursor(Loc.GetString("comms-console-permission-denied"), message.Session, PopupType.Medium);
@@ -256,19 +259,27 @@ namespace Content.Server.Communications
}
}
comp.AnnouncementCooldownRemaining = comp.DelayBetweenAnnouncements;
comp.AnnouncementCooldownRemaining = comp.Delay;
UpdateCommsConsoleInterface(uid, comp);
var ev = new CommunicationConsoleAnnouncementEvent(uid, comp, msg, message.Session.AttachedEntity);
RaiseLocalEvent(ref ev);
// allow admemes with vv
Loc.TryGetString(comp.AnnouncementDisplayName, out var title);
title ??= comp.AnnouncementDisplayName;
Loc.TryGetString(comp.Title, out var title);
title ??= comp.Title;
msg += "\n" + Loc.GetString("comms-console-announcement-sent-by") + " " + author;
// Corvax-Announcements-Start
_chatSystem.DispatchGlobalAnnouncement(msg, title, announcementSound: comp.AnnouncementSound, colorOverride: comp.AnnouncementColor);
if (comp.Global)
{
_chatSystem.DispatchGlobalAnnouncement(msg, title, announcementSound: comp.Sound, colorOverride: comp.Color);
if (message.Session.AttachedEntity != null)
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Session.AttachedEntity.Value):player} has sent the following global announcement: {msg}");
return;
}
_chatSystem.DispatchStationAnnouncement(uid, msg, title, colorOverride: comp.Color);
if (message.Session.AttachedEntity != null)
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Session.AttachedEntity.Value):player} has sent the following {(comp.AnnounceGlobal ? "global" : "station")} announcement: {msg}");
@@ -277,8 +288,12 @@ namespace Content.Server.Communications
private void OnCallShuttleMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleCallEmergencyShuttleMessage message)
{
if (!CanCallOrRecall(comp)) return;
if (message.Session.AttachedEntity is not { Valid: true } mob) return;
if (!CanCallOrRecall(comp))
return;
if (message.Session.AttachedEntity is not { Valid: true } mob)
return;
if (!CanUse(mob, uid))
{
_popupSystem.PopupEntity(Loc.GetString("comms-console-permission-denied"), uid, message.Session);
@@ -299,8 +314,12 @@ namespace Content.Server.Communications
private void OnRecallShuttleMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleRecallEmergencyShuttleMessage message)
{
if (!CanCallOrRecall(comp)) return;
if (message.Session.AttachedEntity is not { Valid: true } mob) return;
if (!CanCallOrRecall(comp))
return;
if (message.Session.AttachedEntity is not { Valid: true } mob)
return;
if (!CanUse(mob, uid))
{
_popupSystem.PopupEntity(Loc.GetString("comms-console-permission-denied"), uid, message.Session);

View File

@@ -1,27 +0,0 @@
using Content.Server.Wires;
using Content.Shared.Construction;
using Content.Shared.Wires;
using JetBrains.Annotations;
namespace Content.Server.Construction.Completions;
[UsedImplicitly]
[DataDefinition]
public sealed partial class ChangeWiresPanelSecurityLevel : IGraphAction
{
[DataField("level")]
[ValidatePrototypeId<WiresPanelSecurityLevelPrototype>]
public string WiresPanelSecurityLevelID = "Level0";
public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager)
{
if (WiresPanelSecurityLevelID == null)
return;
if (entityManager.TryGetComponent(uid, out WiresPanelComponent? wiresPanel)
&& entityManager.TrySystem(out WiresSystem? wiresSystem))
{
wiresSystem.SetWiresPanelSecurityData(uid, wiresPanel, WiresPanelSecurityLevelID);
}
}
}

View File

@@ -0,0 +1,35 @@
using Content.Shared.Construction;
using Content.Shared.Wires;
using JetBrains.Annotations;
namespace Content.Server.Construction.Completions;
/// <summary>
/// This graph action is used to set values on entities with the <see cref="WiresPanelSecurityComponent"/>
/// </summary>
[UsedImplicitly]
[DataDefinition]
public sealed partial class SetWiresPanelSecurity : IGraphAction
{
/// <summary>
/// Sets the Examine field on the entity's <see cref="WiresPanelSecurityComponent"/>
/// </summary>
[DataField("examine")]
public string Examine = string.Empty;
/// <summary>
/// Sets the WiresAccessible field on the entity's <see cref="WiresPanelSecurityComponent"/>
/// </summary>
[DataField("wiresAccessible")]
public bool WiresAccessible = true;
public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager)
{
if (entityManager.TryGetComponent(uid, out WiresPanelSecurityComponent? _))
{
var ev = new WiresPanelSecurityEvent(Examine, WiresAccessible);
entityManager.EventBus.RaiseLocalEvent(uid, ev);
}
}
}

View File

@@ -0,0 +1,43 @@
using Content.Shared.Construction;
using JetBrains.Annotations;
using Content.Shared.Doors.Components;
using Content.Shared.Examine;
using YamlDotNet.Core.Tokens;
using Content.Shared.Tag;
namespace Content.Server.Construction.Conditions
{
/// <summary>
/// This condition checks whether if an entity with the <see cref="TagComponent"/> possesses a specific tag
/// </summary>
[UsedImplicitly]
[DataDefinition]
public sealed partial class HasTag : IGraphCondition
{
/// <summary>
/// The tag the entity is being checked for
/// </summary>
[DataField("tag")]
public string Tag { get; private set; }
public bool Condition(EntityUid uid, IEntityManager entityManager)
{
if (!entityManager.TrySystem<TagSystem>(out var tagSystem))
return false;
return tagSystem.HasTag(uid, Tag);
}
public bool DoExamine(ExaminedEvent args)
{
return false;
}
public IEnumerable<ConstructionGuideEntry> GenerateGuideEntry()
{
yield return new ConstructionGuideEntry()
{
};
}
}
}

View File

@@ -8,6 +8,7 @@ using Content.Shared.Database;
using Robust.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using System.Linq;
namespace Content.Server.Construction
{
@@ -298,9 +299,23 @@ namespace Content.Server.Construction
throw new Exception("Missing construction components");
}
// Exit if the new entity's prototype is the same as the original, or the prototype is invalid
if (newEntity == metaData.EntityPrototype?.ID || !_prototypeManager.HasIndex<EntityPrototype>(newEntity))
return null;
// [Optional] Exit if the new entity's prototype is a parent of the original
// E.g., if an entity with the 'AirlockCommand' prototype was to be replaced with a new entity that
// had the 'Airlock' prototype, and DoNotReplaceInheritingEntities was true, the code block would
// exit here because 'AirlockCommand' is derived from 'Airlock'
if (GetCurrentNode(uid, construction)?.DoNotReplaceInheritingEntities == true &&
metaData.EntityPrototype?.ID != null)
{
var parents = _prototypeManager.EnumerateParents<EntityPrototype>(metaData.EntityPrototype.ID)?.ToList();
if (parents != null && parents.Any(x => x.ID == newEntity))
return null;
}
// Optional resolves.
Resolve(uid, ref containerManager, false);

View File

@@ -12,6 +12,7 @@ using Content.Shared.Interaction;
using Content.Shared.Prying.Systems;
using Content.Shared.Radio.EntitySystems;
using Content.Shared.Tools.Components;
using Content.Shared.Tools.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Utility;
@@ -38,7 +39,7 @@ namespace Content.Server.Construction
// Event handling. Add your subscriptions here! Just make sure they're all handled by EnqueueEvent.
SubscribeLocalEvent<ConstructionComponent, InteractUsingEvent>(EnqueueEvent,
new []{typeof(AnchorableSystem), typeof(PryingSystem) },
new []{typeof(AnchorableSystem), typeof(PryingSystem), typeof(WeldableSystem)},
new []{typeof(EncryptionKeySystem)});
SubscribeLocalEvent<ConstructionComponent, OnTemperatureChangeEvent>(EnqueueEvent);
SubscribeLocalEvent<ConstructionComponent, PartAssemblyPartInsertedEvent>(EnqueueEvent);

View File

@@ -11,6 +11,8 @@ namespace Content.Server.Containers
[UsedImplicitly]
public sealed class EmptyOnMachineDeconstructSystem : EntitySystem
{
[Dependency] private readonly SharedContainerSystem _container = default!;
public override void Initialize()
{
base.Initialize();
@@ -33,12 +35,12 @@ namespace Content.Server.Containers
{
if (!EntityManager.TryGetComponent<ContainerManagerComponent>(uid, out var mComp))
return;
var baseCoords = EntityManager.GetComponent<TransformComponent>(component.Owner).Coordinates;
var baseCoords = EntityManager.GetComponent<TransformComponent>(uid).Coordinates;
foreach (var v in component.Containers)
{
if (mComp.TryGetContainer(v, out var container))
{
container.EmptyContainer(true, baseCoords);
_container.EmptyContainer(container, true, baseCoords);
}
}
}

View File

@@ -24,7 +24,5 @@ namespace Content.Server.Crayon
[ViewVariables(VVAccess.ReadWrite)]
[DataField("deleteEmpty")]
public bool DeleteEmpty = true;
[ViewVariables] public PlayerBoundUserInterface? UserInterface => Owner.GetUIOrNull(CrayonUiKey.Key);
}
}

View File

@@ -74,7 +74,8 @@ public sealed class CrayonSystem : SharedCrayonSystem
// Decrease "Ammo"
component.Charges--;
Dirty(component);
Dirty(uid, component);
_adminLogger.Add(LogType.CrayonDraw, LogImpact.Low, $"{EntityManager.ToPrettyString(args.User):user} drew a {component.Color:color} {component.SelectedState}");
args.Handled = true;
@@ -89,17 +90,16 @@ public sealed class CrayonSystem : SharedCrayonSystem
return;
if (!TryComp<ActorComponent>(args.User, out var actor) ||
component.UserInterface == null)
!_uiSystem.TryGetUi(uid, SharedCrayonComponent.CrayonUiKey.Key, out var ui))
{
return;
}
_uiSystem.ToggleUi(component.UserInterface, actor.PlayerSession);
if (component.UserInterface?.SubscribedSessions.Contains(actor.PlayerSession) == true)
_uiSystem.ToggleUi(ui, actor.PlayerSession);
if (ui.SubscribedSessions.Contains(actor.PlayerSession))
{
// Tell the user interface the selected stuff
_uiSystem.SetUiState(component.UserInterface, new CrayonBoundUserInterfaceState(component.SelectedState, component.SelectableColor, component.Color));
_uiSystem.SetUiState(ui, new CrayonBoundUserInterfaceState(component.SelectedState, component.SelectableColor, component.Color));
}
args.Handled = true;
@@ -108,22 +108,22 @@ public sealed class CrayonSystem : SharedCrayonSystem
private void OnCrayonBoundUI(EntityUid uid, CrayonComponent component, CrayonSelectMessage args)
{
// Check if the selected state is valid
if (!_prototypeManager.TryIndex<DecalPrototype>(args.State, out var prototype) || !prototype.Tags.Contains("crayon")) return;
if (!_prototypeManager.TryIndex<DecalPrototype>(args.State, out var prototype) || !prototype.Tags.Contains("crayon"))
return;
component.SelectedState = args.State;
Dirty(component);
Dirty(uid, component);
}
private void OnCrayonBoundUIColor(EntityUid uid, CrayonComponent component, CrayonColorMessage args)
{
// you still need to ensure that the given color is a valid color
if (component.SelectableColor && args.Color != component.Color)
{
component.Color = args.Color;
if (!component.SelectableColor || args.Color == component.Color)
return;
Dirty(component);
}
component.Color = args.Color;
Dirty(uid, component);
}
@@ -134,7 +134,7 @@ public sealed class CrayonSystem : SharedCrayonSystem
// Get the first one from the catalog and set it as default
var decal = _prototypeManager.EnumeratePrototypes<DecalPrototype>().FirstOrDefault(x => x.Tags.Contains("crayon"));
component.SelectedState = decal?.ID ?? string.Empty;
Dirty(component);
Dirty(uid, component);
}
private void OnCrayonDropped(EntityUid uid, CrayonComponent component, DroppedEvent args)

View File

@@ -16,6 +16,7 @@ using Content.Shared.Destructible;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -36,6 +37,7 @@ namespace Content.Server.Destructible
[Dependency] public readonly TriggerSystem TriggerSystem = default!;
[Dependency] public readonly SolutionContainerSystem SolutionContainerSystem = default!;
[Dependency] public readonly PuddleSystem PuddleSystem = default!;
[Dependency] public readonly SharedContainerSystem ContainerSystem = default!;
[Dependency] public readonly IPrototypeManager PrototypeManager = default!;
[Dependency] public readonly IComponentFactory ComponentFactory = default!;
[Dependency] public readonly IAdminLogManager _adminLogger = default!;

View File

@@ -15,7 +15,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
foreach (var container in containerManager.GetAllContainers())
{
container.EmptyContainer(true, system.EntityManager.GetComponent<TransformComponent>(owner).Coordinates);
system.ContainerSystem.EmptyContainer(container, true, system.EntityManager.GetComponent<TransformComponent>(owner).Coordinates);
}
}
}

View File

@@ -0,0 +1,17 @@
using Content.Server.Nutrition.EntitySystems;
namespace Content.Server.Destructible.Thresholds.Behaviors;
/// <summary>
/// Causes the drink/food to open when the destruction threshold is reached.
/// If it is already open nothing happens.
/// </summary>
[DataDefinition]
public sealed partial class OpenBehavior : IThresholdBehavior
{
public void Execute(EntityUid uid, DestructibleSystem system, EntityUid? cause = null)
{
var openable = EntitySystem.Get<OpenableSystem>();
openable.TryOpen(uid);
}
}

View File

@@ -18,7 +18,6 @@ public sealed class AirlockSystem : SharedAirlockSystem
[Dependency] private readonly WiresSystem _wiresSystem = default!;
[Dependency] private readonly PowerReceiverSystem _power = default!;
[Dependency] private readonly DoorBoltSystem _bolts = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
@@ -154,10 +153,12 @@ public sealed class AirlockSystem : SharedAirlockSystem
{
if (TryComp<WiresPanelComponent>(uid, out var panel) &&
panel.Open &&
_prototypeManager.TryIndex<WiresPanelSecurityLevelPrototype>(panel.CurrentSecurityLevelID, out var securityLevelPrototype) &&
securityLevelPrototype.WiresAccessible &&
TryComp<ActorComponent>(args.User, out var actor))
{
if (TryComp<WiresPanelSecurityComponent>(uid, out var wiresPanelSecurity) &&
!wiresPanelSecurity.WiresAccessible)
return;
_wiresSystem.OpenUserInterface(uid, actor.PlayerSession);
args.Handled = true;
return;

View File

@@ -177,7 +177,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
if (!electrified.OnAttacked)
return;
if (_meleeWeapon.GetDamage(args.Used, args.User).Total == 0)
if (!_meleeWeapon.GetDamage(args.Used, args.User).Any())
return;
TryDoElectrifiedAct(uid, args.User, 1, electrified);
@@ -192,7 +192,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
private void OnLightAttacked(EntityUid uid, PoweredLightComponent component, AttackedEvent args)
{
if (_meleeWeapon.GetDamage(args.Used, args.User).Total == 0)
if (!_meleeWeapon.GetDamage(args.Used, args.User).Any())
return;
if (args.Used != args.User)

View File

@@ -22,20 +22,20 @@ using Content.Server.ServerUpdates;
using Content.Server.Voting.Managers;
using Content.Shared.CCVar;
using Content.Shared.Kitchen;
using Content.Shared.Localizations;
using Robust.Server;
using Robust.Shared.Configuration;
using Robust.Server.ServerStatus;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Shared.Localizations;
namespace Content.Server.Entry
{
public sealed class EntryPoint : GameServer
{
private const string ConfigPresetsDir = "/ConfigPresets/";
internal const string ConfigPresetsDir = "/ConfigPresets/";
private const string ConfigPresetsDirBuild = $"{ConfigPresetsDir}Build/";
private EuiManager _euiManager = default!;

View File

@@ -38,17 +38,6 @@ public sealed class SmokeSystem : EntitySystem
SubscribeLocalEvent<SmokeComponent, EntityUnpausedEvent>(OnSmokeUnpaused);
SubscribeLocalEvent<SmokeComponent, ReactionAttemptEvent>(OnReactionAttempt);
SubscribeLocalEvent<SmokeComponent, SpreadNeighborsEvent>(OnSmokeSpread);
SubscribeLocalEvent<SmokeDissipateSpawnComponent, TimedDespawnEvent>(OnSmokeDissipate);
}
private void OnSmokeDissipate(EntityUid uid, SmokeDissipateSpawnComponent component, ref TimedDespawnEvent args)
{
if (!TryComp<TransformComponent>(uid, out var xform))
{
return;
}
Spawn(component.Prototype, xform.Coordinates);
}
private void OnSmokeSpread(EntityUid uid, SmokeComponent component, ref SpreadNeighborsEvent args)

View File

@@ -7,10 +7,10 @@ namespace Content.Server.Forensics
[RegisterComponent]
public sealed partial class FiberComponent : Component
{
[DataField("fiberMaterial")]
public string FiberMaterial = "fibers-synthetic";
[DataField]
public LocId FiberMaterial = "fibers-synthetic";
[DataField("fiberColor")]
[DataField]
public string? FiberColor;
}
}

View File

@@ -9,7 +9,7 @@ namespace Content.Server.Friends.Systems;
public sealed class PettableFriendSystem : EntitySystem
{
[Dependency] private readonly FactionExceptionSystem _factionException = default!;
[Dependency] private readonly NpcFactionSystem _factionException = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
@@ -26,7 +26,7 @@ public sealed class PettableFriendSystem : EntitySystem
if (args.Handled || !TryComp<FactionExceptionComponent>(uid, out var factionException))
return;
if (_factionException.IsIgnored(factionException, user))
if (_factionException.IsIgnored(uid, user, factionException))
{
_popup.PopupEntity(Loc.GetString(comp.FailureString, ("target", uid)), user, user);
return;
@@ -34,7 +34,7 @@ public sealed class PettableFriendSystem : EntitySystem
// you have made a new friend :)
_popup.PopupEntity(Loc.GetString(comp.SuccessString, ("target", uid)), user, user);
_factionException.IgnoreEntity(factionException, user);
_factionException.IgnoreEntity(uid, user, factionException);
args.Handled = true;
}
@@ -45,6 +45,6 @@ public sealed class PettableFriendSystem : EntitySystem
return;
var targetComp = AddComp<FactionExceptionComponent>(args.Target);
_factionException.IgnoreEntities(targetComp, comp.Ignored);
_factionException.IgnoreEntities(args.Target, comp.Ignored, targetComp);
}
}

View File

@@ -30,8 +30,11 @@ namespace Content.Server.GameTicking
if (_mind.TryGetMind(session.UserId, out var mindId, out var mind))
{
if (args.OldStatus == SessionStatus.Connecting && args.NewStatus == SessionStatus.Connected)
if (args.NewStatus != SessionStatus.Disconnected)
{
mind.Session = session;
_pvsOverride.AddSessionOverride(mindId.Value, session);
}
DebugTools.Assert(mind.Session == session);
}
@@ -113,7 +116,10 @@ namespace Content.Server.GameTicking
{
_chatManager.SendAdminAnnouncement(Loc.GetString("player-leave-message", ("name", args.Session.Name)));
if (mind != null)
{
_pvsOverride.ClearOverride(mindId!.Value);
mind.Session = null;
}
if (_playerGameStatuses.ContainsKey(args.Session.UserId)) // Corvax-Queue: Delete data only if player was in game
_userDb.ClientDisconnected(session);

View File

@@ -94,7 +94,17 @@ public sealed partial class GameTicker
_sawmillReplays.Info($"Moving replay into final position: {state.MoveToPath}");
_taskManager.BlockWaitOnTask(_replays.WaitWriteTasks());
DebugTools.Assert(!_replays.IsWriting());
data.Directory.CreateDir(state.MoveToPath.Value.Directory);
try
{
if (!data.Directory.Exists(state.MoveToPath.Value.Directory))
data.Directory.CreateDir(state.MoveToPath.Value.Directory);
}
catch (UnauthorizedAccessException e)
{
_sawmillReplays.Error($"Error creating replay directory {state.MoveToPath.Value.Directory}: {e}");
}
data.Directory.Rename(data.Path, state.MoveToPath.Value);
}

View File

@@ -27,8 +27,8 @@ namespace Content.Server.Ghost
var minds = _entities.System<SharedMindSystem>();
if (!minds.TryGetMind(player, out var mindId, out var mind))
{
shell.WriteLine("You have no Mind, you can't ghost.");
return;
mindId = minds.CreateMind(player.UserId);
mind = _entities.GetComponent<MindComponent>(mindId);
}
if (!EntitySystem.Get<GameTicker>().OnGhostAttempt(mindId, true, true, mind))

View File

@@ -343,7 +343,7 @@ namespace Content.Server.Ghost.Roles
if (ghostRole.MakeSentient)
MakeSentientCommand.MakeSentient(mob, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech);
mob.EnsureComponent<MindContainerComponent>();
EnsureComp<MindContainerComponent>(mob);
GhostRoleInternalCreateMindAndTransfer(args.Player, uid, mob, ghostRole);

View File

@@ -31,6 +31,7 @@ namespace Content.Server.Guardian
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly BodySystem _bodySystem = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
public override void Initialize()
{
@@ -88,7 +89,7 @@ namespace Content.Server.Guardian
private void OnHostInit(EntityUid uid, GuardianHostComponent component, ComponentInit args)
{
component.GuardianContainer = uid.EnsureContainer<ContainerSlot>("GuardianContainer");
component.GuardianContainer = _container.EnsureContainer<ContainerSlot>(uid, "GuardianContainer");
_actionSystem.AddAction(uid, ref component.ActionEntity, component.Action);
}

View File

@@ -169,9 +169,9 @@ namespace Content.Server.Hands.Systems
if (playerSession.AttachedEntity is not {Valid: true} player ||
!Exists(player) ||
player.IsInContainer() ||
ContainerSystem.IsEntityInContainer(player) ||
!TryComp(player, out HandsComponent? hands) ||
hands.ActiveHandEntity is not EntityUid throwEnt ||
hands.ActiveHandEntity is not { } throwEnt ||
!_actionBlockerSystem.CanThrow(player, throwEnt))
return false;

View File

@@ -20,8 +20,6 @@ public sealed partial class InstrumentComponent : SharedInstrumentComponent
public IPlayerSession? InstrumentPlayer =>
_entMan.GetComponentOrNull<ActivatableUIComponent>(Owner)?.CurrentSingleUser
?? _entMan.GetComponentOrNull<ActorComponent>(Owner)?.PlayerSession;
[ViewVariables] public PlayerBoundUserInterface? UserInterface => Owner.GetUIOrNull(InstrumentUiKey.Key);
}
[RegisterComponent]

View File

@@ -5,7 +5,6 @@ using Content.Server.Stunnable;
using Content.Shared.Administration;
using Content.Shared.Instruments;
using Content.Shared.Instruments.UI;
using Content.Shared.Interaction;
using Content.Shared.Physics;
using Content.Shared.Popups;
using JetBrains.Annotations;
@@ -17,7 +16,6 @@ using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.GameStates;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Instruments;
@@ -435,9 +433,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
// Just in case
Clean(uid);
if (instrument.UserInterface is not null)
_bui.CloseAll(instrument.UserInterface);
_bui.TryCloseAll(uid, InstrumentUiKey.Key);
}
instrument.Timer += frameTime;

View File

@@ -0,0 +1,8 @@
using Content.Shared.DragDrop;
namespace Content.Server.Interaction;
public sealed class DragDropSystem : SharedDragDropSystem
{
}

View File

@@ -32,8 +32,6 @@ namespace Content.Server.Interaction
{
base.Initialize();
SubscribeNetworkEvent<DragDropRequestEvent>(HandleDragDropRequestEvent);
SubscribeLocalEvent<BoundUserInterfaceCheckRangeEvent>(HandleUserInterfaceRangeCheck);
}
@@ -58,45 +56,6 @@ namespace Content.Server.Interaction
return _uiSystem.SessionHasOpenUi(container.Owner, StorageComponent.StorageUiKey.Key, actor.PlayerSession);
}
#region Drag drop
private void HandleDragDropRequestEvent(DragDropRequestEvent msg, EntitySessionEventArgs args)
{
var dragged = GetEntity(msg.Dragged);
var target = GetEntity(msg.Target);
if (Deleted(dragged) || Deleted(target))
return;
var user = args.SenderSession.AttachedEntity;
if (user == null || !_actionBlockerSystem.CanInteract(user.Value, target))
return;
// must be in range of both the target and the object they are drag / dropping
// Client also does this check but ya know we gotta validate it.
if (!InRangeUnobstructed(user.Value, dragged, popup: true)
|| !InRangeUnobstructed(user.Value, target, popup: true))
{
return;
}
var dragArgs = new DragDropDraggedEvent(user.Value, target);
// trigger dragdrops on the dropped entity
RaiseLocalEvent(dragged, ref dragArgs);
if (dragArgs.Handled)
return;
var dropArgs = new DragDropTargetEvent(user.Value, dragged);
// trigger dragdrops on the target entity (what you are dropping onto)
RaiseLocalEvent(GetEntity(msg.Target), ref dropArgs);
}
#endregion
private void HandleUserInterfaceRangeCheck(ref BoundUserInterfaceCheckRangeEvent ev)
{
if (ev.Player.AttachedEntity is not { } user)

View File

@@ -18,6 +18,7 @@ namespace Content.Server.Light.EntitySystems
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
@@ -31,6 +32,13 @@ namespace Content.Server.Light.EntitySystems
SubscribeLocalEvent<UnpoweredFlashlightComponent, ToggleActionEvent>(OnToggleAction);
SubscribeLocalEvent<UnpoweredFlashlightComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<UnpoweredFlashlightComponent, GotEmaggedEvent>(OnGotEmagged);
SubscribeLocalEvent<UnpoweredFlashlightComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(EntityUid uid, UnpoweredFlashlightComponent component, MapInitEvent args)
{
_actionContainer.EnsureAction(uid, ref component.ToggleActionEntity, component.ToggleAction);
Dirty(uid, component);
}
private void OnToggleAction(EntityUid uid, UnpoweredFlashlightComponent component, ToggleActionEvent args)

View File

@@ -1,6 +1,5 @@
using System.Numerics;
using Content.Server.Body.Components;
using Content.Server.Climbing;
using Content.Server.Construction;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Materials;
@@ -9,6 +8,7 @@ using Content.Shared.Administration.Logs;
using Content.Shared.Audio;
using Content.Shared.CCVar;
using Content.Shared.Chemistry.Components;
using Content.Shared.Climbing.Events;
using Content.Shared.Construction.Components;
using Content.Shared.Database;
using Content.Shared.DoAfter;
@@ -160,7 +160,7 @@ namespace Content.Server.Medical.BiomassReclaimer
});
}
private void OnClimbedOn(EntityUid uid, BiomassReclaimerComponent component, ClimbedOnEvent args)
private void OnClimbedOn(EntityUid uid, BiomassReclaimerComponent component, ref ClimbedOnEvent args)
{
if (!CanGib(uid, args.Climber, component))
{

View File

@@ -17,8 +17,6 @@ namespace Content.Server.Medical.Components
[DataField("scanDelay")]
public float ScanDelay = 0.8f;
public PlayerBoundUserInterface? UserInterface => Owner.GetUIOrNull(HealthAnalyzerUiKey.Key);
/// <summary>
/// Sound played on scanning begin
/// </summary>

View File

@@ -7,7 +7,6 @@ using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Climbing;
using Content.Server.Medical.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
@@ -32,6 +31,7 @@ using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Timing;
using Content.Server.Temperature.Components;
using Content.Shared.Climbing.Systems;
namespace Content.Server.Medical;

View File

@@ -51,12 +51,12 @@ namespace Content.Server.Medical
args.Handled = true;
}
private void OpenUserInterface(EntityUid user, HealthAnalyzerComponent healthAnalyzer)
private void OpenUserInterface(EntityUid user, EntityUid analyzer)
{
if (!TryComp<ActorComponent>(user, out var actor) || healthAnalyzer.UserInterface == null)
if (!TryComp<ActorComponent>(user, out var actor) || !_uiSystem.TryGetUi(analyzer, HealthAnalyzerUiKey.Key, out var ui))
return;
_uiSystem.OpenUi(healthAnalyzer.UserInterface ,actor.PlayerSession);
_uiSystem.OpenUi(ui ,actor.PlayerSession);
}
public void UpdateScannedUser(EntityUid uid, EntityUid user, EntityUid? target, HealthAnalyzerComponent? healthAnalyzer)
@@ -64,7 +64,7 @@ namespace Content.Server.Medical
if (!Resolve(uid, ref healthAnalyzer))
return;
if (target == null || healthAnalyzer.UserInterface == null)
if (target == null || !_uiSystem.TryGetUi(uid, HealthAnalyzerUiKey.Key, out var ui))
return;
if (!HasComp<DamageableComponent>(target))
@@ -73,9 +73,9 @@ namespace Content.Server.Medical
TryComp<TemperatureComponent>(target, out var temp);
TryComp<BloodstreamComponent>(target, out var bloodstream);
OpenUserInterface(user, healthAnalyzer);
OpenUserInterface(user, uid);
_uiSystem.SendUiMessage(healthAnalyzer.UserInterface, new HealthAnalyzerScannedUserMessage(GetNetEntity(target), temp != null ? temp.CurrentTemperature : float.NaN,
_uiSystem.SendUiMessage(ui, new HealthAnalyzerScannedUserMessage(GetNetEntity(target), temp != null ? temp.CurrentTemperature : float.NaN,
bloodstream != null ? bloodstream.BloodSolution.FillFraction : float.NaN));
}
}

View File

@@ -1,4 +1,3 @@
using Content.Server.Climbing;
using Content.Server.Cloning;
using Content.Server.Medical.Components;
using Content.Shared.Destructible;
@@ -13,6 +12,7 @@ using Content.Server.DeviceLinking.Systems;
using Content.Shared.DeviceLinking.Events;
using Content.Server.Power.EntitySystems;
using Content.Shared.Body.Components;
using Content.Shared.Climbing.Systems;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Robust.Server.Containers;

View File

@@ -7,6 +7,7 @@ using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Players;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Server.Player;
using Robust.Shared.Map;
using Robust.Shared.Network;
@@ -25,12 +26,32 @@ public sealed class MindSystem : SharedMindSystem
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedGhostSystem _ghosts = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly PvsOverrideSystem _pvsOverride = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MindContainerComponent, EntityTerminatingEvent>(OnMindContainerTerminating);
SubscribeLocalEvent<MindComponent, ComponentShutdown>(OnMindShutdown);
}
private void OnMindShutdown(EntityUid uid, MindComponent mind, ComponentShutdown args)
{
if (mind.UserId is {} user)
{
UserMinds.Remove(user);
if (_players.GetPlayerData(user).ContentData() is { } oldData)
oldData.Mind = null;
mind.UserId = null;
}
if (!TryComp(mind.OwnedEntity, out MetaDataComponent? meta) || meta.EntityLifeStage >= EntityLifeStage.Terminating)
return;
RaiseLocalEvent(mind.OwnedEntity.Value, new MindRemovedMessage(uid, mind), true);
mind.OwnedEntity = null;
mind.OwnedComponent = null;
}
private void OnMindContainerTerminating(EntityUid uid, MindContainerComponent component, ref EntityTerminatingEvent args)
@@ -195,11 +216,11 @@ public sealed class MindSystem : SharedMindSystem
public override void TransferTo(EntityUid mindId, EntityUid? entity, bool ghostCheckOverride = false, bool createGhost = true,
MindComponent? mind = null)
{
base.TransferTo(mindId, entity, ghostCheckOverride, createGhost, mind);
if (!Resolve(mindId, ref mind))
if (mind == null && !Resolve(mindId, ref mind))
return;
base.TransferTo(mindId, entity, ghostCheckOverride, createGhost, mind);
if (entity == mind.OwnedEntity)
return;
@@ -239,6 +260,8 @@ public sealed class MindSystem : SharedMindSystem
var oldEntity = mind.OwnedEntity;
if (oldComp != null && oldEntity != null)
{
if (oldComp.Mind != null)
_pvsOverride.ClearOverride(oldComp.Mind.Value);
oldComp.Mind = null;
RaiseLocalEvent(oldEntity.Value, new MindRemovedMessage(oldEntity.Value, mind), true);
}
@@ -290,6 +313,7 @@ public sealed class MindSystem : SharedMindSystem
if (mind.UserId == userId)
return;
_pvsOverride.ClearOverride(mindId);
if (userId != null && !_players.TryGetPlayerData(userId.Value, out _))
{
Log.Error($"Attempted to set mind user to invalid value {userId}");
@@ -331,6 +355,7 @@ public sealed class MindSystem : SharedMindSystem
if (_players.TryGetSessionById(userId.Value, out var ret))
{
mind.Session = ret;
_pvsOverride.AddSessionOverride(mindId, ret);
_actor.Attach(mind.CurrentEntity, ret);
}

View File

@@ -6,12 +6,18 @@ namespace Content.Server.NPC.Components;
/// Prevents an NPC from attacking ignored entities from enemy factions.
/// Can be added to if pettable, see PettableFriendComponent.
/// </summary>
[RegisterComponent, Access(typeof(FactionExceptionSystem))]
[RegisterComponent, Access(typeof(NpcFactionSystem))]
public sealed partial class FactionExceptionComponent : Component
{
/// <summary>
/// List of entities that this NPC will refuse to attack
/// Collection of entities that this NPC will refuse to attack
/// </summary>
[DataField("ignored")]
public HashSet<EntityUid> Ignored = new();
/// <summary>
/// Collection of entities that this NPC will attack, regardless of faction.
/// </summary>
[DataField("hostiles")]
public HashSet<EntityUid> Hostiles = new();
}

View File

@@ -0,0 +1,16 @@
using Content.Server.NPC.Systems;
namespace Content.Server.NPC.Components;
/// <summary>
/// This is used for tracking entities stored in <see cref="FactionExceptionComponent"/>
/// </summary>
[RegisterComponent, Access(typeof(NpcFactionSystem))]
public sealed partial class FactionExceptionTrackerComponent : Component
{
/// <summary>
/// entities with <see cref="FactionExceptionComponent"/> that are tracking this entity.
/// </summary>
[DataField("entities")]
public HashSet<EntityUid> Entities = new();
}

View File

@@ -0,0 +1,24 @@
using Content.Server.NPC.Systems;
namespace Content.Server.NPC.Components;
/// <summary>
/// Entities with this component will retaliate against those who physically attack them.
/// It has an optional "memory" specification wherein it will only attack those entities for a specified length of time.
/// </summary>
[RegisterComponent, Access(typeof(NPCRetaliationSystem))]
public sealed partial class NPCRetaliationComponent : Component
{
/// <summary>
/// How long after being attacked will an NPC continue to be aggressive to the attacker for.
/// </summary>
[DataField("attackMemoryLength"), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan? AttackMemoryLength;
/// <summary>
/// A dictionary that stores an entity and the time at which they will no longer be considered hostile.
/// </summary>
/// todo: this needs to support timeoffsetserializer at some point
[DataField("attackMemories")]
public Dictionary<EntityUid, TimeSpan> AttackMemories = new();
}

View File

@@ -18,6 +18,7 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using ClimbableComponent = Content.Shared.Climbing.Components.ClimbableComponent;
namespace Content.Server.NPC.Pathfinding;

View File

@@ -1,33 +0,0 @@
using Content.Server.NPC.Components;
namespace Content.Server.NPC.Systems;
/// <summary>
/// Prevents an NPC from attacking some entities from an enemy faction.
/// </summary>
public sealed class FactionExceptionSystem : EntitySystem
{
/// <summary>
/// Returns whether the entity from an enemy faction won't be attacked
/// </summary>
public bool IsIgnored(FactionExceptionComponent comp, EntityUid target)
{
return comp.Ignored.Contains(target);
}
/// <summary>
/// Prevents an entity from an enemy faction from being attacked
/// </summary>
public void IgnoreEntity(FactionExceptionComponent comp, EntityUid target)
{
comp.Ignored.Add(target);
}
/// <summary>
/// Prevents a list of entities from an enemy faction from being attacked
/// </summary>
public void IgnoreEntities(FactionExceptionComponent comp, IEnumerable<EntityUid> ignored)
{
comp.Ignored.UnionWith(ignored);
}
}

View File

@@ -0,0 +1,90 @@
using Content.Server.NPC.Components;
using Content.Shared.CombatMode;
using Content.Shared.Damage;
using Content.Shared.Mobs.Components;
using Robust.Shared.Collections;
using Robust.Shared.Timing;
namespace Content.Server.NPC.Systems;
/// <summary>
/// Handles NPC which become aggressive after being attacked.
/// </summary>
public sealed class NPCRetaliationSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
private readonly HashSet<EntityUid> _deAggroQueue = new();
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<NPCRetaliationComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<NPCRetaliationComponent, DisarmedEvent>(OnDisarmed);
}
private void OnDamageChanged(EntityUid uid, NPCRetaliationComponent component, DamageChangedEvent args)
{
if (!args.DamageIncreased)
return;
if (args.Origin is not { } origin)
return;
TryRetaliate(uid, origin, component);
}
private void OnDisarmed(EntityUid uid, NPCRetaliationComponent component, DisarmedEvent args)
{
TryRetaliate(uid, args.Source, component);
}
public bool TryRetaliate(EntityUid uid, EntityUid target, NPCRetaliationComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
// don't retaliate against inanimate objects.
if (!HasComp<MobStateComponent>(target))
return false;
if (_npcFaction.IsEntityFriendly(uid, target))
return false;
_npcFaction.AggroEntity(uid, target);
if (component.AttackMemoryLength is { } memoryLength)
{
component.AttackMemories[target] = _timing.CurTime + memoryLength;
}
return true;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<NPCRetaliationComponent, FactionExceptionComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var comp, out var factionException, out var metaData))
{
_deAggroQueue.Clear();
foreach (var ent in new ValueList<EntityUid>(comp.AttackMemories.Keys))
{
if (_timing.CurTime < comp.AttackMemories[ent])
continue;
if (TerminatingOrDeleted(ent, metaData))
_deAggroQueue.Add(ent);
_deAggroQueue.Add(ent);
}
foreach (var ent in _deAggroQueue)
{
_npcFaction.DeAggroEntity(uid, ent, factionException);
}
}
}
}

View File

@@ -10,6 +10,7 @@ using Content.Shared.NPC;
using Content.Shared.Physics;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
namespace Content.Server.NPC.Systems;

View File

@@ -9,6 +9,8 @@ using Content.Shared.NPC;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Utility;
using ClimbableComponent = Content.Shared.Climbing.Components.ClimbableComponent;
using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
namespace Content.Server.NPC.Systems;
@@ -132,7 +134,7 @@ public sealed partial class NPCSteeringSystem
{
return SteeringObstacleStatus.Completed;
}
else if (climbing.OwnerIsTransitioning)
else if (climbing.NextTransition != null)
{
return SteeringObstacleStatus.Continuing;
}

Some files were not shown because too many files have changed in this diff Show More