mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-02-15 00:54:51 +01:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
119
Content.Client/Silicons/StationAi/StationAiOverlay.cs
Normal file
119
Content.Client/Silicons/StationAi/StationAiOverlay.cs
Normal 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);
|
||||
|
||||
}
|
||||
}
|
||||
80
Content.Client/Silicons/StationAi/StationAiSystem.cs
Normal file
80
Content.Client/Silicons/StationAi/StationAiSystem.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}"/>
|
||||
|
||||
16
Content.Client/VendingMachines/UI/VendingMachineItem.xaml
Normal file
16
Content.Client/VendingMachines/UI/VendingMachineItem.xaml
Normal 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>
|
||||
19
Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs
Normal file
19
Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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
|
||||
|
||||
1913
Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.Designer.cs
generated
Normal file
1913
Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
90
Content.Server/Database/BanMatcher.cs
Normal file
90
Content.Server/Database/BanMatcher.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
121
Content.Server/Database/ServerDbPostgres.Notifications.cs
Normal file
121
Content.Server/Database/ServerDbPostgres.Notifications.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
67
Content.Shared/Roles/JobRequirement/TraitsRequirement.cs
Normal file
67
Content.Shared/Roles/JobRequirement/TraitsRequirement.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ public sealed class NavInterfaceState
|
||||
|
||||
public Dictionary<NetEntity, List<DockingPortState>> Docks;
|
||||
|
||||
public bool RotateWithEntity = true;
|
||||
|
||||
public NavInterfaceState(
|
||||
float maxRange,
|
||||
NetCoordinates? coordinates,
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
522
Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs
Normal file
522
Content.Shared/Silicons/StationAi/StationAiVisionSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
melee-stamina = Not enough stamina
|
||||
slow-on-damage-modifier-examine = Slowness from injuries is reduced by [color=yellow]{$mod}%[/color]
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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}!
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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...
|
||||
|
||||
1
Resources/Locale/en-US/markings/ears.ftl
Normal file
1
Resources/Locale/en-US/markings/ears.ftl
Normal file
@@ -0,0 +1 @@
|
||||
marking-HumanLongEars = Long Ears
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
17928
Resources/Maps/cog.yml
17928
Resources/Maps/cog.yml
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
- type: entity
|
||||
id: AlertSpriteView
|
||||
categories: [ HideSpawnMenu ]
|
||||
categories: [ HideSpawnMenu ]
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user