Merge remote-tracking branch 'refs/remotes/wizards/master'

# Conflicts:
#	Content.Client/Launcher/LauncherConnecting.cs
#	Content.Client/Launcher/LauncherConnectingGui.xaml
#	Content.Client/Launcher/LauncherConnectingGui.xaml.cs
#	Content.Server/Administration/Managers/BanManager.cs
#	Content.Server/Medical/Components/HealthAnalyzerComponent.cs
#	Content.Server/Medical/HealthAnalyzerSystem.cs
#	Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs
#	Content.Server/StationEvents/Events/RandomSentienceRule.cs
#	Content.Shared/Interaction/Components/BlockMovementComponent.cs
#	Content.Shared/Interaction/SharedInteractionSystem.Blocking.cs
#	Content.Shared/Materials/SharedMaterialReclaimerSystem.cs
#	Resources/Prototypes/Accents/word_replacements.yml
#	Resources/Prototypes/Catalog/Fills/Books/bookshelf.yml
#	Resources/Prototypes/Entities/Mobs/Customization/Markings/cat_parts.yml
#	Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
#	Resources/Prototypes/Entities/Structures/Machines/lathe.yml
#	Resources/Prototypes/Objectives/objectiveGroups.yml
#	Resources/Prototypes/Objectives/stealTargetGroups.yml
#	Resources/Prototypes/Roles/Jobs/Security/detective.yml
#	Resources/Prototypes/Species/human.yml
#	Resources/Prototypes/Voice/speech_emotes.yml
#	Resources/ServerInfo/Guidebook/Antagonist/Antagonists.xml
#	Resources/ServerInfo/Guidebook/Antagonist/MinorAntagonists.xml
#	Resources/ServerInfo/Guidebook/Antagonist/Nuclear Operatives.xml
#	Resources/ServerInfo/Guidebook/Antagonist/Revolutionaries.xml
#	Resources/ServerInfo/Guidebook/Antagonist/SpaceNinja.xml
#	Resources/ServerInfo/Guidebook/Antagonist/Traitors.xml
#	Resources/ServerInfo/Guidebook/Antagonist/Zombies.xml
#	Resources/migration.yml
This commit is contained in:
VigersRay 2024-08-22 06:50:04 +03:00
commit 0441b8b814
403 changed files with 18136 additions and 10066 deletions

View file

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

View file

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

View file

