Merge pull request #413 from crystallpunk-14/ed-19-08-2024-upstream
Ed 19 08 2024 upstream
This commit is contained in:
6
.vscode/tasks.json
vendored
6
.vscode/tasks.json
vendored
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
27
Content.Client/Materials/RecyclerVisualizerSystem.cs
Normal file
27
Content.Client/Materials/RecyclerVisualizerSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
17
Content.Client/Materials/RecyclerVisualsComponent.cs
Normal file
17
Content.Client/Materials/RecyclerVisualsComponent.cs
Normal 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";
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
74
Content.Server/DeviceLinking/Systems/MemoryCellSystem.cs
Normal file
74
Content.Server/DeviceLinking/Systems/MemoryCellSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
68
Content.Server/NPC/Systems/NPCUseActionOnTargetSystem.cs
Normal file
68
Content.Server/NPC/Systems/NPCUseActionOnTargetSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"}!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class GermanAccentComponent : Component;
|
||||
85
Content.Server/Speech/EntitySystems/GermanAccentSystem.cs
Normal file
85
Content.Server/Speech/EntitySystems/GermanAccentSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
69
Content.Shared/Abilities/Goliath/GoliathTentacleSystem.cs
Normal file
69
Content.Shared/Abilities/Goliath/GoliathTentacleSystem.cs
Normal 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
Reference in New Issue
Block a user