Merge pull request #413 from crystallpunk-14/ed-19-08-2024-upstream

Ed 19 08 2024 upstream
This commit is contained in:
Ed
2024-08-21 16:35:11 +03:00
committed by GitHub
684 changed files with 212110 additions and 436251 deletions

6
.vscode/tasks.json vendored
View File

@@ -10,7 +10,7 @@
"args": [
"build",
"/property:GenerateFullPaths=true", // Ask dotnet build to generate full paths for file names.
"/consoleloggerparameters:NoSummary" // Do not generate summary otherwise it leads to duplicate errors in Problems panel
"/consoleloggerparameters:'ForceNoAlign;NoSummary'" // Do not generate summary otherwise it leads to duplicate errors in Problems panel
],
"group": {
"kind": "build",
@@ -29,9 +29,9 @@
"build",
"${workspaceFolder}/Content.YAMLLinter/Content.YAMLLinter.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
"/consoleloggerparameters:'ForceNoAlign;NoSummary'"
],
"problemMatcher": "$msCompile"
}
]
}
}

View File

@@ -46,7 +46,7 @@ public class MapLoadBenchmark
PoolManager.Shutdown();
}
public static readonly string[] MapsSource = { "Empty", "Box", "Bagel", "Dev", "CentComm", "Atlas", "Core", "TestTeg", "Saltern", "Packed", "Omega", "Cluster", "Reach", "Origin", "Meta", "Marathon", "Europa", "MeteorArena", "Fland", "Barratry", "Oasis" };
public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Cog" };
[ParamsSource(nameof(MapsSource))]
public string Map;

View File

@@ -7,67 +7,66 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Content.Client.Administration
namespace Content.Client.Administration;
internal sealed class AdminNameOverlay : Overlay
{
internal sealed class AdminNameOverlay : Overlay
private readonly AdminSystem _system;
private readonly IEntityManager _entityManager;
private readonly IEyeManager _eyeManager;
private readonly EntityLookupSystem _entityLookup;
private readonly Font _font;
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup)
{
private readonly AdminSystem _system;
private readonly IEntityManager _entityManager;
private readonly IEyeManager _eyeManager;
private readonly EntityLookupSystem _entityLookup;
private readonly Font _font;
_system = system;
_entityManager = entityManager;
_eyeManager = eyeManager;
_entityLookup = entityLookup;
ZIndex = 200;
_font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup)
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
protected override void Draw(in OverlayDrawArgs args)
{
var viewport = args.WorldAABB;
foreach (var playerInfo in _system.PlayerList)
{
_system = system;
_entityManager = entityManager;
_eyeManager = eyeManager;
_entityLookup = entityLookup;
ZIndex = 200;
_font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
var entity = _entityManager.GetEntity(playerInfo.NetEntity);
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
protected override void Draw(in OverlayDrawArgs args)
{
var viewport = args.WorldAABB;
foreach (var playerInfo in _system.PlayerList)
// Otherwise the entity can not exist yet
if (entity == null || !_entityManager.EntityExists(entity))
{
var entity = _entityManager.GetEntity(playerInfo.NetEntity);
// Otherwise the entity can not exist yet
if (entity == null || !_entityManager.EntityExists(entity))
{
continue;
}
// if not on the same map, continue
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
{
continue;
}
var aabb = _entityLookup.GetWorldAABB(entity.Value);
// if not on screen, continue
if (!aabb.Intersects(in viewport))
{
continue;
}
var lineoffset = new Vector2(0f, 11f);
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
if (playerInfo.Antag)
{
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "ANTAG", Color.OrangeRed);
}
args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, playerInfo.Connected ? Color.Yellow : Color.White);
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, playerInfo.Connected ? Color.Aquamarine : Color.White);
continue;
}
// if not on the same map, continue
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
{
continue;
}
var aabb = _entityLookup.GetWorldAABB(entity.Value);
// if not on screen, continue
if (!aabb.Intersects(in viewport))
{
continue;
}
var lineoffset = new Vector2(0f, 11f);
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
if (playerInfo.Antag)
{
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "ANTAG", Color.OrangeRed);
}
args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, playerInfo.Connected ? Color.Yellow : Color.White);
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, playerInfo.Connected ? Color.Aquamarine : Color.White);
}
}
}

View File

@@ -70,7 +70,7 @@ public sealed partial class PlayerPanel : FancyWindow
else
{
Whitelisted.Text = Loc.GetString("player-panel-whitelisted");
WhitelistToggle.Text = whitelisted.Value.ToString();
WhitelistToggle.Text = whitelisted.Value ? Loc.GetString("player-panel-true") : Loc.GetString("player-panel-false");
WhitelistToggle.Visible = true;
_isWhitelisted = whitelisted.Value;
}
@@ -124,7 +124,7 @@ public sealed partial class PlayerPanel : FancyWindow
NotesButton.Disabled = !_adminManager.CanCommand("adminnotes");
ShowBansButton.Disabled = !_adminManager.CanCommand("banlist");
WhitelistToggle.Disabled =
!(_adminManager.CanCommand("addwhitelist") && _adminManager.CanCommand("removewhitelist"));
!(_adminManager.CanCommand("whitelistadd") && _adminManager.CanCommand("whitelistremove"));
LogsButton.Disabled = !_adminManager.CanCommand("adminlogs");
RejuvenateButton.Disabled = !_adminManager.HasFlag(AdminFlags.Debug);
DeleteButton.Disabled = !_adminManager.HasFlag(AdminFlags.Debug);

View File

@@ -1,4 +1,4 @@
using Content.Client.Administration.Managers;
using Content.Client.Administration.Managers;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
@@ -13,6 +13,7 @@ public sealed partial class ObjectsTabEntry : PanelContainer
public Action<NetEntity>? OnTeleport;
public Action<NetEntity>? OnDelete;
private readonly Dictionary<Button, ConfirmationData> _confirmations = new();
public ObjectsTabEntry(IClientAdminManager manager, string name, NetEntity nent, StyleBox styleBox)
{
@@ -27,6 +28,13 @@ public sealed partial class ObjectsTabEntry : PanelContainer
DeleteButton.Disabled = !manager.CanCommand("delete");
TeleportButton.OnPressed += _ => OnTeleport?.Invoke(nent);
DeleteButton.OnPressed += _ => OnDelete?.Invoke(nent);
DeleteButton.OnPressed += _ =>
{
if (!AdminUIHelpers.TryConfirm(DeleteButton, _confirmations))
{
return;
}
OnDelete?.Invoke(nent);
};
}
}

View File

@@ -10,220 +10,219 @@ using Robust.Client.UserInterface.XAML;
using static Content.Client.Administration.UI.Tabs.PlayerTab.PlayerTabHeader;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Administration.UI.Tabs.PlayerTab
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
[GenerateTypedNameReferences]
public sealed partial class PlayerTab : Control
{
[GenerateTypedNameReferences]
public sealed partial class PlayerTab : Control
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _playerMan = default!;
private const string ArrowUp = "↑";
private const string ArrowDown = "↓";
private readonly Color _altColor = Color.FromHex("#292B38");
private readonly Color _defaultColor = Color.FromHex("#2F2F3B");
private readonly AdminSystem _adminSystem;
private IReadOnlyList<PlayerInfo> _players = new List<PlayerInfo>();
private Header _headerClicked = Header.Username;
private bool _ascending = true;
private bool _showDisconnected;
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
public PlayerTab()
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _playerMan = default!;
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
private const string ArrowUp = "↑";
private const string ArrowDown = "↓";
private readonly Color _altColor = Color.FromHex("#292B38");
private readonly Color _defaultColor = Color.FromHex("#2F2F3B");
private readonly AdminSystem _adminSystem;
private IReadOnlyList<PlayerInfo> _players = new List<PlayerInfo>();
_adminSystem = _entManager.System<AdminSystem>();
_adminSystem.PlayerListChanged += RefreshPlayerList;
_adminSystem.OverlayEnabled += OverlayEnabled;
_adminSystem.OverlayDisabled += OverlayDisabled;
private Header _headerClicked = Header.Username;
private bool _ascending = true;
private bool _showDisconnected;
OverlayButton.OnPressed += OverlayButtonPressed;
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
ListHeader.BackgroundColorPanel.PanelOverride = new StyleBoxFlat(_altColor);
ListHeader.OnHeaderClicked += HeaderClicked;
public PlayerTab()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
SearchList.SearchBar = SearchLineEdit;
SearchList.GenerateItem += GenerateButton;
SearchList.DataFilterCondition += DataFilterCondition;
SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data);
_adminSystem = _entManager.System<AdminSystem>();
_adminSystem.PlayerListChanged += RefreshPlayerList;
_adminSystem.OverlayEnabled += OverlayEnabled;
_adminSystem.OverlayDisabled += OverlayDisabled;
RefreshPlayerList(_adminSystem.PlayerList);
OverlayButton.OnPressed += OverlayButtonPressed;
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
ListHeader.BackgroundColorPanel.PanelOverride = new StyleBoxFlat(_altColor);
ListHeader.OnHeaderClicked += HeaderClicked;
SearchList.SearchBar = SearchLineEdit;
SearchList.GenerateItem += GenerateButton;
SearchList.DataFilterCondition += DataFilterCondition;
SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data);
RefreshPlayerList(_adminSystem.PlayerList);
}
#region Antag Overlay
private void OverlayEnabled()
{
OverlayButton.Pressed = true;
}
private void OverlayDisabled()
{
OverlayButton.Pressed = false;
}
private void OverlayButtonPressed(ButtonEventArgs args)
{
if (args.Button.Pressed)
{
_adminSystem.AdminOverlayOn();
}
else
{
_adminSystem.AdminOverlayOff();
}
}
#endregion
private void ShowDisconnectedPressed(ButtonEventArgs args)
{
_showDisconnected = args.Button.Pressed;
RefreshPlayerList(_players);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_adminSystem.PlayerListChanged -= RefreshPlayerList;
_adminSystem.OverlayEnabled -= OverlayEnabled;
_adminSystem.OverlayDisabled -= OverlayDisabled;
OverlayButton.OnPressed -= OverlayButtonPressed;
ListHeader.OnHeaderClicked -= HeaderClicked;
}
}
#region ListContainer
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players)
{
_players = players;
PlayerCount.Text = Loc.GetString("player-tab-player-count", ("count", _playerMan.PlayerCount));
var filteredPlayers = players.Where(info => _showDisconnected || info.Connected).ToList();
var sortedPlayers = new List<PlayerInfo>(filteredPlayers);
sortedPlayers.Sort(Compare);
UpdateHeaderSymbols();
SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
$"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
.ToList());
}
private void GenerateButton(ListData data, ListContainerButton button)
{
if (data is not PlayerListData { Info: var player})
return;
var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor));
button.AddChild(entry);
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
}
/// <summary>
/// Determines whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.
/// If all characters are lowercase, the comparison ignores case.
/// If there is an uppercase character, the comparison is case sensitive.
/// </summary>
/// <param name="filter"></param>
/// <param name="listData"></param>
/// <returns>Whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.</returns>
private bool DataFilterCondition(string filter, ListData listData)
{
if (listData is not PlayerListData {Info: var info, FilteringString: var playerString})
return false;
if (!_showDisconnected && !info.Connected)
return false;
if (IsAllLower(filter))
{
if (!playerString.Contains(filter, StringComparison.CurrentCultureIgnoreCase))
return false;
}
else
{
if (!playerString.Contains(filter))
return false;
}
return true;
}
private bool IsAllLower(string input)
{
foreach (var c in input)
{
if (char.IsLetter(c) && !char.IsLower(c))
return false;
}
return true;
}
#endregion
#region Header
private void UpdateHeaderSymbols()
{
ListHeader.ResetHeaderText();
ListHeader.GetHeader(_headerClicked).Text += $" {(_ascending ? ArrowUp : ArrowDown)}";
}
private int Compare(PlayerInfo x, PlayerInfo y)
{
if (!_ascending)
{
(x, y) = (y, x);
}
return _headerClicked switch
{
Header.Username => Compare(x.Username, y.Username),
Header.Character => Compare(x.CharacterName, y.CharacterName),
Header.Job => Compare(x.StartingJob, y.StartingJob),
Header.Antagonist => x.Antag.CompareTo(y.Antag),
Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default),
_ => 1
};
}
private int Compare(string x, string y)
{
return string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
}
private void HeaderClicked(Header header)
{
if (_headerClicked == header)
{
_ascending = !_ascending;
}
else
{
_headerClicked = header;
_ascending = true;
}
RefreshPlayerList(_adminSystem.PlayerList);
}
#endregion
}
public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;
#region Antag Overlay
private void OverlayEnabled()
{
OverlayButton.Pressed = true;
}
private void OverlayDisabled()
{
OverlayButton.Pressed = false;
}
private void OverlayButtonPressed(ButtonEventArgs args)
{
if (args.Button.Pressed)
{
_adminSystem.AdminOverlayOn();
}
else
{
_adminSystem.AdminOverlayOff();
}
}
#endregion
private void ShowDisconnectedPressed(ButtonEventArgs args)
{
_showDisconnected = args.Button.Pressed;
RefreshPlayerList(_players);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_adminSystem.PlayerListChanged -= RefreshPlayerList;
_adminSystem.OverlayEnabled -= OverlayEnabled;
_adminSystem.OverlayDisabled -= OverlayDisabled;
OverlayButton.OnPressed -= OverlayButtonPressed;
ListHeader.OnHeaderClicked -= HeaderClicked;
}
}
#region ListContainer
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players)
{
_players = players;
PlayerCount.Text = Loc.GetString("player-tab-player-count", ("count", _playerMan.PlayerCount));
var filteredPlayers = players.Where(info => _showDisconnected || info.Connected).ToList();
var sortedPlayers = new List<PlayerInfo>(filteredPlayers);
sortedPlayers.Sort(Compare);
UpdateHeaderSymbols();
SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
$"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
.ToList());
}
private void GenerateButton(ListData data, ListContainerButton button)
{
if (data is not PlayerListData { Info: var player})
return;
var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor));
button.AddChild(entry);
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
}
/// <summary>
/// Determines whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.
/// If all characters are lowercase, the comparison ignores case.
/// If there is an uppercase character, the comparison is case sensitive.
/// </summary>
/// <param name="filter"></param>
/// <param name="listData"></param>
/// <returns>Whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.</returns>
private bool DataFilterCondition(string filter, ListData listData)
{
if (listData is not PlayerListData {Info: var info, FilteringString: var playerString})
return false;
if (!_showDisconnected && !info.Connected)
return false;
if (IsAllLower(filter))
{
if (!playerString.Contains(filter, StringComparison.CurrentCultureIgnoreCase))
return false;
}
else
{
if (!playerString.Contains(filter))
return false;
}
return true;
}
private bool IsAllLower(string input)
{
foreach (var c in input)
{
if (char.IsLetter(c) && !char.IsLower(c))
return false;
}
return true;
}
#endregion
#region Header
private void UpdateHeaderSymbols()
{
ListHeader.ResetHeaderText();
ListHeader.GetHeader(_headerClicked).Text += $" {(_ascending ? ArrowUp : ArrowDown)}";
}
private int Compare(PlayerInfo x, PlayerInfo y)
{
if (!_ascending)
{
(x, y) = (y, x);
}
return _headerClicked switch
{
Header.Username => Compare(x.Username, y.Username),
Header.Character => Compare(x.CharacterName, y.CharacterName),
Header.Job => Compare(x.StartingJob, y.StartingJob),
Header.Antagonist => x.Antag.CompareTo(y.Antag),
Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default),
_ => 1
};
}
private int Compare(string x, string y)
{
return string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
}
private void HeaderClicked(Header header)
{
if (_headerClicked == header)
{
_ascending = !_ascending;
}
else
{
_headerClicked = header;
_ascending = true;
}
RefreshPlayerList(_adminSystem.PlayerList);
}
#endregion
}
public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;

View File

@@ -16,6 +16,8 @@ namespace Content.Client.Atmos.UI
[GenerateTypedNameReferences]
public sealed partial class GasAnalyzerWindow : DefaultWindow
{
private NetEntity _currentEntity = NetEntity.Invalid;
public GasAnalyzerWindow()
{
RobustXamlLoader.Load(this);
@@ -55,6 +57,13 @@ namespace Content.Client.Atmos.UI
// Device Tab
if (msg.NodeGasMixes.Length > 1)
{
if (_currentEntity != msg.DeviceUid)
{
// when we get new device data switch to the device tab
CTabContainer.CurrentTab = 0;
_currentEntity = msg.DeviceUid;
}
CTabContainer.SetTabVisible(0, true);
CTabContainer.SetTabTitle(0, Loc.GetString("gas-analyzer-window-tab-title-capitalized", ("title", msg.DeviceName)));
// Set up Grid
@@ -143,6 +152,7 @@ namespace Content.Client.Atmos.UI
CTabContainer.SetTabVisible(0, false);
CTabContainer.CurrentTab = 1;
minSize = new Vector2(CEnvironmentMix.DesiredSize.X + 40, MinSize.Y);
_currentEntity = NetEntity.Invalid;
}
MinSize = minSize;

View File

@@ -5,71 +5,70 @@ using Content.Shared.Chat;
using Robust.Client.Console;
using Robust.Shared.Utility;
namespace Content.Client.Chat.Managers
namespace Content.Client.Chat.Managers;
internal sealed class ChatManager : IChatManager
{
internal sealed class ChatManager : IChatManager
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
[Dependency] private readonly IEntitySystemManager _systems = default!;
private ISawmill _sawmill = default!;
public void Initialize()
{
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
[Dependency] private readonly IEntitySystemManager _systems = default!;
_sawmill = Logger.GetSawmill("chat");
_sawmill.Level = LogLevel.Info;
}
private ISawmill _sawmill = default!;
public void Initialize()
public void SendMessage(string text, ChatSelectChannel channel)
{
var str = text.ToString();
switch (channel)
{
_sawmill = Logger.GetSawmill("chat");
_sawmill.Level = LogLevel.Info;
}
case ChatSelectChannel.Console:
// run locally
_consoleHost.ExecuteCommand(text);
break;
public void SendMessage(string text, ChatSelectChannel channel)
{
var str = text.ToString();
switch (channel)
{
case ChatSelectChannel.Console:
// run locally
_consoleHost.ExecuteCommand(text);
break;
case ChatSelectChannel.LOOC:
_consoleHost.ExecuteCommand($"looc \"{CommandParsing.Escape(str)}\"");
break;
case ChatSelectChannel.LOOC:
_consoleHost.ExecuteCommand($"looc \"{CommandParsing.Escape(str)}\"");
break;
case ChatSelectChannel.OOC:
_consoleHost.ExecuteCommand($"ooc \"{CommandParsing.Escape(str)}\"");
break;
case ChatSelectChannel.OOC:
_consoleHost.ExecuteCommand($"ooc \"{CommandParsing.Escape(str)}\"");
break;
case ChatSelectChannel.Admin:
_consoleHost.ExecuteCommand($"asay \"{CommandParsing.Escape(str)}\"");
break;
case ChatSelectChannel.Admin:
_consoleHost.ExecuteCommand($"asay \"{CommandParsing.Escape(str)}\"");
break;
case ChatSelectChannel.Emotes:
_consoleHost.ExecuteCommand($"me \"{CommandParsing.Escape(str)}\"");
break;
case ChatSelectChannel.Emotes:
_consoleHost.ExecuteCommand($"me \"{CommandParsing.Escape(str)}\"");
break;
case ChatSelectChannel.Dead:
if (_systems.GetEntitySystemOrNull<GhostSystem>() is {IsGhost: true})
goto case ChatSelectChannel.Local;
case ChatSelectChannel.Dead:
if (_systems.GetEntitySystemOrNull<GhostSystem>() is {IsGhost: true})
goto case ChatSelectChannel.Local;
if (_adminMgr.HasFlag(AdminFlags.Admin))
_consoleHost.ExecuteCommand($"dsay \"{CommandParsing.Escape(str)}\"");
else
_sawmill.Warning("Tried to speak on deadchat without being ghost or admin.");
break;
if (_adminMgr.HasFlag(AdminFlags.Admin))
_consoleHost.ExecuteCommand($"dsay \"{CommandParsing.Escape(str)}\"");
else
_sawmill.Warning("Tried to speak on deadchat without being ghost or admin.");
break;
// TODO sepearate radio and say into separate commands.
case ChatSelectChannel.Radio:
case ChatSelectChannel.Local:
_consoleHost.ExecuteCommand($"say \"{CommandParsing.Escape(str)}\"");
break;
// TODO sepearate radio and say into separate commands.
case ChatSelectChannel.Radio:
case ChatSelectChannel.Local:
_consoleHost.ExecuteCommand($"say \"{CommandParsing.Escape(str)}\"");
break;
case ChatSelectChannel.Whisper:
_consoleHost.ExecuteCommand($"whisper \"{CommandParsing.Escape(str)}\"");
break;
case ChatSelectChannel.Whisper:
_consoleHost.ExecuteCommand($"whisper \"{CommandParsing.Escape(str)}\"");
break;
default:
throw new ArgumentOutOfRangeException(nameof(channel), channel, null);
}
default:
throw new ArgumentOutOfRangeException(nameof(channel), channel, null);
}
}
}

View File

@@ -283,7 +283,7 @@ public sealed partial class SingleMarkingPicker : BoxContainer
for (var i = 0; i < PointsUsed; i++)
{
SlotSelector.AddItem($"Slot {i + 1}", i);
SlotSelector.AddItem(Loc.GetString("marking-slot", ("number", $"{i + 1}")), i);
if (i == _slot)
{

View File

@@ -16,6 +16,8 @@ namespace Content.Client.IconSmoothing
[UsedImplicitly]
public sealed partial class IconSmoothSystem : EntitySystem
{
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
private readonly Queue<EntityUid> _dirtyEntities = new();
private readonly Queue<EntityUid> _anchorChangedEntities = new();
@@ -46,7 +48,7 @@ namespace Content.Client.IconSmoothing
if (xform.Anchored)
{
component.LastPosition = TryComp<MapGridComponent>(xform.GridUid, out var grid)
? (xform.GridUid.Value, grid.TileIndicesFor(xform.Coordinates))
? (xform.GridUid.Value, _mapSystem.TileIndicesFor(xform.GridUid.Value, grid, xform.Coordinates))
: (null, new Vector2i(0, 0));
DirtyNeighbours(uid, component);
@@ -151,9 +153,12 @@ namespace Content.Client.IconSmoothing
Vector2i pos;
EntityUid entityUid;
if (transform.Anchored && TryComp<MapGridComponent>(transform.GridUid, out var grid))
{
pos = grid.CoordinatesToTile(transform.Coordinates);
entityUid = transform.GridUid.Value;
pos = _mapSystem.CoordinatesToTile(transform.GridUid.Value, grid, transform.Coordinates);
}
else
{
@@ -164,21 +169,22 @@ namespace Content.Client.IconSmoothing
if (!TryComp(gridId, out grid))
return;
entityUid = gridId;
pos = oldPos;
}
// Yes, we updates ALL smoothing entities surrounding us even if they would never smooth with us.
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, 0)));
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, 0)));
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, 1)));
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, -1)));
DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(1, 0)));
DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(-1, 0)));
DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(0, 1)));
DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(0, -1)));
if (comp.Mode is IconSmoothingMode.Corners or IconSmoothingMode.NoSprite or IconSmoothingMode.Diagonal)
{
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, 1)));
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, -1)));
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, 1)));
DirtyEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, -1)));
DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(1, 1)));
DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(-1, -1)));
DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(-1, 1)));
DirtyEntities(_mapSystem.GetAnchoredEntitiesEnumerator(entityUid, grid, pos + new Vector2i(1, -1)));
}
}
@@ -206,7 +212,7 @@ namespace Content.Client.IconSmoothing
IconSmoothComponent? smooth = null)
{
TransformComponent? xform;
MapGridComponent? grid = null;
Entity<MapGridComponent>? gridEntity = null;
// The generation check prevents updating an entity multiple times per tick.
// As it stands now, it's totally possible for something to get queued twice.
@@ -223,17 +229,20 @@ namespace Content.Client.IconSmoothing
{
var directions = DirectionFlag.None;
if (TryComp(xform.GridUid, out grid))
if (TryComp(xform.GridUid, out MapGridComponent? grid))
{
var pos = grid.TileIndicesFor(xform.Coordinates);
var gridUid = xform.GridUid.Value;
var pos = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates);
if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.North)), smoothQuery))
gridEntity = (gridUid, grid);
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)), smoothQuery))
directions |= DirectionFlag.North;
if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.South)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)), smoothQuery))
directions |= DirectionFlag.South;
if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.East)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)), smoothQuery))
directions |= DirectionFlag.East;
if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.West)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)), smoothQuery))
directions |= DirectionFlag.West;
}
@@ -257,7 +266,11 @@ namespace Content.Client.IconSmoothing
if (xform.Anchored)
{
if (!TryComp(xform.GridUid, out grid))
if (TryComp(xform.GridUid, out MapGridComponent? grid))
{
gridEntity = (xform.GridUid.Value, grid);
}
else
{
Log.Error($"Failed to calculate IconSmoothComponent sprite in {uid} because grid {xform.GridUid} was missing.");
return;
@@ -267,28 +280,31 @@ namespace Content.Client.IconSmoothing
switch (smooth.Mode)
{
case IconSmoothingMode.Corners:
CalculateNewSpriteCorners(grid, smooth, spriteEnt, xform, smoothQuery);
CalculateNewSpriteCorners(gridEntity, smooth, spriteEnt, xform, smoothQuery);
break;
case IconSmoothingMode.CardinalFlags:
CalculateNewSpriteCardinal(grid, smooth, spriteEnt, xform, smoothQuery);
CalculateNewSpriteCardinal(gridEntity, smooth, spriteEnt, xform, smoothQuery);
break;
case IconSmoothingMode.Diagonal:
CalculateNewSpriteDiagonal(grid, smooth, spriteEnt, xform, smoothQuery);
CalculateNewSpriteDiagonal(gridEntity, smooth, spriteEnt, xform, smoothQuery);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private void CalculateNewSpriteDiagonal(MapGridComponent? grid, IconSmoothComponent smooth,
private void CalculateNewSpriteDiagonal(Entity<MapGridComponent>? gridEntity, IconSmoothComponent smooth,
Entity<SpriteComponent> sprite, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
{
if (grid == null)
if (gridEntity == null)
{
sprite.Comp.LayerSetState(0, $"{smooth.StateBase}0");
return;
}
var gridUid = gridEntity.Value.Owner;
var grid = gridEntity.Value.Comp;
var neighbors = new Vector2[]
{
new(1, 0),
@@ -296,14 +312,14 @@ namespace Content.Client.IconSmoothing
new(0, -1),
};
var pos = grid.TileIndicesFor(xform.Coordinates);
var pos = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates);
var rotation = xform.LocalRotation;
var matching = true;
for (var i = 0; i < neighbors.Length; i++)
{
var neighbor = (Vector2i) rotation.RotateVec(neighbors[i]);
matching = matching && MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos + neighbor), smoothQuery);
var neighbor = (Vector2i)rotation.RotateVec(neighbors[i]);
matching = matching && MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos + neighbor), smoothQuery);
}
if (matching)
@@ -316,27 +332,30 @@ namespace Content.Client.IconSmoothing
}
}
private void CalculateNewSpriteCardinal(MapGridComponent? grid, IconSmoothComponent smooth, Entity<SpriteComponent> sprite, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
private void CalculateNewSpriteCardinal(Entity<MapGridComponent>? gridEntity, IconSmoothComponent smooth, Entity<SpriteComponent> sprite, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
{
var dirs = CardinalConnectDirs.None;
if (grid == null)
if (gridEntity == null)
{
sprite.Comp.LayerSetState(0, $"{smooth.StateBase}{(int) dirs}");
sprite.Comp.LayerSetState(0, $"{smooth.StateBase}{(int)dirs}");
return;
}
var pos = grid.TileIndicesFor(xform.Coordinates);
if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.North)), smoothQuery))
var gridUid = gridEntity.Value.Owner;
var grid = gridEntity.Value.Comp;
var pos = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates);
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)), smoothQuery))
dirs |= CardinalConnectDirs.North;
if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.South)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)), smoothQuery))
dirs |= CardinalConnectDirs.South;
if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.East)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)), smoothQuery))
dirs |= CardinalConnectDirs.East;
if (MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.West)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)), smoothQuery))
dirs |= CardinalConnectDirs.West;
sprite.Comp.LayerSetState(0, $"{smooth.StateBase}{(int) dirs}");
sprite.Comp.LayerSetState(0, $"{smooth.StateBase}{(int)dirs}");
var directions = DirectionFlag.None;
@@ -367,11 +386,11 @@ namespace Content.Client.IconSmoothing
return false;
}
private void CalculateNewSpriteCorners(MapGridComponent? grid, IconSmoothComponent smooth, Entity<SpriteComponent> spriteEnt, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
private void CalculateNewSpriteCorners(Entity<MapGridComponent>? gridEntity, IconSmoothComponent smooth, Entity<SpriteComponent> spriteEnt, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
{
var (cornerNE, cornerNW, cornerSW, cornerSE) = grid == null
var (cornerNE, cornerNW, cornerSW, cornerSE) = gridEntity == null
? (CornerFill.None, CornerFill.None, CornerFill.None, CornerFill.None)
: CalculateCornerFill(grid, smooth, xform, smoothQuery);
: CalculateCornerFill(gridEntity.Value, smooth, xform, smoothQuery);
// TODO figure out a better way to set multiple sprite layers.
// This will currently re-calculate the sprite bounding box 4 times.
@@ -379,10 +398,10 @@ namespace Content.Client.IconSmoothing
// At the very least each event currently only queues a sprite for updating.
// Oh god sprite component is a mess.
var sprite = spriteEnt.Comp;
sprite.LayerSetState(CornerLayers.NE, $"{smooth.StateBase}{(int) cornerNE}");
sprite.LayerSetState(CornerLayers.SE, $"{smooth.StateBase}{(int) cornerSE}");
sprite.LayerSetState(CornerLayers.SW, $"{smooth.StateBase}{(int) cornerSW}");
sprite.LayerSetState(CornerLayers.NW, $"{smooth.StateBase}{(int) cornerNW}");
sprite.LayerSetState(CornerLayers.NE, $"{smooth.StateBase}{(int)cornerNE}");
sprite.LayerSetState(CornerLayers.SE, $"{smooth.StateBase}{(int)cornerSE}");
sprite.LayerSetState(CornerLayers.SW, $"{smooth.StateBase}{(int)cornerSW}");
sprite.LayerSetState(CornerLayers.NW, $"{smooth.StateBase}{(int)cornerNW}");
var directions = DirectionFlag.None;
@@ -401,17 +420,20 @@ namespace Content.Client.IconSmoothing
CalculateEdge(spriteEnt, directions, sprite);
}
private (CornerFill ne, CornerFill nw, CornerFill sw, CornerFill se) CalculateCornerFill(MapGridComponent grid, IconSmoothComponent smooth, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
private (CornerFill ne, CornerFill nw, CornerFill sw, CornerFill se) CalculateCornerFill(Entity<MapGridComponent> gridEntity, IconSmoothComponent smooth, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
{
var pos = grid.TileIndicesFor(xform.Coordinates);
var n = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.North)), smoothQuery);
var ne = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.NorthEast)), smoothQuery);
var e = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.East)), smoothQuery);
var se = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.SouthEast)), smoothQuery);
var s = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.South)), smoothQuery);
var sw = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.SouthWest)), smoothQuery);
var w = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.West)), smoothQuery);
var nw = MatchingEntity(smooth, grid.GetAnchoredEntitiesEnumerator(pos.Offset(Direction.NorthWest)), smoothQuery);
var gridUid = gridEntity.Owner;
var grid = gridEntity.Comp;
var pos = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates);
var n = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)), smoothQuery);
var ne = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.NorthEast)), smoothQuery);
var e = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)), smoothQuery);
var se = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.SouthEast)), smoothQuery);
var s = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)), smoothQuery);
var sw = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.SouthWest)), smoothQuery);
var w = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)), smoothQuery);
var nw = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.NorthWest)), smoothQuery);
// ReSharper disable InconsistentNaming
var cornerNE = CornerFill.None;

