Merge remote-tracking branch 'upstream/master' into 20-10-30-admins

This commit is contained in:
Pieter-Jan Briers
2020-10-30 16:23:21 +01:00
747 changed files with 26214 additions and 11240 deletions

View File

@@ -1,7 +1,9 @@
using Content.Client.GameObjects.Components.Arcade;
using System.Linq;
using Content.Client.GameObjects.Components.Arcade;
using Content.Shared.GameObjects.Components.Arcade;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
using Vector2 = Robust.Shared.Maths.Vector2;
namespace Content.Client.Arcade
@@ -16,17 +18,17 @@ namespace Content.Client.Arcade
private Label _enemyInfoLabel;
private Label _playerActionLabel;
private Label _enemyActionLabel;
private Button[] _gameButtons = new Button[3]; //used to disable/enable all game buttons
public SpaceVillainArcadeMenu(SpaceVillainArcadeBoundUserInterface owner)
{
Title = "Space Villain";
Title = Loc.GetString("Space Villain");
Owner = owner;
GridContainer grid = new GridContainer();
grid.Columns = 1;
var grid = new GridContainer {Columns = 1};
GridContainer infoGrid = new GridContainer();
infoGrid.Columns = 3;
infoGrid.AddChild(new Label{ Text = "Player", Align = Label.AlignMode.Center });
var infoGrid = new GridContainer {Columns = 3};
infoGrid.AddChild(new Label{ Text = Loc.GetString("Player"), Align = Label.AlignMode.Center });
infoGrid.AddChild(new Label{ Text = "|", Align = Label.AlignMode.Center });
_enemyNameLabel = new Label{ Align = Label.AlignMode.Center};
infoGrid.AddChild(_enemyNameLabel);
@@ -36,38 +38,35 @@ namespace Content.Client.Arcade
infoGrid.AddChild(new Label{ Text = "|", Align = Label.AlignMode.Center });
_enemyInfoLabel = new Label {Align = Label.AlignMode.Center};
infoGrid.AddChild(_enemyInfoLabel);
CenterContainer centerContainer = new CenterContainer();
var centerContainer = new CenterContainer();
centerContainer.AddChild(infoGrid);
grid.AddChild(centerContainer);
_playerActionLabel = new Label();
_playerActionLabel.Align = Label.AlignMode.Center;
_playerActionLabel = new Label {Align = Label.AlignMode.Center};
grid.AddChild(_playerActionLabel);
_enemyActionLabel = new Label();
_enemyActionLabel.Align = Label.AlignMode.Center;
_enemyActionLabel = new Label {Align = Label.AlignMode.Center};
grid.AddChild(_enemyActionLabel);
GridContainer buttonGrid = new GridContainer();
buttonGrid.Columns = 3;
Button attack = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Attack);
attack.Text = "ATTACK";
buttonGrid.AddChild(attack);
var buttonGrid = new GridContainer {Columns = 3};
_gameButtons[0] = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Attack)
{Text = Loc.GetString("ATTACK")};
buttonGrid.AddChild(_gameButtons[0]);
Button heal = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Heal);
heal.Text = "HEAL";
buttonGrid.AddChild(heal);
_gameButtons[1] = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Heal)
{Text = Loc.GetString("HEAL")};
buttonGrid.AddChild(_gameButtons[1]);
Button recharge = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Recharge);
recharge.Text = "RECHARGE";
buttonGrid.AddChild(recharge);
_gameButtons[2] = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.Recharge)
{Text = Loc.GetString("RECHARGE")};
buttonGrid.AddChild(_gameButtons[2]);
centerContainer = new CenterContainer();
centerContainer.AddChild(buttonGrid);
grid.AddChild(centerContainer);
Button newGame = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.NewGame);
newGame.Text = "New Game";
var newGame = new ActionButton(Owner, SharedSpaceVillainArcadeComponent.PlayerAction.NewGame)
{Text = Loc.GetString("New Game")};
grid.AddChild(newGame);
centerContainer = new CenterContainer();
@@ -79,6 +78,11 @@ namespace Content.Client.Arcade
{
Title = message.GameTitle;
_enemyNameLabel.Text = message.EnemyName;
foreach (var gameButton in _gameButtons)
{
gameButton.Disabled = message.ButtonsDisabled;
}
}
public void UpdateInfo(SharedSpaceVillainArcadeComponent.SpaceVillainArcadeDataUpdateMessage message)

View File

@@ -1,3 +1,4 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Client.Interfaces;
@@ -54,27 +55,23 @@ namespace Content.Client
private void DoNotifyEntity(MsgDoNotifyEntity message)
{
if (!_entityManager.TryGetEntity(message.Entity, out var entity))
if (_playerManager.LocalPlayer?.ControlledEntity == null ||
!_entityManager.TryGetEntity(message.Entity, out var entity))
{
return;
}
PopupMessage(_eyeManager.CoordinatesToScreen(entity.Transform.Coordinates), message.Message);
PopupMessage(entity, _playerManager.LocalPlayer.ControlledEntity, message.Message);
}
public override void PopupMessage(IEntity source, IEntity viewer, string message)
{
if (viewer != _playerManager.LocalPlayer?.ControlledEntity)
{
return;
}
PopupMessage(_eyeManager.CoordinatesToScreen(source.Transform.Coordinates), message);
PopupMessage(_eyeManager.CoordinatesToScreen(source.Transform.Coordinates), message, source);
}
public override void PopupMessage(EntityCoordinates coordinates, IEntity viewer, string message)
{
if (viewer != _playerManager.LocalPlayer.ControlledEntity)
if (viewer != _playerManager.LocalPlayer?.ControlledEntity)
{
return;
}
@@ -84,7 +81,7 @@ namespace Content.Client
public override void PopupMessageCursor(IEntity viewer, string message)
{
if (viewer != _playerManager.LocalPlayer.ControlledEntity)
if (viewer != _playerManager.LocalPlayer?.ControlledEntity)
{
return;
}
@@ -94,13 +91,21 @@ namespace Content.Client
public void PopupMessage(ScreenCoordinates coordinates, string message)
{
var label = new PopupLabel
PopupMessage(coordinates, message, null);
}
public void PopupMessage(ScreenCoordinates coordinates, string message, IEntity? entity)
{
var label = new PopupLabel(_eyeManager)
{
Entity = entity,
Text = message,
StyleClasses = { StyleNano.StyleClassPopupMessage },
};
_userInterfaceManager.PopupRoot.AddChild(label);
var minimumSize = label.CombinedMinimumSize;
LayoutContainer.SetPosition(label, label.InitialPos = coordinates.Position - minimumSize / 2);
_aliveLabels.Add(label);
}
@@ -125,20 +130,30 @@ namespace Content.Client
private class PopupLabel : Label
{
private readonly IEyeManager _eyeManager;
public float TimeLeft { get; private set; }
public Vector2 InitialPos { get; set; }
public IEntity? Entity { get; set; }
public PopupLabel()
public PopupLabel(IEyeManager eyeManager)
{
_eyeManager = eyeManager;
ShadowOffsetXOverride = 1;
ShadowOffsetYOverride = 1;
FontColorShadowOverride = Color.Black;
}
protected override void Update(FrameEventArgs eventArgs)
protected override void FrameUpdate(FrameEventArgs eventArgs)
{
TimeLeft += eventArgs.DeltaSeconds;
LayoutContainer.SetPosition(this, InitialPos - (0, 20 * (TimeLeft * TimeLeft + TimeLeft)));
var position = Entity == null
? InitialPos
: _eyeManager.CoordinatesToScreen(Entity.Transform.Coordinates).Position - CombinedMinimumSize / 2;
LayoutContainer.SetPosition(this, position - (0, 20 * (TimeLeft * TimeLeft + TimeLeft)));
if (TimeLeft > 0.5f)
{
Modulate = Color.White.WithAlpha(1f - 0.2f * (float)Math.Pow(TimeLeft - 0.5f, 3f));

View File

@@ -8,6 +8,7 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>..\bin\Content.Client\</OutputPath>
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
<WarningsAsErrors>CS8604;CS8765</WarningsAsErrors>
</PropertyGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using Content.Client.GameObjects.Components.Disposal;
using Content.Client.GameObjects.Components.MedicalScanner;
using Content.Shared.GameObjects.Components.Body;
@@ -14,7 +14,8 @@ namespace Content.Client.GameObjects.Components.Body
public bool CanDrop(CanDropEventArgs eventArgs)
{
if (eventArgs.Target.HasComponent<DisposalUnitComponent>() ||
eventArgs.Target.HasComponent<MedicalScannerComponent>())
eventArgs.Target.HasComponent<MedicalScannerComponent>() ||
eventArgs.Target.HasComponent<DisposalMailingUnitComponent>())
{
return true;
}

View File

@@ -0,0 +1,55 @@
using Robust.Client.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects.Components.UserInterface;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using static Content.Shared.GameObjects.Components.SharedConfigurationComponent;
namespace Content.Client.GameObjects.Components.Wires
{
public class ConfigurationBoundUserInterface : BoundUserInterface
{
public Regex Validation { get; internal set; }
public ConfigurationBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
}
private ConfigurationMenu _menu;
protected override void Open()
{
base.Open();
_menu = new ConfigurationMenu(this);
_menu.OnClose += Close;
_menu.OpenCentered();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
_menu.Populate(state as ConfigurationBoundUserInterfaceState);
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
base.ReceiveMessage(message);
if (message is ValidationUpdateMessage msg)
{
Validation = new Regex(msg.ValidationString, RegexOptions.Compiled);
}
}
public void SendConfiguration(Dictionary<string, string> config)
{
SendMessage(new ConfigurationUpdatedMessage(config));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_menu.Close();
}
}
}

View File

@@ -0,0 +1,176 @@
using System.Collections.Generic;
using Namotion.Reflection;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using static Content.Shared.GameObjects.Components.SharedConfigurationComponent;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.GameObjects.Components.Wires
{
public class ConfigurationMenu : SS14Window
{
public ConfigurationBoundUserInterface Owner { get; }
private readonly VBoxContainer _baseContainer;
private readonly VBoxContainer _column;
private readonly HBoxContainer _row;
private readonly List<(string name, LineEdit input)> _inputs;
protected override Vector2? CustomSize => (300, 250);
public ConfigurationMenu(ConfigurationBoundUserInterface owner)
{
Owner = owner;
_inputs = new List<(string name, LineEdit input)>();
Title = Loc.GetString("Device Configuration");
var margin = new MarginContainer
{
MarginBottomOverride = 8,
MarginLeftOverride = 8,
MarginRightOverride = 8,
MarginTopOverride = 8
};
_baseContainer = new VBoxContainer
{
SizeFlagsVertical = SizeFlags.FillExpand,
SizeFlagsHorizontal = SizeFlags.FillExpand
};
_column = new VBoxContainer
{
SeparationOverride = 16,
SizeFlagsVertical = SizeFlags.Fill
};
_row = new HBoxContainer
{
SeparationOverride = 16,
SizeFlagsHorizontal = SizeFlags.FillExpand
};
var buttonRow = new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand
};
var spacer1 = new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.Expand
};
var spacer2 = new HBoxContainer()
{
SizeFlagsHorizontal = SizeFlags.Expand
};
var confirmButton = new Button
{
Text = Loc.GetString("Confirm"),
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter
};
confirmButton.OnButtonUp += OnConfirm;
buttonRow.AddChild(spacer1);
buttonRow.AddChild(confirmButton);
buttonRow.AddChild(spacer2);
var outerColumn = new ScrollContainer
{
SizeFlagsVertical = SizeFlags.FillExpand,
SizeFlagsHorizontal = SizeFlags.FillExpand,
ModulateSelfOverride = Color.FromHex("#202025")
};
margin.AddChild(_column);
outerColumn.AddChild(margin);
_baseContainer.AddChild(outerColumn);
_baseContainer.AddChild(buttonRow);
Contents.AddChild(_baseContainer);
}
public void Populate(ConfigurationBoundUserInterfaceState state)
{
_column.Children.Clear();
_inputs.Clear();
foreach (var field in state.Config)
{
var margin = new MarginContainer
{
MarginRightOverride = 8
};
var label = new Label
{
Name = field.Key,
Text = field.Key + ":",
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsStretchRatio = .2f,
CustomMinimumSize = new Vector2(60, 0)
};
var input = new LineEdit
{
Name = field.Key + "-input",
Text = field.Value,
IsValid = Validate,
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsStretchRatio = .8f
};
_inputs.Add((field.Key, input));
var row = new HBoxContainer();
CopyProperties(_row, row);
margin.AddChild(label);
row.AddChild(margin);
row.AddChild(input);
_column.AddChild(row);
}
}
private void OnConfirm(ButtonEventArgs args)
{
var config = GenerateDictionary<string, LineEdit>(_inputs, "Text");
Owner.SendConfiguration(config);
Close();
}
private bool Validate(string value)
{
return Owner.Validation == null || Owner.Validation.IsMatch(value);
}
private Dictionary<string, TConfig> GenerateDictionary<TConfig, TInput>(List<(string name, TInput input)> inputs, string propertyName) where TInput : Control
{
var dictionary = new Dictionary<string, TConfig>();
foreach (var input in inputs)
{
var value = input.input.TryGetPropertyValue<TConfig>(propertyName);
dictionary.Add(input.name, value);
}
return dictionary;
}
private static void CopyProperties<T>(T from, T to) where T : Control
{
foreach (var property in from.AllAttachedProperties)
{
to.SetValue(property.Key, property.Value);
}
}
}
}

View File

@@ -0,0 +1,73 @@
#nullable enable
using JetBrains.Annotations;
using Robust.Client.GameObjects.Components.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects.Components.UserInterface;
using static Content.Shared.GameObjects.Components.Disposal.SharedDisposalMailingUnitComponent;
namespace Content.Client.GameObjects.Components.Disposal
{
/// <summary>
/// Initializes a <see cref="DisposalMailingUnitWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public class DisposalMailingUnitBoundUserInterface : BoundUserInterface
{
private DisposalMailingUnitWindow? _window;
public DisposalMailingUnitBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
}
private void ButtonPressed(UiButton button)
{
SendMessage(new UiButtonPressedMessage(button));
}
protected override void Open()
{
base.Open();
_window = new DisposalMailingUnitWindow();
_window.OpenCentered();
_window.OnClose += Close;
_window.Eject.OnPressed += _ => ButtonPressed(UiButton.Eject);
_window.Engage.OnPressed += _ => ButtonPressed(UiButton.Engage);
_window.Power.OnPressed += _ => ButtonPressed(UiButton.Power);
_window.TargetListContainer.OnItemSelected += TargetSelected;
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (!(state is DisposalMailingUnitBoundUserInterfaceState cast))
{
return;
}
_window?.UpdateState(cast);
}
private void TargetSelected(ItemList.ItemListSelectedEventArgs item)
{
SendMessage(new UiTargetUpdateMessage(_window?.TargetList[item.ItemIndex]));
//(ノ°Д°)ノ︵ ┻━┻
if (_window != null) _window.Engage.Disabled = false;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_window?.Dispose();
}
}
}
}

View File

@@ -0,0 +1,11 @@
using Content.Shared.GameObjects.Components.Disposal;
using Robust.Shared.GameObjects;
namespace Content.Client.GameObjects.Components.Disposal
{
[RegisterComponent]
[ComponentReference(typeof(SharedDisposalMailingUnitComponent))]
public class DisposalMailingUnitComponent : SharedDisposalMailingUnitComponent
{
}
}

View File

@@ -0,0 +1,285 @@
using Content.Shared.GameObjects.Components.Disposal;
using Robust.Client.Graphics.Drawing;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using System.Collections.Generic;
using static Content.Shared.GameObjects.Components.Disposal.SharedDisposalMailingUnitComponent;
namespace Content.Client.GameObjects.Components.Disposal
{
/// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalMailingUnitComponent"/>
/// </summary>
public class DisposalMailingUnitWindow : SS14Window
{
private readonly Label _unitState;
private readonly ProgressBar _pressureBar;
private readonly Label _pressurePercentage;
public readonly Button Engage;
public readonly Button Eject;
public readonly Button Power;
public readonly ItemList TargetListContainer;
public List<string> TargetList;
private readonly Label _tagLabel;
protected override Vector2? CustomSize => (460, 220);
public DisposalMailingUnitWindow()
{
TargetList = new List<string>();
Contents.AddChild(new HBoxContainer
{
Children =
{
new MarginContainer
{
MarginLeftOverride = 8,
MarginRightOverride = 8,
SizeFlagsHorizontal = SizeFlags.FillExpand,
Children =
{
new VBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
Children =
{
new HBoxContainer
{
Children =
{
new Label {Text = Loc.GetString("State: ")},
new Control {CustomMinimumSize = (4, 0)},
(_unitState = new Label {Text = Loc.GetString("Ready")})
}
},
new Control {CustomMinimumSize = (0, 10)},
new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
Children =
{
new Label {Text = Loc.GetString("Pressure:")},
new Control {CustomMinimumSize = (4, 0)},
(_pressureBar = new ProgressBar
{
CustomMinimumSize = (100, 20),
SizeFlagsHorizontal = SizeFlags.FillExpand,
MinValue = 0,
MaxValue = 1,
Page = 0,
Value = 0.5f,
Children =
{
(_pressurePercentage = new Label())
}
})
}
},
new Control {CustomMinimumSize = (0, 10)},
new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
Children =
{
new Label {Text = Loc.GetString("Handle:")},
new Control {
CustomMinimumSize = (4, 0),
SizeFlagsHorizontal = SizeFlags.FillExpand
},
(Engage = new Button
{
CustomMinimumSize = (16, 0),
Text = Loc.GetString("Engage"),
ToggleMode = true,
Disabled = true
})
}
},
new Control {CustomMinimumSize = (0, 10)},
new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
Children =
{
new Label {Text = Loc.GetString("Eject:")},
new Control {
CustomMinimumSize = (4, 0),
SizeFlagsHorizontal = SizeFlags.FillExpand
},
(Eject = new Button {
CustomMinimumSize = (16, 0),
Text = Loc.GetString("Eject Contents"),
//SizeFlagsHorizontal = SizeFlags.ShrinkEnd
})
}
},
new Control {CustomMinimumSize = (0, 10)},
new HBoxContainer
{
Children =
{
(Power = new CheckButton {Text = Loc.GetString("Power")}),
}
}
}
},
}
},
new MarginContainer
{
MarginLeftOverride = 12,
MarginRightOverride = 8,
SizeFlagsHorizontal = SizeFlags.FillExpand,
Children =
{
new VBoxContainer
{
SizeFlagsHorizontal = SizeFlags.Fill,
Children =
{
new HBoxContainer
{
Children =
{
new Label
{
Text = Loc.GetString("Select a destination:")
}
}
},
new Control { CustomMinimumSize = new Vector2(0, 8) },
new HBoxContainer
{
SizeFlagsVertical = SizeFlags.FillExpand,
Children =
{
(TargetListContainer = new ItemList
{
SelectMode = ItemList.ItemListSelectMode.Single,
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsVertical = SizeFlags.FillExpand
})
}
},
new PanelContainer
{
PanelOverride = new StyleBoxFlat
{
BackgroundColor = Color.FromHex("#ACBDBA")
},
SizeFlagsHorizontal = SizeFlags.FillExpand,
CustomMinimumSize = new Vector2(0, 1),
},
new HBoxContainer
{
Children =
{
new VBoxContainer
{
Children =
{
new MarginContainer
{
MarginLeftOverride = 4,
Children =
{
new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
Children =
{
new Label
{
Text = Loc.GetString("This unit:")
},
new Control
{
CustomMinimumSize = new Vector2(4, 0)
},
(_tagLabel = new Label
{
Text = "-",
SizeFlagsVertical = SizeFlags.ShrinkEnd
})
}
}
}
},
}
}
}
}
}
}
}
}
}
});
}
private void UpdatePressureBar(float pressure)
{
_pressureBar.Value = pressure;
var normalized = pressure / _pressureBar.MaxValue;
const float leftHue = 0.0f; // Red
const float middleHue = 0.066f; // Orange
const float rightHue = 0.33f; // Green
const float saturation = 1.0f; // Uniform saturation
const float value = 0.8f; // Uniform value / brightness
const float alpha = 1.0f; // Uniform alpha
// These should add up to 1.0 or your transition won't be smooth
const float leftSideSize = 0.5f; // Fraction of _chargeBar lerped from leftHue to middleHue
const float rightSideSize = 0.5f; // Fraction of _chargeBar lerped from middleHue to rightHue
float finalHue;
if (normalized <= leftSideSize)
{
normalized /= leftSideSize; // Adjust range to 0.0 to 1.0
finalHue = MathHelper.Lerp(leftHue, middleHue, normalized);
}
else
{
normalized = (normalized - leftSideSize) / rightSideSize; // Adjust range to 0.0 to 1.0.
finalHue = MathHelper.Lerp(middleHue, rightHue, normalized);
}
// Check if null first to avoid repeatedly creating this.
_pressureBar.ForegroundStyleBoxOverride ??= new StyleBoxFlat();
var foregroundStyleBoxOverride = (StyleBoxFlat) _pressureBar.ForegroundStyleBoxOverride;
foregroundStyleBoxOverride.BackgroundColor =
Color.FromHsv(new Vector4(finalHue, saturation, value, alpha));
var percentage = pressure / _pressureBar.MaxValue * 100;
_pressurePercentage.Text = $" {percentage:0}%";
}
public void UpdateState(DisposalMailingUnitBoundUserInterfaceState state)
{
Title = state.UnitName;
_unitState.Text = state.UnitState;
UpdatePressureBar(state.Pressure);
Power.Pressed = state.Powered;
Engage.Pressed = state.Engaged;
PopulateTargetList(state.Tags);
_tagLabel.Text = state.Tag;
TargetList = state.Tags;
}
private void PopulateTargetList(List<string> tags)
{
TargetListContainer.Clear();
foreach (var target in tags)
{
TargetListContainer.AddItem(target);
}
}
}
}

View File

@@ -82,7 +82,10 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
foreach (var (slot, entityUid) in cast.Entities)
{
var entity = Owner.EntityManager.GetEntity(entityUid);
if (!Owner.EntityManager.TryGetEntity(entityUid, out var entity))
{
continue;
}
if (!_slots.ContainsKey(slot) || _slots[slot] != entity)
{
_slots[slot] = entity;

View File

@@ -1,5 +1,6 @@
using System;
using Content.Shared.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Client.Graphics.Drawing;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -88,23 +89,27 @@ namespace Content.Client.GameObjects.Components
}
else
{
level = 1 + (int) MathF.Round(charge * 6);
level = ContentHelpers.RoundToNearestLevels(charge, 1.0, 6) + 1;
}
if (level == 1)
if (level == 0)
{
_sections[0].PanelOverride = _styleBoxUnlit;
}
else if (level == 1)
{
// Flash the last light.
_sections[0].PanelOverride = _timer > TimerCycle / 2 ? _styleBoxLit : _styleBoxUnlit;
}
else
{
_sections[0].PanelOverride = level > 2 ? _styleBoxLit : _styleBoxUnlit;
_sections[0].PanelOverride = _styleBoxLit;
}
_sections[1].PanelOverride = level > 3 ? _styleBoxLit : _styleBoxUnlit;
_sections[2].PanelOverride = level > 4 ? _styleBoxLit : _styleBoxUnlit;
_sections[3].PanelOverride = level > 5 ? _styleBoxLit : _styleBoxUnlit;
_sections[4].PanelOverride = level > 6 ? _styleBoxLit : _styleBoxUnlit;
_sections[1].PanelOverride = level >= 3 ? _styleBoxLit : _styleBoxUnlit;
_sections[2].PanelOverride = level >= 4 ? _styleBoxLit : _styleBoxUnlit;
_sections[3].PanelOverride = level >= 5 ? _styleBoxLit : _styleBoxUnlit;
_sections[4].PanelOverride = level >= 6 ? _styleBoxLit : _styleBoxUnlit;
}
}
}

View File

@@ -7,6 +7,7 @@ using Content.Shared.Physics;
using Robust.Client.Audio.Midi;
using Robust.Shared.Audio.Midi;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
@@ -41,6 +42,10 @@ namespace Content.Client.GameObjects.Components.Instruments
private uint _sequenceStartTick;
private bool _allowPercussion;
private bool _allowProgramChange;
/// <summary>
/// A queue of MidiEvents to be sent to the server.
/// </summary>
@@ -67,7 +72,7 @@ namespace Content.Client.GameObjects.Components.Instruments
/// Changes the instrument the midi renderer will play.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public byte InstrumentProgram
public override byte InstrumentProgram
{
get => _instrumentProgram;
set
@@ -84,7 +89,7 @@ namespace Content.Client.GameObjects.Components.Instruments
/// Changes the instrument bank the midi renderer will use.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public byte InstrumentBank
public override byte InstrumentBank
{
get => _instrumentBank;
set
@@ -97,6 +102,34 @@ namespace Content.Client.GameObjects.Components.Instruments
}
}
[ViewVariables(VVAccess.ReadWrite)]
public override bool AllowPercussion
{
get => _allowPercussion;
set
{
_allowPercussion = value;
if (_renderer != null)
{
_renderer.DisablePercussionChannel = !_allowPercussion;
}
}
}
[ViewVariables(VVAccess.ReadWrite)]
public override bool AllowProgramChange
{
get => _allowProgramChange;
set
{
_allowProgramChange = value;
if (_renderer != null)
{
_renderer.DisableProgramChangeEvent = !_allowProgramChange;
}
}
}
/// <summary>
/// Whether this instrument is handheld or not.
/// </summary>
@@ -127,7 +160,7 @@ namespace Content.Client.GameObjects.Components.Instruments
IoCManager.InjectDependencies(this);
}
protected void SetupRenderer(bool fromStateChange = false)
protected virtual void SetupRenderer(bool fromStateChange = false)
{
if (IsRendererAlive) return;
@@ -141,6 +174,8 @@ namespace Content.Client.GameObjects.Components.Instruments
_renderer.MidiBank = _instrumentBank;
_renderer.MidiProgram = _instrumentProgram;
_renderer.TrackingEntity = Owner;
_renderer.DisablePercussionChannel = !_allowPercussion;
_renderer.DisableProgramChangeEvent = !_allowProgramChange;
_renderer.OnMidiPlayerFinished += () =>
{
OnMidiPlaybackEnded?.Invoke();
@@ -173,7 +208,7 @@ namespace Content.Client.GameObjects.Components.Instruments
var renderer = _renderer;
// We dispose of the synth two seconds from now to allow the last notes to stop from playing.
Timer.Spawn(2000, () => { renderer?.Dispose(); });
Owner.SpawnTimer(2000, () => { renderer?.Dispose(); });
_renderer = null;
_midiEventBuffer.Clear();
@@ -195,6 +230,8 @@ namespace Content.Client.GameObjects.Components.Instruments
serializer.DataField(this, x => Handheld, "handheld", false);
serializer.DataField(ref _instrumentProgram, "program", (byte) 1);
serializer.DataField(ref _instrumentBank, "bank", (byte) 0);
serializer.DataField(ref _allowPercussion, "allowPercussion", false);
serializer.DataField(ref _allowProgramChange, "allowProgramChange", false);
}
public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null)
@@ -279,6 +316,11 @@ namespace Content.Client.GameObjects.Components.Instruments
{
EndRenderer(true);
}
AllowPercussion = state.AllowPercussion;
AllowProgramChange = state.AllowProgramChange;
InstrumentBank = state.InstrumentBank;
InstrumentProgram = state.InstrumentProgram;
}
/// <inheritdoc cref="MidiRenderer.OpenInput"/>

