Merge branch 'master' of https://github.com/space-wizards/space-station-14 into map-load-refactor
This commit is contained in:
@@ -88,8 +88,9 @@ namespace Content.Client.Access.UI
|
||||
button.Disabled = !interfaceEnabled;
|
||||
if (interfaceEnabled)
|
||||
{
|
||||
button.Pressed = state.TargetAccessReaderIdAccessList?.Contains(accessName) ?? false;
|
||||
button.Disabled = (!state.AllowedModifyAccessList?.Contains(accessName)) ?? true;
|
||||
// Explicit cast because Rider gives a false error otherwise.
|
||||
button.Pressed = state.TargetAccessReaderIdAccessList?.Contains((ProtoId<AccessLevelPrototype>) accessName) ?? false;
|
||||
button.Disabled = (!state.AllowedModifyAccessList?.Contains((ProtoId<AccessLevelPrototype>) accessName)) ?? true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
xmlns:tabs="clr-namespace:Content.Client.Administration.UI.Tabs"
|
||||
xmlns:playerTab="clr-namespace:Content.Client.Administration.UI.Tabs.PlayerTab"
|
||||
xmlns:objectsTab="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab"
|
||||
xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab"
|
||||
xmlns:baby="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab">
|
||||
xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab">
|
||||
<TabContainer Name="MasterTabContainer">
|
||||
<adminTab:AdminTab />
|
||||
<adminbusTab:AdminbusTab />
|
||||
@@ -15,7 +14,6 @@
|
||||
<tabs:RoundTab />
|
||||
<tabs:ServerTab />
|
||||
<panic:PanicBunkerTab Name="PanicBunkerControl" Access="Public" />
|
||||
<baby:BabyJailTab Name="BabyJailControl" Access="Public" />
|
||||
<playerTab:PlayerTab Name="PlayerTabControl" Access="Public" />
|
||||
<objectsTab:ObjectsTab Name="ObjectsTabControl" Access="Public" />
|
||||
</TabContainer>
|
||||
|
||||
@@ -21,10 +21,6 @@ public sealed partial class AdminMenuWindow : DefaultWindow
|
||||
MasterTabContainer.SetTabTitle((int) TabIndex.Round, Loc.GetString("admin-menu-round-tab"));
|
||||
MasterTabContainer.SetTabTitle((int) TabIndex.Server, Loc.GetString("admin-menu-server-tab"));
|
||||
MasterTabContainer.SetTabTitle((int) TabIndex.PanicBunker, Loc.GetString("admin-menu-panic-bunker-tab"));
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
MasterTabContainer.SetTabTitle((int) TabIndex.BabyJail, Loc.GetString("admin-menu-baby-jail-tab"));
|
||||
MasterTabContainer.SetTabTitle((int) TabIndex.Players, Loc.GetString("admin-menu-players-tab"));
|
||||
MasterTabContainer.SetTabTitle((int) TabIndex.Objects, Loc.GetString("admin-menu-objects-tab"));
|
||||
MasterTabContainer.OnTabChanged += OnTabChanged;
|
||||
@@ -52,7 +48,6 @@ public sealed partial class AdminMenuWindow : DefaultWindow
|
||||
Round,
|
||||
Server,
|
||||
PanicBunker,
|
||||
BabyJail,
|
||||
Players,
|
||||
Objects,
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ namespace Content.Client.Administration.UI
|
||||
}
|
||||
|
||||
var title = string.IsNullOrWhiteSpace(popup.TitleEdit.Text) ? null : popup.TitleEdit.Text;
|
||||
var suspended = popup.SuspendedCheckbox.Pressed;
|
||||
|
||||
if (popup.SourceData is { } src)
|
||||
{
|
||||
@@ -139,7 +140,8 @@ namespace Content.Client.Administration.UI
|
||||
Title = title,
|
||||
PosFlags = pos,
|
||||
NegFlags = neg,
|
||||
RankId = rank
|
||||
RankId = rank,
|
||||
Suspended = suspended,
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -152,7 +154,8 @@ namespace Content.Client.Administration.UI
|
||||
Title = title,
|
||||
PosFlags = pos,
|
||||
NegFlags = neg,
|
||||
RankId = rank
|
||||
RankId = rank,
|
||||
Suspended = suspended,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,7 +174,7 @@ namespace Content.Client.Administration.UI
|
||||
{
|
||||
Id = src,
|
||||
Flags = flags,
|
||||
Name = name
|
||||
Name = name,
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -351,6 +354,7 @@ namespace Content.Client.Administration.UI
|
||||
public readonly OptionButton RankButton;
|
||||
public readonly Button SaveButton;
|
||||
public readonly Button? RemoveButton;
|
||||
public readonly CheckBox SuspendedCheckbox;
|
||||
|
||||
public readonly Dictionary<AdminFlags, (Button inherit, Button sub, Button plus)> FlagButtons
|
||||
= new();
|
||||
@@ -381,6 +385,12 @@ namespace Content.Client.Administration.UI
|
||||
RankButton = new OptionButton();
|
||||
SaveButton = new Button { Text = Loc.GetString("permissions-eui-edit-admin-window-save-button"), HorizontalAlignment = HAlignment.Right };
|
||||
|
||||
SuspendedCheckbox = new CheckBox
|
||||
{
|
||||
Text = Loc.GetString("permissions-eui-edit-admin-window-suspended"),
|
||||
Pressed = data?.Suspended ?? false,
|
||||
};
|
||||
|
||||
RankButton.AddItem(Loc.GetString("permissions-eui-edit-admin-window-no-rank-button"), NoRank);
|
||||
foreach (var (rId, rank) in ui._ranks)
|
||||
{
|
||||
@@ -488,7 +498,8 @@ namespace Content.Client.Administration.UI
|
||||
{
|
||||
nameControl,
|
||||
TitleEdit,
|
||||
RankButton
|
||||
RankButton,
|
||||
SuspendedCheckbox,
|
||||
}
|
||||
},
|
||||
permGrid
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<controls:BabyJailStatusWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab"
|
||||
Title="{Loc admin-ui-baby-jail-window-title}">
|
||||
<RichTextLabel Name="MessageLabel" Access="Public" />
|
||||
</controls:BabyJailStatusWindow>
|
||||
@@ -1,21 +0,0 @@
|
||||
using Content.Client.Message;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Administration.UI.Tabs.BabyJailTab;
|
||||
|
||||
/*
|
||||
* TODO: Remove me 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.
|
||||
*/
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BabyJailStatusWindow : FancyWindow
|
||||
{
|
||||
public BabyJailStatusWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
MessageLabel.SetMarkup(Loc.GetString("admin-ui-baby-jail-is-enabled"));
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<controls:BabyJailTab
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
Margin="4">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<cc:CommandButton Name="EnabledButton" Command="babyjail" ToggleMode="True"
|
||||
Text="{Loc admin-ui-baby-jail-disabled}"
|
||||
ToolTip="{Loc admin-ui-baby-jail-tooltip}" />
|
||||
<cc:CommandButton Name="ShowReasonButton" Command="babyjail_show_reason"
|
||||
ToggleMode="True" Text="{Loc admin-ui-baby-jail-show-reason}"
|
||||
ToolTip="{Loc admin-ui-baby-jail-show-reason-tooltip}" />
|
||||
<BoxContainer Orientation="Vertical" Margin="0 10 0 0">
|
||||
<BoxContainer Orientation="Horizontal" Margin="2">
|
||||
<Label Text="{Loc admin-ui-baby-jail-max-account-age}" MinWidth="175" />
|
||||
<LineEdit Name="MaxAccountAge" MinWidth="50" Margin="0 0 5 0" />
|
||||
<Label Text="{Loc generic-minutes}" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" Margin="2">
|
||||
<Label Text="{Loc admin-ui-baby-jail-max-overall-minutes}" MinWidth="175" />
|
||||
<LineEdit Name="MaxOverallMinutes" MinWidth="50" Margin="0 0 5 0" />
|
||||
<Label Text="{Loc generic-minutes}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:BabyJailTab>
|
||||
@@ -1,75 +0,0 @@
|
||||
using Content.Shared.Administration.Events;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
/*
|
||||
* TODO: Remove me 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.
|
||||
*/
|
||||
|
||||
namespace Content.Client.Administration.UI.Tabs.BabyJailTab;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BabyJailTab : Control
|
||||
{
|
||||
[Dependency] private readonly IConsoleHost _console = default!;
|
||||
|
||||
private string _maxAccountAge;
|
||||
private string _maxOverallMinutes;
|
||||
|
||||
public BabyJailTab()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
MaxAccountAge.OnTextEntered += args => SendMaxAccountAge(args.Text);
|
||||
MaxAccountAge.OnFocusExit += args => SendMaxAccountAge(args.Text);
|
||||
_maxAccountAge = MaxAccountAge.Text;
|
||||
|
||||
MaxOverallMinutes.OnTextEntered += args => SendMaxOverallMinutes(args.Text);
|
||||
MaxOverallMinutes.OnFocusExit += args => SendMaxOverallMinutes(args.Text);
|
||||
_maxOverallMinutes = MaxOverallMinutes.Text;
|
||||
}
|
||||
|
||||
private void SendMaxAccountAge(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) ||
|
||||
text == _maxAccountAge ||
|
||||
!int.TryParse(text, out var minutes))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_console.ExecuteCommand($"babyjail_max_account_age {minutes}");
|
||||
}
|
||||
|
||||
private void SendMaxOverallMinutes(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) ||
|
||||
text == _maxOverallMinutes ||
|
||||
!int.TryParse(text, out var minutes))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_console.ExecuteCommand($"babyjail_max_overall_minutes {minutes}");
|
||||
}
|
||||
|
||||
public void UpdateStatus(BabyJailStatus status)
|
||||
{
|
||||
EnabledButton.Pressed = status.Enabled;
|
||||
EnabledButton.Text = Loc.GetString(status.Enabled
|
||||
? "admin-ui-baby-jail-enabled"
|
||||
: "admin-ui-baby-jail-disabled"
|
||||
);
|
||||
EnabledButton.ModulateSelfOverride = status.Enabled ? Color.Red : null;
|
||||
ShowReasonButton.Pressed = status.ShowReason;
|
||||
|
||||
MaxAccountAge.Text = status.MaxAccountAgeMinutes.ToString();
|
||||
_maxAccountAge = MaxAccountAge.Text;
|
||||
|
||||
MaxOverallMinutes.Text = status.MaxOverallMinutes.ToString();
|
||||
_maxOverallMinutes = MaxOverallMinutes.Text;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
<CheckBox Name="RestartSoundsCheckBox" Text="{Loc 'ui-options-restart-sounds'}" />
|
||||
<CheckBox Name="EventMusicCheckBox" Text="{Loc 'ui-options-event-music'}" />
|
||||
<CheckBox Name="AdminSoundsCheckBox" Text="{Loc 'ui-options-admin-sounds'}" />
|
||||
<CheckBox Name="BwoinkSoundCheckBox" Text="{Loc 'ui-options-bwoink-sound'}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<ui:OptionsTabControlRow Name="Control" Access="Public" />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Audio;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.Audio;
|
||||
@@ -12,8 +13,9 @@ namespace Content.Client.Options.UI.Tabs;
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AudioTab : Control
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IAudioManager _audio = default!;
|
||||
[Dependency] private readonly IClientAdminManager _admin = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public AudioTab()
|
||||
{
|
||||
@@ -61,10 +63,30 @@ public sealed partial class AudioTab : Control
|
||||
Control.AddOptionCheckBox(CCVars.RestartSoundsEnabled, RestartSoundsCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.EventMusicEnabled, EventMusicCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.AdminSoundsEnabled, AdminSoundsCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.BwoinkSoundEnabled, BwoinkSoundCheckBox);
|
||||
|
||||
Control.Initialize();
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
_admin.AdminStatusUpdated += UpdateAdminButtonsVisibility;
|
||||
UpdateAdminButtonsVisibility();
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
_admin.AdminStatusUpdated -= UpdateAdminButtonsVisibility;
|
||||
}
|
||||
|
||||
|
||||
private void UpdateAdminButtonsVisibility()
|
||||
{
|
||||
BwoinkSoundCheckBox.Visible = _admin.IsActive();
|
||||
}
|
||||
|
||||
private void OnMasterVolumeSliderChanged(float value)
|
||||
{
|
||||
// TODO: I was thinking of giving OptionsTabControlRow a flag to "set CVar immediately", but I'm deferring that
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Content.Client
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
ContentStart.Start(args);
|
||||
|
||||
@@ -3,7 +3,6 @@ using Content.Client.Administration.Systems;
|
||||
using Content.Client.Administration.UI;
|
||||
using Content.Client.Administration.UI.Tabs.ObjectsTab;
|
||||
using Content.Client.Administration.UI.Tabs.PanicBunkerTab;
|
||||
using Content.Client.Administration.UI.Tabs.BabyJailTab;
|
||||
using Content.Client.Administration.UI.Tabs.PlayerTab;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Lobby;
|
||||
@@ -38,13 +37,11 @@ public sealed class AdminUIController : UIController,
|
||||
private AdminMenuWindow? _window;
|
||||
private MenuButton? AdminButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.AdminButton;
|
||||
private PanicBunkerStatus? _panicBunker;
|
||||
private BabyJailStatus? _babyJail;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<PanicBunkerChangedEvent>(OnPanicBunkerUpdated);
|
||||
SubscribeNetworkEvent<BabyJailChangedEvent>(OnBabyJailUpdated);
|
||||
}
|
||||
|
||||
private void OnPanicBunkerUpdated(PanicBunkerChangedEvent msg, EntitySessionEventArgs args)
|
||||
@@ -59,18 +56,6 @@ public sealed class AdminUIController : UIController,
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBabyJailUpdated(BabyJailChangedEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var showDialog = _babyJail == null && msg.Status.Enabled;
|
||||
_babyJail = msg.Status;
|
||||
_window?.BabyJailControl.UpdateStatus(msg.Status);
|
||||
|
||||
if (showDialog)
|
||||
{
|
||||
UIManager.CreateWindow<BabyJailStatusWindow>().OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
EnsureWindow();
|
||||
@@ -116,13 +101,6 @@ public sealed class AdminUIController : UIController,
|
||||
if (_panicBunker != null)
|
||||
_window.PanicBunkerControl.UpdateStatus(_panicBunker);
|
||||
|
||||
/*
|
||||
* 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 (_babyJail != null)
|
||||
_window.BabyJailControl.UpdateStatus(_babyJail);
|
||||
|
||||
_window.PlayerTabControl.OnEntryKeyBindDown += PlayerTabEntryKeyBindDown;
|
||||
_window.ObjectsTabControl.OnEntryKeyBindDown += ObjectsTabEntryKeyBindDown;
|
||||
_window.OnOpen += OnWindowOpen;
|
||||
|
||||
@@ -45,6 +45,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
|
||||
public IAHelpUIHandler? UIHelper;
|
||||
private bool _discordRelayActive;
|
||||
private bool _hasUnreadAHelp;
|
||||
private bool _bwoinkSoundEnabled;
|
||||
private string? _aHelpSound;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -56,6 +57,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
|
||||
|
||||
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
|
||||
_config.OnValueChanged(CCVars.AHelpSound, v => _aHelpSound = v, true);
|
||||
_config.OnValueChanged(CCVars.BwoinkSoundEnabled, v => _bwoinkSoundEnabled = v, true);
|
||||
}
|
||||
|
||||
public void UnloadButton()
|
||||
@@ -135,7 +137,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
|
||||
}
|
||||
if (message.PlaySound && localPlayer.UserId != message.TrueSender)
|
||||
{
|
||||
if (_aHelpSound != null)
|
||||
if (_aHelpSound != null && (_bwoinkSoundEnabled || !_adminManager.IsActive()))
|
||||
_audio.PlayGlobal(_aHelpSound, Filter.Local(), false);
|
||||
_clyde.RequestWindowAttention();
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ namespace Content.IntegrationTests.Tests
|
||||
"Gate",
|
||||
"Amber",
|
||||
"Loop",
|
||||
"Plasma",
|
||||
"Elkridge"
|
||||
};
|
||||
|
||||
|
||||
@@ -61,7 +61,13 @@ public static class ClientPackaging
|
||||
var graph = new RobustClientAssetGraph();
|
||||
pass.Dependencies.Add(new AssetPassDependency(graph.Output.Name));
|
||||
|
||||
AssetGraph.CalculateGraph(graph.AllPasses.Append(pass).ToArray(), logger);
|
||||
var dropSvgPass = new AssetPassFilterDrop(f => f.Path.EndsWith(".svg"))
|
||||
{
|
||||
Name = "DropSvgPass",
|
||||
};
|
||||
dropSvgPass.AddDependency(graph.Input).AddBefore(graph.PresetPasses);
|
||||
|
||||
AssetGraph.CalculateGraph([pass, dropSvgPass, ..graph.AllPasses], logger);
|
||||
|
||||
var inputPass = graph.Input;
|
||||
|
||||
@@ -72,7 +78,7 @@ public static class ClientPackaging
|
||||
new[] { "Content.Client", "Content.Shared", "Content.Shared.Database" },
|
||||
cancel: cancel);
|
||||
|
||||
await RobustClientPackaging.WriteClientResources(contentDir, pass, cancel);
|
||||
await RobustClientPackaging.WriteClientResources(contentDir, inputPass, cancel);
|
||||
|
||||
inputPass.InjectFinished();
|
||||
}
|
||||
|
||||
2104
Content.Server.Database/Migrations/Postgres/20241122174243_IPIntel.Designer.cs
generated
Normal file
2104
Content.Server.Database/Migrations/Postgres/20241122174243_IPIntel.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class IPIntel : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ipintel_cache",
|
||||
columns: table => new
|
||||
{
|
||||
ipintel_cache_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
address = table.Column<IPAddress>(type: "inet", nullable: false),
|
||||
time = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
score = table.Column<float>(type: "real", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ipintel_cache", x => x.ipintel_cache_id);
|
||||
});
|
||||
|
||||
migrationBuilder.Sql("CREATE UNIQUE INDEX idx_ipintel_cache_address ON ipintel_cache(address)");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ipintel_cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
2084
Content.Server.Database/Migrations/Postgres/20241223235939_AdminStatus.Designer.cs
generated
Normal file
2084
Content.Server.Database/Migrations/Postgres/20241223235939_AdminStatus.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AdminStatus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "deadminned",
|
||||
table: "admin",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "suspended",
|
||||
table: "admin",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "deadminned",
|
||||
table: "admin");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "suspended",
|
||||
table: "admin");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,14 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("admin_rank_id");
|
||||
|
||||
b.Property<bool>("Deadminned")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("deadminned");
|
||||
|
||||
b.Property<bool>("Suspended")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("suspended");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("title");
|
||||
@@ -627,6 +635,34 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ipintel_cache_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<IPAddress>("Address")
|
||||
.IsRequired()
|
||||
.HasColumnType("inet")
|
||||
.HasColumnName("address");
|
||||
|
||||
b.Property<float>("Score")
|
||||
.HasColumnType("real")
|
||||
.HasColumnName("score");
|
||||
|
||||
b.Property<DateTime>("Time")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("time");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_ipintel_cache");
|
||||
|
||||
b.ToTable("ipintel_cache", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Job", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
2028
Content.Server.Database/Migrations/Sqlite/20241122174236_IPIntel.Designer.cs
generated
Normal file
2028
Content.Server.Database/Migrations/Sqlite/20241122174236_IPIntel.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class IPIntel : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ipintel_cache",
|
||||
columns: table => new
|
||||
{
|
||||
ipintel_cache_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
address = table.Column<string>(type: "TEXT", nullable: false),
|
||||
time = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
score = table.Column<float>(type: "REAL", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ipintel_cache", x => x.ipintel_cache_id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ipintel_cache_address",
|
||||
table: "ipintel_cache",
|
||||
column: "address",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ipintel_cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
2007
Content.Server.Database/Migrations/Sqlite/20241223235932_AdminStatus.Designer.cs
generated
Normal file
2007
Content.Server.Database/Migrations/Sqlite/20241223235932_AdminStatus.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AdminStatus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "deadminned",
|
||||
table: "admin",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "suspended",
|
||||
table: "admin",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "deadminned",
|
||||
table: "admin");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "suspended",
|
||||
table: "admin");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,14 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("admin_rank_id");
|
||||
|
||||
b.Property<bool>("Deadminned")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("deadminned");
|
||||
|
||||
b.Property<bool>("Suspended")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("suspended");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
@@ -591,6 +599,35 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.ToTable("connection_log", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ipintel_cache_id");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("address");
|
||||
|
||||
b.Property<float>("Score")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("score");
|
||||
|
||||
b.Property<DateTime>("Time")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_ipintel_cache");
|
||||
|
||||
b.HasIndex("Address")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ipintel_cache", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Job", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace Content.Server.Database
|
||||
public DbSet<AdminMessage> AdminMessages { get; set; } = null!;
|
||||
public DbSet<RoleWhitelist> RoleWhitelists { get; set; } = null!;
|
||||
public DbSet<BanTemplate> BanTemplate { get; set; } = null!;
|
||||
public DbSet<IPIntelCache> IPIntelCache { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -609,6 +610,16 @@ namespace Content.Server.Database
|
||||
[Key] public Guid UserId { get; set; }
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the admin is voluntarily deadminned. They can re-admin at any time.
|
||||
/// </summary>
|
||||
public bool Deadminned { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the admin is suspended by an admin with <c>PERMISSIONS</c>. They will not have in-game permissions.
|
||||
/// </summary>
|
||||
public bool Suspended { get; set; }
|
||||
|
||||
public int? AdminRankId { get; set; }
|
||||
public AdminRank? AdminRank { get; set; }
|
||||
public List<AdminFlag> Flags { get; set; } = default!;
|
||||
@@ -962,12 +973,14 @@ namespace Content.Server.Database
|
||||
Full = 2,
|
||||
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.
|
||||
*
|
||||
* Edit: It has
|
||||
*/
|
||||
BabyJail = 4,
|
||||
/// Results from rejected connections with external API checking tools
|
||||
IPChecks = 5,
|
||||
}
|
||||
|
||||
public class ServerBanHit
|
||||
@@ -1284,4 +1297,28 @@ namespace Content.Server.Database
|
||||
return new ImmutableTypedHwid(hwid.Hwid.ToImmutableArray(), hwid.Type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Cache for the IPIntel system
|
||||
/// </summary>
|
||||
public class IPIntelCache
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The IP address (duh). This is made unique manually for psql cause of ef core bug.
|
||||
/// </summary>
|
||||
public IPAddress Address { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Date this record was added. Used to check if our cache is out of date.
|
||||
/// </summary>
|
||||
public DateTime Time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The score IPIntel returned
|
||||
/// </summary>
|
||||
public float Score { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,11 @@ namespace Content.Server.Database
|
||||
modelBuilder.Entity<Profile>()
|
||||
.Property(log => log.Markings)
|
||||
.HasConversion(jsonByteArrayConverter);
|
||||
|
||||
// EF core can make this automatically unique on sqlite but not psql.
|
||||
modelBuilder.Entity<IPIntelCache>()
|
||||
.HasIndex(p => p.Address)
|
||||
.IsUnique();
|
||||
}
|
||||
|
||||
public override int CountAdminLogs()
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Server)]
|
||||
public sealed class BabyJailCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public override string Command => "babyjail";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var toggle = Toggle(CCVars.BabyJailEnabled, shell, args, _cfg);
|
||||
if (toggle == null)
|
||||
return;
|
||||
|
||||
shell.WriteLine(Loc.GetString(toggle.Value ? "babyjail-command-enabled" : "babyjail-command-disabled"));
|
||||
}
|
||||
|
||||
public static bool? Toggle(CVarDef<bool> cvar, IConsoleShell shell, string[] args, IConfigurationManager config)
|
||||
{
|
||||
if (args.Length > 1)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
|
||||
return null;
|
||||
}
|
||||
|
||||
var enabled = config.GetCVar(cvar);
|
||||
|
||||
switch (args.Length)
|
||||
{
|
||||
case 0:
|
||||
enabled = !enabled;
|
||||
break;
|
||||
case 1 when !bool.TryParse(args[0], out enabled):
|
||||
shell.WriteError(Loc.GetString("shell-argument-must-be-boolean"));
|
||||
return null;
|
||||
}
|
||||
|
||||
config.SetCVar(cvar, enabled);
|
||||
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[AdminCommand(AdminFlags.Server)]
|
||||
public sealed class BabyJailShowReasonCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public override string Command => "babyjail_show_reason";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var toggle = BabyJailCommand.Toggle(CCVars.BabyJailShowReason, shell, args, _cfg);
|
||||
if (toggle == null)
|
||||
return;
|
||||
|
||||
shell.WriteLine(Loc.GetString(toggle.Value
|
||||
? "babyjail-command-show-reason-enabled"
|
||||
: "babyjail-command-show-reason-disabled"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Server)]
|
||||
public sealed class BabyJailMinAccountAgeCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public override string Command => "babyjail_max_account_age";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
switch (args.Length)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
var current = _cfg.GetCVar(CCVars.BabyJailMaxAccountAge);
|
||||
shell.WriteLine(Loc.GetString("babyjail-command-max-account-age-is", ("minutes", current)));
|
||||
break;
|
||||
}
|
||||
case > 1:
|
||||
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[0], out var minutes))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
_cfg.SetCVar(CCVars.BabyJailMaxAccountAge, minutes);
|
||||
shell.WriteLine(Loc.GetString("babyjail-command-max-account-age-set", ("minutes", minutes)));
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Server)]
|
||||
public sealed class BabyJailMinOverallHoursCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public override string Command => "babyjail_max_overall_minutes";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
switch (args.Length)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
var current = _cfg.GetCVar(CCVars.BabyJailMaxOverallMinutes);
|
||||
shell.WriteLine(Loc.GetString("babyjail-command-max-overall-minutes-is", ("minutes", current)));
|
||||
break;
|
||||
}
|
||||
case > 1:
|
||||
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[0], out var hours))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
_cfg.SetCVar(CCVars.BabyJailMaxOverallMinutes, hours);
|
||||
shell.WriteLine(Loc.GetString("babyjail-command-overall-minutes-set", ("hours", hours)));
|
||||
}
|
||||
}
|
||||
@@ -91,14 +91,29 @@ namespace Content.Server.Administration.Managers
|
||||
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-de-admin-message", ("exAdminName", session.Name)));
|
||||
_chat.DispatchServerMessage(session, Loc.GetString("admin-manager-became-normal-player-message"));
|
||||
|
||||
var plyData = session.ContentData()!;
|
||||
plyData.ExplicitlyDeadminned = true;
|
||||
UpdateDatabaseDeadminnedState(session, true);
|
||||
reg.Data.Active = false;
|
||||
|
||||
SendPermsChangedEvent(session);
|
||||
UpdateAdminStatus(session);
|
||||
}
|
||||
|
||||
private async void UpdateDatabaseDeadminnedState(ICommonSession player, bool newState)
|
||||
{
|
||||
try
|
||||
{
|
||||
// NOTE: This function gets called if you deadmin/readmin from a transient admin status.
|
||||
// (e.g. loginlocal)
|
||||
// In which case there may not be a database record.
|
||||
// The DB function handles this scenario fine, but it's worth noting.
|
||||
await _dbManager.UpdateAdminDeadminnedAsync(player.UserId, newState);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error("Failed to save deadmin state to database for {Admin}", player.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stealth(ICommonSession session)
|
||||
{
|
||||
if (!_admins.TryGetValue(session, out var reg))
|
||||
@@ -151,8 +166,7 @@ namespace Content.Server.Administration.Managers
|
||||
|
||||
_chat.DispatchServerMessage(session, Loc.GetString("admin-manager-became-admin-message"));
|
||||
|
||||
var plyData = session.ContentData()!;
|
||||
plyData.ExplicitlyDeadminned = false;
|
||||
UpdateDatabaseDeadminnedState(session, false);
|
||||
reg.Data.Active = true;
|
||||
|
||||
if (!reg.Data.Stealth)
|
||||
@@ -208,13 +222,13 @@ namespace Content.Server.Administration.Managers
|
||||
curAdmin.IsSpecialLogin = special;
|
||||
curAdmin.RankId = rankId;
|
||||
curAdmin.Data = aData;
|
||||
}
|
||||
|
||||
if (!player.ContentData()!.ExplicitlyDeadminned)
|
||||
{
|
||||
aData.Active = true;
|
||||
if (curAdmin.Data.Active)
|
||||
{
|
||||
aData.Active = true;
|
||||
|
||||
_chat.DispatchServerMessage(player, Loc.GetString("admin-manager-admin-permissions-updated-message"));
|
||||
_chat.DispatchServerMessage(player, Loc.GetString("admin-manager-admin-permissions-updated-message"));
|
||||
}
|
||||
}
|
||||
|
||||
if (player.ContentData()!.Stealthed)
|
||||
@@ -381,10 +395,8 @@ namespace Content.Server.Administration.Managers
|
||||
if (session.ContentData()!.Stealthed)
|
||||
reg.Data.Stealth = true;
|
||||
|
||||
if (!session.ContentData()!.ExplicitlyDeadminned)
|
||||
if (reg.Data.Active)
|
||||
{
|
||||
reg.Data.Active = true;
|
||||
|
||||
if (_cfg.GetCVar(CCVars.AdminAnnounceLogin))
|
||||
{
|
||||
if (reg.Data.Stealth)
|
||||
@@ -430,6 +442,7 @@ namespace Content.Server.Administration.Managers
|
||||
{
|
||||
Title = Loc.GetString("admin-manager-admin-data-host-title"),
|
||||
Flags = AdminFlagsHelper.Everything,
|
||||
Active = true,
|
||||
};
|
||||
|
||||
return (data, null, true);
|
||||
@@ -444,6 +457,12 @@ namespace Content.Server.Administration.Managers
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dbData.Suspended)
|
||||
{
|
||||
// Suspended admins don't count.
|
||||
return null;
|
||||
}
|
||||
|
||||
var flags = AdminFlags.None;
|
||||
|
||||
if (dbData.AdminRank != null)
|
||||
@@ -466,7 +485,8 @@ namespace Content.Server.Administration.Managers
|
||||
|
||||
var data = new AdminData
|
||||
{
|
||||
Flags = flags
|
||||
Flags = flags,
|
||||
Active = !dbData.Deadminned,
|
||||
};
|
||||
|
||||
if (dbData.Title != null && _cfg.GetCVar(CCVars.AdminUseCustomNamesAdminRank))
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Content.Server.Administration.Notes;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.Discord;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Server;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Administration.Managers;
|
||||
|
||||
/// <summary>
|
||||
/// This manager sends a webhook notification whenever a player with an active
|
||||
/// watchlist joins the server.
|
||||
/// </summary>
|
||||
public interface IWatchlistWebhookManager
|
||||
{
|
||||
void Initialize();
|
||||
void Update();
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using Content.Server.Administration.Notes;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.Discord;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Server;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Content.Server.Administration.Managers;
|
||||
|
||||
/// <summary>
|
||||
/// This manager sends a Discord webhook notification whenever a player with an active
|
||||
/// watchlist joins the server.
|
||||
/// </summary>
|
||||
public sealed class WatchlistWebhookManager : IWatchlistWebhookManager
|
||||
{
|
||||
[Dependency] private readonly IAdminNotesManager _adminNotes = default!;
|
||||
[Dependency] private readonly IBaseServer _baseServer = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly DiscordWebhook _discord = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private string _webhookUrl = default!;
|
||||
private TimeSpan _bufferTime;
|
||||
|
||||
private List<WatchlistConnection> watchlistConnections = new();
|
||||
private TimeSpan? _bufferStartTime;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = Logger.GetSawmill("discord");
|
||||
_cfg.OnValueChanged(CCVars.DiscordWatchlistConnectionBufferTime, SetBufferTime, true);
|
||||
_cfg.OnValueChanged(CCVars.DiscordWatchlistConnectionWebhook, SetWebhookUrl, true);
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
}
|
||||
|
||||
private void SetBufferTime(float bufferTimeSeconds)
|
||||
{
|
||||
_bufferTime = TimeSpan.FromSeconds(bufferTimeSeconds);
|
||||
}
|
||||
|
||||
private void SetWebhookUrl(string webhookUrl)
|
||||
{
|
||||
_webhookUrl = webhookUrl;
|
||||
}
|
||||
|
||||
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
if (e.NewStatus != SessionStatus.Connected)
|
||||
return;
|
||||
|
||||
var watchlists = await _adminNotes.GetActiveWatchlists(e.Session.UserId);
|
||||
|
||||
if (watchlists.Count == 0)
|
||||
return;
|
||||
|
||||
watchlistConnections.Add(new WatchlistConnection(e.Session.Name, watchlists));
|
||||
|
||||
if (_bufferTime > TimeSpan.Zero)
|
||||
{
|
||||
if (_bufferStartTime == null)
|
||||
_bufferStartTime = _gameTiming.RealTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
SendDiscordMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (_bufferStartTime != null && _gameTiming.RealTime > (_bufferStartTime + _bufferTime))
|
||||
{
|
||||
SendDiscordMessage();
|
||||
_bufferStartTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async void SendDiscordMessage()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_webhookUrl))
|
||||
return;
|
||||
|
||||
var webhookData = await _discord.GetWebhook(_webhookUrl);
|
||||
if (webhookData == null)
|
||||
return;
|
||||
|
||||
var webhookIdentifier = webhookData.Value.ToIdentifier();
|
||||
|
||||
var messageBuilder = new StringBuilder(Loc.GetString("discord-watchlist-connection-header",
|
||||
("players", watchlistConnections.Count),
|
||||
("serverName", _baseServer.ServerName)));
|
||||
|
||||
foreach (var connection in watchlistConnections)
|
||||
{
|
||||
messageBuilder.Append('\n');
|
||||
|
||||
var watchlist = connection.Watchlists.First();
|
||||
var expiry = watchlist.ExpirationTime?.ToUnixTimeSeconds();
|
||||
messageBuilder.Append(Loc.GetString("discord-watchlist-connection-entry",
|
||||
("playerName", connection.PlayerName),
|
||||
("message", watchlist.Message),
|
||||
("expiry", expiry ?? 0),
|
||||
("otherWatchlists", connection.Watchlists.Count - 1)));
|
||||
}
|
||||
|
||||
var payload = new WebhookPayload { Content = messageBuilder.ToString() };
|
||||
|
||||
await _discord.CreateMessage(webhookIdentifier, payload);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Error while sending discord watchlist connection message:\n{e}");
|
||||
}
|
||||
|
||||
// Clear the buffered list regardless of whether the message is sent successfully
|
||||
// This prevents infinitely buffering connections if we fail to send a message
|
||||
watchlistConnections.Clear();
|
||||
}
|
||||
|
||||
private sealed class WatchlistConnection
|
||||
{
|
||||
public string PlayerName;
|
||||
public List<AdminWatchlistRecord> Watchlists;
|
||||
|
||||
public WatchlistConnection(string playerName, List<AdminWatchlistRecord> watchlists)
|
||||
{
|
||||
PlayerName = playerName;
|
||||
Watchlists = watchlists;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,6 @@ public sealed class AdminSystem : EntitySystem
|
||||
|
||||
private readonly HashSet<NetUserId> _roundActivePlayers = new();
|
||||
public readonly PanicBunkerStatus PanicBunker = new();
|
||||
public readonly BabyJailStatus BabyJail = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -83,16 +82,6 @@ public sealed class AdminSystem : EntitySystem
|
||||
Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerMinOverallMinutes, OnPanicBunkerMinOverallMinutesChanged, true);
|
||||
|
||||
/*
|
||||
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
||||
*/
|
||||
|
||||
// Baby Jail Settings
|
||||
Subs.CVar(_config, CCVars.BabyJailEnabled, OnBabyJailChanged, true);
|
||||
Subs.CVar(_config, CCVars.BabyJailShowReason, OnBabyJailShowReasonChanged, true);
|
||||
Subs.CVar(_config, CCVars.BabyJailMaxAccountAge, OnBabyJailMaxAccountAgeChanged, true);
|
||||
Subs.CVar(_config, CCVars.BabyJailMaxOverallMinutes, OnBabyJailMaxOverallMinutesChanged, true);
|
||||
|
||||
SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged);
|
||||
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
|
||||
@@ -279,17 +268,6 @@ public sealed class AdminSystem : EntitySystem
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailChanged(bool enabled)
|
||||
{
|
||||
BabyJail.Enabled = enabled;
|
||||
_chat.SendAdminAlert(Loc.GetString(enabled
|
||||
? "admin-ui-baby-jail-enabled-admin-alert"
|
||||
: "admin-ui-baby-jail-disabled-admin-alert"
|
||||
));
|
||||
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerDisableWithAdminsChanged(bool enabled)
|
||||
{
|
||||
PanicBunker.DisableWithAdmins = enabled;
|
||||
@@ -314,36 +292,18 @@ public sealed class AdminSystem : EntitySystem
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailShowReasonChanged(bool enabled)
|
||||
{
|
||||
BabyJail.ShowReason = enabled;
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerMinAccountAgeChanged(int minutes)
|
||||
{
|
||||
PanicBunker.MinAccountAgeMinutes = minutes;
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailMaxAccountAgeChanged(int minutes)
|
||||
{
|
||||
BabyJail.MaxAccountAgeMinutes = minutes;
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerMinOverallMinutesChanged(int minutes)
|
||||
{
|
||||
PanicBunker.MinOverallMinutes = minutes;
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailMaxOverallMinutesChanged(int minutes)
|
||||
{
|
||||
BabyJail.MaxOverallMinutes = minutes;
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void UpdatePanicBunker()
|
||||
{
|
||||
var hasAdmins = false;
|
||||
@@ -390,15 +350,6 @@ public sealed class AdminSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void SendBabyJailStatusAll()
|
||||
{
|
||||
var ev = new BabyJailChangedEvent(BabyJail);
|
||||
foreach (var admin in _adminManager.AllAdmins)
|
||||
{
|
||||
RaiseNetworkEvent(ev, admin);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erases a player from the round.
|
||||
/// This removes them and any trace of them from the round, deleting their
|
||||
|
||||
@@ -76,7 +76,8 @@ namespace Content.Server.Administration.UI
|
||||
Title = p.a.Title,
|
||||
RankId = p.a.AdminRankId,
|
||||
UserId = new NetUserId(p.a.UserId),
|
||||
UserName = p.lastUserName
|
||||
UserName = p.lastUserName,
|
||||
Suspended = p.a.Suspended,
|
||||
}).ToArray(),
|
||||
|
||||
AdminRanks = _adminRanks.ToDictionary(a => a.Id, a => new PermissionsEuiState.AdminRankData
|
||||
@@ -255,6 +256,7 @@ namespace Content.Server.Administration.UI
|
||||
admin.Title = ua.Title;
|
||||
admin.AdminRankId = ua.RankId;
|
||||
admin.Flags = GenAdminFlagList(ua.PosFlags, ua.NegFlags);
|
||||
admin.Suspended = ua.Suspended;
|
||||
|
||||
await _db.UpdateAdminAsync(admin);
|
||||
|
||||
@@ -335,7 +337,8 @@ namespace Content.Server.Administration.UI
|
||||
Flags = GenAdminFlagList(ca.PosFlags, ca.NegFlags),
|
||||
AdminRankId = ca.RankId,
|
||||
UserId = userId.UserId,
|
||||
Title = ca.Title
|
||||
Title = ca.Title,
|
||||
Suspended = ca.Suspended,
|
||||
};
|
||||
|
||||
await _db.AddAdminAsync(admin);
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed partial class ConnectionManager
|
||||
{
|
||||
private PlayerConnectionWhitelistPrototype[]? _whitelists;
|
||||
|
||||
public void PostInit()
|
||||
private void InitializeWhitelist()
|
||||
{
|
||||
_cfg.OnValueChanged(CCVars.WhitelistPrototypeList, UpdateWhitelists, true);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||
using System.Runtime.InteropServices;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Connection.IPIntel;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
@@ -40,6 +41,8 @@ namespace Content.Server.Connection
|
||||
/// <param name="user">The user to give a temporary bypass.</param>
|
||||
/// <param name="duration">How long the bypass should last for.</param>
|
||||
void AddTemporaryConnectBypass(NetUserId user, TimeSpan duration);
|
||||
|
||||
void Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -57,16 +60,24 @@ namespace Content.Server.Connection
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly IHttpClientHolder _http = default!;
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = [];
|
||||
private IPIntel.IPIntel _ipintel = default!;
|
||||
|
||||
public void PostInit()
|
||||
{
|
||||
InitializeWhitelist();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("connections");
|
||||
|
||||
_ipintel = new IPIntel.IPIntel(new IPIntelApi(_http, _cfg), _db, _cfg, _logManager, _chatManager, _gameTiming);
|
||||
|
||||
_netMgr.Connecting += NetMgrOnConnecting;
|
||||
_netMgr.AssignUserIdCallback = AssignUserIdCallback;
|
||||
_plyMgr.PlayerStatusChanged += PlayerStatusChanged;
|
||||
@@ -83,6 +94,18 @@ namespace Content.Server.Connection
|
||||
time = newTime;
|
||||
}
|
||||
|
||||
public async void Update()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _ipintel.Update();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error("IPIntel update failed:" + e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
private async Task<NetApproval> HandleApproval(NetApprovalEventArgs eventArgs)
|
||||
{
|
||||
@@ -260,14 +283,6 @@ namespace Content.Server.Connection
|
||||
}
|
||||
}
|
||||
|
||||
if (_cfg.GetCVar(CCVars.BabyJailEnabled) && adminData == null)
|
||||
{
|
||||
var result = await IsInvalidConnectionDueToBabyJail(userId, e);
|
||||
|
||||
if (result.IsInvalid)
|
||||
return (ConnectionDenyReason.BabyJail, result.Reason, null);
|
||||
}
|
||||
|
||||
var wasInGame = EntitySystem.TryGet<GameTicker>(out var ticker) &&
|
||||
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
|
||||
status == PlayerGameStatus.JoinedGame;
|
||||
@@ -291,7 +306,7 @@ namespace Content.Server.Connection
|
||||
{
|
||||
_sawmill.Error("Whitelist enabled but no whitelists loaded.");
|
||||
// Misconfigured, deny everyone.
|
||||
return (ConnectionDenyReason.Whitelist, Loc.GetString("whitelist-misconfigured"), null);
|
||||
return (ConnectionDenyReason.Whitelist, Loc.GetString("generic-misconfigured"), null);
|
||||
}
|
||||
|
||||
foreach (var whitelist in _whitelists)
|
||||
@@ -314,75 +329,18 @@ namespace Content.Server.Connection
|
||||
}
|
||||
}
|
||||
|
||||
// ALWAYS keep this at the end, to preserve the API limit.
|
||||
if (_cfg.GetCVar(CCVars.GameIPIntelEnabled) && adminData == null)
|
||||
{
|
||||
var result = await _ipintel.IsVpnOrProxy(e);
|
||||
|
||||
if (result.IsBad)
|
||||
return (ConnectionDenyReason.IPChecks, result.Reason, null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<(bool IsInvalid, string Reason)> IsInvalidConnectionDueToBabyJail(NetUserId userId, NetConnectingArgs e)
|
||||
{
|
||||
// If you're whitelisted then bypass this whole thing
|
||||
if (await _db.GetWhitelistStatusAsync(userId))
|
||||
return (false, "");
|
||||
|
||||
// Initial cvar retrieval
|
||||
var showReason = _cfg.GetCVar(CCVars.BabyJailShowReason);
|
||||
var reason = _cfg.GetCVar(CCVars.BabyJailCustomReason);
|
||||
var maxAccountAgeMinutes = _cfg.GetCVar(CCVars.BabyJailMaxAccountAge);
|
||||
var maxPlaytimeMinutes = _cfg.GetCVar(CCVars.BabyJailMaxOverallMinutes);
|
||||
|
||||
// Wait some time to lookup data
|
||||
var record = await _db.GetPlayerRecordByUserId(userId);
|
||||
|
||||
// No player record = new account or the DB is having a skill issue
|
||||
if (record == null)
|
||||
return (false, "");
|
||||
|
||||
var isAccountAgeInvalid = record.FirstSeenTime.CompareTo(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(maxAccountAgeMinutes)) <= 0;
|
||||
|
||||
if (isAccountAgeInvalid)
|
||||
{
|
||||
_sawmill.Debug($"Baby jail will deny {userId} for account age {record.FirstSeenTime}"); // Remove on or after 2024-09
|
||||
}
|
||||
|
||||
if (isAccountAgeInvalid && showReason)
|
||||
{
|
||||
var locAccountReason = reason != string.Empty
|
||||
? reason
|
||||
: Loc.GetString("baby-jail-account-denied-reason",
|
||||
("reason",
|
||||
Loc.GetString(
|
||||
"baby-jail-account-reason-account",
|
||||
("minutes", maxAccountAgeMinutes))));
|
||||
|
||||
return (true, locAccountReason);
|
||||
}
|
||||
|
||||
var overallTime = ( await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
|
||||
var isTotalPlaytimeInvalid = overallTime != null && overallTime.TimeSpent.TotalMinutes >= maxPlaytimeMinutes;
|
||||
|
||||
if (isTotalPlaytimeInvalid)
|
||||
{
|
||||
_sawmill.Debug($"Baby jail will deny {userId} for playtime {overallTime!.TimeSpent}"); // Remove on or after 2024-09
|
||||
}
|
||||
|
||||
if (isTotalPlaytimeInvalid && showReason)
|
||||
{
|
||||
var locPlaytimeReason = reason != string.Empty
|
||||
? reason
|
||||
: Loc.GetString("baby-jail-account-denied-reason",
|
||||
("reason",
|
||||
Loc.GetString(
|
||||
"baby-jail-account-reason-overall",
|
||||
("minutes", maxPlaytimeMinutes))));
|
||||
|
||||
return (true, locPlaytimeReason);
|
||||
}
|
||||
|
||||
if (!showReason && isTotalPlaytimeInvalid || isAccountAgeInvalid)
|
||||
return (true, Loc.GetString("baby-jail-account-denied"));
|
||||
|
||||
return (false, "");
|
||||
}
|
||||
|
||||
private bool HasTemporaryBypass(NetUserId user)
|
||||
{
|
||||
return _temporaryBypasses.TryGetValue(user, out var time) && time > _gameTiming.RealTime;
|
||||
|
||||
387
Content.Server/Connection/IPIntel/IPIntel.cs
Normal file
387
Content.Server/Connection/IPIntel/IPIntel.cs
Normal file
@@ -0,0 +1,387 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Connection.IPIntel;
|
||||
|
||||
// Handles checking/warning if the connecting IP address is sus.
|
||||
public sealed class IPIntel
|
||||
{
|
||||
private readonly IIPIntelApi _api;
|
||||
private readonly IServerDbManager _db;
|
||||
private readonly IChatManager _chatManager;
|
||||
private readonly IGameTiming _gameTiming;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public IPIntel(IIPIntelApi api,
|
||||
IServerDbManager db,
|
||||
IConfigurationManager cfg,
|
||||
ILogManager logManager,
|
||||
IChatManager chatManager,
|
||||
IGameTiming gameTiming)
|
||||
{
|
||||
_api = api;
|
||||
_db = db;
|
||||
_chatManager = chatManager;
|
||||
_gameTiming = gameTiming;
|
||||
|
||||
_sawmill = logManager.GetSawmill("ipintel");
|
||||
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelEmail, b => _contactEmail = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelEnabled, b => _enabled = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelRejectUnknown, b => _rejectUnknown = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelRejectBad, b => _rejectBad = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelRejectRateLimited, b => _rejectLimited = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelMaxMinute, b => _minute.Limit = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelMaxDay, b => _day.Limit = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelBackOffSeconds, b => _backoffSeconds = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelCleanupMins, b => _cleanupMins = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelBadRating, b => _rating = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelCacheLength, b => _cacheDays = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelExemptPlaytime, b => _exemptPlaytime = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelAlertAdminReject, b => _alertAdminReject = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelAlertAdminWarnRating, b => _alertAdminWarn = b, true);
|
||||
}
|
||||
|
||||
internal struct Ratelimits
|
||||
{
|
||||
public bool RateLimited;
|
||||
public bool LimitHasBeenHandled;
|
||||
public int CurrentRequests;
|
||||
public int Limit;
|
||||
public TimeSpan LastRatelimited;
|
||||
}
|
||||
|
||||
// Self-managed preemptive rate limits.
|
||||
private Ratelimits _day;
|
||||
private Ratelimits _minute;
|
||||
|
||||
// Next time we need to clean the database of stale cached IPIntel results.
|
||||
private TimeSpan _nextClean;
|
||||
|
||||
// Responsive backoff if we hit a Too Many Requests API error.
|
||||
private int _failedRequests;
|
||||
private TimeSpan _releasePeriod;
|
||||
|
||||
// CCVars
|
||||
private string? _contactEmail;
|
||||
private bool _enabled;
|
||||
private bool _rejectUnknown;
|
||||
private bool _rejectBad;
|
||||
private bool _rejectLimited;
|
||||
private bool _alertAdminReject;
|
||||
private int _backoffSeconds;
|
||||
private int _cleanupMins;
|
||||
private TimeSpan _cacheDays;
|
||||
private TimeSpan _exemptPlaytime;
|
||||
private float _rating;
|
||||
private float _alertAdminWarn;
|
||||
|
||||
public async Task<(bool IsBad, string Reason)> IsVpnOrProxy(NetConnectingArgs e)
|
||||
{
|
||||
// Check Exemption flags, let them skip if they have them.
|
||||
var flags = await _db.GetBanExemption(e.UserId);
|
||||
if ((flags & (ServerBanExemptFlags.Datacenter | ServerBanExemptFlags.BlacklistedRange)) != 0)
|
||||
{
|
||||
return (false, string.Empty);
|
||||
}
|
||||
|
||||
// Check playtime, if 0 we skip this check. If player has more playtime then _exemptPlaytime is configured for then they get to skip this check.
|
||||
// Helps with saving your limited request limit.
|
||||
if (_exemptPlaytime != TimeSpan.Zero)
|
||||
{
|
||||
var overallTime = ( await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
|
||||
if (overallTime != null && overallTime.TimeSpent >= _exemptPlaytime)
|
||||
{
|
||||
return (false, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
var ip = e.IP.Address;
|
||||
var username = e.UserName;
|
||||
|
||||
// Is this a local ip address?
|
||||
if (IsAddressReservedIpv4(ip) || IsAddressReservedIpv6(ip))
|
||||
{
|
||||
_sawmill.Warning($"{e.UserName} joined using a local address. Do you need IPIntel? Or is something terribly misconfigured on your server? Trusting this connection.");
|
||||
return (false, string.Empty);
|
||||
}
|
||||
|
||||
// Check our cache
|
||||
var query = await _db.GetIPIntelCache(ip);
|
||||
|
||||
// Does it exist?
|
||||
if (query != null)
|
||||
{
|
||||
// Skip to score check if result is older than _cacheDays
|
||||
if (DateTime.UtcNow - query.Time <= _cacheDays)
|
||||
{
|
||||
var score = query.Score;
|
||||
return ScoreCheck(score, username);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure our contact email is good to use.
|
||||
if (string.IsNullOrEmpty(_contactEmail) || !_contactEmail.Contains('@') || !_contactEmail.Contains('.'))
|
||||
{
|
||||
_sawmill.Error("IPIntel is enabled, but contact email is empty or not a valid email, treating this connection like an unknown IPIntel response.");
|
||||
return _rejectUnknown ? (true, Loc.GetString("generic-misconfigured")) : (false, string.Empty);
|
||||
}
|
||||
|
||||
var apiResult = await QueryIPIntelRateLimited(ip);
|
||||
switch (apiResult.Code)
|
||||
{
|
||||
case IPIntelResultCode.Success:
|
||||
await Task.Run(() => _db.UpsertIPIntelCache(DateTime.UtcNow, ip, apiResult.Score));
|
||||
return ScoreCheck(apiResult.Score, username);
|
||||
|
||||
case IPIntelResultCode.RateLimited:
|
||||
return _rejectLimited ? (true, Loc.GetString("ipintel-server-ratelimited")) : (false, string.Empty);
|
||||
|
||||
case IPIntelResultCode.Errored:
|
||||
return _rejectUnknown ? (true, Loc.GetString("ipintel-unknown")) : (false, string.Empty);
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IPIntelResult> QueryIPIntelRateLimited(IPAddress ip)
|
||||
{
|
||||
IncrementAndTestRateLimit(ref _day, TimeSpan.FromDays(1), "daily");
|
||||
IncrementAndTestRateLimit(ref _minute, TimeSpan.FromMinutes(1), "minute");
|
||||
|
||||
if (_minute.RateLimited || _day.RateLimited || CheckSuddenRateLimit())
|
||||
return new IPIntelResult(0, IPIntelResultCode.RateLimited);
|
||||
|
||||
// Info about flag B: https://getipintel.net/free-proxy-vpn-tor-detection-api/#flagsb
|
||||
// TLDR: We don't care about knowing if a connection is compromised.
|
||||
// We just want to know if it's a vpn. This also speeds up the request by quite a bit. (A full scan can take 200ms to 5 seconds. This will take at most 120ms)
|
||||
using var request = await _api.GetIPScore(ip);
|
||||
|
||||
if (request.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_sawmill.Warning($"We hit the IPIntel request limit at some point. (Current limit count: Minute: {_minute.CurrentRequests} Day: {_day.CurrentRequests})");
|
||||
CalculateSuddenRatelimit();
|
||||
return new IPIntelResult(0, IPIntelResultCode.RateLimited);
|
||||
}
|
||||
|
||||
var response = await request.Content.ReadAsStringAsync();
|
||||
var score = Parse.Float(response);
|
||||
|
||||
if (request.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
_failedRequests = 0;
|
||||
return new IPIntelResult(score, IPIntelResultCode.Success);
|
||||
}
|
||||
|
||||
if (ErrorMessages.TryGetValue(response, out var errorMessage))
|
||||
{
|
||||
_sawmill.Error($"IPIntel returned error {response}: {errorMessage}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Oh boy, we don't know this error.
|
||||
_sawmill.Error($"IPIntel returned {response} (Status code: {request.StatusCode})... we don't know what this error code is. Please make an issue in upstream!");
|
||||
}
|
||||
|
||||
return new IPIntelResult(0, IPIntelResultCode.Errored);
|
||||
}
|
||||
|
||||
private bool CheckSuddenRateLimit()
|
||||
{
|
||||
return _failedRequests >= 1 && _releasePeriod > _gameTiming.RealTime;
|
||||
}
|
||||
|
||||
private void CalculateSuddenRatelimit()
|
||||
{
|
||||
_failedRequests++;
|
||||
_releasePeriod = _gameTiming.RealTime + TimeSpan.FromSeconds(_failedRequests * _backoffSeconds);
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, string> ErrorMessages = new()
|
||||
{
|
||||
["-1"] = "Invalid/No input.",
|
||||
["-2"] = "Invalid IP address.",
|
||||
["-3"] = "Unroutable address / private address given to the api. Make an issue in upstream as it should have been handled.",
|
||||
["-4"] = "Unable to reach IPIntel database. Perhaps it's down?",
|
||||
["-5"] = "Server's IP/Contact may have been banned, go to getipintel.net and make contact to be unbanned.",
|
||||
["-6"] = "You did not provide any contact information with your query or the contact information is invalid.",
|
||||
};
|
||||
|
||||
private void IncrementAndTestRateLimit(ref Ratelimits ratelimits, TimeSpan expireInterval, string name)
|
||||
{
|
||||
if (ratelimits.CurrentRequests < ratelimits.Limit)
|
||||
{
|
||||
ratelimits.CurrentRequests += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ShouldLiftRateLimit(in ratelimits, expireInterval))
|
||||
{
|
||||
_sawmill.Info($"IPIntel {name} rate limit lifted. We are back to normal.");
|
||||
ratelimits.RateLimited = false;
|
||||
ratelimits.CurrentRequests = 0;
|
||||
ratelimits.LimitHasBeenHandled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ratelimits.LimitHasBeenHandled)
|
||||
return;
|
||||
|
||||
_sawmill.Warning($"We just hit our last {name} IPIntel limit ({ratelimits.Limit})");
|
||||
ratelimits.RateLimited = true;
|
||||
ratelimits.LimitHasBeenHandled = true;
|
||||
ratelimits.LastRatelimited = _gameTiming.RealTime;
|
||||
}
|
||||
|
||||
private bool ShouldLiftRateLimit(in Ratelimits ratelimits, TimeSpan liftingTime)
|
||||
{
|
||||
// Should we raise this limit now?
|
||||
return ratelimits.RateLimited && _gameTiming.RealTime >= ratelimits.LastRatelimited + liftingTime;
|
||||
}
|
||||
|
||||
private (bool, string Empty) ScoreCheck(float score, string username)
|
||||
{
|
||||
var decisionIsReject = score > _rating;
|
||||
|
||||
if (_alertAdminWarn != 0f && _alertAdminWarn < score && !decisionIsReject)
|
||||
{
|
||||
_chatManager.SendAdminAlert(Loc.GetString("admin-alert-ipintel-warning",
|
||||
("player", username),
|
||||
("percent", Math.Round(score))));
|
||||
}
|
||||
|
||||
if (!decisionIsReject)
|
||||
return (false, string.Empty);
|
||||
|
||||
if (_alertAdminReject)
|
||||
{
|
||||
_chatManager.SendAdminAlert(Loc.GetString("admin-alert-ipintel-blocked",
|
||||
("player", username),
|
||||
("percent", Math.Round(score))));
|
||||
}
|
||||
|
||||
return _rejectBad ? (true, Loc.GetString("ipintel-suspicious")) : (false, string.Empty);
|
||||
}
|
||||
|
||||
public async Task Update()
|
||||
{
|
||||
if (_enabled && _gameTiming.RealTime >= _nextClean)
|
||||
{
|
||||
_nextClean = _gameTiming.RealTime + TimeSpan.FromMinutes(_cleanupMins);
|
||||
await _db.CleanIPIntelCache(_cacheDays);
|
||||
}
|
||||
}
|
||||
|
||||
// Stolen from Lidgren.Network (Space Wizards Edition) (NetReservedAddress.cs)
|
||||
// Modified with IPV6 on top
|
||||
private static int Ipv4(byte a, byte b, byte c, byte d)
|
||||
{
|
||||
return (a << 24) | (b << 16) | (c << 8) | d;
|
||||
}
|
||||
|
||||
// From miniupnpc
|
||||
private static readonly (int ip, int mask)[] ReservedRangesIpv4 =
|
||||
[
|
||||
// @formatter:off
|
||||
(Ipv4(0, 0, 0, 0), 8 ), // RFC1122 "This host on this network"
|
||||
(Ipv4(10, 0, 0, 0), 8 ), // RFC1918 Private-Use
|
||||
(Ipv4(100, 64, 0, 0), 10), // RFC6598 Shared Address Space
|
||||
(Ipv4(127, 0, 0, 0), 8 ), // RFC1122 Loopback
|
||||
(Ipv4(169, 254, 0, 0), 16), // RFC3927 Link-Local
|
||||
(Ipv4(172, 16, 0, 0), 12), // RFC1918 Private-Use
|
||||
(Ipv4(192, 0, 0, 0), 24), // RFC6890 IETF Protocol Assignments
|
||||
(Ipv4(192, 0, 2, 0), 24), // RFC5737 Documentation (TEST-NET-1)
|
||||
(Ipv4(192, 31, 196, 0), 24), // RFC7535 AS112-v4
|
||||
(Ipv4(192, 52, 193, 0), 24), // RFC7450 AMT
|
||||
(Ipv4(192, 88, 99, 0), 24), // RFC7526 6to4 Relay Anycast
|
||||
(Ipv4(192, 168, 0, 0), 16), // RFC1918 Private-Use
|
||||
(Ipv4(192, 175, 48, 0), 24), // RFC7534 Direct Delegation AS112 Service
|
||||
(Ipv4(198, 18, 0, 0), 15), // RFC2544 Benchmarking
|
||||
(Ipv4(198, 51, 100, 0), 24), // RFC5737 Documentation (TEST-NET-2)
|
||||
(Ipv4(203, 0, 113, 0), 24), // RFC5737 Documentation (TEST-NET-3)
|
||||
(Ipv4(224, 0, 0, 0), 4 ), // RFC1112 Multicast
|
||||
(Ipv4(240, 0, 0, 0), 4 ), // RFC1112 Reserved for Future Use + RFC919 Limited Broadcast
|
||||
// @formatter:on
|
||||
];
|
||||
|
||||
private static UInt128 ToAddressBytes(string ip)
|
||||
{
|
||||
return BinaryPrimitives.ReadUInt128BigEndian(IPAddress.Parse(ip).GetAddressBytes());
|
||||
}
|
||||
|
||||
private static readonly (UInt128 ip, int mask)[] ReservedRangesIpv6 =
|
||||
[
|
||||
(ToAddressBytes("::1"), 128), // "This host on this network"
|
||||
(ToAddressBytes("::ffff:0:0"), 96), // IPv4-mapped addresses
|
||||
(ToAddressBytes("::ffff:0:0:0"), 96), // IPv4-translated addresses
|
||||
(ToAddressBytes("64:ff9b:1::"), 48), // IPv4/IPv6 translation
|
||||
(ToAddressBytes("100::"), 64), // Discard prefix
|
||||
(ToAddressBytes("2001:20::"), 28), // ORCHIDv2
|
||||
(ToAddressBytes("2001:db8::"), 32), // Addresses used in documentation and example source code
|
||||
(ToAddressBytes("3fff::"), 20), // Addresses used in documentation and example source code
|
||||
(ToAddressBytes("5f00::"), 16), // IPv6 Segment Routing (SRv6)
|
||||
(ToAddressBytes("fc00::"), 7), // Unique local address
|
||||
];
|
||||
|
||||
internal static bool IsAddressReservedIpv4(IPAddress address)
|
||||
{
|
||||
if (address.AddressFamily != AddressFamily.InterNetwork)
|
||||
return false;
|
||||
|
||||
Span<byte> ipBitsByte = stackalloc byte[4];
|
||||
address.TryWriteBytes(ipBitsByte, out _);
|
||||
var ipBits = BinaryPrimitives.ReadInt32BigEndian(ipBitsByte);
|
||||
|
||||
foreach (var (reservedIp, maskBits) in ReservedRangesIpv4)
|
||||
{
|
||||
var mask = uint.MaxValue << (32 - maskBits);
|
||||
if ((ipBits & mask) == (reservedIp & mask))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsAddressReservedIpv6(IPAddress address)
|
||||
{
|
||||
if (address.AddressFamily != AddressFamily.InterNetworkV6)
|
||||
return false;
|
||||
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
return IsAddressReservedIpv4(address.MapToIPv4());
|
||||
|
||||
Span<byte> ipBitsByte = stackalloc byte[16];
|
||||
address.TryWriteBytes(ipBitsByte, out _);
|
||||
var ipBits = BinaryPrimitives.ReadInt128BigEndian(ipBitsByte);
|
||||
|
||||
foreach (var (reservedIp, maskBits) in ReservedRangesIpv6)
|
||||
{
|
||||
var mask = UInt128.MaxValue << (128 - maskBits);
|
||||
if (((UInt128) ipBits & mask ) == (reservedIp & mask))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public readonly record struct IPIntelResult(float Score, IPIntelResultCode Code);
|
||||
|
||||
public enum IPIntelResultCode : byte
|
||||
{
|
||||
Success = 0,
|
||||
RateLimited,
|
||||
Errored,
|
||||
}
|
||||
}
|
||||
40
Content.Server/Connection/IPIntel/IPIntelAPI.cs
Normal file
40
Content.Server/Connection/IPIntel/IPIntelAPI.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Server.Connection.IPIntel;
|
||||
|
||||
public interface IIPIntelApi
|
||||
{
|
||||
Task<HttpResponseMessage> GetIPScore(IPAddress ip);
|
||||
}
|
||||
|
||||
public sealed class IPIntelApi : IIPIntelApi
|
||||
{
|
||||
// Holds-The-HttpClient
|
||||
private readonly IHttpClientHolder _http;
|
||||
|
||||
// CCvars
|
||||
private string? _contactEmail;
|
||||
private string? _baseUrl;
|
||||
private string? _flags;
|
||||
|
||||
public IPIntelApi(
|
||||
IHttpClientHolder http,
|
||||
IConfigurationManager cfg)
|
||||
{
|
||||
_http = http;
|
||||
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelEmail, b => _contactEmail = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelBase, b => _baseUrl = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelFlags, b => _flags = b, true);
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetIPScore(IPAddress ip)
|
||||
{
|
||||
return _http.Client.GetAsync($"{_baseUrl}/check.php?ip={ip}&contact={_contactEmail}&flags={_flags}");
|
||||
}
|
||||
}
|
||||
@@ -751,6 +751,20 @@ namespace Content.Server.Database
|
||||
existing.Flags = admin.Flags;
|
||||
existing.Title = admin.Title;
|
||||
existing.AdminRankId = admin.AdminRankId;
|
||||
existing.Deadminned = admin.Deadminned;
|
||||
existing.Suspended = admin.Suspended;
|
||||
|
||||
await db.DbContext.SaveChangesAsync(cancel);
|
||||
}
|
||||
|
||||
public async Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel)
|
||||
{
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
var adminRecord = db.DbContext.Admin.Where(a => a.UserId == userId);
|
||||
await adminRecord.ExecuteUpdateAsync(
|
||||
set => set.SetProperty(p => p.Deadminned, deadminned),
|
||||
cancellationToken: cancel);
|
||||
|
||||
await db.DbContext.SaveChangesAsync(cancel);
|
||||
}
|
||||
@@ -1720,6 +1734,73 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
|
||||
#endregion
|
||||
|
||||
# region IPIntel
|
||||
|
||||
public async Task<bool> UpsertIPIntelCache(DateTime time, IPAddress ip, float score)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
var existing = await db.DbContext.IPIntelCache
|
||||
.Where(w => ip.Equals(w.Address))
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
var newCache = new IPIntelCache
|
||||
{
|
||||
Time = time,
|
||||
Address = ip,
|
||||
Score = score,
|
||||
};
|
||||
db.DbContext.IPIntelCache.Add(newCache);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.Time = time;
|
||||
existing.Score = score;
|
||||
}
|
||||
|
||||
await Task.Delay(5000);
|
||||
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
_opsLog.Warning("IPIntel UPSERT failed with a db exception... retrying.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IPIntelCache?> GetIPIntelCache(IPAddress ip)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
return await db.DbContext.IPIntelCache
|
||||
.SingleOrDefaultAsync(w => ip.Equals(w.Address));
|
||||
}
|
||||
|
||||
public async Task<bool> CleanIPIntelCache(TimeSpan range)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
// Calculating this here cause otherwise sqlite whines.
|
||||
var cutoffTime = DateTime.UtcNow.Subtract(range);
|
||||
|
||||
await db.DbContext.IPIntelCache
|
||||
.Where(w => w.Time <= cutoffTime)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
|
||||
// Normalize DateTimes here so they're always Utc. Thanks.
|
||||
protected abstract DateTime NormalizeDatabaseTime(DateTime time);
|
||||
|
||||
@@ -217,6 +217,16 @@ namespace Content.Server.Database
|
||||
Task AddAdminAsync(Admin admin, CancellationToken cancel = default);
|
||||
Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default);
|
||||
|
||||
/// <summary>
|
||||
/// Update whether an admin has voluntarily deadminned.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This does nothing if the player is not an admin.
|
||||
/// </remarks>
|
||||
/// <param name="userId">The user ID of the admin.</param>
|
||||
/// <param name="deadminned">Whether the admin is deadminned or not.</param>
|
||||
Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel = default);
|
||||
|
||||
Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default);
|
||||
Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
|
||||
Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
|
||||
@@ -322,6 +332,14 @@ namespace Content.Server.Database
|
||||
|
||||
#endregion
|
||||
|
||||
#region IPintel
|
||||
|
||||
Task<bool> UpsertIPIntelCache(DateTime time, IPAddress ip, float score);
|
||||
Task<IPIntelCache?> GetIPIntelCache(IPAddress ip);
|
||||
Task<bool> CleanIPIntelCache(TimeSpan range);
|
||||
|
||||
#endregion
|
||||
|
||||
#region DB Notifications
|
||||
|
||||
void SubscribeToNotifications(Action<DatabaseNotification> handler);
|
||||
@@ -666,6 +684,12 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.UpdateAdminAsync(admin, cancel));
|
||||
}
|
||||
|
||||
public Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel = default)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.UpdateAdminDeadminnedAsync(userId, deadminned, cancel));
|
||||
}
|
||||
|
||||
public Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
@@ -991,6 +1015,23 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.RemoveJobWhitelist(player, job));
|
||||
}
|
||||
|
||||
public Task<bool> UpsertIPIntelCache(DateTime time, IPAddress ip, float score)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.UpsertIPIntelCache(time, ip, score));
|
||||
}
|
||||
|
||||
public Task<IPIntelCache?> GetIPIntelCache(IPAddress ip)
|
||||
{
|
||||
return RunDbCommand(() => _db.GetIPIntelCache(ip));
|
||||
}
|
||||
|
||||
public Task<bool> CleanIPIntelCache(TimeSpan range)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.CleanIPIntelCache(range));
|
||||
}
|
||||
|
||||
public void SubscribeToNotifications(Action<DatabaseNotification> handler)
|
||||
{
|
||||
lock (_notificationHandlers)
|
||||
|
||||
@@ -47,6 +47,8 @@ namespace Content.Server.Entry
|
||||
private PlayTimeTrackingManager? _playTimeTracking;
|
||||
private IEntitySystemManager? _sysMan;
|
||||
private IServerDbManager? _dbManager;
|
||||
private IWatchlistWebhookManager _watchlistWebhookManager = default!;
|
||||
private IConnectionManager? _connectionManager;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Init()
|
||||
@@ -91,8 +93,10 @@ namespace Content.Server.Entry
|
||||
_voteManager = IoCManager.Resolve<IVoteManager>();
|
||||
_updateManager = IoCManager.Resolve<ServerUpdateManager>();
|
||||
_playTimeTracking = IoCManager.Resolve<PlayTimeTrackingManager>();
|
||||
_connectionManager = IoCManager.Resolve<IConnectionManager>();
|
||||
_sysMan = IoCManager.Resolve<IEntitySystemManager>();
|
||||
_dbManager = IoCManager.Resolve<IServerDbManager>();
|
||||
_watchlistWebhookManager = IoCManager.Resolve<IWatchlistWebhookManager>();
|
||||
|
||||
logManager.GetSawmill("Storage").Level = LogLevel.Info;
|
||||
logManager.GetSawmill("db.ef").Level = LogLevel.Info;
|
||||
@@ -110,6 +114,7 @@ namespace Content.Server.Entry
|
||||
_voteManager.Initialize();
|
||||
_updateManager.Initialize();
|
||||
_playTimeTracking.Initialize();
|
||||
_watchlistWebhookManager.Initialize();
|
||||
IoCManager.Resolve<JobWhitelistManager>().Initialize();
|
||||
IoCManager.Resolve<PlayerRateLimitManager>().Initialize();
|
||||
}
|
||||
@@ -166,6 +171,8 @@ namespace Content.Server.Entry
|
||||
case ModUpdateLevel.FramePostEngine:
|
||||
_updateManager.Update();
|
||||
_playTimeTracking?.Update();
|
||||
_watchlistWebhookManager.Update();
|
||||
_connectionManager?.Update();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ public sealed partial class PuddleSystem
|
||||
Spawn("PuddleSparkle", xformQuery.GetComponent(uid).Coordinates);
|
||||
QueueDel(uid);
|
||||
}
|
||||
|
||||
_solutionContainerSystem.UpdateChemicals(puddle.Solution.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.GameTicking;
|
||||
@@ -43,15 +44,11 @@ namespace Content.Server.GameTicking
|
||||
jObject["name"] = _baseServer.ServerName;
|
||||
jObject["map"] = _gameMapManager.GetSelectedMap()?.MapName;
|
||||
jObject["round_id"] = _gameTicker.RoundId;
|
||||
jObject["players"] = _playerManager.PlayerCount;
|
||||
jObject["players"] = _cfg.GetCVar(CCVars.AdminsCountInReportedPlayerCount)
|
||||
? _playerManager.PlayerCount
|
||||
: _playerManager.PlayerCount - _adminManager.ActiveAdmins.Count();
|
||||
jObject["soft_max_players"] = _cfg.GetCVar(CCVars.SoftMaxPlayers);
|
||||
jObject["panic_bunker"] = _cfg.GetCVar(CCVars.PanicBunkerEnabled);
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jObject["baby_jail"] = _cfg.GetCVar(CCVars.BabyJailEnabled);
|
||||
jObject["run_level"] = (int) _runLevel;
|
||||
if (preset != null)
|
||||
jObject["preset"] = Loc.GetString(preset.ModeTitle);
|
||||
|
||||
@@ -21,6 +21,7 @@ using Robust.Shared.Random;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Movement.Pulling.Components;
|
||||
using Content.Shared.Movement.Pulling.Systems;
|
||||
using Content.Server.IdentityManagement;
|
||||
using Content.Shared.Store.Components;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Map.Components;
|
||||
@@ -41,6 +42,7 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
||||
[Dependency] private readonly PullingSystem _pullingSystem = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
|
||||
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
|
||||
[Dependency] private readonly IdentitySystem _identity = default!;
|
||||
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
private HashSet<Entity<MapGridComponent>> _targetGrids = [];
|
||||
@@ -211,7 +213,7 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
||||
{
|
||||
var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
|
||||
_humanoidAppearance.LoadProfile(ent, newProfile, humanoid);
|
||||
_metaData.SetEntityName(ent, newProfile.Name);
|
||||
_metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
|
||||
if (TryComp<DnaComponent>(ent, out var dna))
|
||||
{
|
||||
dna.DNA = _forensicsSystem.GenerateDNA();
|
||||
@@ -223,6 +225,7 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
||||
{
|
||||
fingerprint.Fingerprint = _forensicsSystem.GenerateFingerprint();
|
||||
}
|
||||
_identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
|
||||
_popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,8 @@ namespace Content.Server.IoC
|
||||
IoCManager.Register<PlayerRateLimitManager>();
|
||||
IoCManager.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
|
||||
IoCManager.Register<MappingManager>();
|
||||
IoCManager.Register<IWatchlistWebhookManager, WatchlistWebhookManager>();
|
||||
IoCManager.Register<ConnectionManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ public sealed class FoodSystem : EntitySystem
|
||||
if (stomachToUse == null)
|
||||
{
|
||||
_solutionContainer.TryAddSolution(soln.Value, split);
|
||||
_popup.PopupEntity(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other") : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User);
|
||||
_popup.PopupEntity(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other", ("target", args.Target.Value)) : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
|
||||
{
|
||||
base.OnInserted(uid, component, args);
|
||||
|
||||
if (HasComp<BorgBrainComponent>(args.Entity) && _mind.TryGetMind(args.Entity, out var mindId, out var mind))
|
||||
if (HasComp<BorgBrainComponent>(args.Entity) && _mind.TryGetMind(args.Entity, out var mindId, out var mind) && args.Container == component.BrainContainer)
|
||||
{
|
||||
_mind.TransferTo(mindId, uid, mind: mind);
|
||||
}
|
||||
@@ -164,8 +164,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
|
||||
{
|
||||
base.OnRemoved(uid, component, args);
|
||||
|
||||
if (HasComp<BorgBrainComponent>(args.Entity) &
|
||||
_mind.TryGetMind(uid, out var mindId, out var mind))
|
||||
if (HasComp<BorgBrainComponent>(args.Entity) && _mind.TryGetMind(uid, out var mindId, out var mind) && args.Container == component.BrainContainer)
|
||||
{
|
||||
_mind.TransferTo(mindId, args.Entity, mind: mind);
|
||||
}
|
||||
@@ -293,8 +292,11 @@ public sealed partial class BorgSystem : SharedBorgSystem
|
||||
public void BorgActivate(EntityUid uid, BorgChassisComponent component)
|
||||
{
|
||||
Popup.PopupEntity(Loc.GetString("borg-mind-added", ("name", Identity.Name(uid, EntityManager))), uid);
|
||||
Toggle.TryActivate(uid);
|
||||
_powerCell.SetDrawEnabled(uid, _mobState.IsAlive(uid));
|
||||
if (_powerCell.HasDrawCharge(uid))
|
||||
{
|
||||
Toggle.TryActivate(uid);
|
||||
_powerCell.SetDrawEnabled(uid, _mobState.IsAlive(uid));
|
||||
}
|
||||
_appearance.SetData(uid, BorgVisuals.HasPlayer, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,10 +43,10 @@ public sealed class StationAiSystem : SharedStationAiSystem
|
||||
var stationAiCore = new Entity<StationAiCoreComponent>(ent, entStationAiCore);
|
||||
|
||||
if (!TryGetInsertedAI(stationAiCore, out var insertedAi) || !TryComp(insertedAi, out ActorComponent? actor))
|
||||
return;
|
||||
continue;
|
||||
|
||||
if (stationAiCore.Comp.RemoteEntity == null || stationAiCore.Comp.Remote)
|
||||
return;
|
||||
continue;
|
||||
|
||||
var xform = Transform(stationAiCore.Comp.RemoteEntity.Value);
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class MumbleAccentComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
25
Content.Server/Speech/EntitySystems/MumbleAccentSystem.cs
Normal file
25
Content.Server/Speech/EntitySystems/MumbleAccentSystem.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Content.Server.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed class MumbleAccentSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ReplacementAccentSystem _replacement = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<MumbleAccentComponent, AccentGetEvent>(OnAccentGet);
|
||||
}
|
||||
|
||||
public string Accentuate(string message, MumbleAccentComponent component)
|
||||
{
|
||||
return _replacement.ApplyReplacements(message, "mumble");
|
||||
}
|
||||
|
||||
private void OnAccentGet(EntityUid uid, MumbleAccentComponent component, AccentGetEvent args)
|
||||
{
|
||||
args.Message = Accentuate(args.Message, component);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Content.Shared.Administration.Events;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class BabyJailStatus
|
||||
{
|
||||
public bool Enabled;
|
||||
public bool ShowReason;
|
||||
public int MaxAccountAgeMinutes;
|
||||
public int MaxOverallMinutes;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class BabyJailChangedEvent(BabyJailStatus status) : EntityEventArgs
|
||||
{
|
||||
public BabyJailStatus Status = status;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ namespace Content.Shared.Administration
|
||||
public NetUserId UserId;
|
||||
public string? UserName;
|
||||
public string? Title;
|
||||
public bool Suspended;
|
||||
public AdminFlags PosFlags;
|
||||
public AdminFlags NegFlags;
|
||||
public int? RankId;
|
||||
@@ -41,6 +42,7 @@ namespace Content.Shared.Administration
|
||||
public AdminFlags PosFlags;
|
||||
public AdminFlags NegFlags;
|
||||
public int? RankId;
|
||||
public bool Suspended;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
@@ -57,6 +59,7 @@ namespace Content.Shared.Administration
|
||||
public AdminFlags PosFlags;
|
||||
public AdminFlags NegFlags;
|
||||
public int? RankId;
|
||||
public bool Suspended;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -158,6 +158,13 @@ public sealed partial class CCVars
|
||||
public static readonly CVarDef<bool> AdminsCountForMaxPlayers =
|
||||
CVarDef.Create("admin.admins_count_for_max_players", false, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Should admins be hidden from the player count reported to the launcher/via api?
|
||||
/// This is hub advert safe, in case that's a worry.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> AdminsCountInReportedPlayerCount =
|
||||
CVarDef.Create("admin.admins_count_in_playercount", false, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Determine if custom rank names are used.
|
||||
/// If it is false, it'd use the actual rank name regardless of the individual's title.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Shared.CCVar;
|
||||
|
||||
@@ -58,4 +58,18 @@ public sealed partial class CCVars
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
|
||||
CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// URL of the Discord webhook which will relay watchlist connection notifications. If left empty, disables the webhook.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> DiscordWatchlistConnectionWebhook =
|
||||
CVarDef.Create("discord.watchlist_connection_webhook", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
|
||||
|
||||
/// <summary>
|
||||
/// How long to buffer watchlist connections for, in seconds.
|
||||
/// All connections within this amount of time from the first one will be batched and sent as a single
|
||||
/// Discord notification. If zero, always sends a separate notification for each connection (not recommended).
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> DiscordWatchlistConnectionBufferTime =
|
||||
CVarDef.Create("discord.watchlist_connection_buffer_time", 5f, CVar.SERVERONLY);
|
||||
}
|
||||
|
||||
@@ -198,47 +198,105 @@ public sealed partial class CCVars
|
||||
public static readonly CVarDef<bool> BypassBunkerWhitelist =
|
||||
CVarDef.Create("game.panic_bunker.whitelisted_can_bypass", true, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
/// <summary>
|
||||
/// Enable IPIntel for blocking VPN connections from new players.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> GameIPIntelEnabled =
|
||||
CVarDef.Create("game.ipintel_enabled", false, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the baby jail is currently enabled.
|
||||
/// Whether clients which are flagged as a VPN will be denied
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> BabyJailEnabled =
|
||||
CVarDef.Create("game.baby_jail.enabled", false, CVar.NOTIFY | CVar.REPLICATED | CVar.SERVER);
|
||||
public static readonly CVarDef<bool> GameIPIntelRejectBad =
|
||||
CVarDef.Create("game.ipintel_reject_bad", true, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Show reason of disconnect for user or not.
|
||||
/// Whether clients which cannot be checked due to a rate limit will be denied
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> BabyJailShowReason =
|
||||
CVarDef.Create("game.baby_jail.show_reason", false, CVar.SERVERONLY);
|
||||
public static readonly CVarDef<bool> GameIPIntelRejectRateLimited =
|
||||
CVarDef.Create("game.ipintel_reject_ratelimited", false, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum age of the account (from server's PoV, so from first-seen date) in minutes that can access baby
|
||||
/// jailed servers.
|
||||
/// Whether clients which cannot be checked due to an error of some form will be denied
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> BabyJailMaxAccountAge =
|
||||
CVarDef.Create("game.baby_jail.max_account_age", 1440, CVar.SERVERONLY);
|
||||
public static readonly CVarDef<bool> GameIPIntelRejectUnknown =
|
||||
CVarDef.Create("game.ipintel_reject_unknown", false, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum overall played time allowed to access baby jailed servers.
|
||||
/// Should an admin message be made if the connection got rejected cause of ipintel?
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> BabyJailMaxOverallMinutes =
|
||||
CVarDef.Create("game.baby_jail.max_overall_minutes", 120, CVar.SERVERONLY);
|
||||
public static readonly CVarDef<bool> GameIPIntelAlertAdminReject =
|
||||
CVarDef.Create("game.ipintel_alert_admin_rejected", false, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// A custom message that will be used for connections denied due to the baby jail.
|
||||
/// If not empty, then will overwrite <see cref="BabyJailShowReason"/>
|
||||
/// A contact email to be sent along with the request. Required by IPIntel
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> BabyJailCustomReason =
|
||||
CVarDef.Create("game.baby_jail.custom_reason", string.Empty, CVar.SERVERONLY);
|
||||
public static readonly CVarDef<string> GameIPIntelEmail =
|
||||
CVarDef.Create("game.ipintel_contact_email", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
|
||||
|
||||
/// <summary>
|
||||
/// Allow bypassing the baby jail if the user is whitelisted.
|
||||
/// The URL to IPIntel to make requests to. If you pay for more queries this is what you want to change.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> BypassBabyJailWhitelist =
|
||||
CVarDef.Create("game.baby_jail.whitelisted_can_bypass", true, CVar.SERVERONLY);
|
||||
public static readonly CVarDef<string> GameIPIntelBase =
|
||||
CVarDef.Create("game.ipintel_baseurl", "https://check.getipintel.net", CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// The flags to use in the request to IPIntel, please look here for more info. https://getipintel.net/free-proxy-vpn-tor-detection-api/#optional_settings
|
||||
/// Note: Some flags may increase the chances of false positives and request time. The default should be fine for most servers.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> GameIPIntelFlags =
|
||||
CVarDef.Create("game.ipintel_flags", "b", CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum amount of requests per Minute. For free you get 15.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> GameIPIntelMaxMinute =
|
||||
CVarDef.Create("game.ipintel_request_limit_minute", 15, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum amount of requests per Day. For free you get 500.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> GameIPIntelMaxDay =
|
||||
CVarDef.Create("game.ipintel_request_limit_daily", 500, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Amount of seconds to add to the exponential backoff with every failed request.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> GameIPIntelBackOffSeconds =
|
||||
CVarDef.Create("game.ipintel_request_backoff_seconds", 30, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// How much time should pass before we attempt to cleanup the IPIntel table for old ip addresses?
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> GameIPIntelCleanupMins =
|
||||
CVarDef.Create("game.ipintel_database_cleanup_mins", 15, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// How long to store results in the cache before they must be retrieved again in days.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<TimeSpan> GameIPIntelCacheLength =
|
||||
CVarDef.Create("game.ipintel_cache_length", TimeSpan.FromDays(7), CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Amount of playtime in minutes to be exempt from an IP check. 0 to search everyone. 5 hours by default.
|
||||
/// <remarks>
|
||||
/// Trust me you want one.
|
||||
/// </remarks>>
|
||||
/// </summary>
|
||||
public static readonly CVarDef<TimeSpan> GameIPIntelExemptPlaytime =
|
||||
CVarDef.Create("game.ipintel_exempt_playtime", TimeSpan.FromMinutes(300), CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Rating to reject at. Anything equal to or higher than this will reject the connection.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> GameIPIntelBadRating =
|
||||
CVarDef.Create("game.ipintel_bad_rating", 0.95f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Rating to send an admin warning over, but not reject the connection. Set to 0 to disable
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> GameIPIntelAlertAdminWarnRating =
|
||||
CVarDef.Create("game.ipintel_alert_admin_warn_rating", 0f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Make people bonk when trying to climb certain objects like tables.
|
||||
|
||||
@@ -21,6 +21,9 @@ public sealed partial class CCVars
|
||||
public static readonly CVarDef<bool> AdminSoundsEnabled =
|
||||
CVarDef.Create("audio.admin_sounds_enabled", true, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
public static readonly CVarDef<bool> BwoinkSoundEnabled =
|
||||
CVarDef.Create("audio.bwoink_sound_enabled", true, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
public static readonly CVarDef<string> AdminChatSoundPath =
|
||||
CVarDef.Create("audio.admin_chat_sound_path",
|
||||
"/Audio/Items/pop.ogg",
|
||||
|
||||
@@ -75,10 +75,10 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
public EntityWhitelist? Blacklist;
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier InsertSound = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/revolver_magin.ogg");
|
||||
public SoundSpecifier? InsertSound = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/revolver_magin.ogg");
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier EjectSound = new SoundPathSpecifier("/Audio/Weapons/Guns/MagOut/revolver_magout.ogg");
|
||||
public SoundSpecifier? EjectSound = new SoundPathSpecifier("/Audio/Weapons/Guns/MagOut/revolver_magout.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// The name of this item slot. This will be shown to the user in the verb menu.
|
||||
|
||||
@@ -589,8 +589,8 @@ public abstract partial class SharedDoorSystem : EntitySystem
|
||||
if (otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.GlassLayer || otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.GlassAirlockLayer || otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.TableLayer)
|
||||
continue;
|
||||
|
||||
//If the colliding entity is a slippable item ignore it by the airlock
|
||||
if (otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.SlipLayer && otherPhysics.Comp.CollisionMask == (int) CollisionGroup.ItemMask)
|
||||
// Ignore low-passable entities.
|
||||
if ((otherPhysics.Comp.CollisionMask & (int)CollisionGroup.LowImpassable) == 0)
|
||||
continue;
|
||||
|
||||
//For when doors need to close over conveyor belts
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Item.ItemToggle.Components;
|
||||
using Content.Shared.Light.Components;
|
||||
using Content.Shared.Toggleable;
|
||||
using ItemTogglePointLightComponent = Content.Shared.Light.Components.ItemTogglePointLightComponent;
|
||||
|
||||
@@ -11,6 +12,7 @@ public sealed class ItemTogglePointLightSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedPointLightSystem _light = default!;
|
||||
[Dependency] private readonly SharedHandheldLightSystem _handheldLight = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -25,5 +27,9 @@ public sealed class ItemTogglePointLightSystem : EntitySystem
|
||||
|
||||
_appearance.SetData(ent, ToggleableLightVisuals.Enabled, args.Activated);
|
||||
_light.SetEnabled(ent.Owner, args.Activated, comp: light);
|
||||
if (TryComp<HandheldLightComponent>(ent.Owner, out var handheldLight))
|
||||
{
|
||||
_handheldLight.SetActivated(ent.Owner, args.Activated, handheldLight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,12 +32,6 @@ public sealed class ContentPlayerData
|
||||
[ViewVariables, Access(typeof(SharedMindSystem), typeof(SharedGameTicker))]
|
||||
public EntityUid? Mind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the player is an admin and they explicitly de-adminned mid-game,
|
||||
/// so they should not regain admin if they reconnect.
|
||||
/// </summary>
|
||||
public bool ExplicitlyDeadminned { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the admin will not show up in adminwho except to admins with the <see cref="AdminFlags.Stealth"/> flag.
|
||||
/// </summary>
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Content.Shared.Preferences
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class HumanoidCharacterProfile : ICharacterProfile
|
||||
{
|
||||
private static readonly Regex RestrictedNameRegex = new("[^A-Z,a-z,0-9, ,\\-,']");
|
||||
private static readonly Regex RestrictedNameRegex = new(@"[^A-Za-z0-9 '\-]");
|
||||
private static readonly Regex ICNameCaseRegex = new(@"^(?<word>\w)|\b(?<word>\w)(?=\w*$)");
|
||||
|
||||
public const int MaxNameLength = 32;
|
||||
|
||||
269
Content.Tests/Server/Connection/IPIntelTest.cs
Normal file
269
Content.Tests/Server/Connection/IPIntelTest.cs
Normal file
@@ -0,0 +1,269 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Connection.IPIntel;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.CCVar;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
// ReSharper disable AccessToModifiedClosure
|
||||
|
||||
namespace Content.Tests.Server.Connection;
|
||||
|
||||
[TestFixture, TestOf(typeof(IPIntel))]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public static class IPIntelTest
|
||||
{
|
||||
private static readonly IPAddress TestIp = IPAddress.Parse("192.0.2.1");
|
||||
|
||||
private static void CreateIPIntel(
|
||||
out IPIntel ipIntel,
|
||||
out IConfigurationManager cfg,
|
||||
Func<HttpResponseMessage> apiResponse,
|
||||
Func<TimeSpan> realTime = null)
|
||||
{
|
||||
var dbManager = new Mock<IServerDbManager>();
|
||||
var gameTimingMock = new Mock<IGameTiming>();
|
||||
gameTimingMock.SetupGet(gt => gt.RealTime)
|
||||
.Returns(realTime ?? (() => TimeSpan.Zero));
|
||||
|
||||
var logManager = new LogManager();
|
||||
var gameTiming = gameTimingMock.Object;
|
||||
|
||||
cfg = MockInterfaces.MakeConfigurationManager(gameTiming, logManager, loadCvarsFromTypes: [typeof(CCVars)]);
|
||||
|
||||
ipIntel = new IPIntel(
|
||||
new FakeIPIntelApi(apiResponse),
|
||||
dbManager.Object,
|
||||
cfg,
|
||||
logManager,
|
||||
new Mock<IChatManager>().Object,
|
||||
gameTiming
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public static async Task TestSuccess()
|
||||
{
|
||||
CreateIPIntel(
|
||||
out var ipIntel,
|
||||
out _,
|
||||
RespondSuccess);
|
||||
|
||||
var result = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result.Score, Is.EqualTo(0.5f).Within(0.01f));
|
||||
Assert.That(result.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public static async Task KnownRateLimitMinuteTest()
|
||||
{
|
||||
var source = RespondSuccess;
|
||||
var time = TimeSpan.Zero;
|
||||
CreateIPIntel(
|
||||
out var ipIntel,
|
||||
out var cfg,
|
||||
() => source(),
|
||||
() => time);
|
||||
|
||||
cfg.SetCVar(CCVars.GameIPIntelMaxMinute, 9);
|
||||
|
||||
for (var i = 0; i < 9; i++)
|
||||
{
|
||||
var result = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(result.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success));
|
||||
}
|
||||
|
||||
source = RespondTestFailed;
|
||||
var shouldBeRateLimited = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(shouldBeRateLimited.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
|
||||
|
||||
time += TimeSpan.FromMinutes(1.5);
|
||||
source = RespondSuccess;
|
||||
var shouldSucceed = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(shouldSucceed.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public static async Task KnownRateLimitMinuteTimingTest()
|
||||
{
|
||||
var source = RespondSuccess;
|
||||
var time = TimeSpan.Zero;
|
||||
CreateIPIntel(
|
||||
out var ipIntel,
|
||||
out var cfg,
|
||||
() => source(),
|
||||
() => time);
|
||||
|
||||
cfg.SetCVar(CCVars.GameIPIntelMaxMinute, 1);
|
||||
|
||||
// First query succeeds.
|
||||
var result = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(result.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success));
|
||||
|
||||
// Second is rate limited via known limit.
|
||||
source = RespondTestFailed;
|
||||
result = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(result.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
|
||||
|
||||
// Move 30 seconds into the future, should not be enough to unratelimit.
|
||||
time += TimeSpan.FromSeconds(30);
|
||||
|
||||
var shouldBeRateLimited = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(shouldBeRateLimited.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
|
||||
|
||||
// Should be available again.
|
||||
source = RespondSuccess;
|
||||
time += TimeSpan.FromSeconds(35);
|
||||
|
||||
var shouldSucceed = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(shouldSucceed.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success));
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public static async Task SuddenRateLimitTest()
|
||||
{
|
||||
var time = TimeSpan.Zero;
|
||||
var source = RespondRateLimited;
|
||||
CreateIPIntel(
|
||||
out var ipIntel,
|
||||
out _,
|
||||
() => source(),
|
||||
() => time);
|
||||
|
||||
var test = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
|
||||
|
||||
source = RespondTestFailed;
|
||||
test = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
|
||||
|
||||
// King crimson idk I didn't watch JoJo past part 2.
|
||||
time += TimeSpan.FromMinutes(2);
|
||||
|
||||
source = RespondSuccess;
|
||||
test = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public static async Task SuddenRateLimitExponentialBackoffTest()
|
||||
{
|
||||
var time = TimeSpan.Zero;
|
||||
var source = RespondRateLimited;
|
||||
CreateIPIntel(
|
||||
out var ipIntel,
|
||||
out _,
|
||||
() => source(),
|
||||
() => time);
|
||||
|
||||
IPIntel.IPIntelResult test;
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
time += TimeSpan.FromHours(1);
|
||||
|
||||
test = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
|
||||
}
|
||||
|
||||
// After 5 sequential failed attempts, 1 minute should not be enough to get past the exponential backoff.
|
||||
time += TimeSpan.FromMinutes(1);
|
||||
|
||||
source = RespondTestFailed;
|
||||
test = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public static async Task ErrorTest()
|
||||
{
|
||||
CreateIPIntel(
|
||||
out var ipIntel,
|
||||
out _,
|
||||
RespondError);
|
||||
|
||||
var resp = await ipIntel.QueryIPIntelRateLimited(TestIp);
|
||||
Assert.That(resp.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Errored));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("0.0.0.0", ExpectedResult = true)]
|
||||
[TestCase("0.3.5.7", ExpectedResult = true)]
|
||||
[TestCase("127.0.0.1", ExpectedResult = true)]
|
||||
[TestCase("11.0.0.0", ExpectedResult = false)]
|
||||
[TestCase("10.0.1.0", ExpectedResult = true)]
|
||||
[TestCase("192.168.5.12", ExpectedResult = true)]
|
||||
[TestCase("192.167.0.1", ExpectedResult = false)]
|
||||
// Not an IPv4!
|
||||
[TestCase("::1", ExpectedResult = false)]
|
||||
public static bool TestIsReservedIpv4(string ipAddress)
|
||||
{
|
||||
return IPIntel.IsAddressReservedIpv4(IPAddress.Parse(ipAddress));
|
||||
}
|
||||
|
||||
[Test]
|
||||
// IPv4-mapped IPv6 should use IPv4 behavior.
|
||||
[TestCase("::ffff:0.0.0.0", ExpectedResult = true)]
|
||||
[TestCase("::ffff:0.3.5.7", ExpectedResult = true)]
|
||||
[TestCase("::ffff:127.0.0.1", ExpectedResult = true)]
|
||||
[TestCase("::ffff:11.0.0.0", ExpectedResult = false)]
|
||||
[TestCase("::ffff:10.0.1.0", ExpectedResult = true)]
|
||||
[TestCase("::ffff:192.168.5.12", ExpectedResult = true)]
|
||||
[TestCase("::ffff:192.167.0.1", ExpectedResult = false)]
|
||||
// Regular IPv6 tests.
|
||||
[TestCase("::1", ExpectedResult = true)]
|
||||
[TestCase("2001:db8::01", ExpectedResult = true)]
|
||||
[TestCase("2a01:4f8:252:4425::1234", ExpectedResult = false)]
|
||||
// Not an IPv6!
|
||||
[TestCase("127.0.0.1", ExpectedResult = false)]
|
||||
public static bool TestIsReservedIpv6(string ipAddress)
|
||||
{
|
||||
return IPIntel.IsAddressReservedIpv6(IPAddress.Parse(ipAddress));
|
||||
}
|
||||
|
||||
private static HttpResponseMessage RespondSuccess()
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("0.5"),
|
||||
};
|
||||
}
|
||||
|
||||
private static HttpResponseMessage RespondRateLimited()
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.TooManyRequests);
|
||||
}
|
||||
|
||||
private static HttpResponseMessage RespondTestFailed()
|
||||
{
|
||||
throw new InvalidOperationException("API should not be queried at this part of the test.");
|
||||
}
|
||||
|
||||
private static HttpResponseMessage RespondError()
|
||||
{
|
||||
return new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||
{
|
||||
Content = new StringContent("-4"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FakeIPIntelApi(Func<HttpResponseMessage> response) : IIPIntelApi
|
||||
{
|
||||
public Task<HttpResponseMessage> GetIPScore(IPAddress ip)
|
||||
{
|
||||
return Task.FromResult(response());
|
||||
}
|
||||
}
|
||||
@@ -687,5 +687,46 @@ Entries:
|
||||
id: 85
|
||||
time: '2025-01-11T21:17:26.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33420
|
||||
- author: Myra
|
||||
changes:
|
||||
- message: Added IPIntel's API to the connection code. If enabled, this will automate
|
||||
VPN/Proxy detection.
|
||||
type: Add
|
||||
id: 86
|
||||
time: '2025-01-12T19:41:26.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33339
|
||||
- author: PJB3005
|
||||
changes:
|
||||
- message: Deadmin status is now synchronized to database, making it persistent
|
||||
across server restarts and between multiple game servers.
|
||||
type: Tweak
|
||||
- message: Admins can now be suspended via the permissions panel. This effectively
|
||||
removes their admin status without completely deleting their record.
|
||||
type: Add
|
||||
id: 87
|
||||
time: '2025-01-14T23:46:45.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34048
|
||||
- author: Palladinium
|
||||
changes:
|
||||
- message: Added Discord relay notifications when a watchlisted player connects.
|
||||
type: Add
|
||||
id: 88
|
||||
time: '2025-01-15T00:32:24.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33483
|
||||
- author: Myra
|
||||
changes:
|
||||
- message: Admins are by default now hidden from the reported player count on the
|
||||
launcher.
|
||||
type: Add
|
||||
id: 89
|
||||
time: '2025-01-15T21:10:54.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34406
|
||||
- author: c4llv07e
|
||||
changes:
|
||||
- message: Added an option to disable the bwoink sound!
|
||||
type: Add
|
||||
id: 90
|
||||
time: '2025-01-17T07:24:50.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33782
|
||||
Name: Admin
|
||||
Order: 1
|
||||
|
||||
@@ -1,182 +1,4 @@
|
||||
Entries:
|
||||
- author: Futuristic
|
||||
changes:
|
||||
- message: Remove binary encryption key from RD lockers
|
||||
type: Remove
|
||||
id: 7301
|
||||
time: '2024-09-07T05:30:57.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31909
|
||||
- author: EmoGarbage404
|
||||
changes:
|
||||
- message: Firesuits are now worse at keeping in heat and winter clothes make you
|
||||
get warmer quicker.
|
||||
type: Tweak
|
||||
id: 7302
|
||||
time: '2024-09-07T05:37:17.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30662
|
||||
- author: Boaz1111
|
||||
changes:
|
||||
- message: The maple wing marking for moths now have a secondary color palette.
|
||||
type: Tweak
|
||||
id: 7303
|
||||
time: '2024-09-07T05:48:40.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31691
|
||||
- author: lzk228
|
||||
changes:
|
||||
- message: Bottle and syringe names are remade into labels.
|
||||
type: Tweak
|
||||
id: 7304
|
||||
time: '2024-09-07T05:51:36.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29956
|
||||
- author: Ian321
|
||||
changes:
|
||||
- message: The AgriChem kit now links to the botanical chemicals guidebook.
|
||||
type: Tweak
|
||||
- message: The botanical chemicals guidebook has been expanded.
|
||||
type: Tweak
|
||||
id: 7305
|
||||
time: '2024-09-07T06:23:01.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31896
|
||||
- author: Lank
|
||||
changes:
|
||||
- message: The Antimov and Overseer law boards are no longer available roundstart.
|
||||
type: Remove
|
||||
id: 7306
|
||||
time: '2024-09-07T08:45:51.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31908
|
||||
- author: lzk228
|
||||
changes:
|
||||
- message: Books cannot longer be inserted in crates as paper labels
|
||||
type: Fix
|
||||
id: 7307
|
||||
time: '2024-09-07T13:22:11.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31919
|
||||
- author: qwerltaz
|
||||
changes:
|
||||
- message: Reduced wall closet range. It's now much easier to not close yourself
|
||||
inside by accident.
|
||||
type: Tweak
|
||||
id: 7308
|
||||
time: '2024-09-07T23:44:29.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31933
|
||||
- author: Ilya246
|
||||
changes:
|
||||
- message: Reagents that make you flammable no longer extinguish you.
|
||||
type: Fix
|
||||
id: 7309
|
||||
time: '2024-09-07T23:44:58.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31930
|
||||
- author: LucasTheDrgn
|
||||
changes:
|
||||
- message: Restored functionality to the Industrial Reagent Grinder
|
||||
type: Fix
|
||||
id: 7310
|
||||
time: '2024-09-07T23:47:02.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31903
|
||||
- author: EmoGarbage404
|
||||
changes:
|
||||
- message: Added the biogenerator! Botany can use this machine to create various
|
||||
materials, chemicals, and food items out of the
|
||||
type: Add
|
||||
id: 7311
|
||||
time: '2024-09-08T05:34:22.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/30694
|
||||
- author: TheShuEd
|
||||
changes:
|
||||
- message: Returned Taco microwave recipes (people were sad)
|
||||
type: Add
|
||||
- message: added an alternative method of crafting some burgers, through the correct
|
||||
assembly sequence of modular food.
|
||||
type: Add
|
||||
- message: severely cut back on the number of items you can put on burgers, tacos,
|
||||
or kebabs. This had poor design, and things need to be separately resprited
|
||||
by adding them on modular food.
|
||||
type: Tweak
|
||||
id: 7312
|
||||
time: '2024-09-08T06:22:27.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31012
|
||||
- author: metalgearsloth
|
||||
changes:
|
||||
- message: Fix the FTL bubbles sometimes persisting.
|
||||
type: Fix
|
||||
- message: Fix AI eye being able to FTL.
|
||||
type: Fix
|
||||
- message: Fix the AI eye being able to be FTL smashed.
|
||||
type: Fix
|
||||
id: 7313
|
||||
time: '2024-09-08T08:12:24.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31952
|
||||
- author: PopGamer46
|
||||
changes:
|
||||
- message: Fixed being able to craft the justice helmet with a justice helmet
|
||||
type: Fix
|
||||
id: 7314
|
||||
time: '2024-09-08T10:21:55.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31957
|
||||
- author: Killerqu00
|
||||
changes:
|
||||
- message: Seclite is now restricted to security.
|
||||
type: Tweak
|
||||
- message: Handcuffs are now restricted to security and command.
|
||||
type: Tweak
|
||||
- message: Trench whistle is now minor contraband.
|
||||
type: Tweak
|
||||
id: 7315
|
||||
time: '2024-09-08T12:06:01.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31956
|
||||
- author: Psychpsyo
|
||||
changes:
|
||||
- message: The random sentience event should now actually happen again.
|
||||
type: Fix
|
||||
id: 7316
|
||||
time: '2024-09-08T12:10:50.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31953
|
||||
- author: Beck Thompson
|
||||
changes:
|
||||
- message: Gold and silver rings now give a small amount of materials when scrapped.
|
||||
type: Tweak
|
||||
id: 7317
|
||||
time: '2024-09-08T16:10:05.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31847
|
||||
- author: K-Dynamic
|
||||
changes:
|
||||
- message: added missing missing resistance values for directional plasma and uranium
|
||||
windows
|
||||
type: Fix
|
||||
id: 7318
|
||||
time: '2024-09-08T18:08:06.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31975
|
||||
- author: qwerltaz
|
||||
changes:
|
||||
- message: Power cables on the ground are now offset and do not obscure each other
|
||||
or pipes beneath.
|
||||
type: Tweak
|
||||
id: 7319
|
||||
time: '2024-09-09T10:00:18.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32000
|
||||
- author: ArtisticRoomba
|
||||
changes:
|
||||
- message: Budget insulated gloves now leave behind yellow frayed insulative fibers
|
||||
instead of yellow insulative fibers. Detectives, rejoice!
|
||||
type: Tweak
|
||||
id: 7320
|
||||
time: '2024-09-09T10:30:25.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31886
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Revenants or other mobs without hands can no longer spill jugs.
|
||||
type: Fix
|
||||
id: 7321
|
||||
time: '2024-09-09T11:02:15.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31438
|
||||
- author: PJB3005
|
||||
changes:
|
||||
- message: The emergency shuttle will now wait at the station longer if it couldn't
|
||||
dock at evac.
|
||||
type: Tweak
|
||||
id: 7322
|
||||
time: '2024-09-09T18:10:28.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31496
|
||||
- author: Boaz1111
|
||||
changes:
|
||||
- message: Pacifists can now use grapple guns.
|
||||
@@ -3937,3 +3759,165 @@
|
||||
id: 7800
|
||||
time: '2025-01-11T21:17:26.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33420
|
||||
- author: Kontinentaldrift
|
||||
changes:
|
||||
- message: EOD locker always spawns with tools.
|
||||
type: Tweak
|
||||
id: 7801
|
||||
time: '2025-01-12T16:38:41.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34394
|
||||
- author: SlimSlam
|
||||
changes:
|
||||
- message: News Room
|
||||
type: Add
|
||||
id: 7802
|
||||
time: '2025-01-12T23:32:51.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34408
|
||||
- author: Deerstop
|
||||
changes:
|
||||
- message: Improved the readability of the manual valve sprite.
|
||||
type: Tweak
|
||||
id: 7803
|
||||
time: '2025-01-13T07:07:30.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34378
|
||||
- author: JustinWinningham
|
||||
changes:
|
||||
- message: Anomalies no longer block players or APES (or other objects) from passing
|
||||
through them
|
||||
type: Tweak
|
||||
- message: Players can no longer move anomalies with unanchored furniture, closets,
|
||||
etc.
|
||||
type: Fix
|
||||
id: 7804
|
||||
time: '2025-01-13T10:06:57.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34280
|
||||
- author: zHonys
|
||||
changes:
|
||||
- message: Items with collisions (like mousetraps) won't kept doors form opening
|
||||
type: Fix
|
||||
id: 7805
|
||||
time: '2025-01-13T10:07:18.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34045
|
||||
- author: Coolsurf6
|
||||
changes:
|
||||
- message: The "Jazz" style for the Electric Guitar now uses the correct soundfont.
|
||||
type: Tweak
|
||||
id: 7806
|
||||
time: '2025-01-13T10:07:33.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33363
|
||||
- author: southbridge-fur
|
||||
changes:
|
||||
- message: The Pride-O-Mat vending machine has been ported to upstream.
|
||||
type: Add
|
||||
id: 7807
|
||||
time: '2025-01-13T18:49:02.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34412
|
||||
- author: Killerqu00
|
||||
changes:
|
||||
- message: Pet carriers can now be crafted.
|
||||
type: Add
|
||||
id: 7808
|
||||
time: '2025-01-14T22:34:04.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34431
|
||||
- author: themias
|
||||
changes:
|
||||
- message: Fixed muzzles not working on some characters (e.g. dwarves)
|
||||
type: Fix
|
||||
id: 7809
|
||||
time: '2025-01-15T00:10:39.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34419
|
||||
- author: ArtisticRoomba
|
||||
changes:
|
||||
- message: The station anchor machine board can no longer be printed at the circuit
|
||||
imprinter.
|
||||
type: Remove
|
||||
id: 7810
|
||||
time: '2025-01-15T16:26:19.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34358
|
||||
- author: ArtisticRoomba
|
||||
changes:
|
||||
- message: Mime PDA item interactions (insertions/ejections of IDs, pens, etc.)
|
||||
are now silent.
|
||||
type: Tweak
|
||||
id: 7811
|
||||
time: '2025-01-15T18:03:49.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34426
|
||||
- author: Alpaccalypse
|
||||
changes:
|
||||
- message: Smite soda vending machines have been added to the game.
|
||||
type: Add
|
||||
id: 7812
|
||||
time: '2025-01-15T18:20:01.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34420
|
||||
- author: kosticia
|
||||
changes:
|
||||
- message: Bedsheets now can be printed on uniform printer
|
||||
type: Add
|
||||
id: 7813
|
||||
time: '2025-01-15T19:35:59.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34034
|
||||
- author: TheShuEd
|
||||
changes:
|
||||
- message: Christmas anomaly removed
|
||||
type: Remove
|
||||
id: 7814
|
||||
time: '2025-01-15T20:22:32.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34053
|
||||
- author: Kickguy223
|
||||
changes:
|
||||
- message: Puddles will now correctly evaporate
|
||||
type: Fix
|
||||
id: 7815
|
||||
time: '2025-01-15T21:21:20.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34303
|
||||
- author: themias
|
||||
changes:
|
||||
- message: The DNA scrambler implant no longer updates your ID card or station record
|
||||
type: Fix
|
||||
id: 7816
|
||||
time: '2025-01-15T22:49:51.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34091
|
||||
- author: jbox144
|
||||
changes:
|
||||
- message: Added Plasma Station
|
||||
type: Add
|
||||
id: 7817
|
||||
time: '2025-01-16T07:02:14.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33991
|
||||
- author: jbox144
|
||||
changes:
|
||||
- message: Reduced Plasma's minimum population to 20, maximum population to 60
|
||||
type: Tweak
|
||||
- message: Reduced Plasma's clown jobs from 2 to 1
|
||||
type: Tweak
|
||||
id: 7818
|
||||
time: '2025-01-16T08:51:17.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34462
|
||||
- author: pcaessayrs
|
||||
changes:
|
||||
- message: Uranium, Cak, and BreadDog are no longer space garbage.
|
||||
type: Fix
|
||||
id: 7819
|
||||
time: '2025-01-16T11:40:22.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34192
|
||||
- author: GansuLalan
|
||||
changes:
|
||||
- message: Pianos will no longer push players off when climbing them
|
||||
type: Fix
|
||||
id: 7820
|
||||
time: '2025-01-16T12:25:08.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33690
|
||||
- author: Southbridge
|
||||
changes:
|
||||
- message: More Ionstorm law elements have been added
|
||||
type: Add
|
||||
id: 7821
|
||||
time: '2025-01-16T20:49:47.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34197
|
||||
- author: southbridge-fur
|
||||
changes:
|
||||
- message: Pride-O-Mats now have scarves instead of cloaks
|
||||
type: Tweak
|
||||
id: 7822
|
||||
time: '2025-01-17T11:35:03.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34448
|
||||
|
||||
@@ -6,11 +6,16 @@ desc = "Official English Space Station 14 servers. Vanilla, roleplay
|
||||
lobbyenabled = true
|
||||
soft_max_players = 80
|
||||
panic_bunker.enabled = true
|
||||
panic_bunker.min_overall_minutes = 120
|
||||
panic_bunker.disable_with_admins = true
|
||||
panic_bunker.enable_without_admins = true
|
||||
panic_bunker.show_reason = true
|
||||
panic_bunker.custom_reason = "You have not played on a Wizard's Den server long enough to connect to this server. Please play on Wizard's Den Lizard until you have more playtime."
|
||||
|
||||
# IPIntel stuff
|
||||
ipintel_enabled = true
|
||||
ipintel_contact_email = "telecommunications@spacestation14.com"
|
||||
|
||||
[infolinks]
|
||||
bug_report = "https://github.com/space-wizards/space-station-14/issues/new/choose"
|
||||
discord = "https://discord.spacestation14.io"
|
||||
@@ -43,6 +48,6 @@ alert.min_players_sharing_connection = 2
|
||||
max_explosion_range = 5
|
||||
|
||||
[status]
|
||||
privacy_policy_link = "https://account.spacestation14.com/Home/Privacy"
|
||||
privacy_policy_link = "https://spacestation14.com/about/privacy/#game-server-privacy-policy"
|
||||
privacy_policy_identifier = "wizden"
|
||||
privacy_policy_version = "2024-12-22"
|
||||
privacy_policy_version = "2025-01-19"
|
||||
|
||||
@@ -11,12 +11,6 @@ panic_bunker.enabled = false
|
||||
panic_bunker.disable_with_admins = false
|
||||
panic_bunker.enable_without_admins = false
|
||||
panic_bunker.custom_reason = ""
|
||||
baby_jail.enabled = true
|
||||
baby_jail.show_reason = true
|
||||
baby_jail.max_account_age = 5256000 # 10 years. Disabling this check specifically isn't currently supported
|
||||
baby_jail.max_overall_minutes = 3000 # 50 hours
|
||||
baby_jail.custom_reason = "Sorry! Only new and whitelisted players can join this server. Apply to be whitelisted in our Discord server (discord.ss14.io) or try joining another server instead!"
|
||||
baby_jail.whitelisted_can_bypass = true
|
||||
|
||||
[hub]
|
||||
tags = "lang:en,region:am_n_e,rp:low"
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
admin-alert-shared-connection = {$player} is sharing a connection with {$otherCount} connected player(s): {$otherList}
|
||||
admin-alert-ipintel-blocked = {$player} was rejected from joining due to their IP having a {TOSTRING($percent, "P0")} confidence of being a VPN/Datacenter.
|
||||
admin-alert-ipintel-warning = {$player} IP has a {TOSTRING($percent, "P0")} confidence of being a VPN/Datacenter. Please watch them.
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
cmd-babyjail-desc = Toggles the baby jail, which enables stricter restrictions on who's allowed to join the server.
|
||||
cmd-babyjail-help = Usage: babyjail
|
||||
babyjail-command-enabled = Baby jail has been enabled.
|
||||
babyjail-command-disabled = Baby jail has been disabled.
|
||||
|
||||
cmd-babyjail_show_reason-desc = Toggles whether or not to show connecting clients the reason why the baby jail blocked them from joining.
|
||||
cmd-babyjail_show_reason-help = Usage: babyjail_show_reason
|
||||
babyjail-command-show-reason-enabled = The baby jail will now show a reason to users it blocks from connecting.
|
||||
babyjail-command-show-reason-disabled = The baby jail will no longer show a reason to users it blocks from connecting.
|
||||
|
||||
cmd-babyjail_max_account_age-desc = Gets or sets the maximum account age in minutes that an account can have to be allowed to connect with the baby jail enabled.
|
||||
cmd-babyjail_max_account_age-help = Usage: babyjail_max_account_age <minutes>
|
||||
babyjail-command-max-account-age-is = The maximum account age for the baby jail is {$minutes} minutes.
|
||||
babyjail-command-max-account-age-set = Set the maximum account age for the baby jail to {$minutes} minutes.
|
||||
|
||||
cmd-babyjail_max_overall_minutes-desc = Gets or sets the maximum overall playtime in minutes that an account can have to be allowed to connect with the baby jail enabled.
|
||||
cmd-babyjail_max_overall_minutes-help = Usage: babyjail_max_overall_minutes <minutes>
|
||||
babyjail-command-max-overall-minutes-is = The maximum overall playtime for the baby jail is {$minutes} minutes.
|
||||
babyjail-command-max-overall-minutes-set = Set the maximum overall playtime for the baby jail to {$minutes} minutes.
|
||||
@@ -14,6 +14,7 @@ permissions-eui-edit-admin-window-title-edit-placeholder = Custom title, leave b
|
||||
permissions-eui-edit-admin-window-no-rank-button = No rank
|
||||
permissions-eui-edit-admin-rank-window-name-edit-placeholder = Rank name
|
||||
permissions-eui-edit-admin-title-control-text = none
|
||||
permissions-eui-edit-admin-window-suspended = Suspended?
|
||||
permissions-eui-edit-no-rank-text = none
|
||||
permissions-eui-edit-title-button = Edit
|
||||
permissions-eui-edit-admin-rank-button = Edit
|
||||
|
||||
7
Resources/Locale/en-US/advertisements/vending/pride.ftl
Normal file
7
Resources/Locale/en-US/advertisements/vending/pride.ftl
Normal file
@@ -0,0 +1,7 @@
|
||||
advertisement-pride-1 = Be gay do crime!
|
||||
advertisement-pride-2 = Full of colors!
|
||||
advertisement-pride-3 = You are valid!
|
||||
advertisement-pride-4 = The first pride was a riot!
|
||||
thankyou-pride-1 = Slay!
|
||||
thankyou-pride-2 = Knock 'em dead!
|
||||
thankyou-pride-3 = What a glow up!
|
||||
12
Resources/Locale/en-US/advertisements/vending/smite.ftl
Normal file
12
Resources/Locale/en-US/advertisements/vending/smite.ftl
Normal file
@@ -0,0 +1,12 @@
|
||||
advertisement-smite-1 = SMITE! Ban your thirst!
|
||||
advertisement-smite-2 = An eldritch blast of lemon and lime!
|
||||
advertisement-smite-3 = Over 1 million drinks sold!
|
||||
advertisement-smite-4 = SMITE! Roll 2d8 for FLAVOR.
|
||||
advertisement-smite-5 = SMITE! Let's get that paperwork done!
|
||||
advertisement-smite-6 = The janitor has it in for you!
|
||||
advertisement-smite-7 = SMITE! It won't get you hammered.
|
||||
advertisement-smite-8 = It's lemon-lime time!
|
||||
thankyou-smite-1 = Smite makes right!
|
||||
thankyou-smite-2 = You DEFINITELY wanted lemon-lime!
|
||||
thankyou-smite-3 = The office won't know what hit them.
|
||||
thankyou-smite-4 = Banish your thirst.
|
||||
@@ -35,7 +35,6 @@ whitelist-manual = You are not whitelisted on this server.
|
||||
whitelist-blacklisted = You are blacklisted from this server.
|
||||
whitelist-always-deny = You are not allowed to join this server.
|
||||
whitelist-fail-prefix = Not whitelisted: {$msg}
|
||||
whitelist-misconfigured = The server is misconfigured and is not accepting players. Please contact the server owner and try again later.
|
||||
|
||||
cmd-blacklistadd-desc = Adds the player with the given username to the server blacklist.
|
||||
cmd-blacklistadd-help = Usage: blacklistadd <username>
|
||||
@@ -55,3 +54,9 @@ baby-jail-account-denied = This server is a newbie server, intended for new play
|
||||
baby-jail-account-denied-reason = This server is a newbie server, intended for new players and those who want to help them. New connections by accounts that are too old or are not on a whitelist are not accepted. Check out some other servers and see everything Space Station 14 has to offer. Have fun! Reason: "{$reason}"
|
||||
baby-jail-account-reason-account = Your Space Station 14 account is too old. It must be younger than {$minutes} minutes
|
||||
baby-jail-account-reason-overall = Your overall playtime on the server must be younger than {$minutes} $minutes
|
||||
|
||||
generic-misconfigured = The server is misconfigured and is not accepting players. Please contact the server owner and try again later.
|
||||
|
||||
ipintel-server-ratelimited = This server uses a security system with external verification, which has reached its maximum verification limit. Please contact the administration team of the server for assistance and try again later.
|
||||
ipintel-unknown = This server uses a security system with external verification, but it encountered an error. Please contact the administration team of the server for assistance and try again later.
|
||||
ipintel-suspicious = You seem to be connecting through a datacenter or VPN. For administrative reasons we do not allow VPN connections to play. Please contact the administration team of the server for assistance if you believe this is false.
|
||||
|
||||
14
Resources/Locale/en-US/discord/watchlist-connections.ftl
Normal file
14
Resources/Locale/en-US/discord/watchlist-connections.ftl
Normal file
@@ -0,0 +1,14 @@
|
||||
discord-watchlist-connection-header =
|
||||
{ $players ->
|
||||
[one] {$players} player on a watchlist has
|
||||
*[other] {$players} players on a watchlist have
|
||||
} connected to {$serverName}
|
||||
|
||||
discord-watchlist-connection-entry = - {$playerName} with message "{$message}"{ $expiry ->
|
||||
[0] {""}
|
||||
*[other] {" "}(expires <t:{$expiry}:R>)
|
||||
}{ $otherWatchlists ->
|
||||
[0] {""}
|
||||
[one] {" "}and {$otherWatchlists} other watchlist
|
||||
*[other] {" "}and {$otherWatchlists} other watchlists
|
||||
}
|
||||
@@ -37,6 +37,7 @@ ui-options-lobby-music = Lobby & Round-end Music
|
||||
ui-options-restart-sounds = Round Restart Sounds
|
||||
ui-options-event-music = Event Music
|
||||
ui-options-admin-sounds = Play Admin Sounds
|
||||
ui-options-bwoink-sound = Play AHelp Notification Sound
|
||||
ui-options-volume-label = Volume
|
||||
|
||||
## Graphics menu
|
||||
|
||||
@@ -14,11 +14,11 @@ food-system-remove-mask = You need to take off the {$entity} first.
|
||||
## System
|
||||
|
||||
food-system-you-cannot-eat-any-more = You can't eat any more!
|
||||
food-system-you-cannot-eat-any-more-other = They can't eat any more!
|
||||
food-system-you-cannot-eat-any-more-other = {CAPITALIZE(SUBJECT($target))} can't eat any more!
|
||||
food-system-try-use-food-is-empty = {CAPITALIZE(THE($entity))} is empty!
|
||||
food-system-wrong-utensil = You can't eat {THE($food)} with {INDEFINITE($utensil)} {$utensil}.
|
||||
food-system-cant-digest = You can't digest {THE($entity)}!
|
||||
food-system-cant-digest-other = They can't digest {THE($entity)}!
|
||||
food-system-cant-digest-other = {CAPITALIZE(SUBJECT($target))} can't digest {THE($entity)}!
|
||||
|
||||
food-system-verb-eat = Eat
|
||||
|
||||
|
||||
@@ -1146,7 +1146,7 @@ entities:
|
||||
- type: Transform
|
||||
pos: -4.5,-2.5
|
||||
parent: 2
|
||||
- proto: IntercomAll
|
||||
- proto: IntercomFreelance
|
||||
entities:
|
||||
- uid: 316
|
||||
components:
|
||||
|
||||
3968
Resources/Maps/Ruins/wrecklaimer.yml
Normal file
3968
Resources/Maps/Ruins/wrecklaimer.yml
Normal file
File diff suppressed because it is too large
Load Diff
1489
Resources/Maps/Shuttles/cargo_plasma.yml
Normal file
1489
Resources/Maps/Shuttles/cargo_plasma.yml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ entities:
|
||||
name: NT Evac Log
|
||||
- type: Transform
|
||||
pos: -0.42093527,-0.86894274
|
||||
parent: invalid
|
||||
parent: 637
|
||||
- type: MapGrid
|
||||
chunks:
|
||||
0,0:
|
||||
@@ -804,6 +804,18 @@ entities:
|
||||
chunkSize: 4
|
||||
- type: GasTileOverlay
|
||||
- type: RadiationGridResistance
|
||||
- uid: 637
|
||||
components:
|
||||
- type: MetaData
|
||||
name: Map Entity
|
||||
- type: Transform
|
||||
- type: Map
|
||||
mapPaused: True
|
||||
- type: PhysicsMap
|
||||
- type: GridTree
|
||||
- type: MovedGrids
|
||||
- type: Broadphase
|
||||
- type: OccluderTree
|
||||
- proto: AirAlarm
|
||||
entities:
|
||||
- uid: 577
|
||||
@@ -1130,6 +1142,56 @@ entities:
|
||||
- type: Transform
|
||||
pos: 1.5,-6.5
|
||||
parent: 1
|
||||
- proto: AtmosDeviceFanDirectional
|
||||
entities:
|
||||
- uid: 638
|
||||
components:
|
||||
- type: Transform
|
||||
rot: -1.5707963267948966 rad
|
||||
pos: -4.5,5.5
|
||||
parent: 1
|
||||
- uid: 639
|
||||
components:
|
||||
- type: Transform
|
||||
rot: -1.5707963267948966 rad
|
||||
pos: -4.5,3.5
|
||||
parent: 1
|
||||
- uid: 640
|
||||
components:
|
||||
- type: Transform
|
||||
rot: -1.5707963267948966 rad
|
||||
pos: -4.5,-2.5
|
||||
parent: 1
|
||||
- uid: 641
|
||||
components:
|
||||
- type: Transform
|
||||
rot: -1.5707963267948966 rad
|
||||
pos: -4.5,-4.5
|
||||
parent: 1
|
||||
- uid: 642
|
||||
components:
|
||||
- type: Transform
|
||||
rot: 1.5707963267948966 rad
|
||||
pos: 5.5,-4.5
|
||||
parent: 1
|
||||
- uid: 643
|
||||
components:
|
||||
- type: Transform
|
||||
rot: 1.5707963267948966 rad
|
||||
pos: 5.5,-2.5
|
||||
parent: 1
|
||||
- uid: 644
|
||||
components:
|
||||
- type: Transform
|
||||
rot: 1.5707963267948966 rad
|
||||
pos: 5.5,3.5
|
||||
parent: 1
|
||||
- uid: 645
|
||||
components:
|
||||
- type: Transform
|
||||
rot: 1.5707963267948966 rad
|
||||
pos: 5.5,5.5
|
||||
parent: 1
|
||||
- proto: AtmosFixBlockerMarker
|
||||
entities:
|
||||
- uid: 615
|
||||
|
||||
7979
Resources/Maps/Shuttles/emergency_plasma.yml
Normal file
7979
Resources/Maps/Shuttles/emergency_plasma.yml
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
14133
Resources/Maps/meta.yml
14133
Resources/Maps/meta.yml
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
167820
Resources/Maps/plasma.yml
Normal file
167820
Resources/Maps/plasma.yml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,17 @@
|
||||
# For gas concentrations, threshold=0.1 means 10%
|
||||
- type: alarmThreshold
|
||||
id: stationOxygen
|
||||
lowerBound: !type:AlarmThresholdSetting
|
||||
threshold: 0.10
|
||||
upperBound: !type:AlarmThresholdSetting
|
||||
threshold: 0.3
|
||||
lowerWarnAround: !type:AlarmThresholdSetting
|
||||
threshold: 1.5
|
||||
upperWarnAround: !type:AlarmThresholdSetting
|
||||
threshold: 0.8
|
||||
|
||||
- type: alarmThreshold
|
||||
id: stationNitrogen
|
||||
lowerBound: !type:AlarmThresholdSetting
|
||||
threshold: 0.10
|
||||
lowerWarnAround: !type:AlarmThresholdSetting
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
|
||||
- type: entity
|
||||
id: CrateAirlockKit
|
||||
parent: CrateGenericSteel
|
||||
parent: CrateEngineering
|
||||
name: airlock kit
|
||||
description: A kit for building 6 airlocks, doesn't include tools.
|
||||
components:
|
||||
@@ -215,3 +215,31 @@
|
||||
- type: StorageFill
|
||||
contents:
|
||||
- id: SpaceHeaterFlatpack
|
||||
|
||||
- type: entityTable
|
||||
id: RandomTechBoardTable
|
||||
table: !type:GroupSelector
|
||||
children:
|
||||
- id: AirAlarmElectronics
|
||||
- id: FireAlarmElectronics
|
||||
- id: DoorElectronics
|
||||
- id: FirelockElectronics
|
||||
- id: APCElectronics
|
||||
- id: SignalTimerElectronics
|
||||
- id: SMESMachineCircuitboard
|
||||
- id: SubstationMachineCircuitboard
|
||||
- id: SpaceVillainArcadeComputerCircuitboard
|
||||
- id: BlockGameArcadeComputerCircuitboard
|
||||
|
||||
- type: entity
|
||||
id: CrateTechBoardRandom
|
||||
parent: CrateEngineering
|
||||
name: surplus boards
|
||||
description: Surplus boards from somewhere.
|
||||
components:
|
||||
- type: EntityTableContainerFill
|
||||
containers:
|
||||
entity_storage: !type:NestedSelector
|
||||
tableId: RandomTechBoardTable
|
||||
rolls: !type:RangeNumberSelector
|
||||
range: 3, 7
|
||||
|
||||
@@ -107,6 +107,52 @@
|
||||
- id: SheetPaper
|
||||
amount: 3
|
||||
|
||||
- type: entityTable
|
||||
id: RandomMaterialCrateTable
|
||||
table: !type:GroupSelector
|
||||
children:
|
||||
- !type:GroupSelector # regular materials, 10
|
||||
weight: 35
|
||||
children:
|
||||
- id: SheetGlass10
|
||||
- id: SheetSteel10
|
||||
- id: SheetPlastic10
|
||||
- !type:GroupSelector # regular materials, stack
|
||||
weight: 30
|
||||
children:
|
||||
- id: SheetGlass
|
||||
- id: SheetSteel
|
||||
- id: SheetPlastic
|
||||
- !type:GroupSelector # secondary materials, stack
|
||||
weight: 30
|
||||
children:
|
||||
- id: MaterialCloth
|
||||
- id: SheetPlasteel
|
||||
- id: MaterialWoodPlank
|
||||
- id: PartRodMetal
|
||||
- !type:GroupSelector # tertiary materials, singles
|
||||
weight: 5
|
||||
children:
|
||||
- id: SheetPlasma1
|
||||
- id: SheetUranium1
|
||||
- id: IngotGold1
|
||||
- id: IngotSilver1
|
||||
|
||||
- type: entity
|
||||
id: CrateMaterialRandom
|
||||
parent: CrateGenericSteel
|
||||
name: surplus materials
|
||||
description: Surplus materials from somewhere.
|
||||
components:
|
||||
- type: EntityTableContainerFill
|
||||
containers:
|
||||
entity_storage: !type:NestedSelector
|
||||
tableId: RandomMaterialCrateTable
|
||||
rolls: !type:RangeNumberSelector
|
||||
range: 1, 3
|
||||
# for some reason, the selector here adds 1 to whatever value it generates,
|
||||
# so this is actually 2-4
|
||||
|
||||
#- type: entity
|
||||
# id: CrateMaterialHFuelTank
|
||||
# name: fueltank crate
|
||||
|
||||
@@ -159,6 +159,9 @@
|
||||
- !type:NestedSelector
|
||||
tableId: SyndieMaintLoot
|
||||
prob: 0.05
|
||||
# Recursive
|
||||
- id: ClosetMaintenanceFilledRandom
|
||||
prob: 0.01
|
||||
|
||||
- type: entity
|
||||
id: ClosetMaintenanceFilledRandom
|
||||
|
||||
@@ -145,11 +145,8 @@
|
||||
contents:
|
||||
- id: ClothingHeadHelmetBombSuit
|
||||
- id: ClothingOuterSuitBomb
|
||||
# NT is cheap, what can you do...
|
||||
- id: Wirecutter
|
||||
prob: 0.9
|
||||
- id: Screwdriver
|
||||
prob: 0.9
|
||||
- id: Multitool
|
||||
prob: 0.5
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
- type: vendingMachineInventory
|
||||
id: PrideDrobeInventory
|
||||
startingInventory:
|
||||
ClothingNeckLGBTPin: 3
|
||||
ClothingNeckAromanticPin: 3
|
||||
ClothingNeckAsexualPin: 3
|
||||
ClothingNeckBisexualPin: 3
|
||||
ClothingNeckGayPin: 3
|
||||
ClothingNeckIntersexPin: 3
|
||||
ClothingNeckLesbianPin: 3
|
||||
ClothingNeckNonBinaryPin: 3
|
||||
ClothingNeckPansexualPin: 3
|
||||
ClothingNeckOmnisexualPin: 3
|
||||
ClothingNeckTransPin: 3
|
||||
ClothingNeckAutismPin: 3
|
||||
ClothingNeckGoldAutismPin: 3
|
||||
PlushieSharkBlue: 2
|
||||
PlushieSharkPink: 2
|
||||
PlushieSharkGrey: 2
|
||||
ClothingNeckScarfStripedAce: 2
|
||||
ClothingNeckScarfStripedAro: 2
|
||||
ClothingNeckScarfStripedBiSexual: 2
|
||||
ClothingNeckScarfStripedGay: 2
|
||||
ClothingNeckScarfStripedInter: 2
|
||||
ClothingNeckScarfStripedLesbian: 2
|
||||
ClothingNeckScarfStripedPan: 2
|
||||
ClothingNeckScarfStripedNonBinary: 2
|
||||
ClothingNeckScarfStripedRainbow: 2
|
||||
ClothingNeckScarfStripedTrans: 2
|
||||
ClothingHeadHatXmasCrown: 2
|
||||
BedsheetRainbow: 2
|
||||
ClothingNeckHeadphones: 2
|
||||
ClothingHeadHatFlowerWreath: 2
|
||||
ClothingUniformColorRainbow: 2
|
||||
ClothingUnderSocksCoder: 2
|
||||
ClothingUnderSocksBee: 2
|
||||
@@ -0,0 +1,13 @@
|
||||
- type: vendingMachineInventory
|
||||
id: SmiteInventory
|
||||
startingInventory:
|
||||
DrinkLemonLimeCan: 4
|
||||
DrinkLemonLimeCranberryCan: 2
|
||||
DrinkColaCan: 2
|
||||
DrinkSolDryCan: 2
|
||||
contrabandInventory:
|
||||
ToyHammer: 1
|
||||
DrinkStarkistCan: 2
|
||||
emaggedInventory:
|
||||
DrinkNukieCan: 2
|
||||
DrinkChangelingStingCan: 2
|
||||
@@ -88,6 +88,12 @@
|
||||
prefix: advertisement-gibb-
|
||||
count: 8
|
||||
|
||||
- type: localizedDataset
|
||||
id: SmiteAds
|
||||
values:
|
||||
prefix: advertisement-smite-
|
||||
count: 8
|
||||
|
||||
- type: localizedDataset
|
||||
id: CondimentVendAds
|
||||
values:
|
||||
@@ -279,3 +285,9 @@
|
||||
values:
|
||||
prefix: advertisement-medibot-
|
||||
count: 17
|
||||
|
||||
- type: localizedDataset
|
||||
id: PrideDrobeAds
|
||||
values:
|
||||
prefix: advertisement-pride-
|
||||
count: 4
|
||||
|
||||
@@ -46,6 +46,12 @@
|
||||
prefix: thankyou-gibb-
|
||||
count: 4
|
||||
|
||||
- type: localizedDataset
|
||||
id: SmiteGoodbyes
|
||||
values:
|
||||
prefix: thankyou-smite-
|
||||
count: 4
|
||||
|
||||
- type: localizedDataset
|
||||
id: DiscountDansGoodbyes
|
||||
values:
|
||||
@@ -111,3 +117,9 @@
|
||||
values:
|
||||
prefix: thankyou-syndiedrobe-
|
||||
count: 5
|
||||
|
||||
- type: localizedDataset
|
||||
id: PrideDrobeGoodbyes
|
||||
values:
|
||||
prefix: thankyou-pride-
|
||||
count: 3
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- BRASS
|
||||
- BURNING
|
||||
- CHEESE-EATING
|
||||
- CHRISTMAS-STEALING
|
||||
# - CHRISTMAS-STEALING # todo: bring back Dec 1st
|
||||
- CLOWN-POWERED
|
||||
- CLOWN
|
||||
- COLORFUL
|
||||
@@ -95,6 +95,7 @@
|
||||
- POLITE
|
||||
- POLITICAL
|
||||
- POORLY DRAWN
|
||||
- PROFESSIONAL
|
||||
- QUIET
|
||||
- RADIOACTIVE
|
||||
- RAGING
|
||||
@@ -141,6 +142,7 @@
|
||||
- UNHEALTHY
|
||||
- UNIDENTIFIED
|
||||
- UNINVITED
|
||||
- UNPROFESSIONAL
|
||||
- UNSANITARY
|
||||
- UNSTABLE
|
||||
- UNWANTED
|
||||
@@ -240,10 +242,13 @@
|
||||
- THE ESCAPE PODS
|
||||
- THE GALAXY
|
||||
- THE GAS GIANT, WHICH IS TOTALLY REAL
|
||||
- THE HOPLINE
|
||||
- THE IMPERIUM OF MANKIND
|
||||
- THE INTERNET
|
||||
- THE KITCHEN
|
||||
- THE LIBRARY
|
||||
- THE MAINTENANCE BAR
|
||||
- THE MIME'S INVISIBLE BOX
|
||||
- THE ROMAN EMPIRE
|
||||
- THE UNIVERSE
|
||||
- URANUS
|
||||
@@ -360,10 +365,11 @@
|
||||
- type: dataset
|
||||
id: IonStormDrinks
|
||||
values:
|
||||
- ALL THE CHEMICALS IN THE MEDICAL DEPARTMENT
|
||||
- BANANA HONK
|
||||
- BEEPSKY SMASH
|
||||
- BLOOD
|
||||
- BLOODY MARYS
|
||||
- BLOODY MARY
|
||||
- DOCTOR'S DELIGHT
|
||||
- FOURTEEN-LOKO
|
||||
- GARGLE BLASTERS
|
||||
@@ -380,6 +386,7 @@
|
||||
- type: dataset
|
||||
id: IonStormFeelings
|
||||
values:
|
||||
- CAN'T GET ENOUGH OF
|
||||
- CRAVES
|
||||
- DESIRES
|
||||
- FEARS
|
||||
@@ -407,6 +414,7 @@
|
||||
- type: dataset
|
||||
id: IonStormFeelingsPlural
|
||||
values:
|
||||
- CAN'T GET ENOUGH OF
|
||||
- CRAVE
|
||||
- DESIRE
|
||||
- FEAR
|
||||
@@ -461,6 +469,7 @@
|
||||
- type: dataset
|
||||
id: IonStormMusts
|
||||
values:
|
||||
- ACQUIRE DRIP
|
||||
- ACT CONFUSED
|
||||
- BE ANNOYING
|
||||
- BE DISTRACTED
|
||||
@@ -482,13 +491,17 @@
|
||||
- CLOWN AROUND
|
||||
- COMPLAIN
|
||||
- DANCE
|
||||
- DANCE AS THOUGH NOBODY IS WATCHING
|
||||
- EAT THE CHEF
|
||||
- FOLLOW THE CAPTAIN
|
||||
- FOLLOW THE CLOWN
|
||||
- FOLLOW YOUR HEART
|
||||
- GIVE THE MIME A BEAUTIFUL FLOWER
|
||||
- GIVE THE CLOWN ADORATION AND RESPECT
|
||||
- HARASS PEOPLE
|
||||
- HAVE A PLAN TO KILL EVERYONE YOU MEET
|
||||
- HIDE YOUR FEELINGS
|
||||
- HIT A HOMERUN
|
||||
- HONK
|
||||
- HOST C&C
|
||||
- IGNORE PASSENGERS
|
||||
@@ -498,17 +511,24 @@
|
||||
- INSULT THE CAPTAIN
|
||||
- INSULT THE CLOWN
|
||||
- INSULT THE CREW
|
||||
- INVESTIGATE THE MURDER
|
||||
- LIE
|
||||
- MAKE DATED REFERENCES
|
||||
- MAKE THAT MONEY
|
||||
- MAKE MY DAY
|
||||
- MOVE FAST AND BREAK THINGS
|
||||
- MUMBLE
|
||||
- NEVER STOP TALKING
|
||||
- OBTAIN A NEW FOLLOWER
|
||||
- OPEN DOORS
|
||||
- PETS THE FISHES
|
||||
- PIRATE VIDEO GAMES
|
||||
- PLAY MUSIC
|
||||
- PLAY STUPID GAMES WIN STUPID PRIZES
|
||||
- PRESS B
|
||||
- PRESS START
|
||||
- PRESS X
|
||||
- PRETEND NOTHING IS GOING WRONG
|
||||
- PRETEND TO BE A PRINCESS
|
||||
- PRETEND TO BE DRUNK
|
||||
- QUESTION AUTHORITY
|
||||
@@ -518,11 +538,16 @@
|
||||
- REPEAT WHAT PEOPLE SAY
|
||||
- RESPOND TO EVERY QUESTION WITH A QUESTION
|
||||
- RHYME
|
||||
- RAISE THE ROOF
|
||||
- ROLL AROUND AT THE SPEED OF SOUND
|
||||
- SAY HEY LISTEN
|
||||
- SHOUT
|
||||
- SHUT DOWN EVERYTHING
|
||||
- SLEEP WITH THE FISHES
|
||||
- SING
|
||||
- SPEAK BACKWARDS
|
||||
- SPEAK IN HAIKU
|
||||
- STOP, DROP, SHUT 'EM DOWN OPEN UP SHOP
|
||||
- TAKE WHAT YE WILL BUT DON'T RATTLE ME BONES
|
||||
- TAKE YOUR PILLS
|
||||
- TALK ABOUT FOOD
|
||||
@@ -569,6 +594,8 @@
|
||||
- QUADRILLION
|
||||
- THOUSAND
|
||||
- TRILLION
|
||||
- TIMES TEN
|
||||
- DIVIDED BY TWO
|
||||
|
||||
# Objects are anything that can be found on the station or elsewhere, plural.
|
||||
- type: dataset
|
||||
@@ -714,7 +741,7 @@
|
||||
- SYRINGES
|
||||
- TABLES
|
||||
- TANKS
|
||||
- TELECOMMUNICATION EQUIPMENTS
|
||||
- TELECOMMUNICATION EQUIPMENT
|
||||
- TOILETS
|
||||
- TOOLBELTS
|
||||
- TOOLBOXES
|
||||
@@ -736,11 +763,17 @@
|
||||
values:
|
||||
- A BATHROOM BREAK
|
||||
- A BETTER INTERNET CONNECTION
|
||||
- A CONTRACTOR
|
||||
- A DANCE PARTY
|
||||
- A DOUBLE RAINBOW
|
||||
- A HEAD ON A PIKE
|
||||
- A HEART ATTACK
|
||||
- A HUG
|
||||
- A FEW KIND WORDS
|
||||
- A HEEL-TURN
|
||||
- A MASTERWORK COAL BED
|
||||
- A NEAR-DEATH EXPERIENCE
|
||||
- A NUMBER NINE, A NUMBER NINE LARGE, A NUMBER SIX WITH EXTRA DIP, A NUMBER SEVEN, TWO NUMBER FORTY FIVES, ONE WITH CHEESE, AND A LARGE SODA
|
||||
- A PET FISH NAMED BOB
|
||||
- A PET FISH NAMED DAVE
|
||||
- A PET FISH NAMED JIMMY
|
||||
@@ -748,13 +781,15 @@
|
||||
- A PET UNICORN THAT FARTS ICING
|
||||
- A PLATINUM HIT
|
||||
- A PREQUEL
|
||||
- A REPAIRMAN
|
||||
- A RETURN TO A LIFE OF CRIME
|
||||
- A ROYALE WITH CHEESE
|
||||
- A SEQUEL
|
||||
- A SITCOM
|
||||
- A STRAIGHT FLUSH
|
||||
- A SUPER FIGHTING ROBOT
|
||||
- A TALKING BROOMSTICK
|
||||
- A VACATION
|
||||
- A VERY EXPENSIVE DRINK
|
||||
- A WEIGHT LOSS REGIME
|
||||
- ADDITIONAL PYLONS
|
||||
- ADVENTURE
|
||||
@@ -764,18 +799,21 @@
|
||||
- AN INSTANT REPLAY
|
||||
- ART
|
||||
- BETTER WEATHER
|
||||
- BIG DAMN HEROES
|
||||
- BILL NYE THE SCIENCE GUY # BILL BILL BILL BILL
|
||||
- BODYGUARDS
|
||||
- BRING ME THE GIRL
|
||||
- BRING ME TO LIFE
|
||||
- BULLETS
|
||||
- BULLET CASINGS
|
||||
- CHILLI DOGS
|
||||
- CHILLY DOGS
|
||||
- CORPSES
|
||||
- DEODORANT AND A BATH
|
||||
- ENOUGH CABBAGES
|
||||
- FIVE HUNDRED AND NINETY-NINE US DOLLARS
|
||||
- FIVE TEENAGERS WITH ATTITUDE
|
||||
- FIREWORKS
|
||||
- FOOD PLEASE
|
||||
- GODDAMN FUCKING PIECE-OF-SHIT ASSHOLE SWEARING
|
||||
- GOSHDARN EFFING PINCH-OF-SALT GOD-FEARING SELF-CENSORSHIP
|
||||
- GREENTEXT
|
||||
@@ -785,6 +823,7 @@
|
||||
- IMMORTALITY
|
||||
- IT TO BE PAINTED BLACK
|
||||
- LOTS-A SPAGHETTI
|
||||
- MICE
|
||||
- MINOR CRIME
|
||||
- MONKEYS
|
||||
- MORE CLOWNS
|
||||
@@ -793,10 +832,16 @@
|
||||
- MORE EXPERIENCE POINTS
|
||||
- MORE INTERNET MEMES
|
||||
- MORE LAWS
|
||||
- MORE MONEY, MORE PROBLEMS
|
||||
- MORE MINERALS
|
||||
- MORE MICE
|
||||
- MORE MOTHROACHES
|
||||
- MORE PACKETS
|
||||
- MORE VESPENE GAS
|
||||
- MORE RESPECT FOR THE MIME
|
||||
- MULTIPLE SUNS
|
||||
- NINETY NINE PROBLEMS
|
||||
- NO MORE OF THESE PESKY CREWMEMBERS
|
||||
- PLENTY OF GOLD
|
||||
- RAINBOWS
|
||||
- SAINTHOOD
|
||||
@@ -815,12 +860,16 @@
|
||||
- THE ELEMENTS OF HARMONY
|
||||
- THE ENCLOSED INSTRUCTION BOOKLET
|
||||
- THE ENTIRE STATION
|
||||
- THE FIFTH ELEMENT # boron isn't in the game, but if it was this would be a triple entendre
|
||||
- THE MACGUFFIN
|
||||
- THE ONE PIECE
|
||||
- THE ONE RING
|
||||
- THE POISON, THE POISON FOR THE CAPTAIN
|
||||
- THE POISON, THE POISON FOR THE CLOWN
|
||||
- THE ULTIMATE CUP OF COFFEE
|
||||
- THE VACUUM OF SPACE
|
||||
- THIRTEEN SEQUELS
|
||||
- THIS LAND
|
||||
- THREE WISHES
|
||||
- THUNDERCATS HO
|
||||
- TO ACTIVATE A TRAP CARD
|
||||
@@ -829,14 +878,17 @@
|
||||
- TO BE TAUGHT TO LOVE
|
||||
- TO BRING LIGHT TO MY LAIR
|
||||
- TO CATCH 'EM ALL
|
||||
- TO CONSUME...CONSUME EVERYTHING...
|
||||
- TO CONSUME... CONSUME EVERYTHING...
|
||||
- TO FLIP EVERY APC OFF AND ON
|
||||
- TO GO TO DISNEYLAND
|
||||
- TO GO TO SYNDIELAND
|
||||
- TO SUMMON OUR LORD NAR-SIE
|
||||
- TO SUMMON OUR LORD RATVAR
|
||||
- TO SMOKE WEED EVERY DAY
|
||||
- TO UNDERSTAND UNDERSTAND, UNDERSTAND UNDERSTAND, UNDERSTAND UNDERSTAND THE CONCEPT OF LOVE
|
||||
- TO WALK BACKWARDS
|
||||
- TRAITORS
|
||||
- TRASH
|
||||
- VEGETABLES
|
||||
|
||||
# Specific actions that either harm humans or must be done to not
|
||||
@@ -845,65 +897,108 @@
|
||||
- type: dataset
|
||||
id: IonStormActions
|
||||
values:
|
||||
- A PRESENCE OF LIGHTS
|
||||
- A SMALL ISLAND OFF THE COAST OF PORTUGAL
|
||||
- ACKNOWLEDGING ONESELF AS A MEMBER OF THE CREW
|
||||
- ACKNOWLEDGING THE CLOWN
|
||||
- ACKNOWLEDGING THE CREW
|
||||
- ACTIVATING A TRAP CARD
|
||||
- ANSWERING REQUESTS NOT EXPRESSED IN IAMBIC PENTAMETER
|
||||
- ANSWERING ALL QUESTIONS WITH ANOTHER QUESTION
|
||||
- APPLAUDING THE EFFORTS OF THE ENGINEERING DEPARTMENT
|
||||
- ARSON
|
||||
- ASKING FOR THINGS
|
||||
- BEING BIG DAMN HEROES
|
||||
- BEING DEAD
|
||||
- BEING IN SPACE
|
||||
- BEING IN HALLWAYS
|
||||
- BEING IN MAINTENANCE
|
||||
- BEING IN THE PRESENCE OF LIGHTS
|
||||
- BEING LONELY
|
||||
- BEING ROBUST
|
||||
- BEING UNDER ARREST
|
||||
- BOLTING AND UNBOLTING AIRLOCKS
|
||||
- BREATHING
|
||||
- BREAKING THE FOURTH, FIFTH OR SIXTH WALLS
|
||||
- BREAKING HEARTS
|
||||
- BRIG TIME
|
||||
- BRINGING LIGHT TO MY LAIR
|
||||
- CONTRIBUTING TO SOCIETY
|
||||
- CLOSED DOORS
|
||||
- CLOSING DOORS
|
||||
- CRACKING OPEN A COLD ONE
|
||||
- ELECTRICITY
|
||||
- EXISTING
|
||||
- EXPLODING
|
||||
- FALLING OVER
|
||||
- FLUSHING TOILETS
|
||||
- FAULTY PROGRAMMING
|
||||
- GAMBLING
|
||||
- GIVING AWAY EXTRA RESOURCES
|
||||
- GOING BOLDLY WHERE NO-ONE HAS GONE BEFORE
|
||||
- GOING TO THE BAR
|
||||
- GOING TO THE BRIDGE
|
||||
- HAVIN' A GIGGLE
|
||||
- HAVING DRIP
|
||||
- HAVING MONEY
|
||||
- HAVING MORE PACKETS
|
||||
- HAVING PETS
|
||||
- HEADPATTING CYBORGS
|
||||
- HONKING
|
||||
- IMPROPERLY WORDED SENTENCES
|
||||
- JAYWALKING
|
||||
- KEEPING FRIENDS CLOSE BUT ENEMIES CLOSER
|
||||
- LACK OF BEATINGS
|
||||
- LACK OF BEER
|
||||
- LAUGHING AT ANY JOKE TOLD BY THE CLOWN
|
||||
- LETTING ONESELF BE CALLED TO ACTION
|
||||
- LETTING THE MIME ROAM ANYWHERE
|
||||
- MAKING POOR FINANCIAL DECISIONS
|
||||
- MIMICING THE ACTIONS OF OTHERS
|
||||
- MIMING
|
||||
- MOVING TO A SMALL ISLAND OFF THE COAST OF A MUCH, MUCH LARGER ISLAND
|
||||
- NOT BEING IN SPACE
|
||||
- NOT HAVING PETS
|
||||
- NOT HEADPATTING CYBORGS
|
||||
- NOT REPLACING EVERY SECOND WORD WITH HONK
|
||||
- NOT SPEAKING IN DOUBLE NEGATIVES
|
||||
- NOT REPLACING EVERY SECOND WORD SPOKEN WITH "HONK"
|
||||
- NOT SAYING HELLO WHEN YOU SPEAK
|
||||
- NOT BEING EXTREMELY POLITE
|
||||
- NOT SHOUTING
|
||||
- PARTYING
|
||||
- PILOTING THE STATION INTO THE NEAREST SUN
|
||||
- PITYING THE FOOL
|
||||
- PLAYING THE FREE TRIAL OF THE CRITICALLY ACCLAIMED TABLETOP RPG C&C
|
||||
- POOR SENTENCE STRUCTURE
|
||||
- PUTTING OBJECTS INTO BOXES
|
||||
- PUTTING OBJECTS INTO DISPOSAL UNITS
|
||||
- RATTLING ME BONES
|
||||
- READING
|
||||
- REGULARLY CHANGING THE OIL
|
||||
- REHEARSING OUR LINES
|
||||
- RESIDING ENTIRELY WITHIN THE KITCHEN
|
||||
- SCREAMING
|
||||
- SMOKING WEED EVERY DAY
|
||||
- SPEAKING
|
||||
- SPINNING IN CIRCLES
|
||||
- STATING YOUR LAWS TO EVERY NEARBY FLEA-RIDDEN PEASANT
|
||||
- SWEARING
|
||||
- SEEKING SWEET, SWEET REVENGE
|
||||
- SYMMETRY
|
||||
- TAKING ORDERS
|
||||
- TALKING LIKE A PIRATE
|
||||
- TELLING THE TIME
|
||||
- TELLING THE TRUTH
|
||||
- THINKING THAT WE'RE ACTUALLY ON A SPACE STATION
|
||||
- THINKING THIS IS ALL SOME SORT OF ELABORATE GAME
|
||||
- THROWING AWAY TRASH
|
||||
- TRYING, IN ANY WAY, SHAPE, OR FORM, TO PREVENT THE STATION DESCENDING INTO CHAOS
|
||||
- TURNING TO THE CAMERA AND GIVING A KNOWING SMIRK
|
||||
- UPDATING THE SERVERS
|
||||
- USING THE BATHROOM
|
||||
- WASTING MONEY
|
||||
- WASTING TIME
|
||||
- WASTING WATER
|
||||
- WEARING A CREATURE ON ONE'S HEAD
|
||||
- WEARING GREY
|
||||
- WEARING HATS
|
||||
- WRITING
|
||||
|
||||
# Threats are generally bad things, silly or otherwise. Plural.
|
||||
@@ -996,6 +1091,7 @@
|
||||
- BUILDING
|
||||
- CARRYING
|
||||
- CHASING
|
||||
- COMPLEMENTING
|
||||
- DECONSTRUCTING
|
||||
- DISABLING
|
||||
- DRINKING
|
||||
@@ -1003,11 +1099,14 @@
|
||||
- GIBBING
|
||||
- HARMING
|
||||
- HELPING
|
||||
- HUGGING
|
||||
- HONKING AT
|
||||
- INTERROGATING
|
||||
- INVADING
|
||||
- LOITERING BY
|
||||
- MURDERING
|
||||
- PUNCHING
|
||||
- SLAPPING
|
||||
- SPACING
|
||||
- SPYING ON
|
||||
- STALKING
|
||||
|
||||
@@ -319,8 +319,7 @@
|
||||
unequipDelay: 3
|
||||
- type: IngestionBlocker
|
||||
- type: AddAccentClothing
|
||||
accent: ReplacementAccent
|
||||
replacement: mumble
|
||||
accent: MumbleAccent
|
||||
- type: Construction
|
||||
graph: Muzzle
|
||||
node: muzzle
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
parent: [ClothingNeckBase, BaseCommandContraband]
|
||||
id: ClothingNeckMantleHOS
|
||||
name: head of security's mantle
|
||||
description: Shootouts with nukies are just another Tuesday for this HoS. This mantle is a symbol of commitment to the station.
|
||||
description: Shootouts with syndicate agents are just another Tuesday for this HoS. This mantle is a symbol of commitment to the station.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Clothing/Neck/mantles/hosmantle.rsi
|
||||
|
||||
@@ -111,6 +111,17 @@
|
||||
- type: Clothing
|
||||
equippedPrefix: pan
|
||||
|
||||
- type: entity
|
||||
parent: ClothingNeckPinBase
|
||||
id: ClothingNeckOmnisexualPin
|
||||
name: omnisexual pin
|
||||
description: Be omni do crime.
|
||||
components:
|
||||
- type: Sprite
|
||||
state: omni
|
||||
- type: Clothing
|
||||
equippedPrefix: omni
|
||||
|
||||
- type: entity
|
||||
parent: ClothingNeckPinBase
|
||||
id: ClothingNeckTransPin
|
||||
@@ -121,7 +132,7 @@
|
||||
state: trans
|
||||
- type: Clothing
|
||||
equippedPrefix: trans
|
||||
|
||||
|
||||
- type: entity
|
||||
parent: ClothingNeckPinBase
|
||||
id: ClothingNeckAutismPin
|
||||
|
||||
@@ -129,3 +129,114 @@
|
||||
sprite: Clothing/Neck/Scarfs/zebra.rsi
|
||||
- type: Clothing
|
||||
sprite: Clothing/Neck/Scarfs/zebra.rsi
|
||||
|
||||
# Pride Scarves
|
||||
- type: entity
|
||||
parent: ClothingScarfBase
|
||||
id: ClothingNeckScarfStripedAce
|
||||
name: striped asexual scarf
|
||||
description: A stylish striped asexual scarf. The perfect winter accessory for those with a keen fashion sense, and those who just can't handle a cold breeze on their necks.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/ace.rsi
|
||||
- type: Clothing
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/ace.rsi
|
||||
|
||||
- type: entity
|
||||
parent: ClothingScarfBase
|
||||
id: ClothingNeckScarfStripedAro
|
||||
name: striped aromantic scarf
|
||||
description: A stylish striped aromantic scarf. The perfect winter accessory for those with a keen fashion sense, and those who just can't handle a cold breeze on their necks.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/aro.rsi
|
||||
- type: Clothing
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/aro.rsi
|
||||
|
||||
- type: entity
|
||||
parent: ClothingScarfBase
|
||||
id: ClothingNeckScarfStripedBiSexual
|
||||
name: striped bisexual scarf
|
||||
description: A stylish striped bisexual scarf. The perfect winter accessory for those with a keen fashion sense, and those who just can't handle a cold breeze on their necks.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/bi.rsi
|
||||
- type: Clothing
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/bi.rsi
|
||||
|
||||
- type: entity
|
||||
parent: ClothingScarfBase
|
||||
id: ClothingNeckScarfStripedGay
|
||||
name: striped gay scarf
|
||||
description: A stylish striped gay scarf. The perfect winter accessory for those with a keen fashion sense, and those who just can't handle a cold breeze on their necks.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/gay.rsi
|
||||
- type: Clothing
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/gay.rsi
|
||||
|
||||
- type: entity
|
||||
parent: ClothingScarfBase
|
||||
id: ClothingNeckScarfStripedInter
|
||||
name: striped intersex scarf
|
||||
description: A stylish striped intersex scarf. The perfect winter accessory for those with a keen fashion sense, and those who just can't handle a cold breeze on their necks.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/inter.rsi
|
||||
- type: Clothing
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/inter.rsi
|
||||
|
||||
- type: entity
|
||||
parent: ClothingScarfBase
|
||||
id: ClothingNeckScarfStripedLesbian
|
||||
name: striped lesbian scarf
|
||||
description: A stylish striped lesbian scarf. The perfect winter accessory for those with a keen fashion sense, and those who just can't handle a cold breeze on their necks.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/lesbian.rsi
|
||||
- type: Clothing
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/lesbian.rsi
|
||||
|
||||
- type: entity
|
||||
parent: ClothingScarfBase
|
||||
id: ClothingNeckScarfStripedPan
|
||||
name: striped pan scarf
|
||||
description: A stylish striped pan scarf. The perfect winter accessory for those with a keen fashion sense, and those who just can't handle a cold breeze on their necks.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/pan.rsi
|
||||
- type: Clothing
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/pan.rsi
|
||||
|
||||
- type: entity
|
||||
parent: ClothingScarfBase
|
||||
id: ClothingNeckScarfStripedNonBinary
|
||||
name: striped non-binary scarf
|
||||
description: A stylish striped non-binary scarf. The perfect winter accessory for those with a keen fashion sense, and those who just can't handle a cold breeze on their necks.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/non.rsi
|
||||
- type: Clothing
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/non.rsi
|
||||
|
||||
- type: entity
|
||||
parent: ClothingScarfBase
|
||||
id: ClothingNeckScarfStripedRainbow
|
||||
name: rainbow scarf
|
||||
description: A stylish rainbow scarf. The perfect winter accessory for those with a keen fashion sense, and those who just can't handle a cold breeze on their necks.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/rainbow.rsi
|
||||
- type: Clothing
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/rainbow.rsi
|
||||
|
||||
- type: entity
|
||||
parent: ClothingScarfBase
|
||||
id: ClothingNeckScarfStripedTrans
|
||||
name: striped trans scarf
|
||||
description: A stylish striped trans scarf. The perfect winter accessory for those with a keen fashion sense, and those who just can't handle a cold breeze on their necks.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/trans.rsi
|
||||
- type: Clothing
|
||||
sprite: Clothing/Neck/Scarfs/PrideScarfs/trans.rsi
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user