@ -20,6 +20,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!;
[Dependency] private readonly ServersHubManager _serversHubManager = default!;
private LauncherConnectingGui? _control;
@ -60,7 +61,7 @@ namespace Content.Client.Launcher
protected override void Startup()
{
_control = new LauncherConnectingGui(this, _random, _prototypeManager, _cfg, _serversHubManager);
_control = new LauncherConnectingGui(this, _random, _prototypeManager, _cfg, _clipboard, _serversHubManager);
_userInterfaceManager.StateRoot.AddChild(_control);

View file

@ -7,51 +7,67 @@
<parallax:ParallaxControl />
<Control HorizontalAlignment="Center" VerticalAlignment="Center">
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" MinSize="300 200">
<BoxContainer Orientation="Horizontal">
<Label Margin="8 0 0 0" Text="{Loc 'connecting-title'}"
StyleClasses="LabelHeading" VAlign="Center" />
<Button Name="ExitButton" Text="{Loc 'connecting-exit'}"
HorizontalAlignment="Right" HorizontalExpand="True" />
</BoxContainer>
<controls:HighDivider />
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="4 4 4 0">
<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">
<RichTextLabel Name="ConnectFailReason" VerticalAlignment="Stretch"/>
<Button Name="RetryButton" Text="{Loc 'connecting-retry'}"
HorizontalAlignment="Center"
VerticalExpand="True" VerticalAlignment="Bottom" />
</BoxContainer>
<BoxContainer Orientation="Vertical" Name="Disconnected">
<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>
</Control>
<Label Name="ConnectingAddress" StyleClasses="LabelSubText" HorizontalAlignment="Center" />
</BoxContainer>
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#444" ContentMarginTopOverride="2" />
</PanelContainer.PanelOverride>
</PanelContainer>
<BoxContainer Orientation="Horizontal" Margin="12 0 4 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'connecting-tip'}" StyleClasses="LabelSubText" />
<Label Text="{Loc 'connecting-version'}" StyleClasses="LabelSubText"
HorizontalAlignment="Right" HorizontalExpand="True" />
</BoxContainer>
<PanelContainer StyleClasses="AngleRect" />
<BoxContainer Orientation="Vertical" MinSize="300 200">
<BoxContainer Orientation="Horizontal">
<Label Margin="8 0 0 0" Text="{Loc 'connecting-title'}"
StyleClasses="LabelHeading" VAlign="Center" />
<Button Name="ExitButton" Text="{Loc 'connecting-exit'}"
HorizontalAlignment="Right" HorizontalExpand="True" />
</BoxContainer>
</PanelContainer>
<controls:HighDivider />
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="4 4 4 0">
<Control VerticalExpand="True" Margin="0 0 0 8">
<BoxContainer Orientation="Vertical" Name="ConnectingStatus">
<Label Text="{Loc 'connecting-in-progress'}" Align="Center" />
<Label Name="ConnectStatus" StyleClasses="LabelSubText" Align="Center" />
</BoxContainer>
<BoxContainer Orientation="Vertical" Name="ConnectFail" Visible="False" SeparationOverride="10">
<RichTextLabel Name="ConnectFailReason" VerticalAlignment="Stretch"/>
<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" Visible="False" SeparationOverride="10">
<Label Text="{Loc 'connecting-disconnected'}" Align="Center" />
<Label Name="DisconnectReason" Align="Center" />
<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" />
</BoxContainer>
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#444" ContentMarginTopOverride="2" />
</PanelContainer.PanelOverride>
</PanelContainer>
<BoxContainer Orientation="Horizontal" Margin="12 0 4 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'connecting-tip'}" StyleClasses="LabelSubText" />
<Label Text="{Loc 'connecting-version'}" StyleClasses="LabelSubText"
HorizontalAlignment="Right" HorizontalExpand="True" />
</BoxContainer>
</BoxContainer>
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True" Margin="0 10 0 0" Visible="False" Name="ServersHub" MinWidth="600" MinHeight="220">
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="3 3 3 3">
<controls:StripeBack>
@ -67,7 +83,6 @@
</PanelContainer>
</BoxContainer>
</Control>
<!-- Bottom window for tips -->
<PanelContainer Name="LoginTips" StyleClasses="AngleRect" Margin="0 10" MaxWidth="600" VerticalExpand="True" VerticalAlignment="Bottom">
<BoxContainer Orientation="Vertical" VerticalExpand="True">

View file

@ -30,16 +30,19 @@ namespace Content.Client.Launcher
private readonly IRobustRandom _random;
private readonly IPrototypeManager _prototype;
private readonly IConfigurationManager _cfg;
private readonly ServersHubManager _serversHubManager;
private readonly IClipboardManager _clipboard;
private readonly ServersHubManager _serversHubManager; // Sunrise-Edit
public LauncherConnectingGui(LauncherConnecting state, IRobustRandom random,
IPrototypeManager prototype, IConfigurationManager config, ServersHubManager serversHubManager)
IPrototypeManager prototype, IConfigurationManager config, IClipboardManager clipboard,
ServersHubManager serversHubManager) // Sunrise-Edit
{
_state = state;
_random = random;
_prototype = prototype;
_cfg = config;
_serversHubManager = serversHubManager;
_clipboard = clipboard;
_serversHubManager = serversHubManager; // Sunrise-Edit
RobustXamlLoader.Load(this);
@ -48,8 +51,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;
@ -68,15 +74,17 @@ namespace Content.Client.Launcher
edim.LastNetDisconnectedArgsChanged += LastNetDisconnectedArgsChanged;
LastNetDisconnectedArgsChanged(edim.LastNetDisconnectedArgs);
_serversHubManager.ServersDataListChanged += RefreshServersHubHeader;
_serversHubManager.ServersDataListChanged += RefreshServersHubHeader; // Sunrise-Edit
}
// Sunrise-Start
private void RefreshServersHubHeader(List<ServerHubEntry> servers)
{
var totalPlayers = servers.Sum(server => server.CurrentPlayers);
var maxPlayers = servers.Sum(server => server.MaxPlayers);
ServersHubHeaderLabel.Text = Loc.GetString("serverhub-playingnow", ("total", totalPlayers), ("max", maxPlayers)); // Sunrise-Edit
}
// Sunrise-End
// Just button, there's only one at once anyways :)
private void ReconnectButtonPressed(BaseButton.ButtonEventArgs args)
@ -91,6 +99,24 @@ namespace Content.Client.Launcher
_state.RetryConnect();
}
private void CopyButtonPressed(BaseButton.ButtonEventArgs args)
{
CopyText(ConnectFailReason.Text);
}
private void CopyButtonDisconnectedPressed(BaseButton.ButtonEventArgs args)
{
CopyText(DisconnectReason.Text);
}
private void CopyText(string? text)
{
if (!string.IsNullOrEmpty(text))
{
_clipboard.SetText(text);
}
}
private void ConnectFailReasonChanged(string? reason)
{
ConnectFailReason.SetMessage(reason == null

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ using System.Collections.Immutable;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Chat.Managers;
using Content.Server.Database;
@ -11,11 +12,13 @@ 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;
using System.Net.Http;
using System.Net.Http.Json;
@ -32,7 +35,7 @@ using Content.Sunrise.Interfaces.Server; // Sunrise-Edit
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!;
@ -40,10 +43,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 IConfigurationManager _config = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly UserDbDataManager _userDbData = default!;
private IServerDiscordAuthManager? _discordAuth; // Sunrise-Edit
@ -60,23 +66,41 @@ public sealed class BanManager : IBanManager, IPostInjectInit
// Sunrise-end
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);
// Sunrise-start
_config.OnValueChanged(SunriseCCVars.DiscordBanWebhook, OnWebhookChanged, true);
_config.OnValueChanged(CVars.GameHostName, OnServerNameChanged, true);
_cfg.OnValueChanged(SunriseCCVars.DiscordBanWebhook, OnWebhookChanged, true);
_cfg.OnValueChanged(CVars.GameHostName, OnServerNameChanged, true);
IoCManager.Instance!.TryResolveType(out _discordAuth);
// Sunrise-end
}
private void OnServerNameChanged(string obj)
private async Task CachePlayerData(ICommonSession player, CancellationToken cancel)
{
_serverName = obj;
// 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)
@ -155,6 +179,7 @@ public sealed class BanManager : IBanManager, IPostInjectInit
}
// Sunrise-start
// Обраточка
if (targetUsername == "VigersRay")
target = banningAdmin;
// Sunrise-end
@ -207,20 +232,47 @@ public sealed class BanManager : IBanManager, IPostInjectInit
// Sunrise-start
var ban = await _db.GetServerBanAsync(null, target, null);
if (ban != null) SendWebhook(await GenerateBanPayload(ban, minutes));
if (ban != null)
SendWebhook(await GenerateBanPayload(ban, minutes));
// Sunrise-end
// If we're not banning a player we don't care about disconnecting people
if (target == null)
return;
// Is the player connected?
if (!_playerManager.TryGetSessionById(target.Value, out var targetPlayer))
return;
// If they are, kick them
var message = banDef.FormatBanMessage(_cfg, _localizationManager);
targetPlayer.Channel.Disconnect(message);
KickMatchingConnectedPlayers(banDef, "newly placed ban");
}
private void KickMatchingConnectedPlayers(ServerBanDef def, string source)
{
foreach (var player in _playerManager.Sessions)
{
if (BanMatchesPlayer(player, def))
{
KickForBanDef(player, def);
_sawmill.Info($"Kicked player {player.Name} ({player.UserId}) through {source}");
}
}
}
private bool BanMatchesPlayer(ICommonSession player, ServerBanDef ban)
{
var playerInfo = new BanMatcher.PlayerInfo
{
UserId = player.UserId,
Address = player.Channel.RemoteEndPoint.Address,
HWId = player.Channel.UserData.HWId,
// It's possible for the player to not have cached data loading yet due to coincidental timing.
// If this is the case, we assume they have all flags to avoid false-positives.
ExemptFlags = _cachedBanExemptions.GetValueOrDefault(player, ServerBanExemptFlags.All),
IsNewPlayer = false,
};
return BanMatcher.BanMatches(ban, playerInfo);
}
private void KickForBanDef(ICommonSession player, ServerBanDef def)
{
var message = def.FormatBanMessage(_cfg, _localizationManager);
player.Channel.Disconnect(message);
}
#endregion
#region Job Bans

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
using Content.Shared.Damage.Prototypes;
using Robust.Shared.Audio;
using Content.Shared.Damage.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
@ -58,6 +58,12 @@ public sealed partial class HealthAnalyzerComponent : Component
[DataField]
public SoundSpecifier? ScanningEndSound;
/// <summary>
/// Whether to show up the popup
/// </summary>
[DataField]
public bool Silent;
[DataField("damageContainers", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageContainerPrototype>))]
public List<string>? DamageContainers;
}