View File

@@ -21,7 +21,6 @@ namespace Content.Client.GameObjects.Components.Items
private HandsGui? _gui;
/// <inheritdoc />
private readonly List<Hand> _hands = new List<Hand>();
[ViewVariables] public IReadOnlyList<Hand> Hands => _hands;
@@ -90,7 +89,7 @@ namespace Content.Client.GameObjects.Components.Items
{
if (!TryHand(sharedHand.Name, out var hand))
{
hand = new Hand(sharedHand, Owner.EntityManager);
hand = new Hand(this, sharedHand, Owner.EntityManager);
AddHand(hand);
}
else
@@ -102,6 +101,8 @@ namespace Content.Client.GameObjects.Components.Items
: null;
}
hand.Enabled = sharedHand.Enabled;
UpdateHandSprites(hand);
}
@@ -197,10 +198,35 @@ namespace Content.Client.GameObjects.Components.Items
_gameHud.HandsContainer.AddChild(_gui);
_gui.UpdateHandIcons();
break;
case PlayerDetachedMsg _:
_gui?.Parent?.RemoveChild(_gui);
break;
case HandEnabledMsg msg:
{
var hand = GetHand(msg.Name);
if (hand?.Button == null)
{
break;
}
hand.Button.Blocked.Visible = false;
break;
}
case HandDisabledMsg msg:
{
var hand = GetHand(msg.Name);
if (hand?.Button == null)
{
break;
}
hand.Button.Blocked.Visible = true;
break;
}
}
}
@@ -235,9 +261,11 @@ namespace Content.Client.GameObjects.Components.Items
public class Hand
{
// TODO: Separate into server hand and client hand
public Hand(SharedHand hand, IEntityManager manager, HandButton? button = null)
private bool _enabled = true;
public Hand(HandsComponent parent, SharedHand hand, IEntityManager manager, HandButton? button = null)
{
Parent = parent;
Index = hand.Index;
Name = hand.Name;
Location = hand.Location;
@@ -252,10 +280,33 @@ namespace Content.Client.GameObjects.Components.Items
Entity = entity;
}
private HandsComponent Parent { get; }
public int Index { get; }
public string Name { get; }
public HandLocation Location { get; set; }
public IEntity? Entity { get; set; }
public HandButton? Button { get; set; }
public bool Enabled
{
get => _enabled;
set
{
if (_enabled == value)
{
return;
}
_enabled = value;
Parent.Dirty();
var message = value
? (ComponentMessage) new HandEnabledMsg(Name)
: new HandDisabledMsg(Name);
Parent.HandleMessage(message, Parent);
Parent.Owner.SendMessage(Parent, message);
}
}
}
}

View File

@@ -21,6 +21,8 @@ namespace Content.Client.GameObjects.Components.Items
[ComponentReference(typeof(IItemComponent))]
public class ItemComponent : Component, IItemComponent, IDraggable
{
[Dependency] private IResourceCache _resourceCache = default!;
public override string Name => "Item";
public override uint? NetID => ContentNetIDs.ITEM;
@@ -72,8 +74,7 @@ namespace Content.Client.GameObjects.Components.Items
protected RSI GetRSI()
{
var resourceCache = IoCManager.Resolve<IResourceCache>();
return resourceCache.GetResource<RSIResource>(SharedSpriteComponent.TextureRoot / RsiPath).RSI;
return _resourceCache.GetResource<RSIResource>(SharedSpriteComponent.TextureRoot / RsiPath).RSI;
}
public override void HandleComponentState(ComponentState curState, ComponentState nextState)

View File

@@ -35,6 +35,8 @@ namespace Content.Client.GameObjects.Components.Mobs
[ViewVariables]
private Dictionary<StatusEffect, CooldownGraphic> _cooldown = new Dictionary<StatusEffect, CooldownGraphic>();
public override IReadOnlyDictionary<StatusEffect, StatusEffectStatus> Statuses => _status;
/// <summary>
/// Allows calculating if we need to act due to this component being controlled by the current mob
/// </summary>

View File

@@ -0,0 +1,30 @@
#nullable enable
using Content.Shared.GameObjects.Components.Morgue;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
namespace Content.Client.GameObjects.Components.Morgue
{
public sealed class BodyBagVisualizer : AppearanceVisualizer
{
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out ISpriteComponent? sprite))
{
return;
}
if (component.TryGetData(BodyBagVisuals.Label, out bool labelVal))
{
sprite.LayerSetVisible(BodyBagVisualLayers.Label, labelVal);
}
}
}
public enum BodyBagVisualLayers
{
Label,
}
}

View File

@@ -0,0 +1,75 @@
#nullable enable
using Content.Shared.GameObjects.Components.Morgue;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Content.Client.GameObjects.Components.Storage
{
public sealed class CrematoriumVisualizer : AppearanceVisualizer
{
private string _stateOpen = "";
private string _stateClosed = "";
private string _lightContents = "";
private string _lightBurning = "";
public override void LoadData(YamlMappingNode node)
{
base.LoadData(node);
if (node.TryGetNode("state_open", out var child))
{
_stateOpen = child.AsString();
}
if (node.TryGetNode("state_closed", out child))
{
_stateClosed = child.AsString();
}
if (node.TryGetNode("light_contents", out child))
{
_lightContents = child.AsString();
}
if (node.TryGetNode("light_burning", out child))
{
_lightBurning = child.AsString();
}
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out ISpriteComponent? sprite)) return;
sprite.LayerSetState(
CrematoriumVisualLayers.Base,
component.GetData<bool>(MorgueVisuals.Open)
? _stateOpen
: _stateClosed
);
var lightState = "";
if (component.TryGetData(MorgueVisuals.HasContents, out bool hasContents) && hasContents) lightState = _lightContents;
if (component.TryGetData(CrematoriumVisuals.Burning, out bool isBurning) && isBurning) lightState = _lightBurning;
if (!string.IsNullOrEmpty(lightState))
{
sprite.LayerSetState(CrematoriumVisualLayers.Light, lightState);
sprite.LayerSetVisible(CrematoriumVisualLayers.Light, true);
}
else
{
sprite.LayerSetVisible(CrematoriumVisualLayers.Light, false);
}
}
}
public enum CrematoriumVisualLayers
{
Base,
Light,
}
}

View File

@@ -0,0 +1,81 @@
#nullable enable
using Content.Shared.GameObjects.Components.Morgue;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Content.Client.GameObjects.Components.Storage
{
public sealed class MorgueVisualizer : AppearanceVisualizer
{
private string _stateOpen = "";
private string _stateClosed = "";
private string _lightContents = "";
private string _lightMob = "";
private string _lightSoul = "";
public override void LoadData(YamlMappingNode node)
{
base.LoadData(node);
if (node.TryGetNode("state_open", out var child))
{
_stateOpen = child.AsString();
}
if (node.TryGetNode("state_closed", out child))
{
_stateClosed = child.AsString();
}
if (node.TryGetNode("light_contents", out child))
{
_lightContents = child.AsString();
}
if (node.TryGetNode("light_mob", out child))
{
_lightMob = child.AsString();
}
if (node.TryGetNode("light_soul", out child))
{
_lightSoul = child.AsString();
}
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out ISpriteComponent? sprite)) return;
sprite.LayerSetState(
MorgueVisualLayers.Base,
component.GetData<bool>(MorgueVisuals.Open)
? _stateOpen
: _stateClosed
);
var lightState = "";
if (component.TryGetData(MorgueVisuals.HasContents, out bool hasContents) && hasContents) lightState = _lightContents;
if (component.TryGetData(MorgueVisuals.HasMob, out bool hasMob) && hasMob) lightState = _lightMob;
if (component.TryGetData(MorgueVisuals.HasSoul, out bool hasSoul) && hasSoul) lightState = _lightSoul;
if (!string.IsNullOrEmpty(lightState))
{
sprite.LayerSetState(MorgueVisualLayers.Light, lightState);
sprite.LayerSetVisible(MorgueVisualLayers.Light, true);
}
else
{
sprite.LayerSetVisible(MorgueVisualLayers.Light, false);
}
}
}
public enum MorgueVisualLayers
{
Base,
Light,
}
}

View File

@@ -22,7 +22,11 @@ namespace Content.Client.GameObjects.Components.Nutrition
public override void OnChangeData(AppearanceComponent component)
{
var sprite = component.Owner.GetComponent<ISpriteComponent>();
if(!component.Owner.TryGetComponent<ISpriteComponent>(out var sprite))
{
return;
};
if (!component.TryGetData<float>(SharedFoodComponent.FoodVisuals.MaxUses, out var maxUses))
{
return;

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Singularity;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Content.Client.GameObjects.Components
{
public class ParticleAcceleratorPartVisualizer : AppearanceVisualizer
{
private Dictionary<ParticleAcceleratorVisualState, string> _states = new Dictionary<ParticleAcceleratorVisualState, string>();
public override void LoadData(YamlMappingNode node)
{
base.LoadData(node);
var serializer = YamlObjectSerializer.NewReader(node);
if (!serializer.TryReadDataField<string>("baseState", out var baseState))
{
throw new PrototypeLoadException("No baseState property specified for ParticleAcceleratorPartVisualizer");
}
_states.Add(ParticleAcceleratorVisualState.Powered, baseState+"p");
_states.Add(ParticleAcceleratorVisualState.Level0, baseState+"p0");
_states.Add(ParticleAcceleratorVisualState.Level1, baseState+"p1");
_states.Add(ParticleAcceleratorVisualState.Level2, baseState+"p2");
_states.Add(ParticleAcceleratorVisualState.Level3, baseState+"p3");
}
public override void InitializeEntity(IEntity entity)
{
base.InitializeEntity(entity);
if (!entity.TryGetComponent<SpriteComponent>(out var sprite))
{
throw new EntityCreationException("No sprite component found in entity that has ParticleAcceleratorPartVisualizer");
}
if (!sprite.AllLayers.Any())
{
throw new EntityCreationException("No Layer set for entity that has ParticleAcceleratorPartVisualizer");
}
}
public override void OnChangeData(AppearanceComponent component)
{
if (component.Owner.Deleted)
return;
if (!component.Owner.TryGetComponent<ISpriteComponent>(out var sprite)) return;
if (!component.TryGetData(ParticleAcceleratorVisuals.VisualState, out ParticleAcceleratorVisualState state))
{
state = ParticleAcceleratorVisualState.Unpowered;
}
if (state != ParticleAcceleratorVisualState.Unpowered)
{
sprite.LayerSetVisible(ParticleAcceleratorVisualLayers.Unlit, true);
sprite.LayerSetState(ParticleAcceleratorVisualLayers.Unlit, _states[state]);
}
else
{
sprite.LayerSetVisible(ParticleAcceleratorVisualLayers.Unlit, false);
}
}
}
}

View File

@@ -36,7 +36,8 @@ namespace Content.Client.GameObjects.Components.Power
var sprite = component.Owner.GetComponent<ISpriteComponent>();
if (component.TryGetData(PowerCellVisuals.ChargeLevel, out float fraction))
{
sprite.LayerSetState(Layers.Charge, $"{_prefix}_{ContentHelpers.RoundToLevels(fraction, 1, 5) * 25}");
int level = ContentHelpers.RoundToNearestLevels(fraction, 1, 4) * 25;
sprite.LayerSetState(Layers.Charge, $"{_prefix}_{level}");
}
}

View File

@@ -0,0 +1,104 @@
using System;
using Content.Client.GameObjects.Components.Doors;
using Content.Client.GameObjects.Components.Wires;
using Content.Shared.GameObjects.Components.Doors;
using Content.Shared.GameObjects.Components.Singularity;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.GameObjects.Components.Animations;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using YamlDotNet.RepresentationModel;
namespace Content.Client.GameObjects.Components
{
public class RadiationCollectorVisualizer : AppearanceVisualizer
{
private const string AnimationKey = "radiationcollector_animation";
private Animation ActivateAnimation;
private Animation DeactiveAnimation;
public override void LoadData(YamlMappingNode node)
{
ActivateAnimation = new Animation {Length = TimeSpan.FromSeconds(0.8f)};
{
var flick = new AnimationTrackSpriteFlick();
ActivateAnimation.AnimationTracks.Add(flick);
flick.LayerKey = RadiationCollectorVisualLayers.Main;
flick.KeyFrames.Add(new AnimationTrackSpriteFlick.KeyFrame("ca_active", 0f));
/*var sound = new AnimationTrackPlaySound();
CloseAnimation.AnimationTracks.Add(sound);
sound.KeyFrames.Add(new AnimationTrackPlaySound.KeyFrame(closeSound, 0));*/
}
DeactiveAnimation = new Animation {Length = TimeSpan.FromSeconds(0.8f)};
{
var flick = new AnimationTrackSpriteFlick();
DeactiveAnimation.AnimationTracks.Add(flick);
flick.LayerKey = RadiationCollectorVisualLayers.Main;
flick.KeyFrames.Add(new AnimationTrackSpriteFlick.KeyFrame("ca_deactive", 0f));
/*var sound = new AnimationTrackPlaySound();
CloseAnimation.AnimationTracks.Add(sound);
sound.KeyFrames.Add(new AnimationTrackPlaySound.KeyFrame(closeSound, 0));*/
}
}
public override void InitializeEntity(IEntity entity)
{
if (!entity.HasComponent<AnimationPlayerComponent>())
{
entity.AddComponent<AnimationPlayerComponent>();
}
}
public override void OnChangeData(AppearanceComponent component)
{
if (component.Owner.Deleted)
return;
if (!component.Owner.TryGetComponent<ISpriteComponent>(out var sprite)) return;
if (!component.Owner.TryGetComponent<AnimationPlayerComponent>(out var animPlayer)) return;
if (!component.TryGetData(RadiationCollectorVisuals.VisualState, out RadiationCollectorVisualState state))
{
state = RadiationCollectorVisualState.Deactive;
}
switch (state)
{
case RadiationCollectorVisualState.Active:
sprite.LayerSetState(RadiationCollectorVisualLayers.Main, "ca_on");
break;
case RadiationCollectorVisualState.Activating:
if (!animPlayer.HasRunningAnimation(AnimationKey))
{
animPlayer.Play(ActivateAnimation, AnimationKey);
animPlayer.AnimationCompleted += _ =>
component.SetData(RadiationCollectorVisuals.VisualState,
RadiationCollectorVisualState.Active);
}
break;
case RadiationCollectorVisualState.Deactivating:
if (!animPlayer.HasRunningAnimation(AnimationKey))
{
animPlayer.Play(DeactiveAnimation, AnimationKey);
animPlayer.AnimationCompleted += _ =>
component.SetData(RadiationCollectorVisuals.VisualState,
RadiationCollectorVisualState.Deactive);
}
break;
case RadiationCollectorVisualState.Deactive:
sprite.LayerSetState(RadiationCollectorVisualLayers.Main, "ca_off");
break;
}
}
}
public enum RadiationCollectorVisualLayers
{
Main
}
}

View File

@@ -0,0 +1,28 @@
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
namespace Content.Client.GameObjects.Components.Singularity
{
public class ContainmentFieldComponent : Component
{
public override string Name => "Containment Field";
private SpriteComponent _spriteComponent;
public override void Initialize()
{
base.Initialize();
if (!Owner.TryGetComponent(out _spriteComponent))
{
Logger.Error("Containmentfieldcomponent created without spritecomponent");
}
else
{
_spriteComponent.Directional = false;
}
}
}
}

View File

@@ -0,0 +1,49 @@
using System;
using Content.Shared.GameObjects.Components.Singularity;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
namespace Content.Client.GameObjects.Components.Singularity
{
public class EmitterVisualizer : AppearanceVisualizer
{
private const string OverlayBeam = "emitter-beam";
private const string OverlayUnderPowered = "emitter-underpowered";
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out ISpriteComponent sprite))
{
return;
}
if (!component.TryGetData(EmitterVisuals.Locked, out bool locked))
locked = false;
if (!component.TryGetData(EmitterVisuals.VisualState, out EmitterVisualState state))
state = EmitterVisualState.Off;
switch (state)
{
case EmitterVisualState.On:
sprite.LayerSetVisible(1, true);
sprite.LayerSetState(1, OverlayBeam);
break;
case EmitterVisualState.Underpowered:
sprite.LayerSetVisible(1, true);
sprite.LayerSetState(1, OverlayUnderPowered);
break;
case EmitterVisualState.Off:
sprite.LayerSetVisible(1, false);
break;
default:
throw new ArgumentOutOfRangeException();
}
sprite.LayerSetVisible(2, locked);
}
}
}

View File

@@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.GameObjects.Components.Sound;
using Content.Shared.Physics;
using Robust.Client.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Random;
@@ -51,11 +54,19 @@ namespace Content.Client.GameObjects.Components.Sound
{
if (!schedule.Play) return;
Timer.Spawn((int) schedule.Delay + (_random.Next((int) schedule.RandomDelay)),() =>
Owner.SpawnTimer((int) schedule.Delay + (_random.Next((int) schedule.RandomDelay)),() =>
{
if (!schedule.Play) return; // We make sure this hasn't changed.
if (_audioSystem == null) _audioSystem = EntitySystem.Get<AudioSystem>();
if (!_audioStreams.ContainsKey(schedule))
{
_audioStreams.Add(schedule,_audioSystem.Play(schedule.Filename, Owner, schedule.AudioParams));
}
else
{
_audioStreams[schedule] = _audioSystem.Play(schedule.Filename, Owner, schedule.AudioParams);
}
if (schedule.Times == 0) return;

View File

@@ -1,4 +1,4 @@
using Content.Shared.GameObjects.Components.Storage;
using Content.Shared.GameObjects.Components.Storage;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
@@ -74,12 +74,15 @@ namespace Content.Client.GameObjects.Components.Storage
}
}
if (component.TryGetData(StorageVisuals.CanWeld, out bool canWeld) && canWeld)
{
if (component.TryGetData(StorageVisuals.Welded, out bool weldedVal))
{
sprite.LayerSetVisible(StorageVisualLayers.Welded, weldedVal);
}
}
}
}
public enum StorageVisualLayers
{

View File

@@ -7,6 +7,7 @@ using Robust.Client.Interfaces.Graphics;
using Robust.Client.Interfaces.Graphics.Overlays;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
@@ -83,7 +84,7 @@ namespace Content.Client.GameObjects.Components.Weapons
}
_cancelToken = new CancellationTokenSource();
Timer.Spawn((int) duration * 1000, DisableOverlay, _cancelToken.Token);
Owner.SpawnTimer((int) duration * 1000, DisableOverlay, _cancelToken.Token);
}
private void DisableOverlay()

View File

@@ -1,4 +1,5 @@
using Content.Client.GameObjects.EntitySystems;
using System.Diagnostics.CodeAnalysis;
using Content.Client.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
@@ -34,14 +35,33 @@ namespace Content.Client.GameObjects.Components
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new WindowSmoothDirtyEvent(Owner));
var state0 = $"{_stateBase}0";
const string cracksRSIPath = "/Textures/Constructible/Structures/Windows/cracks.rsi";
_sprite.LayerMapSet(CornerLayers.SE, _sprite.AddLayerState(state0));
_sprite.LayerSetDirOffset(CornerLayers.SE, SpriteComponent.DirectionOffset.None);
_sprite.LayerMapSet(WindowDamageLayers.DamageSE, _sprite.AddLayerState("0_1", cracksRSIPath));
_sprite.LayerSetShader(WindowDamageLayers.DamageSE, "unshaded");
_sprite.LayerSetVisible(WindowDamageLayers.DamageSE, false);
_sprite.LayerMapSet(CornerLayers.NE, _sprite.AddLayerState(state0));
_sprite.LayerSetDirOffset(CornerLayers.NE, SpriteComponent.DirectionOffset.CounterClockwise);
_sprite.LayerMapSet(WindowDamageLayers.DamageNE, _sprite.AddLayerState("0_1", cracksRSIPath));
_sprite.LayerSetDirOffset(WindowDamageLayers.DamageNE, SpriteComponent.DirectionOffset.CounterClockwise);
_sprite.LayerSetShader(WindowDamageLayers.DamageNE, "unshaded");
_sprite.LayerSetVisible(WindowDamageLayers.DamageNE, false);
_sprite.LayerMapSet(CornerLayers.NW, _sprite.AddLayerState(state0));
_sprite.LayerSetDirOffset(CornerLayers.NW, SpriteComponent.DirectionOffset.Flip);
_sprite.LayerMapSet(WindowDamageLayers.DamageNW, _sprite.AddLayerState("0_1", cracksRSIPath));
_sprite.LayerSetDirOffset(WindowDamageLayers.DamageNW, SpriteComponent.DirectionOffset.Flip);
_sprite.LayerSetShader(WindowDamageLayers.DamageNW, "unshaded");
_sprite.LayerSetVisible(WindowDamageLayers.DamageNW, false);
_sprite.LayerMapSet(CornerLayers.SW, _sprite.AddLayerState(state0));
_sprite.LayerSetDirOffset(CornerLayers.SW, SpriteComponent.DirectionOffset.Clockwise);
_sprite.LayerMapSet(WindowDamageLayers.DamageSW, _sprite.AddLayerState("0_1", cracksRSIPath));
_sprite.LayerSetDirOffset(WindowDamageLayers.DamageSW, SpriteComponent.DirectionOffset.Clockwise);
_sprite.LayerSetShader(WindowDamageLayers.DamageSW, "unshaded");
_sprite.LayerSetVisible(WindowDamageLayers.DamageSW, false);
}
/// <inheritdoc />
@@ -91,4 +111,13 @@ namespace Content.Client.GameObjects.Components
serializer.DataField(ref _stateBase, "base", null);
}
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum WindowDamageLayers
{
DamageSE,
DamageNE,
DamageNW,
DamageSW
}
}

View File

@@ -0,0 +1,60 @@
using System;
using Content.Shared.GameObjects.Components;
using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects.Components.Transform;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Client.GameObjects.Components
{
[UsedImplicitly]
public sealed class WindowVisualizer : AppearanceVisualizer
{
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var sprite = component.Owner.GetComponent<ISpriteComponent>();
var snapGrid = component.Owner.GetComponent<SnapGridComponent>();
var lowWall = FindLowWall(snapGrid);
if (lowWall == null) return;
if (component.TryGetData(WindowVisuals.Damage, out float fraction))
{
var level = Math.Min(ContentHelpers.RoundToLevels(fraction, 1, 7), 5);
if (level == 0)
{
foreach (WindowDamageLayers val in Enum.GetValues(typeof(WindowDamageLayers)))
{
sprite.LayerSetVisible(val, false);
}
return;
}
foreach (WindowDamageLayers val in Enum.GetValues(typeof(WindowDamageLayers)))
{
sprite.LayerSetVisible(val, true);
}
sprite.LayerSetState(WindowDamageLayers.DamageNE, $"{(int) lowWall.LastCornerNE}_{level}");
sprite.LayerSetState(WindowDamageLayers.DamageSE, $"{(int) lowWall.LastCornerSE}_{level}");
sprite.LayerSetState(WindowDamageLayers.DamageSW, $"{(int) lowWall.LastCornerSW}_{level}");
sprite.LayerSetState(WindowDamageLayers.DamageNW, $"{(int) lowWall.LastCornerNW}_{level}");
}
}
private static LowWallComponent FindLowWall(SnapGridComponent snapGrid)
{
foreach (var entity in snapGrid.GetLocal())
{
if (entity.TryGetComponent(out LowWallComponent lowWall))
{
return lowWall;
}
}
return null;
}
}
}

View File

