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

# Conflicts:
#	Resources/Prototypes/Accents/word_replacements.yml
#	Resources/Prototypes/Entities/Clothing/Shoes/boots.yml
#	Resources/Prototypes/Loadouts/loadout_groups.yml
#	Resources/Prototypes/Roles/Jobs/Security/detective.yml
#	Resources/Prototypes/Species/human.yml
#	Resources/ServerInfo/Guidebook/Antagonist/Traitors.xml
#	Resources/Textures/Clothing/Head/Helmets/security.rsi/equipped-HELMET.png
#	Resources/Textures/Clothing/Head/Helmets/security.rsi/icon.png
#	Resources/Textures/Clothing/Head/Helmets/security.rsi/inhand-left.png
#	Resources/Textures/Clothing/Head/Helmets/security.rsi/inhand-right.png
#	Resources/Textures/Structures/Wallmounts/signs.rsi/meta.json
This commit is contained in:
Morb0
2024-08-23 09:38:36 +03:00
243 changed files with 20002 additions and 8248 deletions

View File

@@ -4,14 +4,14 @@
MinSize="400 225">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="5">
<TextEdit Name="MessageInput" HorizontalExpand="True" VerticalExpand="True" Margin="0 0 0 5" MinHeight="100" />
<Button Name="AnnounceButton" Text="{Loc 'comms-console-menu-announcement-button'}" StyleClasses="OpenLeft" Access="Public" />
<Button Name="BroadcastButton" Text="{Loc 'comms-console-menu-broadcast-button'}" StyleClasses="OpenLeft" Access="Public" />
<Button Name="AnnounceButton" Text="{Loc 'comms-console-menu-announcement-button'}" ToolTip="{Loc 'comms-console-menu-announcement-button-tooltip'}" StyleClasses="OpenLeft" Access="Public" />
<Button Name="BroadcastButton" Text="{Loc 'comms-console-menu-broadcast-button'}" ToolTip="{Loc 'comms-console-menu-broadcast-button-tooltip'}" StyleClasses="OpenLeft" Access="Public" />
<OptionButton Name="AlertLevelButton" StyleClasses="OpenRight" Access="Public" />
<OptionButton Name="AlertLevelButton" ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}" StyleClasses="OpenRight" Access="Public" />
<Control MinSize="10 10" />
<RichTextLabel Name="CountdownLabel" VerticalExpand="True" />
<Button Name="EmergencyShuttleButton" Text="Placeholder Text" Access="Public" />
<Button Name="EmergencyShuttleButton" Text="Placeholder Text" ToolTip="{Loc 'comms-console-menu-emergency-shuttle-button-tooltip'}" Access="Public" />
</BoxContainer>
</controls:FancyWindow>

View File

@@ -25,9 +25,6 @@
<ProjectReference Include="..\Corvax\Content.Corvax.Interfaces.Shared\Content.Corvax.Interfaces.Shared.csproj" />
<ProjectReference Include="..\Corvax\Content.Corvax.Interfaces.Client\Content.Corvax.Interfaces.Client.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Spawners\" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\MSBuild\XamlIL.targets" />
</Project>

View File

@@ -99,8 +99,8 @@ namespace Content.Client.Ghost
if (args.Handled)
return;
Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-ghost-visibility-popup"), args.Performer);
var locId = GhostVisibility ? "ghost-gui-toggle-ghost-visibility-popup-off" : "ghost-gui-toggle-ghost-visibility-popup-on";
Popup.PopupEntity(Loc.GetString(locId), args.Performer);
if (uid == _playerManager.LocalEntity)
ToggleGhostVisibility();

View File

@@ -19,6 +19,7 @@ namespace Content.Client.Launcher
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IClipboardManager _clipboard = default!;
private LauncherConnectingGui? _control;
@@ -58,7 +59,7 @@ namespace Content.Client.Launcher
protected override void Startup()
{
_control = new LauncherConnectingGui(this, _random, _prototypeManager, _cfg);
_control = new LauncherConnectingGui(this, _random, _prototypeManager, _cfg, _clipboard);
_userInterfaceManager.StateRoot.AddChild(_control);

View File

@@ -18,21 +18,38 @@
<Control VerticalExpand="True" Margin="0 0 0 8">
<BoxContainer Orientation="Vertical" Name="ConnectingStatus">
<Label Text="{Loc 'connecting-in-progress'}" Align="Center" />
<!-- Who the fuck named these cont- oh wait I did -->
<Label Name="ConnectStatus" StyleClasses="LabelSubText" Align="Center" />
</BoxContainer>
<BoxContainer Orientation="Vertical" Name="ConnectFail" Visible="False">
<BoxContainer Orientation="Vertical" Name="ConnectFail" Visible="False" SeparationOverride="10">
<RichTextLabel Name="ConnectFailReason" VerticalAlignment="Stretch"/>
<Button Name="RetryButton" Text="{Loc 'connecting-retry'}"
HorizontalAlignment="Center"
VerticalExpand="True" VerticalAlignment="Bottom" />
<BoxContainer Orientation="Horizontal" Align="Center">
<Button Name="RetryButton"
Text="{Loc 'connecting-retry'}"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
StyleClasses="OpenRight"/>
<Button Name="CopyButton"
Text="{Loc 'connecting-copy'}"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
StyleClasses="OpenLeft"/>
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" Name="Disconnected">
<BoxContainer Orientation="Vertical" Name="Disconnected" Visible="False" SeparationOverride="10">
<Label Text="{Loc 'connecting-disconnected'}" Align="Center" />
<Label Name="DisconnectReason" Align="Center" />
<Button Name="ReconnectButton" Text="{Loc 'connecting-reconnect'}"
HorizontalAlignment="Center"
VerticalExpand="True" VerticalAlignment="Bottom" />
<BoxContainer Orientation="Horizontal" Align="Center" VerticalAlignment="Bottom">
<Button Name="ReconnectButton"
Text="{Loc 'connecting-reconnect'}"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
StyleClasses="OpenRight"/>
<Button Name="CopyButtonDisconnected"
Text="{Loc 'connecting-copy'}"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
StyleClasses="OpenLeft"/>
</BoxContainer>
</BoxContainer>
</Control>
<Label Name="ConnectingAddress" StyleClasses="LabelSubText" HorizontalAlignment="Center" />

View File

@@ -28,14 +28,16 @@ namespace Content.Client.Launcher
private readonly IRobustRandom _random;
private readonly IPrototypeManager _prototype;
private readonly IConfigurationManager _cfg;
private readonly IClipboardManager _clipboard;
public LauncherConnectingGui(LauncherConnecting state, IRobustRandom random,
IPrototypeManager prototype, IConfigurationManager config)
IPrototypeManager prototype, IConfigurationManager config, IClipboardManager clipboard)
{
_state = state;
_random = random;
_prototype = prototype;
_cfg = config;
_clipboard = clipboard;
RobustXamlLoader.Load(this);
@@ -44,8 +46,11 @@ namespace Content.Client.Launcher
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
ChangeLoginTip();
ReconnectButton.OnPressed += ReconnectButtonPressed;
RetryButton.OnPressed += ReconnectButtonPressed;
ReconnectButton.OnPressed += ReconnectButtonPressed;
CopyButton.OnPressed += CopyButtonPressed;
CopyButtonDisconnected.OnPressed += CopyButtonDisconnectedPressed;
ExitButton.OnPressed += _ => _state.Exit();
var addr = state.Address;
@@ -78,6 +83,24 @@ namespace Content.Client.Launcher
_state.RetryConnect();
}
private void CopyButtonPressed(BaseButton.ButtonEventArgs args)
{
CopyText(ConnectFailReason.Text);
}
private void CopyButtonDisconnectedPressed(BaseButton.ButtonEventArgs args)
{
CopyText(DisconnectReason.Text);
}
private void CopyText(string? text)
{
if (!string.IsNullOrEmpty(text))
{
_clipboard.SetText(text);
}
}
private void ConnectFailReasonChanged(string? reason)
{
ConnectFailReason.SetMessage(reason == null

View File

@@ -35,6 +35,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
public bool ShowIFF { get; set; } = true;
public bool ShowDocks { get; set; } = true;
public bool RotateWithEntity { get; set; } = true;
/// <summary>
/// Raised if the user left-clicks on the radar control with the relevant entitycoordinates.
@@ -109,6 +110,8 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
ActualRadarRange = Math.Clamp(ActualRadarRange, WorldMinRange, WorldMaxRange);
RotateWithEntity = state.RotateWithEntity;
_docks = state.Docks;
}
@@ -138,7 +141,8 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
var mapPos = _transform.ToMapCoordinates(_coordinates.Value);
var offset = _coordinates.Value.Position;
var posMatrix = Matrix3Helpers.CreateTransform(offset, _rotation.Value);
var (_, ourEntRot, ourEntMatrix) = _transform.GetWorldPositionRotationMatrix(_coordinates.Value.EntityId);
var ourEntRot = RotateWithEntity ? _transform.GetWorldRotation(xform) : _rotation.Value;
var ourEntMatrix = Matrix3Helpers.CreateTransform(_transform.GetWorldPosition(xform), ourEntRot);
var ourWorldMatrix = Matrix3x2.Multiply(posMatrix, ourEntMatrix);
Matrix3x2.Invert(ourWorldMatrix, out var ourWorldMatrixInvert);

View File

@@ -0,0 +1,119 @@
using System.Numerics;
using Content.Shared.Silicons.StationAi;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Silicons.StationAi;
public sealed class StationAiOverlay : Overlay
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly HashSet<Vector2i> _visibleTiles = new();
private IRenderTexture? _staticTexture;
private IRenderTexture? _stencilTexture;
public StationAiOverlay()
{
IoCManager.InjectDependencies(this);
}
protected override void Draw(in OverlayDrawArgs args)
{
if (_stencilTexture?.Texture.Size != args.Viewport.Size)
{
_staticTexture?.Dispose();
_stencilTexture?.Dispose();
_stencilTexture = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "station-ai-stencil");
_staticTexture = _clyde.CreateRenderTarget(args.Viewport.Size,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: "station-ai-static");
}
var worldHandle = args.WorldHandle;
var worldBounds = args.WorldBounds;
var playerEnt = _player.LocalEntity;
_entManager.TryGetComponent(playerEnt, out TransformComponent? playerXform);
var gridUid = playerXform?.GridUid ?? EntityUid.Invalid;
_entManager.TryGetComponent(gridUid, out MapGridComponent? grid);
var invMatrix = args.Viewport.GetWorldToLocalMatrix();
if (grid != null)
{
// TODO: Pass in attached entity's grid.
// TODO: Credit OD on the moved to code
// TODO: Call the moved-to code here.
_visibleTiles.Clear();
var lookups = _entManager.System<EntityLookupSystem>();
var xforms = _entManager.System<SharedTransformSystem>();
_entManager.System<StationAiVisionSystem>().GetView((gridUid, grid), worldBounds, _visibleTiles);
var gridMatrix = xforms.GetWorldMatrix(gridUid);
var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
// Draw visible tiles to stencil
worldHandle.RenderInRenderTarget(_stencilTexture!, () =>
{
worldHandle.SetTransform(matty);
foreach (var tile in _visibleTiles)
{
var aabb = lookups.GetLocalBounds(tile, grid.TileSize);
worldHandle.DrawRect(aabb, Color.White);
}
},
Color.Transparent);
// Once this is gucci optimise rendering.
worldHandle.RenderInRenderTarget(_staticTexture!,
() =>
{
worldHandle.SetTransform(invMatrix);
var shader = _proto.Index<ShaderPrototype>("CameraStatic").Instance();
worldHandle.UseShader(shader);
worldHandle.DrawRect(worldBounds, Color.White);
},
Color.Black);
}
// Not on a grid
else
{
worldHandle.RenderInRenderTarget(_stencilTexture!, () =>
{
},
Color.Transparent);
worldHandle.RenderInRenderTarget(_staticTexture!,
() =>
{
worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.DrawRect(worldBounds, Color.Black);
}, Color.Black);
}
// Use the lighting as a mask
worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilMask").Instance());
worldHandle.DrawTextureRect(_stencilTexture!.Texture, worldBounds);
// Draw the static
worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilDraw").Instance());
worldHandle.DrawTextureRect(_staticTexture!.Texture, worldBounds);
worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(null);
}
}

View File

@@ -0,0 +1,80 @@
using Content.Shared.Silicons.StationAi;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Player;
namespace Content.Client.Silicons.StationAi;
public sealed partial class StationAiSystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayMgr = default!;
[Dependency] private readonly IPlayerManager _player = default!;
private StationAiOverlay? _overlay;
public override void Initialize()
{
base.Initialize();
// InitializeAirlock();
// InitializePowerToggle();
SubscribeLocalEvent<StationAiOverlayComponent, LocalPlayerAttachedEvent>(OnAiAttached);
SubscribeLocalEvent<StationAiOverlayComponent, LocalPlayerDetachedEvent>(OnAiDetached);
SubscribeLocalEvent<StationAiOverlayComponent, ComponentInit>(OnAiOverlayInit);
SubscribeLocalEvent<StationAiOverlayComponent, ComponentRemove>(OnAiOverlayRemove);
}
private void OnAiOverlayInit(Entity<StationAiOverlayComponent> ent, ref ComponentInit args)
{
var attachedEnt = _player.LocalEntity;
if (attachedEnt != ent.Owner)
return;
AddOverlay();
}
private void OnAiOverlayRemove(Entity<StationAiOverlayComponent> ent, ref ComponentRemove args)
{
var attachedEnt = _player.LocalEntity;
if (attachedEnt != ent.Owner)
return;
RemoveOverlay();
}
private void AddOverlay()
{
if (_overlay != null)
return;
_overlay = new StationAiOverlay();
_overlayMgr.AddOverlay(_overlay);
}
private void RemoveOverlay()
{
if (_overlay == null)
return;
_overlayMgr.RemoveOverlay(_overlay);
_overlay = null;
}
private void OnAiAttached(Entity<StationAiOverlayComponent> ent, ref LocalPlayerAttachedEvent args)
{
AddOverlay();
}
private void OnAiDetached(Entity<StationAiOverlayComponent> ent, ref LocalPlayerDetachedEvent args)
{
RemoveOverlay();
}
public override void Shutdown()
{
base.Shutdown();
_overlayMgr.RemoveOverlay<StationAiOverlay>();
}
}

View File