View File

@@ -74,6 +74,9 @@ namespace Content.Client.Input
human.AddFunction(ContentKeyFunctions.OpenBackpack);
human.AddFunction(ContentKeyFunctions.OpenBelt);
human.AddFunction(ContentKeyFunctions.MouseMiddle);
human.AddFunction(ContentKeyFunctions.RotateObjectClockwise);
human.AddFunction(ContentKeyFunctions.RotateObjectCounterclockwise);
human.AddFunction(ContentKeyFunctions.FlipObject);
human.AddFunction(ContentKeyFunctions.ArcadeUp);
human.AddFunction(ContentKeyFunctions.ArcadeDown);
human.AddFunction(ContentKeyFunctions.ArcadeLeft);

View File

@@ -73,6 +73,16 @@ public sealed partial class LatheMenu : DefaultWindow
MaterialsList.SetOwner(Entity);
}
protected override void Opened()
{
base.Opened();
if (_entityManager.TryGetComponent<LatheComponent>(Entity, out var latheComp))
{
AmountLineEdit.SetText(latheComp.DefaultProductionAmount.ToString());
}
}
/// <summary>
/// Populates the list of all the recipes
/// </summary>

View File

@@ -0,0 +1,27 @@
using Content.Shared.Conveyor;
using Content.Shared.Materials;
using Robust.Client.GameObjects;
namespace Content.Client.Materials;
public sealed class RecyclerVisualizerSystem : VisualizerSystem<RecyclerVisualsComponent>
{
protected override void OnAppearanceChange(EntityUid uid, RecyclerVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null || !args.Sprite.LayerMapTryGet(RecyclerVisualLayers.Main, out var layer))
return;
AppearanceSystem.TryGetData<ConveyorState>(uid, ConveyorVisuals.State, out var running);
AppearanceSystem.TryGetData<bool>(uid, RecyclerVisuals.Bloody, out var bloody);
AppearanceSystem.TryGetData<bool>(uid, RecyclerVisuals.Broken, out var broken);
var activityState = running == ConveyorState.Off ? 0 : 1;
if (broken) //breakage overrides activity
activityState = 2;
var bloodyKey = bloody ? component.BloodyKey : string.Empty;
var state = $"{component.BaseKey}{activityState}{bloodyKey}";
args.Sprite.LayerSetState(layer, state);
}
}

View File

@@ -0,0 +1,17 @@
namespace Content.Client.Materials;
[RegisterComponent]
public sealed partial class RecyclerVisualsComponent : Component
{
/// <summary>
/// Key appended to state string if bloody.
/// </summary>
[DataField]
public string BloodyKey = "bld";
/// <summary>
/// Base key for the visual state.
/// </summary>
[DataField]
public string BaseKey = "grinder-o";
}

View File

@@ -1,6 +1,7 @@
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Client.GameObjects;
using Robust.Shared.Utility;
namespace Content.Client.Nutrition.EntitySystems;
@@ -38,20 +39,24 @@ public sealed class ClientFoodSequenceSystem : SharedFoodSequenceSystem
if (state.Sprite is null)
continue;
counter++;
var keyCode = $"food-layer-{counter}";
start.Comp.RevealedLayers.Add(keyCode);
var index = sprite.LayerMapReserveBlank(keyCode);
sprite.LayerMapTryGet(start.Comp.TargetLayerMap, out var index);
//Set image
if (start.Comp.InverseLayers)
index++;
sprite.AddBlankLayer(index);
sprite.LayerMapSet(keyCode, index);
sprite.LayerSetSprite(index, state.Sprite);
//Offset the layer
var layerPos = start.Comp.StartPosition;
layerPos += start.Comp.Offset * counter;
layerPos += (start.Comp.Offset * counter) + state.LocalOffset;
sprite.LayerSetOffset(index, layerPos);
counter++;
}
}
}

View File

@@ -195,6 +195,9 @@ namespace Content.Client.Options.UI.Tabs
AddButton(ContentKeyFunctions.MovePulledObject);
AddButton(ContentKeyFunctions.ReleasePulledObject);
AddButton(ContentKeyFunctions.Point);
AddButton(ContentKeyFunctions.RotateObjectClockwise);
AddButton(ContentKeyFunctions.RotateObjectCounterclockwise);
AddButton(ContentKeyFunctions.FlipObject);
AddHeader("ui-options-header-ui");
AddButton(ContentKeyFunctions.FocusChat);

View File

@@ -33,7 +33,7 @@ public sealed partial class MiscTab : Control
var layoutEntries = new List<OptionDropDownCVar<string>.ValueOption>();
foreach (var layout in Enum.GetValues(typeof(ScreenType)))
{
layoutEntries.Add(new OptionDropDownCVar<string>.ValueOption(layout.ToString()!, layout.ToString()!));
layoutEntries.Add(new OptionDropDownCVar<string>.ValueOption(layout.ToString()!, Loc.GetString($"ui-options-hud-layout-{layout.ToString()!.ToLower()}")));
}
// Channel can be null in replays so.

View File

@@ -60,7 +60,7 @@ namespace Content.Client.Physics.Controllers
Physics.UpdateIsPredicted(entity.Owner);
Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
SetMoveInput((entity.Owner, inputMover), MoveButtons.None);
SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
}
private void OnRelayPlayerDetached(Entity<RelayInputMoverComponent> entity, ref LocalPlayerDetachedEvent args)
@@ -68,7 +68,7 @@ namespace Content.Client.Physics.Controllers
Physics.UpdateIsPredicted(entity.Owner);
Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
SetMoveInput((entity.Owner, inputMover), MoveButtons.None);
SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
}
private void OnPlayerAttached(Entity<InputMoverComponent> entity, ref LocalPlayerAttachedEvent args)

View File

@@ -74,8 +74,14 @@ public sealed class PopupOverlay : Overlay
return;
var matrix = args.ViewportControl.GetWorldToScreenMatrix();
var viewPos = new MapCoordinates(args.WorldAABB.Center, args.MapId);
var ourEntity = _playerMgr.LocalEntity;
var viewPos = new MapCoordinates(args.WorldAABB.Center, args.MapId);
var ourPos = args.WorldBounds.Center;
if (ourEntity != null)
{
viewPos = _transform.GetMapCoordinates(ourEntity.Value);
ourPos = viewPos.Position;
}
foreach (var popup in _popup.WorldLabels)
{
@@ -84,7 +90,7 @@ public sealed class PopupOverlay : Overlay
if (mapPos.MapId != args.MapId)
continue;
var distance = (mapPos.Position - args.WorldBounds.Center).Length();
var distance = (mapPos.Position - ourPos).Length();
// Should handle fade here too wyci.
if (!args.WorldBounds.Contains(mapPos.Position) || !_examine.InRangeUnOccluded(viewPos, mapPos, distance,

View File

@@ -11,6 +11,7 @@ namespace Content.Client.Radiation.Overlays;
public sealed class RadiationDebugOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly SharedMapSystem _mapSystem;
private readonly RadiationSystem _radiation;
private readonly Font _font;
@@ -21,6 +22,7 @@ public sealed class RadiationDebugOverlay : Overlay
{
IoCManager.InjectDependencies(this);
_radiation = _entityManager.System<RadiationSystem>();
_mapSystem = _entityManager.System<SharedMapSystem>();
var cache = IoCManager.Resolve<IResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
@@ -67,7 +69,7 @@ public sealed class RadiationDebugOverlay : Overlay
foreach (var (tile, rads) in blockers)
{
var worldPos = grid.GridTileToWorldPos(tile);
var worldPos = _mapSystem.GridTileToWorldPos(gridUid, grid, tile);
var screenCenter = args.ViewportControl.WorldToScreen(worldPos);
handle.DrawString(_font, screenCenter, rads.ToString("F2"), 1.5f, Color.White);
}
@@ -95,8 +97,8 @@ public sealed class RadiationDebugOverlay : Overlay
var offset = new Vector2(grid.TileSize, -grid.TileSize) * 0.25f;
foreach (var (tile, value) in resMap)
{
var localPos = grid.GridTileToLocal(tile).Position + offset;
var worldPos = grid.LocalToWorld(localPos);
var localPos = _mapSystem.GridTileToLocal(gridUid, grid, tile).Position + offset;
var worldPos = _mapSystem.LocalToWorld(gridUid, grid, localPos);
var screenCenter = args.ViewportControl.WorldToScreen(worldPos);
handle.DrawString(_font, screenCenter, value.ToString("F2"), color: Color.White);
}
@@ -129,7 +131,7 @@ public sealed class RadiationDebugOverlay : Overlay
if (!_entityManager.TryGetComponent<MapGridComponent>(gridUid, out var grid))
continue;
var (destTile, _) = blockers.Last();
var destWorld = grid.GridTileToWorldPos(destTile);
var destWorld = _mapSystem.GridTileToWorldPos(gridUid, grid, destTile);
handle.DrawLine(ray.Source, destWorld, Color.Red);
}
}

View File

@@ -1,4 +1,5 @@
using System.Linq;
using Content.Client.Message;
using Content.Shared.Salvage;
using Content.Shared.Salvage.Magnet;
using Robust.Client.UserInterface;
@@ -63,7 +64,7 @@ public sealed class SalvageMagnetBoundUserInterface : BoundUserInterface
switch (offer)
{
case AsteroidOffering asteroid:
option.Title = Loc.GetString($"dungeon-config-proto-{asteroid.DungeonConfig.ID}");
option.Title = Loc.GetString($"dungeon-config-proto-{asteroid.Id}");
var layerKeys = asteroid.MarkerLayers.Keys.ToList();
layerKeys.Sort();
@@ -99,7 +100,31 @@ public sealed class SalvageMagnetBoundUserInterface : BoundUserInterface
break;
case SalvageOffering salvage:
option.Title = Loc.GetString($"salvage-map-proto-{salvage.SalvageMap.ID}");
option.Title = Loc.GetString($"salvage-map-wreck");
var salvContainer = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
};
var sizeLabel = new Label
{
Text = Loc.GetString("salvage-map-wreck-desc-size"),
HorizontalAlignment = Control.HAlignment.Left,
};
var sizeValueLabel = new RichTextLabel
{
HorizontalAlignment = Control.HAlignment.Right,
HorizontalExpand = true,
};
sizeValueLabel.SetMarkup(Loc.GetString(salvage.SalvageMap.SizeString));
salvContainer.AddChild(sizeLabel);
salvContainer.AddChild(sizeValueLabel);
option.AddContent(salvContainer);
break;
default:
throw new ArgumentOutOfRangeException();

View File

@@ -1,10 +1,7 @@
using Content.Server.GameTicking;
using Content.Server.GameTicking.Commands;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.CCVar;
using Content.Shared.GameTicking.Components;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;
@@ -27,8 +24,12 @@ namespace Content.IntegrationTests.Tests.GameRules
var sGameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
var sGameTiming = server.ResolveDependency<IGameTiming>();
sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity);
Assert.That(entityManager.TryGetComponent<MaxTimeRestartRuleComponent>(ruleEntity, out var maxTime));
MaxTimeRestartRuleComponent maxTime = null;
await server.WaitPost(() =>
{
sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity);
Assert.That(entityManager.TryGetComponent<MaxTimeRestartRuleComponent>(ruleEntity, out maxTime));
});
Assert.That(server.EntMan.Count<GameRuleComponent>(), Is.EqualTo(1));
Assert.That(server.EntMan.Count<ActiveGameRuleComponent>(), Is.EqualTo(1));

View File

@@ -77,16 +77,17 @@ public sealed class TraitorRuleTest
await pair.SetAntagPreference(TraitorAntagRoleName, true);
// Add the game rule
var gameRuleEnt = ticker.AddGameRule(TraitorGameRuleProtoId);
Assert.That(entMan.TryGetComponent<TraitorRuleComponent>(gameRuleEnt, out var traitorRule));
// Ready up
ticker.ToggleReadyAll(true);
Assert.That(ticker.PlayerGameStatuses.Values.All(x => x == PlayerGameStatus.ReadyToPlay));
// Start the round
TraitorRuleComponent traitorRule = null;
await server.WaitPost(() =>
{
var gameRuleEnt = ticker.AddGameRule(TraitorGameRuleProtoId);
Assert.That(entMan.TryGetComponent<TraitorRuleComponent>(gameRuleEnt, out traitorRule));
// Ready up
ticker.ToggleReadyAll(true);
Assert.That(ticker.PlayerGameStatuses.Values.All(x => x == PlayerGameStatus.ReadyToPlay));
// Start the round
ticker.StartRound();
// Force traitor mode to start (skip the delay)
ticker.StartGameRule(gameRuleEnt);

View File

@@ -154,21 +154,6 @@ public sealed class ActionOnInteractSystem : EntitySystem
args.Handled = true;
}
private bool ValidAction(BaseActionComponent action, bool canReach = true)
{
if (!action.Enabled)
return false;
if (action.Charges.HasValue && action.Charges <= 0)
return false;
var curTime = _timing.CurTime;
if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime)
return false;
return canReach || action is BaseTargetActionComponent { CheckCanAccess: false };
}
private List<(EntityUid Id, T Comp)> GetValidActions<T>(List<EntityUid>? actions, bool canReach = true) where T : BaseActionComponent
{
var valid = new List<(EntityUid Id, T Comp)>();
@@ -180,7 +165,7 @@ public sealed class ActionOnInteractSystem : EntitySystem
{
if (!_actions.TryGetActionData(id, out var baseAction) ||
baseAction as T is not { } action ||
!ValidAction(action, canReach))
!_actions.ValidAction(action, canReach))
{
continue;
}

View File

@@ -31,426 +31,425 @@ using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.Server.Administration.Systems
namespace Content.Server.Administration.Systems;
public sealed class AdminSystem : EntitySystem
{
public sealed class AdminSystem : EntitySystem
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly HandsSystem _hands = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly MindSystem _minds = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PhysicsSystem _physics = default!;
[Dependency] private readonly PlayTimeTrackingManager _playTime = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
[Dependency] private readonly TransformSystem _transform = default!;
private readonly Dictionary<NetUserId, PlayerInfo> _playerList = new();
/// <summary>
/// Set of players that have participated in this round.
/// </summary>
public IReadOnlySet<NetUserId> RoundActivePlayers => _roundActivePlayers;
private readonly HashSet<NetUserId> _roundActivePlayers = new();
public readonly PanicBunkerStatus PanicBunker = new();
public readonly BabyJailStatus BabyJail = new();
public override void Initialize()
{
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly HandsSystem _hands = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly MindSystem _minds = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PhysicsSystem _physics = default!;
[Dependency] private readonly PlayTimeTrackingManager _playTime = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
[Dependency] private readonly TransformSystem _transform = default!;
base.Initialize();
private readonly Dictionary<NetUserId, PlayerInfo> _playerList = new();
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_adminManager.OnPermsChanged += OnAdminPermsChanged;
_playTime.SessionPlayTimeUpdated += OnSessionPlayTimeUpdated;
/// <summary>
/// Set of players that have participated in this round.
/// </summary>
public IReadOnlySet<NetUserId> RoundActivePlayers => _roundActivePlayers;
// Panic Bunker Settings
Subs.CVar(_config, CCVars.PanicBunkerEnabled, OnPanicBunkerChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerDisableWithAdmins, OnPanicBunkerDisableWithAdminsChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerEnableWithoutAdmins, OnPanicBunkerEnableWithoutAdminsChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerCountDeadminnedAdmins, OnPanicBunkerCountDeadminnedAdminsChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerShowReason, OnPanicBunkerShowReasonChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerMinOverallMinutes, OnPanicBunkerMinOverallMinutesChanged, true);
private readonly HashSet<NetUserId> _roundActivePlayers = new();
public readonly PanicBunkerStatus PanicBunker = new();
public readonly BabyJailStatus BabyJail = new();
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
public override void Initialize()
// Baby Jail Settings
Subs.CVar(_config, CCVars.BabyJailEnabled, OnBabyJailChanged, true);
Subs.CVar(_config, CCVars.BabyJailShowReason, OnBabyJailShowReasonChanged, true);
Subs.CVar(_config, CCVars.BabyJailMaxAccountAge, OnBabyJailMaxAccountAgeChanged, true);
Subs.CVar(_config, CCVars.BabyJailMaxOverallMinutes, OnBabyJailMaxOverallMinutesChanged, true);
SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<RoleAddedEvent>(OnRoleEvent);
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleEvent);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
}
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
{
_roundActivePlayers.Clear();
foreach (var (id, data) in _playerList)
{
base.Initialize();
if (!data.ActiveThisRound)
continue;
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_adminManager.OnPermsChanged += OnAdminPermsChanged;
_playTime.SessionPlayTimeUpdated += OnSessionPlayTimeUpdated;
// Panic Bunker Settings
Subs.CVar(_config, CCVars.PanicBunkerEnabled, OnPanicBunkerChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerDisableWithAdmins, OnPanicBunkerDisableWithAdminsChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerEnableWithoutAdmins, OnPanicBunkerEnableWithoutAdminsChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerCountDeadminnedAdmins, OnPanicBunkerCountDeadminnedAdminsChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerShowReason, OnPanicBunkerShowReasonChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true);
Subs.CVar(_config, CCVars.PanicBunkerMinOverallMinutes, OnPanicBunkerMinOverallMinutesChanged, true);
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
// Baby Jail Settings
Subs.CVar(_config, CCVars.BabyJailEnabled, OnBabyJailChanged, true);
Subs.CVar(_config, CCVars.BabyJailShowReason, OnBabyJailShowReasonChanged, true);
Subs.CVar(_config, CCVars.BabyJailMaxAccountAge, OnBabyJailMaxAccountAgeChanged, true);
Subs.CVar(_config, CCVars.BabyJailMaxOverallMinutes, OnBabyJailMaxOverallMinutesChanged, true);
SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<RoleAddedEvent>(OnRoleEvent);
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleEvent);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
}
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
{
_roundActivePlayers.Clear();
foreach (var (id, data) in _playerList)
{
if (!data.ActiveThisRound)
continue;
if (!_playerManager.TryGetPlayerData(id, out var playerData))
return;
_playerManager.TryGetSessionById(id, out var session);
_playerList[id] = GetPlayerInfo(playerData, session);
}
var updateEv = new FullPlayerListEvent() { PlayersInfo = _playerList.Values.ToList() };
foreach (var admin in _adminManager.ActiveAdmins)
{
RaiseNetworkEvent(updateEv, admin.Channel);
}
}
public void UpdatePlayerList(ICommonSession player)
{
_playerList[player.UserId] = GetPlayerInfo(player.Data, player);
var playerInfoChangedEvent = new PlayerInfoChangedEvent
{
PlayerInfo = _playerList[player.UserId]
};
foreach (var admin in _adminManager.ActiveAdmins)
{
RaiseNetworkEvent(playerInfoChangedEvent, admin.Channel);
}
}
public PlayerInfo? GetCachedPlayerInfo(NetUserId? netUserId)
{
if (netUserId == null)
return null;
_playerList.TryGetValue(netUserId.Value, out var value);
return value ?? null;
}
private void OnIdentityChanged(ref IdentityChangedEvent ev)
{
if (!TryComp<ActorComponent>(ev.CharacterEntity, out var actor))
if (!_playerManager.TryGetPlayerData(id, out var playerData))
return;
UpdatePlayerList(actor.PlayerSession);
_playerManager.TryGetSessionById(id, out var session);
_playerList[id] = GetPlayerInfo(playerData, session);
}
private void OnRoleEvent(RoleEvent ev)
var updateEv = new FullPlayerListEvent() { PlayersInfo = _playerList.Values.ToList() };
foreach (var admin in _adminManager.ActiveAdmins)
{
var session = _minds.GetSession(ev.Mind);
if (!ev.Antagonist || session == null)
return;
UpdatePlayerList(session);
}
private void OnAdminPermsChanged(AdminPermsChangedEventArgs obj)
{
UpdatePanicBunker();
if (!obj.IsAdmin)
{
RaiseNetworkEvent(new FullPlayerListEvent(), obj.Player.Channel);
return;
}
SendFullPlayerList(obj.Player);
}
private void OnPlayerDetached(PlayerDetachedEvent ev)
{
// If disconnected then the player won't have a connected entity to get character name from.
// The disconnected state gets sent by OnPlayerStatusChanged.
if (ev.Player.Status == SessionStatus.Disconnected)
return;
UpdatePlayerList(ev.Player);
}
private void OnPlayerAttached(PlayerAttachedEvent ev)
{
if (ev.Player.Status == SessionStatus.Disconnected)
return;
_roundActivePlayers.Add(ev.Player.UserId);
UpdatePlayerList(ev.Player);
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
_adminManager.OnPermsChanged -= OnAdminPermsChanged;
_playTime.SessionPlayTimeUpdated -= OnSessionPlayTimeUpdated;
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
UpdatePlayerList(e.Session);
UpdatePanicBunker();
}
private void SendFullPlayerList(ICommonSession playerSession)
{
var ev = new FullPlayerListEvent();
ev.PlayersInfo = _playerList.Values.ToList();
RaiseNetworkEvent(ev, playerSession.Channel);
}
private PlayerInfo GetPlayerInfo(SessionData data, ICommonSession? session)
{
var name = data.UserName;
var entityName = string.Empty;
var identityName = string.Empty;
if (session?.AttachedEntity != null)
{
entityName = EntityManager.GetComponent<MetaDataComponent>(session.AttachedEntity.Value).EntityName;
identityName = Identity.Name(session.AttachedEntity.Value, EntityManager);
}
var antag = false;
var startingRole = string.Empty;
if (_minds.TryGetMind(session, out var mindId, out _))
{
antag = _role.MindIsAntagonist(mindId);
startingRole = _jobs.MindTryGetJobName(mindId);
}
var connected = session != null && session.Status is SessionStatus.Connected or SessionStatus.InGame;
TimeSpan? overallPlaytime = null;
if (session != null &&
_playTime.TryGetTrackerTimes(session, out var playTimes) &&
playTimes.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out var playTime))
{
overallPlaytime = playTime;
}
return new PlayerInfo(name, entityName, identityName, startingRole, antag, GetNetEntity(session?.AttachedEntity), data.UserId,
connected, _roundActivePlayers.Contains(data.UserId), overallPlaytime);
}
private void OnPanicBunkerChanged(bool enabled)
{
PanicBunker.Enabled = enabled;
_chat.SendAdminAlert(Loc.GetString(enabled
? "admin-ui-panic-bunker-enabled-admin-alert"
: "admin-ui-panic-bunker-disabled-admin-alert"
));
SendPanicBunkerStatusAll();
}
private void OnBabyJailChanged(bool enabled)
{
BabyJail.Enabled = enabled;
_chat.SendAdminAlert(Loc.GetString(enabled
? "admin-ui-baby-jail-enabled-admin-alert"
: "admin-ui-baby-jail-disabled-admin-alert"
));
SendBabyJailStatusAll();
}
private void OnPanicBunkerDisableWithAdminsChanged(bool enabled)
{
PanicBunker.DisableWithAdmins = enabled;
UpdatePanicBunker();
}
private void OnPanicBunkerEnableWithoutAdminsChanged(bool enabled)
{
PanicBunker.EnableWithoutAdmins = enabled;
UpdatePanicBunker();
}
private void OnPanicBunkerCountDeadminnedAdminsChanged(bool enabled)
{
PanicBunker.CountDeadminnedAdmins = enabled;
UpdatePanicBunker();
}
private void OnPanicBunkerShowReasonChanged(bool enabled)
{
PanicBunker.ShowReason = enabled;
SendPanicBunkerStatusAll();
}
private void OnBabyJailShowReasonChanged(bool enabled)
{
BabyJail.ShowReason = enabled;
SendBabyJailStatusAll();
}
private void OnPanicBunkerMinAccountAgeChanged(int minutes)
{
PanicBunker.MinAccountAgeMinutes = minutes;
SendPanicBunkerStatusAll();
}
private void OnBabyJailMaxAccountAgeChanged(int minutes)
{
BabyJail.MaxAccountAgeMinutes = minutes;
SendBabyJailStatusAll();
}
private void OnPanicBunkerMinOverallMinutesChanged(int minutes)
{
PanicBunker.MinOverallMinutes = minutes;
SendPanicBunkerStatusAll();
}
private void OnBabyJailMaxOverallMinutesChanged(int minutes)
{
BabyJail.MaxOverallMinutes = minutes;
SendBabyJailStatusAll();
}
private void UpdatePanicBunker()
{
var admins = PanicBunker.CountDeadminnedAdmins
? _adminManager.AllAdmins
: _adminManager.ActiveAdmins;
var hasAdmins = admins.Any();
// TODO Fix order dependent Cvars
// Please for the sake of my sanity don't make cvars & order dependent.
// Just make a bool field on the system instead of having some cvars automatically modify other cvars.
//
// I.e., this:
// /sudo cvar game.panic_bunker.enabled true
// /sudo cvar game.panic_bunker.disable_with_admins true
// and this:
// /sudo cvar game.panic_bunker.disable_with_admins true
// /sudo cvar game.panic_bunker.enabled true
//
// should have the same effect, but currently setting the disable_with_admins can modify enabled.
if (hasAdmins && PanicBunker.DisableWithAdmins)
{
_config.SetCVar(CCVars.PanicBunkerEnabled, false);
}
else if (!hasAdmins && PanicBunker.EnableWithoutAdmins)
{
_config.SetCVar(CCVars.PanicBunkerEnabled, true);
}
SendPanicBunkerStatusAll();
}
private void SendPanicBunkerStatusAll()
{
var ev = new PanicBunkerChangedEvent(PanicBunker);
foreach (var admin in _adminManager.AllAdmins)
{
RaiseNetworkEvent(ev, admin);
}
}
private void SendBabyJailStatusAll()
{
var ev = new BabyJailChangedEvent(BabyJail);
foreach (var admin in _adminManager.AllAdmins)
{
RaiseNetworkEvent(ev, admin);
}
}
/// <summary>
/// Erases a player from the round.
/// This removes them and any trace of them from the round, deleting their
/// chat messages and showing a popup to other players.
/// Their items are dropped on the ground.
/// </summary>
public void Erase(ICommonSession player)
{
var entity = player.AttachedEntity;
_chat.DeleteMessagesBy(player);
if (entity != null && !TerminatingOrDeleted(entity.Value))
{
if (TryComp(entity.Value, out TransformComponent? transform))
{
var coordinates = _transform.GetMoverCoordinates(entity.Value, transform);
var name = Identity.Entity(entity.Value, EntityManager);
_popup.PopupCoordinates(Loc.GetString("admin-erase-popup", ("user", name)), coordinates, PopupType.LargeCaution);
var filter = Filter.Pvs(coordinates, 1, EntityManager, _playerManager);
var audioParams = new AudioParams().WithVolume(3);
_audio.PlayStatic("/Audio/Effects/pop_high.ogg", filter, coordinates, true, audioParams);
}
foreach (var item in _inventory.GetHandOrInventoryEntities(entity.Value))
{
if (TryComp(item, out PdaComponent? pda) &&
TryComp(pda.ContainedId, out StationRecordKeyStorageComponent? keyStorage) &&
keyStorage.Key is { } key &&
_stationRecords.TryGetRecord(key, out GeneralStationRecord? record))
{
if (TryComp(entity, out DnaComponent? dna) &&
dna.DNA != record.DNA)
{
continue;
}
if (TryComp(entity, out FingerprintComponent? fingerPrint) &&
fingerPrint.Fingerprint != record.Fingerprint)
{
continue;
}
_stationRecords.RemoveRecord(key);
Del(item);
}
}
if (_inventory.TryGetContainerSlotEnumerator(entity.Value, out var enumerator))
{
while (enumerator.NextItem(out var item, out var slot))
{
if (_inventory.TryUnequip(entity.Value, entity.Value, slot.Name, true, true))
_physics.ApplyAngularImpulse(item, ThrowingSystem.ThrowAngularImpulse);
}
}
if (TryComp(entity.Value, out HandsComponent? hands))
{
foreach (var hand in _hands.EnumerateHands(entity.Value, hands))
{
_hands.TryDrop(entity.Value, hand, checkActionBlocker: false, doDropInteraction: false, handsComp: hands);
}
}
}
_minds.WipeMind(player);
QueueDel(entity);
_gameTicker.SpawnObserver(player);
}
private void OnSessionPlayTimeUpdated(ICommonSession session)
{
UpdatePlayerList(session);
RaiseNetworkEvent(updateEv, admin.Channel);
}
}
public void UpdatePlayerList(ICommonSession player)
{
_playerList[player.UserId] = GetPlayerInfo(player.Data, player);
var playerInfoChangedEvent = new PlayerInfoChangedEvent
{
PlayerInfo = _playerList[player.UserId]
};
foreach (var admin in _adminManager.ActiveAdmins)
{
RaiseNetworkEvent(playerInfoChangedEvent, admin.Channel);
}
}
public PlayerInfo? GetCachedPlayerInfo(NetUserId? netUserId)
{
if (netUserId == null)
return null;
_playerList.TryGetValue(netUserId.Value, out var value);
return value ?? null;
}
private void OnIdentityChanged(ref IdentityChangedEvent ev)
{
if (!TryComp<ActorComponent>(ev.CharacterEntity, out var actor))
return;
UpdatePlayerList(actor.PlayerSession);
}
private void OnRoleEvent(RoleEvent ev)
{
var session = _minds.GetSession(ev.Mind);
if (!ev.Antagonist || session == null)
return;
UpdatePlayerList(session);
}
private void OnAdminPermsChanged(AdminPermsChangedEventArgs obj)
{
UpdatePanicBunker();
if (!obj.IsAdmin)
{
RaiseNetworkEvent(new FullPlayerListEvent(), obj.Player.Channel);
return;
}
SendFullPlayerList(obj.Player);
}
private void OnPlayerDetached(PlayerDetachedEvent ev)
{
// If disconnected then the player won't have a connected entity to get character name from.
// The disconnected state gets sent by OnPlayerStatusChanged.
if (ev.Player.Status == SessionStatus.Disconnected)
return;
UpdatePlayerList(ev.Player);
}
private void OnPlayerAttached(PlayerAttachedEvent ev)
{
if (ev.Player.Status == SessionStatus.Disconnected)
return;
_roundActivePlayers.Add(ev.Player.UserId);
UpdatePlayerList(ev.Player);
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
_adminManager.OnPermsChanged -= OnAdminPermsChanged;
_playTime.SessionPlayTimeUpdated -= OnSessionPlayTimeUpdated;
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
UpdatePlayerList(e.Session);
UpdatePanicBunker();
}
private void SendFullPlayerList(ICommonSession playerSession)
{
var ev = new FullPlayerListEvent();
ev.PlayersInfo = _playerList.Values.ToList();
RaiseNetworkEvent(ev, playerSession.Channel);
}
private PlayerInfo GetPlayerInfo(SessionData data, ICommonSession? session)
{
var name = data.UserName;
var entityName = string.Empty;
var identityName = string.Empty;
if (session?.AttachedEntity != null)
{
entityName = EntityManager.GetComponent<MetaDataComponent>(session.AttachedEntity.Value).EntityName;
identityName = Identity.Name(session.AttachedEntity.Value, EntityManager);
}
var antag = false;
var startingRole = string.Empty;
if (_minds.TryGetMind(session, out var mindId, out _))
{
antag = _role.MindIsAntagonist(mindId);
startingRole = _jobs.MindTryGetJobName(mindId);
}
var connected = session != null && session.Status is SessionStatus.Connected or SessionStatus.InGame;
TimeSpan? overallPlaytime = null;
if (session != null &&
_playTime.TryGetTrackerTimes(session, out var playTimes) &&
playTimes.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out var playTime))
{
overallPlaytime = playTime;
}
return new PlayerInfo(name, entityName, identityName, startingRole, antag, GetNetEntity(session?.AttachedEntity), data.UserId,
connected, _roundActivePlayers.Contains(data.UserId), overallPlaytime);
}
private void OnPanicBunkerChanged(bool enabled)
{
PanicBunker.Enabled = enabled;
_chat.SendAdminAlert(Loc.GetString(enabled
? "admin-ui-panic-bunker-enabled-admin-alert"
: "admin-ui-panic-bunker-disabled-admin-alert"
));
SendPanicBunkerStatusAll();
}
private void OnBabyJailChanged(bool enabled)
{
BabyJail.Enabled = enabled;
_chat.SendAdminAlert(Loc.GetString(enabled
? "admin-ui-baby-jail-enabled-admin-alert"
: "admin-ui-baby-jail-disabled-admin-alert"
));
SendBabyJailStatusAll();
}
private void OnPanicBunkerDisableWithAdminsChanged(bool enabled)
{
PanicBunker.DisableWithAdmins = enabled;
UpdatePanicBunker();
}
private void OnPanicBunkerEnableWithoutAdminsChanged(bool enabled)
{
PanicBunker.EnableWithoutAdmins = enabled;
UpdatePanicBunker();
}
private void OnPanicBunkerCountDeadminnedAdminsChanged(bool enabled)
{
PanicBunker.CountDeadminnedAdmins = enabled;
UpdatePanicBunker();
}
private void OnPanicBunkerShowReasonChanged(bool enabled)
{
PanicBunker.ShowReason = enabled;
SendPanicBunkerStatusAll();
}
private void OnBabyJailShowReasonChanged(bool enabled)
{
BabyJail.ShowReason = enabled;
SendBabyJailStatusAll();
}
private void OnPanicBunkerMinAccountAgeChanged(int minutes)
{
PanicBunker.MinAccountAgeMinutes = minutes;
SendPanicBunkerStatusAll();
}
private void OnBabyJailMaxAccountAgeChanged(int minutes)
{
BabyJail.MaxAccountAgeMinutes = minutes;
SendBabyJailStatusAll();
}
private void OnPanicBunkerMinOverallMinutesChanged(int minutes)
{
PanicBunker.MinOverallMinutes = minutes;
SendPanicBunkerStatusAll();
}
private void OnBabyJailMaxOverallMinutesChanged(int minutes)
{
BabyJail.MaxOverallMinutes = minutes;
SendBabyJailStatusAll();
}
private void UpdatePanicBunker()
{
var admins = PanicBunker.CountDeadminnedAdmins
? _adminManager.AllAdmins
: _adminManager.ActiveAdmins;
var hasAdmins = admins.Any();
// TODO Fix order dependent Cvars
// Please for the sake of my sanity don't make cvars & order dependent.
// Just make a bool field on the system instead of having some cvars automatically modify other cvars.
//
// I.e., this:
// /sudo cvar game.panic_bunker.enabled true
// /sudo cvar game.panic_bunker.disable_with_admins true
// and this:
// /sudo cvar game.panic_bunker.disable_with_admins true
// /sudo cvar game.panic_bunker.enabled true
//
// should have the same effect, but currently setting the disable_with_admins can modify enabled.
if (hasAdmins && PanicBunker.DisableWithAdmins)
{
_config.SetCVar(CCVars.PanicBunkerEnabled, false);
}
else if (!hasAdmins && PanicBunker.EnableWithoutAdmins)
{
_config.SetCVar(CCVars.PanicBunkerEnabled, true);
}
SendPanicBunkerStatusAll();
}
private void SendPanicBunkerStatusAll()
{
var ev = new PanicBunkerChangedEvent(PanicBunker);
foreach (var admin in _adminManager.AllAdmins)
{
RaiseNetworkEvent(ev, admin);
}
}
private void SendBabyJailStatusAll()
{
var ev = new BabyJailChangedEvent(BabyJail);
foreach (var admin in _adminManager.AllAdmins)
{
RaiseNetworkEvent(ev, admin);
}
}
/// <summary>
/// Erases a player from the round.
/// This removes them and any trace of them from the round, deleting their
/// chat messages and showing a popup to other players.
/// Their items are dropped on the ground.
/// </summary>
public void Erase(ICommonSession player)
{
var entity = player.AttachedEntity;
_chat.DeleteMessagesBy(player);
if (entity != null && !TerminatingOrDeleted(entity.Value))
{
if (TryComp(entity.Value, out TransformComponent? transform))
{
var coordinates = _transform.GetMoverCoordinates(entity.Value, transform);
var name = Identity.Entity(entity.Value, EntityManager);
_popup.PopupCoordinates(Loc.GetString("admin-erase-popup", ("user", name)), coordinates, PopupType.LargeCaution);
var filter = Filter.Pvs(coordinates, 1, EntityManager, _playerManager);
var audioParams = new AudioParams().WithVolume(3);
_audio.PlayStatic("/Audio/Effects/pop_high.ogg", filter, coordinates, true, audioParams);
}
foreach (var item in _inventory.GetHandOrInventoryEntities(entity.Value))
{
if (TryComp(item, out PdaComponent? pda) &&
TryComp(pda.ContainedId, out StationRecordKeyStorageComponent? keyStorage) &&
keyStorage.Key is { } key &&
_stationRecords.TryGetRecord(key, out GeneralStationRecord? record))
{
if (TryComp(entity, out DnaComponent? dna) &&
dna.DNA != record.DNA)
{
continue;
}
if (TryComp(entity, out FingerprintComponent? fingerPrint) &&
fingerPrint.Fingerprint != record.Fingerprint)
{
continue;
}
_stationRecords.RemoveRecord(key);
Del(item);
}
}
if (_inventory.TryGetContainerSlotEnumerator(entity.Value, out var enumerator))
{
while (enumerator.NextItem(out var item, out var slot))
{
if (_inventory.TryUnequip(entity.Value, entity.Value, slot.Name, true, true))
_physics.ApplyAngularImpulse(item, ThrowingSystem.ThrowAngularImpulse);
}
}
if (TryComp(entity.Value, out HandsComponent? hands))
{
foreach (var hand in _hands.EnumerateHands(entity.Value, hands))
{
_hands.TryDrop(entity.Value, hand, checkActionBlocker: false, doDropInteraction: false, handsComp: hands);
}
}
}
_minds.WipeMind(player);
QueueDel(entity);
_gameTicker.SpawnObserver(player);
}
private void OnSessionPlayTimeUpdated(ICommonSession session)
{
UpdatePlayerList(session);
}
}