@@ -21,6 +21,8 @@ namespace Content.Client.GameObjects.Components.Wires
{
public class WiresMenu : BaseWindow
{
[Dependency] private IResourceCache _resourceCache = default!;
public WiresBoundUserInterface Owner { get; }
private readonly Control _wiresHBox;
@@ -34,7 +36,7 @@ namespace Content.Client.GameObjects.Components.Wires
public WiresMenu(WiresBoundUserInterface owner)
{
var resourceCache = IoCManager.Resolve<IResourceCache>();
IoCManager.InjectDependencies(this);
Owner = owner;
var rootContainer = new LayoutContainer {Name = "WireRoot"};
@@ -42,7 +44,7 @@ namespace Content.Client.GameObjects.Components.Wires
MouseFilter = MouseFilterMode.Stop;
var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var panelTex = _resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var back = new StyleBoxTexture
{
Texture = panelTex,
@@ -135,8 +137,8 @@ namespace Content.Client.GameObjects.Components.Wires
LayoutContainer.SetAnchorPreset(topContainerWrap, LayoutContainer.LayoutPreset.Wide);
var font = resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13);
var fontSmall = resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 10);
var font = _resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13);
var fontSmall = _resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 10);
Button helpButton;
var topRow = new MarginContainer
@@ -255,7 +257,7 @@ namespace Content.Client.GameObjects.Components.Wires
var mirror = random.Next(2) == 0;
var flip = random.Next(2) == 0;
var type = random.Next(2);
var control = new WireControl(wire.Color, wire.Letter, wire.IsCut, flip, mirror, type)
var control = new WireControl(wire.Color, wire.Letter, wire.IsCut, flip, mirror, type, _resourceCache)
{
SizeFlagsVertical = SizeFlags.ShrinkEnd
};
@@ -278,7 +280,7 @@ namespace Content.Client.GameObjects.Components.Wires
{
if (status.Value is StatusLightData statusLightData)
{
_statusContainer.AddChild(new StatusLight(statusLightData));
_statusContainer.AddChild(new StatusLight(statusLightData, _resourceCache));
}
else
{
@@ -305,18 +307,20 @@ namespace Content.Client.GameObjects.Components.Wires
private sealed class WireControl : Control
{
private IResourceCache _resourceCache;
private const string TextureContact = "/Textures/Interface/WireHacking/contact.svg.96dpi.png";
public event Action WireClicked;
public event Action ContactsClicked;
public WireControl(WireColor color, WireLetter letter, bool isCut, bool flip, bool mirror, int type)
public WireControl(WireColor color, WireLetter letter, bool isCut, bool flip, bool mirror, int type, IResourceCache resourceCache)
{
_resourceCache = resourceCache;
SizeFlagsHorizontal = SizeFlags.ShrinkCenter;
MouseFilter = MouseFilterMode.Stop;
var resourceCache = IoCManager.Resolve<IResourceCache>();
var layout = new LayoutContainer();
AddChild(layout);
@@ -326,7 +330,7 @@ namespace Content.Client.GameObjects.Components.Wires
SizeFlagsVertical = SizeFlags.ShrinkEnd,
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
Align = Label.AlignMode.Center,
FontOverride = resourceCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 12),
FontOverride = _resourceCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 12),
FontColorOverride = Color.Gray,
ToolTip = letter.Name(),
MouseFilter = MouseFilterMode.Stop
@@ -337,7 +341,7 @@ namespace Content.Client.GameObjects.Components.Wires
LayoutContainer.SetGrowVertical(greek, LayoutContainer.GrowDirection.Begin);
LayoutContainer.SetGrowHorizontal(greek, LayoutContainer.GrowDirection.Both);
var contactTexture = resourceCache.GetTexture(TextureContact);
var contactTexture = _resourceCache.GetTexture(TextureContact);
var contact1 = new TextureRect
{
Texture = contactTexture,
@@ -356,7 +360,7 @@ namespace Content.Client.GameObjects.Components.Wires
layout.AddChild(contact2);
LayoutContainer.SetPosition(contact2, (0, 60));
var wire = new WireRender(color, isCut, flip, mirror, type);
var wire = new WireRender(color, isCut, flip, mirror, type, _resourceCache);
layout.AddChild(wire);
LayoutContainer.SetPosition(wire, (2, 16));
@@ -420,8 +424,11 @@ namespace Content.Client.GameObjects.Components.Wires
"/Textures/Interface/WireHacking/wire_2_copper.svg.96dpi.png"
};
public WireRender(WireColor color, bool isCut, bool flip, bool mirror, int type)
private IResourceCache _resourceCache;
public WireRender(WireColor color, bool isCut, bool flip, bool mirror, int type, IResourceCache resourceCache)
{
_resourceCache = resourceCache;
_color = color;
_isCut = isCut;
_flip = flip;
@@ -436,10 +443,8 @@ namespace Content.Client.GameObjects.Components.Wires
protected override void Draw(DrawingHandleScreen handle)
{
var resC = IoCManager.Resolve<IResourceCache>();
var colorValue = _color.ColorValue();
var tex = resC.GetTexture(_isCut ? TextureCut[_type] : TextureNormal[_type]);
var tex = _resourceCache.GetTexture(_isCut ? TextureCut[_type] : TextureNormal[_type]);
var l = 0f;
var r = tex.Width + l;
@@ -465,7 +470,7 @@ namespace Content.Client.GameObjects.Components.Wires
if (_isCut)
{
var copper = Color.Orange;
var copperTex = resC.GetTexture(TextureCopper[_type]);
var copperTex = _resourceCache.GetTexture(TextureCopper[_type]);
handle.DrawTextureRect(copperTex, rect, copper);
}
@@ -516,9 +521,8 @@ namespace Content.Client.GameObjects.Components.Wires
}
};
public StatusLight(StatusLightData data)
public StatusLight(StatusLightData data, IResourceCache resourceCache)
{
var resC = IoCManager.Resolve<IResourceCache>();
var hsv = Color.ToHsv(data.Color);
hsv.Z /= 2;
var dimColor = Color.FromHsv(hsv);
@@ -530,7 +534,7 @@ namespace Content.Client.GameObjects.Components.Wires
{
new TextureRect
{
Texture = resC.GetTexture(
Texture = resourceCache.GetTexture(
"/Textures/Interface/WireHacking/light_off_base.svg.96dpi.png"),
Stretch = TextureRect.StretchMode.KeepCentered,
ModulateSelfOverride = dimColor
@@ -540,7 +544,7 @@ namespace Content.Client.GameObjects.Components.Wires
ModulateSelfOverride = data.Color.WithAlpha(0.4f),
Stretch = TextureRect.StretchMode.KeepCentered,
Texture =
resC.GetTexture("/Textures/Interface/WireHacking/light_on_base.svg.96dpi.png"),
resourceCache.GetTexture("/Textures/Interface/WireHacking/light_on_base.svg.96dpi.png"),
})
}
};
@@ -577,7 +581,7 @@ namespace Content.Client.GameObjects.Components.Wires
};
}
var font = IoCManager.Resolve<IResourceCache>().GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 12);
var font = resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 12);
var hBox = new HBoxContainer {SeparationOverride = 4};
hBox.AddChild(new Label

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Client.GameObjects.Components;
@@ -124,9 +124,7 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter
if (doAfter.BreakOnTargetMove)
{
var targetEntity = _entityManager.GetEntity(doAfter.TargetUid);
if (targetEntity.Transform.Coordinates != doAfter.TargetGrid)
if (_entityManager.TryGetEntity(doAfter.TargetUid, out var targetEntity) && targetEntity.Transform.Coordinates != doAfter.TargetGrid)
{
comp.Cancel(id, currentTime);
continue;

View File

@@ -6,6 +6,7 @@ using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.GameObjects.EntitySystemMessages;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Timing;
@@ -95,7 +96,7 @@ namespace Content.Client.GameObjects.EntitySystems
var newColor = Color.Red * originalColor;
sprite.Color = newColor;
Timer.Spawn(100, () =>
hitEntity.SpawnTimer(100, () =>
{
// Only reset back to the original color if something else didn't change the color in the mean time.
if (sprite.Color == newColor)

View File

@@ -24,12 +24,6 @@ namespace Content.Client.GameObjects.EntitySystems
appearance.SetData(RotationVisuals.RotationState, newState);
}
if (playSound)
{
var file = AudioHelpers.GetRandomFileFromSoundCollection("bodyfall");
Get<AudioSystem>().Play(file, entity, AudioHelpers.WithVariation(0.25f));
}
return true;
}

View File

@@ -33,7 +33,6 @@
"Door",
"PoweredLight",
"Smes",
"Powercell",
"LightBulb",
"Healing",
"Catwalk",
@@ -42,6 +41,7 @@
"HitscanWeaponCapacitor",
"PowerCell",
"PowerCellCharger",
"PowerCellSlot",
"WeaponCapacitorCharger",
"AiController",
"Computer",
@@ -182,6 +182,7 @@
"GasCanisterPort",
"Lung",
"Cleanable",
"Configuration",
"Brain",
"PlantHolder",
"SeedExtractor",
@@ -190,7 +191,27 @@
"Hoe",
"Seed",
"BotanySharp",
"PlantSampleTaker"
"PlantSampleTaker",
"Internals",
"GasTank",
"BreathMask",
"RadiationCollector",
"ContainmentFieldGenerator",
"ContainmentField",
"Emitter",
"Singularity",
"SingularityGenerator",
"EmitterBoltComponent",
"ParticleProjectile",
"ParticleAcceleratorControlBox",
"ParticleAcceleratorEmitter",
"ParticleAcceleratorEndCap",
"ParticleAcceleratorFuelChamber",
"ParticleAcceleratorPowerBox",
"BodyBagEntityStorage",
"MorgueEntityStorage",
"MorgueTray",
"CrematoriumEntityStorage",
};
}
}

View File

@@ -1,6 +1,7 @@
using Content.Client.GameObjects.Components.Instruments;
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Client.Audio.Midi;
using Robust.Client.Graphics.Drawing;
using Robust.Client.Interfaces.UserInterface;
@@ -176,34 +177,20 @@ namespace Content.Client.Instruments
var filters = new FileDialogFilters(new FileDialogFilters.Group("mid", "midi"));
var filename = await _fileDialogManager.OpenFile(filters);
var instrumentEnt = _owner.Instrument.Owner;
var instrument = _owner.Instrument;
ContainerHelpers.TryGetContainerMan(_owner.Instrument.Owner, out var conMan);
var localPlayer = IoCManager.Resolve<IPlayerManager>().LocalPlayer;
// The following checks are only in place to prevent players from playing MIDI songs locally.
// There are equivalents for these checks on the server.
if (string.IsNullOrEmpty(filename)) return;
// If we don't have a player or controlled entity, we return.
if(localPlayer?.ControlledEntity == null) return;
// If the instrument is handheld and we're not holding it, we return.
if((instrument.Handheld && (conMan == null
|| conMan.Owner != localPlayer.ControlledEntity))) return;
// We check that we're in range unobstructed just in case.
if (!localPlayer.InRangeUnobstructed(instrumentEnt)) return;
if (!_midiManager.IsMidiFile(filename))
{
Logger.Warning($"Not a midi file! Chosen file: {filename}");
return;
}
if (!PlayCheck())
return;
MidiStopButtonOnPressed(null);
await Timer.Delay(100);
if (!_owner.Instrument.OpenMidi(filename)) return;
@@ -216,6 +203,9 @@ namespace Content.Client.Instruments
{
if (obj.Pressed)
{
if (!PlayCheck())
return;
MidiStopButtonOnPressed(null);
_owner.Instrument.OpenInput();
}
@@ -223,6 +213,26 @@ namespace Content.Client.Instruments
_owner.Instrument.CloseInput();
}
private bool PlayCheck()
{
var instrumentEnt = _owner.Instrument.Owner;
var instrument = _owner.Instrument;
ContainerHelpers.TryGetContainerMan(_owner.Instrument.Owner, out var conMan);
var localPlayer = IoCManager.Resolve<IPlayerManager>().LocalPlayer;
// If we don't have a player or controlled entity, we return.
if(localPlayer?.ControlledEntity == null) return false;
// If the instrument is handheld and we're not holding it, we return.
if((instrument.Handheld && (conMan == null
|| conMan.Owner != localPlayer.ControlledEntity))) return false;
// We check that we're in range unobstructed just in case.
return localPlayer.InRangeUnobstructed(instrumentEnt, predicate:(e) => e == instrumentEnt || e == localPlayer.ControlledEntity);
}
private void MidiStopButtonOnPressed(BaseButton.ButtonEventArgs obj)
{
MidiPlaybackSetButtonsDisabled(true);

View File

@@ -37,7 +37,7 @@ namespace Content.Client.UserInterface.AdminMenu
{
new KickCommandButton(),
new DirectCommandButton("Admin Ghost", "aghost"),
//TODO: teleport
new TeleportCommandButton(),
};
private readonly List<CommandButton> _adminbusButtons = new List<CommandButton>
{
@@ -534,6 +534,30 @@ namespace Content.Client.UserInterface.AdminMenu
}
}
private class TeleportCommandButton : UICommandButton
{
public override string Name => "Teleport";
public override string RequiredCommand => "tpto";
private readonly CommandUIDropDown _playerDropDown = new CommandUIDropDown
{
Name = "Player",
GetData = () => IoCManager.Resolve<IPlayerManager>().Sessions.ToList<object>(),
GetDisplayName = (obj) => $"{((IPlayerSession) obj).Name} ({((IPlayerSession) obj).AttachedEntity?.Name})",
GetValueFromData = (obj) => ((IPlayerSession) obj).Name,
};
public override List<CommandUIControl> UI => new List<CommandUIControl>
{
_playerDropDown
};
public override void Submit()
{
IoCManager.Resolve<IClientConsole>().ProcessCommand($"tpto \"{_playerDropDown.GetValue()}\"");
}
}
private class AddAtmosCommandButton : UICommandButton
{
public override string Name => "Add Atmos";

View File

@@ -0,0 +1,50 @@
using Content.Shared.GameObjects.Components.Atmos.GasTank;
using JetBrains.Annotations;
using Robust.Client.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects.Components.UserInterface;
namespace Content.Client.UserInterface.Atmos.GasTank
{
[UsedImplicitly]
public class GasTankBoundUserInterface
: BoundUserInterface
{
public GasTankBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) :
base(owner, uiKey)
{
}
private GasTankWindow _window;
public void SetOutputPressure(in float value)
{
SendMessage(new GasTankSetPressureMessage {Pressure = value});
}
public void ToggleInternals()
{
SendMessage(new GasTankToggleInternalsMessage());
}
protected override void Open()
{
base.Open();
_window = new GasTankWindow(this);
_window.OnClose += Close;
_window.OpenCentered();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
_window.UpdateState((GasTankBoundUserInterfaceState) state);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_window.Close();
}
}
}

View File

@@ -0,0 +1,235 @@
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
using Content.Shared.GameObjects.Components.Atmos.GasTank;
using Robust.Client.Graphics.Drawing;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
namespace Content.Client.UserInterface.Atmos.GasTank
{
public class GasTankWindow
: BaseWindow
{
private GasTankBoundUserInterface _owner;
private readonly Label _lblName;
private readonly VBoxContainer _topContainer;
private readonly Control _contentContainer;
private readonly IResourceCache _resourceCache = default!;
private readonly RichTextLabel _lblPressure;
private readonly FloatSpinBox _spbPressure;
private readonly RichTextLabel _lblInternals;
private readonly Button _btnInternals;
public GasTankWindow(GasTankBoundUserInterface owner)
{
TextureButton btnClose;
_resourceCache = IoCManager.Resolve<IResourceCache>();
_owner = owner;
var rootContainer = new LayoutContainer {Name = "GasTankRoot"};
AddChild(rootContainer);
MouseFilter = MouseFilterMode.Stop;
var panelTex = _resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var back = new StyleBoxTexture
{
Texture = panelTex,
Modulate = Color.FromHex("#25252A"),
};
back.SetPatchMargin(StyleBox.Margin.All, 10);
var topPanel = new PanelContainer
{
PanelOverride = back,
MouseFilter = MouseFilterMode.Pass
};
var bottomWrap = new LayoutContainer
{
Name = "BottomWrap"
};
rootContainer.AddChild(topPanel);
rootContainer.AddChild(bottomWrap);
LayoutContainer.SetAnchorPreset(topPanel, LayoutContainer.LayoutPreset.Wide);
LayoutContainer.SetMarginBottom(topPanel, -85);
LayoutContainer.SetAnchorPreset(bottomWrap, LayoutContainer.LayoutPreset.VerticalCenterWide);
LayoutContainer.SetGrowHorizontal(bottomWrap, LayoutContainer.GrowDirection.Both);
var topContainerWrap = new VBoxContainer
{
Children =
{
(_topContainer = new VBoxContainer()),
new Control {CustomMinimumSize = (0, 110)}
}
};
rootContainer.AddChild(topContainerWrap);
LayoutContainer.SetAnchorPreset(topContainerWrap, LayoutContainer.LayoutPreset.Wide);
var font = _resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13);
var topRow = new MarginContainer
{
MarginLeftOverride = 4,
MarginTopOverride = 2,
MarginRightOverride = 12,
MarginBottomOverride = 2,
Children =
{
new HBoxContainer
{
Children =
{
(_lblName = new Label
{
Text = Loc.GetString("Gas Tank"),
FontOverride = font,
FontColorOverride = StyleNano.NanoGold,
SizeFlagsVertical = SizeFlags.ShrinkCenter
}),
new Control
{
CustomMinimumSize = (20, 0),
SizeFlagsHorizontal = SizeFlags.Expand
},
(btnClose = new TextureButton
{
StyleClasses = {SS14Window.StyleClassWindowCloseButton},
SizeFlagsVertical = SizeFlags.ShrinkCenter
})
}
}
}
};
var middle = new PanelContainer
{
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#202025")},
Children =
{
new MarginContainer
{
MarginLeftOverride = 8,
MarginRightOverride = 8,
MarginTopOverride = 4,
MarginBottomOverride = 4,
Children =
{
(_contentContainer = new VBoxContainer())
}
}
}
};
_topContainer.AddChild(topRow);
_topContainer.AddChild(new PanelContainer
{
CustomMinimumSize = (0, 2),
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#525252ff")}
});
_topContainer.AddChild(middle);
_topContainer.AddChild(new PanelContainer
{
CustomMinimumSize = (0, 2),
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#525252ff")}
});
_lblPressure = new RichTextLabel();
_contentContainer.AddChild(_lblPressure);
//internals
_lblInternals = new RichTextLabel
{CustomMinimumSize = (200, 0), SizeFlagsVertical = SizeFlags.ShrinkCenter};
_btnInternals = new Button {Text = Loc.GetString("Toggle")};
_contentContainer.AddChild(
new MarginContainer
{
MarginTopOverride = 7,
Children =
{
new HBoxContainer
{
Children = {_lblInternals, _btnInternals}
}
}
});
// Separator
_contentContainer.AddChild(new Control
{
CustomMinimumSize = new Vector2(0, 10)
});
_contentContainer.AddChild(new Label
{
Text = Loc.GetString("Output Pressure"),
Align = Label.AlignMode.Center
});
_spbPressure = new FloatSpinBox {IsValid = f => f >= 0 || f <= 3000};
_contentContainer.AddChild(
new MarginContainer
{
MarginRightOverride = 25,
MarginLeftOverride = 25,
MarginBottomOverride = 7,
Children =
{
_spbPressure
}
}
);
// Handlers
_spbPressure.OnValueChanged += args =>
{
_owner.SetOutputPressure(args.Value);
};
_btnInternals.OnPressed += args =>
{
_owner.ToggleInternals();
};
btnClose.OnPressed += _ => Close();
}
public void UpdateState(GasTankBoundUserInterfaceState state)
{
_lblPressure.SetMarkup(Loc.GetString("Pressure: {0:0.##} kPa", state.TankPressure));
_btnInternals.Disabled = !state.CanConnectInternals;
_lblInternals.SetMarkup(Loc.GetString("Internals: [color={0}]{1}[/color]",
state.InternalsConnected ? "green" : "red",
state.InternalsConnected ? "Connected" : "Disconnected"));
if (state.OutputPressure.HasValue)
{
_spbPressure.Value = state.OutputPressure.Value;
}
}
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
{
return DragMode.Move;
}
protected override bool HasPoint(Vector2 point)
{
return false;
}
}
}

View File

@@ -1,15 +1,25 @@
using Content.Shared.GameObjects.Components.Items;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.UserInterface
{
public class HandButton : ItemSlotButton
{
public HandButton(Texture texture, Texture storageTexture, HandLocation location) : base(texture, storageTexture)
public HandButton(Texture texture, Texture storageTexture, Texture blockedTexture, HandLocation location) : base(texture, storageTexture)
{
Location = location;
AddChild(Blocked = new TextureRect
{
Texture = blockedTexture,
TextureScale = (2, 2),
MouseFilter = MouseFilterMode.Stop,
Visible = false
});
}
public HandLocation Location { get; }
public TextureRect Blocked { get; }
}
}

View File

