Merge remote-tracking branch 'upstream/stable' into ed-29-10-2024-upstream
# Conflicts: # Content.Server/Chat/Managers/ChatSanitizationManager.cs # Content.Server/Temperature/Systems/TemperatureSystem.cs # Content.Shared/Localizations/ContentLocalizationManager.cs
This commit is contained in:
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -19,7 +19,7 @@
|
||||
/Resources/engineCommandPerms.yml @moonheart08 @Chief-Engineer
|
||||
/Resources/clientCommandPerms.yml @moonheart08 @Chief-Engineer
|
||||
|
||||
/Resources/Prototypes/Maps/ @Emisse
|
||||
/Resources/Prototypes/Maps/** @Emisse
|
||||
|
||||
/Resources/Prototypes/Body/ @DrSmugleaf # suffering
|
||||
/Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
{
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
private readonly SharedNavMapSystem _navMapSystem;
|
||||
|
||||
private EntityUid? _owner;
|
||||
private NetEntity? _trackedEntity;
|
||||
@@ -42,19 +43,32 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
|
||||
private const float SilencingDuration = 2.5f;
|
||||
|
||||
// Colors
|
||||
private Color _wallColor = new Color(64, 64, 64);
|
||||
private Color _tileColor = new Color(28, 28, 28);
|
||||
private Color _monitorBlipColor = Color.Cyan;
|
||||
private Color _untrackedEntColor = Color.DimGray;
|
||||
private Color _regionBaseColor = new Color(154, 154, 154);
|
||||
private Color _inactiveColor = StyleNano.DisabledFore;
|
||||
private Color _statusTextColor = StyleNano.GoodGreenFore;
|
||||
private Color _goodColor = Color.LimeGreen;
|
||||
private Color _warningColor = new Color(255, 182, 72);
|
||||
private Color _dangerColor = new Color(255, 67, 67);
|
||||
|
||||
public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInterface, EntityUid? owner)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_entManager = IoCManager.Resolve<IEntityManager>();
|
||||
_spriteSystem = _entManager.System<SpriteSystem>();
|
||||
_navMapSystem = _entManager.System<SharedNavMapSystem>();
|
||||
|
||||
// Pass the owner to nav map
|
||||
_owner = owner;
|
||||
NavMap.Owner = _owner;
|
||||
|
||||
// Set nav map colors
|
||||
NavMap.WallColor = new Color(64, 64, 64);
|
||||
NavMap.TileColor = Color.DimGray * NavMap.WallColor;
|
||||
NavMap.WallColor = _wallColor;
|
||||
NavMap.TileColor = _tileColor;
|
||||
|
||||
// Set nav map grid uid
|
||||
var stationName = Loc.GetString("atmos-alerts-window-unknown-location");
|
||||
@@ -179,6 +193,9 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
// Add tracked entities to the nav map
|
||||
foreach (var device in console.AtmosDevices)
|
||||
{
|
||||
if (!device.NetEntity.Valid)
|
||||
continue;
|
||||
|
||||
if (!NavMap.Visible)
|
||||
continue;
|
||||
|
||||
@@ -209,7 +226,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
if (consoleCoords != null && consoleUid != null)
|
||||
{
|
||||
var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
|
||||
var blip = new NavMapBlip(consoleCoords.Value, texture, Color.Cyan, true, false);
|
||||
var blip = new NavMapBlip(consoleCoords.Value, texture, _monitorBlipColor, true, false);
|
||||
NavMap.TrackedEntities[consoleUid.Value] = blip;
|
||||
}
|
||||
|
||||
@@ -258,7 +275,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
};
|
||||
|
||||
label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", StyleNano.GoodGreenFore.ToHexNoAlpha())));
|
||||
label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", _statusTextColor.ToHexNoAlpha())));
|
||||
|
||||
AlertsTable.AddChild(label);
|
||||
}
|
||||
@@ -270,6 +287,34 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
else
|
||||
MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount)));
|
||||
|
||||
// Update sensor regions
|
||||
NavMap.RegionOverlays.Clear();
|
||||
var prioritizedRegionOverlays = new Dictionary<NavMapRegionOverlay, int>();
|
||||
|
||||
if (_owner != null &&
|
||||
_entManager.TryGetComponent<TransformComponent>(_owner, out var xform) &&
|
||||
_entManager.TryGetComponent<NavMapComponent>(xform.GridUid, out var navMap))
|
||||
{
|
||||
var regionOverlays = _navMapSystem.GetNavMapRegionOverlays(_owner.Value, navMap, AtmosAlertsComputerUiKey.Key);
|
||||
|
||||
foreach (var (regionOwner, regionOverlay) in regionOverlays)
|
||||
{
|
||||
var alarmState = GetAlarmState(regionOwner);
|
||||
|
||||
if (!TryGetSensorRegionColor(regionOwner, alarmState, out var regionColor))
|
||||
continue;
|
||||
|
||||
regionOverlay.Color = regionColor;
|
||||
|
||||
var priority = (_trackedEntity == regionOwner) ? 999 : (int)alarmState;
|
||||
prioritizedRegionOverlays.Add(regionOverlay, priority);
|
||||
}
|
||||
|
||||
// Sort overlays according to their priority
|
||||
var sortedOverlays = prioritizedRegionOverlays.OrderBy(x => x.Value).Select(x => x.Key).ToList();
|
||||
NavMap.RegionOverlays = sortedOverlays;
|
||||
}
|
||||
|
||||
// Auto-scroll re-enable
|
||||
if (_autoScrollAwaitsUpdate)
|
||||
{
|
||||
@@ -290,7 +335,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
|
||||
|
||||
if (_trackedEntity != null && _trackedEntity != metaData.NetEntity)
|
||||
color *= Color.DimGray;
|
||||
color *= _untrackedEntColor;
|
||||
|
||||
var selectable = true;
|
||||
var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color, _trackedEntity == metaData.NetEntity, selectable);
|
||||
@@ -298,6 +343,24 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
NavMap.TrackedEntities[metaData.NetEntity] = blip;
|
||||
}
|
||||
|
||||
private bool TryGetSensorRegionColor(NetEntity regionOwner, AtmosAlarmType alarmState, out Color color)
|
||||
{
|
||||
color = Color.White;
|
||||
|
||||
var blip = GetBlipTexture(alarmState);
|
||||
|
||||
if (blip == null)
|
||||
return false;
|
||||
|
||||
// Color the region based on alarm state and entity tracking
|
||||
color = blip.Value.Item2 * _regionBaseColor;
|
||||
|
||||
if (_trackedEntity != null && _trackedEntity != regionOwner)
|
||||
color *= _untrackedEntColor;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null)
|
||||
{
|
||||
// Make new UI entry if required
|
||||
@@ -534,13 +597,13 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
switch (alarmState)
|
||||
{
|
||||
case AtmosAlarmType.Invalid:
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), StyleNano.DisabledFore); break;
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), _inactiveColor); break;
|
||||
case AtmosAlarmType.Normal:
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), Color.LimeGreen); break;
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), _goodColor); break;
|
||||
case AtmosAlarmType.Warning:
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), new Color(255, 182, 72)); break;
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), _warningColor); break;
|
||||
case AtmosAlarmType.Danger:
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), new Color(255, 67, 67)); break;
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), _dangerColor); break;
|
||||
}
|
||||
|
||||
return output;
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Client.Chat.Managers;
|
||||
using Content.Client.DebugMon;
|
||||
using Content.Client.Eui;
|
||||
using Content.Client.Fullscreen;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Client.GhostKick;
|
||||
using Content.Client.Guidebook;
|
||||
using Content.Client.Input;
|
||||
@@ -71,6 +72,7 @@ namespace Content.Client.Entry
|
||||
[Dependency] private readonly IReplayLoadManager _replayLoad = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
|
||||
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
@@ -140,6 +142,12 @@ namespace Content.Client.Entry
|
||||
_configManager.SetCVar("interface.resolutionAutoScaleMinimum", 0.5f);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_titleWindowManager.Shutdown();
|
||||
}
|
||||
|
||||
public override void PostInit()
|
||||
{
|
||||
base.PostInit();
|
||||
@@ -160,6 +168,7 @@ namespace Content.Client.Entry
|
||||
_userInterfaceManager.SetDefaultTheme(_configManager.GetCVar(CCVars.UIDefaultInterfaceTheme));
|
||||
_userInterfaceManager.SetActiveTheme(_configManager.GetCVar(CVars.InterfaceTheme));
|
||||
_documentParsingManager.Initialize();
|
||||
_titleWindowManager.Initialize();
|
||||
|
||||
_baseClient.RunLevelChanged += (_, args) =>
|
||||
{
|
||||
|
||||
62
Content.Client/GameTicking/Managers/TitleWindowManager.cs
Normal file
62
Content.Client/GameTicking/Managers/TitleWindowManager.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Client.GameTicking.Managers;
|
||||
|
||||
public sealed class TitleWindowManager
|
||||
{
|
||||
[Dependency] private readonly IBaseClient _client = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameController _gameController = default!;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_cfg.OnValueChanged(CVars.GameHostName, OnHostnameChange, true);
|
||||
_cfg.OnValueChanged(CCVars.GameHostnameInTitlebar, OnHostnameTitleChange, true);
|
||||
|
||||
_client.RunLevelChanged += OnRunLevelChangedChange;
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_cfg.UnsubValueChanged(CVars.GameHostName, OnHostnameChange);
|
||||
_cfg.UnsubValueChanged(CCVars.GameHostnameInTitlebar, OnHostnameTitleChange);
|
||||
}
|
||||
|
||||
private void OnHostnameChange(string hostname)
|
||||
{
|
||||
var defaultWindowTitle = _gameController.GameTitle();
|
||||
|
||||
// Since the game assumes the server name is MyServer and that GameHostnameInTitlebar CCVar is true by default
|
||||
// Lets just... not show anything. This also is used to revert back to just the game title on disconnect.
|
||||
if (_client.RunLevel == ClientRunLevel.Initialize)
|
||||
{
|
||||
_clyde.SetWindowTitle(defaultWindowTitle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cfg.GetCVar(CCVars.GameHostnameInTitlebar))
|
||||
// If you really dislike the dash I guess change it here
|
||||
_clyde.SetWindowTitle(hostname + " - " + defaultWindowTitle);
|
||||
else
|
||||
_clyde.SetWindowTitle(defaultWindowTitle);
|
||||
}
|
||||
|
||||
// Clients by default assume game.hostname_in_titlebar is true
|
||||
// but we need to clear it as soon as we join and actually receive the servers preference on this.
|
||||
// This will ensure we rerun OnHostnameChange and set the correct title bar name.
|
||||
private void OnHostnameTitleChange(bool colonthree)
|
||||
{
|
||||
OnHostnameChange(_cfg.GetCVar(CVars.GameHostName));
|
||||
}
|
||||
|
||||
// This is just used we can rerun the hostname change function when we disconnect to revert back to just the games title.
|
||||
private void OnRunLevelChangedChange(object? sender, RunLevelChangedEventArgs runLevelChangedEventArgs)
|
||||
{
|
||||
OnHostnameChange(_cfg.GetCVar(CVars.GameHostName));
|
||||
}
|
||||
}
|
||||
@@ -130,9 +130,9 @@ namespace Content.Client.Hands.Systems
|
||||
OnPlayerHandsAdded?.Invoke(hands);
|
||||
}
|
||||
|
||||
public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? hands = null)
|
||||
public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? hands = null, bool log = true)
|
||||
{
|
||||
base.DoDrop(uid, hand, doDropInteraction, hands);
|
||||
base.DoDrop(uid, hand, doDropInteraction, hands, log);
|
||||
|
||||
if (TryComp(hand.HeldEntity, out SpriteComponent? sprite))
|
||||
sprite.RenderOrder = EntityManager.CurrentTick.Value;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Localizations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
@@ -16,19 +17,10 @@ public sealed partial class PlaytimeStatsEntry : ContainerButton
|
||||
|
||||
RoleLabel.Text = role;
|
||||
Playtime = playtime; // store the TimeSpan value directly
|
||||
PlaytimeLabel.Text = ConvertTimeSpanToHoursMinutes(playtime); // convert to string for display
|
||||
PlaytimeLabel.Text = ContentLocalizationManager.FormatPlaytime(playtime); // convert to string for display
|
||||
BackgroundColorPanel.PanelOverride = styleBox;
|
||||
}
|
||||
|
||||
private static string ConvertTimeSpanToHoursMinutes(TimeSpan timeSpan)
|
||||
{
|
||||
var hours = (int)timeSpan.TotalHours;
|
||||
var minutes = timeSpan.Minutes;
|
||||
|
||||
var formattedTimeLoc = Loc.GetString("ui-playtime-time-format", ("hours", hours), ("minutes", minutes));
|
||||
return formattedTimeLoc;
|
||||
}
|
||||
|
||||
public void UpdateShading(StyleBoxFlat styleBox)
|
||||
{
|
||||
BackgroundColorPanel.PanelOverride = styleBox;
|
||||
|
||||
@@ -104,8 +104,7 @@ public sealed partial class PlaytimeStatsWindow : FancyWindow
|
||||
{
|
||||
var overallPlaytime = _jobRequirementsManager.FetchOverallPlaytime();
|
||||
|
||||
var formattedPlaytime = ConvertTimeSpanToHoursMinutes(overallPlaytime);
|
||||
OverallPlaytimeLabel.Text = Loc.GetString("ui-playtime-overall", ("time", formattedPlaytime));
|
||||
OverallPlaytimeLabel.Text = Loc.GetString("ui-playtime-overall", ("time", overallPlaytime));
|
||||
|
||||
var rolePlaytimes = _jobRequirementsManager.FetchPlaytimeByRoles();
|
||||
|
||||
@@ -134,13 +133,4 @@ public sealed partial class PlaytimeStatsWindow : FancyWindow
|
||||
_sawmill.Error($"The provided playtime string '{playtimeString}' is not in the correct format.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConvertTimeSpanToHoursMinutes(TimeSpan timeSpan)
|
||||
{
|
||||
var hours = (int) timeSpan.TotalHours;
|
||||
var minutes = timeSpan.Minutes;
|
||||
|
||||
var formattedTimeLoc = Loc.GetString("ui-playtime-time-format", ("hours", hours), ("minutes", minutes));
|
||||
return formattedTimeLoc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Content.Client.Clickable;
|
||||
using Content.Client.DebugMon;
|
||||
using Content.Client.Eui;
|
||||
using Content.Client.Fullscreen;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Client.GhostKick;
|
||||
using Content.Client.Guidebook;
|
||||
using Content.Client.Launcher;
|
||||
@@ -57,6 +58,7 @@ namespace Content.Client.IoC
|
||||
collection.Register<DebugMonitorManager>();
|
||||
collection.Register<PlayerRateLimitManager>();
|
||||
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
|
||||
collection.Register<TitleWindowManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
303
Content.Client/Pinpointer/NavMapSystem.Regions.cs
Normal file
303
Content.Client/Pinpointer/NavMapSystem.Regions.cs
Normal file
@@ -0,0 +1,303 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Pinpointer;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Pinpointer;
|
||||
|
||||
public sealed partial class NavMapSystem
|
||||
{
|
||||
private (AtmosDirection, Vector2i, AtmosDirection)[] _regionPropagationTable =
|
||||
{
|
||||
(AtmosDirection.East, new Vector2i(1, 0), AtmosDirection.West),
|
||||
(AtmosDirection.West, new Vector2i(-1, 0), AtmosDirection.East),
|
||||
(AtmosDirection.North, new Vector2i(0, 1), AtmosDirection.South),
|
||||
(AtmosDirection.South, new Vector2i(0, -1), AtmosDirection.North),
|
||||
};
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
// To prevent compute spikes, only one region is flood filled per frame
|
||||
var query = AllEntityQuery<NavMapComponent>();
|
||||
|
||||
while (query.MoveNext(out var ent, out var entNavMapRegions))
|
||||
FloodFillNextEnqueuedRegion(ent, entNavMapRegions);
|
||||
}
|
||||
|
||||
private void FloodFillNextEnqueuedRegion(EntityUid uid, NavMapComponent component)
|
||||
{
|
||||
if (!component.QueuedRegionsToFlood.Any())
|
||||
return;
|
||||
|
||||
var regionOwner = component.QueuedRegionsToFlood.Dequeue();
|
||||
|
||||
// If the region is no longer valid, flood the next one in the queue
|
||||
if (!component.RegionProperties.TryGetValue(regionOwner, out var regionProperties) ||
|
||||
!regionProperties.Seeds.Any())
|
||||
{
|
||||
FloodFillNextEnqueuedRegion(uid, component);
|
||||
return;
|
||||
}
|
||||
|
||||
// Flood fill the region, using the region seeds as starting points
|
||||
var (floodedTiles, floodedChunks) = FloodFillRegion(uid, component, regionProperties);
|
||||
|
||||
// Combine the flooded tiles into larger rectangles
|
||||
var gridCoords = GetMergedRegionTiles(floodedTiles);
|
||||
|
||||
// Create and assign the new region overlay
|
||||
var regionOverlay = new NavMapRegionOverlay(regionProperties.UiKey, gridCoords)
|
||||
{
|
||||
Color = regionProperties.Color
|
||||
};
|
||||
|
||||
component.RegionOverlays[regionOwner] = regionOverlay;
|
||||
|
||||
// To reduce unnecessary future flood fills, we will track which chunks have been flooded by a region owner
|
||||
|
||||
// First remove an old assignments
|
||||
if (component.RegionOwnerToChunkTable.TryGetValue(regionOwner, out var oldChunks))
|
||||
{
|
||||
foreach (var chunk in oldChunks)
|
||||
{
|
||||
if (component.ChunkToRegionOwnerTable.TryGetValue(chunk, out var oldOwners))
|
||||
{
|
||||
oldOwners.Remove(regionOwner);
|
||||
component.ChunkToRegionOwnerTable[chunk] = oldOwners;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now update with the new assignments
|
||||
component.RegionOwnerToChunkTable[regionOwner] = floodedChunks;
|
||||
|
||||
foreach (var chunk in floodedChunks)
|
||||
{
|
||||
if (!component.ChunkToRegionOwnerTable.TryGetValue(chunk, out var owners))
|
||||
owners = new();
|
||||
|
||||
owners.Add(regionOwner);
|
||||
component.ChunkToRegionOwnerTable[chunk] = owners;
|
||||
}
|
||||
}
|
||||
|
||||
private (HashSet<Vector2i>, HashSet<Vector2i>) FloodFillRegion(EntityUid uid, NavMapComponent component, NavMapRegionProperties regionProperties)
|
||||
{
|
||||
if (!regionProperties.Seeds.Any())
|
||||
return (new(), new());
|
||||
|
||||
var visitedChunks = new HashSet<Vector2i>();
|
||||
var visitedTiles = new HashSet<Vector2i>();
|
||||
var tilesToVisit = new Stack<Vector2i>();
|
||||
|
||||
foreach (var regionSeed in regionProperties.Seeds)
|
||||
{
|
||||
tilesToVisit.Push(regionSeed);
|
||||
|
||||
while (tilesToVisit.Count > 0)
|
||||
{
|
||||
// If the max region area is hit, exit
|
||||
if (visitedTiles.Count > regionProperties.MaxArea)
|
||||
return (new(), new());
|
||||
|
||||
// Pop the top tile from the stack
|
||||
var current = tilesToVisit.Pop();
|
||||
|
||||
// If the current tile position has already been visited,
|
||||
// or is too far away from the seed, continue
|
||||
if ((regionSeed - current).Length > regionProperties.MaxRadius)
|
||||
continue;
|
||||
|
||||
if (visitedTiles.Contains(current))
|
||||
continue;
|
||||
|
||||
// Determine the tile's chunk index
|
||||
var chunkOrigin = SharedMapSystem.GetChunkIndices(current, ChunkSize);
|
||||
var relative = SharedMapSystem.GetChunkRelative(current, ChunkSize);
|
||||
var idx = GetTileIndex(relative);
|
||||
|
||||
// Extract the tile data
|
||||
if (!component.Chunks.TryGetValue(chunkOrigin, out var chunk))
|
||||
continue;
|
||||
|
||||
var flag = chunk.TileData[idx];
|
||||
|
||||
// If the current tile is entirely occupied, continue
|
||||
if ((FloorMask & flag) == 0)
|
||||
continue;
|
||||
|
||||
if ((WallMask & flag) == WallMask)
|
||||
continue;
|
||||
|
||||
if ((AirlockMask & flag) == AirlockMask)
|
||||
continue;
|
||||
|
||||
// Otherwise the tile can be added to this region
|
||||
visitedTiles.Add(current);
|
||||
visitedChunks.Add(chunkOrigin);
|
||||
|
||||
// Determine if we can propagate the region into its cardinally adjacent neighbors
|
||||
// To propagate to a neighbor, movement into the neighbors closest edge must not be
|
||||
// blocked, and vice versa
|
||||
|
||||
foreach (var (direction, tileOffset, reverseDirection) in _regionPropagationTable)
|
||||
{
|
||||
if (!RegionCanPropagateInDirection(chunk, current, direction))
|
||||
continue;
|
||||
|
||||
var neighbor = current + tileOffset;
|
||||
var neighborOrigin = SharedMapSystem.GetChunkIndices(neighbor, ChunkSize);
|
||||
|
||||
if (!component.Chunks.TryGetValue(neighborOrigin, out var neighborChunk))
|
||||
continue;
|
||||
|
||||
visitedChunks.Add(neighborOrigin);
|
||||
|
||||
if (!RegionCanPropagateInDirection(neighborChunk, neighbor, reverseDirection))
|
||||
continue;
|
||||
|
||||
tilesToVisit.Push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (visitedTiles, visitedChunks);
|
||||
}
|
||||
|
||||
private bool RegionCanPropagateInDirection(NavMapChunk chunk, Vector2i tile, AtmosDirection direction)
|
||||
{
|
||||
var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
|
||||
var idx = GetTileIndex(relative);
|
||||
var flag = chunk.TileData[idx];
|
||||
|
||||
if ((FloorMask & flag) == 0)
|
||||
return false;
|
||||
|
||||
var directionMask = 1 << (int)direction;
|
||||
var wallMask = (int)direction << (int)NavMapChunkType.Wall;
|
||||
var airlockMask = (int)direction << (int)NavMapChunkType.Airlock;
|
||||
|
||||
if ((wallMask & flag) > 0)
|
||||
return false;
|
||||
|
||||
if ((airlockMask & flag) > 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<(Vector2i, Vector2i)> GetMergedRegionTiles(HashSet<Vector2i> tiles)
|
||||
{
|
||||
if (!tiles.Any())
|
||||
return new();
|
||||
|
||||
var x = tiles.Select(t => t.X);
|
||||
var minX = x.Min();
|
||||
var maxX = x.Max();
|
||||
|
||||
var y = tiles.Select(t => t.Y);
|
||||
var minY = y.Min();
|
||||
var maxY = y.Max();
|
||||
|
||||
var matrix = new int[maxX - minX + 1, maxY - minY + 1];
|
||||
|
||||
foreach (var tile in tiles)
|
||||
{
|
||||
var a = tile.X - minX;
|
||||
var b = tile.Y - minY;
|
||||
|
||||
matrix[a, b] = 1;
|
||||
}
|
||||
|
||||
return GetMergedRegionTiles(matrix, new Vector2i(minX, minY));
|
||||
}
|
||||
|
||||
private List<(Vector2i, Vector2i)> GetMergedRegionTiles(int[,] matrix, Vector2i offset)
|
||||
{
|
||||
var output = new List<(Vector2i, Vector2i)>();
|
||||
|
||||
var rows = matrix.GetLength(0);
|
||||
var cols = matrix.GetLength(1);
|
||||
|
||||
var dp = new int[rows, cols];
|
||||
var coords = (new Vector2i(), new Vector2i());
|
||||
var maxArea = 0;
|
||||
|
||||
var count = 0;
|
||||
|
||||
while (!IsArrayEmpty(matrix))
|
||||
{
|
||||
count++;
|
||||
|
||||
if (count > rows * cols)
|
||||
break;
|
||||
|
||||
// Clear old values
|
||||
dp = new int[rows, cols];
|
||||
coords = (new Vector2i(), new Vector2i());
|
||||
maxArea = 0;
|
||||
|
||||
// Initialize the first row of dp
|
||||
for (int j = 0; j < cols; j++)
|
||||
{
|
||||
dp[0, j] = matrix[0, j];
|
||||
}
|
||||
|
||||
// Calculate dp values for remaining rows
|
||||
for (int i = 1; i < rows; i++)
|
||||
{
|
||||
for (int j = 0; j < cols; j++)
|
||||
dp[i, j] = matrix[i, j] == 1 ? dp[i - 1, j] + 1 : 0;
|
||||
}
|
||||
|
||||
// Find the largest rectangular area seeded for each position in the matrix
|
||||
for (int i = 0; i < rows; i++)
|
||||
{
|
||||
for (int j = 0; j < cols; j++)
|
||||
{
|
||||
int minWidth = dp[i, j];
|
||||
|
||||
for (int k = j; k >= 0; k--)
|
||||
{
|
||||
if (dp[i, k] <= 0)
|
||||
break;
|
||||
|
||||
minWidth = Math.Min(minWidth, dp[i, k]);
|
||||
var currArea = Math.Max(maxArea, minWidth * (j - k + 1));
|
||||
|
||||
if (currArea > maxArea)
|
||||
{
|
||||
maxArea = currArea;
|
||||
coords = (new Vector2i(i - minWidth + 1, k), new Vector2i(i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the recorded rectangle vertices
|
||||
output.Add((coords.Item1 + offset, coords.Item2 + offset));
|
||||
|
||||
// Removed the tiles covered by the rectangle from matrix
|
||||
for (int i = coords.Item1.X; i <= coords.Item2.X; i++)
|
||||
{
|
||||
for (int j = coords.Item1.Y; j <= coords.Item2.Y; j++)
|
||||
matrix[i, j] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private bool IsArrayEmpty(int[,] matrix)
|
||||
{
|
||||
for (int i = 0; i < matrix.GetLength(0); i++)
|
||||
{
|
||||
for (int j = 0; j < matrix.GetLength(1); j++)
|
||||
{
|
||||
if (matrix[i, j] == 1)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Pinpointer;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
@@ -16,6 +17,7 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
|
||||
{
|
||||
Dictionary<Vector2i, int[]> modifiedChunks;
|
||||
Dictionary<NetEntity, NavMapBeacon> beacons;
|
||||
Dictionary<NetEntity, NavMapRegionProperties> regions;
|
||||
|
||||
switch (args.Current)
|
||||
{
|
||||
@@ -23,6 +25,8 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
|
||||
{
|
||||
modifiedChunks = delta.ModifiedChunks;
|
||||
beacons = delta.Beacons;
|
||||
regions = delta.Regions;
|
||||
|
||||
foreach (var index in component.Chunks.Keys)
|
||||
{
|
||||
if (!delta.AllChunks!.Contains(index))
|
||||
@@ -35,6 +39,8 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
|
||||
{
|
||||
modifiedChunks = state.Chunks;
|
||||
beacons = state.Beacons;
|
||||
regions = state.Regions;
|
||||
|
||||
foreach (var index in component.Chunks.Keys)
|
||||
{
|
||||
if (!state.Chunks.ContainsKey(index))
|
||||
@@ -47,13 +53,54 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
|
||||
return;
|
||||
}
|
||||
|
||||
// Update region data and queue new regions for flooding
|
||||
var prevRegionOwners = component.RegionProperties.Keys.ToList();
|
||||
var validRegionOwners = new List<NetEntity>();
|
||||
|
||||
component.RegionProperties.Clear();
|
||||
|
||||
foreach (var (regionOwner, regionData) in regions)
|
||||
{
|
||||
if (!regionData.Seeds.Any())
|
||||
continue;
|
||||
|
||||
component.RegionProperties[regionOwner] = regionData;
|
||||
validRegionOwners.Add(regionOwner);
|
||||
|
||||
if (component.RegionOverlays.ContainsKey(regionOwner))
|
||||
continue;
|
||||
|
||||
if (component.QueuedRegionsToFlood.Contains(regionOwner))
|
||||
continue;
|
||||
|
||||
component.QueuedRegionsToFlood.Enqueue(regionOwner);
|
||||
}
|
||||
|
||||
// Remove stale region owners
|
||||
var regionOwnersToRemove = prevRegionOwners.Except(validRegionOwners);
|
||||
|
||||
foreach (var regionOwnerRemoved in regionOwnersToRemove)
|
||||
RemoveNavMapRegion(uid, component, regionOwnerRemoved);
|
||||
|
||||
// Modify chunks
|
||||
foreach (var (origin, chunk) in modifiedChunks)
|
||||
{
|
||||
var newChunk = new NavMapChunk(origin);
|
||||
Array.Copy(chunk, newChunk.TileData, chunk.Length);
|
||||
component.Chunks[origin] = newChunk;
|
||||
|
||||
// If the affected chunk intersects one or more regions, re-flood them
|
||||
if (!component.ChunkToRegionOwnerTable.TryGetValue(origin, out var affectedOwners))
|
||||
continue;
|
||||
|
||||
foreach (var affectedOwner in affectedOwners)
|
||||
{
|
||||
if (!component.QueuedRegionsToFlood.Contains(affectedOwner))
|
||||
component.QueuedRegionsToFlood.Enqueue(affectedOwner);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh beacons
|
||||
component.Beacons.Clear();
|
||||
foreach (var (nuid, beacon) in beacons)
|
||||
{
|
||||
|
||||
@@ -48,6 +48,7 @@ public partial class NavMapControl : MapGridControl
|
||||
public List<(Vector2, Vector2)> TileLines = new();
|
||||
public List<(Vector2, Vector2)> TileRects = new();
|
||||
public List<(Vector2[], Color)> TilePolygons = new();
|
||||
public List<NavMapRegionOverlay> RegionOverlays = new();
|
||||
|
||||
// Default colors
|
||||
public Color WallColor = new(102, 217, 102);
|
||||
@@ -228,7 +229,7 @@ public partial class NavMapControl : MapGridControl
|
||||
{
|
||||
if (!blip.Selectable)
|
||||
continue;
|
||||
|
||||
|
||||
var currentDistance = (_transformSystem.ToMapCoordinates(blip.Coordinates).Position - worldPosition).Length();
|
||||
|
||||
if (closestDistance < currentDistance || currentDistance * MinimapScale > MaxSelectableDistance)
|
||||
@@ -319,6 +320,22 @@ public partial class NavMapControl : MapGridControl
|
||||
}
|
||||
}
|
||||
|
||||
// Draw region overlays
|
||||
if (_grid != null)
|
||||
{
|
||||
foreach (var regionOverlay in RegionOverlays)
|
||||
{
|
||||
foreach (var gridCoords in regionOverlay.GridCoords)
|
||||
{
|
||||
var positionTopLeft = ScalePosition(new Vector2(gridCoords.Item1.X, -gridCoords.Item1.Y) - new Vector2(offset.X, -offset.Y));
|
||||
var positionBottomRight = ScalePosition(new Vector2(gridCoords.Item2.X + _grid.TileSize, -gridCoords.Item2.Y - _grid.TileSize) - new Vector2(offset.X, -offset.Y));
|
||||
|
||||
var box = new UIBox2(positionTopLeft, positionBottomRight);
|
||||
handle.DrawRect(box, regionOverlay.Color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw map lines
|
||||
if (TileLines.Any())
|
||||
{
|
||||
|
||||
@@ -51,6 +51,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
{
|
||||
// Reset on disconnect, just in case.
|
||||
_roles.Clear();
|
||||
_jobWhitelists.Clear();
|
||||
_roleBans.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,9 +60,6 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
{
|
||||
_sawmill.Debug($"Received roleban info containing {message.Bans.Count} entries.");
|
||||
|
||||
if (_roleBans.Equals(message.Bans))
|
||||
return;
|
||||
|
||||
_roleBans.Clear();
|
||||
_roleBans.AddRange(message.Bans);
|
||||
Updated?.Invoke();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using Content.Client.Power.APC.UI;
|
||||
using Content.Client.Power.APC.UI;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.APC;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client.Power.APC
|
||||
{
|
||||
@@ -22,6 +23,14 @@ namespace Content.Client.Power.APC
|
||||
_menu = this.CreateWindow<ApcMenu>();
|
||||
_menu.SetEntity(Owner);
|
||||
_menu.OnBreaker += BreakerPressed;
|
||||
|
||||
var hasAccess = false;
|
||||
if (PlayerManager.LocalEntity != null)
|
||||
{
|
||||
var accessReader = EntMan.System<AccessReaderSystem>();
|
||||
hasAccess = accessReader.IsAllowed((EntityUid)PlayerManager.LocalEntity, Owner);
|
||||
}
|
||||
_menu?.SetAccessEnabled(hasAccess);
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -36,19 +36,9 @@ namespace Content.Client.Power.APC.UI
|
||||
{
|
||||
var castState = (ApcBoundInterfaceState) state;
|
||||
|
||||
if (BreakerButton != null)
|
||||
if (!BreakerButton.Disabled)
|
||||
{
|
||||
if(castState.HasAccess == false)
|
||||
{
|
||||
BreakerButton.Disabled = true;
|
||||
BreakerButton.ToolTip = Loc.GetString("apc-component-insufficient-access");
|
||||
}
|
||||
else
|
||||
{
|
||||
BreakerButton.Disabled = false;
|
||||
BreakerButton.ToolTip = null;
|
||||
BreakerButton.Pressed = castState.MainBreaker;
|
||||
}
|
||||
BreakerButton.Pressed = castState.MainBreaker;
|
||||
}
|
||||
|
||||
if (PowerLabel != null)
|
||||
@@ -86,6 +76,20 @@ namespace Content.Client.Power.APC.UI
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAccessEnabled(bool hasAccess)
|
||||
{
|
||||
if(hasAccess)
|
||||
{
|
||||
BreakerButton.Disabled = false;
|
||||
BreakerButton.ToolTip = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
BreakerButton.Disabled = true;
|
||||
BreakerButton.ToolTip = Loc.GetString("apc-component-insufficient-access");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateChargeBarColor(float charge)
|
||||
{
|
||||
if (ChargeBar == null)
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinHeight="210">
|
||||
<BoxContainer Name="MainContainer" Orientation="Vertical">
|
||||
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'vending-machine-component-search-filter'}" HorizontalExpand="True" Margin ="4 4"/>
|
||||
<co:SearchListContainer Name="VendingContents" VerticalExpand="True" Margin="4 4"/>
|
||||
|
||||
@@ -28,6 +28,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
|
||||
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
|
||||
[Dependency] private readonly InputSystem _inputSystem = default!;
|
||||
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
|
||||
[Dependency] private readonly MapSystem _map = default!;
|
||||
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
@@ -109,11 +110,11 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
|
||||
|
||||
if (MapManager.TryFindGridAt(mousePos, out var gridUid, out _))
|
||||
{
|
||||
coordinates = EntityCoordinates.FromMap(gridUid, mousePos, TransformSystem, EntityManager);
|
||||
coordinates = TransformSystem.ToCoordinates(gridUid, mousePos);
|
||||
}
|
||||
else
|
||||
{
|
||||
coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, TransformSystem, EntityManager);
|
||||
coordinates = TransformSystem.ToCoordinates(_map.GetMap(mousePos.MapId), mousePos);
|
||||
}
|
||||
|
||||
// Heavy attack.
|
||||
|
||||
@@ -107,13 +107,41 @@ public sealed partial class TestPair
|
||||
/// <summary>
|
||||
/// Retrieve all entity prototypes that have some component.
|
||||
/// </summary>
|
||||
public List<EntityPrototype> GetPrototypesWithComponent<T>(
|
||||
public List<(EntityPrototype, T)> GetPrototypesWithComponent<T>(
|
||||
HashSet<string>? ignored = null,
|
||||
bool ignoreAbstract = true,
|
||||
bool ignoreTestPrototypes = true)
|
||||
where T : IComponent
|
||||
{
|
||||
var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(T));
|
||||
var list = new List<(EntityPrototype, T)>();
|
||||
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (ignored != null && ignored.Contains(proto.ID))
|
||||
continue;
|
||||
|
||||
if (ignoreAbstract && proto.Abstract)
|
||||
continue;
|
||||
|
||||
if (ignoreTestPrototypes && IsTestPrototype(proto))
|
||||
continue;
|
||||
|
||||
if (proto.Components.TryGetComponent(id, out var cmp))
|
||||
list.Add((proto, (T)cmp));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all entity prototypes that have some component.
|
||||
/// </summary>
|
||||
public List<EntityPrototype> GetPrototypesWithComponent(Type type,
|
||||
HashSet<string>? ignored = null,
|
||||
bool ignoreAbstract = true,
|
||||
bool ignoreTestPrototypes = true)
|
||||
{
|
||||
var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(type);
|
||||
var list = new List<EntityPrototype>();
|
||||
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
@@ -127,7 +155,7 @@ public sealed partial class TestPair
|
||||
continue;
|
||||
|
||||
if (proto.Components.ContainsKey(id))
|
||||
list.Add(proto);
|
||||
list.Add((proto));
|
||||
}
|
||||
|
||||
return list;
|
||||
|
||||
95
Content.IntegrationTests/Tests/Minds/RoleTests.cs
Normal file
95
Content.IntegrationTests/Tests/Minds/RoleTests.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Reflection;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Minds;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class RoleTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Check that any prototype with a <see cref="MindRoleComponent"/> is properly configured
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ValidateRolePrototypes()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
|
||||
var jobComp = pair.Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(JobRoleComponent));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var (proto, comp) in pair.GetPrototypesWithComponent<MindRoleComponent>())
|
||||
{
|
||||
Assert.That(comp.AntagPrototype == null || comp.JobPrototype == null, $"Role {proto.ID} has both a job and antag prototype.");
|
||||
Assert.That(!comp.ExclusiveAntag || comp.Antag, $"Role {proto.ID} is marked as an exclusive antag, despite not being an antag.");
|
||||
Assert.That(comp.Antag || comp.AntagPrototype == null, $"Role {proto.ID} has an antag prototype, despite not being an antag.");
|
||||
|
||||
if (comp.JobPrototype != null)
|
||||
Assert.That(proto.Components.ContainsKey(jobComp), $"Role {proto.ID} is a job, despite not having a job prototype.");
|
||||
|
||||
// It is possible that this is meant to be supported? Though I would assume that it would be for
|
||||
// admin / prototype uploads, and that pre-defined roles should still check this.
|
||||
Assert.That(!comp.Antag || comp.AntagPrototype != null , $"Role {proto.ID} is an antag, despite not having a antag prototype.");
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that any prototype with a <see cref="JobRoleComponent"/> also has a properly configured
|
||||
/// <see cref="MindRoleComponent"/>
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ValidateJobPrototypes()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
|
||||
var mindCompId = pair.Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(MindRoleComponent));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var (proto, comp) in pair.GetPrototypesWithComponent<JobRoleComponent>())
|
||||
{
|
||||
if (proto.Components.TryGetComponent(mindCompId, out var mindComp))
|
||||
Assert.That(((MindRoleComponent)mindComp).JobPrototype, Is.Not.Null);
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that any prototype with a component that inherits from <see cref="BaseMindRoleComponent"/> also has a
|
||||
/// <see cref="MindRoleComponent"/>
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ValidateRolesHaveMindRoleComp()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
|
||||
var refMan = pair.Server.ResolveDependency<IReflectionManager>();
|
||||
var mindCompId = pair.Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(MindRoleComponent));
|
||||
|
||||
var compTypes = refMan.GetAllChildren(typeof(BaseMindRoleComponent))
|
||||
.Append(typeof(RoleBriefingComponent))
|
||||
.Where(x => !x.IsAbstract);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var comp in compTypes)
|
||||
{
|
||||
foreach (var proto in pair.GetPrototypesWithComponent(comp))
|
||||
{
|
||||
Assert.That(proto.Components.ContainsKey(mindCompId), $"Role {proto.ID} does not have a {nameof(MindRoleComponent)} despite having a {comp.Name}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -35,15 +35,16 @@ public sealed class StartingGearPrototypeStorageTest
|
||||
{
|
||||
foreach (var gearProto in protos)
|
||||
{
|
||||
var backpackProto = ((IEquipmentLoadout) gearProto).GetGear("back");
|
||||
if (backpackProto == string.Empty)
|
||||
continue;
|
||||
|
||||
var bag = server.EntMan.SpawnEntity(backpackProto, coords);
|
||||
var ents = new ValueList<EntityUid>();
|
||||
|
||||
foreach (var (slot, entProtos) in gearProto.Storage)
|
||||
{
|
||||
ents.Clear();
|
||||
var storageProto = ((IEquipmentLoadout)gearProto).GetGear(slot);
|
||||
if (storageProto == string.Empty)
|
||||
continue;
|
||||
|
||||
var bag = server.EntMan.SpawnEntity(storageProto, coords);
|
||||
if (entProtos.Count == 0)
|
||||
continue;
|
||||
|
||||
@@ -59,9 +60,8 @@ public sealed class StartingGearPrototypeStorageTest
|
||||
|
||||
server.EntMan.DeleteEntity(ent);
|
||||
}
|
||||
server.EntMan.DeleteEntity(bag);
|
||||
}
|
||||
|
||||
server.EntMan.DeleteEntity(bag);
|
||||
}
|
||||
|
||||
mapManager.DeleteMap(testMap.MapId);
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class PrototypeSaveTest
|
||||
|
||||
await pair.Client.WaitPost(() =>
|
||||
{
|
||||
foreach (var proto in pair.GetPrototypesWithComponent<ItemComponent>(Ignored))
|
||||
foreach (var (proto, _) in pair.GetPrototypesWithComponent<ItemComponent>(Ignored))
|
||||
{
|
||||
var dummy = pair.Client.EntMan.Spawn(proto.ID);
|
||||
pair.Client.EntMan.RunMapInit(dummy, pair.Client.MetaData(dummy));
|
||||
|
||||
@@ -94,14 +94,13 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
await Assert.MultipleAsync(async () =>
|
||||
{
|
||||
foreach (var proto in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<EntityStorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
StorageComponent? storage = null;
|
||||
ItemComponent? item = null;
|
||||
StorageFillComponent fill = default!;
|
||||
var size = 0;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
@@ -112,7 +111,6 @@ namespace Content.IntegrationTests.Tests
|
||||
}
|
||||
|
||||
proto.TryGetComponent("Item", out item);
|
||||
fill = (StorageFillComponent) proto.Components[id].Component;
|
||||
size = GetFillSize(fill, false, protoMan, itemSys);
|
||||
});
|
||||
|
||||
@@ -179,7 +177,7 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
foreach (var proto in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<StorageComponent>(compFact))
|
||||
continue;
|
||||
@@ -192,7 +190,6 @@ namespace Content.IntegrationTests.Tests
|
||||
if (entStorage == null)
|
||||
return;
|
||||
|
||||
var fill = (StorageFillComponent) proto.Components[id].Component;
|
||||
var size = GetFillSize(fill, true, protoMan, itemSys);
|
||||
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
|
||||
$"{proto.ID} storage fill is too large.");
|
||||
|
||||
@@ -14,13 +14,13 @@ using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Administration.Managers;
|
||||
|
||||
@@ -45,14 +45,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
public const string SawmillId = "admin.bans";
|
||||
public const string JobPrefix = "Job:";
|
||||
|
||||
private readonly Dictionary<NetUserId, HashSet<ServerRoleBanDef>> _cachedRoleBans = new();
|
||||
private readonly Dictionary<ICommonSession, List<ServerRoleBanDef>> _cachedRoleBans = new();
|
||||
// Cached ban exemption flags are used to handle
|
||||
private readonly Dictionary<ICommonSession, ServerBanExemptFlags> _cachedBanExemptions = new();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
|
||||
_netManager.RegisterNetMessage<MsgRoleBans>();
|
||||
|
||||
_db.SubscribeToNotifications(OnDatabaseNotification);
|
||||
@@ -63,12 +61,23 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
private async Task CachePlayerData(ICommonSession player, CancellationToken cancel)
|
||||
{
|
||||
// Yeah so role ban loading code isn't integrated with exempt flag loading code.
|
||||
// Have you seen how garbage role ban code code is? I don't feel like refactoring it right now.
|
||||
|
||||
var flags = await _db.GetBanExemption(player.UserId, cancel);
|
||||
|
||||
var netChannel = player.Channel;
|
||||
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
|
||||
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, false);
|
||||
|
||||
var userRoleBans = new List<ServerRoleBanDef>();
|
||||
foreach (var ban in roleBans)
|
||||
{
|
||||
userRoleBans.Add(ban);
|
||||
}
|
||||
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
_cachedBanExemptions[player] = flags;
|
||||
_cachedRoleBans[player] = userRoleBans;
|
||||
|
||||
SendRoleBans(player);
|
||||
}
|
||||
|
||||
private void ClearPlayerData(ICommonSession player)
|
||||
@@ -76,25 +85,15 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
_cachedBanExemptions.Remove(player);
|
||||
}
|
||||
|
||||
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
if (e.NewStatus != SessionStatus.Connected || _cachedRoleBans.ContainsKey(e.Session.UserId))
|
||||
return;
|
||||
|
||||
var netChannel = e.Session.Channel;
|
||||
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
|
||||
await CacheDbRoleBans(e.Session.UserId, netChannel.RemoteEndPoint.Address, hwId);
|
||||
|
||||
SendRoleBans(e.Session);
|
||||
}
|
||||
|
||||
private async Task<bool> AddRoleBan(ServerRoleBanDef banDef)
|
||||
{
|
||||
banDef = await _db.AddServerRoleBanAsync(banDef);
|
||||
|
||||
if (banDef.UserId != null)
|
||||
if (banDef.UserId != null
|
||||
&& _playerManager.TryGetSessionById(banDef.UserId, out var player)
|
||||
&& _cachedRoleBans.TryGetValue(player, out var cachedBans))
|
||||
{
|
||||
_cachedRoleBans.GetOrNew(banDef.UserId.Value).Add(banDef);
|
||||
cachedBans.Add(banDef);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -102,31 +101,21 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
public HashSet<string>? GetRoleBans(NetUserId playerUserId)
|
||||
{
|
||||
return _cachedRoleBans.TryGetValue(playerUserId, out var roleBans)
|
||||
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
|
||||
return null;
|
||||
|
||||
return _cachedRoleBans.TryGetValue(session, out var roleBans)
|
||||
? roleBans.Select(banDef => banDef.Role).ToHashSet()
|
||||
: null;
|
||||
}
|
||||
|
||||
private async Task CacheDbRoleBans(NetUserId userId, IPAddress? address = null, ImmutableArray<byte>? hwId = null)
|
||||
{
|
||||
var roleBans = await _db.GetServerRoleBansAsync(address, userId, hwId, false);
|
||||
|
||||
var userRoleBans = new HashSet<ServerRoleBanDef>();
|
||||
foreach (var ban in roleBans)
|
||||
{
|
||||
userRoleBans.Add(ban);
|
||||
}
|
||||
|
||||
_cachedRoleBans[userId] = userRoleBans;
|
||||
}
|
||||
|
||||
public void Restart()
|
||||
{
|
||||
// Clear out players that have disconnected.
|
||||
var toRemove = new List<NetUserId>();
|
||||
var toRemove = new ValueList<ICommonSession>();
|
||||
foreach (var player in _cachedRoleBans.Keys)
|
||||
{
|
||||
if (!_playerManager.TryGetSessionById(player, out _))
|
||||
if (player.Status == SessionStatus.Disconnected)
|
||||
toRemove.Add(player);
|
||||
}
|
||||
|
||||
@@ -138,7 +127,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
// Check for expired bans
|
||||
foreach (var roleBans in _cachedRoleBans.Values)
|
||||
{
|
||||
roleBans.RemoveWhere(ban => DateTimeOffset.Now > ban.ExpirationTime);
|
||||
roleBans.RemoveAll(ban => DateTimeOffset.Now > ban.ExpirationTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,9 +270,9 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires));
|
||||
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-success", ("target", targetUsername ?? "null"), ("role", role), ("reason", reason), ("length", length)));
|
||||
|
||||
if (target != null)
|
||||
if (target != null && _playerManager.TryGetSessionById(target.Value, out var session))
|
||||
{
|
||||
SendRoleBans(target.Value);
|
||||
SendRoleBans(session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,10 +300,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
await _db.AddServerRoleUnbanAsync(new ServerRoleUnbanDef(banId, unbanningAdmin, DateTimeOffset.Now));
|
||||
|
||||
if (ban.UserId is { } player && _cachedRoleBans.TryGetValue(player, out var roleBans))
|
||||
if (ban.UserId is { } player
|
||||
&& _playerManager.TryGetSessionById(player, out var session)
|
||||
&& _cachedRoleBans.TryGetValue(session, out var roleBans))
|
||||
{
|
||||
roleBans.RemoveWhere(roleBan => roleBan.Id == ban.Id);
|
||||
SendRoleBans(player);
|
||||
roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id);
|
||||
SendRoleBans(session);
|
||||
}
|
||||
|
||||
return $"Pardoned ban with id {banId}";
|
||||
@@ -322,8 +313,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId)
|
||||
{
|
||||
if (!_cachedRoleBans.TryGetValue(playerUserId, out var roleBans))
|
||||
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
|
||||
return null;
|
||||
|
||||
if (!_cachedRoleBans.TryGetValue(session, out var roleBans))
|
||||
return null;
|
||||
|
||||
return roleBans
|
||||
.Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal))
|
||||
.Select(ban => new ProtoId<JobPrototype>(ban.Role[JobPrefix.Length..]))
|
||||
@@ -331,19 +326,9 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void SendRoleBans(NetUserId userId)
|
||||
{
|
||||
if (!_playerManager.TryGetSessionById(userId, out var player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SendRoleBans(player);
|
||||
}
|
||||
|
||||
public void SendRoleBans(ICommonSession pSession)
|
||||
{
|
||||
var roleBans = _cachedRoleBans.GetValueOrDefault(pSession.UserId) ?? new HashSet<ServerRoleBanDef>();
|
||||
var roleBans = _cachedRoleBans.GetValueOrDefault(pSession) ?? new List<ServerRoleBanDef>();
|
||||
var bans = new MsgRoleBans()
|
||||
{
|
||||
Bans = roleBans.Select(o => o.Role).ToList()
|
||||
|
||||
@@ -47,12 +47,6 @@ public interface IBanManager
|
||||
/// <param name="unbanTime">The time at which this role ban was pardoned.</param>
|
||||
public Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime);
|
||||
|
||||
/// <summary>
|
||||
/// Sends role bans to the target
|
||||
/// </summary>
|
||||
/// <param name="pSession">Player's user ID</param>
|
||||
public void SendRoleBans(NetUserId userId);
|
||||
|
||||
/// <summary>
|
||||
/// Sends role bans to the target
|
||||
/// </summary>
|
||||
|
||||
@@ -95,9 +95,10 @@ public sealed partial class AdminVerbSystem
|
||||
if (HasComp<MapComponent>(args.Target) || HasComp<MapGridComponent>(args.Target))
|
||||
return;
|
||||
|
||||
var explodeName = Loc.GetString("admin-smite-explode-name").ToLowerInvariant();
|
||||
Verb explode = new()
|
||||
{
|
||||
Text = "admin-smite-explode-name",
|
||||
Text = explodeName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/smite.svg.192dpi.png")),
|
||||
Act = () =>
|
||||
@@ -111,13 +112,14 @@ public sealed partial class AdminVerbSystem
|
||||
_bodySystem.GibBody(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-explode-description")
|
||||
Message = string.Join(": ", explodeName, Loc.GetString("admin-smite-explode-description")) // we do this so the description tells admins the Text to run it via console.
|
||||
};
|
||||
args.Verbs.Add(explode);
|
||||
|
||||
var chessName = Loc.GetString("admin-smite-chess-dimension-name").ToLowerInvariant();
|
||||
Verb chess = new()
|
||||
{
|
||||
Text = "admin-smite-chess-dimension-name",
|
||||
Text = chessName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Tabletop/chessboard.rsi"), "chessboard"),
|
||||
Act = () =>
|
||||
@@ -137,12 +139,13 @@ public sealed partial class AdminVerbSystem
|
||||
xform.WorldRotation = Angle.Zero;
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-chess-dimension-description")
|
||||
Message = string.Join(": ", chessName, Loc.GetString("admin-smite-chess-dimension-description"))
|
||||
};
|
||||
args.Verbs.Add(chess);
|
||||
|
||||
if (TryComp<FlammableComponent>(args.Target, out var flammable))
|
||||
{
|
||||
var flamesName = Loc.GetString("admin-smite-set-alight-name").ToLowerInvariant();
|
||||
Verb flames = new()
|
||||
{
|
||||
Text = "admin-smite-set-alight-name",
|
||||
@@ -160,14 +163,15 @@ public sealed partial class AdminVerbSystem
|
||||
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-set-alight-description")
|
||||
Message = string.Join(": ", flamesName, Loc.GetString("admin-smite-set-alight-description"))
|
||||
};
|
||||
args.Verbs.Add(flames);
|
||||
}
|
||||
|
||||
var monkeyName = Loc.GetString("admin-smite-monkeyify-name").ToLowerInvariant();
|
||||
Verb monkey = new()
|
||||
{
|
||||
Text = "admin-smite-monkeyify-name",
|
||||
Text = monkeyName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Animals/monkey.rsi"), "monkey"),
|
||||
Act = () =>
|
||||
@@ -175,13 +179,14 @@ public sealed partial class AdminVerbSystem
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminMonkeySmite");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-monkeyify-description")
|
||||
Message = string.Join(": ", monkeyName, Loc.GetString("admin-smite-monkeyify-description"))
|
||||
};
|
||||
args.Verbs.Add(monkey);
|
||||
|
||||
var disposalBinName = Loc.GetString("admin-smite-garbage-can-name").ToLowerInvariant();
|
||||
Verb disposalBin = new()
|
||||
{
|
||||
Text = "admin-smite-electrocute-name",
|
||||
Text = disposalBinName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Piping/disposal.rsi"), "disposal"),
|
||||
Act = () =>
|
||||
@@ -189,16 +194,17 @@ public sealed partial class AdminVerbSystem
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminDisposalsSmite");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-garbage-can-description")
|
||||
Message = string.Join(": ", disposalBinName, Loc.GetString("admin-smite-garbage-can-description"))
|
||||
};
|
||||
args.Verbs.Add(disposalBin);
|
||||
|
||||
if (TryComp<DamageableComponent>(args.Target, out var damageable) &&
|
||||
HasComp<MobStateComponent>(args.Target))
|
||||
{
|
||||
var hardElectrocuteName = Loc.GetString("admin-smite-electrocute-name").ToLowerInvariant();
|
||||
Verb hardElectrocute = new()
|
||||
{
|
||||
Text = "admin-smite-creampie-name",
|
||||
Text = hardElectrocuteName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Hands/Gloves/Color/yellow.rsi"), "icon"),
|
||||
Act = () =>
|
||||
@@ -234,16 +240,17 @@ public sealed partial class AdminVerbSystem
|
||||
TimeSpan.FromSeconds(30), refresh: true, ignoreInsulation: true);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-electrocute-description")
|
||||
Message = string.Join(": ", hardElectrocuteName, Loc.GetString("admin-smite-electrocute-description"))
|
||||
};
|
||||
args.Verbs.Add(hardElectrocute);
|
||||
}
|
||||
|
||||
if (TryComp<CreamPiedComponent>(args.Target, out var creamPied))
|
||||
{
|
||||
var creamPieName = Loc.GetString("admin-smite-creampie-name").ToLowerInvariant();
|
||||
Verb creamPie = new()
|
||||
{
|
||||
Text = "admin-smite-remove-blood-name",
|
||||
Text = creamPieName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Consumable/Food/Baked/pie.rsi"), "plain-slice"),
|
||||
Act = () =>
|
||||
@@ -251,16 +258,17 @@ public sealed partial class AdminVerbSystem
|
||||
_creamPieSystem.SetCreamPied(args.Target, creamPied, true);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-creampie-description")
|
||||
Message = string.Join(": ", creamPieName, Loc.GetString("admin-smite-creampie-description"))
|
||||
};
|
||||
args.Verbs.Add(creamPie);
|
||||
}
|
||||
|
||||
if (TryComp<BloodstreamComponent>(args.Target, out var bloodstream))
|
||||
{
|
||||
var bloodRemovalName = Loc.GetString("admin-smite-remove-blood-name").ToLowerInvariant();
|
||||
Verb bloodRemoval = new()
|
||||
{
|
||||
Text = "admin-smite-vomit-organs-name",
|
||||
Text = bloodRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Fluids/tomato_splat.rsi"), "puddle-1"),
|
||||
Act = () =>
|
||||
@@ -273,7 +281,7 @@ public sealed partial class AdminVerbSystem
|
||||
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-remove-blood-description")
|
||||
Message = string.Join(": ", bloodRemovalName, Loc.GetString("admin-smite-remove-blood-description"))
|
||||
};
|
||||
args.Verbs.Add(bloodRemoval);
|
||||
}
|
||||
@@ -281,9 +289,10 @@ public sealed partial class AdminVerbSystem
|
||||
// bobby...
|
||||
if (TryComp<BodyComponent>(args.Target, out var body))
|
||||
{
|
||||
var vomitOrgansName = Loc.GetString("admin-smite-vomit-organs-name").ToLowerInvariant();
|
||||
Verb vomitOrgans = new()
|
||||
{
|
||||
Text = "admin-smite-remove-hands-name",
|
||||
Text = vomitOrgansName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Fluids/vomit_toxin.rsi"), "vomit_toxin-1"),
|
||||
Act = () =>
|
||||
@@ -305,13 +314,14 @@ public sealed partial class AdminVerbSystem
|
||||
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-vomit-organs-description")
|
||||
Message = string.Join(": ", vomitOrgansName, Loc.GetString("admin-smite-vomit-organs-description"))
|
||||
};
|
||||
args.Verbs.Add(vomitOrgans);
|
||||
|
||||
var handsRemovalName = Loc.GetString("admin-smite-remove-hands-name").ToLowerInvariant();
|
||||
Verb handsRemoval = new()
|
||||
{
|
||||
Text = "admin-smite-remove-hand-name",
|
||||
Text = handsRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/remove-hands.png")),
|
||||
Act = () =>
|
||||
@@ -327,13 +337,14 @@ public sealed partial class AdminVerbSystem
|
||||
Filter.PvsExcept(args.Target), true, PopupType.Medium);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-remove-hands-description")
|
||||
Message = string.Join(": ", handsRemovalName, Loc.GetString("admin-smite-remove-hands-description"))
|
||||
};
|
||||
args.Verbs.Add(handsRemoval);
|
||||
|
||||
var handRemovalName = Loc.GetString("admin-smite-remove-hand-name").ToLowerInvariant();
|
||||
Verb handRemoval = new()
|
||||
{
|
||||
Text = "admin-smite-pinball-name",
|
||||
Text = handRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/remove-hand.png")),
|
||||
Act = () =>
|
||||
@@ -350,13 +361,14 @@ public sealed partial class AdminVerbSystem
|
||||
Filter.PvsExcept(args.Target), true, PopupType.Medium);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-remove-hand-description")
|
||||
Message = string.Join(": ", handRemovalName, Loc.GetString("admin-smite-remove-hand-description"))
|
||||
};
|
||||
args.Verbs.Add(handRemoval);
|
||||
|
||||
var stomachRemovalName = Loc.GetString("admin-smite-stomach-removal-name").ToLowerInvariant();
|
||||
Verb stomachRemoval = new()
|
||||
{
|
||||
Text = "admin-smite-yeet-name",
|
||||
Text = stomachRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "stomach"),
|
||||
Act = () =>
|
||||
@@ -370,13 +382,14 @@ public sealed partial class AdminVerbSystem
|
||||
args.Target, PopupType.LargeCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-stomach-removal-description"),
|
||||
Message = string.Join(": ", stomachRemovalName, Loc.GetString("admin-smite-stomach-removal-description"))
|
||||
};
|
||||
args.Verbs.Add(stomachRemoval);
|
||||
|
||||
var lungRemovalName = Loc.GetString("admin-smite-lung-removal-name").ToLowerInvariant();
|
||||
Verb lungRemoval = new()
|
||||
{
|
||||
Text = "admin-smite-become-bread-name",
|
||||
Text = lungRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "lung-r"),
|
||||
Act = () =>
|
||||
@@ -390,16 +403,17 @@ public sealed partial class AdminVerbSystem
|
||||
args.Target, PopupType.LargeCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-lung-removal-description"),
|
||||
Message = string.Join(": ", lungRemovalName, Loc.GetString("admin-smite-lung-removal-description"))
|
||||
};
|
||||
args.Verbs.Add(lungRemoval);
|
||||
}
|
||||
|
||||
if (TryComp<PhysicsComponent>(args.Target, out var physics))
|
||||
{
|
||||
var pinballName = Loc.GetString("admin-smite-pinball-name").ToLowerInvariant();
|
||||
Verb pinball = new()
|
||||
{
|
||||
Text = "admin-smite-ghostkick-name",
|
||||
Text = pinballName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "basketball"),
|
||||
Act = () =>
|
||||
@@ -427,13 +441,14 @@ public sealed partial class AdminVerbSystem
|
||||
_physics.SetAngularDamping(args.Target, physics, 0f);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-pinball-description")
|
||||
Message = string.Join(": ", pinballName, Loc.GetString("admin-smite-pinball-description"))
|
||||
};
|
||||
args.Verbs.Add(pinball);
|
||||
|
||||
var yeetName = Loc.GetString("admin-smite-yeet-name").ToLowerInvariant();
|
||||
Verb yeet = new()
|
||||
{
|
||||
Text = "admin-smite-nyanify-name",
|
||||
Text = yeetName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")),
|
||||
Act = () =>
|
||||
@@ -457,11 +472,12 @@ public sealed partial class AdminVerbSystem
|
||||
_physics.SetAngularDamping(args.Target, physics, 0f);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-yeet-description")
|
||||
Message = string.Join(": ", yeetName, Loc.GetString("admin-smite-yeet-description"))
|
||||
};
|
||||
args.Verbs.Add(yeet);
|
||||
}
|
||||
|
||||
var breadName = Loc.GetString("admin-smite-become-bread-name").ToLowerInvariant(); // Will I get cancelled for breadName-ing you?
|
||||
Verb bread = new()
|
||||
{
|
||||
Text = "admin-smite-kill-sign-name",
|
||||
@@ -472,10 +488,11 @@ public sealed partial class AdminVerbSystem
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminBreadSmite");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-become-bread-description")
|
||||
Message = string.Join(": ", breadName, Loc.GetString("admin-smite-become-bread-description"))
|
||||
};
|
||||
args.Verbs.Add(bread);
|
||||
|
||||
var mouseName = Loc.GetString("admin-smite-become-mouse-name").ToLowerInvariant();
|
||||
Verb mouse = new()
|
||||
{
|
||||
Text = "admin-smite-cluwne-name",
|
||||
@@ -486,15 +503,16 @@ public sealed partial class AdminVerbSystem
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminMouseSmite");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-become-mouse-description")
|
||||
Message = string.Join(": ", mouseName, Loc.GetString("admin-smite-become-mouse-description"))
|
||||
};
|
||||
args.Verbs.Add(mouse);
|
||||
|
||||
if (TryComp<ActorComponent>(args.Target, out var actorComponent))
|
||||
{
|
||||
var ghostKickName = Loc.GetString("admin-smite-ghostkick-name").ToLowerInvariant();
|
||||
Verb ghostKick = new()
|
||||
{
|
||||
Text = "admin-smite-anger-pointing-arrows-name",
|
||||
Text = ghostKickName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/gavel.svg.192dpi.png")),
|
||||
Act = () =>
|
||||
@@ -502,15 +520,18 @@ public sealed partial class AdminVerbSystem
|
||||
_ghostKickManager.DoDisconnect(actorComponent.PlayerSession.Channel, "Smitten.");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-ghostkick-description")
|
||||
Message = string.Join(": ", ghostKickName, Loc.GetString("admin-smite-ghostkick-description"))
|
||||
|
||||
};
|
||||
args.Verbs.Add(ghostKick);
|
||||
}
|
||||
|
||||
if (TryComp<InventoryComponent>(args.Target, out var inventory)) {
|
||||
if (TryComp<InventoryComponent>(args.Target, out var inventory))
|
||||
{
|
||||
var nyanifyName = Loc.GetString("admin-smite-nyanify-name").ToLowerInvariant();
|
||||
Verb nyanify = new()
|
||||
{
|
||||
Text = "admin-smite-dust-name",
|
||||
Text = nyanifyName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Head/Hats/catears.rsi"), "icon"),
|
||||
Act = () =>
|
||||
@@ -521,13 +542,14 @@ public sealed partial class AdminVerbSystem
|
||||
_inventorySystem.TryEquip(args.Target, ears, "head", true, true, false, inventory);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-nyanify-description")
|
||||
Message = string.Join(": ", nyanifyName, Loc.GetString("admin-smite-nyanify-description"))
|
||||
};
|
||||
args.Verbs.Add(nyanify);
|
||||
|
||||
var killSignName = Loc.GetString("admin-smite-kill-sign-name").ToLowerInvariant();
|
||||
Verb killSign = new()
|
||||
{
|
||||
Text = "admin-smite-buffering-name",
|
||||
Text = killSignName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Misc/killsign.rsi"), "icon"),
|
||||
Act = () =>
|
||||
@@ -535,13 +557,14 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<KillSignComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-kill-sign-description")
|
||||
Message = string.Join(": ", killSignName, Loc.GetString("admin-smite-kill-sign-description"))
|
||||
};
|
||||
args.Verbs.Add(killSign);
|
||||
|
||||
var cluwneName = Loc.GetString("admin-smite-cluwne-name").ToLowerInvariant();
|
||||
Verb cluwne = new()
|
||||
{
|
||||
Text = "admin-smite-become-instrument-name",
|
||||
Text = cluwneName,
|
||||
Category = VerbCategory.Smite,
|
||||
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Mask/cluwne.rsi"), "icon"),
|
||||
@@ -551,13 +574,14 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<CluwneComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-cluwne-description")
|
||||
Message = string.Join(": ", cluwneName, Loc.GetString("admin-smite-cluwne-description"))
|
||||
};
|
||||
args.Verbs.Add(cluwne);
|
||||
|
||||
var maidenName = Loc.GetString("admin-smite-maid-name").ToLowerInvariant();
|
||||
Verb maiden = new()
|
||||
{
|
||||
Text = "admin-smite-remove-gravity-name",
|
||||
Text = maidenName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Uniforms/Jumpskirt/janimaid.rsi"), "icon"),
|
||||
Act = () =>
|
||||
@@ -570,14 +594,15 @@ public sealed partial class AdminVerbSystem
|
||||
});
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-maid-description")
|
||||
Message = string.Join(": ", maidenName, Loc.GetString("admin-smite-maid-description"))
|
||||
};
|
||||
args.Verbs.Add(maiden);
|
||||
}
|
||||
|
||||
var angerPointingArrowsName = Loc.GetString("admin-smite-anger-pointing-arrows-name").ToLowerInvariant();
|
||||
Verb angerPointingArrows = new()
|
||||
{
|
||||
Text = "admin-smite-reptilian-species-swap-name",
|
||||
Text = angerPointingArrowsName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Interface/Misc/pointing.rsi"), "pointing"),
|
||||
Act = () =>
|
||||
@@ -585,13 +610,14 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<PointingArrowAngeringComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-anger-pointing-arrows-description")
|
||||
Message = string.Join(": ", angerPointingArrowsName, Loc.GetString("admin-smite-anger-pointing-arrows-description"))
|
||||
};
|
||||
args.Verbs.Add(angerPointingArrows);
|
||||
|
||||
var dustName = Loc.GetString("admin-smite-dust-name").ToLowerInvariant();
|
||||
Verb dust = new()
|
||||
{
|
||||
Text = "admin-smite-locker-stuff-name",
|
||||
Text = dustName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Materials/materials.rsi"), "ash"),
|
||||
Act = () =>
|
||||
@@ -601,13 +627,14 @@ public sealed partial class AdminVerbSystem
|
||||
_popupSystem.PopupEntity(Loc.GetString("admin-smite-turned-ash-other", ("name", args.Target)), args.Target, PopupType.LargeCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-dust-description"),
|
||||
Message = string.Join(": ", dustName, Loc.GetString("admin-smite-dust-description"))
|
||||
};
|
||||
args.Verbs.Add(dust);
|
||||
|
||||
var youtubeVideoSimulationName = Loc.GetString("admin-smite-buffering-name").ToLowerInvariant();
|
||||
Verb youtubeVideoSimulation = new()
|
||||
{
|
||||
Text = "admin-smite-headstand-name",
|
||||
Text = youtubeVideoSimulationName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Misc/buffering_smite_icon.png")),
|
||||
Act = () =>
|
||||
@@ -615,10 +642,11 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<BufferingComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-buffering-description"),
|
||||
Message = string.Join(": ", youtubeVideoSimulationName, Loc.GetString("admin-smite-buffering-description"))
|
||||
};
|
||||
args.Verbs.Add(youtubeVideoSimulation);
|
||||
|
||||
var instrumentationName = Loc.GetString("admin-smite-become-instrument-name").ToLowerInvariant();
|
||||
Verb instrumentation = new()
|
||||
{
|
||||
Text = "admin-smite-become-mouse-name",
|
||||
@@ -629,13 +657,14 @@ public sealed partial class AdminVerbSystem
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminInstrumentSmite");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-become-instrument-description"),
|
||||
Message = string.Join(": ", instrumentationName, Loc.GetString("admin-smite-become-instrument-description"))
|
||||
};
|
||||
args.Verbs.Add(instrumentation);
|
||||
|
||||
var noGravityName = Loc.GetString("admin-smite-remove-gravity-name").ToLowerInvariant();
|
||||
Verb noGravity = new()
|
||||
{
|
||||
Text = "admin-smite-maid-name",
|
||||
Text = noGravityName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Machines/gravity_generator.rsi"), "off"),
|
||||
Act = () =>
|
||||
@@ -646,13 +675,14 @@ public sealed partial class AdminVerbSystem
|
||||
Dirty(args.Target, grav);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-remove-gravity-description"),
|
||||
Message = string.Join(": ", noGravityName, Loc.GetString("admin-smite-remove-gravity-description"))
|
||||
};
|
||||
args.Verbs.Add(noGravity);
|
||||
|
||||
var reptilianName = Loc.GetString("admin-smite-reptilian-species-swap-name").ToLowerInvariant();
|
||||
Verb reptilian = new()
|
||||
{
|
||||
Text = "admin-smite-zoom-in-name",
|
||||
Text = reptilianName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "plushie_lizard"),
|
||||
Act = () =>
|
||||
@@ -660,13 +690,14 @@ public sealed partial class AdminVerbSystem
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminLizardSmite");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-reptilian-species-swap-description"),
|
||||
Message = string.Join(": ", reptilianName, Loc.GetString("admin-smite-reptilian-species-swap-description"))
|
||||
};
|
||||
args.Verbs.Add(reptilian);
|
||||
|
||||
var lockerName = Loc.GetString("admin-smite-locker-stuff-name").ToLowerInvariant();
|
||||
Verb locker = new()
|
||||
{
|
||||
Text = "admin-smite-flip-eye-name",
|
||||
Text = lockerName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Storage/closet.rsi"), "generic"),
|
||||
Act = () =>
|
||||
@@ -682,10 +713,11 @@ public sealed partial class AdminVerbSystem
|
||||
_weldableSystem.SetWeldedState(locker, true);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-locker-stuff-description"),
|
||||
Message = string.Join(": ", lockerName, Loc.GetString("admin-smite-locker-stuff-description"))
|
||||
};
|
||||
args.Verbs.Add(locker);
|
||||
|
||||
var headstandName = Loc.GetString("admin-smite-headstand-name").ToLowerInvariant();
|
||||
Verb headstand = new()
|
||||
{
|
||||
Text = "admin-smite-run-walk-swap-name",
|
||||
@@ -696,13 +728,14 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<HeadstandComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-headstand-description"),
|
||||
Message = string.Join(": ", headstandName, Loc.GetString("admin-smite-headstand-description"))
|
||||
};
|
||||
args.Verbs.Add(headstand);
|
||||
|
||||
var zoomInName = Loc.GetString("admin-smite-zoom-in-name").ToLowerInvariant();
|
||||
Verb zoomIn = new()
|
||||
{
|
||||
Text = "admin-smite-super-speed-name",
|
||||
Text = zoomInName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/zoom.png")),
|
||||
Act = () =>
|
||||
@@ -711,13 +744,14 @@ public sealed partial class AdminVerbSystem
|
||||
_eyeSystem.SetZoom(args.Target, eye.TargetZoom * 0.2f, ignoreLimits: true);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-zoom-in-description"),
|
||||
Message = string.Join(": ", zoomInName, Loc.GetString("admin-smite-zoom-in-description"))
|
||||
};
|
||||
args.Verbs.Add(zoomIn);
|
||||
|
||||
var flipEyeName = Loc.GetString("admin-smite-flip-eye-name").ToLowerInvariant();
|
||||
Verb flipEye = new()
|
||||
{
|
||||
Text = "admin-smite-stomach-removal-name",
|
||||
Text = flipEyeName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/flip.png")),
|
||||
Act = () =>
|
||||
@@ -726,13 +760,14 @@ public sealed partial class AdminVerbSystem
|
||||
_eyeSystem.SetZoom(args.Target, eye.TargetZoom * -1, ignoreLimits: true);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-flip-eye-description"),
|
||||
Message = string.Join(": ", flipEyeName, Loc.GetString("admin-smite-flip-eye-description"))
|
||||
};
|
||||
args.Verbs.Add(flipEye);
|
||||
|
||||
var runWalkSwapName = Loc.GetString("admin-smite-run-walk-swap-name").ToLowerInvariant();
|
||||
Verb runWalkSwap = new()
|
||||
{
|
||||
Text = "admin-smite-speak-backwards-name",
|
||||
Text = runWalkSwapName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/run-walk-swap.png")),
|
||||
Act = () =>
|
||||
@@ -746,13 +781,14 @@ public sealed partial class AdminVerbSystem
|
||||
args.Target, PopupType.LargeCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-run-walk-swap-description"),
|
||||
Message = string.Join(": ", runWalkSwapName, Loc.GetString("admin-smite-run-walk-swap-description"))
|
||||
};
|
||||
args.Verbs.Add(runWalkSwap);
|
||||
|
||||
var backwardsAccentName = Loc.GetString("admin-smite-speak-backwards-name").ToLowerInvariant();
|
||||
Verb backwardsAccent = new()
|
||||
{
|
||||
Text = "admin-smite-lung-removal-name",
|
||||
Text = backwardsAccentName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/help-backwards.png")),
|
||||
Act = () =>
|
||||
@@ -760,13 +796,14 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<BackwardsAccentComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-speak-backwards-description"),
|
||||
Message = string.Join(": ", backwardsAccentName, Loc.GetString("admin-smite-speak-backwards-description"))
|
||||
};
|
||||
args.Verbs.Add(backwardsAccent);
|
||||
|
||||
var disarmProneName = Loc.GetString("admin-smite-disarm-prone-name").ToLowerInvariant();
|
||||
Verb disarmProne = new()
|
||||
{
|
||||
Text = "admin-smite-disarm-prone-name",
|
||||
Text = disarmProneName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Actions/disarm.png")),
|
||||
Act = () =>
|
||||
@@ -774,10 +811,11 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<DisarmProneComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-disarm-prone-description"),
|
||||
Message = string.Join(": ", disarmProneName, Loc.GetString("admin-smite-disarm-prone-description"))
|
||||
};
|
||||
args.Verbs.Add(disarmProne);
|
||||
|
||||
var superSpeedName = Loc.GetString("admin-smite-super-speed-name").ToLowerInvariant();
|
||||
Verb superSpeed = new()
|
||||
{
|
||||
Text = "admin-smite-garbage-can-name",
|
||||
@@ -792,41 +830,45 @@ public sealed partial class AdminVerbSystem
|
||||
args.Target, PopupType.LargeCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-super-speed-description"),
|
||||
Message = string.Join(": ", superSpeedName, Loc.GetString("admin-smite-super-speed-description"))
|
||||
};
|
||||
args.Verbs.Add(superSpeed);
|
||||
|
||||
//Bonk
|
||||
var superBonkLiteName = Loc.GetString("admin-smite-super-bonk-lite-name").ToLowerInvariant();
|
||||
Verb superBonkLite = new()
|
||||
{
|
||||
Text = "admin-smite-super-bonk-name",
|
||||
Text = superBonkLiteName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("Structures/Furniture/Tables/glass.rsi"), "full"),
|
||||
Act = () =>
|
||||
{
|
||||
_superBonkSystem.StartSuperBonk(args.Target, stopWhenDead: true);
|
||||
},
|
||||
Message = Loc.GetString("admin-smite-super-bonk-lite-description"),
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = string.Join(": ", superBonkLiteName, Loc.GetString("admin-smite-super-bonk-lite-description"))
|
||||
};
|
||||
args.Verbs.Add(superBonkLite);
|
||||
|
||||
var superBonkName = Loc.GetString("admin-smite-super-bonk-name").ToLowerInvariant();
|
||||
Verb superBonk= new()
|
||||
{
|
||||
Text = "admin-smite-super-bonk-lite-name",
|
||||
Text = superBonkName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("Structures/Furniture/Tables/generic.rsi"), "full"),
|
||||
Act = () =>
|
||||
{
|
||||
_superBonkSystem.StartSuperBonk(args.Target);
|
||||
},
|
||||
Message = Loc.GetString("admin-smite-super-bonk-description"),
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = string.Join(": ", superBonkName, Loc.GetString("admin-smite-super-bonk-description"))
|
||||
};
|
||||
args.Verbs.Add(superBonk);
|
||||
|
||||
var superslipName = Loc.GetString("admin-smite-super-slip-name").ToLowerInvariant();
|
||||
Verb superslip = new()
|
||||
{
|
||||
Text = "admin-smite-super-slip-name",
|
||||
Text = superslipName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("Objects/Specific/Janitorial/soap.rsi"), "omega-4"),
|
||||
Act = () =>
|
||||
@@ -846,7 +888,7 @@ public sealed partial class AdminVerbSystem
|
||||
}
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-super-slip-description")
|
||||
Message = string.Join(": ", superslipName, Loc.GetString("admin-smite-super-slip-description"))
|
||||
};
|
||||
args.Verbs.Add(superslip);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Atmos.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Used by FixGridAtmos. Entities with this may get magically auto-deleted on map initialization in future.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[RegisterComponent, EntityCategory("Mapping")]
|
||||
public sealed partial class AtmosFixMarkerComponent : Component
|
||||
{
|
||||
// See FixGridAtmos for more details
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
using Content.Server.Atmos.Monitor.Components;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Pinpointer;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.Consoles;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Atmos.Monitor.Components;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.Pinpointer;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
@@ -21,6 +25,12 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
[Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!;
|
||||
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Dependency] private readonly MapSystem _mapSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly NavMapSystem _navMapSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly DeviceListSystem _deviceListSystem = default!;
|
||||
|
||||
private const float UpdateTime = 1.0f;
|
||||
|
||||
@@ -38,6 +48,9 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
|
||||
// Grid events
|
||||
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
|
||||
|
||||
// Alarm events
|
||||
SubscribeLocalEvent<AtmosAlertsDeviceComponent, EntityTerminatingEvent>(OnDeviceTerminatingEvent);
|
||||
SubscribeLocalEvent<AtmosAlertsDeviceComponent, AnchorStateChangedEvent>(OnDeviceAnchorChanged);
|
||||
}
|
||||
|
||||
@@ -81,6 +94,16 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
}
|
||||
|
||||
private void OnDeviceAnchorChanged(EntityUid uid, AtmosAlertsDeviceComponent component, AnchorStateChangedEvent args)
|
||||
{
|
||||
OnDeviceAdditionOrRemoval(uid, component, args.Anchored);
|
||||
}
|
||||
|
||||
private void OnDeviceTerminatingEvent(EntityUid uid, AtmosAlertsDeviceComponent component, ref EntityTerminatingEvent args)
|
||||
{
|
||||
OnDeviceAdditionOrRemoval(uid, component, false);
|
||||
}
|
||||
|
||||
private void OnDeviceAdditionOrRemoval(EntityUid uid, AtmosAlertsDeviceComponent component, bool isAdding)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
var gridUid = xform.GridUid;
|
||||
@@ -88,10 +111,13 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
if (gridUid == null)
|
||||
return;
|
||||
|
||||
if (!TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data))
|
||||
if (!TryComp<NavMapComponent>(xform.GridUid, out var navMap))
|
||||
return;
|
||||
|
||||
var netEntity = EntityManager.GetNetEntity(uid);
|
||||
if (!TryGetAtmosDeviceNavMapData(uid, component, xform, out var data))
|
||||
return;
|
||||
|
||||
var netEntity = GetNetEntity(uid);
|
||||
|
||||
var query = AllEntityQuery<AtmosAlertsComputerComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
|
||||
@@ -99,11 +125,18 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
if (gridUid != entXform.GridUid)
|
||||
continue;
|
||||
|
||||
if (args.Anchored)
|
||||
if (isAdding)
|
||||
{
|
||||
entConsole.AtmosDevices.Add(data.Value);
|
||||
}
|
||||
|
||||
else if (!args.Anchored)
|
||||
else
|
||||
{
|
||||
entConsole.AtmosDevices.RemoveWhere(x => x.NetEntity == netEntity);
|
||||
_navMapSystem.RemoveNavMapRegion(gridUid.Value, navMap, netEntity);
|
||||
}
|
||||
|
||||
Dirty(ent, entConsole);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +242,12 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
if (entDevice.Group != group)
|
||||
continue;
|
||||
|
||||
if (!TryComp<MapGridComponent>(entXform.GridUid, out var mapGrid))
|
||||
continue;
|
||||
|
||||
if (!TryComp<NavMapComponent>(entXform.GridUid, out var navMap))
|
||||
continue;
|
||||
|
||||
// If emagged, change the alarm type to normal
|
||||
var alarmState = (entAtmosAlarmable.LastAlarmState == AtmosAlarmType.Emagged) ? AtmosAlarmType.Normal : entAtmosAlarmable.LastAlarmState;
|
||||
|
||||
@@ -216,14 +255,45 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
if (TryComp<ApcPowerReceiverComponent>(ent, out var entAPCPower) && !entAPCPower.Powered)
|
||||
alarmState = AtmosAlarmType.Invalid;
|
||||
|
||||
// Create entry
|
||||
var netEnt = GetNetEntity(ent);
|
||||
|
||||
var entry = new AtmosAlertsComputerEntry
|
||||
(GetNetEntity(ent),
|
||||
(netEnt,
|
||||
GetNetCoordinates(entXform.Coordinates),
|
||||
entDevice.Group,
|
||||
alarmState,
|
||||
MetaData(ent).EntityName,
|
||||
entDeviceNetwork.Address);
|
||||
|
||||
// Get the list of sensors attached to the alarm
|
||||
var sensorList = TryComp<DeviceListComponent>(ent, out var entDeviceList) ? _deviceListSystem.GetDeviceList(ent, entDeviceList) : null;
|
||||
|
||||
if (sensorList?.Any() == true)
|
||||
{
|
||||
var alarmRegionSeeds = new HashSet<Vector2i>();
|
||||
|
||||
// If valid and anchored, use the position of sensors as seeds for the region
|
||||
foreach (var (address, sensorEnt) in sensorList)
|
||||
{
|
||||
if (!sensorEnt.IsValid() || !HasComp<AtmosMonitorComponent>(sensorEnt))
|
||||
continue;
|
||||
|
||||
var sensorXform = Transform(sensorEnt);
|
||||
|
||||
if (sensorXform.Anchored && sensorXform.GridUid == entXform.GridUid)
|
||||
alarmRegionSeeds.Add(_mapSystem.CoordinatesToTile(entXform.GridUid.Value, mapGrid, _transformSystem.GetMapCoordinates(sensorEnt, sensorXform)));
|
||||
}
|
||||
|
||||
var regionProperties = new SharedNavMapSystem.NavMapRegionProperties(netEnt, AtmosAlertsComputerUiKey.Key, alarmRegionSeeds);
|
||||
_navMapSystem.AddOrUpdateNavMapRegion(gridUid, navMap, netEnt, regionProperties);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
_navMapSystem.RemoveNavMapRegion(entXform.GridUid.Value, navMap, netEnt);
|
||||
}
|
||||
|
||||
alarmStateData.Add(entry);
|
||||
}
|
||||
|
||||
@@ -306,7 +376,10 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
var query = AllEntityQuery<AtmosAlertsDeviceComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var ent, out var entComponent, out var entXform))
|
||||
{
|
||||
if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, gridUid, out var data))
|
||||
if (entXform.GridUid != gridUid)
|
||||
continue;
|
||||
|
||||
if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, out var data))
|
||||
atmosDeviceNavMapData.Add(data.Value);
|
||||
}
|
||||
|
||||
@@ -317,14 +390,10 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
(EntityUid uid,
|
||||
AtmosAlertsDeviceComponent component,
|
||||
TransformComponent xform,
|
||||
EntityUid gridUid,
|
||||
[NotNullWhen(true)] out AtmosAlertsDeviceNavMapData? output)
|
||||
{
|
||||
output = null;
|
||||
|
||||
if (xform.GridUid != gridUid)
|
||||
return false;
|
||||
|
||||
if (!xform.Anchored)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -472,7 +472,7 @@ public sealed class BloodstreamSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
var currentVolume = bloodSolution.RemoveReagent(component.BloodReagent, bloodSolution.Volume);
|
||||
var currentVolume = bloodSolution.RemoveReagent(component.BloodReagent, bloodSolution.Volume, ignoreReagentData: true);
|
||||
|
||||
component.BloodReagent = reagent;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.Botany.Components;
|
||||
|
||||
@@ -23,6 +24,9 @@ public sealed partial class PlantHolderComponent : Component
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
public TimeSpan LastCycle = TimeSpan.Zero;
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier? WateringSound;
|
||||
|
||||
[DataField]
|
||||
public bool UpdateSpriteAfterUpdate;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Botany.Components;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Server.Kitchen.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
@@ -18,7 +17,6 @@ using Content.Shared.Popups;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -37,7 +35,6 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
|
||||
@@ -53,6 +50,7 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
SubscribeLocalEvent<PlantHolderComponent, ExaminedEvent>(OnExamine);
|
||||
SubscribeLocalEvent<PlantHolderComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
SubscribeLocalEvent<PlantHolderComponent, InteractHandEvent>(OnInteractHand);
|
||||
SubscribeLocalEvent<PlantHolderComponent, SolutionTransferredEvent>(OnSolutionTransferred);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
@@ -158,6 +156,7 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
if (!_botany.TryGetSeed(seeds, out var seed))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
var name = Loc.GetString(seed.Name);
|
||||
var noun = Loc.GetString(seed.Noun);
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-plant-success-message",
|
||||
@@ -185,6 +184,7 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-already-seeded-message",
|
||||
("name", Comp<MetaDataComponent>(uid).EntityName)), args.User, PopupType.Medium);
|
||||
return;
|
||||
@@ -192,6 +192,7 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
|
||||
if (_tagSystem.HasTag(args.Used, "Hoe"))
|
||||
{
|
||||
args.Handled = true;
|
||||
if (component.WeedLevel > 0)
|
||||
{
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-remove-weeds-message",
|
||||
@@ -211,6 +212,7 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
|
||||
if (HasComp<ShovelComponent>(args.Used))
|
||||
{
|
||||
args.Handled = true;
|
||||
if (component.Seed != null)
|
||||
{
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-remove-plant-message",
|
||||
@@ -228,39 +230,9 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (_solutionContainerSystem.TryGetDrainableSolution(args.Used, out var solution, out _)
|
||||
&& _solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution)
|
||||
&& TryComp(args.Used, out SprayComponent? spray))
|
||||
{
|
||||
var amount = FixedPoint2.New(1);
|
||||
|
||||
var targetEntity = uid;
|
||||
var solutionEntity = args.Used;
|
||||
|
||||
_audio.PlayPvs(spray.SpraySound, args.Used, AudioParams.Default.WithVariation(0.125f));
|
||||
|
||||
var split = _solutionContainerSystem.Drain(solutionEntity, solution.Value, amount);
|
||||
|
||||
if (split.Volume == 0)
|
||||
{
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-no-plant-message",
|
||||
("owner", args.Used)), args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-spray-message",
|
||||
("owner", uid),
|
||||
("amount", split.Volume)), args.User, PopupType.Medium);
|
||||
|
||||
_solutionContainerSystem.TryAddSolution(component.SoilSolution.Value, split);
|
||||
|
||||
ForceUpdateByExternalCause(uid, component);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_tagSystem.HasTag(args.Used, "PlantSampleTaker"))
|
||||
{
|
||||
args.Handled = true;
|
||||
if (component.Seed == null)
|
||||
{
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-nothing-to-sample-message"), args.User);
|
||||
@@ -316,10 +288,15 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
}
|
||||
|
||||
if (HasComp<SharpComponent>(args.Used))
|
||||
{
|
||||
args.Handled = true;
|
||||
DoHarvest(uid, args.User, component);
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<ProduceComponent>(args.Used, out var produce))
|
||||
{
|
||||
args.Handled = true;
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-compost-message",
|
||||
("owner", uid),
|
||||
("usingItem", args.Used)), args.User, PopupType.Medium);
|
||||
@@ -351,6 +328,10 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSolutionTransferred(Entity<PlantHolderComponent> ent, ref SolutionTransferredEvent args)
|
||||
{
|
||||
_audio.PlayPvs(ent.Comp.WateringSound, ent.Owner);
|
||||
}
|
||||
private void OnInteractHand(Entity<PlantHolderComponent> entity, ref InteractHandEvent args)
|
||||
{
|
||||
DoHarvest(entity, args.User, entity.Comp);
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Server.Chat.Managers;
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes messages!
|
||||
/// It currently ony removes the shorthands for emotes (like "lol" or "^-^") from a chat message and returns the last
|
||||
/// emote in their message
|
||||
/// </summary>
|
||||
public sealed class ChatSanitizationManager : IChatSanitizationManager
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
|
||||
private static readonly Dictionary<string, string> SmileyToEmote = new()
|
||||
private static readonly Dictionary<string, string> ShorthandToEmote = new()
|
||||
{
|
||||
// CP14-RU-Localization-Start
|
||||
{ "лол", "chatsan-laughs" },
|
||||
@@ -60,7 +63,7 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
|
||||
{ ":D", "chatsan-smiles-widely" },
|
||||
{ "D:", "chatsan-frowns-deeply" },
|
||||
{ ":O", "chatsan-surprised" },
|
||||
{ ":3", "chatsan-smiles" }, //nope
|
||||
{ ":3", "chatsan-smiles" },
|
||||
{ ":S", "chatsan-uncertain" },
|
||||
{ ":>", "chatsan-grins" },
|
||||
{ ":<", "chatsan-pouts" },
|
||||
@@ -102,7 +105,7 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
|
||||
{ "kek", "chatsan-laughs" },
|
||||
{ "rofl", "chatsan-laughs" },
|
||||
{ "o7", "chatsan-salutes" },
|
||||
{ ";_;7", "chatsan-tearfully-salutes"},
|
||||
{ ";_;7", "chatsan-tearfully-salutes" },
|
||||
{ "idk", "chatsan-shrugs" },
|
||||
{ ";)", "chatsan-winks" },
|
||||
{ ";]", "chatsan-winks" },
|
||||
@@ -115,9 +118,12 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
|
||||
{ "(':", "chatsan-tearfully-smiles" },
|
||||
{ "[':", "chatsan-tearfully-smiles" },
|
||||
{ "('=", "chatsan-tearfully-smiles" },
|
||||
{ "['=", "chatsan-tearfully-smiles" },
|
||||
{ "['=", "chatsan-tearfully-smiles" }
|
||||
};
|
||||
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
private bool _doSanitize;
|
||||
|
||||
public void Initialize()
|
||||
@@ -125,29 +131,60 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
|
||||
_configurationManager.OnValueChanged(CCVars.ChatSanitizerEnabled, x => _doSanitize = x, true);
|
||||
}
|
||||
|
||||
public bool TrySanitizeOutSmilies(string input, EntityUid speaker, out string sanitized, [NotNullWhen(true)] out string? emote)
|
||||
/// <summary>
|
||||
/// Remove the shorthands from the message, returning the last one found as the emote
|
||||
/// </summary>
|
||||
/// <param name="message">The pre-sanitized message</param>
|
||||
/// <param name="speaker">The speaker</param>
|
||||
/// <param name="sanitized">The sanitized message with shorthands removed</param>
|
||||
/// <param name="emote">The localized emote</param>
|
||||
/// <returns>True if emote has been sanitized out</returns>
|
||||
public bool TrySanitizeEmoteShorthands(string message,
|
||||
EntityUid speaker,
|
||||
out string sanitized,
|
||||
[NotNullWhen(true)] out string? emote)
|
||||
{
|
||||
if (!_doSanitize)
|
||||
{
|
||||
sanitized = input;
|
||||
emote = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
input = input.TrimEnd();
|
||||
|
||||
foreach (var (smiley, replacement) in SmileyToEmote)
|
||||
{
|
||||
if (input.EndsWith(smiley, true, CultureInfo.InvariantCulture))
|
||||
{
|
||||
sanitized = input.Remove(input.Length - smiley.Length).TrimEnd();
|
||||
emote = Loc.GetString(replacement, ("ent", speaker));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
sanitized = input;
|
||||
emote = null;
|
||||
return false;
|
||||
sanitized = message;
|
||||
|
||||
if (!_doSanitize)
|
||||
return false;
|
||||
|
||||
// -1 is just a canary for nothing found yet
|
||||
var lastEmoteIndex = -1;
|
||||
|
||||
foreach (var (shorthand, emoteKey) in ShorthandToEmote)
|
||||
{
|
||||
// We have to escape it because shorthands like ":)" or "-_-" would break the regex otherwise.
|
||||
var escaped = Regex.Escape(shorthand);
|
||||
|
||||
// So there are 2 cases:
|
||||
// - If there is whitespace before it and after it is either punctuation, whitespace, or the end of the line
|
||||
// Delete the word and the whitespace before
|
||||
// - If it is at the start of the string and is followed by punctuation, whitespace, or the end of the line
|
||||
// Delete the word and the punctuation if it exists.
|
||||
var pattern =
|
||||
$@"\s{escaped}(?=\p{{P}}|\s|$)|^{escaped}(?:\p{{P}}|(?=\s|$))";
|
||||
|
||||
var r = new Regex(pattern, RegexOptions.RightToLeft | RegexOptions.IgnoreCase);
|
||||
|
||||
// We're using sanitized as the original message until the end so that we can make sure the indices of
|
||||
// the emotes are accurate.
|
||||
var lastMatch = r.Match(sanitized);
|
||||
|
||||
if (!lastMatch.Success)
|
||||
continue;
|
||||
|
||||
if (lastMatch.Index > lastEmoteIndex)
|
||||
{
|
||||
lastEmoteIndex = lastMatch.Index;
|
||||
emote = _loc.GetString(emoteKey, ("ent", speaker));
|
||||
}
|
||||
|
||||
message = r.Replace(message, string.Empty);
|
||||
}
|
||||
|
||||
sanitized = message.Trim();
|
||||
return emote is not null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@ public interface IChatSanitizationManager
|
||||
{
|
||||
public void Initialize();
|
||||
|
||||
public bool TrySanitizeOutSmilies(string input, EntityUid speaker, out string sanitized, [NotNullWhen(true)] out string? emote);
|
||||
public bool TrySanitizeEmoteShorthands(string input,
|
||||
EntityUid speaker,
|
||||
out string sanitized,
|
||||
[NotNullWhen(true)] out string? emote);
|
||||
}
|
||||
|
||||
@@ -746,8 +746,12 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true, bool punctuate = false, bool capitalizeTheWordI = true)
|
||||
{
|
||||
var newMessage = message.Trim();
|
||||
newMessage = SanitizeMessageReplaceWords(newMessage);
|
||||
var newMessage = SanitizeMessageReplaceWords(message.Trim());
|
||||
|
||||
GetRadioKeycodePrefix(source, newMessage, out newMessage, out var prefix);
|
||||
|
||||
// Sanitize it first as it might change the word order
|
||||
_sanitizer.TrySanitizeEmoteShorthands(newMessage, source, out newMessage, out emoteStr);
|
||||
|
||||
if (capitalize)
|
||||
newMessage = SanitizeMessageCapital(newMessage);
|
||||
@@ -756,9 +760,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
if (punctuate)
|
||||
newMessage = SanitizeMessagePeriod(newMessage);
|
||||
|
||||
_sanitizer.TrySanitizeOutSmilies(newMessage, source, out newMessage, out emoteStr);
|
||||
|
||||
return newMessage;
|
||||
return prefix + newMessage;
|
||||
}
|
||||
|
||||
private string SanitizeInGameOOCMessage(string message)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
|
||||
namespace Content.Server.Chemistry.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Used for embeddable entities that should try to inject a
|
||||
/// contained solution into a target over time while they are embbeded into.
|
||||
/// </summary>
|
||||
[RegisterComponent, AutoGenerateComponentPause]
|
||||
public sealed partial class SolutionInjectWhileEmbeddedComponent : BaseSolutionInjectOnEventComponent {
|
||||
///<summary>
|
||||
///The time at which the injection will happen.
|
||||
///</summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
|
||||
public TimeSpan NextUpdate;
|
||||
|
||||
///<summary>
|
||||
///The delay between each injection in seconds.
|
||||
///</summary>
|
||||
[DataField]
|
||||
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(3);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Projectiles;
|
||||
@@ -29,6 +30,7 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
|
||||
SubscribeLocalEvent<SolutionInjectOnProjectileHitComponent, ProjectileHitEvent>(HandleProjectileHit);
|
||||
SubscribeLocalEvent<SolutionInjectOnEmbedComponent, EmbedEvent>(HandleEmbed);
|
||||
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(HandleMeleeHit);
|
||||
SubscribeLocalEvent<SolutionInjectWhileEmbeddedComponent, InjectOverTimeEvent>(OnInjectOverTime);
|
||||
}
|
||||
|
||||
private void HandleProjectileHit(Entity<SolutionInjectOnProjectileHitComponent> entity, ref ProjectileHitEvent args)
|
||||
@@ -49,6 +51,11 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
|
||||
TryInjectTargets((entity.Owner, entity.Comp), args.HitEntities, args.User);
|
||||
}
|
||||
|
||||
private void OnInjectOverTime(Entity<SolutionInjectWhileEmbeddedComponent> entity, ref InjectOverTimeEvent args)
|
||||
{
|
||||
DoInjection((entity.Owner, entity.Comp), args.EmbeddedIntoUid);
|
||||
}
|
||||
|
||||
private void DoInjection(Entity<BaseSolutionInjectOnEventComponent> injectorEntity, EntityUid target, EntityUid? source = null)
|
||||
{
|
||||
TryInjectTargets(injectorEntity, [target], source);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Chemistry.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// System for handling injecting into an entity while a projectile is embedded.
|
||||
/// </summary>
|
||||
public sealed class SolutionInjectWhileEmbeddedSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SolutionInjectWhileEmbeddedComponent, MapInitEvent>(OnMapInit);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<SolutionInjectWhileEmbeddedComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<SolutionInjectWhileEmbeddedComponent, EmbeddableProjectileComponent>();
|
||||
while (query.MoveNext(out var uid, out var injectComponent, out var projectileComponent))
|
||||
{
|
||||
if (_gameTiming.CurTime < injectComponent.NextUpdate)
|
||||
continue;
|
||||
|
||||
injectComponent.NextUpdate += injectComponent.UpdateInterval;
|
||||
|
||||
if(projectileComponent.EmbeddedIntoUid == null)
|
||||
continue;
|
||||
|
||||
var ev = new InjectOverTimeEvent(projectileComponent.EmbeddedIntoUid.Value);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -875,10 +875,41 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
|
||||
public async Task AddAdminLogs(List<AdminLog> logs)
|
||||
{
|
||||
const int maxRetryAttempts = 5;
|
||||
var initialRetryDelay = TimeSpan.FromSeconds(5);
|
||||
|
||||
DebugTools.Assert(logs.All(x => x.RoundId > 0), "Adding logs with invalid round ids.");
|
||||
await using var db = await GetDb();
|
||||
db.DbContext.AdminLog.AddRange(logs);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
|
||||
var attempt = 0;
|
||||
var retryDelay = initialRetryDelay;
|
||||
|
||||
while (attempt < maxRetryAttempts)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
db.DbContext.AdminLog.AddRange(logs);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
_opsLog.Debug($"Successfully saved {logs.Count} admin logs.");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
attempt += 1;
|
||||
_opsLog.Error($"Attempt {attempt} failed to save logs: {ex}");
|
||||
|
||||
if (attempt >= maxRetryAttempts)
|
||||
{
|
||||
_opsLog.Error($"Max retry attempts reached. Failed to save {logs.Count} admin logs.");
|
||||
return;
|
||||
}
|
||||
|
||||
_opsLog.Warning($"Retrying in {retryDelay.TotalSeconds} seconds...");
|
||||
await Task.Delay(retryDelay);
|
||||
|
||||
retryDelay *= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract IQueryable<AdminLog> StartAdminLogsQuery(ServerDbContext db, LogFilter? filter = null);
|
||||
|
||||
@@ -26,9 +26,17 @@ public sealed partial class JobCondition : EntityEffectCondition
|
||||
if(!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
|
||||
continue;
|
||||
|
||||
if(!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole)
|
||||
|| mindRole.JobPrototype is null)
|
||||
if (!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole))
|
||||
{
|
||||
Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(MindRoleComponent)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mindRole.JobPrototype == null)
|
||||
{
|
||||
Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(JobPrototype)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Job.Contains(mindRole.JobPrototype.Value))
|
||||
return true;
|
||||
|
||||
@@ -192,9 +192,6 @@ namespace Content.Server.GameTicking
|
||||
if (!_playerManager.TryGetSessionById(userId, out _))
|
||||
continue;
|
||||
|
||||
if (_banManager.GetRoleBans(userId) == null)
|
||||
continue;
|
||||
|
||||
total++;
|
||||
}
|
||||
|
||||
@@ -238,11 +235,7 @@ namespace Content.Server.GameTicking
|
||||
#if DEBUG
|
||||
DebugTools.Assert(_userDb.IsLoadComplete(session), $"Player was readied up but didn't have user DB data loaded yet??");
|
||||
#endif
|
||||
if (_banManager.GetRoleBans(userId) == null)
|
||||
{
|
||||
Logger.ErrorS("RoleBans", $"Role bans for player {session} {userId} have not been loaded yet.");
|
||||
continue;
|
||||
}
|
||||
|
||||
readyPlayers.Add(session);
|
||||
HumanoidCharacterProfile profile;
|
||||
if (_prefsManager.TryGetCachedPreferences(userId, out var preferences))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.NPC.Prototypes;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Roles;
|
||||
@@ -31,6 +32,24 @@ public sealed partial class TraitorRuleComponent : Component
|
||||
[DataField]
|
||||
public ProtoId<DatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
|
||||
|
||||
/// <summary>
|
||||
/// Give this traitor an Uplink on spawn.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool GiveUplink = true;
|
||||
|
||||
/// <summary>
|
||||
/// Give this traitor the codewords.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool GiveCodewords = true;
|
||||
|
||||
/// <summary>
|
||||
/// Give this traitor a briefing in chat.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool GiveBriefing = true;
|
||||
|
||||
public int TotalTraitors => TraitorMinds.Count;
|
||||
public string[] Codewords = new string[3];
|
||||
|
||||
@@ -68,5 +87,5 @@ public sealed partial class TraitorRuleComponent : Component
|
||||
/// The amount of TC traitors start with.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int StartingBalance = 20;
|
||||
public FixedPoint2 StartingBalance = 20;
|
||||
}
|
||||
|
||||
@@ -155,8 +155,8 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
|
||||
if (_mind.TryGetMind(ev.User.Value, out var revMindId, out _))
|
||||
{
|
||||
if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out _, out var role))
|
||||
role.Value.Comp.ConvertedCount++;
|
||||
if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out var role))
|
||||
role.Value.Comp2.ConvertedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Server.PDA.Ringer;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.NPC.Systems;
|
||||
@@ -75,38 +76,46 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
return codewords;
|
||||
}
|
||||
|
||||
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true)
|
||||
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component)
|
||||
{
|
||||
//Grab the mind if it wasn't provided
|
||||
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
|
||||
return false;
|
||||
|
||||
var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
|
||||
var briefing = "";
|
||||
|
||||
if (component.GiveCodewords)
|
||||
briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
|
||||
|
||||
var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers).Values);
|
||||
|
||||
// Uplink code will go here if applicable, but we still need the variable if there aren't any
|
||||
Note[]? code = null;
|
||||
if (giveUplink)
|
||||
|
||||
if (component.GiveUplink)
|
||||
{
|
||||
// Calculate the amount of currency on the uplink.
|
||||
var startingBalance = component.StartingBalance;
|
||||
if (_jobs.MindTryGetJob(mindId, out var prototype))
|
||||
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
|
||||
{
|
||||
if (startingBalance < prototype.AntagAdvantage) // Can't use Math functions on FixedPoint2
|
||||
startingBalance = 0;
|
||||
else
|
||||
startingBalance = startingBalance - prototype.AntagAdvantage;
|
||||
}
|
||||
|
||||
// creadth: we need to create uplink for the antag.
|
||||
// PDA should be in place already
|
||||
var pda = _uplink.FindUplinkTarget(traitor);
|
||||
if (pda == null || !_uplink.AddUplink(traitor, startingBalance, giveDiscounts: true))
|
||||
return false;
|
||||
|
||||
// Give traitors their codewords and uplink code to keep in their character info menu
|
||||
code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
|
||||
|
||||
// If giveUplink is false the uplink code part is omitted
|
||||
briefing = string.Format("{0}\n{1}", briefing,
|
||||
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
|
||||
// Choose and generate an Uplink, and return the uplink code if applicable
|
||||
var uplinkParams = RequestUplink(traitor, startingBalance, briefing);
|
||||
code = uplinkParams.Item1;
|
||||
briefing = uplinkParams.Item2;
|
||||
}
|
||||
|
||||
_antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code, issuer), null, component.GreetSoundNotification);
|
||||
string[]? codewords = null;
|
||||
if (component.GiveCodewords)
|
||||
codewords = component.Codewords;
|
||||
|
||||
if (component.GiveBriefing)
|
||||
_antag.SendBriefing(traitor, GenerateBriefing(codewords, code, issuer), null, component.GreetSoundNotification);
|
||||
|
||||
component.TraitorMinds.Add(mindId);
|
||||
|
||||
@@ -134,6 +143,32 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
return true;
|
||||
}
|
||||
|
||||
private (Note[]?, string) RequestUplink(EntityUid traitor, FixedPoint2 startingBalance, string briefing)
|
||||
{
|
||||
var pda = _uplink.FindUplinkTarget(traitor);
|
||||
Note[]? code = null;
|
||||
|
||||
var uplinked = _uplink.AddUplink(traitor, startingBalance, pda, true);
|
||||
|
||||
if (pda is not null && uplinked)
|
||||
{
|
||||
// Codes are only generated if the uplink is a PDA
|
||||
code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
|
||||
|
||||
// If giveUplink is false the uplink code part is omitted
|
||||
briefing = string.Format("{0}\n{1}",
|
||||
briefing,
|
||||
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
|
||||
return (code, briefing);
|
||||
}
|
||||
else if (pda is null && uplinked)
|
||||
{
|
||||
briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short");
|
||||
}
|
||||
|
||||
return (null, briefing);
|
||||
}
|
||||
|
||||
// TODO: AntagCodewordsComponent
|
||||
private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args)
|
||||
{
|
||||
@@ -141,13 +176,17 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
}
|
||||
|
||||
// TODO: figure out how to handle this? add priority to briefing event?
|
||||
private string GenerateBriefing(string[] codewords, Note[]? uplinkCode, string? objectiveIssuer = null)
|
||||
private string GenerateBriefing(string[]? codewords, Note[]? uplinkCode, string? objectiveIssuer = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(Loc.GetString("traitor-role-greeting", ("corporation", objectiveIssuer ?? Loc.GetString("objective-issuer-unknown"))));
|
||||
sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords))));
|
||||
if (codewords != null)
|
||||
sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords))));
|
||||
if (uplinkCode != null)
|
||||
sb.AppendLine(Loc.GetString("traitor-role-uplink-code", ("code", string.Join("-", uplinkCode).Replace("sharp", "#"))));
|
||||
else
|
||||
sb.AppendLine(Loc.GetString("traitor-role-uplink-implant"));
|
||||
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -516,8 +516,8 @@ public sealed class GhostRoleSystem : EntitySystem
|
||||
|
||||
_roleSystem.MindAddRole(newMind, "MindRoleGhostMarker");
|
||||
|
||||
if(_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind, out _, out var markerRole))
|
||||
markerRole.Value.Comp.Name = role.RoleName;
|
||||
if(_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind!, out var markerRole))
|
||||
markerRole.Value.Comp2.Name = role.RoleName;
|
||||
|
||||
_mindSystem.SetUserId(newMind, player.UserId);
|
||||
_mindSystem.TransferTo(newMind, mob);
|
||||
|
||||
@@ -45,7 +45,7 @@ public sealed class HolosignSystem : EntitySystem
|
||||
if (args.Handled
|
||||
|| !args.CanReach // prevent placing out of range
|
||||
|| HasComp<StorageComponent>(args.Target) // if it's a storage component like a bag, we ignore usage so it can be stored
|
||||
|| !_powerCell.TryUseCharge(uid, component.ChargeUse) // if no battery or no charge, doesn't work
|
||||
|| !_powerCell.TryUseCharge(uid, component.ChargeUse, user: args.User) // if no battery or no charge, doesn't work
|
||||
)
|
||||
return;
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
|
||||
[Dependency] private readonly PowerCellSystem _powerCell = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
// How much the cell score should be increased per 1 AutoRechargeRate.
|
||||
private const int AutoRechargeValue = 100;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -59,15 +62,26 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
|
||||
return;
|
||||
|
||||
// no power cell for some reason??? allow it
|
||||
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery))
|
||||
if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery))
|
||||
return;
|
||||
|
||||
// can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power
|
||||
if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting) || inserting.MaxCharge <= battery.MaxCharge)
|
||||
if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting))
|
||||
{
|
||||
args.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
var user = Transform(uid).ParentUid;
|
||||
|
||||
// can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power
|
||||
if (GetCellScore(inserting.Owner, inserting) <= GetCellScore(battery.Owner, battery))
|
||||
{
|
||||
args.Cancel();
|
||||
Popup.PopupEntity(Loc.GetString("ninja-cell-downgrade"), user, user);
|
||||
return;
|
||||
}
|
||||
|
||||
// tell ninja abilities that use battery to update it so they don't use charge from the old one
|
||||
var user = Transform(uid).ParentUid;
|
||||
if (!_ninja.IsNinja(user))
|
||||
return;
|
||||
|
||||
@@ -76,6 +90,16 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
|
||||
RaiseLocalEvent(user, ref ev);
|
||||
}
|
||||
|
||||
// this function assigns a score to a power cell depending on the capacity, to be used when comparing which cell is better.
|
||||
private float GetCellScore(EntityUid uid, BatteryComponent battcomp)
|
||||
{
|
||||
// if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate,
|
||||
// this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high.
|
||||
if (TryComp<BatterySelfRechargerComponent>(uid, out var selfcomp) && selfcomp.AutoRecharge)
|
||||
return battcomp.MaxCharge + (selfcomp.AutoRechargeRate*AutoRechargeValue);
|
||||
return battcomp.MaxCharge;
|
||||
}
|
||||
|
||||
private void OnEmpAttempt(EntityUid uid, NinjaSuitComponent comp, EmpAttemptEvent args)
|
||||
{
|
||||
// ninja suit (battery) is immune to emp
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Ninja.Components;
|
||||
using Content.Shared.Ninja.Systems;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Sticky;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.Ninja.Systems;
|
||||
|
||||
@@ -19,6 +17,7 @@ public sealed class SpiderChargeSystem : SharedSpiderChargeSystem
|
||||
{
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _role = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SpaceNinjaSystem _ninja = default!;
|
||||
|
||||
@@ -41,7 +40,10 @@ public sealed class SpiderChargeSystem : SharedSpiderChargeSystem
|
||||
|
||||
var user = args.User;
|
||||
|
||||
if (!_mind.TryGetRole<NinjaRoleComponent>(user, out var _))
|
||||
if (!_mind.TryGetMind(args.User, out var mind, out _))
|
||||
return;
|
||||
|
||||
if (!_role.MindHasRole<NinjaRoleComponent>(mind))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("spider-charge-not-ninja"), user, user);
|
||||
args.Cancelled = true;
|
||||
|
||||
@@ -317,7 +317,7 @@ public sealed class DrinkSystem : SharedDrinkSystem
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} drank {ToPrettyString(entity.Owner):drink}");
|
||||
}
|
||||
|
||||
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-2f));
|
||||
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-2f).WithVariation(0.25f));
|
||||
|
||||
_reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion);
|
||||
_stomach.TryTransferSolution(firstStomach.Value.Owner, drained, firstStomach.Value.Comp1);
|
||||
|
||||
@@ -296,7 +296,7 @@ public sealed class FoodSystem : EntitySystem
|
||||
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity.Owner):food}");
|
||||
}
|
||||
|
||||
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-1f));
|
||||
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
|
||||
|
||||
// Try to break all used utensils
|
||||
foreach (var utensil in utensils)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Server.Revolutionary.Components;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
@@ -17,7 +17,6 @@ public sealed class KillPersonConditionSystem : EntitySystem
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedJobSystem _job = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly TargetObjectiveSystem _target = default!;
|
||||
|
||||
@@ -86,11 +85,10 @@ public sealed class KillPersonConditionSystem : EntitySystem
|
||||
}
|
||||
|
||||
var allHeads = new List<EntityUid>();
|
||||
foreach (var mind in allHumans)
|
||||
foreach (var person in allHumans)
|
||||
{
|
||||
// RequireAdminNotify used as a cheap way to check for command department
|
||||
if (_job.MindTryGetJob(mind, out var prototype) && prototype.RequireAdminNotify)
|
||||
allHeads.Add(mind);
|
||||
if (TryComp<MindComponent>(person, out var mind) && mind.OwnedEntity is { } ent && HasComp<CommandStaffComponent>(ent))
|
||||
allHeads.Add(person);
|
||||
}
|
||||
|
||||
if (allHeads.Count == 0)
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Server.Revolutionary.Components;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
|
||||
namespace Content.Server.Objectives.Systems;
|
||||
|
||||
public sealed class NotCommandRequirementSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedJobSystem _job = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -20,8 +18,7 @@ public sealed class NotCommandRequirementSystem : EntitySystem
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
// cheap equivalent to checking that job department is command, since all command members require admin notification when leaving
|
||||
if (_job.MindTryGetJob(args.MindId, out var prototype) && prototype.RequireAdminNotify)
|
||||
if (args.Mind.OwnedEntity is { } ent && HasComp<CommandStaffComponent>(ent))
|
||||
args.Cancelled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,6 @@ public sealed partial class ApcComponent : BaseApcNetComponent
|
||||
|
||||
[DataField("enabled")]
|
||||
public bool MainBreakerEnabled = true;
|
||||
// TODO: remove this since it probably breaks when 2 people use it
|
||||
[DataField("hasAccess")]
|
||||
public bool HasAccess = false;
|
||||
|
||||
/// <summary>
|
||||
/// APC state needs to always be updated after first processing tick.
|
||||
|
||||
@@ -2,7 +2,6 @@ using Content.Server.Emp;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.Pow3r;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.APC;
|
||||
using Content.Shared.Emag.Components;
|
||||
@@ -71,11 +70,8 @@ public sealed class ApcSystem : EntitySystem
|
||||
component.NeedStateUpdate = true;
|
||||
}
|
||||
|
||||
//Update the HasAccess var for UI to read
|
||||
private void OnBoundUiOpen(EntityUid uid, ApcComponent component, BoundUIOpenedEvent args)
|
||||
{
|
||||
// TODO: this should be per-player not stored on the apc
|
||||
component.HasAccess = _accessReader.IsAllowed(args.Actor, uid);
|
||||
UpdateApcState(uid, component);
|
||||
}
|
||||
|
||||
@@ -165,7 +161,7 @@ public sealed class ApcSystem : EntitySystem
|
||||
// TODO: Fix ContentHelpers or make a new one coz this is cooked.
|
||||
var charge = ContentHelpers.RoundToNearestLevels(battery.CurrentStorage / battery.Capacity, 1.0, 100 / ChargeAccuracy) / 100f * ChargeAccuracy;
|
||||
|
||||
var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled, apc.HasAccess,
|
||||
var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled,
|
||||
(int) MathF.Ceiling(battery.CurrentSupply), apc.LastExternalState,
|
||||
charge);
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Content.Server.GameTicking.Rules;
|
||||
|
||||
namespace Content.Server.Revolutionary.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Given to heads at round start for Revs. Used for tracking if heads died or not.
|
||||
/// Given to heads at round start. Used for assigning traitors to kill heads and for revs to check if the heads died or not.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(RevolutionaryRuleSystem))]
|
||||
[RegisterComponent]
|
||||
public sealed partial class CommandStaffComponent : Component
|
||||
{
|
||||
|
||||
|
||||
@@ -12,9 +12,13 @@ using Robust.Shared.Timing;
|
||||
namespace Content.Server.ServerUpdates;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for restarting the server for update, when not disruptive.
|
||||
/// Responsible for restarting the server periodically or for update, when not disruptive.
|
||||
/// </summary>
|
||||
public sealed class ServerUpdateManager
|
||||
/// <remarks>
|
||||
/// This was originally only designed for restarting on *update*,
|
||||
/// but now also handles periodic restarting to keep server uptime via <see cref="CCVars.ServerUptimeRestartMinutes"/>.
|
||||
/// </remarks>
|
||||
public sealed class ServerUpdateManager : IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IWatchdogApi _watchdog = default!;
|
||||
@@ -22,23 +26,43 @@ public sealed class ServerUpdateManager
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly IBaseServer _server = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
[ViewVariables]
|
||||
private bool _updateOnRoundEnd;
|
||||
|
||||
private TimeSpan? _restartTime;
|
||||
|
||||
private TimeSpan _uptimeRestart;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_watchdog.UpdateReceived += WatchdogOnUpdateReceived;
|
||||
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
|
||||
|
||||
_cfg.OnValueChanged(
|
||||
CCVars.ServerUptimeRestartMinutes,
|
||||
minutes => _uptimeRestart = TimeSpan.FromMinutes(minutes),
|
||||
true);
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (_restartTime != null && _restartTime < _gameTiming.RealTime)
|
||||
if (_restartTime != null)
|
||||
{
|
||||
DoShutdown();
|
||||
if (_restartTime < _gameTiming.RealTime)
|
||||
{
|
||||
DoShutdown();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ShouldShutdownDueToUptime())
|
||||
{
|
||||
ServerEmptyUpdateRestartCheck("uptime");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +72,7 @@ public sealed class ServerUpdateManager
|
||||
/// <returns>True if the server is going to restart.</returns>
|
||||
public bool RoundEnded()
|
||||
{
|
||||
if (_updateOnRoundEnd)
|
||||
if (_updateOnRoundEnd || ShouldShutdownDueToUptime())
|
||||
{
|
||||
DoShutdown();
|
||||
return true;
|
||||
@@ -61,11 +85,14 @@ public sealed class ServerUpdateManager
|
||||
{
|
||||
switch (e.NewStatus)
|
||||
{
|
||||
case SessionStatus.Connecting:
|
||||
case SessionStatus.Connected:
|
||||
if (_restartTime != null)
|
||||
_sawmill.Debug("Aborting server restart timer due to player connection");
|
||||
|
||||
_restartTime = null;
|
||||
break;
|
||||
case SessionStatus.Disconnected:
|
||||
ServerEmptyUpdateRestartCheck();
|
||||
ServerEmptyUpdateRestartCheck("last player disconnect");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -74,20 +101,20 @@ public sealed class ServerUpdateManager
|
||||
{
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("server-updates-received"));
|
||||
_updateOnRoundEnd = true;
|
||||
ServerEmptyUpdateRestartCheck();
|
||||
ServerEmptyUpdateRestartCheck("update notification");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether there are still players on the server,
|
||||
/// and if not starts a timer to automatically reboot the server if an update is available.
|
||||
/// </summary>
|
||||
private void ServerEmptyUpdateRestartCheck()
|
||||
private void ServerEmptyUpdateRestartCheck(string reason)
|
||||
{
|
||||
// Can't simple check the current connected player count since that doesn't update
|
||||
// before PlayerStatusChanged gets fired.
|
||||
// So in the disconnect handler we'd still see a single player otherwise.
|
||||
var playersOnline = _playerManager.Sessions.Any(p => p.Status != SessionStatus.Disconnected);
|
||||
if (playersOnline || !_updateOnRoundEnd)
|
||||
if (playersOnline || !(_updateOnRoundEnd || ShouldShutdownDueToUptime()))
|
||||
{
|
||||
// Still somebody online.
|
||||
return;
|
||||
@@ -95,16 +122,30 @@ public sealed class ServerUpdateManager
|
||||
|
||||
if (_restartTime != null)
|
||||
{
|
||||
// Do nothing because I guess we already have a timer running..?
|
||||
// Do nothing because we already have a timer running.
|
||||
return;
|
||||
}
|
||||
|
||||
var restartDelay = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.UpdateRestartDelay));
|
||||
_restartTime = restartDelay + _gameTiming.RealTime;
|
||||
|
||||
_sawmill.Debug("Started server-empty restart timer due to {Reason}", reason);
|
||||
}
|
||||
|
||||
private void DoShutdown()
|
||||
{
|
||||
_server.Shutdown(Loc.GetString("server-updates-shutdown"));
|
||||
_sawmill.Debug($"Shutting down via {nameof(ServerUpdateManager)}!");
|
||||
var reason = _updateOnRoundEnd ? "server-updates-shutdown" : "server-updates-shutdown-uptime";
|
||||
_server.Shutdown(Loc.GetString(reason));
|
||||
}
|
||||
|
||||
private bool ShouldShutdownDueToUptime()
|
||||
{
|
||||
return _uptimeRestart != TimeSpan.Zero && _gameTiming.RealTime > _uptimeRestart;
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("restart");
|
||||
}
|
||||
}
|
||||
|
||||
183
Content.Server/Shuttles/Commands/FTLDiskCommand.cs
Normal file
183
Content.Server/Shuttles/Commands/FTLDiskCommand.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Labels;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Shuttles.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Creates FTL disks, to maps, grids, or entities.
|
||||
/// </summary>
|
||||
[AdminCommand(AdminFlags.Fun)]
|
||||
|
||||
public sealed class FTLDiskCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entSystemManager = default!;
|
||||
|
||||
public override string Command => "ftldisk";
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
public const string CoordinatesDisk = "CoordinatesDisk";
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
public const string DiskCase = "DiskCase";
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-need-minimum-one-argument"));
|
||||
return;
|
||||
}
|
||||
|
||||
var player = shell.Player;
|
||||
|
||||
if (player == null)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("shell-only-players-can-run-this-command"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.AttachedEntity == null)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("shell-must-be-attached-to-entity"));
|
||||
return;
|
||||
}
|
||||
|
||||
EntityUid entity = player.AttachedEntity.Value;
|
||||
var coords = _entManager.GetComponent<TransformComponent>(entity).Coordinates;
|
||||
|
||||
var handsSystem = _entSystemManager.GetEntitySystem<SharedHandsSystem>();
|
||||
var labelSystem = _entSystemManager.GetEntitySystem<LabelSystem>();
|
||||
var mapSystem = _entSystemManager.GetEntitySystem<SharedMapSystem>();
|
||||
var storageSystem = _entSystemManager.GetEntitySystem<SharedStorageSystem>();
|
||||
|
||||
foreach (var destinations in args)
|
||||
{
|
||||
DebugTools.AssertNotNull(destinations);
|
||||
|
||||
// make sure destination is an id.
|
||||
EntityUid dest;
|
||||
|
||||
if (_entManager.TryParseNetEntity(destinations, out var nullableDest))
|
||||
{
|
||||
DebugTools.AssertNotNull(nullableDest);
|
||||
|
||||
dest = (EntityUid) nullableDest;
|
||||
|
||||
// we need to go to a map, so check if the EntID is something else then try for its map
|
||||
if (!_entManager.HasComponent<MapComponent>(dest))
|
||||
{
|
||||
if (!_entManager.TryGetComponent<TransformComponent>(dest, out var entTransform))
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-ftldisk-no-transform", ("destination", destinations)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!mapSystem.TryGetMap(entTransform.MapID, out var mapDest))
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-ftldisk-no-map", ("destination", destinations)));
|
||||
continue;
|
||||
}
|
||||
|
||||
DebugTools.AssertNotNull(mapDest);
|
||||
dest = mapDest!.Value; // explicit cast here should be fine since the previous if should catch it.
|
||||
}
|
||||
|
||||
// find and verify the map is not somehow unusable.
|
||||
if (!_entManager.TryGetComponent<MapComponent>(dest, out var mapComp)) // We have to check for a MapComponent here and above since we could have changed our dest entity.
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-ftldisk-no-map-comp", ("destination", destinations), ("map", dest)));
|
||||
continue;
|
||||
}
|
||||
if (mapComp.MapInitialized == false)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-ftldisk-map-not-init", ("destination", destinations), ("map", dest)));
|
||||
continue;
|
||||
}
|
||||
if (mapComp.MapPaused == true)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-ftldisk-map-paused", ("destination", destinations), ("map", dest)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if our destination works already, if not, make it.
|
||||
if (!_entManager.TryGetComponent<FTLDestinationComponent>(dest, out var ftlDestComp))
|
||||
{
|
||||
FTLDestinationComponent ftlDest = _entManager.AddComponent<FTLDestinationComponent>(dest);
|
||||
ftlDest.RequireCoordinateDisk = true;
|
||||
|
||||
if (_entManager.HasComponent<MapGridComponent>(dest))
|
||||
{
|
||||
ftlDest.BeaconsOnly = true;
|
||||
|
||||
shell.WriteLine(Loc.GetString("cmd-ftldisk-planet", ("destination", destinations), ("map", dest)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// we don't do these automatically, since it isn't clear what the correct resolution is. Instead we provide feedback to the user and carry on like they know what theyre doing.
|
||||
if (ftlDestComp.Enabled == false)
|
||||
shell.WriteLine(Loc.GetString("cmd-ftldisk-already-dest-not-enabled", ("destination", destinations), ("map", dest)));
|
||||
|
||||
if (ftlDestComp.BeaconsOnly == true)
|
||||
shell.WriteLine(Loc.GetString("cmd-ftldisk-requires-ftl-point", ("destination", destinations), ("map", dest)));
|
||||
}
|
||||
|
||||
// create the FTL disk
|
||||
EntityUid cdUid = _entManager.SpawnEntity(CoordinatesDisk, coords);
|
||||
var cd = _entManager.EnsureComponent<ShuttleDestinationCoordinatesComponent>(cdUid);
|
||||
cd.Destination = dest;
|
||||
_entManager.Dirty(cdUid, cd);
|
||||
|
||||
// create disk case
|
||||
EntityUid cdCaseUid = _entManager.SpawnEntity(DiskCase, coords);
|
||||
|
||||
// apply labels
|
||||
if (_entManager.TryGetComponent<MetaDataComponent>(dest, out var meta) && meta != null && meta.EntityName != null)
|
||||
{
|
||||
labelSystem.Label(cdUid, meta.EntityName);
|
||||
labelSystem.Label(cdCaseUid, meta.EntityName);
|
||||
}
|
||||
|
||||
// if the case has a storage, try to place the disk in there and then the case inhand
|
||||
|
||||
if (_entManager.TryGetComponent<StorageComponent>(cdCaseUid, out var storage) && storageSystem.Insert(cdCaseUid, cdUid, out _, storageComp: storage, playSound: false))
|
||||
{
|
||||
if (_entManager.TryGetComponent<HandsComponent>(entity, out var handsComponent) && handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
|
||||
{
|
||||
handsSystem.TryPickup(entity, cdCaseUid, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
|
||||
}
|
||||
}
|
||||
else // the case was messed up, put disk inhand
|
||||
{
|
||||
_entManager.DeleteEntity(cdCaseUid); // something went wrong so just yeet the chaf
|
||||
|
||||
if (_entManager.TryGetComponent<HandsComponent>(entity, out var handsComponent) && handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
|
||||
{
|
||||
handsSystem.TryPickup(entity, cdUid, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("shell-invalid-entity-uid", ("uid", destinations)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length >= 1)
|
||||
return CompletionResult.FromHintOptions(CompletionHelper.MapUids(_entManager), Loc.GetString("cmd-ftldisk-hint"));
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,10 @@ public sealed partial class BorgSystem
|
||||
|
||||
if (_actions.AddAction(chassis, ref component.ModuleSwapActionEntity, out var action, component.ModuleSwapActionId, uid))
|
||||
{
|
||||
if(TryComp<BorgModuleIconComponent>(uid, out var moduleIconComp))
|
||||
{
|
||||
action.Icon = moduleIconComp.Icon;
|
||||
};
|
||||
action.EntityIcon = uid;
|
||||
Dirty(component.ModuleSwapActionEntity.Value, action);
|
||||
}
|
||||
|
||||
@@ -293,6 +293,8 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
|
||||
while (query.MoveNext(out var update))
|
||||
{
|
||||
SetLaws(lawset, update);
|
||||
if (provider.LawUploadSound != null && _mind.TryGetMind(update, out var mindId, out _))
|
||||
_roles.MindPlaySound(mindId, provider.LawUploadSound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ public sealed class TemperatureSystem : EntitySystem
|
||||
TemperatureComponent? temperature = null)
|
||||
{
|
||||
//CrystallPunk may try place on heater and entity, and solutions
|
||||
//if (!Resolve(uid, ref temperature))
|
||||
//if (!Resolve(uid, ref temperature, false))
|
||||
// return;
|
||||
if (temperature == null)
|
||||
return;
|
||||
@@ -315,7 +315,7 @@ public sealed class TemperatureSystem : EntitySystem
|
||||
|
||||
private void ChangeTemperatureOnCollide(Entity<ChangeTemperatureOnCollideComponent> ent, ref ProjectileHitEvent args)
|
||||
{
|
||||
_temperature.ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
|
||||
_temperature.ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
|
||||
}
|
||||
|
||||
private void OnParentChange(EntityUid uid, TemperatureComponent component,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Traitor.Systems;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Traitor.Components;
|
||||
|
||||
@@ -9,14 +10,8 @@ namespace Content.Server.Traitor.Components;
|
||||
public sealed partial class AutoTraitorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to give the traitor an uplink or not.
|
||||
/// The traitor profile to use
|
||||
/// </summary>
|
||||
[DataField("giveUplink"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool GiveUplink = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to give the traitor objectives or not.
|
||||
/// </summary>
|
||||
[DataField("giveObjectives"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool GiveObjectives = true;
|
||||
[DataField]
|
||||
public EntProtoId Profile = "Traitor";
|
||||
}
|
||||
|
||||
@@ -12,9 +12,6 @@ public sealed class AutoTraitorSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string DefaultTraitorRule = "Traitor";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -24,6 +21,6 @@ public sealed class AutoTraitorSystem : EntitySystem
|
||||
|
||||
private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args)
|
||||
{
|
||||
_antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, DefaultTraitorRule);
|
||||
_antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, comp.Profile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,136 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Store.Systems;
|
||||
using Content.Server.StoreDiscount.Systems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Implants;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Store;
|
||||
using Content.Shared.Store.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Traitor.Uplink
|
||||
namespace Content.Server.Traitor.Uplink;
|
||||
|
||||
public sealed class UplinkSystem : EntitySystem
|
||||
{
|
||||
public sealed class UplinkSystem : EntitySystem
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly StoreSystem _store = default!;
|
||||
[Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
|
||||
|
||||
[ValidatePrototypeId<CurrencyPrototype>]
|
||||
public const string TelecrystalCurrencyPrototype = "Telecrystal";
|
||||
private const string FallbackUplinkImplant = "UplinkImplant";
|
||||
private const string FallbackUplinkCatalog = "UplinkUplinkImplanter";
|
||||
|
||||
/// <summary>
|
||||
/// Adds an uplink to the target
|
||||
/// </summary>
|
||||
/// <param name="user">The person who is getting the uplink</param>
|
||||
/// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param>
|
||||
/// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param>
|
||||
/// <param name="giveDiscounts">Marker that enables discounts for uplink items.</param>
|
||||
/// <returns>Whether or not the uplink was added successfully</returns>
|
||||
public bool AddUplink(
|
||||
EntityUid user,
|
||||
FixedPoint2 balance,
|
||||
EntityUid? uplinkEntity = null,
|
||||
bool giveDiscounts = false)
|
||||
{
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||
[Dependency] private readonly StoreSystem _store = default!;
|
||||
// Try to find target item if none passed
|
||||
|
||||
[ValidatePrototypeId<CurrencyPrototype>]
|
||||
public const string TelecrystalCurrencyPrototype = "Telecrystal";
|
||||
uplinkEntity ??= FindUplinkTarget(user);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an uplink to the target
|
||||
/// </summary>
|
||||
/// <param name="user">The person who is getting the uplink</param>
|
||||
/// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param>
|
||||
/// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param>
|
||||
/// <param name="giveDiscounts">Marker that enables discounts for uplink items.</param>
|
||||
/// <returns>Whether or not the uplink was added successfully</returns>
|
||||
public bool AddUplink(
|
||||
EntityUid user,
|
||||
FixedPoint2? balance,
|
||||
EntityUid? uplinkEntity = null,
|
||||
bool giveDiscounts = false
|
||||
)
|
||||
if (uplinkEntity == null)
|
||||
return ImplantUplink(user, balance, giveDiscounts);
|
||||
|
||||
EnsureComp<UplinkComponent>(uplinkEntity.Value);
|
||||
|
||||
SetUplink(user, uplinkEntity.Value, balance, giveDiscounts);
|
||||
|
||||
// TODO add BUI. Currently can't be done outside of yaml -_-
|
||||
// ^ What does this even mean?
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure TC for the uplink
|
||||
/// </summary>
|
||||
private void SetUplink(EntityUid user, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts)
|
||||
{
|
||||
var store = EnsureComp<StoreComponent>(uplink);
|
||||
store.AccountOwner = user;
|
||||
|
||||
store.Balance.Clear();
|
||||
_store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance } },
|
||||
uplink,
|
||||
store);
|
||||
|
||||
var uplinkInitializedEvent = new StoreInitializedEvent(
|
||||
TargetUser: user,
|
||||
Store: uplink,
|
||||
UseDiscounts: giveDiscounts,
|
||||
Listings: _store.GetAvailableListings(user, uplink, store)
|
||||
.ToArray());
|
||||
RaiseLocalEvent(ref uplinkInitializedEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implant an uplink as a fallback measure if the traitor had no PDA
|
||||
/// </summary>
|
||||
private bool ImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts)
|
||||
{
|
||||
var implantProto = new string(FallbackUplinkImplant);
|
||||
|
||||
if (!_proto.TryIndex<ListingPrototype>(FallbackUplinkCatalog, out var catalog))
|
||||
return false;
|
||||
|
||||
if (!catalog.Cost.TryGetValue(TelecrystalCurrencyPrototype, out var cost))
|
||||
return false;
|
||||
|
||||
if (balance < cost) // Can't use Math functions on FixedPoint2
|
||||
balance = 0;
|
||||
else
|
||||
balance = balance - cost;
|
||||
|
||||
var implant = _subdermalImplant.AddImplant(user, implantProto);
|
||||
|
||||
if (!HasComp<StoreComponent>(implant))
|
||||
return false;
|
||||
|
||||
SetUplink(user, implant.Value, balance, giveDiscounts);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the entity that can hold an uplink for a user.
|
||||
/// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.)
|
||||
/// </summary>
|
||||
public EntityUid? FindUplinkTarget(EntityUid user)
|
||||
{
|
||||
// Try to find PDA in inventory
|
||||
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
|
||||
{
|
||||
// Try to find target item if none passed
|
||||
uplinkEntity ??= FindUplinkTarget(user);
|
||||
if (uplinkEntity == null)
|
||||
while (containerSlotEnumerator.MoveNext(out var pdaUid))
|
||||
{
|
||||
return false;
|
||||
if (!pdaUid.ContainedEntity.HasValue)
|
||||
continue;
|
||||
|
||||
if (HasComp<PdaComponent>(pdaUid.ContainedEntity.Value) || HasComp<StoreComponent>(pdaUid.ContainedEntity.Value))
|
||||
return pdaUid.ContainedEntity.Value;
|
||||
}
|
||||
|
||||
EnsureComp<UplinkComponent>(uplinkEntity.Value);
|
||||
var store = EnsureComp<StoreComponent>(uplinkEntity.Value);
|
||||
|
||||
store.AccountOwner = user;
|
||||
store.Balance.Clear();
|
||||
if (balance != null)
|
||||
{
|
||||
store.Balance.Clear();
|
||||
_store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance.Value } }, uplinkEntity.Value, store);
|
||||
}
|
||||
|
||||
var uplinkInitializedEvent = new StoreInitializedEvent(
|
||||
TargetUser: user,
|
||||
Store: uplinkEntity.Value,
|
||||
UseDiscounts: giveDiscounts,
|
||||
Listings: _store.GetAvailableListings(user, uplinkEntity.Value, store)
|
||||
.ToArray()
|
||||
);
|
||||
RaiseLocalEvent(ref uplinkInitializedEvent);
|
||||
// TODO add BUI. Currently can't be done outside of yaml -_-
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the entity that can hold an uplink for a user.
|
||||
/// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.)
|
||||
/// </summary>
|
||||
public EntityUid? FindUplinkTarget(EntityUid user)
|
||||
// Also check hands
|
||||
foreach (var item in _handsSystem.EnumerateHeld(user))
|
||||
{
|
||||
// Try to find PDA in inventory
|
||||
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
|
||||
{
|
||||
while (containerSlotEnumerator.MoveNext(out var pdaUid))
|
||||
{
|
||||
if (!pdaUid.ContainedEntity.HasValue)
|
||||
continue;
|
||||
|
||||
if (HasComp<PdaComponent>(pdaUid.ContainedEntity.Value) || HasComp<StoreComponent>(pdaUid.ContainedEntity.Value))
|
||||
return pdaUid.ContainedEntity.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check hands
|
||||
foreach (var item in _handsSystem.EnumerateHeld(user))
|
||||
{
|
||||
if (HasComp<PdaComponent>(item) || HasComp<StoreComponent>(item))
|
||||
return item;
|
||||
}
|
||||
|
||||
return null;
|
||||
if (HasComp<PdaComponent>(item) || HasComp<StoreComponent>(item))
|
||||
return item;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,14 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
|
||||
_damageExamine.AddDamageExamine(args.Message, damageSpec, Loc.GetString("damage-melee"));
|
||||
}
|
||||
|
||||
protected override bool ArcRaySuccessful(EntityUid targetUid, Vector2 position, Angle angle, Angle arcWidth, float range, MapId mapId,
|
||||
EntityUid ignore, ICommonSession? session)
|
||||
protected override bool ArcRaySuccessful(EntityUid targetUid,
|
||||
Vector2 position,
|
||||
Angle angle,
|
||||
Angle arcWidth,
|
||||
float range,
|
||||
MapId mapId,
|
||||
EntityUid ignore,
|
||||
ICommonSession? session)
|
||||
{
|
||||
// Originally the client didn't predict damage effects so you'd intuit some level of how far
|
||||
// in the future you'd need to predict, but then there was a lot of complaining like "why would you add artifical delay" as if ping is a choice.
|
||||
|
||||
@@ -2,6 +2,8 @@ using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
|
||||
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Teleportation.Systems;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
|
||||
@@ -11,6 +13,7 @@ public sealed class PortalArtifactSystem : EntitySystem
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly LinkedEntitySystem _link = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -21,21 +24,28 @@ public sealed class PortalArtifactSystem : EntitySystem
|
||||
private void OnActivate(Entity<PortalArtifactComponent> artifact, ref ArtifactActivatedEvent args)
|
||||
{
|
||||
var map = Transform(artifact).MapID;
|
||||
var validMinds = new ValueList<EntityUid>();
|
||||
var mindQuery = EntityQueryEnumerator<MindContainerComponent, TransformComponent, MetaDataComponent>();
|
||||
while (mindQuery.MoveNext(out var uid, out var mc, out var xform, out var meta))
|
||||
{
|
||||
// check if the MindContainer has a Mind and if the entity is not in a container (this also auto excludes AI) and if they are on the same map
|
||||
if (mc.HasMind && !_container.IsEntityOrParentInContainer(uid, meta: meta, xform: xform) && xform.MapID == map)
|
||||
{
|
||||
validMinds.Add(uid);
|
||||
}
|
||||
}
|
||||
//this would only be 0 if there were a station full of AIs and no one else, in that case just stop this function
|
||||
if (validMinds.Count == 0)
|
||||
return;
|
||||
|
||||
var firstPortal = Spawn(artifact.Comp.PortalProto, _transform.GetMapCoordinates(artifact));
|
||||
|
||||
var minds = new List<EntityUid>();
|
||||
var mindQuery = EntityQueryEnumerator<MindContainerComponent, TransformComponent>();
|
||||
while (mindQuery.MoveNext(out var uid, out var mc, out var xform))
|
||||
{
|
||||
if (mc.HasMind && xform.MapID == map)
|
||||
minds.Add(uid);
|
||||
}
|
||||
var target = _random.Pick(validMinds);
|
||||
|
||||
var target = _random.Pick(minds);
|
||||
var secondPortal = Spawn(artifact.Comp.PortalProto, _transform.GetMapCoordinates(target));
|
||||
|
||||
//Manual position swapping, because the portal that opens doesn't trigger a collision, and doesn't teleport targets the first time.
|
||||
_transform.SwapPositions(target, secondPortal);
|
||||
_transform.SwapPositions(target, artifact.Owner);
|
||||
|
||||
_link.TryLink(firstPortal, secondPortal, true);
|
||||
}
|
||||
|
||||
@@ -178,15 +178,13 @@ namespace Content.Shared.APC
|
||||
public sealed class ApcBoundInterfaceState : BoundUserInterfaceState, IEquatable<ApcBoundInterfaceState>
|
||||
{
|
||||
public readonly bool MainBreaker;
|
||||
public readonly bool HasAccess;
|
||||
public readonly int Power;
|
||||
public readonly ApcExternalPowerState ApcExternalPower;
|
||||
public readonly float Charge;
|
||||
|
||||
public ApcBoundInterfaceState(bool mainBreaker, bool hasAccess, int power, ApcExternalPowerState apcExternalPower, float charge)
|
||||
public ApcBoundInterfaceState(bool mainBreaker, int power, ApcExternalPowerState apcExternalPower, float charge)
|
||||
{
|
||||
MainBreaker = mainBreaker;
|
||||
HasAccess = hasAccess;
|
||||
Power = power;
|
||||
ApcExternalPower = apcExternalPower;
|
||||
Charge = charge;
|
||||
@@ -197,7 +195,6 @@ namespace Content.Shared.APC
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return MainBreaker == other.MainBreaker &&
|
||||
HasAccess == other.HasAccess &&
|
||||
Power == other.Power &&
|
||||
ApcExternalPower == other.ApcExternalPower &&
|
||||
MathHelper.CloseTo(Charge, other.Charge);
|
||||
@@ -210,7 +207,7 @@ namespace Content.Shared.APC
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(MainBreaker, HasAccess, Power, (int) ApcExternalPower, Charge);
|
||||
return HashCode.Combine(MainBreaker, Power, (int) ApcExternalPower, Charge);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using Content.Shared.Actions;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.Emoting;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Eye.Blinding.Systems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
@@ -61,6 +62,7 @@ public sealed partial class SleepingSystem : EntitySystem
|
||||
|
||||
SubscribeLocalEvent<ForcedSleepingComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<SleepingComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
|
||||
SubscribeLocalEvent<SleepingComponent, EmoteAttemptEvent>(OnEmoteAttempt);
|
||||
}
|
||||
|
||||
private void OnUnbuckleAttempt(Entity<SleepingComponent> ent, ref UnbuckleAttemptEvent args)
|
||||
@@ -310,6 +312,14 @@ public sealed partial class SleepingSystem : EntitySystem
|
||||
Wake((ent, ent.Comp));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prevents the use of emote actions while sleeping
|
||||
/// </summary>
|
||||
public void OnEmoteAttempt(Entity<SleepingComponent> ent, ref EmoteAttemptEvent args)
|
||||
{
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -45,6 +45,21 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<string> DefaultGuide =
|
||||
CVarDef.Create("server.default_guide", "NewPlayer", CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// If greater than 0, automatically restart the server after this many minutes of uptime.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is intended to work around various bugs and performance issues caused by long continuous server uptime.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This uses the same non-disruptive logic as update restarts,
|
||||
/// i.e. the game will only restart at round end or when there is nobody connected.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static readonly CVarDef<int> ServerUptimeRestartMinutes =
|
||||
CVarDef.Create("server.uptime_restart_minutes", 0, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* Ambience
|
||||
*/
|
||||
@@ -449,6 +464,12 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<float> GameEntityMenuLookup =
|
||||
CVarDef.Create("game.entity_menu_lookup", 0.25f, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Should the clients window show the server hostname in the title?
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> GameHostnameInTitlebar =
|
||||
CVarDef.Create("game.hostname_in_titlebar", true, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/*
|
||||
* Discord
|
||||
*/
|
||||
|
||||
@@ -84,6 +84,35 @@ public abstract class SharedChatSystem : EntitySystem
|
||||
return current ?? _prototypeManager.Index<SpeechVerbPrototype>(speech.SpeechVerb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits the input message into a radio prefix part and the rest to preserve it during sanitization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is primarily for the chat emote sanitizer, which can match against ":b" as an emote, which is a valid radio keycode.
|
||||
/// </remarks>
|
||||
public void GetRadioKeycodePrefix(EntityUid source,
|
||||
string input,
|
||||
out string output,
|
||||
out string prefix)
|
||||
{
|
||||
prefix = string.Empty;
|
||||
output = input;
|
||||
|
||||
// If the string is less than 2, then it's probably supposed to be an emote.
|
||||
// No one is sending empty radio messages!
|
||||
if (input.Length <= 2)
|
||||
return;
|
||||
|
||||
if (!(input.StartsWith(RadioChannelPrefix) || input.StartsWith(RadioChannelAltPrefix)))
|
||||
return;
|
||||
|
||||
if (!_keyCodes.TryGetValue(char.ToLower(input[1]), out _))
|
||||
return;
|
||||
|
||||
prefix = input[..2];
|
||||
output = input[2..];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve radio prefixes in chat messages (e.g., remove a leading ":e" and resolve the requested
|
||||
/// channel. Returns true if a radio message was attempted, even if the channel is invalid.
|
||||
|
||||
13
Content.Shared/Chemistry/InjectOverTimeEvent.cs
Normal file
13
Content.Shared/Chemistry/InjectOverTimeEvent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Content.Shared.Chemistry.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed on an entity when it embeds in another entity.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct InjectOverTimeEvent(EntityUid embeddedIntoUid)
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity that is embedded in.
|
||||
/// </summary>
|
||||
public readonly EntityUid EmbeddedIntoUid = embeddedIntoUid;
|
||||
}
|
||||
9
Content.Shared/Ghost/SpectralComponent.cs
Normal file
9
Content.Shared/Ghost/SpectralComponent.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Ghost;
|
||||
|
||||
/// <summary>
|
||||
/// Marker component to identify "ghostly" entities.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class SpectralComponent : Component { }
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory.VirtualItem;
|
||||
@@ -130,7 +131,7 @@ public abstract partial class SharedHandsSystem
|
||||
TransformSystem.DropNextTo((entity, itemXform), (uid, userXform));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// drop the item with heavy calculations from their hands and place it at the calculated interaction range position
|
||||
// The DoDrop is handle if there's no drop target
|
||||
DoDrop(uid, hand, doDropInteraction: doDropInteraction, handsComp);
|
||||
@@ -138,7 +139,7 @@ public abstract partial class SharedHandsSystem
|
||||
// if there's no drop location stop here
|
||||
if (targetDropLocation == null)
|
||||
return true;
|
||||
|
||||
|
||||
// otherwise, also move dropped item and rotate it properly according to grid/map
|
||||
var (itemPos, itemRot) = TransformSystem.GetWorldPositionRotation(entity);
|
||||
var origin = new MapCoordinates(itemPos, itemXform.MapID);
|
||||
@@ -197,7 +198,7 @@ public abstract partial class SharedHandsSystem
|
||||
/// <summary>
|
||||
/// Removes the contents of a hand from its container. Assumes that the removal is allowed. In general, you should not be calling this directly.
|
||||
/// </summary>
|
||||
public virtual void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? handsComp = null)
|
||||
public virtual void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? handsComp = null, bool log = true)
|
||||
{
|
||||
if (!Resolve(uid, ref handsComp))
|
||||
return;
|
||||
@@ -221,6 +222,9 @@ public abstract partial class SharedHandsSystem
|
||||
if (doDropInteraction)
|
||||
_interactionSystem.DroppedInteraction(uid, entity);
|
||||
|
||||
if (log)
|
||||
_adminLogger.Add(LogType.Drop, LogImpact.Low, $"{ToPrettyString(uid):user} dropped {ToPrettyString(entity):entity}");
|
||||
|
||||
if (hand == handsComp.ActiveHand)
|
||||
RaiseLocalEvent(entity, new HandDeselectedEvent(uid));
|
||||
}
|
||||
|
||||
@@ -178,8 +178,8 @@ public abstract partial class SharedHandsSystem : EntitySystem
|
||||
if (!CanPickupToHand(uid, entity, handsComp.ActiveHand, checkActionBlocker, handsComp))
|
||||
return false;
|
||||
|
||||
DoDrop(uid, hand, false, handsComp);
|
||||
DoPickup(uid, handsComp.ActiveHand, entity, handsComp);
|
||||
DoDrop(uid, hand, false, handsComp, log:false);
|
||||
DoPickup(uid, handsComp.ActiveHand, entity, handsComp, log: false);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ public abstract partial class SharedHandsSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Puts an entity into the player's hand, assumes that the insertion is allowed. In general, you should not be calling this function directly.
|
||||
/// </summary>
|
||||
public virtual void DoPickup(EntityUid uid, Hand hand, EntityUid entity, HandsComponent? hands = null)
|
||||
public virtual void DoPickup(EntityUid uid, Hand hand, EntityUid entity, HandsComponent? hands = null, bool log = true)
|
||||
{
|
||||
if (!Resolve(uid, ref hands))
|
||||
return;
|
||||
@@ -235,7 +235,8 @@ public abstract partial class SharedHandsSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
_adminLogger.Add(LogType.Pickup, LogImpact.Low, $"{ToPrettyString(uid):user} picked up {ToPrettyString(entity):entity}");
|
||||
if (log)
|
||||
_adminLogger.Add(LogType.Pickup, LogImpact.Low, $"{ToPrettyString(uid):user} picked up {ToPrettyString(entity):entity}");
|
||||
|
||||
Dirty(uid, hands);
|
||||
|
||||
|
||||
@@ -94,22 +94,38 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
|
||||
/// </summary>
|
||||
public void AddImplants(EntityUid uid, IEnumerable<String> implants)
|
||||
{
|
||||
var coords = Transform(uid).Coordinates;
|
||||
foreach (var id in implants)
|
||||
{
|
||||
var ent = Spawn(id, coords);
|
||||
if (TryComp<SubdermalImplantComponent>(ent, out var implant))
|
||||
{
|
||||
ForceImplant(uid, ent, implant);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"Found invalid starting implant '{id}' on {uid} {ToPrettyString(uid):implanted}");
|
||||
Del(ent);
|
||||
}
|
||||
AddImplant(uid, id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a single implant to a person, and returns the implant.
|
||||
/// Logs any implant ids that don't have <see cref="SubdermalImplantComponent"/>.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The implant, if it was successfully created. Otherwise, null.
|
||||
/// </returns>>
|
||||
public EntityUid? AddImplant(EntityUid uid, String implantId)
|
||||
{
|
||||
var coords = Transform(uid).Coordinates;
|
||||
var ent = Spawn(implantId, coords);
|
||||
|
||||
if (TryComp<SubdermalImplantComponent>(ent, out var implant))
|
||||
{
|
||||
ForceImplant(uid, ent, implant);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"Found invalid starting implant '{implantId}' on {uid} {ToPrettyString(uid):implanted}");
|
||||
Del(ent);
|
||||
return null;
|
||||
}
|
||||
|
||||
return ent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces an implant into a person
|
||||
/// Good for on spawn related code or admin additions
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Content.Shared.Interaction.Events;
|
||||
/// </remarks>
|
||||
public sealed class ContactInteractionEvent : HandledEntityEventArgs
|
||||
{
|
||||
public readonly EntityUid Other;
|
||||
public EntityUid Other;
|
||||
|
||||
public ContactInteractionEvent(EntityUid other)
|
||||
{
|
||||
|
||||
@@ -3,5 +3,7 @@ namespace Content.Shared.Interaction.Events;
|
||||
/// <summary>
|
||||
/// Raised on the target when failing to pet/hug something.
|
||||
/// </summary>
|
||||
// TODO INTERACTION
|
||||
// Rename this, or move it to another namespace to make it clearer that this is specific to "petting/hugging" (InteractionPopupSystem)
|
||||
[ByRefEvent]
|
||||
public readonly record struct InteractionFailureEvent(EntityUid User);
|
||||
|
||||
@@ -3,5 +3,7 @@ namespace Content.Shared.Interaction.Events;
|
||||
/// <summary>
|
||||
/// Raised on the target when successfully petting/hugging something.
|
||||
/// </summary>
|
||||
// TODO INTERACTION
|
||||
// Rename this, or move it to another namespace to make it clearer that this is specific to "petting/hugging" (InteractionPopupSystem)
|
||||
[ByRefEvent]
|
||||
public readonly record struct InteractionSuccessEvent(EntityUid User);
|
||||
|
||||
@@ -456,8 +456,22 @@ namespace Content.Shared.Interaction
|
||||
inRangeUnobstructed);
|
||||
}
|
||||
|
||||
private bool IsDeleted(EntityUid uid)
|
||||
{
|
||||
return TerminatingOrDeleted(uid) || EntityManager.IsQueuedForDeletion(uid);
|
||||
}
|
||||
|
||||
private bool IsDeleted(EntityUid? uid)
|
||||
{
|
||||
//optional / null entities can pass this validation check. I.e., is-deleted returns false for null uids
|
||||
return uid != null && IsDeleted(uid.Value);
|
||||
}
|
||||
|
||||
public void InteractHand(EntityUid user, EntityUid target)
|
||||
{
|
||||
if (IsDeleted(user) || IsDeleted(target))
|
||||
return;
|
||||
|
||||
var complexInteractions = _actionBlockerSystem.CanComplexInteract(user);
|
||||
if (!complexInteractions)
|
||||
{
|
||||
@@ -466,7 +480,8 @@ namespace Content.Shared.Interaction
|
||||
checkCanInteract: false,
|
||||
checkUseDelay: true,
|
||||
checkAccess: false,
|
||||
complexInteractions: complexInteractions);
|
||||
complexInteractions: complexInteractions,
|
||||
checkDeletion: false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -479,6 +494,7 @@ namespace Content.Shared.Interaction
|
||||
return;
|
||||
}
|
||||
|
||||
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target));
|
||||
// all interactions should only happen when in range / unobstructed, so no range check is needed
|
||||
var message = new InteractHandEvent(user, target);
|
||||
RaiseLocalEvent(target, message, true);
|
||||
@@ -487,18 +503,23 @@ namespace Content.Shared.Interaction
|
||||
if (message.Handled)
|
||||
return;
|
||||
|
||||
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target));
|
||||
// Else we run Activate.
|
||||
InteractionActivate(user,
|
||||
target,
|
||||
checkCanInteract: false,
|
||||
checkUseDelay: true,
|
||||
checkAccess: false,
|
||||
complexInteractions: complexInteractions);
|
||||
complexInteractions: complexInteractions,
|
||||
checkDeletion: false);
|
||||
}
|
||||
|
||||
public void InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target,
|
||||
EntityCoordinates clickLocation, bool inRangeUnobstructed)
|
||||
{
|
||||
if (IsDeleted(user) || IsDeleted(used) || IsDeleted(target))
|
||||
return;
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
_adminLogger.Add(
|
||||
@@ -514,9 +535,10 @@ namespace Content.Shared.Interaction
|
||||
$"{ToPrettyString(user):user} interacted with *nothing* using {ToPrettyString(used):used}");
|
||||
}
|
||||
|
||||
if (RangedInteractDoBefore(user, used, target, clickLocation, inRangeUnobstructed))
|
||||
if (RangedInteractDoBefore(user, used, target, clickLocation, inRangeUnobstructed, checkDeletion: false))
|
||||
return;
|
||||
|
||||
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
|
||||
if (target != null)
|
||||
{
|
||||
var rangedMsg = new RangedInteractEvent(user, used, target.Value, clickLocation);
|
||||
@@ -524,12 +546,12 @@ namespace Content.Shared.Interaction
|
||||
|
||||
// We contact the USED entity, but not the target.
|
||||
DoContactInteraction(user, used, rangedMsg);
|
||||
|
||||
if (rangedMsg.Handled)
|
||||
return;
|
||||
}
|
||||
|
||||
InteractDoAfter(user, used, target, clickLocation, inRangeUnobstructed);
|
||||
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
|
||||
InteractDoAfter(user, used, target, clickLocation, inRangeUnobstructed, checkDeletion: false);
|
||||
}
|
||||
|
||||
protected bool ValidateInteractAndFace(EntityUid user, EntityCoordinates coordinates)
|
||||
@@ -933,11 +955,18 @@ namespace Content.Shared.Interaction
|
||||
EntityUid used,
|
||||
EntityUid? target,
|
||||
EntityCoordinates clickLocation,
|
||||
bool canReach)
|
||||
bool canReach,
|
||||
bool checkDeletion = true)
|
||||
{
|
||||
if (checkDeletion && (IsDeleted(user) || IsDeleted(used) || IsDeleted(target)))
|
||||
return false;
|
||||
|
||||
var ev = new BeforeRangedInteractEvent(user, used, target, clickLocation, canReach);
|
||||
RaiseLocalEvent(used, ev);
|
||||
|
||||
if (!ev.Handled)
|
||||
return false;
|
||||
|
||||
// We contact the USED entity, but not the target.
|
||||
DoContactInteraction(user, used, ev);
|
||||
return ev.Handled;
|
||||
@@ -966,6 +995,9 @@ namespace Content.Shared.Interaction
|
||||
bool checkCanInteract = true,
|
||||
bool checkCanUse = true)
|
||||
{
|
||||
if (IsDeleted(user) || IsDeleted(used) || IsDeleted(target))
|
||||
return false;
|
||||
|
||||
if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target))
|
||||
return false;
|
||||
|
||||
@@ -977,9 +1009,10 @@ namespace Content.Shared.Interaction
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target} using {ToPrettyString(used):used}");
|
||||
|
||||
if (RangedInteractDoBefore(user, used, target, clickLocation, true))
|
||||
if (RangedInteractDoBefore(user, used, target, clickLocation, canReach: true, checkDeletion: false))
|
||||
return true;
|
||||
|
||||
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
|
||||
// all interactions should only happen when in range / unobstructed, so no range check is needed
|
||||
var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation);
|
||||
RaiseLocalEvent(target, interactUsingEvent, true);
|
||||
@@ -989,8 +1022,10 @@ namespace Content.Shared.Interaction
|
||||
if (interactUsingEvent.Handled)
|
||||
return true;
|
||||
|
||||
if (InteractDoAfter(user, used, target, clickLocation, canReach: true))
|
||||
if (InteractDoAfter(user, used, target, clickLocation, canReach: true, checkDeletion: false))
|
||||
return true;
|
||||
|
||||
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1004,11 +1039,14 @@ namespace Content.Shared.Interaction
|
||||
/// <param name="canReach">Whether the <paramref name="user"/> is in range of the <paramref name="target"/>.
|
||||
/// </param>
|
||||
/// <returns>True if the interaction was handled. Otherwise, false.</returns>
|
||||
public bool InteractDoAfter(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool canReach)
|
||||
public bool InteractDoAfter(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool canReach, bool checkDeletion = true)
|
||||
{
|
||||
if (target is { Valid: false })
|
||||
target = null;
|
||||
|
||||
if (checkDeletion && (IsDeleted(user) || IsDeleted(used) || IsDeleted(target)))
|
||||
return false;
|
||||
|
||||
var afterInteractEvent = new AfterInteractEvent(user, used, target, clickLocation, canReach);
|
||||
RaiseLocalEvent(used, afterInteractEvent);
|
||||
DoContactInteraction(user, used, afterInteractEvent);
|
||||
@@ -1024,6 +1062,7 @@ namespace Content.Shared.Interaction
|
||||
if (target == null)
|
||||
return false;
|
||||
|
||||
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
|
||||
var afterInteractUsingEvent = new AfterInteractUsingEvent(user, used, target, clickLocation, canReach);
|
||||
RaiseLocalEvent(target.Value, afterInteractUsingEvent);
|
||||
|
||||
@@ -1034,9 +1073,7 @@ namespace Content.Shared.Interaction
|
||||
// Contact interactions are currently only used for forensics, so we don't raise used -> target
|
||||
}
|
||||
|
||||
if (afterInteractUsingEvent.Handled)
|
||||
return true;
|
||||
return false;
|
||||
return afterInteractUsingEvent.Handled;
|
||||
}
|
||||
|
||||
#region ActivateItemInWorld
|
||||
@@ -1068,8 +1105,13 @@ namespace Content.Shared.Interaction
|
||||
bool checkCanInteract = true,
|
||||
bool checkUseDelay = true,
|
||||
bool checkAccess = true,
|
||||
bool? complexInteractions = null)
|
||||
bool? complexInteractions = null,
|
||||
bool checkDeletion = true)
|
||||
{
|
||||
if (checkDeletion && (IsDeleted(user) || IsDeleted(used)))
|
||||
return false;
|
||||
|
||||
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used));
|
||||
_delayQuery.TryComp(used, out var delayComponent);
|
||||
if (checkUseDelay && delayComponent != null && _useDelay.IsDelayed((used, delayComponent)))
|
||||
return false;
|
||||
@@ -1085,21 +1127,32 @@ namespace Content.Shared.Interaction
|
||||
if (checkAccess && !IsAccessible(user, used))
|
||||
return false;
|
||||
|
||||
complexInteractions ??= SupportsComplexInteractions(user);
|
||||
complexInteractions ??= _actionBlockerSystem.CanComplexInteract(user);
|
||||
var activateMsg = new ActivateInWorldEvent(user, used, complexInteractions.Value);
|
||||
RaiseLocalEvent(used, activateMsg, true);
|
||||
if (activateMsg.Handled)
|
||||
{
|
||||
DoContactInteraction(user, used);
|
||||
if (!activateMsg.WasLogged)
|
||||
_adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
|
||||
|
||||
if (delayComponent != null)
|
||||
_useDelay.TryResetDelay(used, component: delayComponent);
|
||||
return true;
|
||||
}
|
||||
|
||||
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used));
|
||||
var userEv = new UserActivateInWorldEvent(user, used, complexInteractions.Value);
|
||||
RaiseLocalEvent(user, userEv, true);
|
||||
if (!activateMsg.Handled && !userEv.Handled)
|
||||
if (!userEv.Handled)
|
||||
return false;
|
||||
|
||||
DoContactInteraction(user, used, activateMsg);
|
||||
DoContactInteraction(user, used);
|
||||
// Still need to call this even without checkUseDelay in case this gets relayed from Activate.
|
||||
if (delayComponent != null)
|
||||
_useDelay.TryResetDelay(used, component: delayComponent);
|
||||
|
||||
if (!activateMsg.WasLogged)
|
||||
_adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
|
||||
_adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
|
||||
return true;
|
||||
}
|
||||
#endregion
|
||||
@@ -1118,6 +1171,9 @@ namespace Content.Shared.Interaction
|
||||
bool checkCanInteract = true,
|
||||
bool checkUseDelay = true)
|
||||
{
|
||||
if (IsDeleted(user) || IsDeleted(used))
|
||||
return false;
|
||||
|
||||
_delayQuery.TryComp(used, out var delayComponent);
|
||||
if (checkUseDelay && delayComponent != null && _useDelay.IsDelayed((used, delayComponent)))
|
||||
return true; // if the item is on cooldown, we consider this handled.
|
||||
@@ -1138,8 +1194,9 @@ namespace Content.Shared.Interaction
|
||||
return true;
|
||||
}
|
||||
|
||||
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used));
|
||||
// else, default to activating the item
|
||||
return InteractionActivate(user, used, false, false, false);
|
||||
return InteractionActivate(user, used, false, false, false, checkDeletion: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1164,10 +1221,11 @@ namespace Content.Shared.Interaction
|
||||
|
||||
public void DroppedInteraction(EntityUid user, EntityUid item)
|
||||
{
|
||||
if (IsDeleted(user) || IsDeleted(item))
|
||||
return;
|
||||
|
||||
var dropMsg = new DroppedEvent(user);
|
||||
RaiseLocalEvent(item, dropMsg, true);
|
||||
if (dropMsg.Handled)
|
||||
_adminLogger.Add(LogType.Drop, LogImpact.Low, $"{ToPrettyString(user):user} dropped {ToPrettyString(item):entity}");
|
||||
|
||||
// If the dropper is rotated then use their targetrelativerotation as the drop rotation
|
||||
var rotation = Angle.Zero;
|
||||
@@ -1314,15 +1372,21 @@ namespace Content.Shared.Interaction
|
||||
if (uidB == null || args?.Handled == false)
|
||||
return;
|
||||
|
||||
// Entities may no longer exist (banana was eaten, or human was exploded)?
|
||||
if (!Exists(uidA) || !Exists(uidB))
|
||||
if (uidA == uidB.Value)
|
||||
return;
|
||||
|
||||
if (Paused(uidA) || Paused(uidB.Value))
|
||||
if (!TryComp(uidA, out MetaDataComponent? metaA) || metaA.EntityPaused)
|
||||
return;
|
||||
|
||||
RaiseLocalEvent(uidA, new ContactInteractionEvent(uidB.Value));
|
||||
RaiseLocalEvent(uidB.Value, new ContactInteractionEvent(uidA));
|
||||
if (!TryComp(uidB, out MetaDataComponent? metaB) || metaB.EntityPaused)
|
||||
return ;
|
||||
|
||||
// TODO Struct event
|
||||
var ev = new ContactInteractionEvent(uidB.Value);
|
||||
RaiseLocalEvent(uidA, ev);
|
||||
|
||||
ev.Other = uidA;
|
||||
RaiseLocalEvent(uidB.Value, ev);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ namespace Content.Shared.Localizations
|
||||
_loc.AddFunction(culture, "LOC", FormatLoc);
|
||||
_loc.AddFunction(culture, "NATURALFIXED", FormatNaturalFixed);
|
||||
_loc.AddFunction(culture, "NATURALPERCENT", FormatNaturalPercent);
|
||||
_loc.AddFunction(culture, "PLAYTIME", FormatPlaytime);
|
||||
|
||||
_loc.AddFunction(culture, "MANY", FormatMany); // TODO: Temporary fix for MANY() fluent errors. Remove after resolve errors.
|
||||
|
||||
/*
|
||||
@@ -151,6 +153,16 @@ namespace Content.Shared.Localizations
|
||||
return Loc.GetString($"zzzz-fmt-direction-{dir.ToString()}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats playtime as hours and minutes.
|
||||
/// </summary>
|
||||
public static string FormatPlaytime(TimeSpan time)
|
||||
{
|
||||
var hours = (int)time.TotalHours;
|
||||
var minutes = time.Minutes;
|
||||
return Loc.GetString($"zzzz-fmt-playtime", ("hours", hours), ("minutes", minutes));
|
||||
}
|
||||
|
||||
private static ILocValue FormatLoc(LocArgs args)
|
||||
{
|
||||
var id = ((LocValueString) args.Args[0]).Value;
|
||||
@@ -239,5 +251,15 @@ namespace Content.Shared.Localizations
|
||||
|
||||
return new LocValueString(res);
|
||||
}
|
||||
|
||||
private static ILocValue FormatPlaytime(LocArgs args)
|
||||
{
|
||||
var time = TimeSpan.Zero;
|
||||
if (args.Args is { Count: > 0 } && args.Args[0].Value is TimeSpan timeArg)
|
||||
{
|
||||
time = timeArg;
|
||||
}
|
||||
return new LocValueString(FormatPlaytime(time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,19 +483,6 @@ public abstract class SharedMindSystem : EntitySystem
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a role component from a player's mind.
|
||||
/// </summary>
|
||||
/// <returns>Whether a role was found</returns>
|
||||
public bool TryGetRole<T>(EntityUid user, [NotNullWhen(true)] out T? role) where T : IComponent
|
||||
{
|
||||
role = default;
|
||||
if (!TryComp<MindContainerComponent>(user, out var mindContainer) || mindContainer.Mind == null)
|
||||
return false;
|
||||
|
||||
return TryComp(mindContainer.Mind, out role);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Mind's UserId, Session, and updates the player's PlayerData. This should have no direct effect on the
|
||||
/// entity that any mind is connected to, except as a side effect of the fact that it may change a player's
|
||||
|
||||
@@ -27,6 +27,50 @@ public sealed partial class NavMapComponent : Component
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Dictionary<NetEntity, SharedNavMapSystem.NavMapBeacon> Beacons = new();
|
||||
|
||||
/// <summary>
|
||||
/// Describes the properties of a region on the station.
|
||||
/// It is indexed by the entity assigned as the region owner.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public Dictionary<NetEntity, SharedNavMapSystem.NavMapRegionProperties> RegionProperties = new();
|
||||
|
||||
/// <summary>
|
||||
/// All flood filled regions, ready for display on a NavMapControl.
|
||||
/// It is indexed by the entity assigned as the region owner.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For client use only
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public Dictionary<NetEntity, NavMapRegionOverlay> RegionOverlays = new();
|
||||
|
||||
/// <summary>
|
||||
/// A queue of all region owners that are waiting their associated regions to be floodfilled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For client use only
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public Queue<NetEntity> QueuedRegionsToFlood = new();
|
||||
|
||||
/// <summary>
|
||||
/// A look up table to get a list of region owners associated with a flood filled chunk.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For client use only
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public Dictionary<Vector2i, HashSet<NetEntity>> ChunkToRegionOwnerTable = new();
|
||||
|
||||
/// <summary>
|
||||
/// A look up table to find flood filled chunks associated with a given region owner.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For client use only
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public Dictionary<NetEntity, HashSet<Vector2i>> RegionOwnerToChunkTable = new();
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
@@ -51,10 +95,30 @@ public sealed class NavMapChunk(Vector2i origin)
|
||||
public GameTick LastUpdate;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class NavMapRegionOverlay(Enum uiKey, List<(Vector2i, Vector2i)> gridCoords)
|
||||
{
|
||||
/// <summary>
|
||||
/// The key to the UI that will be displaying this region on its navmap
|
||||
/// </summary>
|
||||
public Enum UiKey = uiKey;
|
||||
|
||||
/// <summary>
|
||||
/// The local grid coordinates of the rectangles that make up the region
|
||||
/// Item1 is the top left corner, Item2 is the bottom right corner
|
||||
/// </summary>
|
||||
public List<(Vector2i, Vector2i)> GridCoords = gridCoords;
|
||||
|
||||
/// <summary>
|
||||
/// Color of the region
|
||||
/// </summary>
|
||||
public Color Color = Color.White;
|
||||
}
|
||||
|
||||
public enum NavMapChunkType : byte
|
||||
{
|
||||
// Values represent bit shift offsets when retrieving data in the tile array.
|
||||
Invalid = byte.MaxValue,
|
||||
Invalid = byte.MaxValue,
|
||||
Floor = 0, // I believe floors have directional information for diagonal tiles?
|
||||
Wall = SharedNavMapSystem.Directions,
|
||||
Airlock = 2 * SharedNavMapSystem.Directions,
|
||||
|
||||
@@ -3,10 +3,9 @@ using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Pinpointer;
|
||||
|
||||
@@ -16,7 +15,7 @@ public abstract class SharedNavMapSystem : EntitySystem
|
||||
public const int Directions = 4; // Not directly tied to number of atmos directions
|
||||
|
||||
public const int ChunkSize = 8;
|
||||
public const int ArraySize = ChunkSize* ChunkSize;
|
||||
public const int ArraySize = ChunkSize * ChunkSize;
|
||||
|
||||
public const int AllDirMask = (1 << Directions) - 1;
|
||||
public const int AirlockMask = AllDirMask << (int) NavMapChunkType.Airlock;
|
||||
@@ -24,6 +23,7 @@ public abstract class SharedNavMapSystem : EntitySystem
|
||||
public const int FloorMask = AllDirMask << (int) NavMapChunkType.Floor;
|
||||
|
||||
[Robust.Shared.IoC.Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly INetManager _net = default!;
|
||||
|
||||
private static readonly ProtoId<TagPrototype>[] WallTags = {"Wall", "Window"};
|
||||
private EntityQuery<NavMapDoorComponent> _doorQuery;
|
||||
@@ -57,7 +57,7 @@ public abstract class SharedNavMapSystem : EntitySystem
|
||||
public NavMapChunkType GetEntityType(EntityUid uid)
|
||||
{
|
||||
if (_doorQuery.HasComp(uid))
|
||||
return NavMapChunkType.Airlock;
|
||||
return NavMapChunkType.Airlock;
|
||||
|
||||
if (_tagSystem.HasAnyTag(uid, WallTags))
|
||||
return NavMapChunkType.Wall;
|
||||
@@ -81,6 +81,57 @@ public abstract class SharedNavMapSystem : EntitySystem
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddOrUpdateNavMapRegion(EntityUid uid, NavMapComponent component, NetEntity regionOwner, NavMapRegionProperties regionProperties)
|
||||
{
|
||||
// Check if a new region has been added or an existing one has been altered
|
||||
var isDirty = !component.RegionProperties.TryGetValue(regionOwner, out var oldProperties) || oldProperties != regionProperties;
|
||||
|
||||
if (isDirty)
|
||||
{
|
||||
component.RegionProperties[regionOwner] = regionProperties;
|
||||
|
||||
if (_net.IsServer)
|
||||
Dirty(uid, component);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveNavMapRegion(EntityUid uid, NavMapComponent component, NetEntity regionOwner)
|
||||
{
|
||||
bool regionOwnerRemoved = component.RegionProperties.Remove(regionOwner) | component.RegionOverlays.Remove(regionOwner);
|
||||
|
||||
if (regionOwnerRemoved)
|
||||
{
|
||||
if (component.RegionOwnerToChunkTable.TryGetValue(regionOwner, out var affectedChunks))
|
||||
{
|
||||
foreach (var affectedChunk in affectedChunks)
|
||||
{
|
||||
if (component.ChunkToRegionOwnerTable.TryGetValue(affectedChunk, out var regionOwners))
|
||||
regionOwners.Remove(regionOwner);
|
||||
}
|
||||
|
||||
component.RegionOwnerToChunkTable.Remove(regionOwner);
|
||||
}
|
||||
|
||||
if (_net.IsServer)
|
||||
Dirty(uid, component);
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<NetEntity, NavMapRegionOverlay> GetNavMapRegionOverlays(EntityUid uid, NavMapComponent component, Enum uiKey)
|
||||
{
|
||||
var regionOverlays = new Dictionary<NetEntity, NavMapRegionOverlay>();
|
||||
|
||||
foreach (var (regionOwner, regionOverlay) in component.RegionOverlays)
|
||||
{
|
||||
if (!regionOverlay.UiKey.Equals(uiKey))
|
||||
continue;
|
||||
|
||||
regionOverlays.Add(regionOwner, regionOverlay);
|
||||
}
|
||||
|
||||
return regionOverlays;
|
||||
}
|
||||
|
||||
#region: Event handling
|
||||
|
||||
private void OnGetState(EntityUid uid, NavMapComponent component, ref ComponentGetState args)
|
||||
@@ -97,7 +148,7 @@ public abstract class SharedNavMapSystem : EntitySystem
|
||||
chunks.Add(origin, chunk.TileData);
|
||||
}
|
||||
|
||||
args.State = new NavMapState(chunks, component.Beacons);
|
||||
args.State = new NavMapState(chunks, component.Beacons, component.RegionProperties);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,7 +161,7 @@ public abstract class SharedNavMapSystem : EntitySystem
|
||||
chunks.Add(origin, chunk.TileData);
|
||||
}
|
||||
|
||||
args.State = new NavMapDeltaState(chunks, component.Beacons, new(component.Chunks.Keys));
|
||||
args.State = new NavMapDeltaState(chunks, component.Beacons, component.RegionProperties, new(component.Chunks.Keys));
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -120,22 +171,26 @@ public abstract class SharedNavMapSystem : EntitySystem
|
||||
[Serializable, NetSerializable]
|
||||
protected sealed class NavMapState(
|
||||
Dictionary<Vector2i, int[]> chunks,
|
||||
Dictionary<NetEntity, NavMapBeacon> beacons)
|
||||
Dictionary<NetEntity, NavMapBeacon> beacons,
|
||||
Dictionary<NetEntity, NavMapRegionProperties> regions)
|
||||
: ComponentState
|
||||
{
|
||||
public Dictionary<Vector2i, int[]> Chunks = chunks;
|
||||
public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons;
|
||||
public Dictionary<NetEntity, NavMapRegionProperties> Regions = regions;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
protected sealed class NavMapDeltaState(
|
||||
Dictionary<Vector2i, int[]> modifiedChunks,
|
||||
Dictionary<NetEntity, NavMapBeacon> beacons,
|
||||
Dictionary<NetEntity, NavMapRegionProperties> regions,
|
||||
HashSet<Vector2i> allChunks)
|
||||
: ComponentState, IComponentDeltaState<NavMapState>
|
||||
{
|
||||
public Dictionary<Vector2i, int[]> ModifiedChunks = modifiedChunks;
|
||||
public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons;
|
||||
public Dictionary<NetEntity, NavMapRegionProperties> Regions = regions;
|
||||
public HashSet<Vector2i> AllChunks = allChunks;
|
||||
|
||||
public void ApplyToFullState(NavMapState state)
|
||||
@@ -159,11 +214,18 @@ public abstract class SharedNavMapSystem : EntitySystem
|
||||
{
|
||||
state.Beacons.Add(nuid, beacon);
|
||||
}
|
||||
|
||||
state.Regions.Clear();
|
||||
foreach (var (nuid, region) in Regions)
|
||||
{
|
||||
state.Regions.Add(nuid, region);
|
||||
}
|
||||
}
|
||||
|
||||
public NavMapState CreateNewFullState(NavMapState state)
|
||||
{
|
||||
var chunks = new Dictionary<Vector2i, int[]>(state.Chunks.Count);
|
||||
|
||||
foreach (var (index, data) in state.Chunks)
|
||||
{
|
||||
if (!AllChunks!.Contains(index))
|
||||
@@ -177,12 +239,25 @@ public abstract class SharedNavMapSystem : EntitySystem
|
||||
Array.Copy(newData, data, ArraySize);
|
||||
}
|
||||
|
||||
return new NavMapState(chunks, new(Beacons));
|
||||
return new NavMapState(chunks, new(Beacons), new(Regions));
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public record struct NavMapBeacon(NetEntity NetEnt, Color Color, string Text, Vector2 Position);
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public record struct NavMapRegionProperties(NetEntity Owner, Enum UiKey, HashSet<Vector2i> Seeds)
|
||||
{
|
||||
// Server defined color for the region
|
||||
public Color Color = Color.White;
|
||||
|
||||
// The maximum number of tiles that can be assigned to this region
|
||||
public int MaxArea = 625;
|
||||
|
||||
// The maximum distance this region can propagate from its seeds
|
||||
public int MaxRadius = 25;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using Robust.Shared.GameStates;
|
||||
namespace Content.Shared.Power.Generator;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class ActiveGeneratorRevvingComponent: Component
|
||||
public sealed partial class ActiveGeneratorRevvingComponent : Component
|
||||
{
|
||||
[DataField, ViewVariables(VVAccess.ReadOnly), AutoNetworkedField]
|
||||
public TimeSpan CurrentTime = TimeSpan.Zero;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Content.Shared.Power.Generator;
|
||||
|
||||
public sealed class ActiveGeneratorRevvingSystem: EntitySystem
|
||||
public sealed class ActiveGeneratorRevvingSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -25,7 +25,7 @@ public sealed class ActiveGeneratorRevvingSystem: EntitySystem
|
||||
/// <param name="component">ActiveGeneratorRevvingComponent of the generator entity.</param>
|
||||
public void StartAutoRevving(EntityUid uid, ActiveGeneratorRevvingComponent? component = null)
|
||||
{
|
||||
if (Resolve(uid, ref component))
|
||||
if (Resolve(uid, ref component, false))
|
||||
{
|
||||
// reset the revving
|
||||
component.CurrentTime = TimeSpan.FromSeconds(0);
|
||||
|
||||
@@ -13,37 +13,43 @@ public sealed partial class EmbeddableProjectileComponent : Component
|
||||
/// <summary>
|
||||
/// Minimum speed of the projectile to embed.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public float MinimumSpeed = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// Delete the entity on embedded removal?
|
||||
/// Does nothing if there's no RemovalTime.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool DeleteOnRemove;
|
||||
|
||||
/// <summary>
|
||||
/// How long it takes to remove the embedded object.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public float? RemovalTime = 3f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity will embed when thrown, or only when shot as a projectile.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool EmbedOnThrow = true;
|
||||
|
||||
/// <summary>
|
||||
/// How far into the entity should we offset (0 is wherever we collided).
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public Vector2 Offset = Vector2.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Sound to play after embedding into a hit target.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public SoundSpecifier? Sound;
|
||||
|
||||
/// <summary>
|
||||
/// Uid of the entity the projectile is embed into.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntityUid? EmbeddedIntoUid;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ public abstract partial class SharedProjectileSystem : EntitySystem
|
||||
TryComp<PhysicsComponent>(uid, out var physics);
|
||||
_physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform);
|
||||
_transform.AttachToGridOrMap(uid, xform);
|
||||
component.EmbeddedIntoUid = null;
|
||||
Dirty(uid, component);
|
||||
|
||||
// Reset whether the projectile has damaged anything if it successfully was removed
|
||||
if (TryComp<ProjectileComponent>(uid, out var projectile))
|
||||
@@ -127,8 +129,10 @@ public abstract partial class SharedProjectileSystem : EntitySystem
|
||||
}
|
||||
|
||||
_audio.PlayPredicted(component.Sound, uid, null);
|
||||
component.EmbeddedIntoUid = target;
|
||||
var ev = new EmbedEvent(user, target);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
Dirty(uid, component);
|
||||
}
|
||||
|
||||
private void PreventCollision(EntityUid uid, ProjectileComponent component, ref PreventCollideEvent args)
|
||||
|
||||
@@ -42,10 +42,12 @@ public abstract class SharedJammerSystem : EntitySystem
|
||||
{
|
||||
entity.Comp.SelectedPowerLevel = currIndex;
|
||||
Dirty(entity);
|
||||
if (_jammer.TrySetRange(entity.Owner, GetCurrentRange(entity)))
|
||||
{
|
||||
Popup.PopupPredicted(Loc.GetString(setting.Message), user, user);
|
||||
}
|
||||
|
||||
// If the jammer is off, this won't do anything which is fine.
|
||||
// The range should be updated when it turns on again!
|
||||
_jammer.TrySetRange(entity.Owner, GetCurrentRange(entity));
|
||||
|
||||
Popup.PopupClient(Loc.GetString(setting.Message), user, user);
|
||||
},
|
||||
Text = Loc.GetString(setting.Name),
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed partial class AgeRequirement : JobRequirement
|
||||
|
||||
if (!Inverted)
|
||||
{
|
||||
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-to-young",
|
||||
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-too-young",
|
||||
("age", RequiredAge)));
|
||||
|
||||
if (profile.Age < RequiredAge)
|
||||
@@ -38,7 +38,7 @@ public sealed partial class AgeRequirement : JobRequirement
|
||||
}
|
||||
else
|
||||
{
|
||||
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-to-old",
|
||||
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-age-too-old",
|
||||
("age", RequiredAge)));
|
||||
|
||||
if (profile.Age > RequiredAge)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Localizations;
|
||||
using Content.Shared.Preferences;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -15,7 +16,7 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement
|
||||
/// Which department needs the required amount of time.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<DepartmentPrototype> Department = default!;
|
||||
public ProtoId<DepartmentPrototype> Department;
|
||||
|
||||
/// <summary>
|
||||
/// How long (in seconds) this requirement is.
|
||||
@@ -47,7 +48,9 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement
|
||||
playtime += otherTime;
|
||||
}
|
||||
|
||||
var deptDiff = Time.TotalMinutes - playtime.TotalMinutes;
|
||||
var deptDiffSpan = Time - playtime;
|
||||
var deptDiff = deptDiffSpan.TotalMinutes;
|
||||
var formattedDeptDiff = ContentLocalizationManager.FormatPlaytime(deptDiffSpan);
|
||||
var nameDepartment = "role-timer-department-unknown";
|
||||
|
||||
if (protoManager.TryIndex(Department, out var departmentIndexed))
|
||||
@@ -62,7 +65,7 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement
|
||||
|
||||
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
|
||||
"role-timer-department-insufficient",
|
||||
("time", Math.Ceiling(deptDiff)),
|
||||
("time", formattedDeptDiff),
|
||||
("department", Loc.GetString(nameDepartment)),
|
||||
("departmentColor", department.Color.ToHex())));
|
||||
return false;
|
||||
@@ -72,7 +75,7 @@ public sealed partial class DepartmentTimeRequirement : JobRequirement
|
||||
{
|
||||
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
|
||||
"role-timer-department-too-high",
|
||||
("time", -deptDiff),
|
||||
("time", formattedDeptDiff),
|
||||
("department", Loc.GetString(nameDepartment)),
|
||||
("departmentColor", department.Color.ToHex())));
|
||||
return false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Localizations;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Preferences;
|
||||
using JetBrains.Annotations;
|
||||
@@ -25,7 +26,9 @@ public sealed partial class OverallPlaytimeRequirement : JobRequirement
|
||||
reason = new FormattedMessage();
|
||||
|
||||
var overallTime = playTimes.GetValueOrDefault(PlayTimeTrackingShared.TrackerOverall);
|
||||
var overallDiff = Time.TotalMinutes - overallTime.TotalMinutes;
|
||||
var overallDiffSpan = Time - overallTime;
|
||||
var overallDiff = overallDiffSpan.TotalMinutes;
|
||||
var formattedOverallDiff = ContentLocalizationManager.FormatPlaytime(overallDiffSpan);
|
||||
|
||||
if (!Inverted)
|
||||
{
|
||||
@@ -34,14 +37,14 @@ public sealed partial class OverallPlaytimeRequirement : JobRequirement
|
||||
|
||||
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
|
||||
"role-timer-overall-insufficient",
|
||||
("time", Math.Ceiling(overallDiff))));
|
||||
("time", formattedOverallDiff)));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (overallDiff <= 0 || overallTime >= Time)
|
||||
{
|
||||
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-overall-too-high",
|
||||
("time", -overallDiff)));
|
||||
("time", formattedOverallDiff)));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Localizations;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
@@ -17,7 +18,7 @@ public sealed partial class RoleTimeRequirement : JobRequirement
|
||||
/// What particular role they need the time requirement with.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<PlayTimeTrackerPrototype> Role = default!;
|
||||
public ProtoId<PlayTimeTrackerPrototype> Role;
|
||||
|
||||
/// <inheritdoc cref="DepartmentTimeRequirement.Time"/>
|
||||
[DataField(required: true)]
|
||||
@@ -34,7 +35,9 @@ public sealed partial class RoleTimeRequirement : JobRequirement
|
||||
string proto = Role;
|
||||
|
||||
playTimes.TryGetValue(proto, out var roleTime);
|
||||
var roleDiff = Time.TotalMinutes - roleTime.TotalMinutes;
|
||||
var roleDiffSpan = Time - roleTime;
|
||||
var roleDiff = roleDiffSpan.TotalMinutes;
|
||||
var formattedRoleDiff = ContentLocalizationManager.FormatPlaytime(roleDiffSpan);
|
||||
var departmentColor = Color.Yellow;
|
||||
|
||||
if (entManager.EntitySysManager.TryGetEntitySystem(out SharedJobSystem? jobSystem))
|
||||
@@ -52,7 +55,7 @@ public sealed partial class RoleTimeRequirement : JobRequirement
|
||||
|
||||
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
|
||||
"role-timer-role-insufficient",
|
||||
("time", Math.Ceiling(roleDiff)),
|
||||
("time", formattedRoleDiff),
|
||||
("job", Loc.GetString(proto)),
|
||||
("departmentColor", departmentColor.ToHex())));
|
||||
return false;
|
||||
@@ -62,7 +65,7 @@ public sealed partial class RoleTimeRequirement : JobRequirement
|
||||
{
|
||||
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
|
||||
"role-timer-role-too-high",
|
||||
("time", -roleDiff),
|
||||
("time", formattedRoleDiff),
|
||||
("job", Loc.GetString(proto)),
|
||||
("departmentColor", departmentColor.ToHex())));
|
||||
return false;
|
||||
|
||||
@@ -103,7 +103,6 @@ public abstract class SharedJobSystem : EntitySystem
|
||||
public bool MindHasJobWithId(EntityUid? mindId, string prototypeId)
|
||||
{
|
||||
|
||||
MindRoleComponent? comp = null;
|
||||
if (mindId is null)
|
||||
return false;
|
||||
|
||||
@@ -112,9 +111,7 @@ public abstract class SharedJobSystem : EntitySystem
|
||||
if (role is null)
|
||||
return false;
|
||||
|
||||
comp = role.Value.Comp;
|
||||
|
||||
return (comp.JobPrototype == prototypeId);
|
||||
return role.Value.Comp1.JobPrototype == prototypeId;
|
||||
}
|
||||
|
||||
public bool MindTryGetJob(
|
||||
@@ -124,7 +121,7 @@ public abstract class SharedJobSystem : EntitySystem
|
||||
prototype = null;
|
||||
MindTryGetJobId(mindId, out var protoId);
|
||||
|
||||
return (_prototypes.TryIndex<JobPrototype>(protoId, out prototype) || prototype is not null);
|
||||
return _prototypes.TryIndex(protoId, out prototype) || prototype is not null;
|
||||
}
|
||||
|
||||
public bool MindTryGetJobId(
|
||||
@@ -137,9 +134,9 @@ public abstract class SharedJobSystem : EntitySystem
|
||||
return false;
|
||||
|
||||
if (_roles.MindHasRole<JobRoleComponent>(mindId.Value, out var role))
|
||||
job = role.Value.Comp.JobPrototype;
|
||||
job = role.Value.Comp1.JobPrototype;
|
||||
|
||||
return (job is not null);
|
||||
return job is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -42,6 +42,8 @@ public sealed partial class MindRoleComponent : BaseMindRoleComponent
|
||||
public ProtoId<JobPrototype>? JobPrototype { get; set; }
|
||||
}
|
||||
|
||||
// Why does this base component actually exist? It does make auto-categorization easy, but before that it was useless?
|
||||
[EntityCategory("Roles")]
|
||||
public abstract partial class BaseMindRoleComponent : Component
|
||||
{
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Roles;
|
||||
|
||||
@@ -92,19 +93,18 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
bool silent = false,
|
||||
string? jobPrototype = null)
|
||||
{
|
||||
// Can't have someone get paid for two jobs now, can we
|
||||
if (MindHasRole<JobRoleComponent>(mindId, out var jobRole)
|
||||
&& jobRole.Value.Comp.JobPrototype != jobPrototype)
|
||||
{
|
||||
Resolve(mindId, ref mind);
|
||||
if (mind is not null)
|
||||
{
|
||||
_adminLogger.Add(LogType.Mind,
|
||||
LogImpact.Low,
|
||||
$"Job Role of {ToPrettyString(mind.OwnedEntity)} changed from '{jobRole.Value.Comp.JobPrototype}' to '{jobPrototype}'");
|
||||
}
|
||||
if (!Resolve(mindId, ref mind))
|
||||
return;
|
||||
|
||||
jobRole.Value.Comp.JobPrototype = jobPrototype;
|
||||
// Can't have someone get paid for two jobs now, can we
|
||||
if (MindHasRole<JobRoleComponent>((mindId, mind), out var jobRole)
|
||||
&& jobRole.Value.Comp1.JobPrototype != jobPrototype)
|
||||
{
|
||||
_adminLogger.Add(LogType.Mind,
|
||||
LogImpact.Low,
|
||||
$"Job Role of {ToPrettyString(mind.OwnedEntity)} changed from '{jobRole.Value.Comp1.JobPrototype}' to '{jobPrototype}'");
|
||||
|
||||
jobRole.Value.Comp1.JobPrototype = jobPrototype;
|
||||
}
|
||||
else
|
||||
MindAddRoleDo(mindId, "MindRoleJob", mind, silent, jobPrototype);
|
||||
@@ -146,11 +146,12 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
{
|
||||
mindRoleComp.JobPrototype = jobPrototype;
|
||||
EnsureComp<JobRoleComponent>(mindRoleId);
|
||||
DebugTools.AssertNull(mindRoleComp.AntagPrototype);
|
||||
DebugTools.Assert(!mindRoleComp.Antag);
|
||||
DebugTools.Assert(!mindRoleComp.ExclusiveAntag);
|
||||
}
|
||||
|
||||
if (mindRoleComp.Antag || mindRoleComp.ExclusiveAntag)
|
||||
antagonist = true;
|
||||
|
||||
antagonist |= mindRoleComp.Antag;
|
||||
mind.MindRoles.Add(mindRoleId);
|
||||
|
||||
var mindEv = new MindRoleAddedEvent(silent);
|
||||
@@ -182,51 +183,55 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Removes all instances of a specific role from this mind.
|
||||
/// </summary>
|
||||
/// <param name="mindId">The mind to remove the role from.</param>
|
||||
/// <param name="mind">The mind to remove the role from.</param>
|
||||
/// <typeparam name="T">The type of the role to remove.</typeparam>
|
||||
/// <exception cref="ArgumentException">Thrown if the mind does not exist or does not have this role.</exception>
|
||||
/// <returns>Returns False if there was something wrong with the mind or the removal. True if successful</returns>>
|
||||
public bool MindRemoveRole<T>(EntityUid mindId) where T : IComponent
|
||||
/// <returns>Returns false if the role did not exist. True if successful</returns>>
|
||||
public bool MindRemoveRole<T>(Entity<MindComponent?> mind) where T : IComponent
|
||||
{
|
||||
if (!TryComp<MindComponent>(mindId, out var mind) )
|
||||
throw new ArgumentException($"{mindId} does not exist or does not have mind component");
|
||||
if (typeof(T) == typeof(MindRoleComponent))
|
||||
throw new InvalidOperationException();
|
||||
|
||||
if (!Resolve(mind.Owner, ref mind.Comp))
|
||||
return false;
|
||||
|
||||
var found = false;
|
||||
var antagonist = false;
|
||||
var delete = new List<EntityUid>();
|
||||
foreach (var role in mind.MindRoles)
|
||||
foreach (var role in mind.Comp.MindRoles)
|
||||
{
|
||||
if (!HasComp<T>(role))
|
||||
continue;
|
||||
|
||||
var roleComp = Comp<MindRoleComponent>(role);
|
||||
antagonist = roleComp.Antag;
|
||||
_entityManager.DeleteEntity(role);
|
||||
if (!TryComp(role, out MindRoleComponent? roleComp))
|
||||
{
|
||||
Log.Error($"Encountered mind role entity {ToPrettyString(role)} without a {nameof(MindRoleComponent)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
antagonist |= roleComp.Antag | roleComp.ExclusiveAntag;
|
||||
_entityManager.DeleteEntity(role);
|
||||
delete.Add(role);
|
||||
found = true;
|
||||
|
||||
}
|
||||
|
||||
foreach (var role in delete)
|
||||
{
|
||||
mind.MindRoles.Remove(role);
|
||||
}
|
||||
|
||||
if (!found)
|
||||
return false;
|
||||
|
||||
foreach (var role in delete)
|
||||
{
|
||||
throw new ArgumentException($"{mindId} does not have this role: {typeof(T)}");
|
||||
mind.Comp.MindRoles.Remove(role);
|
||||
}
|
||||
|
||||
var message = new RoleRemovedEvent(mindId, mind, antagonist);
|
||||
|
||||
if (mind.OwnedEntity != null)
|
||||
if (mind.Comp.OwnedEntity != null)
|
||||
{
|
||||
RaiseLocalEvent(mind.OwnedEntity.Value, message, true);
|
||||
var message = new RoleRemovedEvent(mind.Owner, mind.Comp, antagonist);
|
||||
RaiseLocalEvent(mind.Comp.OwnedEntity.Value, message, true);
|
||||
}
|
||||
|
||||
_adminLogger.Add(LogType.Mind,
|
||||
LogImpact.Low,
|
||||
$"'Role {typeof(T).Name}' removed from mind of {ToPrettyString(mind.OwnedEntity)}");
|
||||
$"All roles of type '{typeof(T).Name}' removed from mind of {ToPrettyString(mind.Comp.OwnedEntity)}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -238,16 +243,14 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
/// <returns>True if the role existed and was removed</returns>
|
||||
public bool MindTryRemoveRole<T>(EntityUid mindId) where T : IComponent
|
||||
{
|
||||
if (!MindHasRole<T>(mindId))
|
||||
{
|
||||
Log.Warning($"Failed to remove role {typeof(T)} from {mindId} : mind does not have role ");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(MindRoleComponent))
|
||||
return false;
|
||||
|
||||
return MindRemoveRole<T>(mindId);
|
||||
if (MindRemoveRole<T>(mindId))
|
||||
return true;
|
||||
|
||||
Log.Warning($"Failed to remove role {typeof(T)} from {ToPrettyString(mindId)} : mind does not have role ");
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -259,30 +262,29 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
/// <param name="role">The Mind Role entity component</param>
|
||||
/// <param name="roleT">The Mind Role's entity component for T</param>
|
||||
/// <returns>True if the role is found</returns>
|
||||
public bool MindHasRole<T>(EntityUid mindId,
|
||||
[NotNullWhen(true)] out Entity<MindRoleComponent>? role,
|
||||
[NotNullWhen(true)] out Entity<T>? roleT) where T : IComponent
|
||||
public bool MindHasRole<T>(Entity<MindComponent?> mind,
|
||||
[NotNullWhen(true)] out Entity<MindRoleComponent, T>? role) where T : IComponent
|
||||
{
|
||||
role = null;
|
||||
roleT = null;
|
||||
|
||||
if (!TryComp<MindComponent>(mindId, out var mind))
|
||||
if (!Resolve(mind.Owner, ref mind.Comp))
|
||||
return false;
|
||||
|
||||
var found = false;
|
||||
|
||||
foreach (var roleEnt in mind.MindRoles)
|
||||
foreach (var roleEnt in mind.Comp.MindRoles)
|
||||
{
|
||||
if (!HasComp<T>(roleEnt))
|
||||
if (!TryComp(roleEnt, out T? tcomp))
|
||||
continue;
|
||||
|
||||
role = (roleEnt,Comp<MindRoleComponent>(roleEnt));
|
||||
roleT = (roleEnt,Comp<T>(roleEnt));
|
||||
found = true;
|
||||
break;
|
||||
if (!TryComp(roleEnt, out MindRoleComponent? roleComp))
|
||||
{
|
||||
Log.Error($"Encountered mind role entity {ToPrettyString(roleEnt)} without a {nameof(MindRoleComponent)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
role = (roleEnt, roleComp, tcomp);
|
||||
return true;
|
||||
}
|
||||
|
||||
return found;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -317,7 +319,13 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
if (!HasComp(roleEnt, type))
|
||||
continue;
|
||||
|
||||
role = (roleEnt,Comp<MindRoleComponent>(roleEnt));
|
||||
if (!TryComp(roleEnt, out MindRoleComponent? roleComp))
|
||||
{
|
||||
Log.Error($"Encountered mind role entity {ToPrettyString(roleEnt)} without a {nameof(MindRoleComponent)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
role = (roleEnt, roleComp);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@@ -325,20 +333,6 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first mind role of a specific type on a mind entity.
|
||||
/// Outputs an entity component for the mind role's MindRoleComponent
|
||||
/// </summary>
|
||||
/// <param name="mindId">The mind entity</param>
|
||||
/// <param name="role">The Mind Role entity component</param>
|
||||
/// <typeparam name="T">The type of the role to find.</typeparam>
|
||||
/// <returns>True if the role is found</returns>
|
||||
public bool MindHasRole<T>(EntityUid mindId,
|
||||
[NotNullWhen(true)] out Entity<MindRoleComponent>? role) where T : IComponent
|
||||
{
|
||||
return MindHasRole<T>(mindId, out role, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first mind role of a specific type on a mind entity.
|
||||
/// </summary>
|
||||
@@ -347,7 +341,7 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
/// <returns>True if the role is found</returns>
|
||||
public bool MindHasRole<T>(EntityUid mindId) where T : IComponent
|
||||
{
|
||||
return MindHasRole<T>(mindId, out _, out _);
|
||||
return MindHasRole<T>(mindId, out _);
|
||||
}
|
||||
|
||||
//TODO: Delete this later
|
||||
@@ -374,28 +368,31 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Reads all Roles of a mind Entity and returns their data as RoleInfo
|
||||
/// </summary>
|
||||
/// <param name="mindId">The mind entity</param>
|
||||
/// <param name="mind">The mind entity</param>
|
||||
/// <returns>RoleInfo list</returns>
|
||||
public List<RoleInfo> MindGetAllRoleInfo(EntityUid mindId)
|
||||
public List<RoleInfo> MindGetAllRoleInfo(Entity<MindComponent?> mind)
|
||||
{
|
||||
var roleInfo = new List<RoleInfo>();
|
||||
|
||||
if (!TryComp<MindComponent>(mindId, out var mind))
|
||||
if (!Resolve(mind.Owner, ref mind.Comp))
|
||||
return roleInfo;
|
||||
|
||||
foreach (var role in mind.MindRoles)
|
||||
foreach (var role in mind.Comp.MindRoles)
|
||||
{
|
||||
var valid = false;
|
||||
var name = "game-ticker-unknown-role";
|
||||
var prototype = "";
|
||||
string? playTimeTracker = null;
|
||||
string? playTimeTracker = null;
|
||||
|
||||
var comp = Comp<MindRoleComponent>(role);
|
||||
if (comp.AntagPrototype is not null)
|
||||
if (!TryComp(role, out MindRoleComponent? comp))
|
||||
{
|
||||
prototype = comp.AntagPrototype;
|
||||
Log.Error($"Encountered mind role entity {ToPrettyString(role)} without a {nameof(MindRoleComponent)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (comp.AntagPrototype is not null)
|
||||
prototype = comp.AntagPrototype;
|
||||
|
||||
if (comp.JobPrototype is not null && comp.AntagPrototype is null)
|
||||
{
|
||||
prototype = comp.JobPrototype;
|
||||
@@ -429,7 +426,7 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
}
|
||||
|
||||
if (valid)
|
||||
roleInfo.Add(new RoleInfo(name, comp.Antag || comp.ExclusiveAntag , playTimeTracker, prototype));
|
||||
roleInfo.Add(new RoleInfo(name, comp.Antag, playTimeTracker, prototype));
|
||||
}
|
||||
return roleInfo;
|
||||
}
|
||||
@@ -442,12 +439,9 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
public bool MindIsAntagonist(EntityUid? mindId)
|
||||
{
|
||||
if (mindId is null)
|
||||
{
|
||||
Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind entity not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
return CheckAntagonistStatus(mindId.Value).Item1;
|
||||
return CheckAntagonistStatus(mindId.Value).Antag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -458,37 +452,28 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
public bool MindIsExclusiveAntagonist(EntityUid? mindId)
|
||||
{
|
||||
if (mindId is null)
|
||||
{
|
||||
Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind entity not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
return CheckAntagonistStatus(mindId.Value).Item2;
|
||||
return CheckAntagonistStatus(mindId.Value).ExclusiveAntag;
|
||||
}
|
||||
|
||||
private (bool, bool) CheckAntagonistStatus(EntityUid mindId)
|
||||
public (bool Antag, bool ExclusiveAntag) CheckAntagonistStatus(Entity<MindComponent?> mind)
|
||||
{
|
||||
if (!TryComp<MindComponent>(mindId, out var mind))
|
||||
{
|
||||
Log.Warning($"Antagonist status of mind entity {mindId} could not be determined - mind component not found");
|
||||
if (!Resolve(mind.Owner, ref mind.Comp))
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
var antagonist = false;
|
||||
var exclusiveAntag = false;
|
||||
foreach (var role in mind.MindRoles)
|
||||
foreach (var role in mind.Comp.MindRoles)
|
||||
{
|
||||
if (!TryComp<MindRoleComponent>(role, out var roleComp))
|
||||
{
|
||||
//If this ever shows up outside of an integration test, then we need to look into this further.
|
||||
Log.Warning($"Mind Role Entity {role} does not have MindRoleComponent!");
|
||||
Log.Error($"Mind Role Entity {ToPrettyString(role)} does not have a MindRoleComponent, despite being listed as a role belonging to {ToPrettyString(mind)}|");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (roleComp.Antag || exclusiveAntag)
|
||||
antagonist = true;
|
||||
if (roleComp.ExclusiveAntag)
|
||||
exclusiveAntag = true;
|
||||
antagonist |= roleComp.Antag;
|
||||
exclusiveAntag |= roleComp.ExclusiveAntag;
|
||||
}
|
||||
|
||||
return (antagonist, exclusiveAntag);
|
||||
@@ -504,6 +489,9 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
_audio.PlayGlobal(sound, mind.Session);
|
||||
}
|
||||
|
||||
// TODO ROLES Change to readonly.
|
||||
// Passing around a reference to a prototype's hashset makes me uncomfortable because it might be accidentally
|
||||
// mutated.
|
||||
public HashSet<JobRequirement>? GetJobRequirement(JobPrototype job)
|
||||
{
|
||||
if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job.ID, out var req))
|
||||
@@ -512,6 +500,7 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
return job.Requirements;
|
||||
}
|
||||
|
||||
// TODO ROLES Change to readonly.
|
||||
public HashSet<JobRequirement>? GetJobRequirement(ProtoId<JobPrototype> job)
|
||||
{
|
||||
if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job, out var req))
|
||||
@@ -520,6 +509,7 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
return _prototypes.Index(job).Requirements;
|
||||
}
|
||||
|
||||
// TODO ROLES Change to readonly.
|
||||
public HashSet<JobRequirement>? GetAntagRequirement(ProtoId<AntagPrototype> antag)
|
||||
{
|
||||
if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag, out var req))
|
||||
@@ -528,6 +518,7 @@ public abstract class SharedRoleSystem : EntitySystem
|
||||
return _prototypes.Index(antag).Requirements;
|
||||
}
|
||||
|
||||
// TODO ROLES Change to readonly.
|
||||
public HashSet<JobRequirement>? GetAntagRequirement(AntagPrototype antag)
|
||||
{
|
||||
if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag.ID, out var req))
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
//using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Silicons.Borgs.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used to override the action icon for cyborg actions.
|
||||
/// Without this component the no-action state will be used.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class BorgModuleIconComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The action icon for this module
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SpriteSpecifier.Rsi Icon = default!;
|
||||
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Shared.Silicons.Laws.Components;
|
||||
|
||||
@@ -20,4 +21,12 @@ public sealed partial class SiliconLawProviderComponent : Component
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public SiliconLawset? Lawset;
|
||||
|
||||
/// <summary>
|
||||
/// The sound that plays for the Silicon player
|
||||
/// when the particular lawboard has been inserted.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? LawUploadSound = new SoundPathSpecifier("/Audio/Misc/cryo_warning.ogg");
|
||||
|
||||
}
|
||||
|
||||
@@ -285,6 +285,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
||||
|
||||
private bool SetupEye(Entity<StationAiCoreComponent> ent)
|
||||
{
|
||||
if (_net.IsClient)
|
||||
return false;
|
||||
if (ent.Comp.RemoteEntity != null)
|
||||
return false;
|
||||
|
||||
@@ -299,8 +301,11 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
||||
|
||||
private void ClearEye(Entity<StationAiCoreComponent> ent)
|
||||
{
|
||||
if (_net.IsClient)
|
||||
return;
|
||||
QueueDel(ent.Comp.RemoteEntity);
|
||||
ent.Comp.RemoteEntity = null;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void AttachEye(Entity<StationAiCoreComponent> ent)
|
||||
@@ -330,6 +335,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
||||
if (_timing.ApplyingState)
|
||||
return;
|
||||
|
||||
SetupEye(ent);
|
||||
|
||||
// Just so text and the likes works properly
|
||||
_metadata.SetEntityName(ent.Owner, MetaData(args.Entity).EntityName);
|
||||
|
||||
@@ -351,6 +358,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
||||
{
|
||||
_eye.SetTarget(args.Entity, null, eyeComp);
|
||||
}
|
||||
ClearEye(ent);
|
||||
}
|
||||
|
||||
private void UpdateAppearance(Entity<StationAiHolderComponent?> entity)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Sound.Components;
|
||||
@@ -8,4 +9,9 @@ namespace Content.Shared.Sound.Components;
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class EmitSoundOnUIOpenComponent : BaseEmitSoundComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Blacklist for making the sound not play if certain entities open the UI
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist Blacklist = new();
|
||||
}
|
||||
|
||||
@@ -58,7 +58,10 @@ public abstract class SharedEmitSoundSystem : EntitySystem
|
||||
|
||||
private void HandleEmitSoundOnUIOpen(EntityUid uid, EmitSoundOnUIOpenComponent component, AfterActivatableUIOpenEvent args)
|
||||
{
|
||||
TryEmitSound(uid, component, args.User);
|
||||
if (_whitelistSystem.IsBlacklistFail(component.Blacklist, args.User))
|
||||
{
|
||||
TryEmitSound(uid, component, args.User);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMobState(Entity<SoundWhileAliveComponent> entity, ref MobStateChangedEvent args)
|
||||
|
||||
@@ -150,6 +150,7 @@ public abstract class SharedStationSpawningSystem : EntitySystem
|
||||
|
||||
foreach (var (slot, entProtos) in startingGear.Storage)
|
||||
{
|
||||
ents.Clear();
|
||||
if (entProtos.Count == 0)
|
||||
continue;
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ using Content.Shared.Verbs;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Tools.EntitySystems;
|
||||
using Content.Shared.Whitelist;
|
||||
using Content.Shared.Materials;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Storage.EntitySystems;
|
||||
|
||||
@@ -35,6 +37,7 @@ public sealed class SecretStashSystem : EntitySystem
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SecretStashComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<SecretStashComponent, DestructionEventArgs>(OnDestroyed);
|
||||
SubscribeLocalEvent<SecretStashComponent, GotReclaimedEvent>(OnReclaimed);
|
||||
SubscribeLocalEvent<SecretStashComponent, InteractUsingEvent>(OnInteractUsing, after: new[] { typeof(ToolOpenableSystem) });
|
||||
SubscribeLocalEvent<SecretStashComponent, InteractHandEvent>(OnInteractHand);
|
||||
SubscribeLocalEvent<SecretStashComponent, GetVerbsEvent<InteractionVerb>>(OnGetVerb);
|
||||
@@ -47,12 +50,12 @@ public sealed class SecretStashSystem : EntitySystem
|
||||
|
||||
private void OnDestroyed(Entity<SecretStashComponent> entity, ref DestructionEventArgs args)
|
||||
{
|
||||
var storedInside = _containerSystem.EmptyContainer(entity.Comp.ItemContainer);
|
||||
if (storedInside != null && storedInside.Count >= 1)
|
||||
{
|
||||
var popup = Loc.GetString("comp-secret-stash-on-destroyed-popup", ("stashname", GetStashName(entity)));
|
||||
_popupSystem.PopupEntity(popup, storedInside[0], PopupType.MediumCaution);
|
||||
}
|
||||
DropContentsAndAlert(entity);
|
||||
}
|
||||
|
||||
private void OnReclaimed(Entity<SecretStashComponent> entity, ref GotReclaimedEvent args)
|
||||
{
|
||||
DropContentsAndAlert(entity, args.ReclaimerCoordinates);
|
||||
}
|
||||
|
||||
private void OnInteractUsing(Entity<SecretStashComponent> entity, ref InteractUsingEvent args)
|
||||
@@ -211,5 +214,18 @@ public sealed class SecretStashSystem : EntitySystem
|
||||
return entity.Comp.ItemContainer.ContainedEntity != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drop the item stored in the stash and alert all nearby players with a popup.
|
||||
/// </summary>
|
||||
private void DropContentsAndAlert(Entity<SecretStashComponent> entity, EntityCoordinates? cords = null)
|
||||
{
|
||||
var storedInside = _containerSystem.EmptyContainer(entity.Comp.ItemContainer, true, cords);
|
||||
if (storedInside != null && storedInside.Count >= 1)
|
||||
{
|
||||
var popup = Loc.GetString("comp-secret-stash-on-destroyed-popup", ("stashname", GetStashName(entity)));
|
||||
_popupSystem.PopupPredicted(popup, storedInside[0], null, PopupType.MediumCaution);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -673,7 +673,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
|
||||
private void OnSaveItemLocation(StorageSaveItemLocationEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (!ValidateInput(args, msg.Storage, msg.Item, out var player, out var storage, out var item, held: true))
|
||||
if (!ValidateInput(args, msg.Storage, msg.Item, out var player, out var storage, out var item))
|
||||
return;
|
||||
|
||||
SaveItemLocation(storage!, item.Owner);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user