@@ -21,6 +21,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Utility;
using static Content.Client.Inventory.ClientInventorySystem;
@@ -399,6 +400,9 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
foreach (var slotData in clientInv.SlotData.Values)
{
AddSlot(slotData);
if (_inventoryButton != null)
_inventoryButton.Visible = true;
}
UpdateInventoryHotbar(_playerInventory);
@@ -406,6 +410,9 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
private void UnloadSlots()
{
if (_inventoryButton != null)
_inventoryButton.Visible = false;
_playerUid = null;
_playerInventory = null;
foreach (var slotGroup in _slotGroups.Values)

View File

@@ -9,9 +9,11 @@
Orientation="Horizontal"
HorizontalAlignment="Center">
<Control HorizontalAlignment="Center">
<!-- Needs to default to invisible because if we attach to a non-slots entity this will never get unset -->
<controls:SlotButton
Name="InventoryButton"
Access="Public"
Visible="False"
VerticalAlignment="Bottom"
HorizontalExpand="False"
VerticalExpand="False"

View File

@@ -7,14 +7,17 @@ using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.DecalPlacer;
using Content.Client.UserInterface.Systems.Sandbox.Windows;
using Content.Shared.Input;
using Content.Shared.Silicons.StationAi;
using JetBrains.Annotations;
using Robust.Client.Console;
using Robust.Client.Debugging;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controllers.Implementations;
using Robust.Shared.Console;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Player;
@@ -27,10 +30,12 @@ namespace Content.Client.UserInterface.Systems.Sandbox;
[UsedImplicitly]
public sealed class SandboxUIController : UIController, IOnStateChanged<GameplayState>, IOnSystemChanged<SandboxSystem>
{
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly IEyeManager _eye = default!;
[Dependency] private readonly IInputManager _input = default!;
[Dependency] private readonly ILightManager _light = default!;
[Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[UISystemDependency] private readonly DebugPhysicsSystem _debugPhysics = default!;
[UISystemDependency] private readonly MarkerSystem _marker = default!;
@@ -116,6 +121,21 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
_window.ShowMarkersButton.Pressed = _marker.MarkersVisible;
_window.ShowBbButton.Pressed = (_debugPhysics.Flags & PhysicsDebugFlags.Shapes) != 0x0;
_window.AiOverlayButton.OnPressed += args =>
{
var player = _player.LocalEntity;
if (player == null)
return;
var pnent = EntityManager.GetNetEntity(player.Value);
// Need NetworkedAddComponent but engine PR.
if (args.Button.Pressed)
_console.ExecuteCommand($"addcomp {pnent.Id} StationAiOverlay");
else
_console.ExecuteCommand($"rmcomp {pnent.Id} StationAiOverlay");
};
_window.RespawnButton.OnPressed += _ => _sandbox.Respawn();
_window.SpawnTilesButton.OnPressed += _ => TileSpawningController.ToggleWindow();
_window.SpawnEntitiesButton.OnPressed += _ => EntitySpawningController.ToggleWindow();

View File

@@ -4,6 +4,7 @@
Title="{Loc sandbox-window-title}"
Resizable="False">
<BoxContainer Orientation="Vertical" SeparationOverride="4">
<Button Name="AiOverlayButton" Access="Public" Text="{Loc sandbox-window-ai-overlay-button}" ToggleMode="True"/>
<Button Name="RespawnButton" Access="Public" Text="{Loc sandbox-window-respawn-button}"/>
<Button Name="SpawnEntitiesButton" Access="Public" Text="{Loc sandbox-window-spawn-entities-button}"/>
<Button Name="SpawnTilesButton" Access="Public" Text="{Loc sandbox-window-spawn-tiles-button}"/>

View File

@@ -0,0 +1,16 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Horizontal"
HorizontalExpand="True"
SeparationOverride="4">
<EntityPrototypeView
Name="ItemPrototype"
Margin="4 4"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MinSize="32 32"
/>
<Label Name="NameLabel"
SizeFlagsStretchRatio="3"
HorizontalExpand="True"
ClipText="True"/>
</BoxContainer>

View File

@@ -0,0 +1,19 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.VendingMachines.UI;
[GenerateTypedNameReferences]
public sealed partial class VendingMachineItem : BoxContainer
{
public VendingMachineItem(EntProtoId entProto, string text)
{
RobustXamlLoader.Load(this);
ItemPrototype.SetPrototype(entProto);
NameLabel.Text = text;
}
}

View File

@@ -2,17 +2,10 @@
xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:style="clr-namespace:Content.Client.Stylesheets">
<BoxContainer Orientation="Vertical">
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'vending-machine-component-search-filter'}" HorizontalExpand="True" Margin ="4 4" Access="Public"/>
<ItemList Name="VendingContents"
SizeFlagsStretchRatio="8"
VerticalExpand="True"
ItemSeparation="2"
Margin="4 0"
SelectMode="Button"
StyleClasses="transparentBackgroundItemList">
</ItemList>
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls">
<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 0"/>
<!-- Footer -->
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />

View File

@@ -1,14 +1,13 @@
using System.Numerics;
using Content.Shared.VendingMachines;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
using Content.Shared.IdentityManagement;
using Robust.Shared.Timing;
using Robust.Client.UserInterface;
using Content.Client.UserInterface.Controls;
using Robust.Client.Graphics;
namespace Content.Client.VendingMachines.UI
{
@@ -16,12 +15,10 @@ namespace Content.Client.VendingMachines.UI
public sealed partial class VendingMachineMenu : FancyWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly Dictionary<EntProtoId, EntityUid> _dummies = [];
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
public event Action<ItemList.ItemListSelectedEventArgs>? OnItemSelected;
public event Action<string>? OnSearchChanged;
private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
public VendingMachineMenu()
{
@@ -30,106 +27,90 @@ namespace Content.Client.VendingMachines.UI
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
SearchBar.OnTextChanged += _ =>
{
OnSearchChanged?.Invoke(SearchBar.Text);
};
VendingContents.OnItemSelected += args =>
{
OnItemSelected?.Invoke(args);
};
VendingContents.SearchBar = SearchBar;
VendingContents.DataFilterCondition += DataFilterCondition;
VendingContents.GenerateItem += GenerateButton;
VendingContents.ItemKeyBindDown += (args, data) => OnItemSelected?.Invoke(args, data);
}
protected override void Dispose(bool disposing)
private bool DataFilterCondition(string filter, ListData data)
{
base.Dispose(disposing);
if (data is not VendorItemsListData { ItemText: var text })
return false;
// Don't clean up dummies during disposal or we'll just have to spawn them again
if (!disposing)
if (string.IsNullOrEmpty(filter))
return true;
return text.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
}
private void GenerateButton(ListData data, ListContainerButton button)
{
if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
return;
// Delete any dummy items we spawned
foreach (var entity in _dummies.Values)
{
_entityManager.QueueDeleteEntity(entity);
}
_dummies.Clear();
button.AddChild(new VendingMachineItem(protoID, text));
button.ToolTip = text;
button.StyleBoxOverride = _styleBox;
}
/// <summary>
/// Populates the list of available items on the vending machine interface
/// and sets icons based on their prototypes
/// </summary>
public void Populate(List<VendingMachineInventoryEntry> inventory, out List<int> filteredInventory, string? filter = null)
public void Populate(List<VendingMachineInventoryEntry> inventory)
{
filteredInventory = new();
if (inventory.Count == 0)
if (inventory.Count == 0 && VendingContents.Visible)
{
VendingContents.Clear();
var outOfStockText = Loc.GetString("vending-machine-component-try-eject-out-of-stock");
VendingContents.AddItem(outOfStockText);
SetSizeAfterUpdate(outOfStockText.Length, VendingContents.Count);
SearchBar.Visible = false;
VendingContents.Visible = false;
var outOfStockLabel = new Label()
{
Text = Loc.GetString("vending-machine-component-try-eject-out-of-stock"),
Margin = new Thickness(4, 4),
HorizontalExpand = true,
VerticalAlignment = VAlignment.Stretch,
HorizontalAlignment = HAlignment.Center
};
MainContainer.AddChild(outOfStockLabel);
SetSizeAfterUpdate(outOfStockLabel.Text.Length, 0);
return;
}
while (inventory.Count != VendingContents.Count)
{
if (inventory.Count > VendingContents.Count)
VendingContents.AddItem(string.Empty);
else
VendingContents.RemoveAt(VendingContents.Count - 1);
}
var longestEntry = string.Empty;
var spriteSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
var listData = new List<VendorItemsListData>();
var filterCount = 0;
for (var i = 0; i < inventory.Count; i++)
{
var entry = inventory[i];
var vendingItem = VendingContents[i - filterCount];
vendingItem.Text = string.Empty;
vendingItem.Icon = null;
if (!_dummies.TryGetValue(entry.ID, out var dummy))
{
dummy = _entityManager.Spawn(entry.ID);
_dummies.Add(entry.ID, dummy);
}
var itemName = Identity.Name(dummy, _entityManager);
Texture? icon = null;
if (_prototypeManager.TryIndex<EntityPrototype>(entry.ID, out var prototype))
{
icon = spriteSystem.GetPrototypeIcon(prototype).Default;
}
// search filter
if (!string.IsNullOrEmpty(filter) &&
!itemName.ToLowerInvariant().Contains(filter.Trim().ToLowerInvariant()))
{
VendingContents.Remove(vendingItem);
filterCount++;
if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
continue;
}
if (itemName.Length > longestEntry.Length)
longestEntry = itemName;
var itemText = $"{prototype.Name} [{entry.Amount}]";
vendingItem.Text = $"{itemName} [{entry.Amount}]";
vendingItem.Icon = icon;
filteredInventory.Add(i);
if (itemText.Length > longestEntry.Length)
longestEntry = itemText;
listData.Add(new VendorItemsListData(prototype.ID, itemText, i));
}
VendingContents.PopulateList(listData);
SetSizeAfterUpdate(longestEntry.Length, inventory.Count);
}
private void SetSizeAfterUpdate(int longestEntryLength, int contentCount)
{
SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 300),
SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400),
Math.Clamp(contentCount * 50, 150, 350));
}
}
}
public record VendorItemsListData(EntProtoId ItemProtoID, string ItemText, int ItemIndex) : ListData;

View File

@@ -1,6 +1,9 @@
using Content.Client.UserInterface.Controls;
using Content.Client.VendingMachines.UI;
using Content.Shared.VendingMachines;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using System.Linq;
using Robust.Client.UserInterface;
@@ -14,9 +17,6 @@ namespace Content.Client.VendingMachines
[ViewVariables]
private List<VendingMachineInventoryEntry> _cachedInventory = new();
[ViewVariables]
private List<int> _cachedFilteredIndex = new();
public VendingMachineBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
@@ -34,9 +34,10 @@ namespace Content.Client.VendingMachines
_menu.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
_menu.OnItemSelected += OnItemSelected;
_menu.OnSearchChanged += OnSearchChanged;
_menu.Populate(_cachedInventory, out _cachedFilteredIndex);
_menu.Populate(_cachedInventory);
_menu.OpenCenteredLeft();
}
protected override void UpdateState(BoundUserInterfaceState state)
@@ -48,15 +49,21 @@ namespace Content.Client.VendingMachines
_cachedInventory = newState.Inventory;
_menu?.Populate(_cachedInventory, out _cachedFilteredIndex, _menu.SearchBar.Text);
_menu?.Populate(_cachedInventory);
}
private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
{
if (args.Function != EngineKeyFunctions.UIClick)
return;
if (data is not VendorItemsListData { ItemIndex: var itemIndex })
return;
if (_cachedInventory.Count == 0)
return;
var selectedItem = _cachedInventory.ElementAtOrDefault(_cachedFilteredIndex.ElementAtOrDefault(args.ItemIndex));
var selectedItem = _cachedInventory.ElementAtOrDefault(itemIndex);
if (selectedItem == null)
return;
@@ -77,10 +84,5 @@ namespace Content.Client.VendingMachines
_menu.OnClose -= Close;
_menu.Dispose();
}
private void OnSearchChanged(string? filter)
{
_menu?.Populate(_cachedInventory, out _cachedFilteredIndex, filter);
}
}
}

View File

@@ -1,8 +1,8 @@
<Control xmlns="https://spacestation14.io" MinWidth="300">
<Control xmlns="https://spacestation14.io" MinWidth="300" MaxWidth="500">
<PanelContainer StyleClasses="AngleRect" />
<BoxContainer Margin="4" Orientation="Vertical">
<Label Name="VoteCaller" />
<Label Name="VoteTitle" />
<RichTextLabel Name="VoteTitle" />
<GridContainer Columns="3" Name="VoteOptionsContainer" />
<BoxContainer Orientation="Horizontal">

View File