@@ -111,7 +111,8 @@ namespace Content.Client.UserInterface
{
var buttonTexture = HandTexture(buttonLocation);
var storageTexture = _resourceCache.GetTexture("/Textures/Interface/Inventory/back.png");
var button = new HandButton(buttonTexture, storageTexture, buttonLocation);
var blockedTexture = _resourceCache.GetTexture("/Textures/Interface/Inventory/blocked.png");
var button = new HandButton(buttonTexture, storageTexture, blockedTexture, buttonLocation);
var slot = hand.Name;
button.OnPressed += args => HandKeyBindDown(args, slot);

View File

@@ -0,0 +1,52 @@
using Content.Shared.GameObjects.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects.Components.UserInterface;
namespace Content.Client.ParticleAccelerator
{
public class ParticleAcceleratorBoundUserInterface : BoundUserInterface
{
private ParticleAcceleratorControlMenu _menu;
public ParticleAcceleratorBoundUserInterface([NotNull] ClientUserInterfaceComponent owner, [NotNull] object uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_menu = new ParticleAcceleratorControlMenu(this);
_menu.OnClose += Close;
_menu.OpenCentered();
}
public void SendEnableMessage(bool enable)
{
SendMessage(new ParticleAcceleratorSetEnableMessage(enable));
}
public void SendPowerStateMessage(ParticleAcceleratorPowerState state)
{
SendMessage(new ParticleAcceleratorSetPowerStateMessage(state));
}
public void SendScanPartsMessage()
{
SendMessage(new ParticleAcceleratorRescanPartsMessage());
}
protected override void UpdateState(BoundUserInterfaceState state)
{
_menu.DataUpdate((ParticleAcceleratorUIState) state);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_menu.Close();
}
}
}

View File

@@ -0,0 +1,545 @@
using System;
using Content.Client.Animations;
using Content.Client.UserInterface;
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
using Content.Shared.GameObjects.Components;
using Robust.Client.Animations;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Drawing;
using Robust.Client.Graphics.Shaders;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Noise;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.ParticleAccelerator
{
public sealed class ParticleAcceleratorControlMenu : BaseWindow
{
private ShaderInstance _greyScaleShader;
private readonly ParticleAcceleratorBoundUserInterface Owner;
private readonly Label _drawLabel;
private readonly NoiseGenerator _drawNoiseGenerator;
private readonly Button _onButton;
private readonly Button _offButton;
private readonly Button _scanButton;
private readonly Label _statusLabel;
private readonly SpinBox _stateSpinBox;
private readonly VBoxContainer _alarmControl;
private readonly Animation _alarmControlAnimation;
private readonly PASegmentControl _endCapTexture;
private readonly PASegmentControl _fuelChamberTexture;
private readonly PASegmentControl _controlBoxTexture;
private readonly PASegmentControl _powerBoxTexture;
private readonly PASegmentControl _emitterCenterTexture;
private readonly PASegmentControl _emitterRightTexture;
private readonly PASegmentControl _emitterLeftTexture;
private float _time;
private int _lastDraw;
private int _lastReceive;
private bool _blockSpinBox;
private bool _assembled;
private bool _shouldContinueAnimating;
public ParticleAcceleratorControlMenu(ParticleAcceleratorBoundUserInterface owner)
{
_greyScaleShader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("Greyscale").Instance();
Owner = owner;
_drawNoiseGenerator = new NoiseGenerator(NoiseGenerator.NoiseType.Fbm);
_drawNoiseGenerator.SetFrequency(0.5f);
var resourceCache = IoCManager.Resolve<IResourceCache>();
var font = resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13);
var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
MouseFilter = MouseFilterMode.Stop;
_alarmControlAnimation = new Animation
{
Length = TimeSpan.FromSeconds(1),
AnimationTracks =
{
new AnimationTrackControlProperty
{
Property = nameof(Control.Visible),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(true, 0),
new AnimationTrackProperty.KeyFrame(false, 0.75f),
}
}
}
};
var back = new StyleBoxTexture
{
Texture = panelTex,
Modulate = Color.FromHex("#25252A"),
};
back.SetPatchMargin(StyleBox.Margin.All, 10);
var back2 = new StyleBoxTexture(back)
{
Modulate = Color.FromHex("#202023")
};
AddChild(new PanelContainer
{
PanelOverride = back,
MouseFilter = MouseFilterMode.Pass
});
_stateSpinBox = new SpinBox
{
Value = 0,
};
_stateSpinBox.IsValid = StrengthSpinBoxValid;
_stateSpinBox.InitDefaultButtons();
_stateSpinBox.ValueChanged += PowerStateChanged;
_stateSpinBox.LineEditDisabled = true;
_offButton = new Button
{
ToggleMode = false,
Text = "Off",
StyleClasses = {StyleBase.ButtonOpenRight},
};
_offButton.OnPressed += args => owner.SendEnableMessage(false);
_onButton = new Button
{
ToggleMode = false,
Text = "On",
StyleClasses = {StyleBase.ButtonOpenLeft},
};
_onButton.OnPressed += args => owner.SendEnableMessage(true);
var closeButton = new TextureButton
{
StyleClasses = {"windowCloseButton"},
SizeFlagsHorizontal = SizeFlags.ShrinkEnd
};
closeButton.OnPressed += args => Close();
var serviceManual = new Label
{
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
StyleClasses = {StyleBase.StyleClassLabelSubText},
Text = Loc.GetString("Refer to p.132 of service manual")
};
_drawLabel = new Label();
var imgSize = new Vector2(32, 32);
AddChild(new VBoxContainer
{
Children =
{
new MarginContainer
{
MarginLeftOverride = 2,
MarginTopOverride = 2,
Children =
{
new Label
{
Text = Loc.GetString("Mark 2 Particle Accelerator"),
FontOverride = font,
FontColorOverride = StyleNano.NanoGold,
},
new MarginContainer
{
MarginRightOverride = 8,
Children =
{
closeButton
}
}
}
},
new PanelContainer
{
PanelOverride = new StyleBoxFlat {BackgroundColor = StyleNano.NanoGold},
CustomMinimumSize = (0, 2),
},
new Control
{
CustomMinimumSize = (0, 4)
},
new HBoxContainer
{
SizeFlagsVertical = SizeFlags.FillExpand,
Children =
{
new MarginContainer
{
MarginLeftOverride = 4,
Children =
{
new VBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
Children =
{
new HBoxContainer
{
Children =
{
new Label
{
Text = Loc.GetString("Power: "),
SizeFlagsHorizontal = SizeFlags.Expand
},
_offButton,
_onButton
}
},
new HBoxContainer
{
Children =
{
new Label
{
Text = Loc.GetString("Strength: "),
SizeFlagsHorizontal = SizeFlags.Expand
},
_stateSpinBox
}
},
new Control
{
CustomMinimumSize = (0, 10),
},
_drawLabel,
new Control
{
SizeFlagsVertical = SizeFlags.Expand
},
(_alarmControl = new VBoxContainer
{
Children =
{
new Label
{
Text = Loc.GetString("PARTICLE STRENGTH\nLIMITER FAILURE"),
FontColorOverride = Color.Red,
Align = Label.AlignMode.Center
},
serviceManual
}
}),
}
}
}
},
new VBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
Children =
{
(_statusLabel = new Label
{
SizeFlagsHorizontal = SizeFlags.ShrinkCenter
}),
new Control
{
CustomMinimumSize = (0, 20)
},
new PanelContainer
{
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
PanelOverride = back2,
Children =
{
new GridContainer
{
Columns = 3,
VSeparationOverride = 0,
HSeparationOverride = 0,
Children =
{
new Control {CustomMinimumSize = imgSize},
(_endCapTexture = Segment("end_cap")),
new Control {CustomMinimumSize = imgSize},
(_controlBoxTexture = Segment("control_box")),
(_fuelChamberTexture = Segment("fuel_chamber")),
new Control {CustomMinimumSize = imgSize},
new Control {CustomMinimumSize = imgSize},
(_powerBoxTexture = Segment("power_box")),
new Control {CustomMinimumSize = imgSize},
(_emitterLeftTexture = Segment("emitter_left")),
(_emitterCenterTexture = Segment("emitter_center")),
(_emitterRightTexture = Segment("emitter_right")),
}
}
}
},
(_scanButton = new Button
{
Text = Loc.GetString("Scan Parts"),
SizeFlagsHorizontal = SizeFlags.ShrinkCenter
})
}
}
}
},
new StripeBack
{
Children =
{
new MarginContainer
{
MarginLeftOverride = 4,
MarginTopOverride = 4,
MarginBottomOverride = 4,
Children =
{
new Label
{
Text = Loc.GetString("Ensure containment field is active before operation"),
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
StyleClasses = {StyleBase.StyleClassLabelSubText},
}
}
}
}
},
new MarginContainer
{
MarginLeftOverride = 12,
Children =
{
new HBoxContainer
{
Children =
{
new Label
{
Text = "FOO-BAR-BAZ",
StyleClasses = {StyleBase.StyleClassLabelSubText}
}
}
}
}
},
}
});
_scanButton.OnPressed += args => Owner.SendScanPartsMessage();
_alarmControl.AnimationCompleted += s =>
{
if (_shouldContinueAnimating)
{
_alarmControl.PlayAnimation(_alarmControlAnimation, "warningAnim");
}
else
{
_alarmControl.Visible = false;
}
};
PASegmentControl Segment(string name)
{
return new PASegmentControl(this, resourceCache, name);
}
}
private bool StrengthSpinBoxValid(int n)
{
return (n >= 0 && n <= 4 && !_blockSpinBox);
}
private void PowerStateChanged(object sender, ValueChangedEventArgs e)
{
ParticleAcceleratorPowerState newState;
switch (e.Value)
{
case 0:
newState = ParticleAcceleratorPowerState.Standby;
break;
case 1:
newState = ParticleAcceleratorPowerState.Level0;
break;
case 2:
newState = ParticleAcceleratorPowerState.Level1;
break;
case 3:
newState = ParticleAcceleratorPowerState.Level2;
break;
case 4:
newState = ParticleAcceleratorPowerState.Level3;
break;
default:
return;
}
Owner.SendPowerStateMessage(newState);
}
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
{
return DragMode.Move;
}
protected override Vector2 CalculateMinimumSize()
{
return (400, 300);
}
public void DataUpdate(ParticleAcceleratorUIState uiState)
{
_assembled = uiState.Assembled;
UpdateUI(uiState.Assembled, uiState.InterfaceBlock, uiState.Enabled,
uiState.WirePowerBlock);
_statusLabel.Text = Loc.GetString($"Status: {(uiState.Assembled ? "Operational" : "Incomplete")}");
UpdatePowerState(uiState.State, uiState.Enabled, uiState.Assembled,
uiState.MaxLevel);
UpdatePreview(uiState);
_lastDraw = uiState.PowerDraw;
_lastReceive = uiState.PowerReceive;
}
private void UpdatePowerState(ParticleAcceleratorPowerState state, bool enabled, bool assembled,
ParticleAcceleratorPowerState maxState)
{
_stateSpinBox.OverrideValue(state switch
{
ParticleAcceleratorPowerState.Standby => 0,
ParticleAcceleratorPowerState.Level0 => 1,
ParticleAcceleratorPowerState.Level1 => 2,
ParticleAcceleratorPowerState.Level2 => 3,
ParticleAcceleratorPowerState.Level3 => 4,
_ => 0
});
_shouldContinueAnimating = false;
_alarmControl.StopAnimation("warningAnim");
_alarmControl.Visible = false;
if (maxState == ParticleAcceleratorPowerState.Level3 && enabled == true && assembled == true)
{
_shouldContinueAnimating = true;
_alarmControl.PlayAnimation(_alarmControlAnimation, "warningAnim");
}
}
private void UpdateUI(bool assembled, bool blocked, bool enabled, bool powerBlock)
{
_onButton.Pressed = enabled;
_offButton.Pressed = !enabled;
var cantUse = !assembled || blocked || powerBlock;
_onButton.Disabled = cantUse;
_offButton.Disabled = cantUse;
_scanButton.Disabled = blocked;
var cantChangeLevel = !assembled || blocked;
_stateSpinBox.SetButtonDisabled(cantChangeLevel);
_blockSpinBox = cantChangeLevel;
}
private void UpdatePreview(ParticleAcceleratorUIState updateMessage)
{
_endCapTexture.SetPowerState(updateMessage, updateMessage.EndCapExists);
_fuelChamberTexture.SetPowerState(updateMessage, updateMessage.FuelChamberExists);
_controlBoxTexture.SetPowerState(updateMessage, true);
_powerBoxTexture.SetPowerState(updateMessage, updateMessage.PowerBoxExists);
_emitterCenterTexture.SetPowerState(updateMessage, updateMessage.EmitterCenterExists);
_emitterLeftTexture.SetPowerState(updateMessage, updateMessage.EmitterLeftExists);
_emitterRightTexture.SetPowerState(updateMessage, updateMessage.EmitterRightExists);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_assembled)
{
_drawLabel.Text = Loc.GetString("Draw: N/A");
return;
}
_time += args.DeltaSeconds;
var watts = 0;
if (_lastDraw != 0)
{
var val = _drawNoiseGenerator.GetNoise(_time);
watts = (int) (_lastDraw + val * 5);
}
_drawLabel.Text = Loc.GetString("Draw: {0:##,##0}/{1:##,##0} W", watts, _lastReceive);
}
private sealed class PASegmentControl : Control
{
private readonly ParticleAcceleratorControlMenu _menu;
private readonly string _baseState;
private readonly TextureRect _base;
private readonly TextureRect _unlit;
private readonly RSI _rsi;
public PASegmentControl(ParticleAcceleratorControlMenu menu, IResourceCache cache, string name)
{
_menu = menu;
_baseState = name;
_rsi = cache.GetResource<RSIResource>($"/Textures/Constructible/Power/PA/{name}.rsi").RSI;
AddChild(_base = new TextureRect {Texture = _rsi[$"{name}c"].Frame0});
AddChild(_unlit = new TextureRect());
}
public void SetPowerState(ParticleAcceleratorUIState state, bool exists)
{
_base.ShaderOverride = exists ? null : _menu._greyScaleShader;
_base.ModulateSelfOverride = exists ? (Color?)null : new Color(127, 127, 127);
if (!state.Enabled || !exists)
{
_unlit.Visible = false;
return;
}
_unlit.Visible = true;
var suffix = state.State switch
{
ParticleAcceleratorPowerState.Standby => "_unlitp",
ParticleAcceleratorPowerState.Level0 => "_unlitp0",
ParticleAcceleratorPowerState.Level1 => "_unlitp1",
ParticleAcceleratorPowerState.Level2 => "_unlitp2",
ParticleAcceleratorPowerState.Level3 => "_unlitp3",
_ => ""
};
if (!_rsi.TryGetState(_baseState + suffix, out var rState))
{
_unlit.Visible = false;
return;
}
_unlit.Texture = rState.Frame0;
}
protected override Vector2 CalculateMinimumSize()
{
return _rsi.Size;
}
}
}
}

View File