View File

@@ -736,11 +736,11 @@ public sealed partial class AdminVerbSystem
}
}
private void RefillEquippedTanks(EntityUid target, Gas plasma)
private void RefillEquippedTanks(EntityUid target, Gas gasType)
{
foreach (var held in _inventorySystem.GetHandOrInventoryEntities(target))
{
RefillGasTank(held, Gas.Plasma);
RefillGasTank(held, gasType);
}
}

View File

@@ -6,7 +6,6 @@ using Content.Shared.Anomaly.Components;
using Content.Shared.Database;
using Content.Shared.Mobs.Components;
using Content.Shared.Teleportation.Components;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Collections;
using Robust.Shared.Random;
@@ -34,8 +33,11 @@ public sealed class BluespaceAnomalySystem : EntitySystem
var xformQuery = GetEntityQuery<TransformComponent>();
var xform = xformQuery.GetComponent(uid);
var range = component.MaxShuffleRadius * args.Severity * args.PowerModifier;
// get a list of all entities in range with the MobStateComponent
// we filter out those inside a container
// otherwise borg brains get removed from their body, or PAIs from a PDA
var mobs = new HashSet<Entity<MobStateComponent>>();
_lookup.GetEntitiesInRange(xform.Coordinates, range, mobs);
_lookup.GetEntitiesInRange(xform.Coordinates, range, mobs, flags: LookupFlags.Uncontained);
var allEnts = new ValueList<EntityUid>(mobs.Select(m => m.Owner)) { uid };
var coords = new ValueList<Vector2>();
foreach (var ent in allEnts)
@@ -59,7 +61,7 @@ public sealed class BluespaceAnomalySystem : EntitySystem
var radius = component.SupercriticalTeleportRadius * args.PowerModifier;
var gridBounds = new Box2(mapPos - new Vector2(radius, radius), mapPos + new Vector2(radius, radius));
var mobs = new HashSet<Entity<MobStateComponent>>();
_lookup.GetEntitiesInRange(xform.Coordinates, component.MaxShuffleRadius, mobs);
_lookup.GetEntitiesInRange(xform.Coordinates, component.MaxShuffleRadius, mobs, flags: LookupFlags.Uncontained);
foreach (var comp in mobs)
{
var ent = comp.Owner;

View File

@@ -1,5 +1,4 @@
using System.Linq;
using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
@@ -10,274 +9,266 @@ using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
namespace Content.Server.Atmos.EntitySystems
namespace Content.Server.Atmos.EntitySystems;
[UsedImplicitly]
public sealed class GasAnalyzerSystem : EntitySystem
{
[UsedImplicitly]
public sealed class GasAnalyzerSystem : EntitySystem
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly AtmosphereSystem _atmo = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
/// <summary>
/// Minimum moles of a gas to be sent to the client.
/// </summary>
private const float UIMinMoles = 0.01f;
public override void Initialize()
{
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly AtmosphereSystem _atmo = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly TransformSystem _transform = default!;
base.Initialize();
/// <summary>
/// Minimum moles of a gas to be sent to the client.
/// </summary>
private const float UIMinMoles = 0.01f;
SubscribeLocalEvent<GasAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<GasAnalyzerComponent, GasAnalyzerDisableMessage>(OnDisabledMessage);
SubscribeLocalEvent<GasAnalyzerComponent, DroppedEvent>(OnDropped);
SubscribeLocalEvent<GasAnalyzerComponent, UseInHandEvent>(OnUseInHand);
}
public override void Initialize()
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveGasAnalyzerComponent>();
while (query.MoveNext(out var uid, out var analyzer))
{
base.Initialize();
// Don't update every tick
analyzer.AccumulatedFrametime += frameTime;
SubscribeLocalEvent<GasAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<GasAnalyzerComponent, GasAnalyzerDisableMessage>(OnDisabledMessage);
SubscribeLocalEvent<GasAnalyzerComponent, DroppedEvent>(OnDropped);
SubscribeLocalEvent<GasAnalyzerComponent, UseInHandEvent>(OnUseInHand);
if (analyzer.AccumulatedFrametime < analyzer.UpdateInterval)
continue;
analyzer.AccumulatedFrametime -= analyzer.UpdateInterval;
if (!UpdateAnalyzer(uid))
RemCompDeferred<ActiveGasAnalyzerComponent>(uid);
}
}
public override void Update(float frameTime)
/// <summary>
/// Activates the analyzer when used in the world, scanning the target entity (if it exists) and the tile the analyzer is in
/// </summary>
private void OnAfterInteract(Entity<GasAnalyzerComponent> entity, ref AfterInteractEvent args)
{
var target = args.Target;
if (target != null && !_interactionSystem.InRangeUnobstructed((args.User, null), (target.Value, null)))
{
var query = EntityQueryEnumerator<ActiveGasAnalyzerComponent>();
while (query.MoveNext(out var uid, out var analyzer))
target = null; // if the target is out of reach, invalidate it
}
// always run the analyzer, regardless of weather or not there is a target
// since we can always show the local environment.
ActivateAnalyzer(entity, args.User, target);
args.Handled = true;
}
/// <summary>
/// Activates the analyzer with no target, so it only scans the tile the user was on when activated
/// </summary>
private void OnUseInHand(Entity<GasAnalyzerComponent> entity, ref UseInHandEvent args)
{
if (!entity.Comp.Enabled)
{
ActivateAnalyzer(entity, args.User);
}
else
{
DisableAnalyzer(entity, args.User);
}
args.Handled = true;
}
/// <summary>
/// Handles analyzer activation logic
/// </summary>
private void ActivateAnalyzer(Entity<GasAnalyzerComponent> entity, EntityUid user, EntityUid? target = null)
{
if (!_userInterface.TryOpenUi(entity.Owner, GasAnalyzerUiKey.Key, user))
return;
entity.Comp.Target = target;
entity.Comp.User = user;
entity.Comp.Enabled = true;
Dirty(entity);
_appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled);
EnsureComp<ActiveGasAnalyzerComponent>(entity.Owner);
UpdateAnalyzer(entity.Owner, entity.Comp);
}
/// <summary>
/// Close the UI, turn the analyzer off, and don't update when it's dropped
/// </summary>
private void OnDropped(Entity<GasAnalyzerComponent> entity, ref DroppedEvent args)
{
if (args.User is var userId && entity.Comp.Enabled)
_popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId);
DisableAnalyzer(entity, args.User);
}
/// <summary>
/// Closes the UI, sets the icon to off, and removes it from the update list
/// </summary>
private void DisableAnalyzer(Entity<GasAnalyzerComponent> entity, EntityUid? user = null)
{
_userInterface.CloseUi(entity.Owner, GasAnalyzerUiKey.Key, user);
entity.Comp.Enabled = false;
Dirty(entity);
_appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled);
RemCompDeferred<ActiveGasAnalyzerComponent>(entity.Owner);
}
/// <summary>
/// Disables the analyzer when the user closes the UI
/// </summary>
private void OnDisabledMessage(Entity<GasAnalyzerComponent> entity, ref GasAnalyzerDisableMessage message)
{
DisableAnalyzer(entity);
}
/// <summary>
/// Fetches fresh data for the analyzer. Should only be called by Update or when the user requests an update via refresh button
/// </summary>
private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
// check if the user has walked away from what they scanned
if (component.Target.HasValue)
{
// Listen! Even if you don't want the Gas Analyzer to work on moving targets, you should use
// this code to determine if the object is still generally in range so that the check is consistent with the code
// in OnAfterInteract() and also consistent with interaction code in general.
if (!_interactionSystem.InRangeUnobstructed((component.User, null), (component.Target.Value, null)))
{
// Don't update every tick
analyzer.AccumulatedFrametime += frameTime;
if (component.User is { } userId && component.Enabled)
_popup.PopupEntity(Loc.GetString("gas-analyzer-object-out-of-range"), userId, userId);
if (analyzer.AccumulatedFrametime < analyzer.UpdateInterval)
continue;
analyzer.AccumulatedFrametime -= analyzer.UpdateInterval;
if (!UpdateAnalyzer(uid))
RemCompDeferred<ActiveGasAnalyzerComponent>(uid);
component.Target = null;
}
}
/// <summary>
/// Activates the analyzer when used in the world, scanning either the target entity or the tile clicked
/// </summary>
private void OnAfterInteract(EntityUid uid, GasAnalyzerComponent component, AfterInteractEvent args)
var gasMixList = new List<GasMixEntry>();
// Fetch the environmental atmosphere around the scanner. This must be the first entry
var tileMixture = _atmo.GetContainingMixture(uid, true);
if (tileMixture != null)
{
if (!args.CanReach)
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, tileMixture.Pressure, tileMixture.Temperature,
GenerateGasEntryArray(tileMixture)));
}
else
{
// No gases were found
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f, 0f));
}
var deviceFlipped = false;
if (component.Target != null)
{
if (Deleted(component.Target))
{
_popup.PopupEntity(Loc.GetString("gas-analyzer-component-player-cannot-reach-message"), args.User, args.User);
return;
}
ActivateAnalyzer(uid, component, args.User, args.Target);
args.Handled = true;
}
/// <summary>
/// Activates the analyzer with no target, so it only scans the tile the user was on when activated
/// </summary>
private void OnUseInHand(EntityUid uid, GasAnalyzerComponent component, UseInHandEvent args)
{
ActivateAnalyzer(uid, component, args.User);
args.Handled = true;
}
/// <summary>
/// Handles analyzer activation logic
/// </summary>
private void ActivateAnalyzer(EntityUid uid, GasAnalyzerComponent component, EntityUid user, EntityUid? target = null)
{
if (!TryOpenUserInterface(uid, user, component))
return;
component.Target = target;
component.User = user;
if (target != null)
component.LastPosition = Transform(target.Value).Coordinates;
else
component.LastPosition = null;
component.Enabled = true;
Dirty(uid, component);
UpdateAppearance(uid, component);
EnsureComp<ActiveGasAnalyzerComponent>(uid);
UpdateAnalyzer(uid, component);
}
/// <summary>
/// Close the UI, turn the analyzer off, and don't update when it's dropped
/// </summary>
private void OnDropped(EntityUid uid, GasAnalyzerComponent component, DroppedEvent args)
{
if (args.User is var userId && component.Enabled)
_popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId);
DisableAnalyzer(uid, component, args.User);
}
/// <summary>
/// Closes the UI, sets the icon to off, and removes it from the update list
/// </summary>
private void DisableAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null, EntityUid? user = null)
{
if (!Resolve(uid, ref component))
return;
_userInterface.CloseUi(uid, GasAnalyzerUiKey.Key, user);
component.Enabled = false;
Dirty(uid, component);
UpdateAppearance(uid, component);
RemCompDeferred<ActiveGasAnalyzerComponent>(uid);
}
/// <summary>
/// Disables the analyzer when the user closes the UI
/// </summary>
private void OnDisabledMessage(EntityUid uid, GasAnalyzerComponent component, GasAnalyzerDisableMessage message)
{
DisableAnalyzer(uid, component);
}
private bool TryOpenUserInterface(EntityUid uid, EntityUid user, GasAnalyzerComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return false;
return _userInterface.TryOpenUi(uid, GasAnalyzerUiKey.Key, user);
}
/// <summary>
/// Fetches fresh data for the analyzer. Should only be called by Update or when the user requests an update via refresh button
/// </summary>
private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
if (!TryComp(component.User, out TransformComponent? xform))
{
DisableAnalyzer(uid, component);
component.Target = null;
DisableAnalyzer((uid, component), component.User);
return false;
}
// check if the user has walked away from what they scanned
var userPos = xform.Coordinates;
if (component.LastPosition.HasValue)
var validTarget = false;
// gas analyzed was used on an entity, try to request gas data via event for override
var ev = new GasAnalyzerScanEvent();
RaiseLocalEvent(component.Target.Value, ev);
if (ev.GasMixtures != null)
{
// Check if position is out of range => don't update and disable
if (!_transform.InRange(component.LastPosition.Value, userPos, SharedInteractionSystem.InteractionRange))
foreach (var mixes in ev.GasMixtures)
{
if (component.User is { } userId && component.Enabled)
_popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId);
DisableAnalyzer(uid, component, component.User);
return false;
}
}
var gasMixList = new List<GasMixEntry>();
// Fetch the environmental atmosphere around the scanner. This must be the first entry
var tileMixture = _atmo.GetContainingMixture(uid, true);
if (tileMixture != null)
{
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, tileMixture.Pressure, tileMixture.Temperature,
GenerateGasEntryArray(tileMixture)));
}
else
{
// No gases were found
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f, 0f));
}
var deviceFlipped = false;
if (component.Target != null)
{
if (Deleted(component.Target))
{
component.Target = null;
DisableAnalyzer(uid, component, component.User);
return false;
}
// gas analyzed was used on an entity, try to request gas data via event for override
var ev = new GasAnalyzerScanEvent();
RaiseLocalEvent(component.Target.Value, ev);
if (ev.GasMixtures != null)
{
foreach (var mixes in ev.GasMixtures)
if (mixes.Item2 != null)
{
if (mixes.Item2 != null)
gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2)));
gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2)));
validTarget = true;
}
deviceFlipped = ev.DeviceFlipped;
}
else
deviceFlipped = ev.DeviceFlipped;
}
else
{
// No override, fetch manually, to handle flippable devices you must subscribe to GasAnalyzerScanEvent
if (TryComp(component.Target, out NodeContainerComponent? node))
{
// No override, fetch manually, to handle flippable devices you must subscribe to GasAnalyzerScanEvent
if (TryComp(component.Target, out NodeContainerComponent? node))
foreach (var pair in node.Nodes)
{
foreach (var pair in node.Nodes)
if (pair.Value is PipeNode pipeNode)
{
if (pair.Value is PipeNode pipeNode)
{
// check if the volume is zero for some reason so we don't divide by zero
if (pipeNode.Air.Volume == 0f)
continue;
// only display the gas in the analyzed pipe element, not the whole system
var pipeAir = pipeNode.Air.Clone();
pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume);
pipeAir.Volume = pipeNode.Volume;
gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir)));
}
// check if the volume is zero for some reason so we don't divide by zero
if (pipeNode.Air.Volume == 0f)
continue;
// only display the gas in the analyzed pipe element, not the whole system
var pipeAir = pipeNode.Air.Clone();
pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume);
pipeAir.Volume = pipeNode.Volume;
gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir)));
validTarget = true;
}
}
}
}
// Don't bother sending a UI message with no content, and stop updating I guess?
if (gasMixList.Count == 0)
return false;
_userInterface.ServerSendUiMessage(uid, GasAnalyzerUiKey.Key,
new GasAnalyzerUserMessage(gasMixList.ToArray(),
component.Target != null ? Name(component.Target.Value) : string.Empty,
GetNetEntity(component.Target) ?? NetEntity.Invalid,
deviceFlipped));
return true;
}
/// <summary>
/// Sets the appearance based on the analyzers Enabled state
/// </summary>
private void UpdateAppearance(EntityUid uid, GasAnalyzerComponent analyzer)
{
_appearance.SetData(uid, GasAnalyzerVisuals.Enabled, analyzer.Enabled);
}
/// <summary>
/// Generates a GasEntry array for a given GasMixture
/// </summary>
private GasEntry[] GenerateGasEntryArray(GasMixture? mixture)
{
var gases = new List<GasEntry>();
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
// If the target doesn't actually have any gas mixes to add,
// invalidate it as the target
if (!validTarget)
{
var gas = _atmo.GetGas(i);
if (mixture?[i] <= UIMinMoles)
continue;
if (mixture != null)
{
var gasName = Loc.GetString(gas.Name);
gases.Add(new GasEntry(gasName, mixture[i], gas.Color));
}
component.Target = null;
}
var gasesOrdered = gases.OrderByDescending(gas => gas.Amount);
return gasesOrdered.ToArray();
}
// Don't bother sending a UI message with no content, and stop updating I guess?
if (gasMixList.Count == 0)
return false;
_userInterface.ServerSendUiMessage(uid, GasAnalyzerUiKey.Key,
new GasAnalyzerUserMessage(gasMixList.ToArray(),
component.Target != null ? Name(component.Target.Value) : string.Empty,
GetNetEntity(component.Target) ?? NetEntity.Invalid,
deviceFlipped));
return true;
}
/// <summary>
/// Generates a GasEntry array for a given GasMixture
/// </summary>
private GasEntry[] GenerateGasEntryArray(GasMixture? mixture)
{
var gases = new List<GasEntry>();
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
var gas = _atmo.GetGas(i);
if (mixture?[i] <= UIMinMoles)
continue;
if (mixture != null)
{
var gasName = Loc.GetString(gas.Name);
gases.Add(new GasEntry(gasName, mixture[i], gas.Color));
}
}
var gasesOrdered = gases.OrderByDescending(gas => gas.Amount);
return gasesOrdered.ToArray();
}
}