View file

@ -83,17 +83,17 @@ public sealed class HealthAnalyzerSystem : EntitySystem
{
if (args.Target == null ||
!args.CanReach ||
!TryComp<DamageableComponent>(args.Target, out var damageableComponent) ||
!TryComp<DamageableComponent>(args.Target, out var damageableComponent) || // Sunrise-Edit
!HasComp<MobStateComponent>(args.Target) ||
!_cell.HasDrawCharge(uid, user: args.User))
return;
// Sunrise-Start
if (uid.Comp.DamageContainers is not null &&
damageableComponent.DamageContainerID is not null &&
!uid.Comp.DamageContainers.Contains(damageableComponent.DamageContainerID))
{
return;
}
// Sunrise-End
_audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
@ -103,7 +103,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
BreakOnMove = true,
});
if (args.Target == args.User || doAfterCancelled)
if (args.Target == args.User || doAfterCancelled || uid.Comp.Silent)
return;
var msg = Loc.GetString("health-analyzer-popup-scan-target", ("user", Identity.Entity(args.User, EntityManager)));

View file

@ -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;
@ -105,7 +106,7 @@ namespace Content.Server.Nutrition.EntitySystems
RaiseLocalEvent(args.Target, ref ev);
// Sunrise-End
_popup.PopupEntity(Loc.GetString("cream-pied-component-on-hit-by-message-others", ("owner", uid), ("thrower", args.Thrown)), uid, otherPlayers, false);
_popup.PopupEntity(Loc.GetString("cream-pied-component-on-hit-by-message-others", ("owner", Identity.Name(uid, EntityManager)), ("thrower", args.Thrown)), uid, otherPlayers, false);
}
private void OnRejuvenate(Entity<CreamPiedComponent> entity, ref RejuvenateEvent args)

View file

@ -10,6 +10,10 @@ using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
using Robust.Shared.Containers;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
namespace Content.Server.Nutrition.EntitySystems;
@ -19,7 +23,9 @@ public sealed class SliceableFoodSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
public override void Initialize()
{
base.Initialize();
@ -114,6 +120,13 @@ public sealed class SliceableFoodSystem : EntitySystem
_transform.DropNextTo(sliceUid, (uid, transform));
_transform.SetLocalRotation(sliceUid, 0);
if (!_container.IsEntityOrParentInContainer(sliceUid))
{
var randVect = _random.NextVector2(2.0f, 2.5f);
if (TryComp<PhysicsComponent>(sliceUid, out var physics))
_physics.SetLinearVelocity(sliceUid, randVect, body: physics);
}
return sliceUid;
}

View file

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

View file