@@ -127,19 +127,26 @@ namespace Content.IntegrationTests
return grid;
}
protected async Task WaitUntil(IntegrationInstance instance, Func<bool> func, int tickStep = 10, int maxTicks = 600)
protected async Task WaitUntil(IntegrationInstance instance, Func<bool> func, int maxTicks = 600, int tickStep = 1)
{
var ticksAwaited = 0;
bool passed;
await instance.WaitIdleAsync();
while (!(passed = func()) && ticksAwaited < maxTicks)
{
await instance.WaitIdleAsync();
instance.RunTicks(tickStep);
ticksAwaited += tickStep;
var ticksToRun = tickStep;
if (ticksAwaited + tickStep > maxTicks)
{
ticksToRun = maxTicks - ticksAwaited;
}
await instance.WaitIdleAsync();
await instance.WaitRunTicks(ticksToRun);
ticksAwaited += ticksToRun;
}
Assert.That(passed);
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Atmos;
@@ -46,7 +47,7 @@ namespace Content.IntegrationTests.Tests.Body
var originalOxygen = 2;
var originalNitrogen = 8;
var breathedPercentage = Atmospherics.BreathPercentage;
var breathedPercentage = Atmospherics.BreathVolume / gas.Volume;
gas.AdjustMoles(Gas.Oxygen, originalOxygen);
gas.AdjustMoles(Gas.Nitrogen, originalNitrogen);
@@ -76,7 +77,7 @@ namespace Content.IntegrationTests.Tests.Body
lung.Exhale(1, gas);
var lungOxygenAfterExhale = lung.Air.GetMoles(Gas.Oxygen);
var exhaledOxygen = lungOxygenBeforeExhale - lungOxygenAfterExhale;
var exhaledOxygen = Math.Abs(lungOxygenBeforeExhale - lungOxygenAfterExhale);
// Not completely empty
Assert.Positive(lung.Air.Gases.Sum());

View File

@@ -14,7 +14,7 @@ using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests
namespace Content.IntegrationTests.Tests.Buckle
{
[TestFixture]
[TestOf(typeof(BuckleComponent))]
@@ -22,7 +22,7 @@ namespace Content.IntegrationTests.Tests
public class BuckleTest : ContentIntegrationTest
{
[Test]
public async Task Test()
public async Task BuckleUnbuckleCooldownRangeTest()
{
var server = StartServerDummyTicker();
@@ -31,7 +31,7 @@ namespace Content.IntegrationTests.Tests
BuckleComponent buckle = null;
StrapComponent strap = null;
server.Assert(() =>
await server.WaitAssertion(() =>
{
var mapManager = IoCManager.Resolve<IMapManager>();
@@ -82,9 +82,9 @@ namespace Content.IntegrationTests.Tests
});
// Wait enough ticks for the unbuckling cooldown to run out
server.RunTicks(60);
await server.WaitRunTicks(60);
server.Assert(() =>
await server.WaitAssertion(() =>
{
// Still buckled
Assert.True(buckle.Buckled);
@@ -115,9 +115,9 @@ namespace Content.IntegrationTests.Tests
});
// Wait enough ticks for the unbuckling cooldown to run out
server.RunTicks(60);
await server.WaitRunTicks(60);
server.Assert(() =>
await server.WaitAssertion(() =>
{
// Still buckled
Assert.True(buckle.Buckled);
@@ -159,17 +159,15 @@ namespace Content.IntegrationTests.Tests
human.Transform.WorldPosition += (1, 0);
});
server.RunTicks(1);
await server.WaitRunTicks(1);
server.Assert(() =>
await server.WaitAssertion(() =>
{
// No longer buckled
Assert.False(buckle.Buckled);
Assert.Null(buckle.BuckledTo);
Assert.IsEmpty(strap.BuckledEntities);
});
await server.WaitIdleAsync();
}
[Test]
@@ -177,14 +175,13 @@ namespace Content.IntegrationTests.Tests
{
var server = StartServer();
IEntity human = null;
IEntity chair = null;
IEntity human;
IEntity chair;
BuckleComponent buckle = null;
StrapComponent strap = null;
HandsComponent hands = null;
IBody body = null;
server.Assert(() =>
await server.WaitAssertion(() =>
{
var mapManager = IoCManager.Resolve<IMapManager>();
@@ -206,7 +203,7 @@ namespace Content.IntegrationTests.Tests
// Component sanity check
Assert.True(human.TryGetComponent(out buckle));
Assert.True(chair.TryGetComponent(out strap));
Assert.True(chair.HasComponent<StrapComponent>());
Assert.True(human.TryGetComponent(out hands));
Assert.True(human.TryGetComponent(out body));
@@ -226,9 +223,9 @@ namespace Content.IntegrationTests.Tests
}
});
server.RunTicks(10);
await server.WaitRunTicks(10);
server.Assert(() =>
await server.WaitAssertion(() =>
{
// Still buckled
Assert.True(buckle.Buckled);
@@ -248,9 +245,9 @@ namespace Content.IntegrationTests.Tests
}
});
server.RunTicks(10);
await server.WaitRunTicks(10);
server.Assert(() =>
await server.WaitAssertion(() =>
{
// Still buckled
Assert.True(buckle.Buckled);
@@ -261,8 +258,73 @@ namespace Content.IntegrationTests.Tests
Assert.Null(hands.GetItem(slot));
}
});
}
await server.WaitIdleAsync();
[Test]
public async Task ForceUnbuckleBuckleTest()
{
var server = StartServer();
IEntity human = null;
IEntity chair = null;
BuckleComponent buckle = null;
await server.WaitAssertion(() =>
{
var mapManager = IoCManager.Resolve<IMapManager>();
var mapId = new MapId(1);
mapManager.CreateNewMapEntity(mapId);
var entityManager = IoCManager.Resolve<IEntityManager>();
var gridId = new GridId(1);
var grid = mapManager.CreateGrid(mapId, gridId);
var coordinates = grid.GridEntityId.ToCoordinates();
var tileManager = IoCManager.Resolve<ITileDefinitionManager>();
var tileId = tileManager["underplating"].TileId;
var tile = new Tile(tileId);
grid.SetTile(coordinates, tile);
human = entityManager.SpawnEntity("HumanMob_Content", coordinates);
chair = entityManager.SpawnEntity("ChairWood", coordinates);
// Component sanity check
Assert.True(human.TryGetComponent(out buckle));
Assert.True(chair.HasComponent<StrapComponent>());
// Buckle
Assert.True(buckle.TryBuckle(human, chair));
Assert.NotNull(buckle.BuckledTo);
Assert.True(buckle.Buckled);
// Move the buckled entity away
human.Transform.LocalPosition += (100, 0);
});
await WaitUntil(server, () => !buckle.Buckled, maxTicks: 10);
Assert.False(buckle.Buckled);
await server.WaitAssertion(() =>
{
// Move the now unbuckled entity back onto the chair
human.Transform.LocalPosition -= (100, 0);
// Buckle
Assert.True(buckle.TryBuckle(human, chair));
Assert.NotNull(buckle.BuckledTo);
Assert.True(buckle.Buckled);
});
await server.WaitRunTicks(60);
await server.WaitAssertion(() =>
{
// Still buckled
Assert.NotNull(buckle.BuckledTo);
Assert.True(buckle.Buckled);
});
}
}
}

View File

@@ -0,0 +1,51 @@
using System.Threading.Tasks;
using Content.Server.GlobalVerbs;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage;
using NUnit.Framework;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Commands
{
[TestFixture]
[TestOf(typeof(RejuvenateVerb))]
public class RejuvenateTest : ContentIntegrationTest
{
[Test]
public async Task RejuvenateDeadTest()
{
var server = StartServerDummyTicker();
await server.WaitAssertion(() =>
{
var mapManager = IoCManager.Resolve<IMapManager>();
mapManager.CreateNewMapEntity(MapId.Nullspace);
var entityManager = IoCManager.Resolve<IEntityManager>();
var human = entityManager.SpawnEntity("HumanMob_Content", MapCoordinates.Nullspace);
// Sanity check
Assert.True(human.TryGetComponent(out IDamageableComponent damageable));
Assert.That(damageable.CurrentState, Is.EqualTo(DamageState.Alive));
// Kill the entity
damageable.ChangeDamage(DamageClass.Brute, 10000000, true);
// Check that it is dead
Assert.That(damageable.CurrentState, Is.EqualTo(DamageState.Dead));
// Rejuvenate them
RejuvenateVerb.PerformRejuvenate(human);
// Check that it is alive and with no damage
Assert.That(damageable.CurrentState, Is.EqualTo(DamageState.Alive));
Assert.That(damageable.TotalDamage, Is.Zero);
});
}
}
}

View File

@@ -0,0 +1,96 @@
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Gravity;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Gravity;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Utility;
using NUnit.Framework;
using Robust.Server.Interfaces.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Gravity
{
[TestFixture]
[TestOf(typeof(WeightlessSystem))]
[TestOf(typeof(GravityGeneratorComponent))]
public class WeightlessStatusTests : ContentIntegrationTest
{
[Test]
public async Task WeightlessStatusTest()
{
var server = StartServer();
await server.WaitIdleAsync();
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var pauseManager = server.ResolveDependency<IPauseManager>();
var tileDefinitionManager = server.ResolveDependency<ITileDefinitionManager>();
IEntity human = null;
SharedStatusEffectsComponent statusEffects = null;
await server.WaitAssertion(() =>
{
var mapId = mapManager.CreateMap();
pauseManager.AddUninitializedMap(mapId);
var gridId = new GridId(1);
if (!mapManager.TryGetGrid(gridId, out var grid))
{
grid = mapManager.CreateGrid(mapId, gridId);
}
var tileDefinition = tileDefinitionManager["underplating"];
var tile = new Tile(tileDefinition.TileId);
var coordinates = grid.ToCoordinates();
grid.SetTile(coordinates, tile);
pauseManager.DoMapInitialize(mapId);
human = entityManager.SpawnEntity("HumanMob_Content", coordinates);
Assert.True(human.TryGetComponent(out statusEffects));
});
// Let WeightlessSystem and GravitySystem tick
await server.WaitRunTicks(1);
GravityGeneratorComponent gravityGenerator = null;
await server.WaitAssertion(() =>
{
// No gravity without a gravity generator
Assert.True(statusEffects.Statuses.ContainsKey(StatusEffect.Weightless));
gravityGenerator = human.EnsureComponent<GravityGeneratorComponent>();
});
// Let WeightlessSystem and GravitySystem tick
await server.WaitRunTicks(1);
await server.WaitAssertion(() =>
{
Assert.False(statusEffects.Statuses.ContainsKey(StatusEffect.Weightless));
// Disable the gravity generator
var args = new BreakageEventArgs {Owner = human};
gravityGenerator.OnBreak(args);
});
await server.WaitRunTicks(1);
await server.WaitAssertion(() =>
{
Assert.False(statusEffects.Statuses.ContainsKey(StatusEffect.Weightless));
});
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Content.Server.GameObjects.Components;
using Content.Server.GameObjects.Components.Power;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.Components.Power.PowerNetComponents;
@@ -34,6 +35,11 @@ namespace Content.IntegrationTests.Tests
var consumerEnt1 = entityMan.SpawnEntity("DebugConsumer", grid.ToCoordinates(0, 1));
var consumerEnt2 = entityMan.SpawnEntity("DebugConsumer", grid.ToCoordinates(0, 2));
if (generatorEnt.TryGetComponent(out AnchorableComponent anchorable))
{
anchorable.TryAnchor(null, force:true);
}
Assert.That(generatorEnt.TryGetComponent(out supplier));
Assert.That(consumerEnt1.TryGetComponent(out consumer1));
Assert.That(consumerEnt2.TryGetComponent(out consumer2));

View File

@@ -65,6 +65,38 @@ namespace Content.Server.Atmos
}
}
/// <summary>
/// Heat capacity ratio of gas mixture
/// </summary>
[ViewVariables]
public float HeatCapacityRatio
{
get
{
var delimiterSum = 0f;
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
delimiterSum += _moles[i] / (_atmosphereSystem.GetGas(i).HeatCapacityRatio - 1);
}
return 1 + TotalMoles / delimiterSum;
}
}
public float MolarMass
{
get
{
var molarMass = 0f;
var totalMoles = TotalMoles;
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
molarMass += _atmosphereSystem.GetGas(i).MolarMass * (_moles[i] / totalMoles);
}
return molarMass;
}
}
[ViewVariables]
public float HeatCapacityArchived
{
@@ -136,14 +168,15 @@ namespace Content.Server.Atmos
public GasMixture(AtmosphereSystem? atmosphereSystem)
{
_atmosphereSystem = atmosphereSystem ?? EntitySystem.Get<AtmosphereSystem>();
_moles = new float[_atmosphereSystem.Gases.Count()];
_molesArchived = new float[_moles.Length];
}
public GasMixture(float volume, AtmosphereSystem? atmosphereSystem = null)
public GasMixture(float volume, AtmosphereSystem? atmosphereSystem = null): this(atmosphereSystem)
{
if (volume < 0)
volume = 0;
Volume = volume;
_atmosphereSystem = atmosphereSystem ?? EntitySystem.Get<AtmosphereSystem>();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -2,6 +2,7 @@
using System;
using Content.Server.GameObjects.Components.Atmos;
using Content.Shared.Atmos;
using Content.Shared.Physics;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.Interfaces.Physics;
using Robust.Shared.Interfaces.Random;
@@ -13,7 +14,7 @@ using Robust.Shared.Random;
namespace Content.Server.Atmos
{
public class HighPressureMovementController : VirtualController
public class HighPressureMovementController : FrictionController
{
[Dependency] private IRobustRandom _robustRandom = default!;
[Dependency] private IPhysicsManager _physicsManager = default!;
@@ -69,17 +70,5 @@ namespace Content.Server.Atmos
}
}
}
public override void UpdateAfterProcessing()
{
base.UpdateAfterProcessing();
if (ControlledComponent != null && !_physicsManager.IsWeightless(ControlledComponent.Owner.Transform.Coordinates))
{
LinearVelocity *= 0.85f;
if (MathF.Abs(LinearVelocity.Length) < 1f)
Stop();
}
}
}
}

View File

@@ -1127,6 +1127,10 @@ namespace Content.Server.Atmos
UpdateVisuals();
if (!Excited)
{
_gridAtmosphereComponent.AddActiveTile(this);
}
return true;
}

View File

@@ -10,6 +10,7 @@
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
<NoWarn>1998</NoWarn>
<WarningsAsErrors>CS8604;CS8765</WarningsAsErrors>
</PropertyGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>

View File

@@ -0,0 +1,198 @@
using Content.Server.Interfaces;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using System.Collections.Generic;
namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork
{
public delegate void OnReceiveNetMessage(int frequency, string sender, IReadOnlyDictionary<string, string> payload, Metadata metadata, bool broadcast);
public class DeviceNetwork : IDeviceNetwork
{
private const int PACKAGES_PER_TICK = 30;
private readonly IRobustRandom _random = IoCManager.Resolve<IRobustRandom>();
private readonly Dictionary<int, List<NetworkDevice>> _devices = new Dictionary<int, List<NetworkDevice>>();
private readonly Queue<NetworkPackage> _packages = new Queue<NetworkPackage>();
/// <inheritdoc/>
public DeviceNetworkConnection Register(int netId, int frequency, OnReceiveNetMessage messageHandler, bool receiveAll = false)
{
var address = GenerateValidAddress(netId, frequency);
var device = new NetworkDevice
{
Address = address,
Frequency = frequency,
ReceiveAll = receiveAll,
ReceiveNetMessage = messageHandler
};
AddDevice(netId, device);
return new DeviceNetworkConnection(this, netId, address, frequency);
}
public DeviceNetworkConnection Register(int netId, OnReceiveNetMessage messageHandler, bool receiveAll = false)
{
return Register(netId, 0, messageHandler, receiveAll);
}
public void Update()
{
var i = PACKAGES_PER_TICK;
while (_packages.Count > 0 && i > 0)
{
i--;
var package = _packages.Dequeue();
if (package.Broadcast)
{
BroadcastPackage(package);
continue;
}
SendPackage(package);
}
}
public bool EnqueuePackage(int netId, int frequency, string address, IReadOnlyDictionary<string, string> data, string sender, Metadata metadata, bool broadcast = false)
{
if (!_devices.ContainsKey(netId))
return false;
var package = new NetworkPackage()
{
NetId = netId,
Frequency = frequency,
Address = address,
Broadcast = broadcast,
Data = data,
Sender = sender,
Metadata = metadata
};
_packages.Enqueue(package);
return true;
}
public void RemoveDevice(int netId, int frequency, string address)
{
var device = DeviceWithAddress(netId, frequency, address);
_devices[netId].Remove(device);
}
public void SetDeviceReceiveAll(int netId, int frequency, string address, bool receiveAll)
{
var device = DeviceWithAddress(netId, frequency, address);
device.ReceiveAll = receiveAll;
}
public bool GetDeviceReceiveAll(int netId, int frequency, string address)
{
var device = DeviceWithAddress(netId, frequency, address);
return device.ReceiveAll;
}
private string GenerateValidAddress(int netId, int frequency)
{
var unique = false;
var devices = DevicesForFrequency(netId, frequency);
var address = "";
while (!unique)
{
address = _random.Next().ToString("x");
unique = !devices.Exists(device => device.Address == address);
}
return address;
}
private void AddDevice(int netId, NetworkDevice networkDevice)
{
if(!_devices.ContainsKey(netId))
_devices[netId] = new List<NetworkDevice>();
_devices[netId].Add(networkDevice);
}
private List<NetworkDevice> DevicesForFrequency(int netId, int frequency)
{
if (!_devices.ContainsKey(netId))
return new List<NetworkDevice>();
var result = _devices[netId].FindAll(device => device.Frequency == frequency);
return result;
}
private NetworkDevice DeviceWithAddress(int netId, int frequency, string address)
{
var devices = DevicesForFrequency(netId, frequency);
var device = devices.Find(device => device.Address == address);
return device;
}
private List<NetworkDevice> DevicesWithReceiveAll(int netId, int frequency)
{
if (!_devices.ContainsKey(netId))
return new List<NetworkDevice>();
var result = _devices[netId].FindAll(device => device.Frequency == frequency && device.ReceiveAll);
return result;
}
private void BroadcastPackage(NetworkPackage package)
{
var devices = DevicesForFrequency(package.NetId, package.Frequency);
SendToDevices(devices, package, true);
}
private void SendPackage(NetworkPackage package)
{
var devices = DevicesWithReceiveAll(package.NetId, package.Frequency);
var device = DeviceWithAddress(package.NetId, package.Frequency, package.Address);
devices.Add(device);
SendToDevices(devices, package, false);
}
private void SendToDevices(List<NetworkDevice> devices, NetworkPackage package, bool broadcast)
{
for (var index = 0; index < devices.Count; index++)
{
var device = devices[index];
if (device.Address == package.Sender)
continue;
device.ReceiveNetMessage(package.Frequency, package.Sender, package.Data, package.Metadata, broadcast);
}
}
internal class NetworkDevice
{
public int Frequency;
public string Address;
public OnReceiveNetMessage ReceiveNetMessage;
public bool ReceiveAll;
}
internal class NetworkPackage
{
public int NetId;
public int Frequency;
public string Address;
public bool Broadcast;
public IReadOnlyDictionary<string, string> Data { get; set; }
public Metadata Metadata;
public string Sender;
}
}
}

View File

@@ -0,0 +1,72 @@
using Content.Server.Interfaces;
using Robust.Shared.ViewVariables;
using System.Collections.Generic;
namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork
{
public class DeviceNetworkConnection : IDeviceNetworkConnection
{
private readonly DeviceNetwork _network;
[ViewVariables]
private readonly int _netId;
[ViewVariables]
public bool Open { get; internal set; }
[ViewVariables]
public string Address { get; internal set; }
[ViewVariables]
public int Frequency { get; internal set; }
[ViewVariables]
public bool RecieveAll
{
get => _network.GetDeviceReceiveAll(_netId, Frequency, Address);
set => _network.SetDeviceReceiveAll(_netId, Frequency, Address, value);
}
public DeviceNetworkConnection(DeviceNetwork network, int netId, string address, int frequency)
{
_network = network;
_netId = netId;
Open = true;
Address = address;
Frequency = frequency;
}
public bool Send(int frequency, string address, IReadOnlyDictionary<string, string> payload, Metadata metadata)
{
return Open && _network.EnqueuePackage(_netId, frequency, address, payload, Address, metadata);
}
public bool Send(int frequency, string address, Dictionary<string, string> payload)
{
return Send(frequency, address, payload);
}
public bool Send(string address, Dictionary<string, string> payload)
{
return Send(0, address, payload);
}
public bool Broadcast(int frequency, IReadOnlyDictionary<string, string> payload, Metadata metadata)
{
return Open && _network.EnqueuePackage(_netId, frequency, "", payload, Address, metadata, true);
}
public bool Broadcast(int frequency, Dictionary<string, string> payload)
{
return Broadcast(frequency, payload);
}
public bool Broadcast(Dictionary<string, string> payload)
{
return Broadcast(0, payload);
}
public void Close()
{
_network.RemoveDevice(_netId, Frequency, Address);
Open = false;
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork
{
public class Metadata : Dictionary<string, object>
{
public bool TryParseMetadata<T>(string key, [NotNullWhen(true)] out T data)
{
if(TryGetValue(key, out var value) && value is T typedValue)
{
data = typedValue;
return true;
}
data = default;
return false;
}
}
}

View File

@@ -0,0 +1,70 @@
using Content.Server.Interfaces;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables;
using System.Collections.Generic;
namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork
{
public abstract class BaseNetworkConnection : IDeviceNetworkConnection
{
protected readonly DeviceNetworkConnection Connection;
protected OnReceiveNetMessage MessageHandler;
[ViewVariables]
public bool Open => Connection.Open;
[ViewVariables]
public string Address => Connection.Address;
[ViewVariables]
public int Frequency => Connection.Frequency;
protected BaseNetworkConnection(int netId, int frequency, OnReceiveNetMessage onReceive, bool receiveAll)
{
var network = IoCManager.Resolve<IDeviceNetwork>();
Connection = network.Register(netId, frequency, OnReceiveNetMessage, receiveAll);
MessageHandler = onReceive;
}
public bool Send(int frequency, string address, Dictionary<string, string> payload)
{
var data = ManipulatePayload(payload);
var metadata = GetMetadata();
return Connection.Send(frequency, address, data, metadata);
}
public bool Send(string address, Dictionary<string, string> payload)
{
return Send(0, address, payload);
}
public bool Broadcast(int frequency, Dictionary<string, string> payload)
{
var data = ManipulatePayload(payload);
var metadata = GetMetadata();
return Connection.Broadcast(frequency, data, metadata);
}
public bool Broadcast(Dictionary<string, string> payload)
{
return Broadcast(0, payload);
}
public void Close()
{
Connection.Close();
}
private void OnReceiveNetMessage(int frequency, string sender, IReadOnlyDictionary<string, string> payload, Metadata metadata, bool broadcast)
{
if (CanReceive(frequency, sender, payload, metadata, broadcast))
{
MessageHandler(frequency, sender, payload, metadata, broadcast);
}
}
protected abstract bool CanReceive(int frequency, string sender, IReadOnlyDictionary<string, string> payload, Metadata metadata, bool broadcast);
protected abstract Dictionary<string, string> ManipulatePayload(Dictionary<string, string> payload);
protected abstract Metadata GetMetadata();
}
}

View File

@@ -0,0 +1,84 @@
using Content.Server.GameObjects.Components.NodeContainer;
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Robust.Shared.Interfaces.GameObjects;
using System.Collections.Generic;
namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork
{
public class WiredNetworkConnection : BaseNetworkConnection
{
public const string WIRENET = "powernet";
private readonly IEntity _owner;
public WiredNetworkConnection(OnReceiveNetMessage onReceive, bool receiveAll, IEntity owner) : base(NetworkUtils.WIRED, 0, onReceive, receiveAll)
{
_owner = owner;
}
protected override bool CanReceive(int frequency, string sender, IReadOnlyDictionary<string, string> payload, Metadata metadata, bool broadcast)
{
if (_owner.Deleted)
{
Connection.Close();
return false;
}
if (_owner.TryGetComponent<PowerReceiverComponent>(out var powerReceiver)
&& TryGetWireNet(powerReceiver, out var ownNet)
&& metadata.TryParseMetadata<INodeGroup>(WIRENET, out var senderNet))
{
return ownNet.Equals(senderNet);
}
return false;
}
protected override Metadata GetMetadata()
{
if (_owner.Deleted)
{
Connection.Close();
return new Metadata();
}
if (_owner.TryGetComponent<PowerReceiverComponent>(out var powerReceiver)
&& TryGetWireNet(powerReceiver, out var net))
{
var metadata = new Metadata
{
{WIRENET, net }
};
return metadata;
}
return new Metadata();
}
protected override Dictionary<string, string> ManipulatePayload(Dictionary<string, string> payload)
{
return payload;
}
private bool TryGetWireNet(PowerReceiverComponent powerReceiver, out INodeGroup net)
{
if (powerReceiver.Provider is PowerProviderComponent && powerReceiver.Provider.ProviderOwner.TryGetComponent<NodeContainerComponent>(out var nodeContainer))
{
var nodes = nodeContainer.Nodes;
for (var index = 0; index < nodes.Count; index++)
{
if (nodes[index].NodeGroupID == NodeGroupID.WireNet)
{
net = nodes[index].NodeGroup;
return true;
}
}
}
net = default;
return false;
}
}
}

View File

@@ -0,0 +1,63 @@
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Maths;
using System;
using System.Collections.Generic;
namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork
{
public class WirelessNetworkConnection : BaseNetworkConnection
{
public const string WIRELESS_POSITION = "position";
private readonly IEntity _owner;
private float _range;
public float Range { get => _range; set => _range = Math.Abs(value); }
public WirelessNetworkConnection(int frequency, OnReceiveNetMessage onReceive, bool receiveAll, IEntity owner, float range) : base(NetworkUtils.WIRELESS, frequency, onReceive, receiveAll)
{
_owner = owner;
Range = range;
}
protected override bool CanReceive(int frequency, string sender, IReadOnlyDictionary<string, string> payload, Metadata metadata, bool broadcast)
{
if (_owner.Deleted)
{
Connection.Close();
return false;
}
if (metadata.TryParseMetadata<Vector2>(WIRELESS_POSITION, out var position))
{
var ownPosition = _owner.Transform.WorldPosition;
var distance = (ownPosition - position).Length;
return distance <= Range;
}
//Only receive packages with the same frequency
return frequency == Frequency;
}
protected override Metadata GetMetadata()
{
if (_owner.Deleted)
{
Connection.Close();
return new Metadata();
}
var position = _owner.Transform.WorldPosition;
var metadata = new Metadata
{
{WIRELESS_POSITION, position}
};
return metadata;
}
protected override Dictionary<string, string> ManipulatePayload(Dictionary<string, string> payload)
{
return payload;
}
}
}

View File

@@ -0,0 +1,38 @@
using Content.Server.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace Content.Server.GameObjects.EntitySystems.DeviceNetwork
{
/// <summary>
/// A collection of utilities to help with using device networks
/// </summary>
public static class NetworkUtils
{
public const int PRIVATE = 0;
public const int WIRED = 1;
public const int WIRELESS = 2;
public const string COMMAND = "command";
public const string MESSAGE = "message";
public const string PING = "ping";
/// <summary>
/// Handles responding to pings.
/// </summary>
public static void PingResponse<T>(T connection, string sender, IReadOnlyDictionary<string, string> payload, string message = "") where T : IDeviceNetworkConnection
{
if (payload.TryGetValue(COMMAND, out var command) && command == PING)
{
var response = new Dictionary<string, string>
{
{COMMAND, "ping_response"},
{MESSAGE, message}
};
connection.Send(connection.Frequency, sender, response);
}
}
}
}

View File

@@ -13,6 +13,7 @@ using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.ComponentDependencies;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
@@ -29,10 +30,11 @@ namespace Content.Server.GameObjects.Components.Arcade
public override string Name => "BlockGameArcade";
public override uint? NetID => ContentNetIDs.BLOCKGAME_ARCADE;
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
[ComponentDependency] private PowerReceiverComponent? _powerReceiverComponent = default!;
private bool Powered => _powerReceiverComponent?.Powered ?? false;
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(BlockGameUiKey.Key);
private BlockGame _game = null!;
private BlockGame? _game;
private IPlayerSession? _player;
private List<IPlayerSession> _spectators = new List<IPlayerSession>();
@@ -47,7 +49,7 @@ namespace Content.Server.GameObjects.Components.Arcade
{
return;
}
if(!ActionBlockerSystem.CanInteract(Owner)) return;
if(!ActionBlockerSystem.CanInteract(actor.playerSession.AttachedEntity)) return;
UserInterface?.Toggle(actor.playerSession);
RegisterPlayerSession(actor.playerSession);
@@ -59,7 +61,7 @@ namespace Content.Server.GameObjects.Components.Arcade
else _spectators.Add(session);
UpdatePlayerStatus(session);
_game.UpdateNewPlayerUI(session);
_game?.UpdateNewPlayerUI(session);
}
private void DeactivePlayer(IPlayerSession session)
@@ -104,38 +106,56 @@ namespace Content.Server.GameObjects.Components.Arcade
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
if (_powerReceiverComponent != null)
{
_powerReceiverComponent.OnPowerStateChanged += OnPowerStateChanged;
}
_game = new BlockGame(this);
}
private void OnPowerStateChanged(object? sender, PowerStateEventArgs e)
{
if (e.Powered) return;
UserInterface?.CloseAll();
_player = null;
_spectators.Clear();
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Message is BlockGameMessages.BlockGameUserUnregisterMessage unregisterMessage)
switch (obj.Message)
{
case BlockGameMessages.BlockGameUserUnregisterMessage unregisterMessage:
UnRegisterPlayerSession(obj.Session);
return;
}
if (obj.Session != _player) return;
break;
case BlockGameMessages.BlockGamePlayerActionMessage playerActionMessage:
if (obj.Session != _player) break;
if (!ActionBlockerSystem.CanInteract(Owner))
{
DeactivePlayer(obj.Session);
break;
}
if (!(obj.Message is BlockGameMessages.BlockGamePlayerActionMessage message)) return;
if (message.PlayerAction == BlockGamePlayerAction.NewGame)
if (playerActionMessage.PlayerAction == BlockGamePlayerAction.NewGame)
{
if(_game.Started) _game = new BlockGame(this);
_game.StartGame();
if(_game?.Started == true) _game = new BlockGame(this);
_game?.StartGame();
}
else
{
_game.ProcessInput(message.PlayerAction);
_game?.ProcessInput(playerActionMessage.PlayerAction);
}
break;
}
}
public void DoGameTick(float frameTime)
{
_game.GameTick(frameTime);
_game?.GameTick(frameTime);
}
private class BlockGame
@@ -196,13 +216,12 @@ namespace Content.Server.GameObjects.Components.Arcade
private Vector2i _currentPiecePosition;
private BlockGamePieceRotation _currentRotation;
private float _softDropOverride = 0.1f;
private float _softDropModifier = 0.1f;
private float Speed => !_softDropPressed
? -0.03f * Level + 1
: _softDropOverride;
private float Speed =>
-0.03f * Level + 1 * (!_softDropPressed ? 1 : _softDropModifier);
private float _pressCheckSpeed = 0.08f;
private const float _pressCheckSpeed = 0.08f;
private bool _running;
public bool Paused => !(_running && _started);
@@ -278,7 +297,8 @@ namespace Content.Server.GameObjects.Components.Arcade
public BlockGame(BlockGameArcadeComponent component)
{
_component = component;
_internalNextPiece = BlockGamePiece.GetRandom(_component._random);
_allBlockGamePieces = (BlockGamePieceType[]) Enum.GetValues(typeof(BlockGamePieceType));
_internalNextPiece = GetRandomBlockGamePiece(_component._random);
}
private void SendHighscoreUpdate()
@@ -343,7 +363,7 @@ namespace Content.Server.GameObjects.Components.Arcade
{
_accumulatedLeftPressTime += frameTime;
if (_accumulatedLeftPressTime >= _pressCheckSpeed)
while (_accumulatedLeftPressTime >= _pressCheckSpeed)
{
if (_currentPiece.Positions(_currentPiecePosition.AddToX(-1), _currentRotation)
@@ -361,7 +381,7 @@ namespace Content.Server.GameObjects.Components.Arcade
{
_accumulatedRightPressTime += frameTime;
if (_accumulatedRightPressTime >= _pressCheckSpeed)
while (_accumulatedRightPressTime >= _pressCheckSpeed)
{
if (_currentPiece.Positions(_currentPiecePosition.AddToX(1), _currentRotation)
.All(MoveCheck))
@@ -384,14 +404,15 @@ namespace Content.Server.GameObjects.Components.Arcade
var checkTime = Speed;
if (_accumulatedFieldFrameTime < checkTime) return;
while (_accumulatedFieldFrameTime >= checkTime)
{
if (_softDropPressed) AddPoints(1);
InternalFieldTick();
_accumulatedFieldFrameTime -= checkTime;
}
}
private void InternalFieldTick()
{
@@ -490,7 +511,7 @@ namespace Content.Server.GameObjects.Components.Arcade
private void InitializeNewBlock()
{
InitializeNewBlock(_nextPiece);
_nextPiece = BlockGamePiece.GetRandom(_component._random);
_nextPiece = GetRandomBlockGamePiece(_component._random);
_holdBlock = false;
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock));
@@ -538,6 +559,7 @@ namespace Content.Server.GameObjects.Components.Arcade
break;
case BlockGamePlayerAction.SoftdropStart:
_softDropPressed = true;
if (_accumulatedFieldFrameTime > Speed) _accumulatedFieldFrameTime = Speed; //to prevent jumps
break;
case BlockGamePlayerAction.SoftdropEnd:
_softDropPressed = false;
@@ -707,6 +729,22 @@ namespace Content.Server.GameObjects.Components.Arcade
};
}
private readonly BlockGamePieceType[] _allBlockGamePieces;
private List<BlockGamePieceType> _blockGamePiecesBuffer = new List<BlockGamePieceType>();
private BlockGamePiece GetRandomBlockGamePiece(IRobustRandom random)
{
if (_blockGamePiecesBuffer.Count == 0)
{
_blockGamePiecesBuffer = _allBlockGamePieces.ToList();
}
var chosenPiece = random.Pick(_blockGamePiecesBuffer);
_blockGamePiecesBuffer.Remove(chosenPiece);
return BlockGamePiece.GetPiece(chosenPiece);
}
private struct BlockGamePiece
{
public Vector2i[] Offsets;
@@ -770,13 +808,6 @@ namespace Content.Server.GameObjects.Components.Arcade
return Blocks(new Vector2i(-xOffset, -yOffset), BlockGamePieceRotation.North);
}
public static BlockGamePiece GetRandom(IRobustRandom random)
{
var pieces = (BlockGamePieceType[])Enum.GetValues(typeof(BlockGamePieceType));
var choice = random.Pick(pieces);
return GetPiece(choice);
}
public static BlockGamePiece GetPiece(BlockGamePieceType type)
{
//switch statement, hardcoded offsets

View File

@@ -0,0 +1,34 @@
using System;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Random;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Arcade
{
[RegisterComponent]
public class RandomArcadeGameComponent : Component, IMapInit
{
public override string Name => "RandomArcade";
public void MapInit()
{
var arcades = new[]
{
"BlockGameArcade",
"SpaceVillainArcade"
};
var entityManager = IoCManager.Resolve<IEntityManager>();
entityManager.SpawnEntity(
IoCManager.Resolve<IRobustRandom>().Pick(arcades),
Owner.Transform.Coordinates);
Owner.Delete();
}
}
}

View File

@@ -5,16 +5,20 @@ using Content.Server.GameObjects.Components.VendingMachines;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Arcade;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.ComponentDependencies;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
@@ -27,7 +31,10 @@ namespace Content.Server.GameObjects.Components.Arcade
{
[Dependency] private IRobustRandom _random = null!;
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
[ComponentDependency] private PowerReceiverComponent? _powerReceiverComponent = default!;
[ComponentDependency] private WiresComponent? _wiresComponent = default!;
private bool Powered => _powerReceiverComponent != null && _powerReceiverComponent.Powered;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SpaceVillainArcadeUiKey.Key);
[ViewVariables] private bool _overflowFlag;
@@ -73,11 +80,11 @@ namespace Content.Server.GameObjects.Components.Arcade
{
return;
}
if(!ActionBlockerSystem.CanInteract(actor.playerSession.AttachedEntity)) return;
var wires = Owner.GetComponent<WiresComponent>();
if (wires.IsPanelOpen)
if (_wiresComponent?.IsPanelOpen == true)
{
wires.OpenInterface(actor.playerSession);
_wiresComponent.OpenInterface(actor.playerSession);
} else
{
UserInterface?.Toggle(actor.playerSession);
@@ -92,6 +99,18 @@ namespace Content.Server.GameObjects.Components.Arcade
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
if (_powerReceiverComponent != null)
{
_powerReceiverComponent.OnPowerStateChanged += OnOnPowerStateChanged;
}
}
private void OnOnPowerStateChanged(object? sender, PowerStateEventArgs e)
{
if(e.Powered) return;
UserInterface?.CloseAll();
}
@@ -145,6 +164,10 @@ namespace Content.Server.GameObjects.Components.Arcade
builder.CreateWire(Wires.Overflow);
builder.CreateWire(Wires.PlayerInvincible);
builder.CreateWire(Wires.EnemyInvincible);
builder.CreateWire(4);
builder.CreateWire(5);
builder.CreateWire(6);
IndicatorUpdate();
}
public void WiresUpdate(WiresUpdateEventArgs args)
@@ -163,6 +186,24 @@ namespace Content.Server.GameObjects.Components.Arcade
_enemyInvincibilityFlag = value;
break;
}
IndicatorUpdate();
}
public void IndicatorUpdate()
{
_wiresComponent?.SetStatus(Indicators.HealthManager,
new SharedWiresComponent.StatusLightData(Color.Purple,
_playerInvincibilityFlag || _enemyInvincibilityFlag
? SharedWiresComponent.StatusLightState.BlinkingSlow
: SharedWiresComponent.StatusLightState.On,
"MNGR"));
_wiresComponent?.SetStatus(Indicators.HealthLimiter,
new SharedWiresComponent.StatusLightData(Color.Red,
_overflowFlag
? SharedWiresComponent.StatusLightState.BlinkingSlow
: SharedWiresComponent.StatusLightState.On,
"LIMT"));
}
/// <summary>
@@ -257,7 +298,7 @@ namespace Content.Server.GameObjects.Components.Arcade
{
case PlayerAction.Attack:
var attackAmount = _random.Next(2, 6);
_latestPlayerActionMessage = $"You attack {_enemyName} for {attackAmount}!";
_latestPlayerActionMessage = Loc.GetString("You attack {0} for {1}!", _enemyName, attackAmount);
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Effects/Arcade/player_attack.ogg", Owner.Owner, AudioParams.Default.WithVolume(-4f));
if(!Owner._enemyInvincibilityFlag) _enemyHp -= attackAmount;
_turtleTracker -= _turtleTracker > 0 ? 1 : 0;
@@ -265,24 +306,23 @@ namespace Content.Server.GameObjects.Components.Arcade
case PlayerAction.Heal:
var pointAmount = _random.Next(1, 3);
var healAmount = _random.Next(6, 8);
_latestPlayerActionMessage = $"You use {pointAmount} magic to heal for {healAmount} damage!";
_latestPlayerActionMessage = Loc.GetString("You use {0} magic to heal for {1} damage!", pointAmount, healAmount);
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Effects/Arcade/player_heal.ogg", Owner.Owner, AudioParams.Default.WithVolume(-4f));
if(!Owner._playerInvincibilityFlag) _playerMp -= pointAmount;
_playerHp += healAmount;
_turtleTracker++;
break;
case PlayerAction.Recharge:
var charge_amount = _random.Next(4, 7);
_latestPlayerActionMessage = $"You regain {charge_amount} points";
var chargeAmount = _random.Next(4, 7);
_latestPlayerActionMessage = Loc.GetString("You regain {0} points", chargeAmount);
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Effects/Arcade/player_charge.ogg", Owner.Owner, AudioParams.Default.WithVolume(-4f));
_playerMp += charge_amount;
_playerMp += chargeAmount;
_turtleTracker -= _turtleTracker > 0 ? 1 : 0;
break;
}
if (!CheckGameConditions())
{
_running = false;
return;
}
@@ -291,7 +331,6 @@ namespace Content.Server.GameObjects.Components.Arcade
if (!CheckGameConditions())
{
_running = false;
return;
}
ValidateVars();
@@ -304,22 +343,28 @@ namespace Content.Server.GameObjects.Components.Arcade
/// <returns>A bool indicating if the game should continue.</returns>
private bool CheckGameConditions()
{
if ((_enemyHp <= 0 || _enemyMp <= 0) && (_playerHp > 0 && _playerMp > 0))
if ((_playerHp > 0 && _playerMp > 0) && (_enemyHp <= 0 || _enemyMp <= 0))
{
UpdateUi("You won!", $"{_enemyName} dies.");
_running = false;
UpdateUi(Loc.GetString("You won!"), Loc.GetString("{0} dies.", _enemyName), true);
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Effects/Arcade/win.ogg", Owner.Owner, AudioParams.Default.WithVolume(-4f));
Owner.ProcessWin();
return false;
}
if ((_playerHp <= 0 || _playerMp <= 0) && _enemyHp > 0 && _enemyMp > 0)
if (_playerHp > 0 && _playerMp > 0) return true;
if ((_enemyHp > 0 && _enemyMp > 0))
{
UpdateUi("You lost!", $"{_enemyName} cheers.");
_running = false;
UpdateUi(Loc.GetString("You lost!"), Loc.GetString("{0} cheers.", _enemyName), true);
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Effects/Arcade/gameover.ogg", Owner.Owner, AudioParams.Default.WithVolume(-4f));
return false;
}
if ((_playerHp <= 0 || _playerMp <= 0) && (_enemyHp <= 0 || _enemyMp <= 0))
if (_enemyHp <= 0 || _enemyMp <= 0)
{
UpdateUi("You lost!", $"{_enemyName} dies, but takes you with him.");
_running = false;
UpdateUi(Loc.GetString("You lost!"), Loc.GetString("{0} dies, but takes you with him.", _enemyName), true);
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Effects/Arcade/gameover.ogg", Owner.Owner, AudioParams.Default.WithVolume(-4f));
return false;
}
@@ -330,16 +375,16 @@ namespace Content.Server.GameObjects.Components.Arcade
/// <summary>
/// Updates the UI.
/// </summary>
private void UpdateUi()
private void UpdateUi(bool metadata = false)
{
Owner.UserInterface?.SendMessage(GenerateUpdateMessage(_latestPlayerActionMessage, _latestEnemyActionMessage));
Owner.UserInterface?.SendMessage(metadata ? GenerateMetaDataMessage() : GenerateUpdateMessage());
}
private void UpdateUi(string message1, string message2)
private void UpdateUi(string message1, string message2, bool metadata = false)
{
_latestPlayerActionMessage = message1;
_latestEnemyActionMessage = message2;
UpdateUi();
UpdateUi(metadata);
}
/// <summary>
@@ -351,14 +396,14 @@ namespace Content.Server.GameObjects.Components.Arcade
if (_turtleTracker >= 4)
{
var boomAmount = _random.Next(5, 10);
_latestEnemyActionMessage = $"{_enemyName} throws a bomb, exploding you for {boomAmount} damage!";
_latestEnemyActionMessage = Loc.GetString("{0} throws a bomb, exploding you for {1} damage!", _enemyName, boomAmount);
if (Owner._playerInvincibilityFlag) return;
_playerHp -= boomAmount;
_turtleTracker--;
}else if (_enemyMp <= 5 && _random.Prob(0.7f))
{
var stealAmount = _random.Next(2, 3);
_latestEnemyActionMessage = $"{_enemyName} steals {stealAmount} of your power!";
_latestEnemyActionMessage = Loc.GetString("{0} steals {1} of your power!", _enemyName, stealAmount);
if (Owner._playerInvincibilityFlag) return;
_playerMp -= stealAmount;
_enemyMp += stealAmount;
@@ -366,12 +411,13 @@ namespace Content.Server.GameObjects.Components.Arcade
{
_enemyHp += 4;
_enemyMp -= 4;
_latestEnemyActionMessage = $"{_enemyName} heals for 4 health!";
_latestEnemyActionMessage = Loc.GetString("{0} heals for 4 health!", _enemyName);
}
else
{
var attackAmount = _random.Next(3, 6);
_latestEnemyActionMessage = $"{_enemyName} attacks you for {attackAmount} damage!";
_latestEnemyActionMessage =
Loc.GetString("{0} attacks you for {1} damage!", _enemyName, attackAmount);
if (Owner._playerInvincibilityFlag) return;
_playerHp -= attackAmount;
}
@@ -383,20 +429,18 @@ namespace Content.Server.GameObjects.Components.Arcade
/// <returns>A Metadata-message.</returns>
public SpaceVillainArcadeMetaDataUpdateMessage GenerateMetaDataMessage()
{
return new SpaceVillainArcadeMetaDataUpdateMessage(_playerHp, _playerMp, _enemyHp, _enemyMp, _latestPlayerActionMessage, _latestEnemyActionMessage, Name, _enemyName);
return new SpaceVillainArcadeMetaDataUpdateMessage(_playerHp, _playerMp, _enemyHp, _enemyMp, _latestPlayerActionMessage, _latestEnemyActionMessage, Name, _enemyName, !_running);
}
/// <summary>
/// Creates an Update-message based on the objects values.
/// </summary>
/// <param name="playerAction">Content of the Playeraction-field.</param>
/// <param name="enemyAction">Content of the Enemyaction-field.</param>
/// <returns></returns>
/// <returns>An Update-Message.</returns>
public SpaceVillainArcadeDataUpdateMessage
GenerateUpdateMessage(string playerAction = "", string enemyAction = "")
GenerateUpdateMessage()
{
return new SpaceVillainArcadeDataUpdateMessage(_playerHp, _playerMp, _enemyHp, _enemyMp, playerAction,
enemyAction);
return new SpaceVillainArcadeDataUpdateMessage(_playerHp, _playerMp, _enemyHp, _enemyMp, _latestPlayerActionMessage,
_latestEnemyActionMessage);
}
}
}

View File

@@ -0,0 +1,70 @@
#nullable enable
using Content.Server.GameObjects.Components.Body.Respiratory;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.Interfaces.GameObjects.Components;
using Npgsql.TypeHandlers;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Atmos
{
/// <summary>
/// Used in internals as breath tool.
/// </summary>
[RegisterComponent]
public class BreathToolComponent : Component, IEquipped, IUnequipped
{
/// <summary>
/// Tool is functional only in allowed slots
/// </summary>
private EquipmentSlotDefines.SlotFlags _allowedSlots;
public override string Name => "BreathMask";
public bool IsFunctional { get; private set; }
public IEntity? ConnectedInternalsEntity { get; private set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _allowedSlots, "allowedSlots", EquipmentSlotDefines.SlotFlags.MASK);
}
protected override void Shutdown()
{
base.Shutdown();
DisconnectInternals();
}
public void Equipped(EquippedEventArgs eventArgs)
{
if ((EquipmentSlotDefines.SlotMasks[eventArgs.Slot] & _allowedSlots) != _allowedSlots) return;
IsFunctional = true;
if (eventArgs.User.TryGetComponent(out InternalsComponent? internals))
{
ConnectedInternalsEntity = eventArgs.User;
internals.ConnectBreathTool(Owner);
}
}
public void Unequipped(UnequippedEventArgs eventArgs)
{
DisconnectInternals();
}
public void DisconnectInternals()
{
var old = ConnectedInternalsEntity;
ConnectedInternalsEntity = null;
if (old != null && old.TryGetComponent<InternalsComponent>(out var internalsComponent))
{
internalsComponent.DisconnectBreathTool();
}
IsFunctional = false;
}
}
}