View File

@@ -290,7 +290,7 @@ public sealed class RespiratorSystem : EntitySystem
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs)
{
_alertsSystem.ShowAlert(entity.Owner, entity.Comp1.Alert);
_alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
}
}
@@ -306,7 +306,7 @@ public sealed class RespiratorSystem : EntitySystem
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs)
{
_alertsSystem.ClearAlert(entity.Owner, entity.Comp1.Alert);
_alertsSystem.ClearAlert(ent, entity.Comp1.Alert);
}
_damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);

View File

@@ -227,9 +227,12 @@ public partial class SeedData
[DataField("plantIconState")] public string PlantIconState { get; set; } = "produce";
/// <summary>
/// Screams random sound, could be strict sound SoundPathSpecifier or collection SoundCollectionSpecifier
/// base class is SoundSpecifier
/// </summary>
[DataField("screamSound")]
public SoundSpecifier ScreamSound = new SoundPathSpecifier("/Audio/Voice/Human/malescream_1.ogg");
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("PlantScreams", AudioParams.Default.WithVolume(-10));
[DataField("screaming")] public bool CanScream;

View File

@@ -153,6 +153,12 @@ public sealed class MutationSystem : EntitySystem
if (!Random(probBitflip))
return;
if (min == max)
{
val = min;
return;
}
// Starting number of bits that are high, between 0 and bits.
// In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
@@ -186,10 +192,22 @@ public sealed class MutationSystem : EntitySystem
if (!Random(probBitflip))
return;
if (min == max)
{
val = min;
return;
}
// Starting number of bits that are high, between 0 and bits.
// In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
// val may be outside the range of min/max due to starting prototype values, so clamp.
valInt = Math.Clamp(valInt, 0, bits);
// Probability that the bit flip increases n.
// The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasive it it.
// The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasing it.
// In other words, it tends to go to the middle.
float probIncrease = 1 - (float)val / bits;
float probIncrease = 1 - (float)valInt / bits;
int valMutated;
if (Random(probIncrease))
{

View File

@@ -313,11 +313,8 @@ public sealed class PlantHolderSystem : EntitySystem
var displayName = Loc.GetString(component.Seed.DisplayName);
_popup.PopupCursor(Loc.GetString("plant-holder-component-take-sample-message",
("seedName", displayName)), args.User);
if (component.Seed != null && component.Seed.CanScream)
{
_audio.PlayPvs(component.Seed.ScreamSound, uid, AudioParams.Default.WithVolume(-2));
}
DoScream(entity.Owner, component.Seed);
if (_random.Prob(0.3f))
component.Sampled = true;
@@ -525,10 +522,9 @@ public sealed class PlantHolderSystem : EntitySystem
var environment = _atmosphere.GetContainingMixture(uid, true, true) ?? GasMixture.SpaceGas;
component.MissingGas = 0;
if (component.Seed.ConsumeGasses.Count > 0)
{
component.MissingGas = 0;
foreach (var (gas, amount) in component.Seed.ConsumeGasses)
{
if (environment.GetMoles(gas) < amount)
@@ -748,6 +744,19 @@ public sealed class PlantHolderSystem : EntitySystem
return true;
}
/// <summary>
/// Force do scream on PlantHolder (like plant is screaming) using seed's ScreamSound specifier (collection or soundPath)
/// </summary>
/// <returns></returns>
public bool DoScream(EntityUid plantholder, SeedData? seed = null)
{
if (seed == null || seed.CanScream == false)
return false;
_audio.PlayPvs(seed.ScreamSound, plantholder);
return true;
}
public void AutoHarvest(EntityUid uid, PlantHolderComponent? component = null)
{
if (!Resolve(uid, ref component))
@@ -768,8 +777,7 @@ public sealed class PlantHolderSystem : EntitySystem
component.Harvest = false;
component.LastProduce = component.Age;
if (component.Seed != null && component.Seed.CanScream)
_audio.PlayPvs(component.Seed.ScreamSound, uid, AudioParams.Default.WithVolume(-2));
DoScream(uid, component.Seed);
if (component.Seed?.HarvestRepeat == HarvestType.NoRepeat)
RemovePlant(uid, component);

View File

@@ -18,385 +18,384 @@ using Robust.Shared.Player;
using Robust.Shared.Replays;
using Robust.Shared.Utility;
namespace Content.Server.Chat.Managers
namespace Content.Server.Chat.Managers;
/// <summary>
/// Dispatches chat messages to clients.
/// </summary>
internal sealed partial class ChatManager : IChatManager
{
/// <summary>
/// Dispatches chat messages to clients.
/// </summary>
internal sealed partial class ChatManager : IChatManager
private static readonly Dictionary<string, string> PatronOocColors = new()
{
private static readonly Dictionary<string, string> PatronOocColors = new()
// I had plans for multiple colors and those went nowhere so...
{ "nuclear_operative", "#aa00ff" },
{ "syndicate_agent", "#aa00ff" },
{ "revolutionary", "#aa00ff" }
};
[Dependency] private readonly IReplayRecordingManager _replay = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IMoMMILink _mommiLink = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
/// <summary>
/// The maximum length a player-sent message can be sent
/// </summary>
public int MaxMessageLength => _configurationManager.GetCVar(CCVars.ChatMaxMessageLength);
private bool _oocEnabled = true;
private bool _adminOocEnabled = true;
private readonly Dictionary<NetUserId, ChatUser> _players = new();
public void Initialize()
{
_netManager.RegisterNetMessage<MsgChatMessage>();
_netManager.RegisterNetMessage<MsgDeleteChatMessagesBy>();
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
RegisterRateLimits();
}
private void OnOocEnabledChanged(bool val)
{
if (_oocEnabled == val) return;
_oocEnabled = val;
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-ooc-chat-enabled-message" : "chat-manager-ooc-chat-disabled-message"));
}
private void OnAdminOocEnabledChanged(bool val)
{
if (_adminOocEnabled == val) return;
_adminOocEnabled = val;
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-admin-ooc-chat-enabled-message" : "chat-manager-admin-ooc-chat-disabled-message"));
}
public void DeleteMessagesBy(ICommonSession player)
{
if (!_players.TryGetValue(player.UserId, out var user))
return;
var msg = new MsgDeleteChatMessagesBy { Key = user.Key, Entities = user.Entities };
_netManager.ServerSendToAll(msg);
}
[return: NotNullIfNotNull(nameof(author))]
public ChatUser? EnsurePlayer(NetUserId? author)
{
if (author == null)
return null;
ref var user = ref CollectionsMarshal.GetValueRefOrAddDefault(_players, author.Value, out var exists);
if (!exists || user == null)
user = new ChatUser(_players.Count);
return user;
}
#region Server Announcements
public void DispatchServerAnnouncement(string message, Color? colorOverride = null)
{
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride);
Logger.InfoS("SERVER", message);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server announcement: {message}");
}
public void DispatchServerMessage(ICommonSession player, string message, bool suppressLog = false)
{
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, player.Channel);
if (!suppressLog)
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server message to {player:Player}: {message}");
}
public void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist, AdminFlags? flagWhitelist)
{
var clients = _adminManager.ActiveAdmins.Where(p =>
{
// I had plans for multiple colors and those went nowhere so...
{ "nuclear_operative", "#aa00ff" },
{ "syndicate_agent", "#aa00ff" },
{ "revolutionary", "#aa00ff" }
};
var adminData = _adminManager.GetAdminData(p);
[Dependency] private readonly IReplayRecordingManager _replay = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IMoMMILink _mommiLink = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
DebugTools.AssertNotNull(adminData);
/// <summary>
/// The maximum length a player-sent message can be sent
/// </summary>
public int MaxMessageLength => _configurationManager.GetCVar(CCVars.ChatMaxMessageLength);
private bool _oocEnabled = true;
private bool _adminOocEnabled = true;
private readonly Dictionary<NetUserId, ChatUser> _players = new();
public void Initialize()
{
_netManager.RegisterNetMessage<MsgChatMessage>();
_netManager.RegisterNetMessage<MsgDeleteChatMessagesBy>();
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
RegisterRateLimits();
}
private void OnOocEnabledChanged(bool val)
{
if (_oocEnabled == val) return;
_oocEnabled = val;
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-ooc-chat-enabled-message" : "chat-manager-ooc-chat-disabled-message"));
}
private void OnAdminOocEnabledChanged(bool val)
{
if (_adminOocEnabled == val) return;
_adminOocEnabled = val;
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-admin-ooc-chat-enabled-message" : "chat-manager-admin-ooc-chat-disabled-message"));
}
public void DeleteMessagesBy(ICommonSession player)
{
if (!_players.TryGetValue(player.UserId, out var user))
return;
var msg = new MsgDeleteChatMessagesBy { Key = user.Key, Entities = user.Entities };
_netManager.ServerSendToAll(msg);
}
[return: NotNullIfNotNull(nameof(author))]
public ChatUser? EnsurePlayer(NetUserId? author)
{
if (author == null)
return null;
ref var user = ref CollectionsMarshal.GetValueRefOrAddDefault(_players, author.Value, out var exists);
if (!exists || user == null)
user = new ChatUser(_players.Count);
return user;
}
#region Server Announcements
public void DispatchServerAnnouncement(string message, Color? colorOverride = null)
{
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride);
Logger.InfoS("SERVER", message);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server announcement: {message}");
}
public void DispatchServerMessage(ICommonSession player, string message, bool suppressLog = false)
{
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, player.Channel);
if (!suppressLog)
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server message to {player:Player}: {message}");
}
public void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist, AdminFlags? flagWhitelist)
{
var clients = _adminManager.ActiveAdmins.Where(p =>
{
var adminData = _adminManager.GetAdminData(p);
DebugTools.AssertNotNull(adminData);
if (adminData == null)
return false;
if (flagBlacklist != null && adminData.HasFlag(flagBlacklist.Value))
return false;
return flagWhitelist == null || adminData.HasFlag(flagWhitelist.Value);
}).Select(p => p.Channel);
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
ChatMessageToMany(ChatChannel.Admin, message, wrappedMessage, default, false, true, clients);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin announcement: {message}");
}
public void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true)
{
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
("message", FormattedMessage.EscapeText(message)));
ChatMessageToOne(ChatChannel.Admin, message, wrappedMessage, default, false, player.Channel);
}
public void SendAdminAlert(string message)
{
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
ChatMessageToMany(ChatChannel.AdminAlert, message, wrappedMessage, default, false, true, clients);
}
public void SendAdminAlert(EntityUid player, string message)
{
var mindSystem = _entityManager.System<SharedMindSystem>();
if (!mindSystem.TryGetMind(player, out var mindId, out var mind))
{
SendAdminAlert(message);
return;
}
var adminSystem = _entityManager.System<AdminSystem>();
var antag = mind.UserId != null && (adminSystem.GetCachedPlayerInfo(mind.UserId.Value)?.Antag ?? false);
SendAdminAlert($"{mind.Session?.Name}{(antag ? " (ANTAG)" : "")} {message}");
}
public void SendHookOOC(string sender, string message)
{
if (!_oocEnabled && _configurationManager.GetCVar(CCVars.DisablingOOCDisablesRelay))
{
return;
}
var wrappedMessage = Loc.GetString("chat-manager-send-hook-ooc-wrap-message", ("senderName", sender), ("message", FormattedMessage.EscapeText(message)));
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, source: EntityUid.Invalid, hideChat: false, recordReplay: true);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Hook OOC from {sender}: {message}");
}
#endregion
#region Public OOC Chat API
/// <summary>
/// Called for a player to attempt sending an OOC, out-of-game. message.
/// </summary>
/// <param name="player">The player sending the message.</param>
/// <param name="message">The message.</param>
/// <param name="type">The type of message.</param>
public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
{
if (HandleRateLimit(player) != RateLimitStatus.Allowed)
return;
// Check if message exceeds the character limit
if (message.Length > MaxMessageLength)
{
DispatchServerMessage(player, Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength)));
return;
}
switch (type)
{
case OOCChatType.OOC:
SendOOC(player, message);
break;
case OOCChatType.Admin:
SendAdminChat(player, message);
break;
}
}
#endregion
#region Private API
private void SendOOC(ICommonSession player, string message)
{
if (_adminManager.IsAdmin(player))
{
if (!_adminOocEnabled)
{
return;
}
}
else if (!_oocEnabled)
{
return;
}
Color? colorOverride = null;
var wrappedMessage = Loc.GetString("chat-manager-send-ooc-wrap-message", ("playerName",player.Name), ("message", FormattedMessage.EscapeText(message)));
if (_adminManager.HasAdminFlag(player, AdminFlags.Admin))
{
var prefs = _preferencesManager.GetPreferences(player.UserId);
colorOverride = prefs.AdminOOCColor;
}
if ( _netConfigManager.GetClientCVar(player.Channel, CCVars.ShowOocPatronColor) && player.Channel.UserData.PatronTier is { } patron && PatronOocColors.TryGetValue(patron, out var patronColor))
{
wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", patronColor),("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
}
//TODO: player.Name color, this will need to change the structure of the MsgChatMessage
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride, author: player.UserId);
_mommiLink.SendOOCMessage(player.Name, message.Replace("@", "\\@").Replace("<", "\\<").Replace("/", "\\/")); // @ and < are both problematic for discord due to pinging. / is sanitized solely to kneecap links to murder embeds via blunt force
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"OOC from {player:Player}: {message}");
}
private void SendAdminChat(ICommonSession player, string message)
{
if (!_adminManager.IsAdmin(player))
{
_adminLogger.Add(LogType.Chat, LogImpact.Extreme, $"{player:Player} attempted to send admin message but was not admin");
return;
}
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
var wrappedMessage = Loc.GetString("chat-manager-send-admin-chat-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
foreach (var client in clients)
{
var isSource = client != player.Channel;
ChatMessageToOne(ChatChannel.AdminChat,
message,
wrappedMessage,
default,
false,
client,
audioPath: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundPath) : default,
audioVolume: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundVolume) : default,
author: player.UserId);
}
_adminLogger.Add(LogType.Chat, $"Admin chat from {player:Player}: {message}");
}
#endregion
#region Utility
public void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
{
var user = author == null ? null : EnsurePlayer(author);
var netSource = _entityManager.GetNetEntity(source);
user?.AddEntity(netSource);
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_netManager.ServerSendMessage(new MsgChatMessage() { Message = msg }, client);
if (!recordReplay)
return;
if ((channel & ChatChannel.AdminRelated) == 0 ||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
{
_replay.RecordServerMessage(msg);
}
}
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, IEnumerable<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
=> ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients.ToList(), colorOverride, audioPath, audioVolume, author);
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, List<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
{
var user = author == null ? null : EnsurePlayer(author);
var netSource = _entityManager.GetNetEntity(source);
user?.AddEntity(netSource);
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_netManager.ServerSendToMany(new MsgChatMessage() { Message = msg }, clients);
if (!recordReplay)
return;
if ((channel & ChatChannel.AdminRelated) == 0 ||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
{
_replay.RecordServerMessage(msg);
}
}
public void ChatMessageToManyFiltered(Filter filter, ChatChannel channel, string message, string wrappedMessage, EntityUid source,
bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0)
{
if (!recordReplay && !filter.Recipients.Any())
return;
var clients = new List<INetChannel>();
foreach (var recipient in filter.Recipients)
{
clients.Add(recipient.Channel);
}
ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients, colorOverride, audioPath, audioVolume);
}
public void ChatMessageToAll(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
{
var user = author == null ? null : EnsurePlayer(author);
var netSource = _entityManager.GetNetEntity(source);
user?.AddEntity(netSource);
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_netManager.ServerSendToAll(new MsgChatMessage() { Message = msg });
if (!recordReplay)
return;
if ((channel & ChatChannel.AdminRelated) == 0 ||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
{
_replay.RecordServerMessage(msg);
}
}
public bool MessageCharacterLimit(ICommonSession? player, string message)
{
var isOverLength = false;
// Non-players don't need to be checked.
if (player == null)
if (adminData == null)
return false;
// Check if message exceeds the character limit if the sender is a player
if (message.Length > MaxMessageLength)
{
var feedback = Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength));
if (flagBlacklist != null && adminData.HasFlag(flagBlacklist.Value))
return false;
DispatchServerMessage(player, feedback);
return flagWhitelist == null || adminData.HasFlag(flagWhitelist.Value);
isOverLength = true;
}
}).Select(p => p.Channel);
return isOverLength;
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
ChatMessageToMany(ChatChannel.Admin, message, wrappedMessage, default, false, true, clients);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin announcement: {message}");
}
public void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true)
{
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
("message", FormattedMessage.EscapeText(message)));
ChatMessageToOne(ChatChannel.Admin, message, wrappedMessage, default, false, player.Channel);
}
public void SendAdminAlert(string message)
{
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
ChatMessageToMany(ChatChannel.AdminAlert, message, wrappedMessage, default, false, true, clients);
}
public void SendAdminAlert(EntityUid player, string message)
{
var mindSystem = _entityManager.System<SharedMindSystem>();
if (!mindSystem.TryGetMind(player, out var mindId, out var mind))
{
SendAdminAlert(message);
return;
}
#endregion
var adminSystem = _entityManager.System<AdminSystem>();
var antag = mind.UserId != null && (adminSystem.GetCachedPlayerInfo(mind.UserId.Value)?.Antag ?? false);
SendAdminAlert($"{mind.Session?.Name}{(antag ? " (ANTAG)" : "")} {message}");
}
public enum OOCChatType : byte
public void SendHookOOC(string sender, string message)
{
OOC,
Admin
if (!_oocEnabled && _configurationManager.GetCVar(CCVars.DisablingOOCDisablesRelay))
{
return;
}
var wrappedMessage = Loc.GetString("chat-manager-send-hook-ooc-wrap-message", ("senderName", sender), ("message", FormattedMessage.EscapeText(message)));
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, source: EntityUid.Invalid, hideChat: false, recordReplay: true);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Hook OOC from {sender}: {message}");
}
#endregion
#region Public OOC Chat API
/// <summary>
/// Called for a player to attempt sending an OOC, out-of-game. message.
/// </summary>
/// <param name="player">The player sending the message.</param>
/// <param name="message">The message.</param>
/// <param name="type">The type of message.</param>
public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
{
if (HandleRateLimit(player) != RateLimitStatus.Allowed)
return;
// Check if message exceeds the character limit
if (message.Length > MaxMessageLength)
{
DispatchServerMessage(player, Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength)));
return;
}
switch (type)
{
case OOCChatType.OOC:
SendOOC(player, message);
break;
case OOCChatType.Admin:
SendAdminChat(player, message);
break;
}
}
#endregion
#region Private API
private void SendOOC(ICommonSession player, string message)
{
if (_adminManager.IsAdmin(player))
{
if (!_adminOocEnabled)
{
return;
}
}
else if (!_oocEnabled)
{
return;
}
Color? colorOverride = null;
var wrappedMessage = Loc.GetString("chat-manager-send-ooc-wrap-message", ("playerName",player.Name), ("message", FormattedMessage.EscapeText(message)));
if (_adminManager.HasAdminFlag(player, AdminFlags.Admin))
{
var prefs = _preferencesManager.GetPreferences(player.UserId);
colorOverride = prefs.AdminOOCColor;
}
if ( _netConfigManager.GetClientCVar(player.Channel, CCVars.ShowOocPatronColor) && player.Channel.UserData.PatronTier is { } patron && PatronOocColors.TryGetValue(patron, out var patronColor))
{
wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", patronColor),("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
}
//TODO: player.Name color, this will need to change the structure of the MsgChatMessage
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride, author: player.UserId);
_mommiLink.SendOOCMessage(player.Name, message.Replace("@", "\\@").Replace("<", "\\<").Replace("/", "\\/")); // @ and < are both problematic for discord due to pinging. / is sanitized solely to kneecap links to murder embeds via blunt force
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"OOC from {player:Player}: {message}");
}
private void SendAdminChat(ICommonSession player, string message)
{
if (!_adminManager.IsAdmin(player))
{
_adminLogger.Add(LogType.Chat, LogImpact.Extreme, $"{player:Player} attempted to send admin message but was not admin");
return;
}
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
var wrappedMessage = Loc.GetString("chat-manager-send-admin-chat-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
foreach (var client in clients)
{
var isSource = client != player.Channel;
ChatMessageToOne(ChatChannel.AdminChat,
message,
wrappedMessage,
default,
false,
client,
audioPath: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundPath) : default,
audioVolume: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundVolume) : default,
author: player.UserId);
}
_adminLogger.Add(LogType.Chat, $"Admin chat from {player:Player}: {message}");
}
#endregion
#region Utility
public void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
{
var user = author == null ? null : EnsurePlayer(author);
var netSource = _entityManager.GetNetEntity(source);
user?.AddEntity(netSource);
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_netManager.ServerSendMessage(new MsgChatMessage() { Message = msg }, client);
if (!recordReplay)
return;
if ((channel & ChatChannel.AdminRelated) == 0 ||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
{
_replay.RecordServerMessage(msg);
}
}
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, IEnumerable<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
=> ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients.ToList(), colorOverride, audioPath, audioVolume, author);
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, List<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
{
var user = author == null ? null : EnsurePlayer(author);
var netSource = _entityManager.GetNetEntity(source);
user?.AddEntity(netSource);
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_netManager.ServerSendToMany(new MsgChatMessage() { Message = msg }, clients);
if (!recordReplay)
return;
if ((channel & ChatChannel.AdminRelated) == 0 ||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
{
_replay.RecordServerMessage(msg);
}
}
public void ChatMessageToManyFiltered(Filter filter, ChatChannel channel, string message, string wrappedMessage, EntityUid source,
bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0)
{
if (!recordReplay && !filter.Recipients.Any())
return;
var clients = new List<INetChannel>();
foreach (var recipient in filter.Recipients)
{
clients.Add(recipient.Channel);
}
ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients, colorOverride, audioPath, audioVolume);
}
public void ChatMessageToAll(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
{
var user = author == null ? null : EnsurePlayer(author);
var netSource = _entityManager.GetNetEntity(source);
user?.AddEntity(netSource);
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_netManager.ServerSendToAll(new MsgChatMessage() { Message = msg });
if (!recordReplay)
return;
if ((channel & ChatChannel.AdminRelated) == 0 ||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
{
_replay.RecordServerMessage(msg);
}
}
public bool MessageCharacterLimit(ICommonSession? player, string message)
{
var isOverLength = false;
// Non-players don't need to be checked.
if (player == null)
return false;
// Check if message exceeds the character limit if the sender is a player
if (message.Length > MaxMessageLength)
{
var feedback = Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength));
DispatchServerMessage(player, feedback);
isOverLength = true;
}
return isOverLength;
}
#endregion
}
public enum OOCChatType : byte
{
OOC,
Admin
}

View File

@@ -96,18 +96,14 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
{ ":/", "chatsan-uncertain" },
{ ":\\", "chatsan-uncertain" },
{ "lmao", "chatsan-laughs" },
{ "lmao.", "chatsan-laughs" },
{ "lmfao", "chatsan-laughs" },
{ "lol", "chatsan-laughs" },
{ "lol.", "chatsan-laughs" },
{ "lel", "chatsan-laughs" },
{ "lel.", "chatsan-laughs" },
{ "kek", "chatsan-laughs" },
{ "kek.", "chatsan-laughs" },
{ "rofl", "chatsan-laughs" },
{ "o7", "chatsan-salutes" },
{ ";_;7", "chatsan-tearfully-salutes"},
{ "idk", "chatsan-shrugs" },
{ "idk.", "chatsan-shrugs" },
{ ";)", "chatsan-winks" },
{ ";]", "chatsan-winks" },
{ "(;", "chatsan-winks" },

View File

@@ -161,14 +161,32 @@ public partial class ChatSystem
/// <param name="textInput"></param>
private void TryEmoteChatInput(EntityUid uid, string textInput)
{
var actionLower = textInput.ToLower();
if (!_wordEmoteDict.TryGetValue(actionLower, out var emote))
var actionTrimmedLower = TrimPunctuation(textInput.ToLower());
if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote))
return;
if (!AllowedToUseEmote(uid, emote))
return;
InvokeEmoteEvent(uid, emote);
return;
static string TrimPunctuation(string textInput)
{
var trimEnd = textInput.Length;
while (trimEnd > 0 && char.IsPunctuation(textInput[trimEnd - 1]))
{
trimEnd--;
}
var trimStart = 0;
while (trimStart < trimEnd && char.IsPunctuation(textInput[trimStart]))
{
trimStart++;
}
return textInput[trimStart..trimEnd];
}
}
/// <summary>
/// Checks if we can use this emote based on the emotes whitelist, blacklist, and availibility to the entity.

View File

@@ -22,6 +22,7 @@ using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.Emag.Components;
using Content.Shared.Popups;
using Content.Shared.Silicons.Borgs.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
@@ -255,10 +256,17 @@ namespace Content.Server.Communications
return;
}
// User has an id
if (_idCardSystem.TryFindIdCard(mob, out var id))
{
author = $"{id.Comp.FullName} ({CultureInfo.CurrentCulture.TextInfo.ToTitleCase(id.Comp.JobTitle ?? string.Empty)})".Trim();
}
// User does not have an id and is a borg, so use the borg's name
if (HasComp<BorgChassisComponent>(mob))
{
author = Name(mob).Trim();
}
}
comp.AnnouncementCooldownRemaining = comp.Delay;

View File

@@ -22,8 +22,9 @@ namespace Content.Server.Construction.Completions
public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager)
{
var scale = (float) IoCManager.Resolve<IRobustRandom>().NextGaussian(1, Variation);
entityManager.EntitySysManager.GetEntitySystem<SharedAudioSystem>()
.PlayPvs(Sound, uid, AudioParams.WithPitchScale(scale));
if (entityManager.TryGetComponent<TransformComponent>(uid, out var xform))
entityManager.EntitySysManager.GetEntitySystem<SharedAudioSystem>()
.PlayPvs(Sound, xform.Coordinates, AudioParams.WithPitchScale(scale));
}
}
}

View File

@@ -2,6 +2,7 @@ using Content.Server.Administration.Logs;
using Content.Server.Damage.Components;
using Content.Server.Weapons.Ranged.Systems;
using Content.Shared._CP14.MeleeWeapon.Components;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Camera;
using Content.Shared.Damage;
using Content.Shared.Damage.Events;
@@ -10,6 +11,7 @@ using Content.Shared.Database;
using Content.Shared.Effects;
using Content.Shared.Mobs.Components;
using Content.Shared.Throwing;
using Content.Shared.Wires;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
@@ -29,6 +31,7 @@ namespace Content.Server.Damage.Systems
{
SubscribeLocalEvent<DamageOtherOnHitComponent, ThrowDoHitEvent>(OnDoHit);
SubscribeLocalEvent<DamageOtherOnHitComponent, DamageExamineEvent>(OnDamageExamine);
SubscribeLocalEvent<DamageOtherOnHitComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow);
}
private void OnDoHit(EntityUid uid, DamageOtherOnHitComponent component, ThrowDoHitEvent args)
@@ -71,5 +74,13 @@ namespace Content.Server.Damage.Systems
_damageExamine.AddDamageExamine(args.Message, damage, Loc.GetString("damage-throw"));
}
/// <summary>
/// Prevent players with the Pacified status effect from throwing things that deal damage.
/// </summary>
private void OnAttemptPacifiedThrow(Entity<DamageOtherOnHitComponent> ent, ref AttemptPacifiedThrowEvent args)
{
args.Cancel("pacified-cannot-throw");
}
}
}

View File