@@ -8,6 +8,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Voting.UI
{
@@ -48,7 +49,7 @@ namespace Content.Client.Voting.UI
public void UpdateData()
{
VoteTitle.Text = _vote.Title;
VoteTitle.SetMessage(FormattedMessage.FromUnformatted(_vote.Title));
VoteCaller.Text = Loc.GetString("ui-vote-created", ("initiator", _vote.Initiator));
for (var i = 0; i < _voteButtons.Length; i++)

View File

@@ -22,6 +22,7 @@ public static class ServerPackaging
new PlatformReg("win-x86", "Windows", false),
new PlatformReg("linux-x86", "Linux", false),
new PlatformReg("linux-arm", "Linux", false),
new PlatformReg("freebsd-x64", "FreeBSD", false),
};
private static List<string> PlatformRids => Platforms

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class ban_notify_trigger : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
create or replace function send_server_ban_notification()
returns trigger as $$
declare
x_server_id integer;
begin
select round.server_id into x_server_id from round where round.round_id = NEW.round_id;
perform pg_notify('ban_notification', json_build_object('ban_id', NEW.server_ban_id, 'server_id', x_server_id)::text);
return NEW;
end;
$$ LANGUAGE plpgsql;
""");
migrationBuilder.Sql("""
create or replace trigger notify_on_server_ban_insert
after insert on server_ban
for each row
execute function send_server_ban_notification();
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
drop trigger notify_on_server_ban_insert on server_ban;
drop function send_server_ban_notification;
""");
}
}
}

View File

@@ -706,6 +706,11 @@ namespace Content.Server.Database
/// Intended for use with residential IP ranges that are often used maliciously.
/// </remarks>
BlacklistedRange = 1 << 2,
/// <summary>
/// Represents having all possible exemption flags.
/// </summary>
All = int.MaxValue,
// @formatter:on
}
@@ -904,7 +909,7 @@ namespace Content.Server.Database
Panic = 3,
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*
*
* If baby jail is removed, please reserve this value for as long as can reasonably be done to prevent causing ambiguity in connection denial reasons.
* Reservation by commenting out the value is likely sufficient for this purpose, but may impact projects which depend on SS14 like SS14.Admin.
*/

View File

@@ -0,0 +1,123 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Content.Server.Database;
namespace Content.Server.Administration.Managers;
public sealed partial class BanManager
{
// Responsible for ban notification handling.
// Ban notifications are sent through the database to notify the entire server group that a new ban has been added,
// so that people will get kicked if they are banned on a different server than the one that placed the ban.
//
// Ban notifications are currently sent by a trigger in the database, automatically.
/// <summary>
/// The notification channel used to broadcast information about new bans.
/// </summary>
public const string BanNotificationChannel = "ban_notification";
// Rate limit to avoid undue load from mass-ban imports.
// Only process 10 bans per 30 second interval.
//
// I had the idea of maybe binning this by postgres transaction ID,
// to avoid any possibility of dropping a normal ban by coincidence.
// Didn't bother implementing this though.
private static readonly TimeSpan BanNotificationRateLimitTime = TimeSpan.FromSeconds(30);
private const int BanNotificationRateLimitCount = 10;
private readonly object _banNotificationRateLimitStateLock = new();
private TimeSpan _banNotificationRateLimitStart;
private int _banNotificationRateLimitCount;
private void OnDatabaseNotification(DatabaseNotification notification)
{
if (notification.Channel != BanNotificationChannel)
return;
if (notification.Payload == null)
{
_sawmill.Error("Got ban notification with null payload!");
return;
}
BanNotificationData data;
try
{
data = JsonSerializer.Deserialize<BanNotificationData>(notification.Payload)
?? throw new JsonException("Content is null");
}
catch (JsonException e)
{
_sawmill.Error($"Got invalid JSON in ban notification: {e}");
return;
}
if (!CheckBanRateLimit())
{
_sawmill.Verbose("Not processing ban notification due to rate limit");
return;
}
_taskManager.RunOnMainThread(() => ProcessBanNotification(data));
}
private async void ProcessBanNotification(BanNotificationData data)
{
if ((await _entryManager.ServerEntity).Id == data.ServerId)
{
_sawmill.Verbose("Not processing ban notification: came from this server");
return;
}
_sawmill.Verbose($"Processing ban notification for ban {data.BanId}");
var ban = await _db.GetServerBanAsync(data.BanId);
if (ban == null)
{
_sawmill.Warning($"Ban in notification ({data.BanId}) didn't exist?");
return;
}
KickMatchingConnectedPlayers(ban, "ban notification");
}
private bool CheckBanRateLimit()
{
lock (_banNotificationRateLimitStateLock)
{
var now = _gameTiming.RealTime;
if (_banNotificationRateLimitStart + BanNotificationRateLimitTime < now)
{
// Rate limit period expired, restart it.
_banNotificationRateLimitCount = 1;
_banNotificationRateLimitStart = now;
return true;
}
_banNotificationRateLimitCount += 1;
return _banNotificationRateLimitCount <= BanNotificationRateLimitCount;
}
}
/// <summary>
/// Data sent along the notification channel for a single ban notification.
/// </summary>
private sealed class BanNotificationData
{
/// <summary>
/// The ID of the new ban object in the database to check.
/// </summary>
[JsonRequired, JsonPropertyName("ban_id")]
public int BanId { get; init; }
/// <summary>
/// The id of the server the ban was made on.
/// This is used to avoid double work checking the ban on the originating server.
/// </summary>
/// <remarks>
/// This is optional in case the ban was made outside a server (SS14.Admin)
/// </remarks>
[JsonPropertyName("server_id")]
public int? ServerId { get; init; }
}
}

View File

@@ -2,6 +2,7 @@ using System.Collections.Immutable;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Chat.Managers;
using Content.Server.Database;
@@ -12,16 +13,18 @@ using Content.Shared.Players;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Asynchronous;
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;
public sealed class BanManager : IBanManager, IPostInjectInit
public sealed partial class BanManager : IBanManager, IPostInjectInit
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -29,9 +32,13 @@ public sealed class BanManager : IBanManager, IPostInjectInit
[Dependency] private readonly IEntitySystemManager _systems = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILocalizationManager _localizationManager = default!;
[Dependency] private readonly ServerDbEntryManager _entryManager = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly UserDbDataManager _userDbData = default!;
private ISawmill _sawmill = default!;
@@ -39,12 +46,34 @@ public sealed class BanManager : IBanManager, IPostInjectInit
public const string JobPrefix = "Job:";
private readonly Dictionary<NetUserId, HashSet<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);
_userDbData.AddOnLoadPlayer(CachePlayerData);
_userDbData.AddOnPlayerDisconnect(ClearPlayerData);
}
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);
cancel.ThrowIfCancellationRequested();
_cachedBanExemptions[player] = flags;
}
private void ClearPlayerData(ICommonSession player)
{
_cachedBanExemptions.Remove(player);
}
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
@@ -168,17 +197,43 @@ public sealed class BanManager : IBanManager, IPostInjectInit
_sawmill.Info(logMessage);
_chat.SendAdminAlert(logMessage);
// If we're not banning a player we don't care about disconnecting people
if (target == null)
return;
// Is the player connected?
if (!_playerManager.TryGetSessionById(target.Value, out var targetPlayer))
return;
// If they are, kick them
var message = banDef.FormatBanMessage(_cfg, _localizationManager);
targetPlayer.Channel.Disconnect(message);
KickMatchingConnectedPlayers(banDef, "newly placed ban");
}
private void KickMatchingConnectedPlayers(ServerBanDef def, string source)
{
foreach (var player in _playerManager.Sessions)
{
if (BanMatchesPlayer(player, def))
{
KickForBanDef(player, def);
_sawmill.Info($"Kicked player {player.Name} ({player.UserId}) through {source}");
}
}
}
private bool BanMatchesPlayer(ICommonSession player, ServerBanDef ban)
{
var playerInfo = new BanMatcher.PlayerInfo
{
UserId = player.UserId,
Address = player.Channel.RemoteEndPoint.Address,
HWId = player.Channel.UserData.HWId,
// It's possible for the player to not have cached data loading yet due to coincidental timing.
// If this is the case, we assume they have all flags to avoid false-positives.
ExemptFlags = _cachedBanExemptions.GetValueOrDefault(player, ServerBanExemptFlags.All),
IsNewPlayer = false,
};
return BanMatcher.BanMatches(ban, playerInfo);
}
private void KickForBanDef(ICommonSession player, ServerBanDef def)
{
var message = def.FormatBanMessage(_cfg, _localizationManager);
player.Channel.Disconnect(message);
}
#endregion
#region Job Bans

View File

@@ -74,7 +74,7 @@ public sealed partial class AtmosphereSystem
newGridAtmos = AddComp<GridAtmosphereComponent>(newGrid);
// We assume the tiles on the new grid have the same coordinates as they did on the old grid...
var enumerator = mapGrid.GetAllTilesEnumerator();
var enumerator = _mapSystem.GetAllTilesEnumerator(newGrid, mapGrid);
while (enumerator.MoveNext(out var tile))
{
@@ -176,7 +176,7 @@ public sealed partial class AtmosphereSystem
tile.AdjacentBits = AtmosDirection.Invalid;
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
var direction = (AtmosDirection)(1 << i);
var adjacentIndices = tile.GridIndices.Offset(direction);
TileAtmosphere? adjacent;
@@ -196,7 +196,7 @@ public sealed partial class AtmosphereSystem
AddActiveTile(atmos, adjacent);
var oppositeIndex = i.ToOppositeIndex();
var oppositeDirection = (AtmosDirection) (1 << oppositeIndex);
var oppositeDirection = (AtmosDirection)(1 << oppositeIndex);
if (adjBlockDirs.IsFlagSet(oppositeDirection) || blockedDirs.IsFlagSet(direction))
{
@@ -269,7 +269,7 @@ public sealed partial class AtmosphereSystem
private void GridFixTileVacuum(TileAtmosphere tile)
{
DebugTools.AssertNotNull(tile.Air);
DebugTools.Assert(tile.Air?.Immutable == false );
DebugTools.Assert(tile.Air?.Immutable == false);
Array.Clear(tile.MolesArchived);
tile.ArchivedCycle = 0;

View File

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

View File

@@ -6,103 +6,102 @@ using Content.Shared.Tag;
using Robust.Shared.Console;
using Robust.Shared.Map.Components;
namespace Content.Server.Construction.Commands
namespace Content.Server.Construction.Commands;
[AdminCommand(AdminFlags.Mapping)]
public sealed class FixRotationsCommand : IConsoleCommand
{
[AdminCommand(AdminFlags.Mapping)]
public sealed class FixRotationsCommand : IConsoleCommand
[Dependency] private readonly IEntityManager _entManager = default!;
// ReSharper disable once StringLiteralTypo
public string Command => "fixrotations";
public string Description => "Sets the rotation of all occluders, low walls and windows to south.";
public string Help => $"Usage: {Command} <gridId> | {Command}";
public void Execute(IConsoleShell shell, string argsOther, string[] args)
{
[Dependency] private readonly IEntityManager _entManager = default!;
var player = shell.Player;
EntityUid? gridId;
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
// ReSharper disable once StringLiteralTypo
public string Command => "fixrotations";
public string Description => "Sets the rotation of all occluders, low walls and windows to south.";
public string Help => $"Usage: {Command} <gridId> | {Command}";
public void Execute(IConsoleShell shell, string argsOther, string[] args)
switch (args.Length)
{
var player = shell.Player;
EntityUid? gridId;
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
switch (args.Length)
{
case 0:
if (player?.AttachedEntity is not {Valid: true} playerEntity)
{
shell.WriteError("Only a player can run this command.");
return;
}
gridId = xformQuery.GetComponent(playerEntity).GridUid;
break;
case 1:
if (!NetEntity.TryParse(args[0], out var idNet) || !_entManager.TryGetEntity(idNet, out var id))
{
shell.WriteError($"{args[0]} is not a valid entity.");
return;
}
gridId = id;
break;
default:
shell.WriteLine(Help);
case 0:
if (player?.AttachedEntity is not { Valid: true } playerEntity)
{
shell.WriteError("Only a player can run this command.");
return;
}
}
if (!_entManager.TryGetComponent(gridId, out MapGridComponent? grid))
{
shell.WriteError($"No grid exists with id {gridId}");
gridId = xformQuery.GetComponent(playerEntity).GridUid;
break;
case 1:
if (!NetEntity.TryParse(args[0], out var idNet) || !_entManager.TryGetEntity(idNet, out var id))
{
shell.WriteError($"{args[0]} is not a valid entity.");
return;
}
gridId = id;
break;
default:
shell.WriteLine(Help);
return;
}
if (!_entManager.EntityExists(gridId))
{
shell.WriteError($"Grid {gridId} doesn't have an associated grid entity.");
return;
}
var changed = 0;
var tagSystem = _entManager.EntitySysManager.GetEntitySystem<TagSystem>();
var enumerator = xformQuery.GetComponent(gridId.Value).ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
if (!_entManager.EntityExists(child))
{
continue;
}
var valid = false;
// Occluders should only count if the state of it right now is enabled.
// This prevents issues with edge firelocks.
if (_entManager.TryGetComponent<OccluderComponent>(child, out var occluder))
{
valid |= occluder.Enabled;
}
// low walls & grilles
valid |= _entManager.HasComponent<SharedCanBuildWindowOnTopComponent>(child);
// cables
valid |= _entManager.HasComponent<CableComponent>(child);
// anything else that might need this forced
valid |= tagSystem.HasTag(child, "ForceFixRotations");
// override
valid &= !tagSystem.HasTag(child, "ForceNoFixRotations");
if (!valid)
continue;
var childXform = xformQuery.GetComponent(child);
if (childXform.LocalRotation != Angle.Zero)
{
childXform.LocalRotation = Angle.Zero;
changed++;
}
}
shell.WriteLine($"Changed {changed} entities. If things seem wrong, reconnect.");
}
if (!_entManager.TryGetComponent(gridId, out MapGridComponent? grid))
{
shell.WriteError($"No grid exists with id {gridId}");
return;
}
if (!_entManager.EntityExists(gridId))
{
shell.WriteError($"Grid {gridId} doesn't have an associated grid entity.");
return;
}
var changed = 0;
var tagSystem = _entManager.EntitySysManager.GetEntitySystem<TagSystem>();
var enumerator = xformQuery.GetComponent(gridId.Value).ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
if (!_entManager.EntityExists(child))
{
continue;
}
var valid = false;
// Occluders should only count if the state of it right now is enabled.
// This prevents issues with edge firelocks.
if (_entManager.TryGetComponent<OccluderComponent>(child, out var occluder))
{
valid |= occluder.Enabled;
}
// low walls & grilles
valid |= _entManager.HasComponent<SharedCanBuildWindowOnTopComponent>(child);
// cables
valid |= _entManager.HasComponent<CableComponent>(child);
// anything else that might need this forced
valid |= tagSystem.HasTag(child, "ForceFixRotations");
// override
valid &= !tagSystem.HasTag(child, "ForceNoFixRotations");
if (!valid)
continue;
var childXform = xformQuery.GetComponent(child);
if (childXform.LocalRotation != Angle.Zero)
{
childXform.LocalRotation = Angle.Zero;
changed++;
}
}
shell.WriteLine($"Changed {changed} entities. If things seem wrong, reconnect.");
}
}

View File

@@ -7,7 +7,7 @@ using Robust.Shared.Map.Components;
namespace Content.Server.Construction.Commands;
[AdminCommand(AdminFlags.Mapping)]
sealed class TileReplaceCommand : IConsoleCommand
public sealed class TileReplaceCommand : IConsoleCommand
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly ITileDefinitionManager _tileDef = default!;
@@ -27,9 +27,9 @@ sealed class TileReplaceCommand : IConsoleCommand
switch (args.Length)
{
case 2:
if (player?.AttachedEntity is not {Valid: true} playerEntity)
if (player?.AttachedEntity is not { Valid: true } playerEntity)
{
shell.WriteLine("Only a player can run this command without a grid ID.");
shell.WriteError("Only a player can run this command without a grid ID.");
return;
}
@@ -41,7 +41,7 @@ sealed class TileReplaceCommand : IConsoleCommand
if (!NetEntity.TryParse(args[0], out var idNet) ||
!_entManager.TryGetEntity(idNet, out var id))
{
shell.WriteLine($"{args[0]} is not a valid entity.");
shell.WriteError($"{args[0]} is not a valid entity.");
return;
}
@@ -59,23 +59,25 @@ sealed class TileReplaceCommand : IConsoleCommand
if (!_entManager.TryGetComponent(gridId, out MapGridComponent? grid))
{
shell.WriteLine($"No grid exists with id {gridId}");
shell.WriteError($"No grid exists with id {gridId}");
return;
}
if (!_entManager.EntityExists(gridId))
{
shell.WriteLine($"Grid {gridId} doesn't have an associated grid entity.");
shell.WriteError($"Grid {gridId} doesn't have an associated grid entity.");
return;
}
var mapSystem = _entManager.System<SharedMapSystem>();
var changed = 0;
foreach (var tile in grid.GetAllTiles())
foreach (var tile in mapSystem.GetAllTiles(gridId.Value, grid))
{
var tileContent = tile.Tile;
if (tileContent.TypeId == tileA.TileId)
{
grid.SetTile(tile.GridIndices, new Tile(tileB.TileId));
mapSystem.SetTile(gridId.Value, grid, tile.GridIndices, new Tile(tileB.TileId));
changed++;
}
}

View File

@@ -7,105 +7,104 @@ using Robust.Shared.Map;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
namespace Content.Server.Construction.Commands
namespace Content.Server.Construction.Commands;
[AdminCommand(AdminFlags.Mapping)]
public sealed class TileWallsCommand : IConsoleCommand
{
[AdminCommand(AdminFlags.Mapping)]
sealed class TileWallsCommand : IConsoleCommand
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
// ReSharper disable once StringLiteralTypo
public string Command => "tilewalls";
public string Description => "Puts an underplating tile below every wall on a grid.";
public string Help => $"Usage: {Command} <gridId> | {Command}";
[ValidatePrototypeId<ContentTileDefinition>]
public const string TilePrototypeId = "Plating";
[ValidatePrototypeId<TagPrototype>]
public const string WallTag = "Wall";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
var player = shell.Player;
EntityUid? gridId;
// ReSharper disable once StringLiteralTypo
public string Command => "tilewalls";
public string Description => "Puts an underplating tile below every wall on a grid.";
public string Help => $"Usage: {Command} <gridId> | {Command}";
[ValidatePrototypeId<ContentTileDefinition>]
public const string TilePrototypeId = "Plating";
[ValidatePrototypeId<TagPrototype>]
public const string WallTag = "Wall";
public void Execute(IConsoleShell shell, string argStr, string[] args)
switch (args.Length)
{
var player = shell.Player;
EntityUid? gridId;
switch (args.Length)
{
case 0:
if (player?.AttachedEntity is not {Valid: true} playerEntity)
{
shell.WriteLine("Only a player can run this command.");
return;
}
gridId = _entManager.GetComponent<TransformComponent>(playerEntity).GridUid;
break;
case 1:
if (!NetEntity.TryParse(args[0], out var idNet) || !_entManager.TryGetEntity(idNet, out var id))
{
shell.WriteLine($"{args[0]} is not a valid entity.");
return;
}
gridId = id;
break;
default:
shell.WriteLine(Help);
case 0:
if (player?.AttachedEntity is not { Valid: true } playerEntity)
{
shell.WriteError("Only a player can run this command.");
return;
}
}
if (!_entManager.TryGetComponent(gridId, out MapGridComponent? grid))
{
shell.WriteLine($"No grid exists with id {gridId}");
gridId = _entManager.GetComponent<TransformComponent>(playerEntity).GridUid;
break;
case 1:
if (!NetEntity.TryParse(args[0], out var idNet) || !_entManager.TryGetEntity(idNet, out var id))
{
shell.WriteError($"{args[0]} is not a valid entity.");
return;
}
gridId = id;
break;
default:
shell.WriteLine(Help);
return;
}
if (!_entManager.EntityExists(gridId))
{
shell.WriteLine($"Grid {gridId} doesn't have an associated grid entity.");
return;
}
var tagSystem = _entManager.EntitySysManager.GetEntitySystem<TagSystem>();
var underplating = _tileDefManager[TilePrototypeId];
var underplatingTile = new Tile(underplating.TileId);
var changed = 0;
var enumerator = _entManager.GetComponent<TransformComponent>(gridId.Value).ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
if (!_entManager.EntityExists(child))
{
continue;
}
if (!tagSystem.HasTag(child, WallTag))
{
continue;
}
var childTransform = _entManager.GetComponent<TransformComponent>(child);
if (!childTransform.Anchored)
{
continue;
}
var mapSystem = _entManager.System<MapSystem>();
var tile = mapSystem.GetTileRef(gridId.Value, grid, childTransform.Coordinates);
var tileDef = (ContentTileDefinition) _tileDefManager[tile.Tile.TypeId];
if (tileDef.ID == TilePrototypeId)
{
continue;
}
grid.SetTile(childTransform.Coordinates, underplatingTile);
changed++;
}
shell.WriteLine($"Changed {changed} tiles.");
}
if (!_entManager.TryGetComponent(gridId, out MapGridComponent? grid))
{
shell.WriteError($"No grid exists with id {gridId}");
return;
}
if (!_entManager.EntityExists(gridId))
{
shell.WriteError($"Grid {gridId} doesn't have an associated grid entity.");
return;
}
var tagSystem = _entManager.EntitySysManager.GetEntitySystem<TagSystem>();
var underplating = _tileDefManager[TilePrototypeId];
var underplatingTile = new Tile(underplating.TileId);
var changed = 0;
var enumerator = _entManager.GetComponent<TransformComponent>(gridId.Value).ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
if (!_entManager.EntityExists(child))
{
continue;
}
if (!tagSystem.HasTag(child, WallTag))
{
continue;
}
var childTransform = _entManager.GetComponent<TransformComponent>(child);
if (!childTransform.Anchored)
{
continue;
}
var mapSystem = _entManager.System<MapSystem>();
var tile = mapSystem.GetTileRef(gridId.Value, grid, childTransform.Coordinates);
var tileDef = (ContentTileDefinition)_tileDefManager[tile.Tile.TypeId];
if (tileDef.ID == TilePrototypeId)
{
continue;
}
mapSystem.SetTile(gridId.Value, grid, childTransform.Coordinates, underplatingTile);
changed++;
}
shell.WriteLine($"Changed {changed} tiles.");
}
}

View File

@@ -27,8 +27,5 @@
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
<ProjectReference Include="..\Corvax\Content.Corvax.Interfaces.Server\Content.Corvax.Interfaces.Server.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Objectives\Interfaces\" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
</Project>

View File

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

View File

@@ -0,0 +1,90 @@
using System.Collections.Immutable;
using System.Net;
using Content.Server.IP;
using Robust.Shared.Network;
namespace Content.Server.Database;
/// <summary>
/// Implements logic to match a <see cref="ServerBanDef"/> against a player query.
/// </summary>
/// <remarks>
/// <para>
/// This implementation is used by in-game ban matching code, and partially by the SQLite database layer.
/// Some logic is duplicated into both the SQLite and PostgreSQL database layers to provide more optimal SQL queries.
/// Both should be kept in sync, please!
/// </para>
/// </remarks>
public static class BanMatcher
{
/// <summary>
/// Check whether a ban matches the specified player info.
/// </summary>
/// <remarks>
/// <para>
/// This function does not check whether the ban itself is expired or manually unbanned.
/// </para>
/// </remarks>
/// <param name="ban">The ban information.</param>
/// <param name="player">Information about the player to match against.</param>
/// <returns>True if the ban matches the provided player info.</returns>
public static bool BanMatches(ServerBanDef ban, in PlayerInfo player)
{
var exemptFlags = player.ExemptFlags;
// Any flag to bypass BlacklistedRange bans.
if (exemptFlags != ServerBanExemptFlags.None)
exemptFlags |= ServerBanExemptFlags.BlacklistedRange;
if ((ban.ExemptFlags & exemptFlags) != 0)
return false;
if (!player.ExemptFlags.HasFlag(ServerBanExemptFlags.IP)
&& player.Address != null
&& ban.Address is not null
&& player.Address.IsInSubnet(ban.Address.Value)
&& (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) || player.IsNewPlayer))
{
return true;
}
if (player.UserId is { } id && ban.UserId == id.UserId)
{
return true;
}
return player.HWId is { Length: > 0 } hwIdVar
&& ban.HWId != null
&& hwIdVar.AsSpan().SequenceEqual(ban.HWId.Value.AsSpan());
}
/// <summary>
/// A simple struct containing player info used to match bans against.
/// </summary>
public struct PlayerInfo
{
/// <summary>
/// The user ID of the player.
/// </summary>
public NetUserId? UserId;
/// <summary>
/// The IP address of the player.
/// </summary>
public IPAddress? Address;
/// <summary>
/// The hardware ID of the player.
/// </summary>
public ImmutableArray<byte>? HWId;
/// <summary>
/// Exemption flags the player has been granted.
/// </summary>
public ServerBanExemptFlags ExemptFlags;
/// <summary>
/// True if this player is new and is thus eligible for more bans.
/// </summary>
public bool IsNewPlayer;
}
}

View File

@@ -23,9 +23,9 @@ namespace Content.Server.Database
public NoteSeverity Severity { get; set; }
public NetUserId? BanningAdmin { get; }
public ServerUnbanDef? Unban { get; }
public ServerBanExemptFlags ExemptFlags { get; }
public ServerBanDef(
int? id,
public ServerBanDef(int? id,
NetUserId? userId,
(IPAddress, int)? address,
ImmutableArray<byte>? hwId,
@@ -36,7 +36,8 @@ namespace Content.Server.Database
string reason,
NoteSeverity severity,
NetUserId? banningAdmin,
ServerUnbanDef? unban)
ServerUnbanDef? unban,
ServerBanExemptFlags exemptFlags = default)
{
if (userId == null && address == null && hwId == null)
{
@@ -62,6 +63,7 @@ namespace Content.Server.Database
Severity = severity;
BanningAdmin = banningAdmin;
Unban = unban;
ExemptFlags = exemptFlags;
}
public string FormatBanMessage(IConfigurationManager cfg, ILocalizationManager loc)

View File

@@ -28,6 +28,8 @@ namespace Content.Server.Database
{
private readonly ISawmill _opsLog;
public event Action<DatabaseNotification>? OnNotificationReceived;
/// <param name="opsLog">Sawmill to trace log database operations to.</param>
public ServerDbBase(ISawmill opsLog)
{
@@ -433,13 +435,16 @@ namespace Content.Server.Database
await db.DbContext.SaveChangesAsync();
}
protected static async Task<ServerBanExemptFlags?> GetBanExemptionCore(DbGuard db, NetUserId? userId)
protected static async Task<ServerBanExemptFlags?> GetBanExemptionCore(
DbGuard db,
NetUserId? userId,
CancellationToken cancel = default)
{
if (userId == null)
return null;
var exemption = await db.DbContext.BanExemption
.SingleOrDefaultAsync(e => e.UserId == userId.Value.UserId);
.SingleOrDefaultAsync(e => e.UserId == userId.Value.UserId, cancellationToken: cancel);
return exemption?.Flags;
}
@@ -470,11 +475,11 @@ namespace Content.Server.Database
await db.DbContext.SaveChangesAsync();
}
public async Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId)
public async Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel)
{
await using var db = await GetDb();
await using var db = await GetDb(cancel);
var flags = await GetBanExemptionCore(db, userId);
var flags = await GetBanExemptionCore(db, userId, cancel);
return flags ?? ServerBanExemptFlags.None;
}
@@ -1685,5 +1690,15 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
public abstract ValueTask DisposeAsync();
}
protected void NotificationReceived(DatabaseNotification notification)
{
OnNotificationReceived?.Invoke(notification);
}
public virtual void Shutdown()
{
}
}
}

View File

@@ -116,7 +116,7 @@ namespace Content.Server.Database
/// Get current ban exemption flags for a user
/// </summary>
/// <returns><see cref="ServerBanExemptFlags.None"/> if the user is not exempt from any bans.</returns>
Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId);
Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel = default);
#endregion
@@ -304,6 +304,43 @@ namespace Content.Server.Database
Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job);
#endregion
#region DB Notifications
void SubscribeToNotifications(Action<DatabaseNotification> handler);
/// <summary>
/// Inject a notification as if it was created by the database. This is intended for testing.
/// </summary>
/// <param name="notification">The notification to trigger</param>
void InjectTestNotification(DatabaseNotification notification);
#endregion
}
/// <summary>
/// Represents a notification sent between servers via the database layer.
/// </summary>
/// <remarks>
/// <para>
/// Database notifications are a simple system to broadcast messages to an entire server group
/// backed by the same database. For example, this is used to notify all servers of new ban records.
/// </para>
/// <para>
/// They are currently implemented by the PostgreSQL <c>NOTIFY</c> and <c>LISTEN</c> commands.
/// </para>
/// </remarks>
public struct DatabaseNotification
{
/// <summary>
/// The channel for the notification. This can be used to differentiate notifications for different purposes.
/// </summary>
public required string Channel { get; set; }
/// <summary>
/// The actual contents of the notification. Optional.
/// </summary>
public string? Payload { get; set; }
}
public sealed class ServerDbManager : IServerDbManager
@@ -333,6 +370,8 @@ namespace Content.Server.Database
// This is that connection, close it when we shut down.
private SqliteConnection? _sqliteInMemoryConnection;
private readonly List<Action<DatabaseNotification>> _notificationHandlers = [];
public void Init()
{
_msLogProvider = new LoggingProvider(_logMgr);
@@ -345,6 +384,7 @@ namespace Content.Server.Database
var engine = _cfg.GetCVar(CCVars.DatabaseEngine).ToLower();
var opsLog = _logMgr.GetSawmill("db.op");
var notifyLog = _logMgr.GetSawmill("db.notify");
switch (engine)
{
case "sqlite":
@@ -352,17 +392,22 @@ namespace Content.Server.Database
_db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog);
break;
case "postgres":
var pgOptions = CreatePostgresOptions();
_db = new ServerDbPostgres(pgOptions, _cfg, opsLog);
var (pgOptions, conString) = CreatePostgresOptions();
_db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog);
break;
default:
throw new InvalidDataException($"Unknown database engine {engine}.");
}
_db.OnNotificationReceived += HandleDatabaseNotification;
}
public void Shutdown()
{
_db.OnNotificationReceived -= HandleDatabaseNotification;
_sqliteInMemoryConnection?.Dispose();
_db.Shutdown();
}
public Task<PlayerPreferences> InitPrefsAsync(
@@ -465,10 +510,10 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.UpdateBanExemption(userId, flags));
}
public Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId)
public Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel = default)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetBanExemption(userId));
return RunDbCommand(() => _db.GetBanExemption(userId, cancel));
}
#region Role Ban
@@ -806,7 +851,7 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id));
}
public Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
public Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAllAdminRemarks(player));
@@ -907,6 +952,30 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.RemoveJobWhitelist(player, job));
}
public void SubscribeToNotifications(Action<DatabaseNotification> handler)
{
lock (_notificationHandlers)
{
_notificationHandlers.Add(handler);
}
}
public void InjectTestNotification(DatabaseNotification notification)
{
HandleDatabaseNotification(notification);
}
private async void HandleDatabaseNotification(DatabaseNotification notification)
{
lock (_notificationHandlers)
{
foreach (var handler in _notificationHandlers)
{
handler(notification);
}
}
}
// Wrapper functions to run DB commands from the thread pool.
// This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread.
// For SQLite, this will also enable read parallelization (within limits).
@@ -962,7 +1031,7 @@ namespace Content.Server.Database
return enumerable;
}
private DbContextOptions<PostgresServerDbContext> CreatePostgresOptions()
private (DbContextOptions<PostgresServerDbContext> options, string connectionString) CreatePostgresOptions()
{
var host = _cfg.GetCVar(CCVars.DatabasePgHost);
var port = _cfg.GetCVar(CCVars.DatabasePgPort);
@@ -984,7 +1053,7 @@ namespace Content.Server.Database
builder.UseNpgsql(connectionString);
SetupLogging(builder);
return builder.Options;
return (builder.Options, connectionString);
}
private void SetupSqlite(out Func<DbContextOptions<SqliteServerDbContext>> contextFunc, out bool inMemory)

View File

@@ -0,0 +1,121 @@
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Managers;
using Npgsql;
namespace Content.Server.Database;
/// Listens for ban_notification containing the player id and the banning server id using postgres listen/notify.
/// Players a ban_notification got received for get banned, except when the current server id and the one in the notification payload match.
public sealed partial class ServerDbPostgres
{
/// <summary>
/// The list of notify channels to subscribe to.
/// </summary>
private static readonly string[] NotificationChannels =
[
BanManager.BanNotificationChannel,
];
private static readonly TimeSpan ReconnectWaitIncrease = TimeSpan.FromSeconds(10);
private readonly CancellationTokenSource _notificationTokenSource = new();
private NpgsqlConnection? _notificationConnection;
private TimeSpan _reconnectWaitTime = TimeSpan.Zero;
/// <summary>
/// Sets up the database connection and the notification handler
/// </summary>
private void InitNotificationListener(string connectionString)
{
_notificationConnection = new NpgsqlConnection(connectionString);
_notificationConnection.Notification += OnNotification;
var cancellationToken = _notificationTokenSource.Token;
Task.Run(() => NotificationListener(cancellationToken), cancellationToken);
}
/// <summary>
/// Listens to the notification channel with basic error handling and reopens the connection if it got closed
/// </summary>
private async Task NotificationListener(CancellationToken cancellationToken)
{
if (_notificationConnection == null)
return;
_notifyLog.Verbose("Starting notification listener");
while (!cancellationToken.IsCancellationRequested)
{
try
{
if (_notificationConnection.State == ConnectionState.Broken)
{
_notifyLog.Debug("Notification listener entered broken state, closing...");
await _notificationConnection.CloseAsync();
}
if (_notificationConnection.State == ConnectionState.Closed)
{
_notifyLog.Debug("Opening notification listener connection...");
if (_reconnectWaitTime != TimeSpan.Zero)
{
_notifyLog.Verbose($"_reconnectWaitTime is {_reconnectWaitTime}");
await Task.Delay(_reconnectWaitTime, cancellationToken);
}
await _notificationConnection.OpenAsync(cancellationToken);
_reconnectWaitTime = TimeSpan.Zero;
_notifyLog.Verbose($"Notification connection opened...");
}
foreach (var channel in NotificationChannels)
{
_notifyLog.Verbose($"Listening on channel {channel}");
await using var cmd = new NpgsqlCommand($"LISTEN {channel}", _notificationConnection);
await cmd.ExecuteNonQueryAsync(cancellationToken);
}
while (!cancellationToken.IsCancellationRequested)
{
_notifyLog.Verbose("Waiting on notifications...");
await _notificationConnection.WaitAsync(cancellationToken);
}
}
catch (OperationCanceledException)
{
// Abort loop on cancel.
_notifyLog.Verbose($"Shutting down notification listener due to cancellation");
return;
}
catch (Exception e)
{
_reconnectWaitTime += ReconnectWaitIncrease;
_notifyLog.Error($"Error in notification listener: {e}");
}
}
}
private void OnNotification(object _, NpgsqlNotificationEventArgs notification)
{
_notifyLog.Verbose($"Received notification on channel {notification.Channel}");
NotificationReceived(new DatabaseNotification
{
Channel = notification.Channel,
Payload = notification.Payload,
});
}
public override void Shutdown()
{
_notificationTokenSource.Cancel();
if (_notificationConnection == null)
return;
_notificationConnection.Notification -= OnNotification;
_notificationConnection.Dispose();
}
}

View File

@@ -16,23 +16,26 @@ using Robust.Shared.Utility;
namespace Content.Server.Database
{
public sealed class ServerDbPostgres : ServerDbBase
public sealed partial class ServerDbPostgres : ServerDbBase
{
private readonly DbContextOptions<PostgresServerDbContext> _options;
private readonly ISawmill _notifyLog;
private readonly SemaphoreSlim _prefsSemaphore;
private readonly Task _dbReadyTask;
private int _msLag;
public ServerDbPostgres(
DbContextOptions<PostgresServerDbContext> options,
public ServerDbPostgres(DbContextOptions<PostgresServerDbContext> options,
string connectionString,
IConfigurationManager cfg,
ISawmill opsLog)
ISawmill opsLog,
ISawmill notifyLog)
: base(opsLog)
{
var concurrency = cfg.GetCVar(CCVars.DatabasePgConcurrency);
_options = options;
_notifyLog = notifyLog;
_prefsSemaphore = new SemaphoreSlim(concurrency, concurrency);
_dbReadyTask = Task.Run(async () =>
@@ -49,6 +52,8 @@ namespace Content.Server.Database
});
cfg.OnValueChanged(CCVars.DatabasePgFakeLag, v => _msLag = v, true);
InitNotificationListener(connectionString);
}
#region Ban
@@ -214,7 +219,8 @@ namespace Content.Server.Database
ban.Reason,
ban.Severity,
aUid,
unbanDef);
unbanDef,
ban.ExemptFlags);
}
private static ServerUnbanDef? ConvertUnban(ServerUnban? unban)
@@ -251,7 +257,8 @@ namespace Content.Server.Database
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
RoundId = serverBan.RoundId,
PlaytimeAtNote = serverBan.PlaytimeAtNote,
PlayerUserId = serverBan.UserId?.UserId
PlayerUserId = serverBan.UserId?.UserId,
ExemptFlags = serverBan.ExemptFlags
});
await db.PgDbContext.SaveChangesAsync();

View File

@@ -84,25 +84,27 @@ namespace Content.Server.Database
{
await using var db = await GetDbImpl();
var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value);
// SQLite can't do the net masking stuff we need to match IP address ranges.
// So just pull down the whole list into memory.
var bans = await GetAllBans(db.SqliteDbContext, includeUnbanned: false, exempt);
return bans.FirstOrDefault(b => BanMatches(b, address, userId, hwId, exempt, newPlayer)) is { } foundBan
? ConvertBan(foundBan)
: null;
return (await GetServerBanQueryAsync(db, address, userId, hwId, includeUnbanned: false)).FirstOrDefault();
}
public override async Task<List<ServerBanDef>> GetServerBansAsync(IPAddress? address,
public override async Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId, bool includeUnbanned)
ImmutableArray<byte>? hwId,
bool includeUnbanned)
{
await using var db = await GetDbImpl();
return (await GetServerBanQueryAsync(db, address, userId, hwId, includeUnbanned)).ToList();
}
private async Task<IEnumerable<ServerBanDef>> GetServerBanQueryAsync(
DbGuardImpl db,
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned)
{
var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = !await db.SqliteDbContext.Player.AnyAsync(p => p.UserId == userId);
@@ -111,10 +113,18 @@ namespace Content.Server.Database
// So just pull down the whole list into memory.
var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned, exempt);
var playerInfo = new BanMatcher.PlayerInfo
{
Address = address,
UserId = userId,
ExemptFlags = exempt ?? default,
HWId = hwId,
IsNewPlayer = newPlayer,
};
return queryBans
.Where(b => BanMatches(b, address, userId, hwId, exempt, newPlayer))
.Select(ConvertBan)
.ToList()!;
.Where(b => BanMatcher.BanMatches(b!, playerInfo))!;
}
private static async Task<List<ServerBan>> GetAllBans(
@@ -141,31 +151,6 @@ namespace Content.Server.Database
return await query.ToListAsync();
}
private static bool BanMatches(ServerBan ban,
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ServerBanExemptFlags? exemptFlags,
bool newPlayer)
{
if (!exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP)
&& address != null
&& ban.Address is not null
&& address.IsInSubnet(ban.Address.ToTuple().Value)
&& (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) ||
newPlayer))
{
return true;
}
if (userId is { } id && ban.PlayerUserId == id.UserId)
{
return true;
}
return hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId);
}
public override async Task AddServerBanAsync(ServerBanDef serverBan)
{
await using var db = await GetDbImpl();
@@ -181,7 +166,8 @@ namespace Content.Server.Database
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
RoundId = serverBan.RoundId,
PlaytimeAtNote = serverBan.PlaytimeAtNote,
PlayerUserId = serverBan.UserId?.UserId
PlayerUserId = serverBan.UserId?.UserId,
ExemptFlags = serverBan.ExemptFlags
});
await db.SqliteDbContext.SaveChangesAsync();
@@ -364,6 +350,7 @@ namespace Content.Server.Database
}
#endregion
[return: NotNullIfNotNull(nameof(ban))]
private static ServerBanDef? ConvertBan(ServerBan? ban)
{
if (ban == null)

View File

@@ -8,6 +8,9 @@ namespace Content.Server.Discord;
public sealed class DiscordWebhook : IPostInjectInit
{
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
[Dependency] private readonly ILogManager _log = default!;
private const string BaseUrl = "https://discord.com/api/v10/webhooks";
@@ -68,7 +71,11 @@ public sealed class DiscordWebhook : IPostInjectInit
public async Task<HttpResponseMessage> CreateMessage(WebhookIdentifier identifier, WebhookPayload payload)
{
var url = $"{GetUrl(identifier)}?wait=true";
return await _http.PostAsJsonAsync(url, payload, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
var response = await _http.PostAsJsonAsync(url, payload, JsonOptions);
LogResponse(response, "Create");
return response;
}
/// <summary>
@@ -80,7 +87,11 @@ public sealed class DiscordWebhook : IPostInjectInit
public async Task<HttpResponseMessage> DeleteMessage(WebhookIdentifier identifier, ulong messageId)
{
var url = $"{GetUrl(identifier)}/messages/{messageId}";
return await _http.DeleteAsync(url);
var response = await _http.DeleteAsync(url);
LogResponse(response, "Delete");
return response;
}
/// <summary>
@@ -93,11 +104,40 @@ public sealed class DiscordWebhook : IPostInjectInit
public async Task<HttpResponseMessage> EditMessage(WebhookIdentifier identifier, ulong messageId, WebhookPayload payload)
{
var url = $"{GetUrl(identifier)}/messages/{messageId}";
return await _http.PatchAsJsonAsync(url, payload, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
var response = await _http.PatchAsJsonAsync(url, payload, JsonOptions);
LogResponse(response, "Edit");
return response;
}
void IPostInjectInit.PostInject()
{
_sawmill = _log.GetSawmill("DISCORD");
}
/// <summary>
/// Logs detailed information about the HTTP response received from a Discord webhook request.
/// If the response status code is non-2XX it logs the status code, relevant rate limit headers.
/// </summary>
/// <param name="response">The HTTP response received from the Discord API.</param>
/// <param name="methodName">The name (constant) of the method that initiated the webhook request (e.g., "Create", "Edit", "Delete").</param>
private void LogResponse(HttpResponseMessage response, string methodName)
{
if (!response.IsSuccessStatusCode)
{
_sawmill.Error($"Failed to {methodName} message. Status code: {response.StatusCode}.");
if (response.Headers.TryGetValues("Retry-After", out var retryAfter))
_sawmill.Debug($"Failed webhook response Retry-After: {string.Join(", ", retryAfter)}");
if (response.Headers.TryGetValues("X-RateLimit-Global", out var globalRateLimit))
_sawmill.Debug($"Failed webhook response X-RateLimit-Global: {string.Join(", ", globalRateLimit)}");
if (response.Headers.TryGetValues("X-RateLimit-Scope", out var rateLimitScope))
_sawmill.Debug($"Failed webhook response X-RateLimit-Scope: {string.Join(", ", rateLimitScope)}");
}
}
}

View File

@@ -78,8 +78,11 @@ public sealed partial class GatherableSystem : EntitySystem
}
var getLoot = _proto.Index(table);
var spawnLoot = getLoot.GetSpawns(_random);
var spawnPos = pos.Offset(_random.NextVector2(component.GatherOffset));
Spawn(spawnLoot[0], spawnPos);
foreach (var loot in spawnLoot)
{
var spawnPos = pos.Offset(_random.NextVector2(component.GatherOffset));
Spawn(loot, spawnPos);
}
}
}
}

View File

@@ -6,66 +6,66 @@ using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Server.Interaction
namespace Content.Server.Interaction;
[AdminCommand(AdminFlags.Debug)]
public sealed class TilePryCommand : IConsoleCommand
{
[AdminCommand(AdminFlags.Debug)]
sealed class TilePryCommand : IConsoleCommand
[Dependency] private readonly IEntityManager _entities = default!;
public string Command => "tilepry";
public string Description => "Pries up all tiles in a radius around the user.";
public string Help => $"Usage: {Command} <radius>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
[Dependency] private readonly IEntityManager _entities = default!;
public string Command => "tilepry";
public string Description => "Pries up all tiles in a radius around the user.";
public string Help => $"Usage: {Command} <radius>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
var player = shell.Player;
if (player?.AttachedEntity is not { } attached)
{
var player = shell.Player;
if (player?.AttachedEntity is not {} attached)
return;
}
if (args.Length != 1)
{
shell.WriteLine(Help);
return;
}
if (!int.TryParse(args[0], out var radius))
{
shell.WriteError($"{args[0]} isn't a valid integer.");
return;
}
if (radius < 0)
{
shell.WriteError("Radius must be positive.");
return;
}
var mapSystem = _entities.System<SharedMapSystem>();
var xform = _entities.GetComponent<TransformComponent>(attached);
var playerGrid = xform.GridUid;
if (!_entities.TryGetComponent<MapGridComponent>(playerGrid, out var mapGrid))
return;
var playerPosition = xform.Coordinates;
var tileDefinitionManager = IoCManager.Resolve<ITileDefinitionManager>();
for (var i = -radius; i <= radius; i++)
{
for (var j = -radius; j <= radius; j++)
{
return;
}
var tile = mapSystem.GetTileRef(playerGrid.Value, mapGrid, playerPosition.Offset(new Vector2(i, j)));
var coordinates = mapSystem.GridTileToLocal(playerGrid.Value, mapGrid, tile.GridIndices);
var tileDef = (ContentTileDefinition)tileDefinitionManager[tile.Tile.TypeId];
if (args.Length != 1)
{
shell.WriteLine(Help);
return;
}
if (!tileDef.CanCrowbar) continue;
if (!int.TryParse(args[0], out var radius))
{
shell.WriteLine($"{args[0]} isn't a valid integer.");
return;
}
if (radius < 0)
{
shell.WriteLine("Radius must be positive.");
return;
}
var mapManager = IoCManager.Resolve<IMapManager>();
var xform = _entities.GetComponent<TransformComponent>(attached);
var playerGrid = xform.GridUid;
if (!_entities.TryGetComponent<MapGridComponent>(playerGrid, out var mapGrid))
return;
var playerPosition = xform.Coordinates;
var tileDefinitionManager = IoCManager.Resolve<ITileDefinitionManager>();
for (var i = -radius; i <= radius; i++)
{
for (var j = -radius; j <= radius; j++)
{
var tile = mapGrid.GetTileRef(playerPosition.Offset(new Vector2(i, j)));
var coordinates = mapGrid.GridTileToLocal(tile.GridIndices);
var tileDef = (ContentTileDefinition) tileDefinitionManager[tile.Tile.TypeId];
if (!tileDef.CanCrowbar) continue;
var plating = tileDefinitionManager["Plating"];
mapGrid.SetTile(coordinates, new Tile(plating.TileId));
}
var plating = tileDefinitionManager["Plating"];
mapSystem.SetTile(playerGrid.Value, mapGrid, coordinates, new Tile(plating.TileId));
}
}
}

View File

@@ -5,6 +5,7 @@ using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Explosion.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
@@ -99,7 +100,7 @@ namespace Content.Server.Nutrition.EntitySystems
{
otherPlayers.RemovePlayer(actor.PlayerSession);
}
_popup.PopupEntity(Loc.GetString("cream-pied-component-on-hit-by-message-others", ("owner", uid), ("thrower", args.Thrown)), uid, otherPlayers, false);
_popup.PopupEntity(Loc.GetString("cream-pied-component-on-hit-by-message-others", ("owner", Identity.Name(uid, EntityManager)), ("thrower", args.Thrown)), uid, otherPlayers, false);
}
private void OnRejuvenate(Entity<CreamPiedComponent> entity, ref RejuvenateEvent args)

View File

@@ -77,8 +77,7 @@ internal sealed class RandomWalkController : VirtualController
randomWalk.BiasVector *= 0f;
var pushStrength = _random.NextFloat(randomWalk.MinSpeed, randomWalk.MaxSpeed);
_physics.SetLinearVelocity(uid, physics.LinearVelocity * randomWalk.AccumulatorRatio, body: physics);
_physics.ApplyLinearImpulse(uid, pushVec * (pushStrength * physics.Mass), body: physics);
_physics.SetLinearVelocity(uid, physics.LinearVelocity * randomWalk.AccumulatorRatio + pushVec * pushStrength, body: physics);
}
/// <summary>

View File

@@ -11,6 +11,7 @@ using Content.Shared.Respawn;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
namespace Content.Server.Respawn;
@@ -23,6 +24,7 @@ public sealed class SpecialRespawnSystem : SharedSpecialRespawnSystem
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TurfSystem _turf = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public override void Initialize()
{
@@ -87,6 +89,10 @@ public sealed class SpecialRespawnSystem : SharedSpecialRespawnSystem
if (!TryComp<MapGridComponent>(entityGridUid, out var grid) || MetaData(entityGridUid.Value).EntityLifeStage >= EntityLifeStage.Terminating)
return;
//Invalid prototype
if (!_proto.HasIndex(component.Prototype))
return;
if (TryFindRandomTile(entityGridUid.Value, entityMapUid.Value, 10, out var coords))
Respawn(uid, component.Prototype, coords);

View File

@@ -3,5 +3,16 @@ namespace Content.Server.Salvage.Magnet;
[RegisterComponent]
public sealed partial class SalvageMagnetComponent : Component
{
/// <summary>
/// The max distance at which the magnet will pull in wrecks.
/// Scales from 50% to 100%.
/// </summary>
[DataField]
public float MagnetSpawnDistance = 128f;
/// <summary>
/// How far offset to either side will the magnet wreck spawn.
/// </summary>
[DataField]
public float LateralOffset = 32f;
}

View File

@@ -338,7 +338,7 @@ public sealed partial class SalvageSystem
worldAngle = _random.NextAngle();
}
if (!TryGetSalvagePlacementLocation(mapId, attachedBounds, bounds!.Value, worldAngle, out var spawnLocation, out var spawnAngle))
if (!TryGetSalvagePlacementLocation(magnet, mapId, attachedBounds, bounds!.Value, worldAngle, out var spawnLocation, out var spawnAngle))
{
Report(magnet.Owner, MagnetChannel, "salvage-system-announcement-spawn-no-debris-available");
_mapManager.DeleteMap(salvMapXform.MapID);
@@ -390,22 +390,19 @@ public sealed partial class SalvageSystem
RaiseLocalEvent(ref active);
}
private bool TryGetSalvagePlacementLocation(MapId mapId, Box2Rotated attachedBounds, Box2 bounds, Angle worldAngle, out MapCoordinates coords, out Angle angle)
private bool TryGetSalvagePlacementLocation(Entity<SalvageMagnetComponent> magnet, MapId mapId, Box2Rotated attachedBounds, Box2 bounds, Angle worldAngle, out MapCoordinates coords, out Angle angle)
{
// Grid intersection only does AABB atm.
var attachedAABB = attachedBounds.CalcBoundingBox();
var minDistance = (attachedAABB.Height < attachedAABB.Width ? attachedAABB.Width : attachedAABB.Height) / 2f;
var minActualDistance = bounds.Height < bounds.Width ? minDistance + bounds.Width / 2f : minDistance + bounds.Height / 2f;
var attachedCenter = attachedAABB.Center;
var fraction = 0.25f;
var magnetPos = _transform.GetWorldPosition(magnet) + worldAngle.ToVec() * bounds.MaxDimension;
var origin = attachedAABB.ClosestPoint(magnetPos);
var fraction = 0.50f;
// Thanks 20kdc
for (var i = 0; i < 20; i++)
{
var randomPos = attachedCenter +
worldAngle.ToVec() * (minActualDistance * fraction);
var randomPos = origin +
worldAngle.ToVec() * (magnet.Comp.MagnetSpawnDistance * fraction) +
(worldAngle + Math.PI / 2).ToVec() * _random.NextFloat(-magnet.Comp.LateralOffset, magnet.Comp.LateralOffset);
var finalCoords = new MapCoordinates(randomPos, mapId);
angle = _random.NextAngle();
@@ -417,7 +414,7 @@ public sealed partial class SalvageSystem
if (_mapManager.FindGridsIntersecting(finalCoords.MapId, box2Rot).Any())
{
// Bump it further and further just in case.
fraction += 0.25f;
fraction += 0.1f;
continue;
}

View File

@@ -53,6 +53,8 @@ public sealed class RadarConsoleSystem : SharedRadarConsoleSystem
state = _console.GetNavState(uid, docks);
}
state.RotateWithEntity = !component.FollowEntity;
_uiSystem.SetUiState(uid, RadarConsoleUiKey.Key, new NavBoundUserInterfaceState(state));
}
}

View File

@@ -292,7 +292,7 @@ public sealed partial class GunSystem : SharedGunSystem
{
var newuid = Spawn(ammoSpreadComp.Proto, fromEnt);
ShootOrThrow(newuid, angles[i].ToVec(), gunVelocity, gun, gunUid, user);
shotProjectiles.Add(ammoEnt);
shotProjectiles.Add(newuid);
}
}
else

View File

@@ -725,7 +725,7 @@ public sealed class WiresSystem : SharedWiresSystem
break;
}
Tool.PlayToolSound(toolEntity, tool, user);
Tool.PlayToolSound(toolEntity, tool, null);
if (wire.Action == null || wire.Action.Cut(user, wire))
{
wire.IsCut = true;
@@ -746,7 +746,7 @@ public sealed class WiresSystem : SharedWiresSystem
break;
}
Tool.PlayToolSound(toolEntity, tool, user);
Tool.PlayToolSound(toolEntity, tool, null);
if (wire.Action == null || wire.Action.Mend(user, wire))
{
wire.IsCut = false;

View File

@@ -43,7 +43,7 @@ public abstract partial class SharedBuckleSystem
}
else
{
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, component.BuckleDoafterTime, new BuckleDoAfterEvent(), args.User, args.Dragged, uid)
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, component.BuckleDoafterTime, new BuckleDoAfterEvent(), args.Dragged, args.Dragged, uid)
{
BreakOnMove = true,
BreakOnDamage = true,

View File

@@ -24,9 +24,6 @@
</ProjectReference>
<ProjectReference Include="..\Corvax\Content.Corvax.Interfaces.Shared\Content.Corvax.Interfaces.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Power\Components\" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\MSBuild\Robust.CompNetworkGenerator.targets" />
</Project>

View File

@@ -0,0 +1,17 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Damage.Components;
/// <summary>
/// This is used for a clothing item that modifies the slowdown from taking damage.
/// Used for entities with <see cref="SlowOnDamageComponent"/>
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SlowOnDamageSystem))]
public sealed partial class ClothingSlowOnDamageModifierComponent : Component
{
/// <summary>
/// A coefficient modifier for the slowdown
/// </summary>
[DataField]
public float Modifier = 1;
}

View File

@@ -1,5 +1,8 @@
using Content.Shared.Clothing;
using Content.Shared.Damage.Components;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
using Content.Shared.Movement.Systems;
namespace Content.Shared.Damage
@@ -14,6 +17,11 @@ namespace Content.Shared.Damage
SubscribeLocalEvent<SlowOnDamageComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<SlowOnDamageComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, InventoryRelayedEvent<ModifySlowOnDamageSpeedEvent>>(OnModifySpeed);
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, ClothingGotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
}
private void OnRefreshMovespeed(EntityUid uid, SlowOnDamageComponent component, RefreshMovementSpeedModifiersEvent args)
@@ -36,7 +44,10 @@ namespace Content.Shared.Damage
if (closest != FixedPoint2.Zero)
{
var speed = component.SpeedModifierThresholds[closest];
args.ModifySpeed(speed, speed);
var ev = new ModifySlowOnDamageSpeedEvent(speed);
RaiseLocalEvent(uid, ref ev);
args.ModifySpeed(ev.Speed, ev.Speed);
}
}
@@ -47,5 +58,37 @@ namespace Content.Shared.Damage
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(uid);
}
private void OnModifySpeed(Entity<ClothingSlowOnDamageModifierComponent> ent, ref InventoryRelayedEvent<ModifySlowOnDamageSpeedEvent> args)
{
var dif = 1 - args.Args.Speed;
if (dif <= 0)
return;
// reduces the slowness modifier by the given coefficient
args.Args.Speed += dif * ent.Comp.Modifier;
}
private void OnExamined(Entity<ClothingSlowOnDamageModifierComponent> ent, ref ExaminedEvent args)
{
var msg = Loc.GetString("slow-on-damage-modifier-examine", ("mod", (1 - ent.Comp.Modifier) * 100));
args.PushMarkup(msg);
}
private void OnGotEquipped(Entity<ClothingSlowOnDamageModifierComponent> ent, ref ClothingGotEquippedEvent args)
{
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer);
}
private void OnGotUnequipped(Entity<ClothingSlowOnDamageModifierComponent> ent, ref ClothingGotUnequippedEvent args)
{
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer);
}
}
[ByRefEvent]
public record struct ModifySlowOnDamageSpeedEvent(float Speed) : IInventoryRelayEvent
{
public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET;
}
}

View File

@@ -6,6 +6,7 @@ namespace Content.Shared.Humanoid
[Serializable, NetSerializable]
public enum HumanoidVisualLayers : byte
{
Special, // for the cat ears
Tail,
Hair,
FacialHair,
@@ -18,6 +19,7 @@ namespace Content.Shared.Humanoid
RArm,
LArm,
RHand,
LHand,
RLeg,
LLeg,
@@ -27,5 +29,6 @@ namespace Content.Shared.Humanoid
StencilMask,
Ensnare,
Fire,
}
}

View File

@@ -5,6 +5,7 @@ namespace Content.Shared.Humanoid.Markings
[Serializable, NetSerializable]
public enum MarkingCategories : byte
{
Special,
Hair,
FacialHair,
Head,
@@ -24,6 +25,7 @@ namespace Content.Shared.Humanoid.Markings
{
return layer switch
{
HumanoidVisualLayers.Special => MarkingCategories.Special,
HumanoidVisualLayers.Hair => MarkingCategories.Hair,
HumanoidVisualLayers.FacialHair => MarkingCategories.FacialHair,
HumanoidVisualLayers.Head => MarkingCategories.Head,

View File

@@ -34,6 +34,7 @@ public partial class InventorySystem
// by-ref events
SubscribeLocalEvent<InventoryComponent, GetExplosionResistanceEvent>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, IsWeightlessEvent>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, ModifySlowOnDamageSpeedEvent>(RefRelayInventoryEvent);
// Eye/vision events
SubscribeLocalEvent<InventoryComponent, CanSeeAttemptEvent>(RelayInventoryEvent);

View File

@@ -38,7 +38,6 @@ public abstract partial class SharedProjectileSystem : EntitySystem
SubscribeLocalEvent<EmbeddableProjectileComponent, ThrowDoHitEvent>(OnEmbedThrowDoHit);
SubscribeLocalEvent<EmbeddableProjectileComponent, ActivateInWorldEvent>(OnEmbedActivate);
SubscribeLocalEvent<EmbeddableProjectileComponent, RemoveEmbeddedProjectileEvent>(OnEmbedRemove);
SubscribeLocalEvent<EmbeddableProjectileComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow);
}
private void OnEmbedActivate(EntityUid uid, EmbeddableProjectileComponent component, ActivateInWorldEvent args)
@@ -154,14 +153,6 @@ public abstract partial class SharedProjectileSystem : EntitySystem
{
public override DoAfterEvent Clone() => this;
}
/// <summary>
/// Prevent players with the Pacified status effect from throwing embeddable projectiles.
/// </summary>
private void OnAttemptPacifiedThrow(Entity<EmbeddableProjectileComponent> ent, ref AttemptPacifiedThrowEvent args)
{
args.Cancel("pacified-cannot-throw-embed");
}
}
[Serializable, NetSerializable]

View File

@@ -0,0 +1,67 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Traits;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Roles;
/// <summary>
/// Requires a character to have, or not have, certain traits
/// </summary>
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class TraitsRequirement : JobRequirement
{
[DataField(required: true)]
public HashSet<ProtoId<TraitPrototype>> Traits = new();
public override bool Check(IEntityManager entManager,
IPrototypeManager protoManager,
HumanoidCharacterProfile? profile,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason)
{
reason = new FormattedMessage();
if (profile is null) //the profile could be null if the player is a ghost. In this case we don't need to block the role selection for ghostrole
return true;
var sb = new StringBuilder();
sb.Append("[color=yellow]");
foreach (var t in Traits)
{
sb.Append(Loc.GetString(protoManager.Index(t).Name) + " ");
}
sb.Append("[/color]");
if (!Inverted)
{
reason = FormattedMessage.FromMarkupPermissive($"{Loc.GetString("role-timer-whitelisted-traits")}\n{sb}");
//at least one of
foreach (var trait in Traits)
{
if (profile.TraitPreferences.Contains(trait))
return true;
}
return false;
}
else
{
reason = FormattedMessage.FromMarkupPermissive($"{Loc.GetString("role-timer-blacklisted-traits")}\n{sb}");
foreach (var trait in Traits)
{
if (profile.TraitPreferences.Contains(trait))
return false;
}
}
return true;
}
}

View File

@@ -20,6 +20,8 @@ public sealed class NavInterfaceState
public Dictionary<NetEntity, List<DockingPortState>> Docks;
public bool RotateWithEntity = true;
public NavInterfaceState(
float maxRange,
NetCoordinates? coordinates,

View File

@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Silicons.StationAi;
/// <summary>
/// Handles the static overlay for station AI.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class StationAiOverlayComponent : Component;

View File

@@ -0,0 +1,19 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Silicons.StationAi;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]//, Access(typeof(SharedStationAiSystem))]
public sealed partial class StationAiVisionComponent : Component
{
[DataField, AutoNetworkedField]
public bool Enabled = true;
[DataField, AutoNetworkedField]
public bool Occluded = true;
/// <summary>
/// Range in tiles
/// </summary>
[DataField, AutoNetworkedField]
public float Range = 7.5f;
}

View File

@@ -0,0 +1,522 @@
using Robust.Shared.Map.Components;
using Robust.Shared.Threading;
using Robust.Shared.Utility;
namespace Content.Shared.Silicons.StationAi;
public sealed class StationAiVisionSystem : EntitySystem
{
/*
* This class handles 2 things:
* 1. It handles general "what tiles are visible" line of sight checks.
* 2. It does single-tile lookups to tell if they're visible or not with support for a faster range-only path.
*/
[Dependency] private readonly IParallelManager _parallel = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedMapSystem _maps = default!;
[Dependency] private readonly SharedTransformSystem _xforms = default!;
private SeedJob _seedJob;
private ViewJob _job;
private readonly HashSet<Entity<OccluderComponent>> _occluders = new();
private readonly HashSet<Entity<StationAiVisionComponent>> _seeds = new();
private readonly HashSet<Vector2i> _viewportTiles = new();
// Dummy set
private readonly HashSet<Vector2i> _singleTiles = new();
// Occupied tiles per-run.
// For now it's only 1-grid supported but updating to TileRefs if required shouldn't be too hard.
private readonly HashSet<Vector2i> _opaque = new();
/// <summary>
/// Do we skip line of sight checks and just check vision ranges.
/// </summary>
private bool FastPath;
/// <summary>
/// Have we found the target tile if we're only checking for a single one.
/// </summary>
private bool TargetFound;
public override void Initialize()
{
base.Initialize();
_seedJob = new()
{
System = this,
};
_job = new ViewJob()
{
EntManager = EntityManager,
Maps = _maps,
System = this,
};
}
/// <summary>
/// Returns whether a tile is accessible based on vision.
/// </summary>
public bool IsAccessible(Entity<MapGridComponent> grid, Vector2i tile, float expansionSize = 8.5f, bool fastPath = false)
{
_viewportTiles.Clear();
_opaque.Clear();
_seeds.Clear();
_viewportTiles.Add(tile);
var localBounds = _lookup.GetLocalBounds(tile, grid.Comp.TileSize);
var expandedBounds = localBounds.Enlarged(expansionSize);
_seedJob.Grid = grid;
_seedJob.ExpandedBounds = expandedBounds;
_parallel.ProcessNow(_seedJob);
_job.Data.Clear();
FastPath = fastPath;
foreach (var seed in _seeds)
{
if (!seed.Comp.Enabled)
continue;
_job.Data.Add(seed);
}
if (_seeds.Count == 0)
return false;
// Skip occluders step if we're just doing range checks.
if (!fastPath)
{
var tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, expandedBounds, ignoreEmpty: false);
// Get all other relevant tiles.
while (tileEnumerator.MoveNext(out var tileRef))
{
if (IsOccluded(grid, tileRef.GridIndices))
{
_opaque.Add(tileRef.GridIndices);
}
}
}
for (var i = _job.Vis1.Count; i < _job.Data.Count; i++)
{
_job.Vis1.Add(new Dictionary<Vector2i, int>());
_job.Vis2.Add(new Dictionary<Vector2i, int>());
_job.SeedTiles.Add(new HashSet<Vector2i>());
_job.BoundaryTiles.Add(new HashSet<Vector2i>());
}
_job.TargetTile = tile;
TargetFound = false;
_singleTiles.Clear();
_job.Grid = grid;
_job.VisibleTiles = _singleTiles;
_parallel.ProcessNow(_job, _job.Data.Count);
return TargetFound;
}
private bool IsOccluded(Entity<MapGridComponent> grid, Vector2i tile)
{
var tileBounds = _lookup.GetLocalBounds(tile, grid.Comp.TileSize).Enlarged(-0.05f);
_occluders.Clear();
_lookup.GetLocalEntitiesIntersecting(grid.Owner, tileBounds, _occluders, LookupFlags.Static);
var anyOccluders = false;
foreach (var occluder in _occluders)
{
if (!occluder.Comp.Enabled)
continue;
anyOccluders = true;
break;
}
return anyOccluders;
}
/// <summary>
/// Gets a byond-equivalent for tiles in the specified worldAABB.
/// </summary>
/// <param name="expansionSize">How much to expand the bounds before to find vision intersecting it. Makes this the largest vision size + 1 tile.</param>
public void GetView(Entity<MapGridComponent> grid, Box2Rotated worldBounds, HashSet<Vector2i> visibleTiles, float expansionSize = 8.5f)
{
_viewportTiles.Clear();
_opaque.Clear();
_seeds.Clear();
var expandedBounds = worldBounds.Enlarged(expansionSize);
// TODO: Would be nice to be able to run this while running the other stuff.
_seedJob.Grid = grid;
var localAABB = _xforms.GetInvWorldMatrix(grid).TransformBox(expandedBounds);
_seedJob.ExpandedBounds = localAABB;
_parallel.ProcessNow(_seedJob);
_job.Data.Clear();
FastPath = false;
foreach (var seed in _seeds)
{
if (!seed.Comp.Enabled)
continue;
_job.Data.Add(seed);
}
if (_seeds.Count == 0)
return;
// Get viewport tiles
var tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAABB, ignoreEmpty: false);
while (tileEnumerator.MoveNext(out var tileRef))
{
if (IsOccluded(grid, tileRef.GridIndices))
{
_opaque.Add(tileRef.GridIndices);
}
_viewportTiles.Add(tileRef.GridIndices);
}
tileEnumerator = _maps.GetLocalTilesEnumerator(grid, grid, localAABB, ignoreEmpty: false);
// Get all other relevant tiles.
while (tileEnumerator.MoveNext(out var tileRef))
{
if (_viewportTiles.Contains(tileRef.GridIndices))
continue;
if (IsOccluded(grid, tileRef.GridIndices))
{
_opaque.Add(tileRef.GridIndices);
}
}
// Wait for seed job here
for (var i = _job.Vis1.Count; i < _job.Data.Count; i++)
{
_job.Vis1.Add(new Dictionary<Vector2i, int>());
_job.Vis2.Add(new Dictionary<Vector2i, int>());
_job.SeedTiles.Add(new HashSet<Vector2i>());
_job.BoundaryTiles.Add(new HashSet<Vector2i>());
}
_job.TargetTile = null;
TargetFound = false;
_job.Grid = grid;
_job.VisibleTiles = visibleTiles;
_parallel.ProcessNow(_job, _job.Data.Count);
}
private int GetMaxDelta(Vector2i tile, Vector2i center)
{
var delta = tile - center;
return Math.Max(Math.Abs(delta.X), Math.Abs(delta.Y));
}
private int GetSumDelta(Vector2i tile, Vector2i center)
{
var delta = tile - center;
return Math.Abs(delta.X) + Math.Abs(delta.Y);
}
/// <summary>
/// Checks if any of a tile's neighbors are visible.
/// </summary>
private bool CheckNeighborsVis(
Dictionary<Vector2i, int> vis,
Vector2i index,
int d)
{
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x == 0 && y == 0)
continue;
var neighbor = index + new Vector2i(x, y);
var neighborD = vis.GetValueOrDefault(neighbor);
if (neighborD == d)
return true;
}
}
return false;
}
/// Checks whether this tile fits the definition of a "corner"
/// </summary>
private bool IsCorner(
HashSet<Vector2i> tiles,
HashSet<Vector2i> blocked,
Dictionary<Vector2i, int> vis1,
Vector2i index,
Vector2i delta)
{
var diagonalIndex = index + delta;
if (!tiles.TryGetValue(diagonalIndex, out var diagonal))
return false;
var cardinal1 = new Vector2i(index.X, diagonal.Y);
var cardinal2 = new Vector2i(diagonal.X, index.Y);
return vis1.GetValueOrDefault(diagonal) != 0 &&
vis1.GetValueOrDefault(cardinal1) != 0 &&
vis1.GetValueOrDefault(cardinal2) != 0 &&
blocked.Contains(cardinal1) &&
blocked.Contains(cardinal2) &&
!blocked.Contains(diagonal);
}
/// <summary>
/// Gets the relevant vision seeds for later.
/// </summary>
private record struct SeedJob() : IRobustJob
{
public StationAiVisionSystem System;
public Entity<MapGridComponent> Grid;
public Box2 ExpandedBounds;
public void Execute()
{
System._lookup.GetLocalEntitiesIntersecting(Grid.Owner, ExpandedBounds, System._seeds);
}
}
private record struct ViewJob() : IParallelRobustJob
{
public int BatchSize => 1;
public IEntityManager EntManager;
public SharedMapSystem Maps;
public StationAiVisionSystem System;
public Entity<MapGridComponent> Grid;
public List<Entity<StationAiVisionComponent>> Data = new();
// If we're doing range-checks might be able to early out
public Vector2i? TargetTile;
public HashSet<Vector2i> VisibleTiles;
public readonly List<Dictionary<Vector2i, int>> Vis1 = new();
public readonly List<Dictionary<Vector2i, int>> Vis2 = new();
public readonly List<HashSet<Vector2i>> SeedTiles = new();
public readonly List<HashSet<Vector2i>> BoundaryTiles = new();
public void Execute(int index)
{
// If we're looking for a single tile then early-out if someone else has found it.
if (TargetTile != null)
{
lock (System)
{
if (System.TargetFound)
{
return;
}
}
}
var seed = Data[index];
var seedXform = EntManager.GetComponent<TransformComponent>(seed);
// Fastpath just get tiles in range.
// Either xray-vision or system is doing a quick-and-dirty check.
if (!seed.Comp.Occluded || System.FastPath)
{
var squircles = Maps.GetLocalTilesIntersecting(Grid.Owner,
Grid.Comp,
new Circle(System._xforms.GetWorldPosition(seedXform), seed.Comp.Range), ignoreEmpty: false);
// Try to find the target tile.
if (TargetTile != null)
{
foreach (var tile in squircles)
{
if (tile.GridIndices == TargetTile)
{
lock (System)
{
System.TargetFound = true;
}
return;
}
}
}
else
{
lock (VisibleTiles)
{
foreach (var tile in squircles)
{
VisibleTiles.Add(tile.GridIndices);
}
}
}
return;
}
// Code based upon https://github.com/OpenDreamProject/OpenDream/blob/c4a3828ccb997bf3722673620460ebb11b95ccdf/OpenDreamShared/Dream/ViewAlgorithm.cs
var range = seed.Comp.Range;
var vis1 = Vis1[index];
var vis2 = Vis2[index];
var seedTiles = SeedTiles[index];
var boundary = BoundaryTiles[index];
// Cleanup last run
vis1.Clear();
vis2.Clear();
seedTiles.Clear();
boundary.Clear();
var maxDepthMax = 0;
var sumDepthMax = 0;
var eyePos = Maps.GetTileRef(Grid.Owner, Grid, seedXform.Coordinates).GridIndices;
for (var x = Math.Floor(eyePos.X - range); x <= eyePos.X + range; x++)
{
for (var y = Math.Floor(eyePos.Y - range); y <= eyePos.Y + range; y++)
{
var tile = new Vector2i((int)x, (int)y);
var delta = tile - eyePos;
var xDelta = Math.Abs(delta.X);
var yDelta = Math.Abs(delta.Y);
var deltaSum = xDelta + yDelta;
maxDepthMax = Math.Max(maxDepthMax, Math.Max(xDelta, yDelta));
sumDepthMax = Math.Max(sumDepthMax, deltaSum);
seedTiles.Add(tile);
}
}
// Step 3, Diagonal shadow loop
for (var d = 0; d < maxDepthMax; d++)
{
foreach (var tile in seedTiles)
{
var maxDelta = System.GetMaxDelta(tile, eyePos);
if (maxDelta == d + 1 && System.CheckNeighborsVis(vis2, tile, d))
{
vis2[tile] = (System._opaque.Contains(tile) ? -1 : d + 1);
}
}
}
// Step 4, Straight shadow loop
for (var d = 0; d < sumDepthMax; d++)
{
foreach (var tile in seedTiles)
{
var sumDelta = System.GetSumDelta(tile, eyePos);
if (sumDelta == d + 1 && System.CheckNeighborsVis(vis1, tile, d))
{
if (System._opaque.Contains(tile))
{
vis1[tile] = -1;
}
else if (vis2.GetValueOrDefault(tile) != 0)
{
vis1[tile] = d + 1;
}
}
}
}
// Add the eye itself
vis1[eyePos] = 1;
// Step 6.
// Step 7.
// Step 8.
foreach (var tile in seedTiles)
{
vis2[tile] = vis1.GetValueOrDefault(tile, 0);
}
// Step 9
foreach (var tile in seedTiles)
{
if (!System._opaque.Contains(tile))
continue;
var tileVis1 = vis1.GetValueOrDefault(tile);
if (tileVis1 != 0)
continue;
if (System.IsCorner(seedTiles, System._opaque, vis1, tile, Vector2i.UpRight) ||
System.IsCorner(seedTiles, System._opaque, vis1, tile, Vector2i.UpLeft) ||
System.IsCorner(seedTiles, System._opaque, vis1, tile, Vector2i.DownLeft) ||
System.IsCorner(seedTiles, System._opaque, vis1, tile, Vector2i.DownRight))
{
boundary.Add(tile);
}
}
// Make all wall/corner tiles visible
foreach (var tile in boundary)
{
vis1[tile] = -1;
}
if (TargetTile != null)
{
if (vis2.TryGetValue(TargetTile.Value, out var tileVis2))
{
DebugTools.Assert(seedTiles.Contains(TargetTile.Value));
if (tileVis2 != 0)
{
lock (System)
{
System.TargetFound = true;
return;
}
}
}
}
else
{
// vis2 is what we care about for LOS.
foreach (var tile in seedTiles)
{
// If not in viewport don't care.
if (!System._viewportTiles.Contains(tile))
continue;
var tileVis2 = vis2.GetValueOrDefault(tile, 0);
if (tileVis2 != 0)
{
// No idea if it's better to do this inside or out.
lock (VisibleTiles)
{
VisibleTiles.Add(tile);
}
}
}
}
}
}
}

View File

@@ -125,7 +125,9 @@ public sealed class UseDelaySystem : EntitySystem
/// </summary>
public UseDelayInfo GetLastEndingDelay(Entity<UseDelayComponent> ent)
{
var last = ent.Comp.Delays[DefaultId];
if (!ent.Comp.Delays.TryGetValue(DefaultId, out var last))
return new UseDelayInfo(TimeSpan.Zero);
foreach (var entry in ent.Comp.Delays)
{
if (entry.Value.EndTime > last.EndTime)

View File

@@ -1,9 +1,9 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Server.UserInterface;
namespace Content.Shared.UserInterface;
[RegisterComponent]
[RegisterComponent, NetworkedComponent]
public sealed partial class IntrinsicUIComponent : Component
{
/// <summary>
@@ -15,8 +15,8 @@ public sealed partial class IntrinsicUIComponent : Component
[DataDefinition]
public sealed partial class IntrinsicUIEntry
{
[DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>), required: true)]
public string? ToggleAction;
[DataField("toggleAction", required: true)]
public EntProtoId? ToggleAction;
/// <summary>
/// The action used for this BUI.

View File

@@ -1,14 +1,11 @@
using Content.Server.Actions;
using Content.Shared.UserInterface;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Content.Shared.Actions;
namespace Content.Server.UserInterface;
namespace Content.Shared.UserInterface;
public sealed class IntrinsicUISystem : EntitySystem
{
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
@@ -32,9 +29,9 @@ public sealed class IntrinsicUISystem : EntitySystem
}
}
public bool InteractUI(EntityUid uid, Enum key, IntrinsicUIComponent? iui = null, ActorComponent? actor = null)
public bool InteractUI(EntityUid uid, Enum key, IntrinsicUIComponent? iui = null)
{
if (!Resolve(uid, ref iui, ref actor))
if (!Resolve(uid, ref iui))
return false;
var attempt = new IntrinsicUIOpenAttemptEvent(uid, key);
@@ -42,7 +39,7 @@ public sealed class IntrinsicUISystem : EntitySystem
if (attempt.Cancelled)
return false;
return _uiSystem.TryToggleUi(uid, key, actor.PlayerSession);
return _uiSystem.TryToggleUi(uid, key, uid);
}
}

View File

@@ -484,5 +484,12 @@ Entries:
id: 60
time: '2024-08-19T03:02:18.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31097
- author: lzk228
changes:
- message: Admeme implanter injects and draws faster than default implanter.
type: Tweak
id: 61
time: '2024-08-23T00:11:28.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31045
Name: Admin
Order: 1

View File

@@ -1,237 +1,4 @@
Entries:
- author: Beck Thompson
changes:
- message: ID computer will now select passenger as the default id icon instead
of atmospheric technician.
type: Fix
id: 6660
time: '2024-06-01T17:29:46.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28462
- author: AJCM-git
changes:
- message: Machine parts are now stackable
type: Tweak
id: 6661
time: '2024-06-01T17:49:28.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28434
- author: DrSmugleaf
changes:
- message: Disabled the seeing rainbows screen effect when reduced motion is enabled
in the options menu.
type: Tweak
id: 6662
time: '2024-06-02T01:41:06.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28496
- author: Cojoke-dot
changes:
- message: Lasers now pass through Glass External Airlocks and Glass Shuttle Airlocks
type: Fix
id: 6663
time: '2024-06-02T04:13:12.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28065
- author: slarticodefast
changes:
- message: Fixed the flash effect getting darker with each appearence.
type: Fix
- message: Fixed short-sightedness making you immune to flashes.
type: Fix
id: 6664
time: '2024-06-02T04:17:53.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27369
- author: AJCM-git
changes:
- message: Midround antags work again
type: Fix
id: 6665
time: '2024-06-02T16:52:40.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28523
- author: Errant-4
changes:
- message: Map labels no longer suddenly disappear for the rest of the round.
type: Fix
id: 6666
time: '2024-06-02T17:30:27.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28518
- author: Whisper
changes:
- message: Renamed the player-obtainable "admin cloak" to "weh cloak".
type: Tweak
id: 6667
time: '2024-06-03T02:18:49.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28540
- author: Vermidia
changes:
- message: Glass shards are no longer weldable with an unlit welder.
type: Fix
id: 6668
time: '2024-06-03T03:28:53.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27959
- author: AJCM-git
changes:
- message: Biomass reclaimers no longer act as a void to any unfortunate belongings
a corpse may be wearing
type: Tweak
id: 6669
time: '2024-06-03T03:30:00.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28544
- author: Laneron
changes:
- message: Jetpack UI window now has correctly displayed header
type: Fix
id: 6670
time: '2024-06-03T03:33:31.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28545
- author: Vermidia
changes:
- message: SyndiCats now spawn from a radio like other reinforcements.
type: Tweak
id: 6671
time: '2024-06-03T11:54:32.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28492
- author: Cojoke-dot
changes:
- message: Medibots will now occasionally talk
type: Tweak
id: 6672
time: '2024-06-03T12:05:14.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28543
- author: Cojoke-dot
changes:
- message: The player can now use the gasping emote
type: Tweak
id: 6673
time: '2024-06-03T12:10:25.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28466
- author: Cojoke-dot
changes:
- message: Bullets no longer hit crates unless directly clicked on while shooting.
type: Tweak
- message: Bullets now pass over mobs that are lying down unless directly clicked
on while shooting.
type: Tweak
id: 6674
time: '2024-06-03T13:04:07.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28072
- author: AJCM-git
changes:
- message: Ninja stealth mode, stealth boxes and everything with stealth won't show
up in equipment HUDs like the medical or security HUD anymore.
type: Fix
id: 6675
time: '2024-06-03T16:12:21.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28270
- author: TheShuEd
changes:
- message: Added italian and cowboy accents
type: Add
- message: Now you can't choose all the accents at once.
type: Fix
id: 6676
time: '2024-06-03T18:47:06.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28046
- author: RenQ
changes:
- message: The moths now have their own "death grasp"
type: Add
id: 6677
time: '2024-06-03T18:48:01.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28409
- author: lzk228
changes:
- message: Borgs ID now will be shown in LogProbe instead of Unknown.
type: Tweak
id: 6678
time: '2024-06-03T18:48:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27788
- author: FairlySadPanda
changes:
- message: Stacking multiple deflecting items at once has been removed; now only
your best item is used.
type: Tweak
id: 6679
time: '2024-06-04T13:26:20.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28539
- author: moonheart08
changes:
- message: AME power output is no longer super-buffed. Larger stations will now
require multiple engines to stay powered.
type: Tweak
id: 6680
time: '2024-06-04T15:59:33.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28419
- author: blueDev2
changes:
- message: Fixed microwave recipes that use stacked materials
type: Fix
id: 6681
time: '2024-06-04T17:12:01.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28225
- author: Tayrtahn
changes:
- message: Mops and rags now show the liquids they soak up.
type: Add
id: 6682
time: '2024-06-04T18:48:24.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28590
- author: Boaz1111
changes:
- message: Revamped Cluster's Head Offices
type: Tweak
id: 6683
time: '2024-06-05T17:50:38.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28627
- author: Plykiya
changes:
- message: Handcuff range is buffed to one tile of distance.
type: Tweak
id: 6684
time: '2024-06-05T20:14:56.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28576
- author: Cojoke-dot
changes:
- message: A variety of objects around the station now need to be clicked on in
order for bullets to hit them.
type: Tweak
id: 6685
time: '2024-06-05T20:47:28.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28571
- author: ps3moira
changes:
- message: Lit cigars inhand sprites are now visible.
type: Fix
id: 6686
time: '2024-06-05T21:16:32.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28641
- author: Tayrtahn
changes:
- message: Slimes, diona, and kudzu are less dramatically affected by having certain
chemicals splashed on them.
type: Tweak
id: 6687
time: '2024-06-05T22:08:41.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28591
- author: Plykiya
changes:
- message: Internals are no longer toggled off if you take your helmet off but still
have a gas mask on and vice versa.
type: Tweak
id: 6688
time: '2024-06-06T07:01:45.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28595
- author: Cojoke-dot
changes:
- message: Bots now have Insulation and NoSlip
type: Tweak
id: 6689
time: '2024-06-06T09:32:37.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28621
- author: Flareguy
changes:
- message: Nerfed the explosive power of fuel tanks.
type: Tweak
id: 6690
time: '2024-06-06T10:08:25.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28650
- author: ElectroJr
changes:
- message: The job/antag preferences window now has some buttons that link to relevant
@@ -3857,3 +3624,244 @@
id: 7159
time: '2024-08-19T03:39:00.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30802
- author: lzk228
changes:
- message: Cheese wheel now Normal sized and slices in 4 slices instead of 3.
type: Tweak
id: 7160
time: '2024-08-19T07:54:35.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31168
- author: Beck Thompson
changes:
- message: Slightly increased the price of the seed restock.
type: Tweak
id: 7161
time: '2024-08-19T10:33:41.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31185
- author: Boaz1111
changes:
- message: The HoS has been given a new experimental weapon:An energy shotgun with
multiple fire modes. This experimental tech is highly wanted by the syndicate,
and agents will try to steal it.
type: Add
- message: Removed the Head of Security's Emergency Orders.
type: Remove
id: 7162
time: '2024-08-19T11:14:30.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30643
- author: tosatur
changes:
- message: Added more words to chat sanitisation
type: Tweak
id: 7163
time: '2024-08-19T14:03:07.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31085
- author: tosatur
changes:
- message: Changed text for ghost visibility toggle
type: Tweak
id: 7164
time: '2024-08-19T17:51:55.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30998
- author: themias
changes:
- message: Lizards can now eat meat dumplings.
type: Fix
id: 7165
time: '2024-08-19T18:17:28.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31212
- author: slarticodefast
changes:
- message: Made the straitjacked non-printable again.
type: Remove
id: 7166
time: '2024-08-19T18:47:24.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31197
- author: themias
changes:
- message: Added the Combat Bakery Kit to the syndicate uplink. Includes a deadly
edible baguette sword and throwing star croissants for 3 TC.
type: Add
- message: Baguettes can be worn on the belt and normal ones do one blunt damage.
type: Tweak
- message: Mimes have a baguette instead of an MRE in their survival kits.
type: Tweak
id: 7167
time: '2024-08-19T18:57:30.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31179
- author: metalgearsloth
changes:
- message: Fix wire sounds not playing.
type: Fix
id: 7168
time: '2024-08-19T19:50:03.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31067
- author: redmushie
changes:
- message: Communications console buttons now have descriptive tooltips
type: Add
id: 7169
time: '2024-08-19T20:36:36.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31217
- author: Mephisto72
changes:
- message: The Detective can now access Externals and Cryogenics like their Officer
counterpart.
type: Tweak
id: 7170
time: '2024-08-19T23:44:51.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30203
- author: Vermidia
changes:
- message: Mothroaches can now wear anything hamlet can wear
type: Add
id: 7171
time: '2024-08-20T04:20:12.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28956
- author: Cojoke-dot
changes:
- message: Devouring bodies now reduces bleed for Space Dragons.
type: Tweak
id: 7172
time: '2024-08-20T10:57:06.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29661
- author: cranberriez
changes:
- message: Added Discord webhook error logging!
type: Tweak
id: 7173
time: '2024-08-20T21:12:31.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30835
- author: Repo
changes:
- message: Added copy to clipboard button for connection failure UI.
type: Add
id: 7174
time: '2024-08-20T21:31:10.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30760
- author: Lank
changes:
- message: Removed the shock collar and associated thief objective.
type: Remove
id: 7175
time: '2024-08-21T17:56:57.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31229
- author: EmoGarbage404
changes:
- message: Increased the view range on the handheld mass scanner and decreased the
power draw.
type: Tweak
- message: Handheld radars no longer spin with the player while moving.
type: Fix
id: 7176
time: '2024-08-21T18:33:01.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31284
- author: Beck Thompson
changes:
- message: Nuclear authentication code folder that spawns the nuclear authentication
codes when opened.
type: Add
id: 7177
time: '2024-08-21T18:53:04.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31272
- author: Brandon-Huu
changes:
- message: Nukie shuttle now spawns with the nuclear authentication code folder.
This fixes the issue where the nuclear codes would only ever have the codes
for the nuclear operatives nuke and not the stations.
type: Fix
id: 7178
time: '2024-08-21T18:55:18.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31273
- author: Winkarst-cpu
changes:
- message: Now getting creamed will not reveal a person's identity.
type: Fix
id: 7179
time: '2024-08-21T21:50:34.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31291
- author: Sarahon
changes:
- message: Now can add head(top) cosmetics to humanoids and dwarfs.
type: Add
- message: Added "long ears" for human and dwarf head(top) cosmetic.
type: Add
id: 7180
time: '2024-08-21T23:44:43.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30490
- author: Winkarst-cpu
changes:
- message: Now vending machines show valid icons.
type: Fix
id: 7181
time: '2024-08-22T14:40:39.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30064
- author: EmoGarbage404
changes:
- message: Jackboots now reduce slowness from injuries by 50%.
type: Add
- message: Removed combat boots from the security loadout.
type: Remove
id: 7182
time: '2024-08-22T14:56:47.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30586
- author: metalgearsloth
changes:
- message: Fix the inventory GUI being visible when you don't have an inventory.
type: Fix
id: 7183
time: '2024-08-22T17:05:17.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31306
- author: EmoGarbage404
changes:
- message: Moved the mining asteroid slightly closer to the station.
type: Tweak
- message: Things pulled in by the salvage magnet should spawn closer to the station
at a more consistent distance.
type: Tweak
id: 7184
time: '2024-08-22T19:29:56.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31296
- author: lzk228
changes:
- message: Removed names from implanters. No more meta. (You still can see which
implant is inside via examining or holding implanter in hand).
type: Tweak
id: 7185
time: '2024-08-23T00:11:28.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31045
- author: lzk228
changes:
- message: Added second jester suit and hat in clown loadouts.
type: Add
id: 7186
time: '2024-08-23T01:29:51.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30673
- author: Dutch-VanDerLinde
changes:
- message: Added the clown skirt
type: Add
id: 7187
time: '2024-08-23T04:59:18.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31207
- author: Aquif
changes:
- message: Boiling Vox blood results in ammonia.
type: Add
id: 7188
time: '2024-08-23T05:05:46.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30749
- author: Sarahon
changes:
- message: Brand new emote sprites and colours to the Y menu.
type: Add
id: 7189
time: '2024-08-23T05:08:10.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30887
- author: jimmy12or
changes:
- message: Added a recipe for cream. Heat some milk and shake some air into it.
type: Add
id: 7190
time: '2024-08-23T05:13:14.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30503

View File

@@ -2,9 +2,13 @@
comms-console-menu-title = Communications Console
comms-console-menu-announcement-placeholder = Announcement text...
comms-console-menu-announcement-button = Announce
comms-console-menu-announcement-button-tooltip = Send your message as a station-wide radio announcement.
comms-console-menu-broadcast-button = Broadcast
comms-console-menu-broadcast-button-tooltip = Broadcast your message to wall-mounted screens around the station. Note: They fit only ten characters!
comms-console-menu-alert-level-button-tooltip = Change the station alert level. Applies immediately on selecting.
comms-console-menu-call-shuttle = Call emergency shuttle
comms-console-menu-recall-shuttle = Recall emergency shuttle
comms-console-menu-emergency-shuttle-button-tooltip = Calls or recalls the emergency shuttle. You can only recall when there's enough time left.
comms-console-menu-time-remaining = Time remaining: {$time}
# Popup

View File

@@ -1 +1,2 @@
melee-stamina = Not enough stamina
slow-on-damage-modifier-examine = Slowness from injuries is reduced by [color=yellow]{$mod}%[/color]

View File

@@ -1,7 +1,8 @@
ghost-gui-return-to-body-button = Return to body
ghost-gui-ghost-warp-button = Ghost Warp
ghost-gui-ghost-roles-button = Ghost Roles ({$count})
ghost-gui-toggle-ghost-visibility-popup = Toggled visibility of ghosts.
ghost-gui-toggle-ghost-visibility-popup-on = Enabled visibility of ghosts.
ghost-gui-toggle-ghost-visibility-popup-off = Disabled visibility of ghosts.
ghost-gui-toggle-lighting-manager-popup = Toggled all lighting.
ghost-gui-toggle-fov-popup = Toggled field-of-view.

View File

@@ -2,7 +2,7 @@
implanter-component-implanting-target = {$user} is trying to implant you with something!
implanter-component-implant-failed = The {$implant} cannot be given to {$target}!
implanter-draw-failed-permanent = The {$implant} in {$target} is fused with them and cannot be removed!
implanter-draw-failed-permanent = The {$implant} in {$target} is fused with { OBJECT($target) } and cannot be removed!
implanter-draw-failed = You tried to remove an implant but found nothing.
implanter-component-implant-already = {$target} already has the {$implant}!

View File

@@ -82,3 +82,6 @@ ban-panel-erase = Erase chat messages and player from round
server-ban-string = {$admin} created a {$severity} severity server ban that expires {$expires} for [{$name}, {$ip}, {$hwid}], with reason: {$reason}
server-ban-string-no-pii = {$admin} created a {$severity} severity server ban that expires {$expires} for {$name} with reason: {$reason}
server-ban-string-never = never
# Kick on ban
ban-kick-reason = You have been banned

View File

@@ -8,6 +8,8 @@ role-timer-age-to-old = Your character's age must be at most [color=yellow]{$age
role-timer-age-to-young = Your character's age must be at least [color=yellow]{$age}[/color] to play this role.
role-timer-whitelisted-species = Your character must be one of the following species to play this role:
role-timer-blacklisted-species = Your character must not be one of the following species to play this role:
role-timer-whitelisted-traits = Your character must have one of the following traits:
role-timer-blacklisted-traits = Your character must not have any of the following traits:
role-timer-locked = Locked (hover for details)

View File

@@ -4,6 +4,7 @@ connecting-title = Space Station 14
connecting-exit = Exit
connecting-retry = Retry
connecting-reconnect = Reconnect
connecting-copy = Copy Message
connecting-redial = Relaunch
connecting-redial-wait = Please wait: { TOSTRING($time, "G3") }
connecting-in-progress = Connecting to server...

View File

@@ -0,0 +1 @@
marking-HumanLongEars = Long Ears

View File

@@ -15,6 +15,7 @@ loadout-group-survival-syndicate = Github is forcing me to write text that is li
loadout-group-breath-tool = Species-dependent breath tools
loadout-group-tank-harness = Species-specific survival equipment
loadout-group-EVA-tank = Species-specific gas tank
loadout-group-survival-mime = Mime Survival Box
# Command
loadout-group-captain-head = Captain head

View File

@@ -14,6 +14,7 @@ marking-slot = Slot {$number}
# Categories
markings-category-Special = Special
markings-category-Hair = Hair
markings-category-FacialHair = Facial Hair
markings-category-Head = Head

View File

@@ -1,4 +1,5 @@
sandbox-window-title = Sandbox Panel
sandbox-window-ai-overlay-button = AI Overlay
sandbox-window-respawn-button = Respawn
sandbox-window-spawn-entities-button = Spawn Entities
sandbox-window-spawn-tiles-button = Spawn Tiles

View File

@@ -156,3 +156,36 @@ chatsan-replacement-55 = not gonna lie
chatsan-word-56 = fml
chatsan-replacement-56 = fuck my life
chatsan-word-57 = wtaf
chatsan-replacement-57 = what the actual fuck
chatsan-word-58 = wsg
chatsan-replacement-58 = what's good
chatsan-word-59 = mb
chatsan-replacement-59 = my bad
chatsan-word-60 = jfc
chatsan-replacement-60 = jesus fucking christ
chatsan-word-61 = omw
chatsan-replacement-61 = on my way
chatsan-word-62 = otw
chatsan-replacement-62 = on the way
chatsan-word-63 = yk
chatsan-replacement-63 = you know
chatsan-word-64 = istfg
chatsan-replacement-64 = i swear to fucking god
chatsan-word-65 = idgaf
chatsan-replacement-65 = i don't give a fuck
chatsan-word-66 = smth
chatsan-replacement-66 = something
chatsan-word-67 = allg
chatsan-replacement-67 = all good

View File

@@ -439,3 +439,6 @@ uplink-barber-scissors-desc = A good tool to give your fellow agent a nice hairc
uplink-backpack-syndicate-name = Syndicate backpack
uplink-backpack-syndicate-desc = Lightweight explosion-proof а backpack for holding various traitor goods
uplink-combat-bakery-name = Combat Bakery Kit
uplink-combat-bakery-desc = A kit of clandestine baked weapons. Contains a baguette which a skilled mime could use as a sword and a pair of throwing croissants. Once the job is done, eat the evidence.

View File

@@ -3733,7 +3733,7 @@ entities:
- type: Transform
pos: -2.5,-12.5
parent: 1
- proto: NukeCodePaper
- proto: BoxFolderNuclearCodes
entities:
- uid: 510
components:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -30424,6 +30424,11 @@ entities:
- type: Transform
pos: -16.498352,-16.302273
parent: 13329
- uid: 15721
components:
- type: Transform
pos: 35.299934,68.63487
parent: 13329
- uid: 18968
components:
- type: Transform
@@ -30484,13 +30489,6 @@ entities:
- type: Transform
pos: 4.460208,71.69925
parent: 13329
- proto: BoxSurvivalSyndicate
entities:
- uid: 15335
components:
- type: Transform
pos: 55.45186,-22.33047
parent: 13329
- proto: BoxSyringe
entities:
- uid: 18481
@@ -93569,13 +93567,6 @@ entities:
- type: Transform
pos: 17.46724,68.57425
parent: 13329
- proto: ClothingBeltMilitaryWebbing
entities:
- uid: 34561
components:
- type: Transform
pos: 67.49193,-41.442646
parent: 13329
- proto: ClothingBeltSecurityFilled
entities:
- uid: 10775
@@ -94300,11 +94291,6 @@ entities:
- type: Transform
pos: 53.29546,-42.48607
parent: 13329
- uid: 34626
components:
- type: Transform
pos: 36.680786,68.70615
parent: 13329
- proto: ClothingHeadHelmetRiot
entities:
- uid: 10654
@@ -94574,13 +94560,6 @@ entities:
- type: Transform
pos: -23.522526,76.500786
parent: 13329
- proto: ClothingNeckCloakHerald
entities:
- uid: 15720
components:
- type: Transform
pos: 62.5,-57.5
parent: 13329
- proto: ClothingNeckCloakMiner
entities:
- uid: 33335
@@ -94768,13 +94747,6 @@ entities:
- type: Transform
pos: -9.515972,-16.273252
parent: 13329
- proto: ClothingOuterCoatDetectiveLoadout
entities:
- uid: 31694
components:
- type: Transform
pos: -28.586462,68.64356
parent: 13329
- proto: ClothingOuterCoatLab
entities:
- uid: 19754
@@ -94782,6 +94754,13 @@ entities:
- type: Transform
pos: 32.341858,-50.533268
parent: 13329
- proto: ClothingOuterCoatTrench
entities:
- uid: 15720
components:
- type: Transform
pos: -29.56662,69.60553
parent: 13329
- proto: ClothingOuterGhostSheet
entities:
- uid: 34242
@@ -94802,11 +94781,6 @@ entities:
- type: Transform
pos: 53.592335,-42.45482
parent: 13329
- uid: 34205
components:
- type: Transform
pos: 36.407623,68.61223
parent: 13329
- proto: ClothingOuterHardsuitSecurity
entities:
- uid: 9436
@@ -94949,13 +94923,6 @@ entities:
- type: Transform
pos: 70.55321,-5.514928
parent: 13329
- proto: ClothingShoesBootsMagSyndie
entities:
- uid: 34793
components:
- type: Transform
pos: 67.44477,-42.60581
parent: 13329
- proto: ClothingShoesBootsSalvage
entities:
- uid: 31680
@@ -163657,6 +163624,11 @@ entities:
- type: Transform
pos: 19.5,-22.5
parent: 13329
- uid: 20554
components:
- type: Transform
pos: 36.5,68.5
parent: 13329
- uid: 29283
components:
- type: Transform
@@ -163786,6 +163758,11 @@ entities:
parent: 13329
- proto: MaintenanceToolSpawner
entities:
- uid: 15335
components:
- type: Transform
pos: 62.5,-57.5
parent: 13329
- uid: 31817
components:
- type: Transform
@@ -163848,6 +163825,16 @@ entities:
parent: 13329
- proto: MaintenanceWeaponSpawner
entities:
- uid: 20553
components:
- type: Transform
pos: 67.5,-41.5
parent: 13329
- uid: 20555
components:
- type: Transform
pos: 79.5,10.5
parent: 13329
- uid: 21278
components:
- type: Transform
@@ -163906,18 +163893,6 @@ entities:
- type: Transform
pos: 46.429523,54.487045
parent: 13329
- proto: MaterialReclaimer
entities:
- uid: 6766
components:
- type: Transform
pos: 125.5,-1.5
parent: 13329
- uid: 13314
components:
- type: Transform
pos: 15.5,-27.5
parent: 13329
- proto: MaterialWoodPlank
entities:
- uid: 32167
@@ -164275,13 +164250,6 @@ entities:
- type: Transform
pos: 82.5,54.5
parent: 13329
- proto: MirrorShield
entities:
- uid: 15721
components:
- type: Transform
pos: 62.5,-57.5
parent: 13329
- proto: MonkeyCubeWrapped
entities:
- uid: 1300
@@ -173050,6 +173018,11 @@ entities:
rot: 3.141592653589793 rad
pos: -36.5,-44.5
parent: 13329
- uid: 13314
components:
- type: Transform
pos: 36.5,68.5
parent: 13329
- uid: 13699
components:
- type: Transform
@@ -173510,11 +173483,6 @@ entities:
- type: Transform
pos: 34.5,66.5
parent: 13329
- uid: 34203
components:
- type: Transform
pos: 36.5,68.5
parent: 13329
- uid: 34208
components:
- type: Transform
@@ -180580,8 +180548,14 @@ entities:
- type: MetaData
name: Mailroom Garage Door Opener
- type: Transform
pos: 21.53215,-13.842299
pos: 21.52355,-14.18487
parent: 13329
- type: DeviceLinkSource
linkedPorts:
9596:
- Pressed: Toggle
9595:
- Pressed: Toggle
- uid: 31585
components:
- type: Transform
@@ -180656,13 +180630,6 @@ entities:
- type: Transform
pos: 21.482563,-11.35629
parent: 13329
- proto: SalvageLootSpawner
entities:
- uid: 29379
components:
- type: Transform
pos: 79.5,10.5
parent: 13329
- proto: SalvageMagnet
entities:
- uid: 11642
@@ -195952,6 +195919,11 @@ entities:
- type: Transform
pos: 8.5,5.5
parent: 13329
- uid: 6766
components:
- type: Transform
pos: 67.5,-42.5
parent: 13329
- uid: 29363
components:
- type: Transform

View File

@@ -10999,7 +10999,7 @@ entities:
pos: -20.5,-5.5
parent: 30
- type: Door
secondsUntilStateChange: -21816.006
secondsUntilStateChange: -22976.215
state: Opening
- type: DeviceLinkSource
lastSignals:
@@ -13022,6 +13022,11 @@ entities:
rot: -1.5707963267948966 rad
pos: -65.5,13.5
parent: 30
- uid: 4393
components:
- type: Transform
pos: -52.5,2.5
parent: 30
- uid: 11451
components:
- type: Transform
@@ -13039,6 +13044,23 @@ entities:
rot: -1.5707963267948966 rad
pos: -65.5,21.5
parent: 30
- uid: 20731
components:
- type: Transform
rot: 3.141592653589793 rad
pos: -59.5,-7.5
parent: 30
- uid: 20732
components:
- type: Transform
rot: 3.141592653589793 rad
pos: -52.5,-7.5
parent: 30
- uid: 20733
components:
- type: Transform
pos: -59.5,2.5
parent: 30
- proto: AtmosDeviceFanTiny
entities:
- uid: 9123
@@ -50356,13 +50378,6 @@ entities:
- type: Transform
pos: 12.458031,22.694685
parent: 30
- proto: ClothingBeltMilitaryWebbing
entities:
- uid: 15115
components:
- type: Transform
pos: 33.456085,32.56812
parent: 30
- proto: ClothingBeltPlantFilled
entities:
- uid: 3498
@@ -50504,11 +50519,6 @@ entities:
- type: Transform
pos: -48.531364,30.586998
parent: 30
- uid: 15265
components:
- type: Transform
pos: 47.465996,44.649086
parent: 30
- proto: ClothingEyesGlassesThermal
entities:
- uid: 9397
@@ -50987,20 +50997,6 @@ entities:
- type: Transform
pos: 7.503625,43.354942
parent: 30
- proto: ClothingNeckCloakGoliathCloak
entities:
- uid: 10443
components:
- type: Transform
pos: 41.46685,-71.67568
parent: 30
- proto: ClothingNeckCloakMiner
entities:
- uid: 12304
components:
- type: Transform
pos: 38.513725,-64.51438
parent: 30
- proto: ClothingNeckCloakTrans
entities:
- uid: 3344
@@ -51143,13 +51139,6 @@ entities:
- type: Transform
pos: -39.286102,56.736275
parent: 30
- proto: ClothingOuterArmorCult
entities:
- uid: 18249
components:
- type: Transform
pos: -72.51071,-48.502575
parent: 30
- proto: ClothingOuterArmorReflective
entities:
- uid: 16932
@@ -51181,13 +51170,6 @@ entities:
- type: Transform
pos: 40.490578,30.452217
parent: 30
- proto: ClothingOuterCoatDetectiveLoadout
entities:
- uid: 11366
components:
- type: Transform
pos: 4.503509,-14.329939
parent: 30
- proto: ClothingOuterCoatGentle
entities:
- uid: 16947
@@ -51230,6 +51212,13 @@ entities:
- type: Transform
pos: 19.488726,8.340895
parent: 30
- proto: ClothingOuterCoatTrench
entities:
- uid: 15265
components:
- type: Transform
pos: 4.48031,-14.490032
parent: 30
- proto: ClothingOuterHardsuitEVAPrisoner
entities:
- uid: 2381
@@ -51304,20 +51293,6 @@ entities:
- type: Transform
pos: -9.241632,-47.33087
parent: 30
- proto: ClothingOuterVestWeb
entities:
- uid: 16931
components:
- type: Transform
pos: -40.528282,22.694292
parent: 30
- proto: ClothingOuterVestWebMerc
entities:
- uid: 16034
components:
- type: Transform
pos: 16.536156,36.4687
parent: 30
- proto: ClothingOuterWinterSec
entities:
- uid: 11426
@@ -51500,6 +51475,13 @@ entities:
- type: Transform
pos: -56.564133,39.58168
parent: 30
- proto: ClothingUniformJumpskirtOldDress
entities:
- uid: 15115
components:
- type: Transform
pos: -72.460976,-48.531662
parent: 30
- proto: ClothingUniformJumpskirtPerformer
entities:
- uid: 15070
@@ -53203,6 +53185,13 @@ entities:
- type: Transform
pos: -33.36232,-13.359482
parent: 30
- proto: CultAltarSpawner
entities:
- uid: 11366
components:
- type: Transform
pos: 38.5,-67.5
parent: 30
- proto: d20Dice
entities:
- uid: 19470
@@ -98455,6 +98444,11 @@ entities:
- type: Transform
pos: -3.5,-16.5
parent: 30
- uid: 10357
components:
- type: Transform
pos: 33.5,32.5
parent: 30
- uid: 13449
components:
- type: Transform
@@ -98596,6 +98590,11 @@ entities:
- type: Transform
pos: 46.5,45.5
parent: 30
- uid: 16034
components:
- type: Transform
pos: 39.5,-67.5
parent: 30
- uid: 16218
components:
- type: Transform
@@ -98675,13 +98674,6 @@ entities:
- type: Transform
pos: -24.501097,-36.300484
parent: 30
- proto: MaterialReclaimer
entities:
- uid: 4393
components:
- type: Transform
pos: 30.5,-7.5
parent: 30
- proto: MaterialWoodPlank
entities:
- uid: 15132
@@ -110462,13 +110454,6 @@ entities:
- type: Transform
pos: 33.5,15.5
parent: 30
- proto: RitualDagger
entities:
- uid: 10357
components:
- type: Transform
pos: 39.487907,-67.53489
parent: 30
- proto: SalvageMagnet
entities:
- uid: 2040
@@ -110477,6 +110462,18 @@ entities:
rot: 1.5707963267948966 rad
pos: 44.5,-18.5
parent: 30
- proto: SalvageSpawnerScrapCommon
entities:
- uid: 10443
components:
- type: Transform
pos: 41.5,-73.5
parent: 30
- uid: 12304
components:
- type: Transform
pos: 38.5,-64.5
parent: 30
- proto: Scalpel
entities:
- uid: 21253
@@ -116460,11 +116457,6 @@ entities:
- SurveillanceCameraCommand
nameSet: True
id: Vault
- uid: 21183
components:
- type: Transform
pos: -0.5,19.5
parent: 30
- uid: 21286
components:
- type: Transform

View File

@@ -10804,7 +10804,7 @@ entities:
pos: -42.5,29.5
parent: 5350
- type: Door
secondsUntilStateChange: -1271.2194
secondsUntilStateChange: -2186.3599
state: Opening
- type: DeviceLinkSink
invokeCounter: 2
@@ -34442,13 +34442,6 @@ entities:
- type: Transform
pos: -29.53436,40.699295
parent: 5350
- proto: Cablecuffs
entities:
- uid: 22690
components:
- type: Transform
pos: 38.5058,-36.452374
parent: 5350
- proto: CableHV
entities:
- uid: 1211
@@ -60128,11 +60121,6 @@ entities:
- type: Transform
pos: 22.521395,-12.417425
parent: 5350
- uid: 19020
components:
- type: Transform
pos: 66.56803,33.64219
parent: 5350
- proto: ClothingEyesGlassesThermal
entities:
- uid: 23947
@@ -60785,13 +60773,6 @@ entities:
- type: Transform
pos: 6.498452,-14.246428
parent: 5350
- proto: ClothingNeckCloakHerald
entities:
- uid: 24179
components:
- type: Transform
pos: -32.42196,-65.44334
parent: 5350
- proto: ClothingNeckCloakTrans
entities:
- uid: 24116
@@ -60996,13 +60977,6 @@ entities:
- type: Transform
pos: -54.487633,-18.620468
parent: 5350
- proto: ClothingOuterCoatDetectiveLoadout
entities:
- uid: 23129
components:
- type: Transform
pos: -22.593464,38.504482
parent: 5350
- proto: ClothingOuterCoatGentle
entities:
- uid: 13742
@@ -61031,6 +61005,13 @@ entities:
- type: Transform
pos: -5.527274,-39.534935
parent: 5350
- proto: ClothingOuterCoatTrench
entities:
- uid: 16785
components:
- type: Transform
pos: -23.450022,38.487732
parent: 5350
- proto: ClothingOuterHardsuitEVAPrisoner
entities:
- uid: 8249
@@ -115293,11 +115274,6 @@ entities:
- type: Transform
pos: 27.507141,-21.391407
parent: 5350
- uid: 22700
components:
- type: Transform
pos: 39.5683,-36.56175
parent: 5350
- proto: HydroponicsToolMiniHoe
entities:
- uid: 3137
@@ -116364,29 +116340,6 @@ entities:
- type: Transform
pos: 24.5,-13.5
parent: 5350
- uid: 16785
components:
- type: Transform
pos: -56.5,-23.5
parent: 5350
- type: EntityStorage
air:
volume: 200
immutable: False
temperature: 293.1495
moles:
- 3.848459
- 14.477538
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- proto: LockerBotanistFilled
entities:
- uid: 2618
@@ -117695,6 +117648,11 @@ entities:
parent: 5350
- proto: MachineFrame
entities:
- uid: 22686
components:
- type: Transform
pos: 41.5,-31.5
parent: 5350
- uid: 26436
components:
- type: Transform
@@ -117765,6 +117723,11 @@ entities:
- type: Transform
pos: 52.5,36.5
parent: 5350
- uid: 19020
components:
- type: Transform
pos: 37.5,-33.5
parent: 5350
- uid: 22114
components:
- type: Transform
@@ -117839,6 +117802,11 @@ entities:
- type: Transform
pos: 36.5,-51.5
parent: 5350
- uid: 22690
components:
- type: Transform
pos: 37.5,-36.5
parent: 5350
- uid: 22702
components:
- type: Transform
@@ -117861,6 +117829,11 @@ entities:
parent: 5350
- proto: MaintenanceWeaponSpawner
entities:
- uid: 5341
components:
- type: Transform
pos: 38.5,-36.5
parent: 5350
- uid: 17307
components:
- type: Transform
@@ -117931,13 +117904,6 @@ entities:
- type: Transform
pos: -17.38306,-3.5393329
parent: 5350
- proto: MaterialReclaimer
entities:
- uid: 5341
components:
- type: Transform
pos: -37.5,22.5
parent: 5350
- proto: MaterialWoodPlank1
entities:
- uid: 22703
@@ -143622,11 +143588,6 @@ entities:
- type: Transform
pos: 4.5,-49.5
parent: 5350
- uid: 22686
components:
- type: Transform
pos: 41.5,-31.5
parent: 5350
- uid: 26437
components:
- type: Transform
@@ -167150,13 +167111,6 @@ entities:
- type: Transform
pos: -23.5,-63.5
parent: 5350
- proto: WoodenBuckler
entities:
- uid: 22704
components:
- type: Transform
pos: 37.46614,-33.453953
parent: 5350
- proto: Wrench
entities:
- uid: 1287

View File

@@ -139087,13 +139087,6 @@ entities:
- type: Transform
pos: 41.613995,-4.120341
parent: 2
- proto: MaterialReclaimer
entities:
- uid: 3264
components:
- type: Transform
pos: -54.5,-30.5
parent: 2
- proto: MatterBinStockPart
entities:
- uid: 23592

View File

@@ -56894,13 +56894,6 @@ entities:
- type: Transform
pos: 8.48969,29.428799
parent: 4812
- proto: MaterialReclaimer
entities:
- uid: 4807
components:
- type: Transform
pos: 20.5,19.5
parent: 4812
- proto: MaterialWoodPlank
entities:
- uid: 8415
@@ -58630,7 +58623,7 @@ entities:
- uid: 10860
components:
- type: Transform
pos: -22.5,-1.5
pos: -17.5,1.5
parent: 4812
- type: ContainerContainer
containers:
@@ -70309,6 +70302,13 @@ entities:
- type: Transform
pos: -37.5,-33.5
parent: 4812
- proto: VendingMachineWinter
entities:
- uid: 4807
components:
- type: Transform
pos: -22.5,-1.5
parent: 4812
- proto: VendingMachineYouTool
entities:
- uid: 8577

View File

@@ -60401,13 +60401,6 @@ entities:
- type: Transform
pos: 18.624886,-3.516942
parent: 2
- proto: MaterialReclaimer
entities:
- uid: 5877
components:
- type: Transform
pos: 3.5,-32.5
parent: 2
- proto: MaterialWoodPlank
entities:
- uid: 2898

File diff suppressed because it is too large Load Diff

View File

@@ -49397,13 +49397,6 @@ entities:
- type: Transform
pos: 7.2571726,18.545927
parent: 31
- proto: MaterialReclaimer
entities:
- uid: 9264
components:
- type: Transform
pos: -32.5,-17.5
parent: 31
- proto: MaterialWoodPlank1
entities:
- uid: 9680

View File

@@ -78002,13 +78002,6 @@ entities:
- type: Transform
pos: 2.6536188,-121.53795
parent: 2
- proto: MaterialReclaimer
entities:
- uid: 11482
components:
- type: Transform
pos: 6.5,-276.5
parent: 2
- proto: MaterialWoodPlank10
entities:
- uid: 11483

View File

@@ -560,6 +560,17 @@
# chatsan-word-54: chatsan-replacement-54
# chatsan-word-55: chatsan-replacement-55
# chatsan-word-56: chatsan-replacement-56
# chatsan-word-57: chatsan-replacement-57
# chatsan-word-58: chatsan-replacement-58
# chatsan-word-59: chatsan-replacement-59
# chatsan-word-60: chatsan-replacement-60
# chatsan-word-61: chatsan-replacement-61
# chatsan-word-62: chatsan-replacement-62
# chatsan-word-63: chatsan-replacement-63
# chatsan-word-64: chatsan-replacement-64
# chatsan-word-65: chatsan-replacement-65
# chatsan-word-66: chatsan-replacement-66
# chatsan-word-67: chatsan-replacement-67
corvax-chatsan-word-1: corvax-chatsan-replacement-1
corvax-chatsan-word-2: corvax-chatsan-replacement-2
corvax-chatsan-word-3: corvax-chatsan-replacement-3

View File

@@ -27,7 +27,7 @@
- type: entity
id: AlertSpriteView
categories: [ HideSpawnMenu ]
categories: [ HideSpawnMenu ]
components:
- type: Sprite
layers:

View File

@@ -221,7 +221,7 @@
- type: cargoProduct
id: LivestockMothroach
icon:
sprite: Mobs/Animals/mothroach.rsi
sprite: Mobs/Animals/mothroach/mothroach.rsi
state: mothroach
product: CrateNPCMothroach
cost: 5000

View File

@@ -174,7 +174,7 @@
sprite: Objects/Specific/Service/vending_machine_restock.rsi
state: base
product: CrateVendingMachineRestockSeedsFilled
cost: 3470
cost: 3600
category: cargoproduct-category-name-hydroponics
group: market

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