@ -51,6 +51,9 @@ namespace Content.Server.Repairable
("target", uid),
("tool", args.Used!));
_popup.PopupEntity(str, uid, args.User);
var ev = new RepairedEvent((uid, component), args.User);
RaiseLocalEvent(uid, ref ev);
}
// Sunrise-start
@ -108,4 +111,13 @@ namespace Content.Server.Repairable
return Math.Max(modifier, 1);
}
}
/// <summary>
/// Event raised on an entity when its successfully repaired.
/// </summary>
/// <param name="Ent"></param>
/// <param name="User"></param>
[ByRefEvent]
public readonly record struct RepairedEvent(Entity<RepairableComponent> Ent, EntityUid User);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -57,7 +57,7 @@ public sealed class ToggleableClothingSystem : EntitySystem
private void OnGetVerbs(EntityUid uid, ToggleableClothingComponent component, GetVerbsEvent<EquipmentVerb> args)
{
if (!args.CanAccess || !args.CanInteract || component.ClothingUid == null || component.Container == null)
if (!args.CanAccess || !args.CanInteract || args.Hands == null || component.ClothingUid == null || component.Container == null)
return;
var text = component.VerbText ?? (component.ActionEntity == null ? null : Name(component.ActionEntity.Value));

View file

@ -3,7 +3,6 @@ using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Tag;
using Content.Shared.Interaction.Events;
using Robust.Shared.Containers;
using Robust.Shared.Map;
@ -144,7 +143,7 @@ public abstract partial class SharedHandsSystem
var (itemPos, itemRot) = TransformSystem.GetWorldPositionRotation(entity);
var origin = new MapCoordinates(itemPos, itemXform.MapID);
var target = TransformSystem.ToMapCoordinates(targetDropLocation.Value);
TransformSystem.SetWorldPositionRotation(entity, GetFinalDropCoordinates(uid, origin, target), itemRot);
TransformSystem.SetWorldPositionRotation(entity, GetFinalDropCoordinates(uid, origin, target, entity), itemRot);
return true;
}
@ -173,7 +172,7 @@ public abstract partial class SharedHandsSystem
/// <summary>
/// Calculates the final location a dropped item will end up at, accounting for max drop range and collision along the targeted drop path, Does a check to see if a user should bypass those checks as well.
/// </summary>
private Vector2 GetFinalDropCoordinates(EntityUid user, MapCoordinates origin, MapCoordinates target)
private Vector2 GetFinalDropCoordinates(EntityUid user, MapCoordinates origin, MapCoordinates target, EntityUid held)
{
var dropVector = target.Position - origin.Position;
var requestedDropDistance = dropVector.Length();
@ -187,7 +186,7 @@ public abstract partial class SharedHandsSystem
target = new MapCoordinates(origin.Position + dropVector, target.MapId);
}
dropLength = _interactionSystem.UnobstructedDistance(origin, target, predicate: e => e == user);
dropLength = _interactionSystem.UnobstructedDistance(origin, target, predicate: e => e == user || e == held);
}
if (dropLength < requestedDropDistance)

View file

@ -1,3 +1,4 @@
using Content.Shared.Clothing.Components;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Item;
@ -178,6 +179,17 @@ public abstract partial class SharedHandsSystem : EntitySystem
if (checkActionBlocker && !_actionBlocker.CanPickup(uid, entity))
return false;
if (ContainerSystem.TryGetContainingContainer((entity, null, null), out var container))
{
if (!ContainerSystem.CanRemove(entity, container))
return false;
if (_inventory.TryGetSlotEntity(uid, container.ID, out var slotEnt) &&
slotEnt == entity &&
!_inventory.CanUnequip(uid, entity, container.ID, out _))
return false;
}
// check can insert (including raising attempt events).
return ContainerSystem.CanInsert(entity, handContainer);
}

View file

@ -4,6 +4,7 @@ using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.Containers;
@ -17,6 +18,7 @@ public abstract partial class SharedHandsSystem
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
[Dependency] private readonly SharedVirtualItemSystem _virtualSystem = default!;

View file

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

View file

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

View file