@@ -0,0 +1,40 @@
using Content.Server.DeviceLinking.Systems;
using Content.Shared.DeviceLinking;
using Robust.Shared.Prototypes;
namespace Content.Server.DeviceLinking.Components;
/// <summary>
/// Memory cell that sets the output to the input when enabled.
/// </summary>
[RegisterComponent, Access(typeof(MemoryCellSystem))]
public sealed partial class MemoryCellComponent : Component
{
/// <summary>
/// Name of the input port.
/// </summary>
[DataField]
public ProtoId<SinkPortPrototype> InputPort = "MemoryInput";
/// <summary>
/// Name of the enable port.
/// </summary>
[DataField]
public ProtoId<SinkPortPrototype> EnablePort = "MemoryEnable";
/// <summary>
/// Name of the output port.
/// </summary>
[DataField]
public ProtoId<SourcePortPrototype> OutputPort = "Output";
// State
[DataField]
public SignalState InputState = SignalState.Low;
[DataField]
public SignalState EnableState = SignalState.Low;
[DataField]
public bool LastOutput;
}

View File

@@ -0,0 +1,74 @@
using Content.Server.DeviceLinking.Components;
using Content.Server.DeviceLinking.Events;
using Content.Server.DeviceNetwork;
using Content.Shared.DeviceLinking;
namespace Content.Server.DeviceLinking.Systems;
/// <summary>
/// Handles the control of output based on the input and enable ports.
/// </summary>
public sealed class MemoryCellSystem : EntitySystem
{
[Dependency] private readonly DeviceLinkSystem _deviceLink = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MemoryCellComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<MemoryCellComponent, SignalReceivedEvent>(OnSignalReceived);
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
var query = EntityQueryEnumerator<MemoryCellComponent, DeviceLinkSourceComponent>();
while (query.MoveNext(out var uid, out var comp, out var source))
{
if (comp.InputState == SignalState.Momentary)
comp.InputState = SignalState.Low;
if (comp.EnableState == SignalState.Momentary)
comp.EnableState = SignalState.Low;
UpdateOutput((uid, comp, source));
}
}
private void OnInit(Entity<MemoryCellComponent> ent, ref ComponentInit args)
{
var (uid, comp) = ent;
_deviceLink.EnsureSinkPorts(uid, comp.InputPort, comp.EnablePort);
_deviceLink.EnsureSourcePorts(uid, comp.OutputPort);
}
private void OnSignalReceived(Entity<MemoryCellComponent> ent, ref SignalReceivedEvent args)
{
var state = SignalState.Momentary;
args.Data?.TryGetValue(DeviceNetworkConstants.LogicState, out state);
if (args.Port == ent.Comp.InputPort)
ent.Comp.InputState = state;
else if (args.Port == ent.Comp.EnablePort)
ent.Comp.EnableState = state;
UpdateOutput(ent);
}
private void UpdateOutput(Entity<MemoryCellComponent, DeviceLinkSourceComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp2))
return;
if (ent.Comp1.EnableState == SignalState.Low)
return;
var value = ent.Comp1.InputState != SignalState.Low;
if (value == ent.Comp1.LastOutput)
return;
ent.Comp1.LastOutput = value;
_deviceLink.SendSignal(ent, ent.Comp1.OutputPort, value, ent.Comp2);
}
}

View File

@@ -102,6 +102,13 @@ namespace Content.Server.DeviceNetwork.Components
[DataField("sendBroadcastAttemptEvent")]
public bool SendBroadcastAttemptEvent = false;
/// <summary>
/// Whether this device's address can be saved to device-lists
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("savableAddress")]
public bool SavableAddress = true;
/// <summary>
/// A list of device-lists that this device is on.
/// </summary>

View File

@@ -106,6 +106,13 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
if (!targetUid.HasValue || !Resolve(targetUid.Value, ref device, false))
return;
//This checks if the device is marked as having a savable address,
//to avoid adding pdas and whatnot to air alarms. This flag is true
//by default, so this will only prevent devices from being added to
//network configurator lists if manually set to false in the prototype
if (!device.SavableAddress)
return;
var address = device.Address;
if (string.IsNullOrEmpty(address))
{

View File

@@ -32,6 +32,8 @@ namespace Content.Server.Disposal.Tube
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosSystem = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
public override void Initialize()
{
base.Initialize();
@@ -345,7 +347,7 @@ namespace Content.Server.Disposal.Tube
return null;
var position = xform.Coordinates;
foreach (var entity in grid.GetInDir(position, nextDirection))
foreach (var entity in _map.GetInDir(xform.GridUid.Value, grid, position, nextDirection))
{
if (!TryComp(entity, out DisposalTubeComponent? tube))
{

View File

@@ -54,6 +54,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
public override void Initialize()
{
@@ -474,7 +475,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
var delay = insertingSelf ? unit.EntryDelay : unit.DraggedEntryDelay;
if (userId != null && !insertingSelf)
_popupSystem.PopupEntity(Loc.GetString("disposal-unit-being-inserted", ("user", Identity.Entity((EntityUid) userId, EntityManager))), toInsertId, toInsertId, PopupType.Large);
_popupSystem.PopupEntity(Loc.GetString("disposal-unit-being-inserted", ("user", Identity.Entity((EntityUid)userId, EntityManager))), toInsertId, toInsertId, PopupType.Large);
if (delay <= 0 || userId == null)
{
@@ -520,7 +521,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
return false;
var coords = xform.Coordinates;
var entry = grid.GetLocal(coords)
var entry = _map.GetLocal(xform.GridUid.Value, grid, coords)
.FirstOrDefault(HasComp<DisposalEntryComponent>);
if (entry == default || component is not DisposalUnitComponent sDisposals)

View File

@@ -13,8 +13,9 @@ namespace Content.Server.EntityEffects.Effects
[DataField]
public float Multiplier = 0.05f;
// The fire stack multiplier if fire stacks already exist on target, only works if 0 or greater
[DataField]
public float MultiplierOnExisting = 1f;
public float MultiplierOnExisting = -1f;
public override bool ShouldLog => true;
@@ -28,7 +29,8 @@ namespace Content.Server.EntityEffects.Effects
if (!args.EntityManager.TryGetComponent(args.TargetEntity, out FlammableComponent? flammable))
return;
var multiplier = flammable.FireStacks == 0f ? Multiplier : MultiplierOnExisting;
// Sets the multiplier for FireStacks to MultiplierOnExisting is 0 or greater and target already has FireStacks
var multiplier = flammable.FireStacks != 0f && MultiplierOnExisting >= 0 ? MultiplierOnExisting : Multiplier;
var quantity = 1f;
if (args is EntityEffectReagentArgs reagentArgs)
{

View File

@@ -0,0 +1,28 @@
using Content.Server.Botany.Systems;
using Content.Shared.EntityEffects;
namespace Content.Server.EntityEffects.Effects.PlantMetabolism;
/// <summary>
/// Handles increase or decrease of plant potency.
/// </summary>
public sealed partial class PlantAdjustPotency : PlantAdjustAttribute
{
public override string GuidebookAttributeName { get; set; } = "plant-attribute-potency";
public override void Effect(EntityEffectBaseArgs args)
{
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager))
return;
if (plantHolderComp.Seed == null)
return;
var plantHolder = args.EntityManager.System<PlantHolderSystem>();
plantHolder.EnsureUniqueSeed(args.TargetEntity, plantHolderComp);
plantHolderComp.Seed.Potency = Math.Max(plantHolderComp.Seed.Potency + Amount, 1);
}
}

View File

@@ -0,0 +1,42 @@
using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Shared.EntityEffects;
using Content.Shared.Popups;
using Robust.Shared.Prototypes;
namespace Content.Server.EntityEffects.Effects.PlantMetabolism;
/// <summary>
/// Handles removal of seeds on a plant.
/// </summary>
public sealed partial class PlantDestroySeeds : EntityEffect
{
public override void Effect(EntityEffectBaseArgs args)
{
if (
!args.EntityManager.TryGetComponent(args.TargetEntity, out PlantHolderComponent? plantHolderComp)
|| plantHolderComp.Seed == null
|| plantHolderComp.Dead
|| plantHolderComp.Seed.Immutable
)
return;
var plantHolder = args.EntityManager.System<PlantHolderSystem>();
var popupSystem = args.EntityManager.System<SharedPopupSystem>();
if (plantHolderComp.Seed.Seedless == false)
{
plantHolder.EnsureUniqueSeed(args.TargetEntity, plantHolderComp);
popupSystem.PopupEntity(
Loc.GetString("botany-plant-seedsdestroyed"),
args.TargetEntity,
PopupType.SmallCaution
);
plantHolderComp.Seed.Seedless = true;
}
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) =>
Loc.GetString("reagent-effect-guidebook-plant-seeds-remove", ("chance", Probability));
}

View File

@@ -0,0 +1,38 @@
using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Shared.EntityEffects;
using Content.Shared.Popups;
using Robust.Shared.Prototypes;
namespace Content.Server.EntityEffects.Effects.PlantMetabolism;
/// <summary>
/// Handles restoral of seeds on a plant.
/// </summary>
public sealed partial class PlantRestoreSeeds : EntityEffect
{
public override void Effect(EntityEffectBaseArgs args)
{
if (
!args.EntityManager.TryGetComponent(args.TargetEntity, out PlantHolderComponent? plantHolderComp)
|| plantHolderComp.Seed == null
|| plantHolderComp.Dead
|| plantHolderComp.Seed.Immutable
)
return;
var plantHolder = args.EntityManager.System<PlantHolderSystem>();
var popupSystem = args.EntityManager.System<SharedPopupSystem>();
if (plantHolderComp.Seed.Seedless)
{
plantHolder.EnsureUniqueSeed(args.TargetEntity, plantHolderComp);
popupSystem.PopupEntity(Loc.GetString("botany-plant-seedsrestored"), args.TargetEntity);
plantHolderComp.Seed.Seedless = false;
}
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) =>
Loc.GetString("reagent-effect-guidebook-plant-seeds-add", ("chance", Probability));
}

View File

@@ -0,0 +1,37 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Content.Server.Explosion.EntitySystems;
namespace Content.Server.Explosion.Components;
/// <summary>
/// A component that electrocutes an entity having this component when a trigger is triggered.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
[Access(typeof(TriggerSystem))]
public sealed partial class ShockOnTriggerComponent : Component
{
/// <summary>
/// The force of an electric shock when the trigger is triggered.
/// </summary>
[DataField]
public int Damage = 5;
/// <summary>
/// Duration of electric shock when the trigger is triggered.
/// </summary>
[DataField]
public TimeSpan Duration = TimeSpan.FromSeconds(2);
/// <summary>
/// The minimum delay between repeating triggers.
/// </summary>
[DataField]
public TimeSpan Cooldown = TimeSpan.FromSeconds(4);
/// <summary>
/// When can the trigger run again?
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextTrigger = TimeSpan.Zero;
}

View File

@@ -3,6 +3,7 @@ using Content.Server.Body.Systems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Explosion.Components;
using Content.Server.Flash;
using Content.Server.Electrocution;
using Content.Server.Pinpointer;
using Content.Shared.Flash.Components;
using Content.Server.Radio.EntitySystems;
@@ -33,6 +34,7 @@ using Robust.Shared.Random;
using Robust.Shared.Player;
using Content.Shared.Coordinates;
using Robust.Shared.Utility;
using Robust.Shared.Timing;
namespace Content.Server.Explosion.EntitySystems
{
@@ -75,6 +77,7 @@ namespace Content.Server.Explosion.EntitySystems
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly ElectrocutionSystem _electrocution = default!;
public override void Initialize()
{
@@ -104,6 +107,7 @@ namespace Content.Server.Explosion.EntitySystems
SubscribeLocalEvent<AnchorOnTriggerComponent, TriggerEvent>(OnAnchorTrigger);
SubscribeLocalEvent<SoundOnTriggerComponent, TriggerEvent>(OnSoundTrigger);
SubscribeLocalEvent<ShockOnTriggerComponent, TriggerEvent>(HandleShockTrigger);
SubscribeLocalEvent<RattleComponent, TriggerEvent>(HandleRattleTrigger);
}
@@ -120,6 +124,24 @@ namespace Content.Server.Explosion.EntitySystems
}
}
private void HandleShockTrigger(Entity<ShockOnTriggerComponent> shockOnTrigger, ref TriggerEvent args)
{
if (!_container.TryGetContainingContainer(shockOnTrigger, out var container))
return;
var containerEnt = container.Owner;
var curTime = _timing.CurTime;
if (curTime < shockOnTrigger.Comp.NextTrigger)
{
// The trigger's on cooldown.
return;
}
_electrocution.TryDoElectrocution(containerEnt, null, shockOnTrigger.Comp.Damage, shockOnTrigger.Comp.Duration, true);
shockOnTrigger.Comp.NextTrigger = curTime + shockOnTrigger.Comp.Cooldown;
}
private void OnAnchorTrigger(EntityUid uid, AnchorOnTriggerComponent component, TriggerEvent args)
{
var xform = Transform(uid);

View File

@@ -9,6 +9,7 @@ using JetBrains.Annotations;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Localization;
namespace Content.Server.GameTicking;
@@ -71,6 +72,16 @@ public sealed partial class GameTicker
var ruleEntity = Spawn(ruleId, MapCoordinates.Nullspace);
_sawmill.Info($"Added game rule {ToPrettyString(ruleEntity)}");
_adminLogger.Add(LogType.EventStarted, $"Added game rule {ToPrettyString(ruleEntity)}");
var str = Loc.GetString("station-event-system-run-event", ("eventName", ToPrettyString(ruleEntity)));
#if DEBUG
_chatManager.SendAdminAlert(str);
#else
if (RunLevel == GameRunLevel.InRound) // avoids telling admins the round type before it starts so that can be handled elsewhere.
{
_chatManager.SendAdminAlert(str);
}
#endif
Log.Info(str);
var ev = new GameRuleAddedEvent(ruleEntity, ruleId);
RaiseLocalEvent(ruleEntity, ref ev, true);

View File

@@ -96,36 +96,6 @@ public sealed partial class NukeopsRuleComponent : Component
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg");
}
/// <summary>
/// Stores the presets for each operative type
/// Ie Commander, Agent and Operative
/// </summary>
[DataDefinition, Serializable]
public sealed partial class NukeopSpawnPreset
{
[DataField]
public ProtoId<AntagPrototype> AntagRoleProto = "Nukeops";
/// <summary>
/// The equipment set this operative will be given when spawned
/// </summary>
[DataField]
public ProtoId<StartingGearPrototype> GearProto = "SyndicateOperativeGearFull";
/// <summary>
/// The name prefix, ie "Agent"
/// </summary>
[DataField]
public LocId NamePrefix = "nukeops-role-operator";
/// <summary>
/// The entity name suffix will be chosen from this list randomly
/// </summary>
[DataField]
public ProtoId<DatasetPrototype> NameList = "SyndicateNamesNormal";
}
public enum WinType : byte
{
/// <summary>

View File

@@ -1,11 +1,10 @@
namespace Content.Server.Ghost
namespace Content.Server.Ghost;
/// <summary>
/// This is used to mark Observers properly, as they get Minds
/// </summary>
[RegisterComponent]
public sealed partial class ObserverRoleComponent : Component
{
/// <summary>
/// This is used to mark Observers properly, as they get Minds
/// </summary>
[RegisterComponent]
public sealed partial class ObserverRoleComponent : Component
{
public string Name => Loc.GetString("observer-role-name");
}
public string Name => Loc.GetString("observer-role-name");
}

View File

@@ -2,102 +2,101 @@
using Content.Server.Mind.Commands;
using Content.Shared.Roles;
namespace Content.Server.Ghost.Roles.Components
namespace Content.Server.Ghost.Roles.Components;
[RegisterComponent]
[Access(typeof(GhostRoleSystem))]
public sealed partial class GhostRoleComponent : Component
{
[RegisterComponent]
[Access(typeof(GhostRoleSystem))]
public sealed partial class GhostRoleComponent : Component
[DataField("name")] private string _roleName = "Unknown";
[DataField("description")] private string _roleDescription = "Unknown";
[DataField("rules")] private string _roleRules = "ghost-role-component-default-rules";
// TODO ROLE TIMERS
// Actually make use of / enforce this requirement?
// Why is this even here.
// Move to ghost role prototype & respect CCvars.GameRoleTimerOverride
[DataField("requirements")]
public HashSet<JobRequirement>? Requirements;
/// <summary>
/// Whether the <see cref="MakeSentientCommand"/> should run on the mob.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField("makeSentient")]
public bool MakeSentient = true;
/// <summary>
/// The probability that this ghost role will be available after init.
/// Used mostly for takeover roles that want some probability of being takeover, but not 100%.
/// </summary>
[DataField("prob")]
public float Probability = 1f;
// We do this so updating RoleName and RoleDescription in VV updates the open EUIs.
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string RoleName
{
[DataField("name")] private string _roleName = "Unknown";
[DataField("description")] private string _roleDescription = "Unknown";
[DataField("rules")] private string _roleRules = "ghost-role-component-default-rules";
// TODO ROLE TIMERS
// Actually make use of / enforce this requirement?
// Why is this even here.
// Move to ghost role prototype & respect CCvars.GameRoleTimerOverride
[DataField("requirements")]
public HashSet<JobRequirement>? Requirements;
/// <summary>
/// Whether the <see cref="MakeSentientCommand"/> should run on the mob.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField("makeSentient")]
public bool MakeSentient = true;
/// <summary>
/// The probability that this ghost role will be available after init.
/// Used mostly for takeover roles that want some probability of being takeover, but not 100%.
/// </summary>
[DataField("prob")]
public float Probability = 1f;
// We do this so updating RoleName and RoleDescription in VV updates the open EUIs.
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string RoleName
get => Loc.GetString(_roleName);
set
{
get => Loc.GetString(_roleName);
set
{
_roleName = value;
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
}
_roleName = value;
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
}
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string RoleDescription
{
get => Loc.GetString(_roleDescription);
set
{
_roleDescription = value;
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
}
}
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string RoleRules
{
get => Loc.GetString(_roleRules);
set
{
_roleRules = value;
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
}
}
[DataField("allowSpeech")]
[ViewVariables(VVAccess.ReadWrite)]
public bool AllowSpeech { get; set; } = true;
[DataField("allowMovement")]
[ViewVariables(VVAccess.ReadWrite)]
public bool AllowMovement { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
public bool Taken { get; set; }
[ViewVariables]
public uint Identifier { get; set; }
/// <summary>
/// Reregisters the ghost role when the current player ghosts.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("reregister")]
public bool ReregisterOnGhost { get; set; } = true;
/// <summary>
/// If set, ghost role is raffled, otherwise it is first-come-first-serve.
/// </summary>
[DataField("raffle")]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public GhostRoleRaffleConfig? RaffleConfig { get; set; }
}
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string RoleDescription
{
get => Loc.GetString(_roleDescription);
set
{
_roleDescription = value;
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
}
}
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public string RoleRules
{
get => Loc.GetString(_roleRules);
set
{
_roleRules = value;
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
}
}
[DataField("allowSpeech")]
[ViewVariables(VVAccess.ReadWrite)]
public bool AllowSpeech { get; set; } = true;
[DataField("allowMovement")]
[ViewVariables(VVAccess.ReadWrite)]
public bool AllowMovement { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
public bool Taken { get; set; }
[ViewVariables]
public uint Identifier { get; set; }
/// <summary>
/// Reregisters the ghost role when the current player ghosts.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("reregister")]
public bool ReregisterOnGhost { get; set; } = true;
/// <summary>
/// If set, ghost role is raffled, otherwise it is first-come-first-serve.
/// </summary>
[DataField("raffle")]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public GhostRoleRaffleConfig? RaffleConfig { get; set; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,12 @@ namespace Content.Server.IP
public static bool IsInSubnet(this System.Net.IPAddress address, System.Net.IPAddress maskAddress, int maskLength)
{
if (maskAddress.AddressFamily != address.AddressFamily)
{
// We got something like an IPV4-Address for an IPv6-Mask. This is not valid.
return false;
}
if (maskAddress.AddressFamily == AddressFamily.InterNetwork)
{
// Convert the mask address to an unsigned integer.
@@ -89,7 +95,7 @@ namespace Content.Server.IP
if (maskAddressBits.Length != ipAddressBits.Length)
{
throw new ArgumentException("Length of IP Address and Subnet Mask do not match.");
return false;
}
// Compare the prefix bits.

View File

@@ -197,11 +197,11 @@ namespace Content.Server.Lathe
var recipe = component.Queue.First();
component.Queue.RemoveAt(0);
var time = _reagentSpeed.ApplySpeed(uid, recipe.CompleteTime);
var time = _reagentSpeed.ApplySpeed(uid, recipe.CompleteTime) * component.TimeMultiplier;
var lathe = EnsureComp<LatheProducingComponent>(uid);
lathe.StartTime = _timing.CurTime;
lathe.ProductionLength = time * component.TimeMultiplier;
lathe.ProductionLength = time;
component.CurrentRecipe = recipe;
var ev = new LatheStartPrintingEvent(recipe);
@@ -210,6 +210,11 @@ namespace Content.Server.Lathe
_audio.PlayPvs(component.ProducingSound, uid);
UpdateRunningAppearance(uid, true);
UpdateUserInterfaceState(uid, component);
if (time == TimeSpan.Zero)
{
FinishProducing(uid, component, lathe);
}
return true;
}

View File

@@ -1,11 +1,14 @@
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Access.Systems;
using Content.Server.Administration.Logs;
using Content.Server.CartridgeLoader;
using Content.Server.CartridgeLoader.Cartridges;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Access.Systems;
using Content.Server.Interaction;
using Content.Server.MassMedia.Components;
using Content.Server.Popups;
using Content.Server.Station.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.CartridgeLoader;
@@ -13,20 +16,18 @@ using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.Database;
using Content.Shared.MassMedia.Components;
using Content.Shared.MassMedia.Systems;
using Robust.Server.GameObjects;
using Content.Server.MassMedia.Components;
using Robust.Shared.Timing;
using Content.Server.Station.Systems;
using Content.Shared.Popups;
using Content.Shared.StationRecords;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Content.Server.Chat.Managers;
using Robust.Shared.Timing;
namespace Content.Server.MassMedia.Systems;
public sealed class NewsSystem : SharedNewsSystem
{
[Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly InteractionSystem _interaction = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoaderSystem = default!;
@@ -95,7 +96,7 @@ public sealed class NewsSystem : SharedNewsSystem
return;
var article = articles[msg.ArticleNum];
if (CheckDeleteAccess(article, ent, msg.Actor))
if (CanUse(msg.Actor, ent.Owner))
{
_adminLogger.Add(
LogType.Chat, LogImpact.Medium,
@@ -131,15 +132,15 @@ public sealed class NewsSystem : SharedNewsSystem
if (!ent.Comp.PublishEnabled)
return;
ent.Comp.PublishEnabled = false;
ent.Comp.NextPublish = _timing.CurTime + TimeSpan.FromSeconds(ent.Comp.PublishCooldown);
if (!TryGetArticles(ent, out var articles))
return;
if (!_accessReader.FindStationRecordKeys(msg.Actor, out _))
if (!CanUse(msg.Actor, ent.Owner))
return;
ent.Comp.PublishEnabled = false;
ent.Comp.NextPublish = _timing.CurTime + TimeSpan.FromSeconds(ent.Comp.PublishCooldown);
string? authorName = null;
if (_idCardSystem.TryFindIdCard(msg.Actor, out var idCard))
authorName = idCard.Comp.FullName;
@@ -305,21 +306,17 @@ public sealed class NewsSystem : SharedNewsSystem
}
}
private bool CheckDeleteAccess(NewsArticle articleToDelete, EntityUid device, EntityUid user)
private bool CanUse(EntityUid user, EntityUid console)
{
if (TryComp<AccessReaderComponent>(device, out var accessReader) &&
_accessReader.IsAllowed(user, device, accessReader))
return true;
// This shouldn't technically be possible because of BUI but don't trust client.
if (!_interaction.InRangeUnobstructed(console, user))
return false;
if (articleToDelete.AuthorStationRecordKeyIds == null || articleToDelete.AuthorStationRecordKeyIds.Count == 0)
return true;
return _accessReader.FindStationRecordKeys(user, out var recordKeys)
&& StationRecordsToNetEntities(recordKeys).Intersect(articleToDelete.AuthorStationRecordKeyIds).Any();
if (TryComp<AccessReaderComponent>(console, out var accessReaderComponent))
{
return _accessReaderSystem.IsAllowed(user, console, accessReaderComponent);
}
return true;
}
private ICollection<(NetEntity, uint)> StationRecordsToNetEntities(IEnumerable<StationRecordKey> records)
{
return records.Select(record => (GetNetEntity(record.OriginStation), record.Id)).ToList();
}
}

View File

@@ -20,19 +20,24 @@ using Robust.Shared.Player;
using Robust.Shared.Utility;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Repairable;
using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.Emag.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Materials;
/// <inheritdoc/>
public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly SharedBodySystem _body = default!; //bobby
[Dependency] private readonly PuddleSystem _puddle = default!;
[Dependency] private readonly StackSystem _stack = default!;
@@ -44,16 +49,14 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
{
base.Initialize();
SubscribeLocalEvent<MaterialReclaimerComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<MaterialReclaimerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<MaterialReclaimerComponent, InteractUsingEvent>(OnInteractUsing,
before: new []{typeof(WiresSystem), typeof(SolutionTransferSystem)});
before: [typeof(WiresSystem), typeof(SolutionTransferSystem)]);
SubscribeLocalEvent<MaterialReclaimerComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<ActiveMaterialReclaimerComponent, PowerChangedEvent>(OnActivePowerChanged);
}
private void OnStartup(Entity<MaterialReclaimerComponent> entity, ref ComponentStartup args)
{
_solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionContainerId);
SubscribeLocalEvent<MaterialReclaimerComponent, BreakageEventArgs>(OnBreakage);
SubscribeLocalEvent<MaterialReclaimerComponent, RepairedEvent>(OnRepaired);
}
private void OnPowerChanged(Entity<MaterialReclaimerComponent> entity, ref PowerChangedEvent args)
@@ -119,6 +122,30 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
TryFinishProcessItem(entity, null, entity.Comp);
}
private void OnBreakage(Entity<MaterialReclaimerComponent> ent, ref BreakageEventArgs args)
{
//un-emags itself when it breaks
RemComp<EmaggedComponent>(ent);
SetBroken(ent, true);
}
private void OnRepaired(Entity<MaterialReclaimerComponent> ent, ref RepairedEvent args)
{
SetBroken(ent, false);
}
public void SetBroken(Entity<MaterialReclaimerComponent> ent, bool val)
{
if (ent.Comp.Broken == val)
return;
_appearance.SetData(ent, RecyclerVisuals.Broken, val);
SetReclaimerEnabled(ent, false);
ent.Comp.Broken = val;
Dirty(ent);
}
/// <inheritdoc/>
public override bool TryFinishProcessItem(EntityUid uid, MaterialReclaimerComponent? component = null, ActiveMaterialReclaimerComponent? active = null)
{
@@ -136,7 +163,8 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
// scales the output if the process was interrupted.
var completion = 1f - Math.Clamp((float) Math.Round((active.EndTime - Timing.CurTime) / active.Duration),
0f, 1f);
0f,
1f);
Reclaim(uid, item, completion, component);
return true;
@@ -193,7 +221,8 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
foreach (var (storedMaterial, storedAmount) in storage.Storage)
{
var stacks = _materialStorage.SpawnMultipleFromMaterial(storedAmount, storedMaterial,
var stacks = _materialStorage.SpawnMultipleFromMaterial(storedAmount,
storedMaterial,
xform.Coordinates,
out var materialOverflow);
var amountConsumed = storedAmount - materialOverflow;
@@ -215,8 +244,6 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
{
if (!Resolve(reclaimer, ref reclaimerComponent, ref xform))
return;
if (!_solutionContainer.TryGetSolution(reclaimer, reclaimerComponent.SolutionContainerId, out var outputSolution))
return;
efficiency *= reclaimerComponent.Efficiency;
@@ -232,20 +259,14 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
}
// if the item we inserted has reagents, add it in.
if (TryComp<SolutionContainerManagerComponent>(item, out var solutionContainer))
if (_solutionContainer.TryGetDrainableSolution(item, out _, out var drainableSolution))
{
foreach (var (_, soln) in _solutionContainer.EnumerateSolutions((item, solutionContainer)))
{
var solution = soln.Comp.Solution;
foreach (var quantity in solution.Contents)
{
totalChemicals.AddReagent(quantity.Reagent.Prototype, quantity.Quantity * efficiency, false);
}
}
totalChemicals.AddSolution(drainableSolution, _prototype);
}
_solutionContainer.TryTransferSolution(outputSolution.Value, totalChemicals, totalChemicals.Volume);
if (totalChemicals.Volume > 0)
if (!_solutionContainer.TryGetSolution(reclaimer, reclaimerComponent.SolutionContainerId, out var outputSolution) ||
!_solutionContainer.TryTransferSolution(outputSolution.Value, totalChemicals, totalChemicals.Volume) ||
totalChemicals.Volume > 0)
{
_puddle.TrySpillAt(reclaimer, totalChemicals, out _, sound, transformComponent: xform);
}

View File

@@ -55,4 +55,10 @@ public sealed partial class HealthAnalyzerComponent : Component
/// </summary>
[DataField]
public SoundSpecifier? ScanningEndSound;
/// <summary>
/// Whether to show up the popup
/// </summary>
[DataField]
public bool Silent;
}

View File

@@ -91,8 +91,8 @@ public sealed class HealthAnalyzerSystem : EntitySystem
NeedHand = true,
BreakOnMove = true,
});
if (args.Target == args.User || doAfterCancelled)
if (args.Target == args.User || doAfterCancelled || uid.Comp.Silent)
return;
var msg = Loc.GetString("health-analyzer-popup-scan-target", ("user", Identity.Entity(args.User, EntityManager)));

View File

@@ -0,0 +1,27 @@
using Content.Server.NPC.Systems;
using Content.Shared.Actions;
using Robust.Shared.Prototypes;
namespace Content.Server.NPC.Components;
/// <summary>
/// This is used for an NPC that constantly tries to use an action on a given target.
/// </summary>
[RegisterComponent, Access(typeof(NPCUseActionOnTargetSystem))]
public sealed partial class NPCUseActionOnTargetComponent : Component
{
/// <summary>
/// HTN blackboard key for the target entity
/// </summary>
[DataField]
public string TargetKey = "Target";
/// <summary>
/// Action that's going to attempt to be used.
/// </summary>
[DataField(required: true)]
public EntProtoId<EntityWorldTargetActionComponent> ActionId;
[DataField]
public EntityUid? ActionEnt;
}

View File

@@ -34,6 +34,7 @@ public sealed partial class NPCBlackboard : IEnumerable<KeyValuePair<string, obj
{"RangedRange", 10f},
{"RotateSpeed", float.MaxValue},
{"VisionRadius", 10f},
{"AggroVisionRadius", 10f},
};
/// <summary>
@@ -269,6 +270,13 @@ public sealed partial class NPCBlackboard : IEnumerable<KeyValuePair<string, obj
return _blackboard.Remove(key);
}
public string GetVisionRadiusKey(IEntityManager entMan)
{
return TryGetValue<EntityUid>("Target", out _, entMan)
? AggroVisionRadius
: VisionRadius;
}
// I Ummd and Ahhd about using strings vs enums and decided on tags because
// if a fork wants to do their own thing they don't need to touch the enum.
@@ -317,9 +325,11 @@ public sealed partial class NPCBlackboard : IEnumerable<KeyValuePair<string, obj
public const string PathfindKey = "MovementPathfind";
public const string RotateSpeed = "RotateSpeed";
public const string VisionRadius = "VisionRadius";
public const string UtilityTarget = "UtilityTarget";
private const string VisionRadius = "VisionRadius";
private const string AggroVisionRadius = "AggroVisionRadius";
/// <summary>
/// A configurable "order" enum that can be given to an NPC from an external source.
/// </summary>

View File

@@ -1,5 +1,8 @@
using Content.Shared.Gravity;
using Content.Shared.Maps;
using Content.Shared.NPC;
using Robust.Shared.Map.Components;
using Robust.Shared.Spawners;
namespace Content.Server.NPC.Pathfinding;
@@ -44,8 +47,7 @@ public sealed partial class PathfindingSystem
var modifier = 1f;
// TODO
if ((end.Data.Flags & PathfindingBreadcrumbFlag.Space) != 0x0 &&
(!TryComp<GravityComponent>(end.GraphUid, out var gravity) || !gravity.Enabled))
if ((end.Data.Flags & PathfindingBreadcrumbFlag.Space) != 0x0)
{
return 0f;
}

View File

@@ -0,0 +1,68 @@
using Content.Server.NPC.Components;
using Content.Server.NPC.HTN;
using Content.Shared.Actions;
using Robust.Shared.Timing;
namespace Content.Server.NPC.Systems;
public sealed class NPCUseActionOnTargetSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NPCUseActionOnTargetComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<NPCUseActionOnTargetComponent> ent, ref MapInitEvent args)
{
ent.Comp.ActionEnt = _actions.AddAction(ent, ent.Comp.ActionId);
}
public bool TryUseTentacleAttack(Entity<NPCUseActionOnTargetComponent?> user, EntityUid target)
{
if (!Resolve(user, ref user.Comp, false))
return false;
if (!TryComp<EntityWorldTargetActionComponent>(user.Comp.ActionEnt, out var action))
return false;
if (!_actions.ValidAction(action))
return false;
if (action.Event != null)
{
action.Event.Performer = user;
action.Event.Action = user.Comp.ActionEnt.Value;
action.Event.Coords = Transform(target).Coordinates;
}
_actions.PerformAction(user,
null,
user.Comp.ActionEnt.Value,
action,
action.BaseEvent,
_timing.CurTime,
false);
return true;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// Tries to use the attack on the current target.
var query = EntityQueryEnumerator<NPCUseActionOnTargetComponent, HTNComponent>();
while (query.MoveNext(out var uid, out var comp, out var htn))
{
if (!htn.Blackboard.TryGetValue<EntityUid>(comp.TargetKey, out var target, EntityManager))
continue;
TryUseTentacleAttack((uid, comp), target);
}
}
}

View File

@@ -264,7 +264,7 @@ public sealed class NPCUtilitySystem : EntitySystem
}
case TargetDistanceCon:
{
var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager);
var radius = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager);
if (!TryComp(targetUid, out TransformComponent? targetXform) ||
!TryComp(owner, out TransformComponent? xform))
@@ -309,13 +309,13 @@ public sealed class NPCUtilitySystem : EntitySystem
}
case TargetInLOSCon:
{
var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager);
var radius = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager);
return _examine.InRangeUnOccluded(owner, targetUid, radius + 0.5f, null) ? 1f : 0f;
}
case TargetInLOSOrCurrentCon:
{
var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager);
var radius = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager);
const float bufferRange = 0.5f;
if (blackboard.TryGetValue<EntityUid>("Target", out var currentTarget, EntityManager) &&
@@ -375,7 +375,7 @@ public sealed class NPCUtilitySystem : EntitySystem
private void Add(NPCBlackboard blackboard, HashSet<EntityUid> entities, UtilityQuery query)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
var vision = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager);
var vision = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager);
switch (query)
{

View File

@@ -11,21 +11,27 @@ public sealed partial class SliceableFoodComponent : Component
/// Prototype to spawn after slicing.
/// If null then it can't be sliced.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public EntProtoId? Slice;
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Items/Culinary/chop.ogg");
/// <summary>
/// Number of slices the food starts with.
/// </summary>
[DataField("count"), ViewVariables(VVAccess.ReadWrite)]
[DataField("count")]
public ushort TotalCount = 5;
/// <summary>
/// Number of slices left.
/// how long it takes for this food to be sliced
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public ushort Count;
[DataField]
public float SliceTime = 1f;
/// <summary>
/// all the pieces will be shifted in random directions.
/// </summary>
[DataField]
public float SpawnOffset = 0.5f;
}