View File

@@ -16,6 +16,7 @@ using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
@@ -205,7 +206,7 @@ namespace Content.Server.GameObjects.Components.Atmos
Owner.PopupMessage(Loc.GetString("You stop, drop, and roll!"));
stunnable.Paralyze(2f);
Timer.Spawn(2000, () =>
Owner.SpawnTimer(2000, () =>
{
_resisting = false;
FireStacks -= 2f;

View File

@@ -1,4 +1,5 @@
using Content.Server.Atmos;
using Content.Server.Interfaces;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
@@ -6,23 +7,19 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class GasMixtureHolderComponent : Component
public class GasMixtureHolderComponent : Component, IGasMixtureHolder
{
public override string Name => "GasMixtureHolder";
[ViewVariables] public GasMixture GasMixture { get; set; }
[ViewVariables] public GasMixture Air { get; set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
GasMixture = new GasMixture();
Air = new GasMixture();
serializer.DataReadWriteFunction(
"volume",
0f,
vol => GasMixture.Volume = vol,
() => GasMixture.Volume);
serializer.DataField(this, x => x.Air, "air", new GasMixture());
}
}
}

View File

@@ -1,10 +1,356 @@
using Robust.Shared.GameObjects;
#nullable enable
using System;
using Content.Server.Atmos;
using Content.Server.Explosions;
using Content.Server.GameObjects.Components.Body.Respiratory;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.Interfaces;
using Content.Server.Utility;
using Content.Shared.Atmos;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Atmos.GasTank;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.EntitySystemMessages;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Atmos
{
[RegisterComponent]
public class GasTankComponent : Component
[ComponentReference(typeof(IActivate))]
public class GasTankComponent : SharedGasTankComponent, IExamine, IGasMixtureHolder, IUse, IDropped, IActivate
{
public override string Name => "GasTank";
private const float MaxExplosionRange = 14f;
private const float DefaultOutputPressure = Atmospherics.OneAtmosphere;
private float _pressureResistance;
private int _integrity = 3;
[Dependency] private readonly IEntityManager _entityManager = default!;
[ViewVariables] private BoundUserInterface? _userInterface;
[ViewVariables] public GasMixture? Air { get; set; }
/// <summary>
/// Distributed pressure.
/// </summary>
[ViewVariables] public float OutputPressure { get; private set; }
/// <summary>
/// Tank is connected to internals.
/// </summary>
[ViewVariables] public bool IsConnected { get; set; }
/// <summary>
/// Represents that tank is functional and can be connected to internals.
/// </summary>
public bool IsFunctional => GetInternalsComponent() != null;
/// <summary>
/// Pressure at which tanks start leaking.
/// </summary>
public float TankLeakPressure { get; set; } = 30 * Atmospherics.OneAtmosphere;
/// <summary>
/// Pressure at which tank spills all contents into atmosphere.
/// </summary>
public float TankRupturePressure { get; set; } = 40 * Atmospherics.OneAtmosphere;
/// <summary>
/// Base 3x3 explosion.
/// </summary>
public float TankFragmentPressure { get; set; } = 50 * Atmospherics.OneAtmosphere;
/// <summary>
/// Increases explosion for each scale kPa above threshold.
/// </summary>
public float TankFragmentScale { get; set; } = 10 * Atmospherics.OneAtmosphere;
public override void Initialize()
{
base.Initialize();
_userInterface = Owner.GetUIOrNull(SharedGasTankUiKey.Key);
if (_userInterface != null)
{
_userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
}
public void OpenInterface(IPlayerSession session)
{
_userInterface?.Open(session);
UpdateUserInterface(true);
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Air, "air", new GasMixture());
serializer.DataField(this, x => x.OutputPressure, "outputPressure", DefaultOutputPressure);
serializer.DataField(this, x => x.TankLeakPressure, "tankLeakPressure", 30 * Atmospherics.OneAtmosphere);
serializer.DataField(this, x => x.TankRupturePressure, "tankRupturePressure", 40 * Atmospherics.OneAtmosphere);
serializer.DataField(this, x => x.TankFragmentPressure, "tankFragmentPressure", 50 * Atmospherics.OneAtmosphere);
serializer.DataField(this, x => x.TankFragmentScale, "tankFragmentScale", 10 * Atmospherics.OneAtmosphere);
serializer.DataField(ref _pressureResistance, "pressureResistance", Atmospherics.OneAtmosphere * 5f);
}
public void Examine(FormattedMessage message, bool inDetailsRange)
{
message.AddMarkup(Loc.GetString("Pressure: [color=orange]{0}[/color] kPa.\n",
Math.Round(Air?.Pressure ?? 0)));
if (IsConnected)
{
message.AddMarkup(Loc.GetString("Connected to external component"));
}
}
protected override void Shutdown()
{
base.Shutdown();
DisconnectFromInternals();
}
public void Update()
{
Air?.React(this);
CheckStatus();
UpdateUserInterface();
}
public GasMixture? RemoveAir(float amount)
{
var gas = Air?.Remove(amount);
CheckStatus();
return gas;
}
public GasMixture RemoveAirVolume(float volume)
{
if (Air == null)
return new GasMixture(volume);
var tankPressure = Air.Pressure;
if (tankPressure < OutputPressure)
{
OutputPressure = tankPressure;
UpdateUserInterface();
}
var molesNeeded = OutputPressure * volume / (Atmospherics.R * Air.Temperature);
var air = RemoveAir(molesNeeded);
if (air != null)
air.Volume = volume;
else
return new GasMixture(volume);
return air;
}
public bool UseEntity(UseEntityEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor)) return false;
OpenInterface(actor.playerSession);
return true;
}
public void Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor)) return;
OpenInterface(actor.playerSession);
}
public void ConnectToInternals()
{
if (IsConnected || !IsFunctional) return;
var internals = GetInternalsComponent();
if (internals == null) return;
IsConnected = internals.TryConnectTank(Owner);
UpdateUserInterface();
}
public void DisconnectFromInternals(IEntity? owner = null)
{
if (!IsConnected) return;
IsConnected = false;
GetInternalsComponent(owner)?.DisconnectTank();
UpdateUserInterface();
}
private void UpdateUserInterface(bool initialUpdate = false)
{
_userInterface?.SetState(
new GasTankBoundUserInterfaceState
{
TankPressure = Air?.Pressure ?? 0,
OutputPressure = initialUpdate ? OutputPressure : (float?) null,
InternalsConnected = IsConnected,
CanConnectInternals = IsFunctional && GetInternalsComponent() != null
});
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
{
switch (message.Message)
{
case GasTankSetPressureMessage msg:
OutputPressure = msg.Pressure;
break;
case GasTankToggleInternalsMessage _:
ToggleInternals();
break;
}
}
private void ToggleInternals()
{
if (IsConnected)
{
DisconnectFromInternals();
return;
}
ConnectToInternals();
}
private InternalsComponent? GetInternalsComponent(IEntity? owner = null)
{
if (owner != null) return owner.GetComponentOrNull<InternalsComponent>();
return ContainerHelpers.TryGetContainer(Owner, out var container)
? container.Owner.GetComponentOrNull<InternalsComponent>()
: null;
}
public void AssumeAir(GasMixture giver)
{
Air?.Merge(giver);
CheckStatus();
}
private void CheckStatus()
{
if (Air == null)
return;
var pressure = Air.Pressure;
if (pressure > TankFragmentPressure)
{
// Give the gas a chance to build up more pressure.
for (var i = 0; i < 3; i++)
{
Air.React(this);
}
pressure = Air.Pressure;
var range = (pressure - TankFragmentPressure) / TankFragmentScale;
// Let's cap the explosion, yeah?
if (range > MaxExplosionRange)
{
range = MaxExplosionRange;
}
Owner.SpawnExplosion((int) (range * 0.25f), (int) (range * 0.5f), (int) (range * 1.5f), 1);
Owner.Delete();
return;
}
if (pressure > TankRupturePressure)
{
if (_integrity <= 0)
{
var tileAtmos = Owner.Transform.Coordinates.GetTileAtmosphere();
tileAtmos?.AssumeAir(Air);
EntitySystem.Get<AudioSystem>().PlayAtCoords("Audio/Effects/spray.ogg", Owner.Transform.Coordinates,
AudioHelpers.WithVariation(0.125f));
Owner.Delete();
return;
}
_integrity--;
return;
}
if (pressure > TankLeakPressure)
{
if (_integrity <= 0)
{
var tileAtmos = Owner.Transform.Coordinates.GetTileAtmosphere();
if (tileAtmos == null)
return;
var leakedGas = Air.RemoveRatio(0.25f);
tileAtmos.AssumeAir(leakedGas);
}
else
{
_integrity--;
}
return;
}
if (_integrity < 3)
_integrity++;
}
/// <summary>
/// Open interaction window
/// </summary>
[Verb]
private sealed class ControlVerb : Verb<GasTankComponent>
{
public override bool RequireInteractionRange => true;
protected override void GetData(IEntity user, GasTankComponent component, VerbData data)
{
data.Visibility = VerbVisibility.Invisible;
if (!user.HasComponent<IActorComponent>())
{
return;
}
data.Visibility = VerbVisibility.Visible;
data.Text = "Open Control Panel";
}
protected override void Activate(IEntity user, GasTankComponent component)
{
if (!user.TryGetComponent<IActorComponent>(out var actor))
{
return;
}
component.OpenInterface(actor.playerSession);
}
}
public void Dropped(DroppedEventArgs eventArgs)
{
DisconnectFromInternals(eventArgs.User);
}
}
}

View File

@@ -2,7 +2,9 @@
using System;
using System.Linq;
using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Server.GameObjects.Components.Body.Respiratory;
using Content.Server.Utility;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.Components.Body.Behavior;
@@ -10,6 +12,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
@@ -147,6 +150,16 @@ namespace Content.Server.GameObjects.Components.Body.Behavior
public override void Inhale(float frameTime)
{
if (Body != null && Body.Owner.TryGetComponent(out InternalsComponent? internals)
&& internals.BreathToolEntity != null && internals.GasTankEntity != null
&& internals.BreathToolEntity.TryGetComponent(out BreathToolComponent? breathTool)
&& breathTool.IsFunctional && internals.GasTankEntity.TryGetComponent(out GasTankComponent? gasTank)
&& gasTank.Air != null)
{
Inhale(frameTime, gasTank.RemoveAirVolume(Atmospherics.BreathVolume));
return;
}
if (!Owner.Transform.Coordinates.TryGetTileAir(out var tileAir))
{
return;
@@ -157,8 +170,7 @@ namespace Content.Server.GameObjects.Components.Body.Behavior
public void Inhale(float frameTime, GasMixture from)
{
var ratio = Atmospherics.BreathPercentage * frameTime;
var ratio = (Atmospherics.BreathVolume / from.Volume) * frameTime;
Transfer(from, Air, ratio);
ToBloodstream(Air);

View File

@@ -0,0 +1,63 @@
#nullable enable
using Content.Server.GameObjects.Components.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Body.Respiratory
{
[RegisterComponent]
public class InternalsComponent : Component
{
public override string Name => "Internals";
[ViewVariables] public IEntity? GasTankEntity { get; set; }
[ViewVariables] public IEntity? BreathToolEntity { get; set; }
public void DisconnectBreathTool()
{
var old = BreathToolEntity;
BreathToolEntity = null;
if (old != null && old.TryGetComponent(out BreathToolComponent? breathTool) )
{
breathTool.DisconnectInternals();
DisconnectTank();
}
}
public void ConnectBreathTool(IEntity toolEntity)
{
if (BreathToolEntity != null && BreathToolEntity.TryGetComponent(out BreathToolComponent? tool))
{
tool.DisconnectInternals();
}
BreathToolEntity = toolEntity;
}
public void DisconnectTank()
{
if (GasTankEntity != null && GasTankEntity.TryGetComponent(out GasTankComponent? tank))
{
tank.DisconnectFromInternals(Owner);
}
GasTankEntity = null;
}
public bool TryConnectTank(IEntity tankEntity)
{
if (BreathToolEntity == null)
return false;
if (GasTankEntity != null && GasTankEntity.TryGetComponent(out GasTankComponent? tank))
{
tank.DisconnectFromInternals(Owner);
}
GasTankEntity = tankEntity;
return true;
}
}
}

View File

@@ -460,6 +460,7 @@ namespace Content.Server.GameObjects.Components.Buckle
if (Moved)
{
TryUnbuckle(Owner, true);
Moved = false;
return;
}

View File

@@ -0,0 +1,118 @@
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Content.Server.GameObjects.Components
{
[RegisterComponent]
[ComponentReference(typeof(SharedConfigurationComponent))]
public class ConfigurationComponent : SharedConfigurationComponent, IInteractUsing
{
[ViewVariables] private BoundUserInterface UserInterface => Owner.GetUIOrNull(ConfigurationUiKey.Key);
[ViewVariables]
private readonly Dictionary<string, string> _config = new Dictionary<string, string>();
private Regex _validation;
public event Action<Dictionary<string, string>> OnConfigUpdate;
public override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
}
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataReadWriteFunction("keys", new List<string>(),
(list) => FillConfiguration(list, _config, ""),
() => _config.Keys.ToList());
serializer.DataReadFunction("vailidation", "^[a-zA-Z0-9 ]*$", value => _validation = new Regex("^[a-zA-Z0-9 ]*$", RegexOptions.Compiled));
}
public string GetConfig(string name)
{
return _config.GetValueOrDefault(name);
}
protected override void Startup()
{
base.Startup();
UpdateUserInterface();
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (UserInterface == null || !eventArgs.User.TryGetComponent(out IActorComponent actor))
return false;
if (!eventArgs.Using.TryGetComponent<ToolComponent>(out var tool))
return false;
if (!await tool.UseTool(eventArgs.User, Owner, 0.2f, ToolQuality.Multitool))
return false;
UpdateUserInterface();
UserInterface.Open(actor.playerSession);
UserInterface.SendMessage(new ValidationUpdateMessage(_validation.ToString()), actor.playerSession);
return true;
}
private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
{
var message = serverMsg.Message;
var config = new Dictionary<string, string>(_config);
if (message is ConfigurationUpdatedMessage msg)
{
foreach (var key in config.Keys)
{
var value = msg.Config.GetValueOrDefault(key);
if (_validation != null && !_validation.IsMatch(value) && value != "")
continue;
_config[key] = value;
}
OnConfigUpdate(_config);
}
}
private void UpdateUserInterface()
{
if (UserInterface == null)
return;
UserInterface.SetState(new ConfigurationBoundUserInterfaceState(_config));
}
private static void FillConfiguration<T>(List<string> list, Dictionary<string, T> configuration, T value){
for (var index = 0; index < list.Count; index++)
{
configuration.Add(list[index], value);
}
}
}
}

View File

@@ -155,7 +155,7 @@ namespace Content.Server.GameObjects.Components.Conveyor
if (entity.TryGetComponent(out IPhysicsComponent? physics))
{
var controller = physics.EnsureController<ConveyedController>();
controller.Move(direction, _speed * frameTime);
controller.Move(direction, _speed);
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects.EntitySystems;
@@ -7,6 +8,7 @@ using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Random;
namespace Content.Server.GameObjects.Components.Damage
@@ -65,11 +67,6 @@ namespace Content.Server.GameObjects.Components.Damage
protected override void DestructionBehavior()
{
_actSystem.HandleBreakage(Owner);
if (!Owner.Deleted && DestroySound != string.Empty)
{
var pos = Owner.Transform.Coordinates;
EntitySystem.Get<AudioSystem>().PlayAtCoords(DestroySound, pos);
}
}
}
}

View File

@@ -1,10 +1,13 @@
using Content.Shared.GameObjects.Components.Damage;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Stack;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects.EntitySystems;
using Content.Shared.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Damage
@@ -17,6 +20,8 @@ namespace Content.Server.GameObjects.Components.Damage
public class DestructibleComponent : RuinableComponent, IDestroyAct
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
protected ActSystem ActSystem;
@@ -24,22 +29,51 @@ namespace Content.Server.GameObjects.Components.Damage
public override string Name => "Destructible";
/// <summary>
/// Entity spawned upon destruction.
/// Entities spawned on destruction plus the min and max amount spawned.
/// </summary>
public string SpawnOnDestroy { get; private set; }
public Dictionary<string, MinMax> SpawnOnDestroy { get; private set; }
void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
{
if (!string.IsNullOrWhiteSpace(SpawnOnDestroy) && eventArgs.IsSpawnWreck)
if (SpawnOnDestroy == null || !eventArgs.IsSpawnWreck) return;
foreach (var (key, value) in SpawnOnDestroy)
{
Owner.EntityManager.SpawnEntity(SpawnOnDestroy, Owner.Transform.Coordinates);
int count;
if (value.Min >= value.Max)
{
count = value.Min;
}
else
{
count = _random.Next(value.Min, value.Max + 1);
}
if (count == 0) continue;
if (EntityPrototypeHelpers.HasComponent<StackComponent>(key))
{
var spawned = Owner.EntityManager.SpawnEntity(key, Owner.Transform.Coordinates);
var stack = spawned.GetComponent<StackComponent>();
stack.Count = count;
spawned.RandomOffset(0.5f);
}
else
{
for (var i = 0; i < count; i++)
{
var spawned = Owner.EntityManager.SpawnEntity(key, Owner.Transform.Coordinates);
spawned.RandomOffset(0.5f);
}
}
}
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, d => d.SpawnOnDestroy, "spawnOnDestroy", string.Empty);
serializer.DataField(this, d => d.SpawnOnDestroy, "spawnOnDestroy", null);
}
public override void Initialize()
@@ -56,11 +90,13 @@ namespace Content.Server.GameObjects.Components.Damage
var pos = Owner.Transform.Coordinates;
ActSystem.HandleDestruction(Owner,
true); //This will call IDestroyAct.OnDestroy on this component (and all other components on this entity)
if (DestroySound != string.Empty)
}
}
public struct MinMax
{
EntitySystem.Get<AudioSystem>().PlayAtCoords(DestroySound, pos);
}
}
public int Min;
public int Max;
}
}
}

View File