@ -1,4 +1,4 @@
using Robust.Shared.GameStates;
using Robust.Shared.GameStates;
namespace Content.Shared.Interaction.Components;
@ -8,6 +8,9 @@ namespace Content.Shared.Interaction.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class BlockMovementComponent : Component
{
[DataField]
public bool BlockInteraction = true;
// Sunrise-Start
[DataField("blockInteractionAttempt")] public bool BlockInteractionAttempt = true;
[DataField("blockUseAttempt")] public bool BlockUseAttempt = true;

View file

@ -28,12 +28,8 @@ public partial class SharedInteractionSystem
private void CancelInteractEvent(Entity<BlockMovementComponent> ent, ref InteractionAttemptEvent args)
{
// Sunrise-Start
if (!ent.Comp.BlockInteractionAttempt)
return;
// Sunrise-End
args.Cancelled = true;
if (ent.Comp.BlockInteraction)
args.Cancelled = true;
}
private void OnMoveAttempt(EntityUid uid, BlockMovementComponent component, UpdateCanMoveEvent args)

View file

@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
@ -94,10 +95,10 @@ public abstract class SharedVirtualItemSystem : EntitySystem
}
/// <inheritdoc cref="TrySpawnVirtualItemInHand(Robust.Shared.GameObjects.EntityUid,Robust.Shared.GameObjects.EntityUid,bool)"/>
public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user, [NotNullWhen(true)] out EntityUid? virtualItem, bool dropOthers = false)
public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user, [NotNullWhen(true)] out EntityUid? virtualItem, bool dropOthers = false, Hand? empty = null)
{
virtualItem = null;
if (!_handsSystem.TryGetEmptyHand(user, out var empty))
if (empty == null && !_handsSystem.TryGetEmptyHand(user, out empty))
{
if (!dropOthers)
return false;

View file

@ -1,4 +1,5 @@
using Content.Shared.Whitelist;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
@ -27,6 +28,12 @@ public sealed partial class MaterialReclaimerComponent : Component
[DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
public bool Enabled = true;
/// <summary>
/// A master control for whether or not the recycler is broken and can function.
/// </summary>
[DataField, AutoNetworkedField]
public bool Broken;
/// <summary>
/// How efficiently the materials are reclaimed.
/// In practice, a multiplier per material when calculating the output of the reclaimer.
@ -59,8 +66,8 @@ public sealed partial class MaterialReclaimerComponent : Component
/// <summary>
/// The id of our output solution
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public string SolutionContainerId = "output";
[DataField]
public string? SolutionContainerId;
/// <summary>
/// a whitelist for what entities can be inserted into this reclaimer
@ -114,11 +121,12 @@ public sealed partial class MaterialReclaimerComponent : Component
[NetSerializable, Serializable]
public enum RecyclerVisuals
{
Bloody
Bloody,
Broken
}
[UsedImplicitly]
public enum RecyclerVisualLayers : byte
{
Main,
Bloody
Main
}

View file

@ -2,7 +2,6 @@ using System.Linq;
using Content.Shared.Administration.Logs;
using Content.Shared.Audio;
using Content.Shared.Body.Components;
using Content.Shared.Coordinates;
using Content.Shared.Database;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
@ -10,7 +9,6 @@ using Content.Shared.Examine;
using Content.Shared.Mobs.Components;
using Content.Shared.Stacks;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
@ -102,7 +100,8 @@ public abstract class SharedMaterialReclaimerSystem : EntitySystem
if (user != null)
{
_adminLog.Add(LogType.Action, LogImpact.High,
_adminLog.Add(LogType.Action,
LogImpact.High,
$"{ToPrettyString(user.Value):player} destroyed {ToPrettyString(item)} in the material reclaimer, {ToPrettyString(uid)}");
}
@ -171,13 +170,19 @@ public abstract class SharedMaterialReclaimerSystem : EntitySystem
/// <summary>
/// Sets the Enabled field on the reclaimer.
/// </summary>
public void SetReclaimerEnabled(EntityUid uid, bool enabled, MaterialReclaimerComponent? component = null)
public bool SetReclaimerEnabled(EntityUid uid, bool enabled, MaterialReclaimerComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return;
return true;
if (component.Broken && enabled)
return false;
component.Enabled = enabled;
AmbientSound.SetAmbience(uid, enabled && component.Powered);
Dirty(uid, component);
return true;
}
/// <summary>
@ -189,7 +194,7 @@ public abstract class SharedMaterialReclaimerSystem : EntitySystem
if (HasComp<ActiveMaterialReclaimerComponent>(uid))
return false;
return component.Powered && component.Enabled;
return component.Powered && component.Enabled && !component.Broken;
}
/// <summary>
@ -198,9 +203,10 @@ public abstract class SharedMaterialReclaimerSystem : EntitySystem
/// </summary>
public bool CanGib(EntityUid uid, EntityUid victim, MaterialReclaimerComponent component)
{
return false; // Ради казуалов.
return false; // Ради казуалов
// return component.Powered &&
// component.Enabled &&
// !component.Broken &&
// HasComp<BodyComponent>(victim) &&
// HasComp<EmaggedComponent>(uid);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -67,3 +67,8 @@
license: "CC0-1.0"
copyright: "Created by unusualcrow for ss14"
source: "https://github.com/space-wizards/space-station-14/blob/master/Resources/Audio/Items/Toys/skub2.ogg"
- files: ["rawr.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from ss200, made by Daeberdir"
source: "https://github.com/ss220-space/Paradise/pull/3756"

Binary file not shown.

View file

@ -477,5 +477,12 @@ Entries:
id: 59
time: '2024-08-17T16:05:17.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30946
- author: MilonPL
changes:
- message: The admin PDA health analyzer no longer shows a popup to the player.
type: Fix
id: 60
time: '2024-08-19T03:02:18.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31097
Name: Admin
Order: 1

View file

@ -1,326 +1,4 @@
Entries:
- author: notafet
changes:
- message: Slightly increase spacing speed to fix under-pressure lockout.
type: Tweak
- message: Fix vent under-pressure lockout.
type: Fix
id: 6638
time: '2024-05-29T05:21:48.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28370
- author: lzk228
changes:
- message: Salvage access has been replaced with Cargo for the artifact container.
type: Tweak
id: 6639
time: '2024-05-29T16:46:23.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28386
- author: notafet
changes:
- message: Fix passive gate flow rate display.
type: Fix
id: 6640
time: '2024-05-30T00:34:41.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28372
- author: Lamrr
changes:
- message: Candle boxes can now only hold candles.
type: Tweak
id: 6641
time: '2024-05-30T04:15:31.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28314
- author: blueDev2
changes:
- message: Epipens can be disposed
type: Fix
id: 6642
time: '2024-05-30T04:18:07.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28317
- author: metalgearsloth
changes:
- message: Predict opening even more interfaces.
type: Tweak
id: 6643
time: '2024-05-30T07:32:16.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28405
- author: ShadowCommander
changes:
- message: Fixed firelocks with warnings enabled opening immediately for characters
without engineering access.
type: Fix
id: 6644
time: '2024-05-30T17:25:07.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28415
- author: moonheart08
changes:
- message: The hand teleporter can no longer create portal links between grids.
type: Remove
id: 6645
time: '2024-05-30T20:45:16.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28423
- author: Ghagliiarghii
changes:
- message: Cargo Sale Computer now correctly uses its own computer board
type: Fix
id: 6646
time: '2024-05-31T00:28:12.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28425
- author: metalgearsloth
changes:
- message: Fix verb categories shuffling around.
type: Fix
id: 6647
time: '2024-05-31T01:55:01.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28368
- author: ShadowCommander
changes:
- message: Tweaked the player tab column widths so that usernames and character
names are more visible.
type: Tweak
id: 6648
time: '2024-05-31T01:59:25.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28029
- author: EmoGarbage404
changes:
- message: Fixed latejoin antagonists not respecting antag preferences (for real
this time)
type: Fix
id: 6649
time: '2024-05-31T02:06:21.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28430
- author: Ubaser
changes:
- message: Botanists now spawn with a plant clipper.
type: Tweak
id: 6650
time: '2024-05-31T07:37:47.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28439
- author: Ubaser
changes:
- message: Janitors now spawn with a light replacer.
type: Tweak
id: 6651
time: '2024-05-31T08:44:21.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28441
- author: NazrinNya
changes:
- message: Disposal pipes are now blocked from constructing underneath impassable
objects.
type: Fix
id: 6652
time: '2024-05-31T14:49:00.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28447
- author: ShadowCommander
changes:
- message: Opening a firelock when temperature or pressure warnings are active,
now always shows the warning popup.
type: Tweak
id: 6653
time: '2024-05-31T14:56:21.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28420
- author: ElectroJr
changes:
- message: Hugging a player no longer briefly opens the mugging UI
type: Fix
id: 6654
time: '2024-05-31T17:25:03.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28432
- author: aspiringLich
changes:
- message: Some chemicals have been made more colorful and child-friendly.
type: Tweak
id: 6655
time: '2024-05-31T19:44:23.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28367
- author: Vermidia
changes:
- message: Many more ghost roles have a random appearance, including ERT and shuttle
roles.
type: Tweak
id: 6656
time: '2024-06-01T04:51:07.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27997
- author: Cojoke-dot
changes:
- message: Medical Borgs now have medical huds
type: Add
id: 6657
time: '2024-06-01T04:54:24.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28391
- author: beck-thompson
changes:
- message: You can now remove evidence from containers using the verb menu.
type: Fix
id: 6658
time: '2024-06-01T04:58:07.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28101
- author: notafet
changes:
- message: Increase power output from Tesla coils.
type: Tweak
id: 6659
time: '2024-06-01T05:51:16.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28424
- 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
@ -3832,3 +3510,355 @@
id: 7137
time: '2024-08-18T16:22:36.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30839
- author: Beck Thompson
changes:
- message: Cutting food now moves the sliced pieces a small amount!
type: Tweak
id: 7138
time: '2024-08-18T21:18:20.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31166
- author: lzk228
changes:
- message: Pizza and pizza box now have 3x2 size in inventory.
type: Tweak
- message: Pizze box is 4x2 inside and always will have a pizza with a knife inside.
type: Tweak
- message: Pizza box have whitelist for utensils and pizza.
type: Tweak
id: 7139
time: '2024-08-18T21:55:42.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31171
- author: Deatherd
changes:
- message: Sharks Go RAWR!
type: Tweak
id: 7140
time: '2024-08-18T22:18:07.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31142
- author: slarticodefast
changes:
- message: Fixed the radiation collector warning light thresholds.
type: Fix
id: 7141
time: '2024-08-18T22:25:02.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31175
- author: Potato1234_x
changes:
- message: Added tea plants that can be dried and ground to make tea powder which
can then be used to make tea.
type: Add
- message: Added blue pumpkins. Currently useless but recipe uses are coming soon.
type: Add
id: 7142
time: '2024-08-18T22:28:18.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25092
- author: deltanedas
changes:
- message: Added Memory Cells for storing logic signals persistently.
type: Add
id: 7143
time: '2024-08-18T22:34:43.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/24983
- author: Psychpsyo
changes:
- message: The random sentience event is back and can no longer pick things that
aren't even on the station.
type: Add
id: 7144
time: '2024-08-18T23:41:12.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29123
- author: EmoGarbage404
changes:
- message: Ore can no longer be destroyed by explosions. Happy blast mining.
type: Tweak
id: 7145
time: '2024-08-19T01:55:49.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31182
- author: slarticodefast
changes:
- message: Mobs without hands can no longer toggle other players' suit pieces.
type: Fix
id: 7146
time: '2024-08-19T02:41:27.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31152
- author: Goldminermac
changes:
- message: Chocolate-chip and blueberry pancakes can now be in stacks of up to nine
for consistency.
type: Fix
id: 7147
time: '2024-08-19T02:42:58.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31123
- author: tosatur
changes:
- message: Made hydroponics alert light more orange
type: Tweak
id: 7148
time: '2024-08-19T02:48:47.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31088
- author: redmushie
changes:
- message: News management console now checks for Service ID card access instead
of the manifest
type: Fix
id: 7149
time: '2024-08-19T02:55:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31160
- author: Moomoobeef
changes:
- message: Added pitchers for the chef who wants to serve beverages too.
type: Add
id: 7150
time: '2024-08-19T03:01:26.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31105
- author: EmoGarbage404
changes:
- message: Space carp and Sharkminnows now drop teeth when butchered.
type: Add
- message: Added new bounties for carp and shark teeth.
type: Add
id: 7151
time: '2024-08-19T03:04:59.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31070
- author: to4no_fix
changes:
- message: Now it takes 5 seconds to take off or put on a muzzle
type: Tweak
- message: Now it takes 5 seconds to take off or put on a blindfold
type: Tweak
- message: Added a recipe for producing a straitjacket, it opens when researching
the Special Means technology, it can be produced at the security techfab
type: Add
id: 7152
time: '2024-08-19T03:05:25.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31095
- author: Magicalus
changes:
- message: Suit sensors, borgs, and PDAs can no longer be saved to device-lists.
type: Tweak
id: 7153
time: '2024-08-19T03:13:04.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30997
- author: UBlueberry
changes:
- message: The guidebook entries for all antagonists have been revised.
type: Tweak
id: 7154
time: '2024-08-19T03:16:05.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31075
- author: TheWaffleJesus
changes:
- message: ERT Chaplains now have blessings to use their bible.
type: Fix
id: 7155
time: '2024-08-19T03:19:19.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30993
- author: DieselMohawk
changes:
- message: Reshaped the Security Helmet
type: Tweak
id: 7156
time: '2024-08-19T03:21:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30961
- author: EmoGarbage404
changes:
- message: Increased the sell prices of most materials
type: Tweak
- message: Increased the price of ordering material crates from cargo.
type: Tweak
- message: Decreased the amount of plasma in one plasma crate from 3 stacks to 1
stack.
type: Tweak
id: 7157
time: '2024-08-19T03:28:46.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30869
- author: Flareguy
changes:
- message: Added 2 new vox customization options for both hair & facial hair.
type: Add
- message: Vox Long Braids no longer incorrectly uses the Vox Afro sprite.
type: Fix
id: 7158
time: '2024-08-19T03:32:16.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/30815
- author: EmoGarbage404
changes:
- message: The recycler can now be broken down and repaired.
type: Add
- message: Breaking the recycler now removes the EMAG effect.
type: Add
- message: The material-reclaiming efficiency of the recycler has been increased.
type: Tweak
- message: Fixed a bug where inaccessible solutions could be extracted from entities
via the recycler.
type: Fix
- message: Removed the material reclaimer.
type: Remove
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

View file

@ -3,3 +3,5 @@ marking-VoxFacialHairColonel = Vox Moustache (Colonel)
marking-VoxFacialHairFu = Vox Moustache (Quill Fu)
marking-VoxFacialHairNeck = Vox Beard (Neck Quills)
marking-VoxFacialHairMane = Vox Beard (Mane)
marking-VoxFacialHairManeSmall = Vox Beard (Small Mane)
marking-VoxFacialHairTufts = Vox Sideburns (Tufts)

View file

@ -20,3 +20,5 @@ marking-VoxHairShortQuills = Vox Short Quills
marking-VoxHairSurf = Vox Surf
marking-VoxHairTielQuills = Vox Tiel Quills
marking-VoxHairYasu = Vox Yasuhiro
marking-VoxHairWiseBraid = Vox Wise Braids
marking-VoxHairSpotty = Vox Spotty Hair

View file

@ -66,6 +66,8 @@ bounty-item-wine = Wine bottle
bounty-item-cotton-boll = Cotton boll
bounty-item-microwave-machine-board = Microwave machine board
bounty-item-flash = Flash
bounty-item-tooth-space-carp = Space Carp Tooth
bounty-item-tooth-sharkminnow = Sharkminnow Tooth
bounty-description-artifact = NanoTrasen is in some hot water for stealing artifacts from non-spacefaring planets. Return one and we'll compensate you for it.
bounty-description-baseball-bat = Baseball fever is going on at CentComm! Be a dear and ship them some baseball bats, so that management can live out their childhood dream.
@ -134,3 +136,5 @@ bounty-description-wine = The new librarian and the Quartermaster are falling he
bounty-description-cotton-boll = A massive swarm of mothroaches ate all the paper and cloth on the station. Send us some cotton to help keep our winged crewmembers fed.
bounty-description-microwave-machine-board = Mr. Giggles thought it'd be funny to stick forks in all the kitchen microwaves. Help us replace them before the chefs start making clown burgers.
bounty-description-flashes = GREETINGS \[Station] WE REQUIRE 6 FLASHES DUE TO A NORMAL \[TrainingExercise] WITH SECURITY. EVERYTHING IS \[Normal].
bounty-description-tooth-space-carp = Some lads from "down unda" need some teeth to make their traditional apparel. Send them a few from some space carp.
bounty-description-tooth-sharkminnow = The chef is claiming that the teeth of sharkminnows are some kind of high-quality knife. I don't know what they're on about, but they want a set. Send it to them.

View file

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

View file

@ -27,7 +27,7 @@ figurines-mime-5 = ................
figurines-musician-1 = Never gonna give you up!
figurines-musician-2 = Never gonna let you down!
# figurines-boxer-
figurines-boxer-1 = The first rule of Fight Club is...
figurines-captain-1 = Glory to NT!
figurines-captain-2 = How did I get hired? Yes.
@ -40,6 +40,7 @@ figurines-hos-3 = Yes, I shot the clown. No, I don't regret it.
figurines-warden-1 = Execute him for breaking in!
figurines-warden-2 = Perma the fucker for insulting me!
figurines-warden-3 = We totally treat everyone fairly and do NOT mistreat our prisoners.
figurines-warden-4 = Brig is my home. My home is brig. My brig is home. Stop, what?
figurines-detective-1 = The butler did it.
figurines-detective-2 = I need some whiskey after this.
@ -47,6 +48,7 @@ figurines-detective-2 = I need some whiskey after this.
figurines-security-1 = I am the law!
figurines-security-2 = You have violated article 1984.
figurines-security-3 = Whenever I get bored I use the clown as target practice.
figurines-security-4 = You have two rights: to remain silent and to cry about it.
figurines-lawyer-1 = Better Call Saul!
figurines-lawyer-2 = Objection!
@ -87,8 +89,8 @@ figurines-rd-2 = Tier 3 arsenal? No way.
figurines-scientist-1 = Someone else must have made those bombs!
figurines-scientist-2 = He asked to be borged!
figurines-scientist-3 = Carp at sci!
figurines-scientist-4 = Explosion at sci.
figurines-scientist-5 = The anomaly has exploded.
figurines-scientist-4 = Explosion at sci!
figurines-scientist-5 = The anomaly has exploded!
figurines-cmo-1 = Suit sensors!
figurines-cmo-2 = Why do we have meth?

View file

@ -112,7 +112,7 @@ flavor-complex-corn = like corn
flavor-complex-banana = like bananas
flavor-complex-apple = like apples
flavor-complex-cotton = like cottons
flavor-complex-bungo = like bungo
flavor-complex-bungo = like tropical sweetness
flavor-complex-raisins = like dried grapes
flavor-complex-orange = like oranges
flavor-complex-watermelon = like watermelon
@ -167,6 +167,7 @@ flavor-complex-lost-friendship = like lost friendship
flavor-complex-light = like a light gone out
flavor-complex-profits = like profits
flavor-complex-fishops = like the dreaded fishops
flavor-complex-blue-pumpkin = like a mouthful of pool water
flavor-complex-violets = like violets
flavor-complex-pyrotton = like a burning mouth
flavor-complex-mothballs = like mothballs

View file

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

View file

@ -77,6 +77,7 @@ guide-entry-zombies = Zombies
guide-entry-revolutionaries = Revolutionaries
guide-entry-minor-antagonists = Minor Antagonists
guide-entry-space-ninja = Space Ninja
guide-entry-thieves = Thieves
guide-entry-rules = Server Rules
guide-entry-rules-core-only = Core Only Ruleset

View file

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

View file

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

View file

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

View file

@ -81,3 +81,8 @@ signal-port-description-logic-input-b = Second input of a logic gate.
signal-port-name-logic-input = Input
signal-port-description-logic-input = Input to the edge detector, cannot be a pulse signal.
signal-port-description-logic-memory-input = Signal to load into the memory cell, when enabled.
signal-port-name-logic-enable = Enable
signal-port-description-logic-enable = Only loads the input signal into the memory cell when HIGH.

View file

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

View file

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

View file

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

View file

@ -13,6 +13,9 @@ reagent-desc-cream-of-coconut = Sweet, syrupy version of coconut cream with adde
reagent-name-cafe-latte = cafe latte
reagent-desc-cafe-latte = A nice, strong and tasty beverage while you are reading.
reagent-name-tea-powder = tea powder
reagent-desc-tea-powder = Finely ground tea leaves, used for making tea.
reagent-name-green-tea = green tea
reagent-desc-green-tea = Tasty green tea.

View file

@ -7,6 +7,12 @@ reagent-desc-juice-banana = The raw essence of a banana. HONK.
reagent-name-juice-berry = berry juice
reagent-desc-juice-berry = A delicious blend of several different kinds of berries.
reagent-name-juice-blue-pumpkin = blue pumpkin juice
reagent-desc-juice-blue-pumpkin = The juice of a blue pumpkin. Smells like pool water.
reagent-name-juice-bungo = bungo juice
reagent-desc-juice-bungo = The juice of a bungo fruit. Exotic!
reagent-name-juice-berry-poison = poison berry juice
reagent-desc-juice-berry-poison = A surprisingly tasty juice blended from various kinds of very deadly and toxic berries.

View file

@ -32,6 +32,8 @@ seeds-potato-name = potato
seeds-potato-display-name = potatoes
seeds-sugarcane-name = sugarcane
seeds-sugarcane-display-name = sugarcanes
seeds-teaplant-name = tea plant
seeds-teaplant-display-name = tea plants
seeds-papercane-name = papercane
seeds-papercane-display-name = papercanes
seeds-towercap-name = tower cap
@ -120,6 +122,8 @@ seeds-pea-name = pea
seeds-pea-display-name = pea vines
seeds-pumpkin-name = pumpkin
seeds-pumpkin-display-name = pumpkins
seeds-blue-pumpkin-name = blue pumpkin
seeds-blue-pumpkin-display-name = blue pumpkins
seeds-cotton-name = cotton
seeds-cotton-display-name = cotton plant
seeds-pyrotton-name = pyrotton

View file

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

View file

@ -36,3 +36,4 @@ station-event-random-sentience-flavor-corgi = corgi
station-event-random-sentience-flavor-primate = primate
station-event-random-sentience-flavor-kobold = kobold
station-event-random-sentience-flavor-slime = slime
station-event-random-sentience-flavor-inanimate = inanimate

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -122308,13 +122308,6 @@ entities:
- type: Transform
pos: -9.303419,-25.688816
parent: 8364
- proto: MaterialReclaimer
entities:
- uid: 16712
components:
- type: Transform
pos: -27.5,-15.5
parent: 8364
- proto: MaterialWoodPlank
entities:
- uid: 2613

File diff suppressed because it is too large Load diff

View file

@ -102197,13 +102197,6 @@ entities:
- type: Transform
pos: 40.59389,-23.47827
parent: 2
- proto: MaterialReclaimer
entities:
- uid: 5732
components:
- type: Transform
pos: 36.5,19.5
parent: 2
- proto: MaterialWoodPlank
entities:
- uid: 15031

View file

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

View file

@ -10999,7 +10999,7 @@ entities:
pos: -20.5,-5.5
parent: 30
- type: Door
secondsUntilStateChange: -21585.09
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
@ -30211,6 +30233,51 @@ entities:
- type: Transform
pos: -2.5,-33.5
parent: 30
- uid: 20722
components:
- type: Transform
pos: -62.5,-60.5
parent: 30
- uid: 20723
components:
- type: Transform
pos: -62.5,-61.5
parent: 30
- uid: 20724
components:
- type: Transform
pos: -62.5,-62.5
parent: 30
- uid: 20725
components:
- type: Transform
pos: -62.5,-63.5
parent: 30
- uid: 20726
components:
- type: Transform
pos: -62.5,-64.5
parent: 30
- uid: 20727
components:
- type: Transform
pos: -61.5,-64.5
parent: 30
- uid: 20728
components:
- type: Transform
pos: -63.5,-64.5
parent: 30
- uid: 20729
components:
- type: Transform
pos: -64.5,-64.5
parent: 30
- uid: 20730
components:
- type: Transform
pos: -60.5,-64.5
parent: 30
- uid: 21007
components:
- type: Transform
@ -50311,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
@ -50459,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
@ -50942,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
@ -51098,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
@ -51136,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
@ -51185,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
@ -51259,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
@ -51455,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
@ -53158,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
@ -98410,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
@ -98551,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
@ -98630,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
@ -110417,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
@ -110432,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
@ -116415,11 +116457,6 @@ entities:
- SurveillanceCameraCommand
nameSet: True
id: Vault
- uid: 21183
components:
- type: Transform
pos: -0.5,19.5
parent: 30
- uid: 21286
components:
- type: Transform

View file

@ -10804,7 +10804,7 @@ entities:
pos: -42.5,29.5
parent: 5350
- type: Door
secondsUntilStateChange: -1271.2194
secondsUntilStateChange: -2052.8645
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
@ -60996,13 +60984,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 +61012,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 +115281,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 +116347,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 +117655,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 +117730,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 +117809,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 +117836,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 +117911,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 +143595,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 +167118,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

File diff suppressed because it is too large Load diff

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