View File

@@ -1,10 +1,14 @@
using System.Numerics;
using System.Text;
using Content.Server.Nutrition.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Tag;
using Robust.Shared.Random;
namespace Content.Server.Nutrition.EntitySystems;
@@ -13,6 +17,9 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
@@ -42,27 +49,39 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
if (elementData is null)
return false;
if (TryComp<FoodComponent>(element, out var elementFood) && elementFood.RequireDead)
{
if (_mobState.IsAlive(element))
return false;
}
//if we run out of space, we can still put in one last, final finishing element.
if (start.Comp.FoodLayers.Count >= start.Comp.MaxLayers && !elementData.Value.Final || start.Comp.Finished)
if (start.Comp.FoodLayers.Count >= start.Comp.MaxLayers && !elementData.Final || start.Comp.Finished)
{
if (user is not null)
_popup.PopupEntity(Loc.GetString("food-sequence-no-space"), start, user.Value);
return false;
}
if (elementData.Value.Sprite is not null)
{
start.Comp.FoodLayers.Add(elementData.Value);
Dirty(start);
}
//If no specific sprites are specified, standard sprites will be used.
if (elementData.Sprite is null && element.Comp.Sprite is not null)
elementData.Sprite = element.Comp.Sprite;
if (elementData.Value.Final)
elementData.LocalOffset = new Vector2(
_random.NextFloat(start.Comp.MinLayerOffset.X,start.Comp.MaxLayerOffset.X),
_random.NextFloat(start.Comp.MinLayerOffset.Y,start.Comp.MaxLayerOffset.Y));
start.Comp.FoodLayers.Add(elementData);
Dirty(start);
if (elementData.Final)
start.Comp.Finished = true;
UpdateFoodName(start);
MergeFoodSolutions(start, element);
MergeFlavorProfiles(start, element);
MergeTrash(start, element);
MergeTags(start, element);
QueueDel(element);
return true;
}
@@ -81,12 +100,17 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
foreach (var layer in start.Comp.FoodLayers)
{
if (layer.Name is not null && !existedContentNames.Contains(layer.Name.Value))
{
content.Append(Loc.GetString(layer.Name.Value));
existedContentNames.Add(layer.Name.Value);
}
}
content.Append(separator);
var nameCounter = 1;
foreach (var name in existedContentNames)
{
content.Append(Loc.GetString(name));
if (nameCounter < existedContentNames.Count)
content.Append(separator);
nameCounter++;
}
var newName = Loc.GetString(start.Comp.NameGeneration.Value,
@@ -137,4 +161,14 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
startFood.Trash.Add(trash);
}
}
private void MergeTags(Entity<FoodSequenceStartPointComponent> start, Entity<FoodSequenceElementComponent> element)
{
if (!TryComp<TagComponent>(element, out var elementTags))
return;
EnsureComp<TagComponent>(start.Owner);
_tag.TryAddTags(start.Owner, elementTags.Tags);
}
}

View File

@@ -1,173 +1,175 @@
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.DoAfter;
using Content.Server.Nutrition.Components;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Examine;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
using Robust.Shared.Containers;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
namespace Content.Server.Nutrition.EntitySystems
namespace Content.Server.Nutrition.EntitySystems;
public sealed class SliceableFoodSystem : EntitySystem
{
public sealed class SliceableFoodSystem : EntitySystem
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
public override void Initialize()
{
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly TransformSystem _xformSystem = default!;
base.Initialize();
public override void Initialize()
SubscribeLocalEvent<SliceableFoodComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<SliceableFoodComponent, SliceFoodDoAfterEvent>(OnSlicedoAfter);
SubscribeLocalEvent<SliceableFoodComponent, ComponentStartup>(OnComponentStartup);
}
private void OnInteractUsing(Entity<SliceableFoodComponent> entity, ref InteractUsingEvent args)
{
if (args.Handled)
return;
var doAfterArgs = new DoAfterArgs(EntityManager,
args.User,
entity.Comp.SliceTime,
new SliceFoodDoAfterEvent(),
entity,
entity,
args.Used)
{
base.Initialize();
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
};
_doAfter.TryStartDoAfter(doAfterArgs);
}
SubscribeLocalEvent<SliceableFoodComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<SliceableFoodComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<SliceableFoodComponent, ComponentStartup>(OnComponentStartup);
}
private void OnSlicedoAfter(Entity<SliceableFoodComponent> entity, ref SliceFoodDoAfterEvent args)
{
if (args.Cancelled || args.Handled || args.Args.Target == null)
return;
private void OnInteractUsing(Entity<SliceableFoodComponent> entity, ref InteractUsingEvent args)
if (TrySliceFood(entity, args.User, args.Used, entity.Comp))
args.Handled = true;
}
private bool TrySliceFood(EntityUid uid,
EntityUid user,
EntityUid? usedItem,
SliceableFoodComponent? component = null,
FoodComponent? food = null,
TransformComponent? transform = null)
{
if (!Resolve(uid, ref component, ref food, ref transform) ||
string.IsNullOrEmpty(component.Slice))
return false;
if (!_solutionContainer.TryGetSolution(uid, food.Solution, out var soln, out var solution))
return false;
if (!TryComp<UtensilComponent>(usedItem, out var utensil) || (utensil.Types & UtensilType.Knife) == 0)
return false;
var sliceVolume = solution.Volume / FixedPoint2.New(component.TotalCount);
for (int i = 0; i < component.TotalCount; i++)
{
if (args.Handled)
return;
if (TrySliceFood(entity, args.User, args.Used, entity.Comp))
args.Handled = true;
}
private bool TrySliceFood(EntityUid uid, EntityUid user, EntityUid usedItem,
SliceableFoodComponent? component = null, FoodComponent? food = null, TransformComponent? transform = null)
{
if (!Resolve(uid, ref component, ref food, ref transform) ||
string.IsNullOrEmpty(component.Slice))
{
return false;
}
if (!_solutionContainerSystem.TryGetSolution(uid, food.Solution, out var soln, out var solution))
{
return false;
}
if (!TryComp<UtensilComponent>(usedItem, out var utensil) || (utensil.Types & UtensilType.Knife) == 0)
{
return false;
}
var sliceUid = Slice(uid, user, component, transform);
var lostSolution = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume / FixedPoint2.New(component.Count));
var lostSolution =
_solutionContainer.SplitSolution(soln.Value, sliceVolume);
// Fill new slice
FillSlice(sliceUid, lostSolution);
_audio.PlayPvs(component.Sound, transform.Coordinates, AudioParams.Default.WithVolume(-2));
var ev = new SliceFoodEvent();
RaiseLocalEvent(uid, ref ev);
// Decrease size of item based on count - Could implement in the future
// Bug with this currently is the size in a container is not updated
// if (TryComp(uid, out ItemComponent? itemComp) && TryComp(sliceUid, out ItemComponent? sliceComp))
// {
// itemComp.Size -= sliceComp.Size;
// }
component.Count--;
// If someone makes food proto with 1 slice...
if (component.Count < 1)
{
DeleteFood(uid, user, food);
return true;
}
// Split last slice
if (component.Count > 1)
return true;
sliceUid = Slice(uid, user, component, transform);
// Fill last slice with the rest of the solution
FillSlice(sliceUid, solution);
DeleteFood(uid, user, food);
return true;
}
/// <summary>
/// Create a new slice in the world and returns its entity.
/// The solutions must be set afterwards.
/// </summary>
public EntityUid Slice(EntityUid uid, EntityUid user, SliceableFoodComponent? comp = null, TransformComponent? transform = null)
_audio.PlayPvs(component.Sound, transform.Coordinates, AudioParams.Default.WithVolume(-2));
var ev = new SliceFoodEvent();
RaiseLocalEvent(uid, ref ev);
DeleteFood(uid, user, food);
return true;
}
/// <summary>
/// Create a new slice in the world and returns its entity.
/// The solutions must be set afterwards.
/// </summary>
public EntityUid Slice(EntityUid uid,
EntityUid user,
SliceableFoodComponent? comp = null,
TransformComponent? transform = null)
{
if (!Resolve(uid, ref comp, ref transform))
return EntityUid.Invalid;
var sliceUid = Spawn(comp.Slice, _transform.GetMapCoordinates(uid));
// try putting the slice into the container if the food being sliced is in a container!
// this lets you do things like slice a pizza up inside of a hot food cart without making a food-everywhere mess
_transform.DropNextTo(sliceUid, (uid, transform));
_transform.SetLocalRotation(sliceUid, 0);
if (!_container.IsEntityOrParentInContainer(sliceUid))
{
if (!Resolve(uid, ref comp, ref transform))
return EntityUid.Invalid;
var sliceUid = Spawn(comp.Slice, _xformSystem.GetMapCoordinates(uid));
// try putting the slice into the container if the food being sliced is in a container!
// this lets you do things like slice a pizza up inside of a hot food cart without making a food-everywhere mess
_xformSystem.DropNextTo(sliceUid, (uid, transform));
_xformSystem.SetLocalRotation(sliceUid, 0);
return sliceUid;
var randVect = _random.NextVector2(2.0f, 2.5f);
if (TryComp<PhysicsComponent>(sliceUid, out var physics))
_physics.SetLinearVelocity(sliceUid, randVect, body: physics);
}
private void DeleteFood(EntityUid uid, EntityUid user, FoodComponent foodComp)
return sliceUid;
}
private void DeleteFood(EntityUid uid, EntityUid user, FoodComponent foodComp)
{
var ev = new BeforeFullySlicedEvent
{
var ev = new BeforeFullySlicedEvent
{
User = user
};
RaiseLocalEvent(uid, ev);
if (ev.Cancelled)
return;
User = user
};
RaiseLocalEvent(uid, ev);
if (ev.Cancelled)
return;
if (foodComp.Trash.Count == 0)
{
QueueDel(uid);
return;
}
// Locate the sliced food and spawn its trash
foreach (var trash in foodComp.Trash)
{
var trashUid = Spawn(trash, _transform.GetMapCoordinates(uid));
// Locate the sliced food and spawn its trash
foreach (var trash in foodComp.Trash)
{
var trashUid = Spawn(trash, _xformSystem.GetMapCoordinates(uid));
// try putting the trash in the food's container too, to be consistent with slice spawning?
_xformSystem.DropNextTo(trashUid, uid);
_xformSystem.SetLocalRotation(trashUid, 0);
}
QueueDel(uid);
// try putting the trash in the food's container too, to be consistent with slice spawning?
_transform.DropNextTo(trashUid, uid);
_transform.SetLocalRotation(trashUid, 0);
}
private void FillSlice(EntityUid sliceUid, Solution solution)
QueueDel(uid);
}
private void FillSlice(EntityUid sliceUid, Solution solution)
{
// Replace all reagents on prototype not just copying poisons (example: slices of eaten pizza should have less nutrition)
if (TryComp<FoodComponent>(sliceUid, out var sliceFoodComp) &&
_solutionContainer.TryGetSolution(sliceUid, sliceFoodComp.Solution, out var itsSoln, out var itsSolution))
{
// Replace all reagents on prototype not just copying poisons (example: slices of eaten pizza should have less nutrition)
if (TryComp<FoodComponent>(sliceUid, out var sliceFoodComp) &&
_solutionContainerSystem.TryGetSolution(sliceUid, sliceFoodComp.Solution, out var itsSoln, out var itsSolution))
{
_solutionContainerSystem.RemoveAllSolution(itsSoln.Value);
_solutionContainer.RemoveAllSolution(itsSoln.Value);
var lostSolutionPart = solution.SplitSolution(itsSolution.AvailableVolume);
_solutionContainerSystem.TryAddSolution(itsSoln.Value, lostSolutionPart);
}
}
private void OnComponentStartup(Entity<SliceableFoodComponent> entity, ref ComponentStartup args)
{
entity.Comp.Count = entity.Comp.TotalCount;
var foodComp = EnsureComp<FoodComponent>(entity);
_solutionContainerSystem.EnsureSolution(entity.Owner, foodComp.Solution);
}
private void OnExamined(Entity<SliceableFoodComponent> entity, ref ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("sliceable-food-component-on-examine-remaining-slices-text", ("remainingCount", entity.Comp.Count)));
var lostSolutionPart = solution.SplitSolution(itsSolution.AvailableVolume);
_solutionContainer.TryAddSolution(itsSoln.Value, lostSolutionPart);
}
}
private void OnComponentStartup(Entity<SliceableFoodComponent> entity, ref ComponentStartup args)
{
var foodComp = EnsureComp<FoodComponent>(entity);
_solutionContainer.EnsureSolution(entity.Owner, foodComp.Solution);
}
}

View File

@@ -623,14 +623,14 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
var groupSize = rand.Next(layerProto.MinGroupSize, layerProto.MaxGroupSize + 1);
// While we have remaining tiles keep iterating
while (groupSize >= 0 && remainingTiles.Count > 0)
while (groupSize > 0 && remainingTiles.Count > 0)
{
var startNode = rand.PickAndTake(remainingTiles);
frontier.Clear();
frontier.Add(startNode);
// This essentially may lead to a vein being split in multiple areas but the count matters more than position.
while (frontier.Count > 0 && groupSize >= 0)
while (frontier.Count > 0 && groupSize > 0)
{
// Need to pick a random index so we don't just get straight lines of ores.
var frontierIndex = rand.Next(frontier.Count);
@@ -643,9 +643,6 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
{
for (var y = -1; y <= 1; y++)
{
if (x != 0 && y != 0)
continue;
var neighbor = new Vector2i(node.X + x, node.Y + y);
if (frontier.Contains(neighbor) || !remainingTiles.Contains(neighbor))

View File

@@ -3,6 +3,7 @@ using Content.Server.DeviceLinking.Systems;
using Content.Server.Materials;
using Content.Server.Power.Components;
using Content.Shared.Conveyor;
using Content.Shared.Destructible;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Physics.Controllers;
@@ -26,7 +27,7 @@ public sealed class ConveyorController : SharedConveyorController
UpdatesAfter.Add(typeof(MoverController));
SubscribeLocalEvent<ConveyorComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<ConveyorComponent, ComponentShutdown>(OnConveyorShutdown);
SubscribeLocalEvent<ConveyorComponent, BreakageEventArgs>(OnBreakage);
SubscribeLocalEvent<ConveyorComponent, SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<ConveyorComponent, PowerChangedEvent>(OnPowerChanged);
@@ -61,6 +62,11 @@ public sealed class ConveyorController : SharedConveyorController
_fixtures.DestroyFixture(uid, ConveyorFixture, body: physics);
}
private void OnBreakage(Entity<ConveyorComponent> ent, ref BreakageEventArgs args)
{
SetState(ent, ConveyorState.Off, ent);
}
private void OnPowerChanged(EntityUid uid, ConveyorComponent component, ref PowerChangedEvent args)
{
component.Powered = args.Powered;
@@ -96,13 +102,14 @@ public sealed class ConveyorController : SharedConveyorController
if (!Resolve(uid, ref component))
return;
if (!_materialReclaimer.SetReclaimerEnabled(uid, state != ConveyorState.Off))
return;
component.State = state;
if (TryComp<PhysicsComponent>(uid, out var physics))
_broadphase.RegenerateContacts(uid, physics);
_materialReclaimer.SetReclaimerEnabled(uid, component.State != ConveyorState.Off);
UpdateAppearance(uid, component);
Dirty(uid, component);
}

View File

@@ -33,13 +33,13 @@ namespace Content.Server.Physics.Controllers
private void OnRelayPlayerAttached(Entity<RelayInputMoverComponent> entity, ref PlayerAttachedEvent args)
{
if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
SetMoveInput((entity.Owner, inputMover), MoveButtons.None);
SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
}
private void OnRelayPlayerDetached(Entity<RelayInputMoverComponent> entity, ref PlayerDetachedEvent args)
{
if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
SetMoveInput((entity.Owner, inputMover), MoveButtons.None);
SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
}
private void OnPlayerAttached(Entity<InputMoverComponent> entity, ref PlayerAttachedEvent args)

View File

@@ -88,14 +88,14 @@ public sealed partial class DungeonJob
var groupSize = random.Next(gen.MinGroupSize, gen.MaxGroupSize + 1);
// While we have remaining tiles keep iterating
while (groupSize >= 0 && availableTiles.Count > 0)
while (groupSize > 0 && availableTiles.Count > 0)
{
var startNode = random.PickAndTake(availableTiles);
frontier.Clear();
frontier.Add(startNode);
// This essentially may lead to a vein being split in multiple areas but the count matters more than position.
while (frontier.Count > 0 && groupSize >= 0)
while (frontier.Count > 0 && groupSize > 0)
{
// Need to pick a random index so we don't just get straight lines of ores.
var frontierIndex = random.Next(frontier.Count);
@@ -108,9 +108,6 @@ public sealed partial class DungeonJob
{
for (var y = -1; y <= 1; y++)
{
if (x != 0 && y != 0)
continue;
var neighbor = new Vector2i(node.X + x, node.Y + y);
if (frontier.Contains(neighbor) || !availableTiles.Contains(neighbor))
@@ -142,7 +139,7 @@ public sealed partial class DungeonJob
if (groupSize > 0)
{
_sawmill.Warning($"Found remaining group size for ore veins!");
_sawmill.Warning($"Found remaining group size for ore veins of {gen.Replacement ?? "null"}!");
}
}
}

View File

@@ -44,7 +44,7 @@ public sealed partial class DungeonJob : Job<List<Dungeon>>
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<TransformComponent> _xformQuery;
private readonly DungeonConfigPrototype _gen;
private readonly DungeonConfig _gen;
private readonly int _seed;
private readonly Vector2i _position;
@@ -65,7 +65,7 @@ public sealed partial class DungeonJob : Job<List<Dungeon>>
EntityLookupSystem lookup,
TileSystem tile,
SharedTransformSystem transform,
DungeonConfigPrototype gen,
DungeonConfig gen,
MapGridComponent grid,
EntityUid gridUid,
int seed,
@@ -102,7 +102,7 @@ public sealed partial class DungeonJob : Job<List<Dungeon>>
/// <param name="reserve">Should we reserve tiles even if the config doesn't specify.</param>
private async Task<List<Dungeon>> GetDungeons(
Vector2i position,
DungeonConfigPrototype config,
DungeonConfig config,
DungeonData data,
List<IDunGenLayer> layers,
HashSet<Vector2i> reservedTiles,
@@ -139,7 +139,7 @@ public sealed partial class DungeonJob : Job<List<Dungeon>>
protected override async Task<List<Dungeon>?> Process()
{
_sawmill.Info($"Generating dungeon {_gen.ID} with seed {_seed} on {_entManager.ToPrettyString(_gridUid)}");
_sawmill.Info($"Generating dungeon {_gen} with seed {_seed} on {_entManager.ToPrettyString(_gridUid)}");
_grid.CanSplit = false;
var random = new Random(_seed);
var position = (_position + random.NextPolarVector2(_gen.MinOffset, _gen.MaxOffset)).Floored();
@@ -177,7 +177,7 @@ public sealed partial class DungeonJob : Job<List<Dungeon>>
int seed,
Random random)
{
_sawmill.Debug($"Doing postgen {layer.GetType()} for {_gen.ID} with seed {_seed}");
_sawmill.Debug($"Doing postgen {layer.GetType()} for {_gen} with seed {_seed}");
// If there's a way to just call the methods directly for the love of god tell me.
// Some of these don't care about reservedtiles because they only operate on dungeon tiles (which should

View File

@@ -183,7 +183,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
return mapId;
}
public void GenerateDungeon(DungeonConfigPrototype gen,
public void GenerateDungeon(DungeonConfig gen,
EntityUid gridUid,
MapGridComponent grid,
Vector2i position,
@@ -214,7 +214,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
}
public async Task<List<Dungeon>> GenerateDungeonAsync(
DungeonConfigPrototype gen,
DungeonConfig gen,
EntityUid gridUid,
MapGridComponent grid,
Vector2i position,

View File

@@ -46,6 +46,9 @@ namespace Content.Server.Repairable
("target", uid),
("tool", args.Used!));
_popup.PopupEntity(str, uid, args.User);
var ev = new RepairedEvent((uid, component), args.User);
RaiseLocalEvent(uid, ref ev);
}
public async void Repair(EntityUid uid, RepairableComponent component, InteractUsingEvent args)
@@ -72,4 +75,13 @@ namespace Content.Server.Repairable
args.Handled = _toolSystem.UseTool(args.Used, args.User, uid, delay, component.QualityNeeded, new RepairFinishedEvent(), component.FuelCost);
}
}
/// <summary>
/// Event raised on an entity when its successfully repaired.
/// </summary>
/// <param name="Ent"></param>
/// <param name="User"></param>
[ByRefEvent]
public readonly record struct RepairedEvent(Entity<RepairableComponent> Ent, EntityUid User);
}

View File

@@ -1,7 +1,12 @@
using Content.Server.Popups;
using Content.Shared.Popups;
using Content.Shared.ActionBlocker;
using Content.Shared.Input;
using Content.Shared.Interaction;
using Content.Shared.Rotatable;
using Content.Shared.Verbs;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Utility;
@@ -14,11 +19,19 @@ namespace Content.Server.Rotatable
public sealed class RotatableSystem : EntitySystem
{
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
public override void Initialize()
{
SubscribeLocalEvent<FlippableComponent, GetVerbsEvent<Verb>>(AddFlipVerb);
SubscribeLocalEvent<RotatableComponent, GetVerbsEvent<Verb>>(AddRotateVerbs);
CommandBinds.Builder
.Bind(ContentKeyFunctions.RotateObjectClockwise, new PointerInputCmdHandler(HandleRotateObjectClockwise))
.Bind(ContentKeyFunctions.RotateObjectCounterclockwise, new PointerInputCmdHandler(HandleRotateObjectCounterclockwise))
.Bind(ContentKeyFunctions.FlipObject, new PointerInputCmdHandler(HandleFlipObject))
.Register<RotatableSystem>();
}
private void AddFlipVerb(EntityUid uid, FlippableComponent component, GetVerbsEvent<Verb> args)
@@ -26,12 +39,16 @@ namespace Content.Server.Rotatable
if (!args.CanAccess || !args.CanInteract)
return;
// Check if the object is anchored.
if (EntityManager.TryGetComponent(uid, out PhysicsComponent? physics) && physics.BodyType == BodyType.Static)
return;
Verb verb = new()
{
Act = () => TryFlip(uid, component, args.User),
Act = () => Flip(uid, component),
Text = Loc.GetString("flippable-verb-get-data-text"),
Category = VerbCategory.Rotate,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/flip.svg.192dpi.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/flip.svg.192dpi.png")),
Priority = -3, // show flip last
DoContactInteraction = true
};
@@ -51,12 +68,12 @@ namespace Content.Server.Rotatable
physics.BodyType == BodyType.Static)
return;
Verb resetRotation = new ()
Verb resetRotation = new()
{
DoContactInteraction = true,
Act = () => EntityManager.GetComponent<TransformComponent>(uid).LocalRotation = Angle.Zero,
Category = VerbCategory.Rotate,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
Text = "Reset",
Priority = -2, // show CCW, then CW, then reset
CloseMenu = false,
@@ -68,7 +85,7 @@ namespace Content.Server.Rotatable
{
Act = () => EntityManager.GetComponent<TransformComponent>(uid).LocalRotation -= component.Increment,
Category = VerbCategory.Rotate,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png")),
Priority = -1,
CloseMenu = false, // allow for easy double rotations.
};
@@ -79,7 +96,7 @@ namespace Content.Server.Rotatable
{
Act = () => EntityManager.GetComponent<TransformComponent>(uid).LocalRotation += component.Increment,
Category = VerbCategory.Rotate,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png")),
Priority = 0,
CloseMenu = false, // allow for easy double rotations.
};
@@ -89,15 +106,8 @@ namespace Content.Server.Rotatable
/// <summary>
/// Replace a flippable entity with it's flipped / mirror-symmetric entity.
/// </summary>
public void TryFlip(EntityUid uid, FlippableComponent component, EntityUid user)
public void Flip(EntityUid uid, FlippableComponent component)
{
if (EntityManager.TryGetComponent(uid, out PhysicsComponent? physics) &&
physics.BodyType == BodyType.Static)
{
_popup.PopupEntity(Loc.GetString("flippable-component-try-flip-is-stuck"), uid, user);
return;
}
var oldTransform = EntityManager.GetComponent<TransformComponent>(uid);
var entity = EntityManager.SpawnEntity(component.MirrorEntity, oldTransform.Coordinates);
var newTransform = EntityManager.GetComponent<TransformComponent>(entity);
@@ -105,5 +115,73 @@ namespace Content.Server.Rotatable
newTransform.Anchored = false;
EntityManager.DeleteEntity(uid);
}
public bool HandleRotateObjectClockwise(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity)
{
if (playerSession?.AttachedEntity is not { Valid: true } player || !Exists(player))
return false;
if (!TryComp<RotatableComponent>(entity, out var rotatableComp))
return false;
if (!_actionBlocker.CanInteract(player, entity) || !_interaction.InRangeAndAccessible(player, entity))
return false;
// Check if the object is anchored, and whether we are still allowed to rotate it.
if (!rotatableComp.RotateWhileAnchored && EntityManager.TryGetComponent(entity, out PhysicsComponent? physics) &&
physics.BodyType == BodyType.Static)
{
_popup.PopupEntity(Loc.GetString("rotatable-component-try-rotate-stuck"), entity, player);
return false;
}
Transform(entity).LocalRotation -= rotatableComp.Increment;
return true;
}
public bool HandleRotateObjectCounterclockwise(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity)
{
if (playerSession?.AttachedEntity is not { Valid: true } player || !Exists(player))
return false;
if (!TryComp<RotatableComponent>(entity, out var rotatableComp))
return false;
if (!_actionBlocker.CanInteract(player, entity) || !_interaction.InRangeAndAccessible(player, entity))
return false;
// Check if the object is anchored, and whether we are still allowed to rotate it.
if (!rotatableComp.RotateWhileAnchored && EntityManager.TryGetComponent(entity, out PhysicsComponent? physics) &&
physics.BodyType == BodyType.Static)
{
_popup.PopupEntity(Loc.GetString("rotatable-component-try-rotate-stuck"), entity, player);
return false;
}
Transform(entity).LocalRotation += rotatableComp.Increment;
return true;
}
public bool HandleFlipObject(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity)
{
if (playerSession?.AttachedEntity is not { Valid: true } player || !Exists(player))
return false;
if (!TryComp<FlippableComponent>(entity, out var flippableComp))
return false;
if (!_actionBlocker.CanInteract(player, entity) || !_interaction.InRangeAndAccessible(player, entity))
return false;
// Check if the object is anchored.
if (EntityManager.TryGetComponent(entity, out PhysicsComponent? physics) && physics.BodyType == BodyType.Static)
{
_popup.PopupEntity(Loc.GetString("flippable-component-try-flip-is-stuck"), entity, player);
return false;
}
Flip(entity, flippableComp);
return true;
}
}
}

View File

@@ -175,7 +175,7 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
var dungeonOffset = new Vector2(0f, dungeonOffsetDistance);
dungeonOffset = dungeonRotation.RotateVec(dungeonOffset);
var dungeonMod = _prototypeManager.Index<SalvageDungeonModPrototype>(mission.Dungeon);
var dungeonConfig = _prototypeManager.Index<DungeonConfigPrototype>(dungeonMod.Proto);
var dungeonConfig = _prototypeManager.Index(dungeonMod.Proto);
var dungeons = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i) dungeonOffset,
_missionParams.Seed));

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Components;
using Content.Server.Popups;
using Content.Server.Power.Components;
@@ -103,7 +103,7 @@ public sealed class RadiationCollectorSystem : EntitySystem
if (gas.Byproduct != null)
{
gasTankComponent.Air.AdjustMoles((int) gas.Byproduct, delta * gas.MolarRatio);
gasTankComponent.Air.AdjustMoles((int)gas.Byproduct, delta * gas.MolarRatio);
}
}
@@ -204,13 +204,14 @@ public sealed class RadiationCollectorSystem : EntitySystem
if (!Resolve(uid, ref appearance, false))
return;
// gas canisters can fill tanks up to 10 atm, so we set the warning level thresholds 1/3 and 2/3 of that
if (gasTank == null || gasTank.Air.Pressure < 10)
_appearance.SetData(uid, RadiationCollectorVisuals.PressureState, 0, appearance);
else if (gasTank.Air.Pressure < Atmospherics.OneAtmosphere)
else if (gasTank.Air.Pressure < 3.33f * Atmospherics.OneAtmosphere)
_appearance.SetData(uid, RadiationCollectorVisuals.PressureState, 1, appearance);
else if (gasTank.Air.Pressure < 3f * Atmospherics.OneAtmosphere)
else if (gasTank.Air.Pressure < 6.66f * Atmospherics.OneAtmosphere)
_appearance.SetData(uid, RadiationCollectorVisuals.PressureState, 2, appearance);
else