@@ -1,11 +1,15 @@
using System.Collections.Generic;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Damage;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
using Logger = Robust.Shared.Log.Logger;
namespace Content.Server.GameObjects.Components.Damage
{
@@ -16,12 +20,19 @@ namespace Content.Server.GameObjects.Components.Damage
[ComponentReference(typeof(IDamageableComponent))]
public abstract class RuinableComponent : DamageableComponent
{
[Dependency] private IRobustRandom _random = default!;
/// <summary>
/// Sound played upon destruction.
/// </summary>
[ViewVariables]
protected string DestroySound { get; private set; }
/// <summary>
/// Used instead of <see cref="DestroySound"/> if specified.
/// </summary>
[ViewVariables]
protected string DestroySoundCollection { get; private set; }
public override List<DamageState> SupportedDamageStates =>
new List<DamageState> {DamageState.Alive, DamageState.Dead};
@@ -44,6 +55,7 @@ namespace Content.Server.GameObjects.Components.Damage
() => Thresholds.TryGetValue(DamageState.Dead, out var value) ? value : (int?) null);
serializer.DataField(this, ruinable => ruinable.DestroySound, "destroySound", string.Empty);
serializer.DataField(this, ruinable => ruinable.DestroySoundCollection, "destroySoundCollection", string.Empty);
}
protected override void EnterState(DamageState state)
@@ -65,10 +77,24 @@ namespace Content.Server.GameObjects.Components.Damage
{
CurrentState = DamageState.Dead;
if (!Owner.Deleted && DestroySound != string.Empty)
if (!Owner.Deleted)
{
var pos = Owner.Transform.Coordinates;
EntitySystem.Get<AudioSystem>().PlayAtCoords(DestroySound, pos);
string sound = string.Empty;
if (DestroySoundCollection != string.Empty)
{
sound = AudioHelpers.GetRandomFileFromSoundCollection(DestroySoundCollection);
}
else if (DestroySound != string.Empty)
{
sound = DestroySound;
}
if (sound != string.Empty)
{
Logger.Debug("Playing destruction sound");
EntitySystem.Get<AudioSystem>().PlayAtCoords(sound, pos, AudioHelpers.WithVariation(0.125f));
}
}
DestructionBehavior();

View File

@@ -33,7 +33,7 @@ namespace Content.Server.GameObjects.Components.Disposal
return TryInsert(holderComponent);
}
private bool TryInsert(DisposalHolderComponent holder)
public bool TryInsert(DisposalHolderComponent holder)
{
if (!Contents.Insert(holder.Owner))
{

View File

@@ -0,0 +1,840 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.EntitySystems.DeviceNetwork;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Server.Interfaces;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Disposal;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Components.Transform;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
using Timer = Robust.Shared.Timers.Timer;
namespace Content.Server.GameObjects.Components.Disposal
{
[RegisterComponent]
[ComponentReference(typeof(SharedDisposalMailingUnitComponent))]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(IInteractUsing))]
public class DisposalMailingUnitComponent : SharedDisposalMailingUnitComponent, IInteractHand, IActivate, IInteractUsing, IDragDropOn
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
private const string HolderPrototypeId = "DisposalHolder";
/// <summary>
/// The delay for an entity trying to move out of this unit.
/// </summary>
private static readonly TimeSpan ExitAttemptDelay = TimeSpan.FromSeconds(0.5);
/// <summary>
/// Last time that an entity tried to exit this disposal unit.
/// </summary>
[ViewVariables]
private TimeSpan _lastExitAttempt;
public static readonly Regex TagRegex = new Regex("^[a-zA-Z0-9, ]*$", RegexOptions.Compiled);
/// <summary>
/// The current pressure of this disposal unit.
/// Prevents it from flushing if it is not equal to or bigger than 1.
/// </summary>
[ViewVariables]
private float _pressure;
private bool _engaged;
[ViewVariables(VVAccess.ReadWrite)]
private TimeSpan _automaticEngageTime;
[ViewVariables(VVAccess.ReadWrite)]
private TimeSpan _flushDelay;
[ViewVariables(VVAccess.ReadWrite)]
private float _entryDelay;
/// <summary>
/// Token used to cancel the automatic engage of a disposal unit
/// after an entity enters it.
/// </summary>
private CancellationTokenSource? _automaticEngageToken;
/// <summary>
/// Container of entities inside this disposal unit.
/// </summary>
[ViewVariables]
private Container _container = default!;
[ViewVariables]
private WiredNetworkConnection? _connection;
[ViewVariables] public IReadOnlyList<IEntity> ContainedEntities => _container.ContainedEntities;
[ViewVariables]
private readonly List<string> _targetList = new List<string>();
[ViewVariables]
private string _target = "";
[ViewVariables(VVAccess.ReadWrite)]
private string _tag = "";
[ViewVariables]
public bool Powered =>
!Owner.TryGetComponent(out PowerReceiverComponent? receiver) ||
receiver.Powered;
[ViewVariables]
private PressureState State => _pressure >= 1 ? PressureState.Ready : PressureState.Pressurizing;
[ViewVariables(VVAccess.ReadWrite)]
private bool Engaged
{
get => _engaged;
set
{
var oldEngaged = _engaged;
_engaged = value;
if (oldEngaged == value)
{
return;
}
UpdateVisualState();
}
}
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(DisposalMailingUnitUiKey.Key);
private DisposalMailingUnitBoundUserInterfaceState? _lastUiState;
/// <summary>
/// Store the translated state.
/// </summary>
private (PressureState State, string Localized) _locState;
public bool CanInsert(IEntity entity)
{
if (!Anchored)
{
return false;
}
if (!entity.TryGetComponent(out IPhysicsComponent? physics) ||
!physics.CanCollide)
{
return false;
}
if (!entity.HasComponent<ItemComponent>() &&
!entity.HasComponent<IBody>())
{
return false;
}
return _container.CanInsert(entity);
}
private void TryQueueEngage()
{
if (!Powered && ContainedEntities.Count == 0)
{
return;
}
_automaticEngageToken = new CancellationTokenSource();
Timer.Spawn(_automaticEngageTime, () =>
{
if (!TryFlush())
{
TryQueueEngage();
}
}, _automaticEngageToken.Token);
}
private void AfterInsert(IEntity entity)
{
TryQueueEngage();
if (entity.TryGetComponent(out IActorComponent? actor))
{
UserInterface?.Close(actor.playerSession);
}
UpdateVisualState();
}
public async Task<bool> TryInsert(IEntity entity, IEntity? user = default)
{
if (!CanInsert(entity))
return false;
if (user != null && _entryDelay > 0f)
{
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
var doAfterArgs = new DoAfterEventArgs(user, _entryDelay, default, Owner)
{
BreakOnDamage = true,
BreakOnStun = true,
BreakOnTargetMove = true,
BreakOnUserMove = true,
NeedHand = false,
};
var result = await doAfterSystem.DoAfter(doAfterArgs);
if (result == DoAfterStatus.Cancelled)
return false;
}
if (!_container.Insert(entity))
return false;
AfterInsert(entity);
return true;
}
private bool TryDrop(IEntity user, IEntity entity)
{
if (!user.TryGetComponent(out HandsComponent? hands))
{
return false;
}
if (!CanInsert(entity) || !hands.Drop(entity, _container))
{
return false;
}
AfterInsert(entity);
return true;
}
private void Remove(IEntity entity)
{
_container.Remove(entity);
if (ContainedEntities.Count == 0)
{
_automaticEngageToken?.Cancel();
_automaticEngageToken = null;
}
UpdateVisualState();
}
private bool CanFlush()
{
return _pressure >= 1 && Powered && Anchored;
}
private void ToggleEngage()
{
Engaged ^= true;
if (Engaged && CanFlush())
{
Timer.Spawn(_flushDelay, () => TryFlush());
}
}
public bool TryFlush()
{
if (!CanFlush())
{
return false;
}
var snapGrid = Owner.GetComponent<SnapGridComponent>();
var entry = snapGrid
.GetLocal()
.FirstOrDefault(entity => entity.HasComponent<DisposalEntryComponent>());
if (entry == null)
{
return false;
}
var entryComponent = entry.GetComponent<DisposalEntryComponent>();
var entities = _container.ContainedEntities.ToList();
foreach (var entity in _container.ContainedEntities.ToList())
{
_container.Remove(entity);
}
var holder = CreateTaggedHolder(entities, _target);
entryComponent.TryInsert(holder);
_automaticEngageToken?.Cancel();
_automaticEngageToken = null;
_pressure = 0;
Engaged = false;
UpdateVisualState(true);
UpdateInterface();
if (_connection != null)
{
var data = new Dictionary<string, string>
{
{ NetworkUtils.COMMAND, NET_CMD_SENT },
{ NET_SRC, _tag },
{ NET_TARGET, _target }
};
_connection.Broadcast(_connection.Frequency, data);
}
return true;
}
private DisposalHolderComponent CreateTaggedHolder(IReadOnlyCollection<IEntity> entities, string tag)
{
var holder = Owner.EntityManager.SpawnEntity(HolderPrototypeId, Owner.Transform.MapPosition);
var holderComponent = holder.GetComponent<DisposalHolderComponent>();
holderComponent.Tags.Add(tag);
holderComponent.Tags.Add(TAGS_MAIL);
foreach (var entity in entities)
{
holderComponent.TryInsert(entity);
}
return holderComponent;
}
private void UpdateTargetList()
{
_targetList.Clear();
var payload = new Dictionary<string, string>
{
{ NetworkUtils.COMMAND, NET_CMD_REQUEST }
};
_connection?.Broadcast(_connection.Frequency, payload);
}
private void TryEjectContents()
{
foreach (var entity in _container.ContainedEntities.ToArray())
{
Remove(entity);
}
}
private void TogglePower()
{
if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver))
{
return;
}
receiver.PowerDisabled = !receiver.PowerDisabled;
UpdateInterface();
}
private DisposalMailingUnitBoundUserInterfaceState GetInterfaceState()
{
string stateString;
if (_locState.State != State)
{
stateString = Loc.GetString($"{State}");
_locState = (State, stateString);
}
else
{
stateString = _locState.Localized;
}
return new DisposalMailingUnitBoundUserInterfaceState(Owner.Name, stateString, _pressure, Powered, Engaged, _tag, _targetList, _target);
}
private void UpdateInterface(bool checkEqual = true)
{
var state = GetInterfaceState();
if (checkEqual && _lastUiState != null && _lastUiState.Equals(state))
{
return;
}
_lastUiState = state;
UserInterface?.SetState((DisposalMailingUnitBoundUserInterfaceState) state.Clone());
}
private bool PlayerCanUse(IEntity? player)
{
if (player == null)
{
return false;
}
if (!ActionBlockerSystem.CanInteract(player) ||
!ActionBlockerSystem.CanUse(player))
{
return false;
}
return true;
}
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Session.AttachedEntity == null)
{
return;
}
if (!PlayerCanUse(obj.Session.AttachedEntity))
{
return;
}
if (obj.Message is UiButtonPressedMessage buttonMessage)
{
switch (buttonMessage.Button)
{
case UiButton.Eject:
TryEjectContents();
break;
case UiButton.Engage:
ToggleEngage();
break;
case UiButton.Power:
TogglePower();
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
break;
default:
throw new ArgumentOutOfRangeException();
}
}
if (obj.Message is UiTargetUpdateMessage tagMessage && TagRegex.IsMatch(tagMessage.Target))
{
_target = tagMessage.Target;
}
}
private void OnConfigUpdate(Dictionary<string, string> config)
{
if (config.TryGetValue("Tag", out var tag))
_tag = tag;
}
private void UpdateVisualState()
{
UpdateVisualState(false);
}
private void UpdateVisualState(bool flush)
{
if (!Owner.TryGetComponent(out AppearanceComponent? appearance))
{
return;
}
if (!Anchored)
{
appearance.SetData(Visuals.VisualState, VisualState.UnAnchored);
appearance.SetData(Visuals.Handle, HandleState.Normal);
appearance.SetData(Visuals.Light, LightState.Off);
return;
}
else if (_pressure < 1)
{
appearance.SetData(Visuals.VisualState, VisualState.Charging);
}
else
{
appearance.SetData(Visuals.VisualState, VisualState.Anchored);
}
appearance.SetData(Visuals.Handle, Engaged
? HandleState.Engaged
: HandleState.Normal);
if (!Powered)
{
appearance.SetData(Visuals.Light, LightState.Off);
return;
}
if (flush)
{
appearance.SetData(Visuals.VisualState, VisualState.Flushing);
appearance.SetData(Visuals.Light, LightState.Off);
return;
}
if (ContainedEntities.Count > 0)
{
appearance.SetData(Visuals.Light, LightState.Full);
return;
}
appearance.SetData(Visuals.Light, _pressure < 1
? LightState.Charging
: LightState.Ready);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!Powered)
{
return;
}
var oldPressure = _pressure;
_pressure = _pressure + frameTime > 1
? 1
: _pressure + 0.05f * frameTime;
if (oldPressure < 1 && _pressure >= 1)
{
UpdateVisualState();
if (Engaged)
{
TryFlush();
}
}
UpdateInterface();
}
private void PowerStateChanged(object? sender, PowerStateEventArgs args)
{
if (!args.Powered)
{
_automaticEngageToken?.Cancel();
_automaticEngageToken = null;
}
UpdateVisualState();
if (Engaged && !TryFlush())
{
TryQueueEngage();
}
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataReadWriteFunction(
"pressure",
1.0f,
pressure => _pressure = pressure,
() => _pressure);
serializer.DataReadWriteFunction(
"automaticEngageTime",
30,
seconds => _automaticEngageTime = TimeSpan.FromSeconds(seconds),
() => (int) _automaticEngageTime.TotalSeconds);
serializer.DataReadWriteFunction(
"flushDelay",
3,
seconds => _flushDelay = TimeSpan.FromSeconds(seconds),
() => (int) _flushDelay.TotalSeconds);
serializer.DataReadWriteFunction(
"entryDelay",
0.5f,
seconds => _entryDelay = seconds,
() => (int) _entryDelay);
serializer.DataField(ref _tag, "Tag", "");
}
public override void Initialize()
{
base.Initialize();
_container = ContainerManagerComponent.Ensure<Container>(Name, Owner);
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
var network = IoCManager.Resolve<IDeviceNetwork>();
_connection = new WiredNetworkConnection(OnReceiveNetMessage, false, Owner);
if (Owner.TryGetComponent<ConfigurationComponent>(out var configuration))
configuration.OnConfigUpdate += OnConfigUpdate;
UpdateInterface();
}
protected override void Startup()
{
base.Startup();
if(!Owner.HasComponent<AnchorableComponent>())
{
Logger.WarningS("VitalComponentMissing", $"Disposal unit {Owner.Uid} is missing an anchorable component");
}
if (Owner.TryGetComponent(out IPhysicsComponent? physics))
{
physics.AnchoredChanged += UpdateVisualState;
}
if (Owner.TryGetComponent(out PowerReceiverComponent? receiver))
{
receiver.OnPowerStateChanged += PowerStateChanged;
}
UpdateTargetList();
UpdateVisualState();
}
public override void OnRemove()
{
if (Owner.TryGetComponent(out IPhysicsComponent? physics))
{
physics.AnchoredChanged -= UpdateVisualState;
}
if (Owner.TryGetComponent(out PowerReceiverComponent? receiver))
{
receiver.OnPowerStateChanged -= PowerStateChanged;
}
if (_container != null)
{
foreach (var entity in _container.ContainedEntities.ToArray())
{
_container.ForceRemove(entity);
}
}
UserInterface?.CloseAll();
_automaticEngageToken?.Cancel();
_automaticEngageToken = null;
_container = null!;
_connection!.Close();
base.OnRemove();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case RelayMovementEntityMessage msg:
if (!msg.Entity.TryGetComponent(out HandsComponent? hands) ||
hands.Count == 0 ||
_gameTiming.CurTime < _lastExitAttempt + ExitAttemptDelay)
{
break;
}
_lastExitAttempt = _gameTiming.CurTime;
Remove(msg.Entity);
break;
}
}
private void OnReceiveNetMessage(int frequency, string sender, IReadOnlyDictionary<string, string> payload, object _, bool broadcast)
{
if (payload.TryGetValue(NetworkUtils.COMMAND, out var command) && Powered)
{
if (command == NET_CMD_RESPONSE && payload.TryGetValue(NET_TAG, out var tag))
{
_targetList.Add(tag);
UpdateInterface(false);
}
if (command == NET_CMD_REQUEST)
{
if (_tag == "" || !Powered)
return;
var data = new Dictionary<string, string>
{
{NetworkUtils.COMMAND, NET_CMD_RESPONSE},
{NET_TAG, _tag}
};
_connection?.Send(frequency, sender, data);
}
}
}
private bool IsValidInteraction(ITargetedInteractEventArgs eventArgs)
{
if (!ActionBlockerSystem.CanInteract(eventArgs.User))
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("You can't do that!"));
return false;
}
if (ContainerHelpers.IsInContainer(eventArgs.User))
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("You can't reach there!"));
return false;
}
// This popup message doesn't appear on clicks, even when code was seperate. Unsure why.
if (!eventArgs.User.HasComponent<IHandsComponent>())
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("You have no hands!"));
return false;
}
return true;
}
bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
return false;
}
// Duplicated code here, not sure how else to get actor inside to make UserInterface happy.
if (IsValidInteraction(eventArgs))
{
UpdateTargetList();
UpdateInterface(false);
UserInterface?.Open(actor.playerSession);
return true;
}
return false;
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
return;
}
if (IsValidInteraction(eventArgs))
{
UserInterface?.Open(actor.playerSession);
}
return;
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryDrop(eventArgs.User, eventArgs.Using);
}
bool IDragDropOn.CanDragDropOn(DragDropEventArgs eventArgs)
{
return CanInsert(eventArgs.Dragged);
}
bool IDragDropOn.DragDropOn(DragDropEventArgs eventArgs)
{
_ = TryInsert(eventArgs.Dragged, eventArgs.User);
return true;
}
[Verb]
private sealed class SelfInsertVerb : Verb<DisposalMailingUnitComponent>
{
protected override void GetData(IEntity user, DisposalMailingUnitComponent component, VerbData data)
{
data.Visibility = VerbVisibility.Invisible;
if (!ActionBlockerSystem.CanInteract(user) ||
component.ContainedEntities.Contains(user))
{
return;
}
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Jump inside");
}
protected override void Activate(IEntity user, DisposalMailingUnitComponent component)
{
_ = component.TryInsert(user, user);
}
}
[Verb]
private sealed class FlushVerb : Verb<DisposalMailingUnitComponent>
{
protected override void GetData(IEntity user, DisposalMailingUnitComponent component, VerbData data)
{
data.Visibility = VerbVisibility.Invisible;
if (!ActionBlockerSystem.CanInteract(user) ||
component.ContainedEntities.Contains(user))
{
return;
}
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Flush");
}
protected override void Activate(IEntity user, DisposalMailingUnitComponent component)
{
component.Engaged = true;
component.TryFlush();
}
}
}
}

View File

