Merge branch 'master' of https://github.com/space-wizards/space-station-14 into map-load-refactor

This commit is contained in:
ElectroJr
2025-01-18 04:14:18 +13:00
246 changed files with 209699 additions and 13785 deletions

View File

@@ -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;
}
}
}

View File

@@ -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>

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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"));
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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" />

View File

@@ -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

View File

@@ -4,6 +4,7 @@ namespace Content.Client
{
internal static class Program
{
[STAThread]
public static void Main(string[] args)
{
ContentStart.Start(args);

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -66,6 +66,7 @@ namespace Content.IntegrationTests.Tests
"Gate",
"Amber",
"Loop",
"Plasma",
"Elkridge"
};

View File

@@ -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();
}

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View File

@@ -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")

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View File

@@ -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")

View File

@@ -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; }
}
}

View File

@@ -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()

View File

@@ -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)));
}
}

View File

@@ -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,14 +222,14 @@ namespace Content.Server.Administration.Managers
curAdmin.IsSpecialLogin = special;
curAdmin.RankId = rankId;
curAdmin.Data = aData;
}
if (!player.ContentData()!.ExplicitlyDeadminned)
if (curAdmin.Data.Active)
{
aData.Active = true;
_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))

View File

@@ -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();
}

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -17,7 +17,7 @@ public sealed partial class ConnectionManager
{
private PlayerConnectionWhitelistPrototype[]? _whitelists;
public void PostInit()
private void InitializeWhitelist()
{
_cfg.OnValueChanged(CCVars.WhitelistPrototypeList, UpdateWhitelists, true);
}

View File

@@ -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;

View 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,
}
}

View 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}");
}
}

View File

@@ -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);

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -55,6 +55,8 @@ public sealed partial class PuddleSystem
Spawn("PuddleSparkle", xformQuery.GetComponent(uid).Coordinates);
QueueDel(uid);
}
_solutionContainerSystem.UpdateChemicals(puddle.Solution.Value);
}
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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>();
}
}
}

View File

@@ -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;
}

View File

@@ -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);
if (_powerCell.HasDrawCharge(uid))
{
Toggle.TryActivate(uid);
_powerCell.SetDrawEnabled(uid, _mobState.IsAlive(uid));
}
_appearance.SetData(uid, BorgVisuals.HasPlayer, true);
}

View File

@@ -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);

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Speech.Components;
[RegisterComponent]
public sealed partial class MumbleAccentComponent : Component
{
}

View 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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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.

View File

@@ -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",

View File

@@ -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.

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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;

View 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());
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View 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!

View 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.

View File

@@ -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.

View 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
}

View File

@@ -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

View File

@@ -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

View File

@@ -1146,7 +1146,7 @@ entities:
- type: Transform
pos: -4.5,-2.5
parent: 2
- proto: IntercomAll
- proto: IntercomFreelance
entities:
- uid: 316
components:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -159,6 +159,9 @@
- !type:NestedSelector
tableId: SyndieMaintLoot
prob: 0.05
# Recursive
- id: ClosetMaintenanceFilledRandom
prob: 0.01
- type: entity
id: ClosetMaintenanceFilledRandom

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
@@ -830,13 +879,16 @@
- TO BRING LIGHT TO MY LAIR
- TO CATCH 'EM ALL
- 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

View File

@@ -319,8 +319,7 @@
unequipDelay: 3
- type: IngestionBlocker
- type: AddAccentClothing
accent: ReplacementAccent
replacement: mumble
accent: MumbleAccent
- type: Construction
graph: Muzzle
node: muzzle

View File

@@ -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

View File

@@ -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

View File

@@ -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