View File

@@ -0,0 +1,4 @@
namespace Content.Server.Speech.Components;
[RegisterComponent]
public sealed partial class GermanAccentComponent : Component;

View File

@@ -0,0 +1,85 @@
using System.Text;
using Content.Server.Speech.Components;
using Robust.Shared.Random;
using System.Text.RegularExpressions;
namespace Content.Server.Speech.EntitySystems;
public sealed class GermanAccentSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ReplacementAccentSystem _replacement = default!;
private static readonly Regex RegexTh = new(@"(?<=\s|^)th", RegexOptions.IgnoreCase);
private static readonly Regex RegexThe = new(@"(?<=\s|^)the(?=\s|$)", RegexOptions.IgnoreCase);
public override void Initialize()
{
SubscribeLocalEvent<GermanAccentComponent, AccentGetEvent>(OnAccent);
}
public string Accentuate(string message)
{
var msg = message;
// rarely, "the" should become "das" instead of "ze"
// TODO: The ReplacementAccentSystem should have random replacements this built-in.
foreach (Match match in RegexThe.Matches(msg))
{
if (_random.Prob(0.3f))
{
// just shift T, H and E over to D, A and S to preserve capitalization
msg = msg.Substring(0, match.Index) +
(char)(msg[match.Index] - 16) +
(char)(msg[match.Index + 1] - 7) +
(char)(msg[match.Index + 2] + 14) +
msg.Substring(match.Index + 3);
}
}
// now, apply word replacements
msg = _replacement.ApplyReplacements(msg, "german");
// replace th with zh (for zhis, zhat, etc. the => ze is handled by replacements already)
var msgBuilder = new StringBuilder(msg);
foreach (Match match in RegexTh.Matches(msg))
{
// just shift the T over to a Z to preserve capitalization
msgBuilder[match.Index] = (char) (msgBuilder[match.Index] + 6);
}
// Random Umlaut Time! (The joke outweighs the emotional damage this inflicts on actual Germans)
var umlautCooldown = 0;
for (var i = 0; i < msgBuilder.Length; i++)
{
if (umlautCooldown == 0)
{
if (_random.Prob(0.1f)) // 10% of all eligible vowels become umlauts)
{
msgBuilder[i] = msgBuilder[i] switch
{
'A' => 'Ä',
'a' => 'ä',
'O' => 'Ö',
'o' => 'ö',
'U' => 'Ü',
'u' => 'ü',
_ => msgBuilder[i]
};
umlautCooldown = 4;
}
}
else
{
umlautCooldown--;
}
}
return msgBuilder.ToString();
}
private void OnAccent(Entity<GermanAccentComponent> ent, ref AccentGetEvent args)
{
args.Message = Accentuate(args.Message);
}
}

View File

@@ -2,11 +2,12 @@ using System.Linq;
using Content.Server.Administration;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Content.Shared.Administration;
using Content.Shared.EntityTable;
using Content.Shared.GameTicking.Components;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
@@ -23,13 +24,17 @@ namespace Content.Server.StationEvents
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EventManagerSystem _event = default!;
public const float MinEventTime = 60 * 3;
public const float MaxEventTime = 60 * 10;
protected override void Started(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule,
GameRuleStartedEvent args)
{
// A little starting variance so schedulers dont all proc at once.
component.TimeUntilNextEvent = RobustRandom.NextFloat(component.MinimumTimeUntilFirstEvent, component.MinimumTimeUntilFirstEvent + 120);
}
protected override void Ended(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule,
GameRuleEndedEvent args)
{
component.TimeUntilNextEvent = BasicStationEventSchedulerComponent.MinimumTimeUntilFirstEvent;
component.TimeUntilNextEvent = component.MinimumTimeUntilFirstEvent;
}
@@ -49,10 +54,10 @@ namespace Content.Server.StationEvents
if (eventScheduler.TimeUntilNextEvent > 0)
{
eventScheduler.TimeUntilNextEvent -= frameTime;
return;
continue;
}
_event.RunRandomEvent();
_event.RunRandomEvent(eventScheduler.ScheduledGameRules);
ResetTimer(eventScheduler);
}
}
@@ -62,7 +67,7 @@ namespace Content.Server.StationEvents
/// </summary>
private void ResetTimer(BasicStationEventSchedulerComponent component)
{
component.TimeUntilNextEvent = _random.NextFloat(MinEventTime, MaxEventTime);
component.TimeUntilNextEvent = component.MinMaxEventTiming.Next(_random);
}
}
@@ -70,7 +75,8 @@ namespace Content.Server.StationEvents
public sealed class StationEventCommand : ToolshedCommand
{
private EventManagerSystem? _stationEvent;
private BasicStationEventSchedulerSystem? _basicScheduler;
private EntityTableSystem? _entityTable;
private IComponentFactory? _compFac;
private IRobustRandom? _random;
/// <summary>
@@ -88,10 +94,11 @@ namespace Content.Server.StationEvents
/// to even exist) so I think it's fine.
/// </remarks>
[CommandImplementation("simulate")]
public IEnumerable<(string, float)> Simulate([CommandArgument] int rounds, [CommandArgument] int playerCount, [CommandArgument] float roundEndMean, [CommandArgument] float roundEndStdDev)
public IEnumerable<(string, float)> Simulate([CommandArgument] EntityPrototype eventScheduler, [CommandArgument] int rounds, [CommandArgument] int playerCount, [CommandArgument] float roundEndMean, [CommandArgument] float roundEndStdDev)
{
_stationEvent ??= GetSys<EventManagerSystem>();
_basicScheduler ??= GetSys<BasicStationEventSchedulerSystem>();
_entityTable ??= GetSys<EntityTableSystem>();
_compFac ??= IoCManager.Resolve<IComponentFactory>();
_random ??= IoCManager.Resolve<IRobustRandom>();
var occurrences = new Dictionary<string, int>();
@@ -101,6 +108,13 @@ namespace Content.Server.StationEvents
occurrences.Add(ev.Key.ID, 0);
}
if (!eventScheduler.TryGetComponent<BasicStationEventSchedulerComponent>(out var basicScheduler, _compFac))
{
return occurrences.Select(p => (p.Key, (float)p.Value)).OrderByDescending(p => p.Item2);
}
var compMinMax = basicScheduler.MinMaxEventTiming; // we gotta do this since we cant execute on comp w/o an ent.
for (var i = 0; i < rounds; i++)
{
var curTime = TimeSpan.Zero;
@@ -111,9 +125,16 @@ namespace Content.Server.StationEvents
while (curTime.TotalSeconds < randomEndTime)
{
// sim an event
curTime += TimeSpan.FromSeconds(_random.NextFloat(BasicStationEventSchedulerSystem.MinEventTime, BasicStationEventSchedulerSystem.MaxEventTime));
curTime += TimeSpan.FromSeconds(compMinMax.Next(_random));
if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var selectedEvents))
{
continue; // doesnt break because maybe the time is preventing events being available.
}
var available = _stationEvent.AvailableEvents(false, playerCount, curTime);
var ev = _stationEvent.FindEvent(available);
var plausibleEvents = new Dictionary<EntityPrototype, StationEventComponent>(available.Intersect(selectedEvents)); // C# makes me sad
var ev = _stationEvent.FindEvent(plausibleEvents);
if (ev == null)
continue;
@@ -125,26 +146,40 @@ namespace Content.Server.StationEvents
}
[CommandImplementation("lsprob")]
public IEnumerable<(string, float)> LsProb()
public IEnumerable<(string, float)> LsProb([CommandArgument] EntityPrototype eventScheduler)
{
_compFac ??= IoCManager.Resolve<IComponentFactory>();
_stationEvent ??= GetSys<EventManagerSystem>();
var events = _stationEvent.AllEvents();
var totalWeight = events.Sum(x => x.Value.Weight);
if (!eventScheduler.TryGetComponent<BasicStationEventSchedulerComponent>(out var basicScheduler, _compFac))
yield break;
foreach (var (proto, comp) in events)
if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var events))
yield break;
var totalWeight = events.Sum(x => x.Value.Weight); // Well this shit definitely isnt correct now, and I see no way to make it correct.
// Its probably *fine* but it wont be accurate if the EntityTableSelector does any subsetting.
foreach (var (proto, comp) in events) // The only solution I see is to do a simulation, and we already have that, so...!
{
yield return (proto.ID, comp.Weight / totalWeight);
}
}
[CommandImplementation("lsprobtime")]
public IEnumerable<(string, float)> LsProbTime([CommandArgument] float time)
public IEnumerable<(string, float)> LsProbTime([CommandArgument] EntityPrototype eventScheduler, [CommandArgument] float time)
{
_compFac ??= IoCManager.Resolve<IComponentFactory>();
_stationEvent ??= GetSys<EventManagerSystem>();
var events = _stationEvent.AllEvents().Where(pair => pair.Value.EarliestStart <= time).ToList();
var totalWeight = events.Sum(x => x.Value.Weight);
if (!eventScheduler.TryGetComponent<BasicStationEventSchedulerComponent>(out var basicScheduler, _compFac))
yield break;
if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var untimedEvents))
yield break;
var events = untimedEvents.Where(pair => pair.Value.EarliestStart <= time).ToList();
var totalWeight = events.Sum(x => x.Value.Weight); // same subsetting issue as lsprob.
foreach (var (proto, comp) in events)
{
@@ -153,12 +188,18 @@ namespace Content.Server.StationEvents
}
[CommandImplementation("prob")]
public float Prob([CommandArgument] string eventId)
public float Prob([CommandArgument] EntityPrototype eventScheduler, [CommandArgument] string eventId)
{
_compFac ??= IoCManager.Resolve<IComponentFactory>();
_stationEvent ??= GetSys<EventManagerSystem>();
var events = _stationEvent.AllEvents();
var totalWeight = events.Sum(x => x.Value.Weight);
if (!eventScheduler.TryGetComponent<BasicStationEventSchedulerComponent>(out var basicScheduler, _compFac))
return 0f;
if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var events))
return 0f;
var totalWeight = events.Sum(x => x.Value.Weight); // same subsetting issue as lsprob.
var weight = 0f;
if (events.TryFirstOrNull(p => p.Key.ID == eventId, out var pair))
{

View File

@@ -1,14 +1,35 @@
namespace Content.Server.StationEvents.Components;
using Content.Shared.Destructible.Thresholds;
using Content.Shared.EntityTable.EntitySelectors;
namespace Content.Server.StationEvents.Components;
[RegisterComponent, Access(typeof(BasicStationEventSchedulerSystem))]
public sealed partial class BasicStationEventSchedulerComponent : Component
{
public const float MinimumTimeUntilFirstEvent = 300;
/// <summary>
/// How long the the scheduler waits to begin starting rules.
/// </summary>
[DataField]
public float MinimumTimeUntilFirstEvent = 200;
/// <summary>
/// How long until the next check for an event runs
/// The minimum and maximum time between rule starts in seconds.
/// </summary>
/// Default value is how long until first event is allowed
[ViewVariables(VVAccess.ReadWrite)]
public float TimeUntilNextEvent = MinimumTimeUntilFirstEvent;
[DataField]
public MinMax MinMaxEventTiming = new(3 * 60, 10 * 60);
/// <summary>
/// How long until the next check for an event runs, is initially set based on MinimumTimeUntilFirstEvent & MinMaxEventTiming.
/// </summary>
[DataField]
public float TimeUntilNextEvent;
/// <summary>
/// The gamerules that the scheduler can choose from
/// </summary>
/// Reminder that though we could do all selection via the EntityTableSelector, we also need to consider various <see cref="StationEventComponent"/> restrictions.
/// As such, we want to pass a list of acceptable game rules, which are then parsed for restrictions by the <see cref="EventManagerSystem"/>.
[DataField(required: true)]
public EntityTableSelector ScheduledGameRules = default!;
}

View File

@@ -1,24 +0,0 @@
using Content.Shared.Random;
using Robust.Shared.Prototypes;
namespace Content.Server.StationEvents.Components;
/// <summary>
/// This is used for running meteor swarm events at regular intervals.
/// </summary>
[RegisterComponent, Access(typeof(MeteorSchedulerSystem)), AutoGenerateComponentPause]
public sealed partial class MeteorSchedulerComponent : Component
{
/// <summary>
/// The weights for which swarms will be selected.
/// </summary>
[DataField]
public ProtoId<WeightedRandomEntityPrototype> Config = "DefaultConfig";
/// <summary>
/// The time at which the next swarm occurs.
/// </summary>
[DataField, AutoPausedField]
public TimeSpan NextSwarmTime = TimeSpan.Zero;
}

View File

@@ -1,17 +1,41 @@
namespace Content.Server.StationEvents.Components;
using Content.Shared.EntityTable.EntitySelectors;
namespace Content.Server.StationEvents.Components;
[RegisterComponent, Access(typeof(RampingStationEventSchedulerSystem))]
public sealed partial class RampingStationEventSchedulerComponent : Component
{
[DataField("endTime"), ViewVariables(VVAccess.ReadWrite)]
/// <summary>
/// Average ending chaos modifier for the ramping event scheduler. Higher means faster.
/// Max chaos chosen for a round will deviate from this
/// </summary>
[DataField]
public float AverageChaos = 12f;
/// <summary>
/// Average time (in minutes) for when the ramping event scheduler should stop increasing the chaos modifier.
/// Close to how long you expect a round to last, so you'll probably have to tweak this on downstreams.
/// </summary>
[DataField]
public float AverageEndTime = 90f;
[DataField]
public float EndTime;
[DataField("maxChaos"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float MaxChaos;
[DataField("startingChaos"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float StartingChaos;
[DataField("timeUntilNextEvent"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float TimeUntilNextEvent;
/// <summary>
/// The gamerules that the scheduler can choose from
/// </summary>
/// Reminder that though we could do all selection via the EntityTableSelector, we also need to consider various <see cref="StationEventComponent"/> restrictions.
/// As such, we want to pass a list of acceptable game rules, which are then parsed for restrictions by the <see cref="EventManagerSystem"/>.
[DataField(required: true)]
public EntityTableSelector ScheduledGameRules = default!;
}

View File

@@ -1,9 +1,13 @@
using Content.Server.StationEvents.Events;
using Content.Server.StationEvents.Events;
namespace Content.Server.StationEvents.Components;
[RegisterComponent, Access(typeof(RandomSentienceRule))]
public sealed partial class RandomSentienceRuleComponent : Component
{
[DataField]
public int MinSentiences = 1;
[DataField]
public int MaxSentiences = 1;
}

View File

@@ -1,10 +1,13 @@
using Content.Server.StationEvents.Events;
using Content.Server.StationEvents.Events;
namespace Content.Server.StationEvents.Components;
[RegisterComponent, Access(typeof(RandomSentienceRule))]
public sealed partial class SentienceTargetComponent : Component
{
[DataField("flavorKind", required: true)]
[DataField(required: true)]
public string FlavorKind = default!;
[DataField]
public float Weight = 1.0f;
}

View File

@@ -1,5 +1,4 @@
using System.Linq;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.RoundEnd;
using Content.Server.StationEvents.Components;
@@ -8,6 +7,8 @@ using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.EntityTable;
namespace Content.Server.StationEvents;
@@ -17,7 +18,7 @@ public sealed class EventManagerSystem : EntitySystem
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly EntityTableSystem _entityTable = default!;
[Dependency] public readonly GameTicker GameTicker = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
@@ -34,7 +35,8 @@ public sealed class EventManagerSystem : EntitySystem
/// <summary>
/// Randomly runs a valid event.
/// </summary>
public string RunRandomEvent()
[Obsolete("use overload taking EnityTableSelector instead or risk unexpected results")]
public void RunRandomEvent()
{
var randomEvent = PickRandomEvent();
@@ -42,14 +44,86 @@ public sealed class EventManagerSystem : EntitySystem
{
var errStr = Loc.GetString("station-event-system-run-random-event-no-valid-events");
Log.Error(errStr);
return errStr;
return;
}
var ent = GameTicker.AddGameRule(randomEvent);
var str = Loc.GetString("station-event-system-run-event",("eventName", ToPrettyString(ent)));
_chat.SendAdminAlert(str);
Log.Info(str);
return str;
GameTicker.AddGameRule(randomEvent);
}
/// <summary>
/// Randomly runs an event from provided EntityTableSelector.
/// </summary>
public void RunRandomEvent(EntityTableSelector limitedEventsTable)
{
if (!TryBuildLimitedEvents(limitedEventsTable, out var limitedEvents))
{
Log.Warning("Provided event table could not build dict!");
return;
}
var randomLimitedEvent = FindEvent(limitedEvents); // this picks the event, It might be better to use the GetSpawns to do it, but that will be a major rebalancing fuck.
if (randomLimitedEvent == null)
{
Log.Warning("The selected random event is null!");
return;
}
if (!_prototype.TryIndex(randomLimitedEvent, out _))
{
Log.Warning("A requested event is not available!");
return;
}
GameTicker.AddGameRule(randomLimitedEvent);
}
/// <summary>
/// Returns true if the provided EntityTableSelector gives at least one prototype with a StationEvent comp.
/// </summary>
public bool TryBuildLimitedEvents(EntityTableSelector limitedEventsTable, out Dictionary<EntityPrototype, StationEventComponent> limitedEvents)
{
limitedEvents = new Dictionary<EntityPrototype, StationEventComponent>();
var availableEvents = AvailableEvents(); // handles the player counts and individual event restrictions
if (availableEvents.Count == 0)
{
Log.Warning("No events were available to run!");
return false;
}
var selectedEvents = _entityTable.GetSpawns(limitedEventsTable);
if (selectedEvents.Any() != true) // This is here so if you fuck up the table it wont die.
return false;
foreach (var eventid in selectedEvents)
{
if (!_prototype.TryIndex(eventid, out var eventproto))
{
Log.Warning("An event ID has no prototype index!");
continue;
}
if (limitedEvents.ContainsKey(eventproto)) // This stops it from dying if you add duplicate entries in a fucked table
continue;
if (eventproto.Abstract)
continue;
if (!eventproto.TryGetComponent<StationEventComponent>(out var stationEvent, EntityManager.ComponentFactory))
continue;
if (!availableEvents.ContainsKey(eventproto))
continue;
limitedEvents.Add(eventproto, stationEvent);
}
if (!limitedEvents.Any())
return false;
return true;
}
/// <summary>

View File

@@ -1,33 +1,56 @@
using System.Linq;
using Content.Server.GameTicking.Rules.Components;
using System.Linq;
using Content.Shared.Dataset;
using Content.Server.Ghost.Roles.Components;
using Content.Server.StationEvents.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Random.Helpers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
public sealed class RandomSentienceRule : StationEventSystem<RandomSentienceRuleComponent>
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
protected override void Started(EntityUid uid, RandomSentienceRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
HashSet<EntityUid> stationsToNotify = new();
if (!TryGetRandomStation(out var station))
return;
var targetList = new List<Entity<SentienceTargetComponent>>();
var query = EntityQueryEnumerator<SentienceTargetComponent>();
while (query.MoveNext(out var targetUid, out var target))
var query = EntityQueryEnumerator<SentienceTargetComponent, TransformComponent>();
while (query.MoveNext(out var targetUid, out var target, out var xform))
{
if (StationSystem.GetOwningStation(targetUid, xform) != station)
continue;
targetList.Add((targetUid, target));
}
RobustRandom.Shuffle(targetList);
var toMakeSentient = _random.Next(component.MinSentiences, component.MaxSentiences);
var toMakeSentient = RobustRandom.Next(2, 5);
var groups = new HashSet<string>();
foreach (var target in targetList)
for (var i = 0; i < toMakeSentient && targetList.Count > 0; i++)
{
if (toMakeSentient-- == 0)
break;
// weighted random to pick a sentience target
var totalWeight = targetList.Sum(x => x.Comp.Weight);
// This initial target should never be picked.
// It's just so that target doesn't need to be nullable and as a safety fallback for id floating point errors ever mess up the comparison in the foreach.
var target = targetList[0];
var chosenWeight = _random.NextFloat(totalWeight);
var currentWeight = 0.0;
foreach (var potentialTarget in targetList)
{
currentWeight += potentialTarget.Comp.Weight;
if (currentWeight > chosenWeight)
{
target = potentialTarget;
break;
}
}
targetList.Remove(target);
RemComp<SentienceTargetComponent>(target);
var ghostRole = EnsureComp<GhostRoleComponent>(target);
@@ -45,24 +68,15 @@ public sealed class RandomSentienceRule : StationEventSystem<RandomSentienceRule
var kind2 = groupList.Count > 1 ? groupList[1] : "???";
var kind3 = groupList.Count > 2 ? groupList[2] : "???";
foreach (var target in targetList)
{
var station = StationSystem.GetOwningStation(target);
if(station == null)
continue;
stationsToNotify.Add((EntityUid) station);
}
foreach (var station in stationsToNotify)
{
ChatSystem.DispatchStationAnnouncement(
station,
Loc.GetString("station-event-random-sentience-announcement",
("kind1", kind1), ("kind2", kind2), ("kind3", kind3), ("amount", groupList.Count),
("data", Loc.GetString($"random-sentience-event-data-{RobustRandom.Next(1, 6)}")),
("strength", Loc.GetString($"random-sentience-event-strength-{RobustRandom.Next(1, 8)}"))),
playDefaultSound: false,
colorOverride: Color.Gold
);
}
ChatSystem.DispatchStationAnnouncement(
station.Value,
Loc.GetString("station-event-random-sentience-announcement",
("kind1", kind1), ("kind2", kind2), ("kind3", kind3), ("amount", groupList.Count),
("data", _random.Pick(_prototype.Index<LocalizedDatasetPrototype>("RandomSentienceEventData"))),
("strength", _random.Pick(_prototype.Index<LocalizedDatasetPrototype>("RandomSentienceEventStrength")))
),
playDefaultSound: false,
colorOverride: Color.Gold
);
}
}

View File

@@ -1,54 +0,0 @@
using Content.Server.GameTicking.Rules;
using Content.Server.StationEvents.Components;
using Content.Shared.CCVar;
using Content.Shared.GameTicking.Components;
using Content.Shared.Random.Helpers;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Server.StationEvents;
/// <summary>
/// This handles scheduling and launching meteors at a station at regular intervals.
/// TODO: there is 100% a world in which this is genericized and can be used for lots of basic event scheduling
/// </summary>
public sealed class MeteorSchedulerSystem : GameRuleSystem<MeteorSchedulerComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private TimeSpan _meteorMinDelay;
private TimeSpan _meteorMaxDelay;
public override void Initialize()
{
base.Initialize();
_cfg.OnValueChanged(CCVars.MeteorSwarmMinTime, f => { _meteorMinDelay = TimeSpan.FromMinutes(f); }, true);
_cfg.OnValueChanged(CCVars.MeteorSwarmMaxTime, f => { _meteorMaxDelay = TimeSpan.FromMinutes(f); }, true);
}
protected override void Started(EntityUid uid, MeteorSchedulerComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
component.NextSwarmTime = Timing.CurTime + RobustRandom.Next(_meteorMinDelay, _meteorMaxDelay);
}
protected override void ActiveTick(EntityUid uid, MeteorSchedulerComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
if (Timing.CurTime < component.NextSwarmTime)
return;
RunSwarm((uid, component));
component.NextSwarmTime += RobustRandom.Next(_meteorMinDelay, _meteorMaxDelay);
}
private void RunSwarm(Entity<MeteorSchedulerComponent> ent)
{
var swarmWeights = _prototypeManager.Index(ent.Comp.Config);
GameTicker.StartGameRule(swarmWeights.Pick(RobustRandom));
}
}

View File

@@ -1,22 +1,20 @@
using Content.Server.GameTicking;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Content.Server.StationEvents.Events;
using Content.Shared.CCVar;
using Content.Shared.GameTicking.Components;
using Robust.Shared.Configuration;
using Robust.Shared.Random;
namespace Content.Server.StationEvents;
public sealed class RampingStationEventSchedulerSystem : GameRuleSystem<RampingStationEventSchedulerComponent>
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EventManagerSystem _event = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
/// <summary>
/// Returns the ChaosModifier which increases as round time increases to a point.
/// </summary>
public float GetChaosModifier(EntityUid uid, RampingStationEventSchedulerComponent component)
{
var roundTime = (float) _gameTicker.RoundDuration().TotalSeconds;
@@ -30,14 +28,11 @@ public sealed class RampingStationEventSchedulerSystem : GameRuleSystem<RampingS
{
base.Started(uid, component, gameRule, args);
var avgChaos = _cfg.GetCVar(CCVars.EventsRampingAverageChaos);
var avgTime = _cfg.GetCVar(CCVars.EventsRampingAverageEndTime);
// Worlds shittiest probability distribution
// Got a complaint? Send them to
component.MaxChaos = _random.NextFloat(avgChaos - avgChaos / 4, avgChaos + avgChaos / 4);
component.MaxChaos = _random.NextFloat(component.AverageChaos - component.AverageChaos / 4, component.AverageChaos + component.AverageChaos / 4);
// This is in minutes, so *60 for seconds (for the chaos calc)
component.EndTime = _random.NextFloat(avgTime - avgTime / 4, avgTime + avgTime / 4) * 60f;
component.EndTime = _random.NextFloat(component.AverageEndTime - component.AverageEndTime / 4, component.AverageEndTime + component.AverageEndTime / 4) * 60f;
component.StartingChaos = component.MaxChaos / 10;
PickNextEventTime(uid, component);
@@ -54,19 +49,22 @@ public sealed class RampingStationEventSchedulerSystem : GameRuleSystem<RampingS
while (query.MoveNext(out var uid, out var scheduler, out var gameRule))
{
if (!GameTicker.IsGameRuleActive(uid, gameRule))
return;
continue;
if (scheduler.TimeUntilNextEvent > 0f)
{
scheduler.TimeUntilNextEvent -= frameTime;
return;
continue;
}
PickNextEventTime(uid, scheduler);
_event.RunRandomEvent();
_event.RunRandomEvent(scheduler.ScheduledGameRules);
}
}
/// <summary>
/// Sets the timing of the next event addition.
/// </summary>
private void PickNextEventTime(EntityUid uid, RampingStationEventSchedulerComponent component)
{
var mod = GetChaosModifier(uid, component);

View File

@@ -218,7 +218,7 @@ namespace Content.Server.Strip
return;
}
var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime);
var (time, stealth) = GetStripTimeModifiers(user, target, held, slotDef.StripTime);
if (!stealth)
_popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large);
@@ -306,7 +306,7 @@ namespace Content.Server.Strip
return;
}
var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime);
var (time, stealth) = GetStripTimeModifiers(user, target, item, slotDef.StripTime);
if (!stealth)
{
@@ -411,7 +411,7 @@ namespace Content.Server.Strip
if (!CanStripInsertHand(user, target, held, handName))
return;
var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay);
var (time, stealth) = GetStripTimeModifiers(user, target, null, targetStrippable.HandStripDelay);
if (!stealth)
_popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert-hand", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large);
@@ -510,7 +510,7 @@ namespace Content.Server.Strip
if (!CanStripRemoveHand(user, target, item, handName))
return;
var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay);
var (time, stealth) = GetStripTimeModifiers(user, target, null, targetStrippable.HandStripDelay);
if (!stealth)
_popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target);

View File

@@ -0,0 +1,24 @@
using Content.Server.Temperature.Systems;
using Content.Server.Temperature.Components;
using Content.Shared.Temperature;
namespace Content.Server.Temperature.Components;
/// <summary>
/// Put this component on a projectile that you would like to change the temperature on whatever it hits.
/// </summary>
[RegisterComponent, Access(typeof(TemperatureSystem))]
public sealed partial class ChangeTemperatureOnCollideComponent : Component
{
/// <summary>
/// The amount it changes the target's temperature by. In Joules.
/// </summary>
[DataField]
public float Heat = 0f;
/// <summary>
/// If this heat change ignores heat resistance or not.
/// </summary>
[DataField]
public bool IgnoreHeatResistance = true;
}

View File

@@ -1,5 +1,6 @@
using Content.Server.Temperature.Systems;
using Content.Shared.Temperature;
using Robust.Shared.Audio;
namespace Content.Server.Temperature.Components;
@@ -21,4 +22,10 @@ public sealed partial class EntityHeaterComponent : Component
/// </summary>
[DataField]
public EntityHeaterSetting Setting = EntityHeaterSetting.Off;
/// <summary>
/// An optional sound that plays when the setting is changed.
/// </summary>
[DataField]
public SoundPathSpecifier? SettingSound;
}