@@ -6,12 +6,14 @@ using System.Threading;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Mobs.State;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.Components.Projectiles;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Disposal;
using Content.Shared.GameObjects.Components.Items;
using Content.Shared.GameObjects.EntitySystems;
@@ -27,6 +29,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.GameObjects.Components.Transform;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
@@ -136,11 +139,14 @@ namespace Content.Server.GameObjects.Components.Disposal
return false;
}
if (!entity.TryGetComponent(out IPhysicsComponent? physics) ||
!physics.CanCollide)
{
if (!(entity.TryGetComponent(out IDamageableComponent? damageState) && damageState.CurrentState == DamageState.Dead)) {
return false;
}
}
if (!entity.HasComponent<ItemComponent>() &&
!entity.HasComponent<IBody>())
@@ -160,7 +166,7 @@ namespace Content.Server.GameObjects.Components.Disposal
_automaticEngageToken = new CancellationTokenSource();
Timer.Spawn(_automaticEngageTime, () =>
Owner.SpawnTimer(_automaticEngageTime, () =>
{
if (!TryFlush())
{
@@ -255,7 +261,7 @@ namespace Content.Server.GameObjects.Components.Disposal
if (Engaged && CanFlush())
{
Timer.Spawn(_flushDelay, () => TryFlush());
Owner.SpawnTimer(_flushDelay, () => TryFlush());
}
}

View File

@@ -13,6 +13,7 @@ using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
@@ -310,7 +311,7 @@ namespace Content.Server.GameObjects.Components.Doors
PowerWiresPulsed = true;
_powerWiresPulsedTimerCancel.Cancel();
_powerWiresPulsedTimerCancel = new CancellationTokenSource();
Timer.Spawn(PowerWiresTimeout,
Owner.SpawnTimer(PowerWiresTimeout,
() => PowerWiresPulsed = false,
_powerWiresPulsedTimerCancel.Token);
break;

View File

@@ -23,6 +23,7 @@ using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Maths;
@@ -249,7 +250,7 @@ namespace Content.Server.GameObjects.Components.Doors
occluder.Enabled = false;
}
Timer.Spawn(OpenTimeOne, async () =>
Owner.SpawnTimer(OpenTimeOne, async () =>
{
if (Owner.TryGetComponent(out AirtightComponent? airtight))
{
@@ -319,7 +320,7 @@ namespace Content.Server.GameObjects.Components.Doors
stun.Paralyze(DoorStunTime);
// If we hit someone, open up after stun (opens right when stun ends)
Timer.Spawn(TimeSpan.FromSeconds(DoorStunTime) - OpenTimeOne - OpenTimeTwo, Open);
Owner.SpawnTimer(TimeSpan.FromSeconds(DoorStunTime) - OpenTimeOne - OpenTimeTwo, Open);
break;
}
}
@@ -402,7 +403,7 @@ namespace Content.Server.GameObjects.Components.Doors
occluder.Enabled = true;
}
Timer.Spawn(CloseTimeOne, async () =>
Owner.SpawnTimer(CloseTimeOne, async () =>
{
if (shouldCheckCrush && _canCrush)
{
@@ -435,7 +436,7 @@ namespace Content.Server.GameObjects.Components.Doors
return;
SetAppearance(DoorVisualState.Deny);
Timer.Spawn(DenyTime, () =>
Owner.SpawnTimer(DenyTime, () =>
{
SetAppearance(DoorVisualState.Closed);
}, _cancellationTokenSource.Token);

View File

@@ -14,6 +14,7 @@ using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.GameObjects.Components.Transform;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
@@ -254,7 +255,7 @@ namespace Content.Server.GameObjects.Components.Fluids
_evaporationToken = new CancellationTokenSource();
// KYS to evaporate
Timer.Spawn(TimeSpan.FromSeconds(_evaporateTime), Evaporate, _evaporationToken.Token);
Owner.SpawnTimer(TimeSpan.FromSeconds(_evaporateTime), Evaporate, _evaporationToken.Token);
}
private void UpdateSlip()

View File

@@ -4,12 +4,10 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems.Click;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Physics.Pull;
using Robust.Server.GameObjects;
@@ -18,7 +16,6 @@ using Robust.Server.GameObjects.EntitySystemMessages;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Components.Transform;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
@@ -26,7 +23,6 @@ using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
using Content.Server.GameObjects.Components.Pulling;
using Robust.Shared.Map;
namespace Content.Server.GameObjects.Components.GUI
@@ -119,7 +115,8 @@ namespace Content.Server.GameObjects.Components.GUI
: GetItem(ActiveHand);
/// <summary>
/// Enumerates over the hand keys, returning the active hand first.
/// Enumerates over the enabled hand keys,
/// returning the active hand first.
/// </summary>
public IEnumerable<string> ActivePriorityEnumerable()
{
@@ -135,6 +132,11 @@ namespace Content.Server.GameObjects.Components.GUI
continue;
}
if (!hand.Enabled)
{
continue;
}
yield return hand.Name;
}
}
@@ -205,7 +207,11 @@ namespace Content.Server.GameObjects.Components.GUI
if (mobCheck && !ActionBlockerSystem.CanPickup(Owner))
return false;
return GetHand(index)?.Container.CanInsert(item.Owner) == true;
var hand = GetHand(index);
return hand != null &&
hand.Enabled &&
hand.Container.CanInsert(item.Owner) == true;
}
/// <summary>
@@ -245,7 +251,7 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
public bool Drop(string slot, EntityCoordinates coords, bool doMobChecks = true)
public bool Drop(string slot, EntityCoordinates coords, bool doMobChecks = true, bool doDropInteraction = true)
{
var hand = GetHand(slot);
if (!CanDrop(slot, doMobChecks) || hand?.Entity == null)
@@ -260,7 +266,7 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
if (!DroppedInteraction(item, false))
if (doDropInteraction && !DroppedInteraction(item, false))
return false;
item.RemovedFromSlot();
@@ -282,7 +288,7 @@ namespace Content.Server.GameObjects.Components.GUI
return true;
}
public bool Drop(IEntity entity, EntityCoordinates coords, bool doMobChecks = true)
public bool Drop(IEntity entity, EntityCoordinates coords, bool doMobChecks = true, bool doDropInteraction = true)
{
if (entity == null)
{
@@ -294,15 +300,15 @@ namespace Content.Server.GameObjects.Components.GUI
throw new ArgumentException("Entity must be held in one of our hands.", nameof(entity));
}
return Drop(slot, coords, doMobChecks);
return Drop(slot, coords, doMobChecks, doDropInteraction);
}
public bool Drop(string slot, bool mobChecks = true)
public bool Drop(string slot, bool mobChecks = true, bool doDropInteraction = true)
{
return Drop(slot, Owner.Transform.Coordinates, mobChecks);
return Drop(slot, Owner.Transform.Coordinates, mobChecks, doDropInteraction);
}
public bool Drop(IEntity entity, bool mobChecks = true)
public bool Drop(IEntity entity, bool mobChecks = true, bool doDropInteraction = true)
{
if (entity == null)
{
@@ -314,10 +320,10 @@ namespace Content.Server.GameObjects.Components.GUI
throw new ArgumentException("Entity must be held in one of our hands.", nameof(entity));
}
return Drop(slot, Owner.Transform.Coordinates, mobChecks);
return Drop(slot, Owner.Transform.Coordinates, mobChecks, doDropInteraction);
}
public bool Drop(string slot, BaseContainer targetContainer, bool doMobChecks = true)
public bool Drop(string slot, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true)
{
if (slot == null)
{
@@ -352,7 +358,7 @@ namespace Content.Server.GameObjects.Components.GUI
throw new InvalidOperationException();
}
if (!DroppedInteraction(item, doMobChecks))
if (doDropInteraction && !DroppedInteraction(item, doMobChecks))
return false;
item.RemovedFromSlot();
@@ -368,7 +374,7 @@ namespace Content.Server.GameObjects.Components.GUI
return true;
}
public bool Drop(IEntity entity, BaseContainer targetContainer, bool doMobChecks = true)
public bool Drop(IEntity entity, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true)
{
if (entity == null)
{
@@ -380,7 +386,7 @@ namespace Content.Server.GameObjects.Components.GUI
throw new ArgumentException("Entity must be held in one of our hands.", nameof(entity));
}
return Drop(slot, targetContainer, doMobChecks);
return Drop(slot, targetContainer, doMobChecks, doDropInteraction);
}
/// <summary>
@@ -411,7 +417,7 @@ namespace Content.Server.GameObjects.Components.GUI
}
var container = ContainerManagerComponent.Create<ContainerSlot>($"hand {_nextHand++}", Owner);
var hand = new Hand(name, container);
var hand = new Hand(this, name, container);
_hands.Add(hand);
@@ -515,6 +521,53 @@ namespace Content.Server.GameObjects.Components.GUI
return false;
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
if (message is PullMessage pullMessage &&
pullMessage.Puller.Owner != Owner)
{
return;
}
switch (message)
{
case PullAttemptMessage msg:
if (!_hands.Any(hand => hand.Enabled))
{
msg.Cancelled = true;
}
break;
case PullStartedMessage _:
var firstFreeHand = _hands.FirstOrDefault(hand => hand.Enabled);
if (firstFreeHand == null)
{
break;
}
firstFreeHand.Enabled = false;
break;
case PullStoppedMessage _:
var firstOccupiedHand = _hands.FirstOrDefault(hand => !hand.Enabled);
if (firstOccupiedHand == null)
{
break;
}
firstOccupiedHand.Enabled = true;
break;
case HandDisabledMsg msg:
Drop(msg.Name, false);
break;
}
}
public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null)
{
base.HandleNetworkMessage(message, channel, session);
@@ -625,34 +678,6 @@ namespace Content.Server.GameObjects.Components.GUI
}
}
private void AddPullingStatuses(IEntity pulled)
{
if (pulled.TryGetComponent(out ServerStatusEffectsComponent? pulledStatus))
{
pulledStatus.ChangeStatusEffectIcon(StatusEffect.Pulled,
"/Textures/Interface/StatusEffects/Pull/pulled.png");
}
if (Owner.TryGetComponent(out ServerStatusEffectsComponent? ownerStatus))
{
ownerStatus.ChangeStatusEffectIcon(StatusEffect.Pulling,
"/Textures/Interface/StatusEffects/Pull/pulling.png");
}
}
private void RemovePullingStatuses(IEntity pulled)
{
if (pulled.TryGetComponent(out ServerStatusEffectsComponent? pulledStatus))
{
pulledStatus.RemoveStatusEffect(StatusEffect.Pulled);
}
if (Owner.TryGetComponent(out ServerStatusEffectsComponent? ownerStatus))
{
ownerStatus.RemoveStatusEffect(StatusEffect.Pulling);
}
}
void IBodyPartAdded.BodyPartAdded(BodyPartAddedEventArgs args)
{
if (args.Part.PartType != BodyPartType.Hand)
@@ -676,16 +701,42 @@ namespace Content.Server.GameObjects.Components.GUI
public class Hand : IDisposable
{
public Hand(string name, ContainerSlot container)
private bool _enabled = true;
public Hand(HandsComponent parent, string name, ContainerSlot container)
{
Parent = parent;
Name = name;
Container = container;
}
private HandsComponent Parent { get; }
public string Name { get; }
public IEntity? Entity => Container.ContainedEntity;
public ContainerSlot Container { get; }
public bool Enabled
{
get => _enabled;
set
{
if (_enabled == value)
{
return;
}
_enabled = value;
Parent.Dirty();
var message = value
? (ComponentMessage) new HandEnabledMsg(Name)
: new HandDisabledMsg(Name);
Parent.HandleMessage(message, Parent);
Parent.Owner.SendMessage(Parent, message);
}
}
public void Dispose()
{
Container.Shutdown(); // TODO verify this
@@ -693,7 +744,7 @@ namespace Content.Server.GameObjects.Components.GUI
public SharedHand ToShared(int index, HandLocation location)
{
return new SharedHand(index, Name, Entity?.Uid, location);
return new SharedHand(index, Name, Entity?.Uid, location, Enabled);
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.GameObjects.Components.Items.Storage;
using Robust.Server.GameObjects.Components.Container;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Timers;
@@ -67,7 +68,7 @@ namespace Content.Server.GameObjects.Components.GUI
switch (message)
{
case ContainerContentsModifiedMessage contentsModified:
Timer.Spawn(0, DropIdAndPocketsIfWeNoLongerHaveAUniform);
Owner.SpawnTimer(0, DropIdAndPocketsIfWeNoLongerHaveAUniform);
break;
}
}

View File

@@ -156,6 +156,13 @@ namespace Content.Server.GameObjects.Components.GUI
{
return GetSlotItem<ItemComponent>(slot);
}
public IEnumerable<T> LookupItems<T>() where T: Component
{
return _slotContainers.Values.SelectMany(x => x.ContainedEntities.Select(e => e.GetComponentOrNull<T>()))
.Where(x => x != null);
}
public T GetSlotItem<T>(Slots slot) where T : ItemComponent
{
if (!_slotContainers.ContainsKey(slot))
@@ -435,7 +442,7 @@ namespace Content.Server.GameObjects.Components.GUI
var activeHand = hands.GetActiveHand;
if (activeHand != null && activeHand.Owner.TryGetComponent(out ItemComponent clothing))
{
hands.Drop(hands.ActiveHand);
hands.Drop(hands.ActiveHand, doDropInteraction:false);
if (!Equip(msg.Inventoryslot, clothing, true, out var reason))
{
hands.PutInHand(clothing);
@@ -534,7 +541,7 @@ namespace Content.Server.GameObjects.Components.GUI
var list = new List<KeyValuePair<Slots, EntityUid>>();
foreach (var (slot, container) in _slotContainers)
{
if (container.ContainedEntity != null)
if (container != null && container.ContainedEntity != null)
{
list.Add(new KeyValuePair<Slots, EntityUid>(slot, container.ContainedEntity.Uid));
}

View File

@@ -70,6 +70,47 @@ namespace Content.Server.GameObjects.Components.Instruments
[ViewVariables]
private int _midiEventCount = 0;
private byte _instrumentProgram;
private byte _instrumentBank;
private bool _allowPercussion;
private bool _allowProgramChange;
[ViewVariables(VVAccess.ReadWrite)]
public override byte InstrumentProgram { get => _instrumentProgram;
set
{
_instrumentProgram = value;
Dirty();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public override byte InstrumentBank { get => _instrumentBank;
set
{
_instrumentBank = value;
Dirty();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public override bool AllowPercussion { get => _allowPercussion;
set
{
_allowPercussion = value;
Dirty();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public override bool AllowProgramChange { get => _allowProgramChange;
set
{
_allowProgramChange = value;
Dirty();
}
}
/// <summary>
/// Whether the instrument is an item which can be held or not.
/// </summary>
@@ -130,11 +171,15 @@ namespace Content.Server.GameObjects.Components.Instruments
{
base.ExposeData(serializer);
serializer.DataField(ref _handheld, "handheld", false);
serializer.DataField(ref _instrumentProgram, "program", (byte) 1);
serializer.DataField(ref _instrumentBank, "bank", (byte) 0);
serializer.DataField(ref _allowPercussion, "allowPercussion", false);
serializer.DataField(ref _allowProgramChange, "allowProgramChange", false);
}
public override ComponentState GetComponentState()
{
return new InstrumentState(Playing, _lastSequencerTick);
return new InstrumentState(Playing, InstrumentProgram, InstrumentBank, AllowPercussion, AllowProgramChange, _lastSequencerTick);
}
public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null)

View File

@@ -1,52 +1,33 @@
#nullable enable
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Clothing;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Power;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Interactable
{
/// <summary>
/// Component that represents a handheld lightsource which can be toggled on and off.
/// Component that represents a powered handheld light source which can be toggled on and off.
/// </summary>
[RegisterComponent]
internal sealed class HandheldLightComponent : SharedHandheldLightComponent, IUse, IExamine, IInteractUsing,
IMapInit
internal sealed class HandheldLightComponent : SharedHandheldLightComponent, IUse, IExamine, IInteractUsing
{
[ViewVariables(VVAccess.ReadWrite)] public float Wattage { get; set; } = 10;
[ViewVariables] private ContainerSlot _cellContainer = default!;
[ViewVariables]
private BatteryComponent? Cell
{
get
{
if (_cellContainer.ContainedEntity == null) return null;
if (_cellContainer.ContainedEntity.TryGetComponent(out BatteryComponent? cell))
{
return cell;
}
return null;
}
}
[ViewVariables(VVAccess.ReadWrite)] public float Wattage { get; set; } = 10f;
[ViewVariables] private PowerCellSlotComponent _cellSlot = default!;
private PowerCellComponent? Cell => _cellSlot.Cell;
/// <summary>
/// Status of light, whether or not it is emitting light.
@@ -54,26 +35,36 @@ namespace Content.Server.GameObjects.Components.Interactable
[ViewVariables]
public bool Activated { get; private set; }
[ViewVariables] protected override bool HasCell => Cell != null;
[ViewVariables] protected override bool HasCell => _cellSlot.HasCell;
[ViewVariables(VVAccess.ReadWrite)] public string? TurnOnSound;
[ViewVariables(VVAccess.ReadWrite)] public string? TurnOnFailSound;
[ViewVariables(VVAccess.ReadWrite)] public string? TurnOffSound;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Wattage, "wattage", 10f);
serializer.DataField(ref TurnOnSound, "turnOnSound", "/Audio/Items/flashlight_toggle.ogg");
serializer.DataField(ref TurnOnFailSound, "turnOnFailSound", "/Audio/Machines/button.ogg");
serializer.DataField(ref TurnOffSound, "turnOffSound", "/Audio/Items/flashlight_toggle.ogg");
}
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponent<PointLightComponent>();
_cellSlot = Owner.EnsureComponent<PowerCellSlotComponent>();
Dirty();
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!eventArgs.Using.HasComponent<BatteryComponent>()) return false;
if (Cell != null) return false;
var handsComponent = eventArgs.User.GetComponent<IHandsComponent>();
if (!handsComponent.Drop(eventArgs.Using, _cellContainer))
{
return false;
}
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Items/pistol_magin.ogg", Owner);
if (!ActionBlockerSystem.CanInteract(eventArgs.User)) return false;
if (!_cellSlot.InsertCell(eventArgs.Using)) return false;
Dirty();
return true;
}
@@ -83,6 +74,10 @@ namespace Content.Server.GameObjects.Components.Interactable
{
message.AddMarkup(Loc.GetString("The light is currently [color=darkgreen]on[/color]."));
}
else
{
message.AddMarkup(Loc.GetString("The light is currently [color=darkred]off[/color]."));
}
}
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
@@ -90,45 +85,20 @@ namespace Content.Server.GameObjects.Components.Interactable
return ToggleStatus(eventArgs.User);
}
public override void Initialize()
{
base.Initialize();
Owner.EnsureComponent<PointLightComponent>();
_cellContainer =
ContainerManagerComponent.Ensure<ContainerSlot>("flashlight_cell_container", Owner, out _);
Dirty();
}
/// <summary>
/// Illuminates the light if it is not active, extinguishes it if it is active.
/// </summary>
/// <returns>True if the light's status was toggled, false otherwise.</returns>
private bool ToggleStatus(IEntity user)
{
var item = Owner.GetComponent<ItemComponent>();
// Update sprite and light states to match the activation.
if (Activated)
{
TurnOff();
item.EquippedPrefix = "off";
}
else
{
TurnOn(user);
item.EquippedPrefix = "on";
return Activated ? TurnOff() : TurnOn(user);
}
// Toggle always succeeds.
return true;
}
private void TurnOff(bool makeNoise = true)
private bool TurnOff(bool makeNoise = true)
{
if (!Activated)
{
return;
return false;
}
SetState(false);
@@ -136,40 +106,41 @@ namespace Content.Server.GameObjects.Components.Interactable
if (makeNoise)
{
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Items/flashlight_toggle.ogg", Owner);
}
if (TurnOffSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOffSound, Owner);
}
private void TurnOn(IEntity user)
return true;
}
private bool TurnOn(IEntity user)
{
if (Activated)
{
return;
return false;
}
var cell = Cell;
if (cell == null)
if (Cell == null)
{
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Machines/button.ogg", Owner);
if (TurnOnFailSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOnFailSound, Owner);
Owner.PopupMessage(user, Loc.GetString("Cell missing..."));
return;
return false;
}
// To prevent having to worry about frame time in here.
// Let's just say you need a whole second of charge before you can turn it on.
// Simple enough.
if (Wattage > cell.CurrentCharge)
if (Wattage > Cell.CurrentCharge)
{
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Machines/button.ogg", Owner);
if (TurnOnFailSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOnFailSound, Owner);
Owner.PopupMessage(user, Loc.GetString("Dead cell..."));
return;
return false;
}
Activated = true;
SetState(true);
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Items/flashlight_toggle.ogg", Owner);
if (TurnOnSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOnSound, Owner);
return true;
}
private void SetState(bool on)
@@ -188,11 +159,20 @@ namespace Content.Server.GameObjects.Components.Interactable
{
clothing.ClothingEquippedPrefix = on ? "On" : "Off";
}
if (Owner.TryGetComponent(out ItemComponent? item))
{
item.EquippedPrefix = on ? "on" : "off";
}
}
public void OnUpdate(float frameTime)
{
if (!Activated || Cell == null) return;
if (Cell == null)
{
TurnOff(false);
return;
}
var appearanceComponent = Owner.GetComponent<AppearanceComponent>();
@@ -209,43 +189,10 @@ namespace Content.Server.GameObjects.Components.Interactable
appearanceComponent.SetData(HandheldLightVisuals.Power, HandheldLightPowerStates.Dying);
}
if (Cell == null || !Cell.TryUseCharge(Wattage * frameTime)) TurnOff();
if (Activated && !Cell.TryUseCharge(Wattage * frameTime)) TurnOff(false);
Dirty();
}
private void EjectCell(IEntity user)
{
if (Cell == null)
{
return;
}
var cell = Cell;
if (!_cellContainer.Remove(cell.Owner))
{
return;
}
Dirty();
if (!user.TryGetComponent(out HandsComponent? hands))
{
return;
}
if (!hands.PutInHand(cell.Owner.GetComponent<ItemComponent>()))
{
cell.Owner.Transform.Coordinates = user.Transform.Coordinates;
}
// Assuming the battery has just been taken out of the flashlight, make sure it's getting disabled
TurnOff(false);
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Items/pistol_magout.ogg", Owner);
}
public override ComponentState GetComponentState()
{
if (Cell == null)
@@ -262,44 +209,5 @@ namespace Content.Server.GameObjects.Components.Interactable
return new HandheldLightComponentState(Cell.CurrentCharge / Cell.MaxCharge, HasCell);
}
[Verb]
public sealed class EjectCellVerb : Verb<HandheldLightComponent>
{
protected override void GetData(IEntity user, HandheldLightComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
if (component.Cell == null)
{
data.Text = Loc.GetString("Eject cell (cell missing)");
data.Visibility = VerbVisibility.Disabled;
}
else
{
data.Text = Loc.GetString("Eject cell");
}
}
protected override void Activate(IEntity user, HandheldLightComponent component)
{
component.EjectCell(user);
}
}
void IMapInit.MapInit()
{
if (_cellContainer.ContainedEntity != null)
{
return;
}
var cell = Owner.EntityManager.SpawnEntity("PowerCellSmallStandard", Owner.Transform.Coordinates);
_cellContainer.Insert(cell);
}
}
}

View File

@@ -22,7 +22,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage
public override string Name => "CursedEntityStorage";
public override void CloseStorage()
protected override void CloseStorage()
{
base.CloseStorage();
@@ -41,7 +41,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage
var locker = lockerEnt.GetComponent<EntityStorageComponent>();
if(locker.Open)
locker.CloseStorage();
locker.TryCloseStorage(Owner);
foreach (var entity in Contents.ContainedEntities.ToArray())
{

View File

@@ -47,11 +47,15 @@ namespace Content.Server.GameObjects.Components.Items.Storage
[ViewVariables]
private bool _isCollidableWhenOpen;
[ViewVariables]
private IEntityQuery _entityQuery;
protected IEntityQuery EntityQuery;
private bool _showContents;
private bool _occludesLight;
private bool _open;
private bool _canWeldShut;
private bool _isWeldedShut;
private string _closeSound = "/Audio/Machines/closetclose.ogg";
private string _openSound = "/Audio/Machines/closetopen.ogg";
[ViewVariables]
protected Container Contents;
@@ -104,14 +108,24 @@ namespace Content.Server.GameObjects.Components.Items.Storage
}
[ViewVariables(VVAccess.ReadWrite)]
public bool CanWeldShut { get; set; }
public bool CanWeldShut {
get => _canWeldShut;
set
{
_canWeldShut = value;
if (Owner.TryGetComponent(out AppearanceComponent appearance))
{
appearance.SetData(StorageVisuals.CanWeld, value);
}
}
}
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
Contents = ContainerManagerComponent.Ensure<Container>(nameof(EntityStorageComponent), Owner);
_entityQuery = new IntersectingEntityQuery(Owner);
EntityQuery = new IntersectingEntityQuery(Owner);
Contents.ShowContents = _showContents;
Contents.OccludesLight = _occludesLight;
@@ -134,6 +148,8 @@ namespace Content.Server.GameObjects.Components.Items.Storage
serializer.DataField(ref _open, "open", false);
serializer.DataField(this, a => a.IsWeldedShut, "IsWeldedShut", false);
serializer.DataField(this, a => a.CanWeldShut, "CanWeldShut", true);
serializer.DataField(this, x => _closeSound, "closeSound", "/Audio/Machines/closetclose.ogg");
serializer.DataField(this, x => _openSound, "openSound", "/Audio/Machines/closetopen.ogg");
}
public virtual void Activate(ActivateEventArgs eventArgs)
@@ -141,17 +157,26 @@ namespace Content.Server.GameObjects.Components.Items.Storage
ToggleOpen(eventArgs.User);
}
private void ToggleOpen(IEntity user)
public virtual bool CanOpen(IEntity user, bool silent = false)
{
if (IsWeldedShut)
{
Owner.PopupMessage(user, Loc.GetString("It's welded completely shut!"));
return;
if(!silent) Owner.PopupMessage(user, Loc.GetString("It's welded completely shut!"));
return false;
}
return true;
}
public virtual bool CanClose(IEntity user, bool silent = false)
{
return true;
}
private void ToggleOpen(IEntity user)
{
if (Open)
{
CloseStorage();
TryCloseStorage(user);
}
else
{
@@ -159,10 +184,10 @@ namespace Content.Server.GameObjects.Components.Items.Storage
}
}
public virtual void CloseStorage()
protected virtual void CloseStorage()
{
Open = false;
var entities = Owner.EntityManager.GetEntities(_entityQuery);
var entities = Owner.EntityManager.GetEntities(EntityQuery);
var count = 0;
foreach (var entity in entities)
{
@@ -187,16 +212,16 @@ namespace Content.Server.GameObjects.Components.Items.Storage
}
ModifyComponents();
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Machines/closetclose.ogg", Owner);
EntitySystem.Get<AudioSystem>().PlayFromEntity(_closeSound, Owner);
_lastInternalOpenAttempt = default;
}
private void OpenStorage()
protected virtual void OpenStorage()
{
Open = true;
EmptyContents();
ModifyComponents();
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Machines/closetopen.ogg", Owner);
EntitySystem.Get<AudioSystem>().PlayFromEntity(_openSound, Owner);
}
private void ModifyComponents()
@@ -224,8 +249,9 @@ namespace Content.Server.GameObjects.Components.Items.Storage
}
}
private bool AddToContents(IEntity entity)
protected virtual bool AddToContents(IEntity entity)
{
if (entity == Owner) return false;
if (entity.TryGetComponent(out IPhysicsComponent entityPhysicsComponent))
{
if(MaxSize < entityPhysicsComponent.WorldAABB.Size.X
@@ -247,12 +273,18 @@ namespace Content.Server.GameObjects.Components.Items.Storage
return false;
}
public virtual Vector2 ContentsDumpPosition()
{
return Owner.Transform.WorldPosition;
}
private void EmptyContents()
{
foreach (var contained in Contents.ContainedEntities.ToArray())
{
if(Contents.Remove(contained))
{
contained.Transform.WorldPosition = ContentsDumpPosition();
if (contained.TryGetComponent<IPhysicsComponent>(out var physics))
{
physics.CanCollide = true;
@@ -284,14 +316,18 @@ namespace Content.Server.GameObjects.Components.Items.Storage
}
}
protected virtual void TryOpenStorage(IEntity user)
public virtual bool TryOpenStorage(IEntity user)
{
if (IsWeldedShut)
{
Owner.PopupMessage(user, Loc.GetString("It's welded completely shut!"));
return;
}
if (!CanOpen(user)) return false;
OpenStorage();
return true;
}
public virtual bool TryCloseStorage(IEntity user)
{
if (!CanClose(user)) return false;
CloseStorage();
return true;
}
/// <inheritdoc />

View File

@@ -58,12 +58,12 @@ namespace Content.Server.GameObjects.Components.Items.Storage
}
}
public void Equipped(EquippedEventArgs eventArgs)
public virtual void Equipped(EquippedEventArgs eventArgs)
{
EquippedToSlot();
}
public void Unequipped(UnequippedEventArgs eventArgs)
public virtual void Unequipped(UnequippedEventArgs eventArgs)
{
RemovedFromSlot();
}

View File

@@ -69,15 +69,14 @@ namespace Content.Server.GameObjects.Components.Items.Storage
base.Activate(eventArgs);
}
protected override void TryOpenStorage(IEntity user)
public override bool CanOpen(IEntity user, bool silent = false)
{
if (Locked)
{
Owner.PopupMessage(user, "It's locked!");
return;
return false;
}
base.TryOpenStorage(user);
return base.CanOpen(user, silent);
}
protected override void OpenVerbGetData(IEntity user, EntityStorageComponent component, VerbData data)

View File

@@ -23,6 +23,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
private string? _meatPrototype;
private string _meatSource1p = "?";
private string _meatSource0 = "?";
private string _meatName = "?";
void IActivate.Activate(ActivateEventArgs eventArgs)
{
@@ -38,7 +39,11 @@ namespace Content.Server.GameObjects.Components.Kitchen
if (!string.IsNullOrEmpty(_meatPrototype))
{
Owner.EntityManager.SpawnEntity(_meatPrototype, Owner.Transform.Coordinates);
var meat = Owner.EntityManager.SpawnEntity(_meatPrototype, Owner.Transform.Coordinates);
if (meat != null)
{
meat.Name = _meatName;
}
}
if (_meatParts != 0)
@@ -74,6 +79,9 @@ namespace Content.Server.GameObjects.Components.Kitchen
_meatParts = 5;
_meatSource1p = Loc.GetString("You remove some meat from {0:theName}.", victim);
_meatSource0 = Loc.GetString("You remove the last piece of meat from {0:theName}!", victim);
// TODO: This could stand to be improved somehow, but it'd require Name to be much 'richer' in detail than it presently is.
// But Name is RobustToolbox-level, so presumably it'd have to be done in some other way (interface???)
_meatName = Loc.GetString("{0:name} meat", victim);
if (Owner.TryGetComponent(out sprite))
{

View File

@@ -26,6 +26,7 @@ using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
@@ -312,7 +313,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
(_currentCookTimerTime == (uint)recipeToCook.CookTime);
SetAppearance(MicrowaveVisualState.Cooking);
_audioSystem.PlayFromEntity(_startCookingSound, Owner, AudioParams.Default);
Timer.Spawn((int)(_currentCookTimerTime * _cookTimeMultiplier), (Action)(() =>
Owner.SpawnTimer((int)(_currentCookTimerTime * _cookTimeMultiplier), (Action)(() =>
{
if (_lostPower)
{

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
@@ -67,7 +68,7 @@ namespace Content.Server.GameObjects.Components.Markers
{
TokenSource?.Cancel();
TokenSource = new CancellationTokenSource();
Timer.SpawnRepeating(TimeSpan.FromSeconds(IntervalSeconds), OnTimerFired, TokenSource.Token);
Owner.SpawnRepeatingTimer(TimeSpan.FromSeconds(IntervalSeconds), OnTimerFired, TokenSource.Token);
}
private void OnTimerFired()

View File

@@ -1,7 +1,5 @@
#nullable enable
using System;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Observer;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
@@ -19,6 +17,7 @@ using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
@@ -76,12 +75,12 @@ namespace Content.Server.GameObjects.Components.Medical
HandleGhostReturn);
}
public void Update(float frametime)
public void Update(float frameTime)
{
if (_bodyContainer.ContainedEntity != null &&
Powered)
{
_cloningProgress += frametime;
_cloningProgress += frameTime;
_cloningProgress = MathHelper.Clamp(_cloningProgress, 0f, _cloningTime);
}
@@ -122,7 +121,9 @@ namespace Content.Server.GameObjects.Components.Medical
private CloningPodBoundUserInterfaceState GetUserInterfaceState()
{
return new CloningPodBoundUserInterfaceState(CloningSystem.getIdToUser(), _cloningProgress,
var idToUser = EntitySystem.Get<CloningSystem>().GetIdToUser();
return new CloningPodBoundUserInterfaceState(idToUser, _cloningProgress,
(_status == CloningPodStatus.Cloning));
}
@@ -152,11 +153,12 @@ namespace Content.Server.GameObjects.Components.Medical
switch (message.Button)
{
case UiButton.Clone:
if (message.ScanId == null) return;
var cloningSystem = EntitySystem.Get<CloningSystem>();
if (_bodyContainer.ContainedEntity != null ||
!CloningSystem.Minds.TryGetValue(message.ScanId.Value, out var mind))
!cloningSystem.Minds.TryGetValue(message.ScanId.Value, out var mind))
{
return;
}

View File

@@ -20,6 +20,7 @@ using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
@@ -126,9 +127,12 @@ namespace Content.Server.GameObjects.Components.Medical
return new MedicalScannerBoundUserInterfaceState(body.Uid, classes, types, true);
}
var cloningSystem = EntitySystem.Get<CloningSystem>();
var scanned = _bodyContainer.ContainedEntity.TryGetComponent(out MindComponent? mindComponent) &&
mindComponent.Mind != null &&
cloningSystem.HasDnaScan(mindComponent.Mind);
return new MedicalScannerBoundUserInterfaceState(body.Uid, classes, types,
CloningSystem.HasDnaScan(_bodyContainer.ContainedEntity.GetComponent<MindComponent>().Mind));
return new MedicalScannerBoundUserInterfaceState(body.Uid, classes, types, scanned);
}
private void UpdateUserInterface()
@@ -261,7 +265,8 @@ namespace Content.Server.GameObjects.Components.Medical
if (_bodyContainer.ContainedEntity != null)
{
//TODO: Show a 'ERROR: Body is completely devoid of soul' if no Mind owns the entity.
CloningSystem.AddToDnaScans(_playerManager
var cloningSystem = EntitySystem.Get<CloningSystem>();
cloningSystem.AddToDnaScans(_playerManager
.GetPlayersBy(playerSession =>
{
var mindOwnedMob = playerSession.ContentData()?.Mind?.OwnedEntity;

View File

@@ -9,6 +9,7 @@ using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
@@ -135,7 +136,7 @@ namespace Content.Server.GameObjects.Components.Mobs
else
{
var spawnPosition = Owner.Transform.Coordinates;
Timer.Spawn(0, () =>
Owner.SpawnTimer(0, () =>
{
// Async this so that we don't throw if the grid we're on is being deleted.
var mapMan = IoCManager.Resolve<IMapManager>();

View File

@@ -2,8 +2,8 @@
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.Components.Buckle;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Movement;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Pulling;
using Content.Shared.GameObjects.EntitySystems;
@@ -23,6 +23,22 @@ namespace Content.Server.GameObjects.Components.Mobs
[ViewVariables]
private readonly Dictionary<StatusEffect, StatusEffectStatus> _statusEffects = new Dictionary<StatusEffect, StatusEffectStatus>();
public override IReadOnlyDictionary<StatusEffect, StatusEffectStatus> Statuses => _statusEffects;
protected override void Startup()
{
base.Startup();
EntitySystem.Get<WeightlessSystem>().AddStatus(this);
}
public override void OnRemove()
{
EntitySystem.Get<WeightlessSystem>().RemoveStatus(this);
base.OnRemove();
}
public override ComponentState GetComponentState()
{
return new StatusEffectComponentState(_statusEffects);

View File

@@ -38,8 +38,6 @@ namespace Content.Server.GameObjects.Components.Mobs.State
public override void ExitState(IEntity entity)
{
EntitySystem.Get<StandingStateSystem>().Standing(entity);
if (entity.TryGetComponent(out ServerOverlayEffectsComponent overlay))
{
overlay.ClearOverlays();

View File

@@ -44,8 +44,6 @@ namespace Content.Server.GameObjects.Components.Mobs.State
public override void ExitState(IEntity entity)
{
EntitySystem.Get<StandingStateSystem>().Standing(entity);
if (entity.TryGetComponent(out IPhysicsComponent physics))
{
physics.CanCollide = true;

View File

@@ -1,10 +1,10 @@
using Content.Server.GameObjects.Components.Body;
using Content.Server.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Body;
using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.GameObjects.Components.Mobs.State
@@ -13,6 +13,8 @@ namespace Content.Server.GameObjects.Components.Mobs.State
{
public override void EnterState(IEntity entity)
{
EntitySystem.Get<StandingStateSystem>().Standing(entity);
if (entity.TryGetComponent(out AppearanceComponent appearance))
{
appearance.SetData(DamageStateVisuals.State, DamageState.Alive);

View File

@@ -5,6 +5,7 @@ using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.Interfaces.GameObjects.Components;
using NFluidsynth;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Timers;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
@@ -101,7 +102,7 @@ namespace Content.Server.GameObjects.Components.Mobs
if (progress >= length)
{
Timer.Spawn(250, () => status.RemoveStatusEffect(StatusEffect.Stun), StatusRemoveCancellation.Token);
Owner.SpawnTimer(250, () => status.RemoveStatusEffect(StatusEffect.Stun), StatusRemoveCancellation.Token);
LastStun = null;
}
}

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