View File

@@ -7,6 +7,7 @@ using Content.Shared.Placeable;
using Content.Shared.Popups;
using Content.Shared.Temperature;
using Content.Shared.Verbs;
using Robust.Server.Audio;
namespace Content.Server.Temperature.Systems;
@@ -18,6 +19,7 @@ public sealed class EntityHeaterSystem : EntitySystem
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly TemperatureSystem _temperature = default!;
[Dependency] private readonly AudioSystem _audio = default!;
private readonly int SettingCount = Enum.GetValues(typeof(EntityHeaterSetting)).Length;
@@ -94,6 +96,7 @@ public sealed class EntityHeaterSystem : EntitySystem
comp.Setting = setting;
power.Load = SettingPower(setting, comp.Power);
_appearance.SetData(uid, EntityHeaterVisuals.Setting, setting);
_audio.PlayPvs(comp.SettingSound, uid);
}
private float SettingPower(EntityHeaterSetting setting, float max)

View File

@@ -12,6 +12,8 @@ using Content.Shared.Rejuvenate;
using Content.Shared.Temperature;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Physics.Events;
using Content.Shared.Projectiles;
namespace Content.Server.Temperature.Systems;
@@ -21,6 +23,7 @@ public sealed class TemperatureSystem : EntitySystem
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly TemperatureSystem _temperature = default!;
/// <summary>
/// All the components that will have their damage updated at the end of the tick.
@@ -47,6 +50,8 @@ public sealed class TemperatureSystem : EntitySystem
SubscribeLocalEvent<InternalTemperatureComponent, MapInitEvent>(OnInit);
SubscribeLocalEvent<ChangeTemperatureOnCollideComponent, ProjectileHitEvent>(ChangeTemperatureOnCollide);
// Allows overriding thresholds based on the parent's thresholds.
SubscribeLocalEvent<TemperatureComponent, EntParentChangedMessage>(OnParentChange);
SubscribeLocalEvent<ContainerTemperatureDamageThresholdsComponent, ComponentStartup>(
@@ -304,6 +309,11 @@ public sealed class TemperatureSystem : EntitySystem
args.Args.TemperatureDelta *= ev.Coefficient;
}
private void ChangeTemperatureOnCollide(Entity<ChangeTemperatureOnCollideComponent> ent, ref ProjectileHitEvent args)
{
_temperature.ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
}
private void OnParentChange(EntityUid uid, TemperatureComponent component,
ref EntParentChangedMessage args)
{

View File

@@ -151,19 +151,17 @@ public sealed class WiresSystem : SharedWiresSystem
for (var i = 0; i < enumeratedList.Count; i++)
{
(int id, Wire d) = enumeratedList[i];
d.Id = i;
if (d.Action != null)
{
var actionType = d.Action.GetType();
if (types.ContainsKey(actionType))
if (!types.TryAdd(actionType, 1))
types[actionType] += 1;
else
types.Add(actionType, 1);
if (!d.Action.AddWire(d, types[actionType]))
d.Action = null;
}
d.Id = i;
data.Add(id, new WireLayout.WireData(d.Letter, d.Color, i));
wires.WiresList[i] = wireSet[id];

View File

@@ -37,245 +37,244 @@ using Content.Shared.Traits.Assorted;
using Robust.Shared.Audio.Systems;
using Content.Shared.Ghost.Roles.Components;
namespace Content.Server.Zombies
namespace Content.Server.Zombies;
/// <summary>
/// Handles zombie propagation and inherent zombie traits
/// </summary>
/// <remarks>
/// Don't Shitcode Open Inside
/// </remarks>
public sealed partial class ZombieSystem
{
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly ServerInventorySystem _inventory = default!;
[Dependency] private readonly NpcFactionSystem _faction = default!;
[Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly IChatManager _chatMan = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
/// <summary>
/// Handles zombie propagation and inherent zombie traits
/// Handles an entity turning into a zombie when they die or go into crit
/// </summary>
/// <remarks>
/// Don't Shitcode Open Inside
/// </remarks>
public sealed partial class ZombieSystem
private void OnDamageChanged(EntityUid uid, ZombifyOnDeathComponent component, MobStateChangedEvent args)
{
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly ServerInventorySystem _inventory = default!;
[Dependency] private readonly NpcFactionSystem _faction = default!;
[Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly IChatManager _chatMan = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
/// <summary>
/// Handles an entity turning into a zombie when they die or go into crit
/// </summary>
private void OnDamageChanged(EntityUid uid, ZombifyOnDeathComponent component, MobStateChangedEvent args)
if (args.NewMobState == MobState.Dead)
{
if (args.NewMobState == MobState.Dead)
{
ZombifyEntity(uid, args.Component);
}
}
/// <summary>
/// This is the general purpose function to call if you want to zombify an entity.
/// It handles both humanoid and nonhumanoid transformation and everything should be called through it.
/// </summary>
/// <param name="target">the entity being zombified</param>
/// <param name="mobState"></param>
/// <remarks>
/// ALRIGHT BIG BOYS, GIRLS AND ANYONE ELSE. YOU'VE COME TO THE LAYER OF THE BEAST. THIS IS YOUR WARNING.
/// This function is the god function for zombie stuff, and it is cursed. I have
/// attempted to label everything thouroughly for your sanity. I have attempted to
/// rewrite this, but this is how it shall lie eternal. Turn back now.
/// -emo
/// </remarks>
public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null)
{
//Don't zombfiy zombies
if (HasComp<ZombieComponent>(target) || HasComp<ZombieImmuneComponent>(target))
return;
if (!Resolve(target, ref mobState, logMissing: false))
return;
//you're a real zombie now, son.
var zombiecomp = AddComp<ZombieComponent>(target);
//we need to basically remove all of these because zombies shouldn't
//get diseases, breath, be thirst, be hungry, die in space, have offspring or be paraplegic.
RemComp<RespiratorComponent>(target);
RemComp<BarotraumaComponent>(target);
RemComp<HungerComponent>(target);
RemComp<ThirstComponent>(target);
RemComp<ReproductiveComponent>(target);
RemComp<ReproductivePartnerComponent>(target);
RemComp<LegsParalyzedComponent>(target);
RemComp<ComplexInteractionComponent>(target);
//funny voice
var accentType = "zombie";
if (TryComp<ZombieAccentOverrideComponent>(target, out var accent))
accentType = accent.Accent;
EnsureComp<ReplacementAccentComponent>(target).Accent = accentType;
//This is needed for stupid entities that fuck up combat mode component
//in an attempt to make an entity not attack. This is the easiest way to do it.
var combat = EnsureComp<CombatModeComponent>(target);
RemComp<PacifiedComponent>(target);
_combat.SetCanDisarm(target, false, combat);
_combat.SetInCombatMode(target, true, combat);
//This is the actual damage of the zombie. We assign the visual appearance
//and range here because of stuff we'll find out later
var melee = EnsureComp<MeleeWeaponComponent>(target);
melee.Animation = zombiecomp.AttackAnimation;
melee.WideAnimation = zombiecomp.AttackAnimation;
melee.AltDisarm = false;
melee.Range = 1.2f;
melee.Angle = 0.0f;
melee.HitSound = zombiecomp.BiteSound;
if (mobState.CurrentState == MobState.Alive)
{
// Groaning when damaged
EnsureComp<EmoteOnDamageComponent>(target);
_emoteOnDamage.AddEmote(target, "Scream");
// Random groaning
EnsureComp<AutoEmoteComponent>(target);
_autoEmote.AddEmote(target, "ZombieGroan");
}
//We have specific stuff for humanoid zombies because they matter more
if (TryComp<HumanoidAppearanceComponent>(target, out var huApComp)) //huapcomp
{
//store some values before changing them in case the humanoid get cloned later
zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor;
zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor;
zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers);
if (TryComp<BloodstreamComponent>(target, out var stream))
zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent;
_humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
// Messing with the eye layer made it vanish upon cloning, and also it didn't even appear right
huApComp.EyeColor = zombiecomp.EyeColor;
// this might not resync on clone?
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Snout, zombiecomp.BaseLayerExternal, humanoid: huApComp);
//This is done here because non-humanoids shouldn't get baller damage
//lord forgive me for the hardcoded damage
DamageSpecifier dspec = new()
{
DamageDict = new()
{
{ "Slash", 13 },
{ "Piercing", 7 },
{ "Structural", 10 }
}
};
melee.Damage = dspec;
// humanoid zombies get to pry open doors and shit
var pryComp = EnsureComp<PryingComponent>(target);
pryComp.SpeedModifier = 0.75f;
pryComp.PryPowered = true;
pryComp.Force = true;
Dirty(target, pryComp);
}
Dirty(target, melee);
//The zombie gets the assigned damage weaknesses and strengths
_damageable.SetDamageModifierSetId(target, "Zombie");
//This makes it so the zombie doesn't take bloodloss damage.
//NOTE: they are supposed to bleed, just not take damage
_bloodstream.SetBloodLossThreshold(target, 0f);
//Give them zombie blood
_bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent);
//This is specifically here to combat insuls, because frying zombies on grilles is funny as shit.
_inventory.TryUnequip(target, "gloves", true, true);
//Should prevent instances of zombies using comms for information they shouldnt be able to have.
_inventory.TryUnequip(target, "ears", true, true);
//popup
_popup.PopupEntity(Loc.GetString("zombie-transform", ("target", target)), target, PopupType.LargeCaution);
//Make it sentient if it's an animal or something
MakeSentientCommand.MakeSentient(target, EntityManager);
//Make the zombie not die in the cold. Good for space zombies
if (TryComp<TemperatureComponent>(target, out var tempComp))
tempComp.ColdDamage.ClampMax(0);
//Heals the zombie from all the damage it took while human
if (TryComp<DamageableComponent>(target, out var damageablecomp))
_damageable.SetAllDamage(target, damageablecomp, 0);
_mobState.ChangeMobState(target, MobState.Alive);
_faction.ClearFactions(target, dirty: false);
_faction.AddFaction(target, "Zombie");
//gives it the funny "Zombie ___" name.
_nameMod.RefreshNameModifiers(target);
_identity.QueueIdentityUpdate(target);
var htn = EnsureComp<HTNComponent>(target);
htn.RootTask = new HTNCompoundTask() { Task = "SimpleHostileCompound" };
htn.Blackboard.SetValue(NPCBlackboard.Owner, target);
_npc.SleepNPC(target, htn);
//He's gotta have a mind
var hasMind = _mind.TryGetMind(target, out var mindId, out _);
if (hasMind && _mind.TryGetSession(mindId, out var session))
{
//Zombie role for player manifest
_roles.MindAddRole(mindId, new ZombieRoleComponent { PrototypeId = zombiecomp.ZombieRoleId });
//Greeting message for new bebe zombers
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
// Notificate player about new role assignment
_audio.PlayGlobal(zombiecomp.GreetSoundNotification, session);
}
else
{
_npc.WakeNPC(target, htn);
}
if (!HasComp<GhostRoleMobSpawnerComponent>(target) && !hasMind) //this specific component gives build test trouble so pop off, ig
{
//yet more hardcoding. Visit zombie.ftl for more information.
var ghostRole = EnsureComp<GhostRoleComponent>(target);
EnsureComp<GhostTakeoverAvailableComponent>(target);
ghostRole.RoleName = Loc.GetString("zombie-generic");
ghostRole.RoleDescription = Loc.GetString("zombie-role-desc");
ghostRole.RoleRules = Loc.GetString("zombie-role-rules");
}
if (TryComp<HandsComponent>(target, out var handsComp))
{
_hands.RemoveHands(target);
RemComp(target, handsComp);
}
// Sloth: What the fuck?
// How long until compregistry lmao.
RemComp<PullerComponent>(target);
// No longer waiting to become a zombie:
// Requires deferral because this is (probably) the event which called ZombifyEntity in the first place.
RemCompDeferred<PendingZombieComponent>(target);
//zombie gamemode stuff
var ev = new EntityZombifiedEvent(target);
RaiseLocalEvent(target, ref ev, true);
//zombies get slowdown once they convert
_movementSpeedModifier.RefreshMovementSpeedModifiers(target);
ZombifyEntity(uid, args.Component);
}
}
/// <summary>
/// This is the general purpose function to call if you want to zombify an entity.
/// It handles both humanoid and nonhumanoid transformation and everything should be called through it.
/// </summary>
/// <param name="target">the entity being zombified</param>
/// <param name="mobState"></param>
/// <remarks>
/// ALRIGHT BIG BOYS, GIRLS AND ANYONE ELSE. YOU'VE COME TO THE LAYER OF THE BEAST. THIS IS YOUR WARNING.
/// This function is the god function for zombie stuff, and it is cursed. I have
/// attempted to label everything thouroughly for your sanity. I have attempted to
/// rewrite this, but this is how it shall lie eternal. Turn back now.
/// -emo
/// </remarks>
public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null)
{
//Don't zombfiy zombies
if (HasComp<ZombieComponent>(target) || HasComp<ZombieImmuneComponent>(target))
return;
if (!Resolve(target, ref mobState, logMissing: false))
return;
//you're a real zombie now, son.
var zombiecomp = AddComp<ZombieComponent>(target);
//we need to basically remove all of these because zombies shouldn't
//get diseases, breath, be thirst, be hungry, die in space, have offspring or be paraplegic.
RemComp<RespiratorComponent>(target);
RemComp<BarotraumaComponent>(target);
RemComp<HungerComponent>(target);
RemComp<ThirstComponent>(target);
RemComp<ReproductiveComponent>(target);
RemComp<ReproductivePartnerComponent>(target);
RemComp<LegsParalyzedComponent>(target);
RemComp<ComplexInteractionComponent>(target);
//funny voice
var accentType = "zombie";
if (TryComp<ZombieAccentOverrideComponent>(target, out var accent))
accentType = accent.Accent;
EnsureComp<ReplacementAccentComponent>(target).Accent = accentType;
//This is needed for stupid entities that fuck up combat mode component
//in an attempt to make an entity not attack. This is the easiest way to do it.
var combat = EnsureComp<CombatModeComponent>(target);
RemComp<PacifiedComponent>(target);
_combat.SetCanDisarm(target, false, combat);
_combat.SetInCombatMode(target, true, combat);
//This is the actual damage of the zombie. We assign the visual appearance
//and range here because of stuff we'll find out later
var melee = EnsureComp<MeleeWeaponComponent>(target);
melee.Animation = zombiecomp.AttackAnimation;
melee.WideAnimation = zombiecomp.AttackAnimation;
melee.AltDisarm = false;
melee.Range = 1.2f;
melee.Angle = 0.0f;
melee.HitSound = zombiecomp.BiteSound;
if (mobState.CurrentState == MobState.Alive)
{
// Groaning when damaged
EnsureComp<EmoteOnDamageComponent>(target);
_emoteOnDamage.AddEmote(target, "Scream");
// Random groaning
EnsureComp<AutoEmoteComponent>(target);
_autoEmote.AddEmote(target, "ZombieGroan");
}
//We have specific stuff for humanoid zombies because they matter more
if (TryComp<HumanoidAppearanceComponent>(target, out var huApComp)) //huapcomp
{
//store some values before changing them in case the humanoid get cloned later
zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor;
zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor;
zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers);
if (TryComp<BloodstreamComponent>(target, out var stream))
zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent;
_humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
// Messing with the eye layer made it vanish upon cloning, and also it didn't even appear right
huApComp.EyeColor = zombiecomp.EyeColor;
// this might not resync on clone?
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Snout, zombiecomp.BaseLayerExternal, humanoid: huApComp);
//This is done here because non-humanoids shouldn't get baller damage
//lord forgive me for the hardcoded damage
DamageSpecifier dspec = new()
{
DamageDict = new()
{
{ "Slash", 13 },
{ "Piercing", 7 },
{ "Structural", 10 }
}
};
melee.Damage = dspec;
// humanoid zombies get to pry open doors and shit
var pryComp = EnsureComp<PryingComponent>(target);
pryComp.SpeedModifier = 0.75f;
pryComp.PryPowered = true;
pryComp.Force = true;
Dirty(target, pryComp);
}
Dirty(target, melee);
//The zombie gets the assigned damage weaknesses and strengths
_damageable.SetDamageModifierSetId(target, "Zombie");
//This makes it so the zombie doesn't take bloodloss damage.
//NOTE: they are supposed to bleed, just not take damage
_bloodstream.SetBloodLossThreshold(target, 0f);
//Give them zombie blood
_bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent);
//This is specifically here to combat insuls, because frying zombies on grilles is funny as shit.
_inventory.TryUnequip(target, "gloves", true, true);
//Should prevent instances of zombies using comms for information they shouldnt be able to have.
_inventory.TryUnequip(target, "ears", true, true);
//popup
_popup.PopupEntity(Loc.GetString("zombie-transform", ("target", target)), target, PopupType.LargeCaution);
//Make it sentient if it's an animal or something
MakeSentientCommand.MakeSentient(target, EntityManager);
//Make the zombie not die in the cold. Good for space zombies
if (TryComp<TemperatureComponent>(target, out var tempComp))
tempComp.ColdDamage.ClampMax(0);
//Heals the zombie from all the damage it took while human
if (TryComp<DamageableComponent>(target, out var damageablecomp))
_damageable.SetAllDamage(target, damageablecomp, 0);
_mobState.ChangeMobState(target, MobState.Alive);
_faction.ClearFactions(target, dirty: false);
_faction.AddFaction(target, "Zombie");
//gives it the funny "Zombie ___" name.
_nameMod.RefreshNameModifiers(target);
_identity.QueueIdentityUpdate(target);
var htn = EnsureComp<HTNComponent>(target);
htn.RootTask = new HTNCompoundTask() { Task = "SimpleHostileCompound" };
htn.Blackboard.SetValue(NPCBlackboard.Owner, target);
_npc.SleepNPC(target, htn);
//He's gotta have a mind
var hasMind = _mind.TryGetMind(target, out var mindId, out _);
if (hasMind && _mind.TryGetSession(mindId, out var session))
{
//Zombie role for player manifest
_roles.MindAddRole(mindId, new ZombieRoleComponent { PrototypeId = zombiecomp.ZombieRoleId });
//Greeting message for new bebe zombers
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
// Notificate player about new role assignment
_audio.PlayGlobal(zombiecomp.GreetSoundNotification, session);
}
else
{
_npc.WakeNPC(target, htn);
}
if (!HasComp<GhostRoleMobSpawnerComponent>(target) && !hasMind) //this specific component gives build test trouble so pop off, ig
{
//yet more hardcoding. Visit zombie.ftl for more information.
var ghostRole = EnsureComp<GhostRoleComponent>(target);
EnsureComp<GhostTakeoverAvailableComponent>(target);
ghostRole.RoleName = Loc.GetString("zombie-generic");
ghostRole.RoleDescription = Loc.GetString("zombie-role-desc");
ghostRole.RoleRules = Loc.GetString("zombie-role-rules");
}
if (TryComp<HandsComponent>(target, out var handsComp))
{
_hands.RemoveHands(target);
RemComp(target, handsComp);
}
// Sloth: What the fuck?
// How long until compregistry lmao.
RemComp<PullerComponent>(target);
// No longer waiting to become a zombie:
// Requires deferral because this is (probably) the event which called ZombifyEntity in the first place.
RemCompDeferred<PendingZombieComponent>(target);
//zombie gamemode stuff
var ev = new EntityZombifiedEvent(target);
RaiseLocalEvent(target, ref ev, true);
//zombies get slowdown once they convert
_movementSpeedModifier.RefreshMovementSpeedModifiers(target);
}
}

View File

@@ -0,0 +1,31 @@
using Content.Shared.Actions;
using Robust.Shared.Prototypes;
namespace Content.Shared.Abilities.Goliath;
public sealed partial class GoliathSummonTentacleAction : EntityWorldTargetActionEvent
{
/// <summary>
/// The ID of the entity that is spawned.
/// </summary>
[DataField]
public EntProtoId EntityId = "EffectGoliathTentacleSpawn";
/// <summary>
/// Directions determining where the entities will spawn.
/// </summary>
[DataField]
public List<Direction> OffsetDirections = new()
{
Direction.North,
Direction.South,
Direction.East,
Direction.West,
};
/// <summary>
/// How many entities will spawn beyond the original one at the target location?
/// </summary>
[DataField]
public int ExtraSpawns = 3;
};

View File

@@ -0,0 +1,69 @@
using Content.Shared.Directions;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Stunnable;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Random;
namespace Content.Shared.Abilities.Goliath;
public sealed class GoliathTentacleSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TurfSystem _turf = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<GoliathSummonTentacleAction>(OnSummonAction);
}
private void OnSummonAction(GoliathSummonTentacleAction args)
{
if (args.Handled || args.Coords is not { } coords)
return;
// TODO: animation
_popup.PopupPredicted(Loc.GetString("tentacle-ability-use-popup", ("entity", args.Performer)), args.Performer, args.Performer, type: PopupType.SmallCaution);
_stun.TryStun(args.Performer, TimeSpan.FromSeconds(0.8f), false);
List<EntityCoordinates> spawnPos = new();
spawnPos.Add(coords);
var dirs = new List<Direction>();
dirs.AddRange(args.OffsetDirections);
for (var i = 0; i < 3; i++)
{
var dir = _random.PickAndTake(dirs);
spawnPos.Add(coords.Offset(dir));
}
if (_transform.GetGrid(coords) is not { } grid || !TryComp<MapGridComponent>(grid, out var gridComp))
return;
foreach (var pos in spawnPos)
{
if (!_map.TryGetTileRef(grid, gridComp, pos, out var tileRef) ||
tileRef.IsSpace() ||
_turf.IsTileBlocked(tileRef, CollisionGroup.Impassable))
{
continue;
}
if (_net.IsServer)
Spawn(args.EntityId, pos);
}
args.Handled = true;
}
}

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