Merge remote-tracking branch 'upstream/master' into ed-21-06-2024-upstream-sync
# Conflicts: # Resources/Prototypes/Maps/europa.yml # Resources/Prototypes/Maps/train.yml
This commit is contained in:
@@ -21,6 +21,9 @@ public sealed partial class AdminVerbSystem
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string DefaultTraitorRule = "Traitor";
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string DefaultInitialInfectedRule = "Zombie";
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string DefaultNukeOpRule = "LoneOpsSpawn";
|
||||
|
||||
@@ -63,6 +66,20 @@ public sealed partial class AdminVerbSystem
|
||||
};
|
||||
args.Verbs.Add(traitor);
|
||||
|
||||
Verb initialInfected = new()
|
||||
{
|
||||
Text = Loc.GetString("admin-verb-text-make-initial-infected"),
|
||||
Category = VerbCategory.Antag,
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "InitialInfected"),
|
||||
Act = () =>
|
||||
{
|
||||
_antag.ForceMakeAntag<ZombieRuleComponent>(targetPlayer, DefaultInitialInfectedRule);
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = Loc.GetString("admin-verb-make-initial-infected"),
|
||||
};
|
||||
args.Verbs.Add(initialInfected);
|
||||
|
||||
Verb zombie = new()
|
||||
{
|
||||
Text = Loc.GetString("admin-verb-text-make-zombie"),
|
||||
|
||||
@@ -9,6 +9,7 @@ using Content.Server.Administration.Managers;
|
||||
using Content.Server.Afk;
|
||||
using Content.Server.Discord;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Players.RateLimiting;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Mind;
|
||||
@@ -27,6 +28,8 @@ namespace Content.Server.Administration.Systems
|
||||
[UsedImplicitly]
|
||||
public sealed partial class BwoinkSystem : SharedBwoinkSystem
|
||||
{
|
||||
private const string RateLimitKey = "AdminHelp";
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
@@ -35,6 +38,7 @@ namespace Content.Server.Administration.Systems
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly SharedMindSystem _minds = default!;
|
||||
[Dependency] private readonly IAfkManager _afkManager = default!;
|
||||
[Dependency] private readonly PlayerRateLimitManager _rateLimit = default!;
|
||||
|
||||
[GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")]
|
||||
private static partial Regex DiscordRegex();
|
||||
@@ -80,6 +84,22 @@ namespace Content.Server.Administration.Systems
|
||||
|
||||
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
|
||||
SubscribeNetworkEvent<BwoinkClientTypingUpdated>(OnClientTypingUpdated);
|
||||
|
||||
_rateLimit.Register(
|
||||
RateLimitKey,
|
||||
new RateLimitRegistration
|
||||
{
|
||||
CVarLimitPeriodLength = CCVars.AhelpRateLimitPeriod,
|
||||
CVarLimitCount = CCVars.AhelpRateLimitCount,
|
||||
PlayerLimitedAction = PlayerRateLimitedAction
|
||||
});
|
||||
}
|
||||
|
||||
private void PlayerRateLimitedAction(ICommonSession obj)
|
||||
{
|
||||
RaiseNetworkEvent(
|
||||
new BwoinkTextMessage(obj.UserId, default, Loc.GetString("bwoink-system-rate-limited"), playSound: false),
|
||||
obj.Channel);
|
||||
}
|
||||
|
||||
private void OnOverrideChanged(string obj)
|
||||
@@ -395,6 +415,9 @@ namespace Content.Server.Administration.Systems
|
||||
return;
|
||||
}
|
||||
|
||||
if (_rateLimit.CountAction(eventArgs.SenderSession, RateLimitKey) != RateLimitStatus.Allowed)
|
||||
return;
|
||||
|
||||
var escapedText = FormattedMessage.EscapeText(message.Text);
|
||||
|
||||
string bwoinkText;
|
||||
|
||||
7
Content.Server/Antag/Components/AntagImmuneComponent.cs
Normal file
7
Content.Server/Antag/Components/AntagImmuneComponent.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Content.Server.Antag.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public partial class AntagImmuneComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
@@ -11,6 +11,7 @@ using Content.Shared.Tag;
|
||||
using Robust.Server.Audio;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Atmos.Monitor.Systems;
|
||||
@@ -86,7 +87,7 @@ public sealed class AtmosAlarmableSystem : EntitySystem
|
||||
return;
|
||||
|
||||
if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd)
|
||||
|| !args.Data.TryGetValue(AlertSource, out HashSet<string>? sourceTags))
|
||||
|| !args.Data.TryGetValue(AlertSource, out HashSet<ProtoId<TagPrototype>>? sourceTags))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using Content.Shared.Damage;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Bed
|
||||
{
|
||||
@@ -26,25 +27,29 @@ namespace Content.Server.Bed
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<HealOnBuckleComponent, BuckleChangeEvent>(ManageUpdateList);
|
||||
SubscribeLocalEvent<StasisBedComponent, BuckleChangeEvent>(OnBuckleChange);
|
||||
SubscribeLocalEvent<HealOnBuckleComponent, StrappedEvent>(OnStrapped);
|
||||
SubscribeLocalEvent<HealOnBuckleComponent, UnstrappedEvent>(OnUnstrapped);
|
||||
SubscribeLocalEvent<StasisBedComponent, StrappedEvent>(OnStasisStrapped);
|
||||
SubscribeLocalEvent<StasisBedComponent, UnstrappedEvent>(OnStasisUnstrapped);
|
||||
SubscribeLocalEvent<StasisBedComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
SubscribeLocalEvent<StasisBedComponent, GotEmaggedEvent>(OnEmagged);
|
||||
}
|
||||
|
||||
private void ManageUpdateList(EntityUid uid, HealOnBuckleComponent component, ref BuckleChangeEvent args)
|
||||
private void OnStrapped(Entity<HealOnBuckleComponent> bed, ref StrappedEvent args)
|
||||
{
|
||||
if (args.Buckling)
|
||||
{
|
||||
AddComp<HealOnBuckleHealingComponent>(uid);
|
||||
component.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(component.HealTime);
|
||||
_actionsSystem.AddAction(args.BuckledEntity, ref component.SleepAction, SleepingSystem.SleepActionId, uid);
|
||||
return;
|
||||
}
|
||||
EnsureComp<HealOnBuckleHealingComponent>(bed);
|
||||
bed.Comp.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(bed.Comp.HealTime);
|
||||
_actionsSystem.AddAction(args.Buckle, ref bed.Comp.SleepAction, SleepingSystem.SleepActionId, bed);
|
||||
|
||||
_actionsSystem.RemoveAction(args.BuckledEntity, component.SleepAction);
|
||||
_sleepingSystem.TryWaking(args.BuckledEntity);
|
||||
RemComp<HealOnBuckleHealingComponent>(uid);
|
||||
// Single action entity, cannot strap multiple entities to the same bed.
|
||||
DebugTools.AssertEqual(args.Strap.Comp.BuckledEntities.Count, 1);
|
||||
}
|
||||
|
||||
private void OnUnstrapped(Entity<HealOnBuckleComponent> bed, ref UnstrappedEvent args)
|
||||
{
|
||||
_actionsSystem.RemoveAction(args.Buckle, bed.Comp.SleepAction);
|
||||
_sleepingSystem.TryWaking(args.Buckle.Owner);
|
||||
RemComp<HealOnBuckleHealingComponent>(bed);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
@@ -82,18 +87,22 @@ namespace Content.Server.Bed
|
||||
_appearance.SetData(uid, StasisBedVisuals.IsOn, isOn);
|
||||
}
|
||||
|
||||
private void OnBuckleChange(EntityUid uid, StasisBedComponent component, ref BuckleChangeEvent args)
|
||||
private void OnStasisStrapped(Entity<StasisBedComponent> bed, ref StrappedEvent args)
|
||||
{
|
||||
// In testing this also received an unbuckle event when the bed is destroyed
|
||||
// So don't worry about that
|
||||
if (!HasComp<BodyComponent>(args.BuckledEntity))
|
||||
if (!HasComp<BodyComponent>(args.Buckle) || !this.IsPowered(bed, EntityManager))
|
||||
return;
|
||||
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, true);
|
||||
RaiseLocalEvent(args.Buckle, ref metabolicEvent);
|
||||
}
|
||||
|
||||
private void OnStasisUnstrapped(Entity<StasisBedComponent> bed, ref UnstrappedEvent args)
|
||||
{
|
||||
if (!HasComp<BodyComponent>(args.Buckle) || !this.IsPowered(bed, EntityManager))
|
||||
return;
|
||||
|
||||
var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.BuckledEntity, component.Multiplier, args.Buckling);
|
||||
RaiseLocalEvent(args.BuckledEntity, ref metabolicEvent);
|
||||
var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, false);
|
||||
RaiseLocalEvent(args.Buckle, ref metabolicEvent);
|
||||
}
|
||||
|
||||
private void OnPowerChanged(EntityUid uid, StasisBedComponent component, ref PowerChangedEvent args)
|
||||
|
||||
@@ -5,19 +5,26 @@ namespace Content.Server.Bed.Components
|
||||
[RegisterComponent]
|
||||
public sealed partial class HealOnBuckleComponent : Component
|
||||
{
|
||||
[DataField("damage", required: true)]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
/// <summary>
|
||||
/// Damage to apply to entities that are strapped to this entity.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public DamageSpecifier Damage = default!;
|
||||
|
||||
[DataField("healTime", required: false)]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float HealTime = 1f; // How often the bed applies the damage
|
||||
/// <summary>
|
||||
/// How frequently the damage should be applied, in seconds.
|
||||
/// </summary>
|
||||
[DataField(required: false)]
|
||||
public float HealTime = 1f;
|
||||
|
||||
[DataField("sleepMultiplier")]
|
||||
/// <summary>
|
||||
/// Damage multiplier that gets applied if the entity is sleeping.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float SleepMultiplier = 3f;
|
||||
|
||||
public TimeSpan NextHealTime = TimeSpan.Zero; //Next heal
|
||||
|
||||
[DataField("sleepAction")] public EntityUid? SleepAction;
|
||||
[DataField] public EntityUid? SleepAction;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace Content.Server.Bed.Components
|
||||
{
|
||||
// TODO rename this component
|
||||
[RegisterComponent]
|
||||
public sealed partial class HealOnBuckleHealingComponent : Component
|
||||
{}
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace Content.Server.Bed.Components
|
||||
/// <summary>
|
||||
/// What the metabolic update rate will be multiplied by (higher = slower metabolism)
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[ViewVariables(VVAccess.ReadOnly)] // Writing is is not supported. ApplyMetabolicMultiplierEvent needs to be refactored first
|
||||
[DataField]
|
||||
public float Multiplier = 10f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +268,9 @@ public sealed class BloodstreamSystem : EntitySystem
|
||||
Entity<BloodstreamComponent> ent,
|
||||
ref ApplyMetabolicMultiplierEvent args)
|
||||
{
|
||||
// TODO REFACTOR THIS
|
||||
// This will slowly drift over time due to floating point errors.
|
||||
// Instead, raise an event with the base rates and allow modifiers to get applied to it.
|
||||
if (args.Apply)
|
||||
{
|
||||
ent.Comp.UpdateInterval *= args.Multiplier;
|
||||
|
||||
@@ -67,6 +67,9 @@ namespace Content.Server.Body.Systems
|
||||
Entity<MetabolizerComponent> ent,
|
||||
ref ApplyMetabolicMultiplierEvent args)
|
||||
{
|
||||
// TODO REFACTOR THIS
|
||||
// This will slowly drift over time due to floating point errors.
|
||||
// Instead, raise an event with the base rates and allow modifiers to get applied to it.
|
||||
if (args.Apply)
|
||||
{
|
||||
ent.Comp.UpdateInterval *= args.Multiplier;
|
||||
@@ -229,6 +232,9 @@ namespace Content.Server.Body.Systems
|
||||
}
|
||||
}
|
||||
|
||||
// TODO REFACTOR THIS
|
||||
// This will cause rates to slowly drift over time due to floating point errors.
|
||||
// Instead, the system that raised this should trigger an update and subscribe to get-modifier events.
|
||||
[ByRefEvent]
|
||||
public readonly record struct ApplyMetabolicMultiplierEvent(
|
||||
EntityUid Uid,
|
||||
|
||||
@@ -326,6 +326,9 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
Entity<RespiratorComponent> ent,
|
||||
ref ApplyMetabolicMultiplierEvent args)
|
||||
{
|
||||
// TODO REFACTOR THIS
|
||||
// This will slowly drift over time due to floating point errors.
|
||||
// Instead, raise an event with the base rates and allow modifiers to get applied to it.
|
||||
if (args.Apply)
|
||||
{
|
||||
ent.Comp.UpdateInterval *= args.Multiplier;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.PDA;
|
||||
@@ -164,6 +164,15 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
|
||||
if (!Resolve(loaderUid, ref loader))
|
||||
return false;
|
||||
|
||||
if (!TryComp(cartridgeUid, out CartridgeComponent? loadedCartridge))
|
||||
return false;
|
||||
|
||||
foreach (var program in GetInstalled(loaderUid))
|
||||
{
|
||||
if (TryComp(program, out CartridgeComponent? installedCartridge) && installedCartridge.ProgramName == loadedCartridge.ProgramName)
|
||||
return false;
|
||||
}
|
||||
|
||||
//This will eventually be replaced by serializing and deserializing the cartridge to copy it when something needs
|
||||
//the data on the cartridge to carry over when installing
|
||||
|
||||
@@ -191,7 +200,6 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
|
||||
if (container.Count >= loader.DiskSpace)
|
||||
return false;
|
||||
|
||||
// TODO cancel duplicate program installations
|
||||
var ev = new ProgramInstallationAttempt(loaderUid, prototype);
|
||||
RaiseLocalEvent(ref ev);
|
||||
|
||||
|
||||
@@ -1,84 +1,41 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Content.Server.Players.RateLimiting;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Chat.Managers;
|
||||
|
||||
internal sealed partial class ChatManager
|
||||
{
|
||||
private readonly Dictionary<ICommonSession, RateLimitDatum> _rateLimitData = new();
|
||||
private const string RateLimitKey = "Chat";
|
||||
|
||||
public bool HandleRateLimit(ICommonSession player)
|
||||
private void RegisterRateLimits()
|
||||
{
|
||||
ref var datum = ref CollectionsMarshal.GetValueRefOrAddDefault(_rateLimitData, player, out _);
|
||||
var time = _gameTiming.RealTime;
|
||||
if (datum.CountExpires < time)
|
||||
{
|
||||
// Period expired, reset it.
|
||||
var periodLength = _configurationManager.GetCVar(CCVars.ChatRateLimitPeriod);
|
||||
datum.CountExpires = time + TimeSpan.FromSeconds(periodLength);
|
||||
datum.Count = 0;
|
||||
datum.Announced = false;
|
||||
}
|
||||
|
||||
var maxCount = _configurationManager.GetCVar(CCVars.ChatRateLimitCount);
|
||||
datum.Count += 1;
|
||||
|
||||
if (datum.Count <= maxCount)
|
||||
return true;
|
||||
|
||||
// Breached rate limits, inform admins if configured.
|
||||
if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins))
|
||||
{
|
||||
if (datum.NextAdminAnnounce < time)
|
||||
_rateLimitManager.Register(RateLimitKey,
|
||||
new RateLimitRegistration
|
||||
{
|
||||
SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
|
||||
var delay = _configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdminsDelay);
|
||||
datum.NextAdminAnnounce = time + TimeSpan.FromSeconds(delay);
|
||||
}
|
||||
}
|
||||
|
||||
if (!datum.Announced)
|
||||
{
|
||||
DispatchServerMessage(player, Loc.GetString("chat-manager-rate-limited"), suppressLog: true);
|
||||
_adminLogger.Add(LogType.ChatRateLimited, LogImpact.Medium, $"Player {player} breached chat rate limits");
|
||||
|
||||
datum.Announced = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
CVarLimitPeriodLength = CCVars.ChatRateLimitPeriod,
|
||||
CVarLimitCount = CCVars.ChatRateLimitCount,
|
||||
CVarAdminAnnounceDelay = CCVars.ChatRateLimitAnnounceAdminsDelay,
|
||||
PlayerLimitedAction = RateLimitPlayerLimited,
|
||||
AdminAnnounceAction = RateLimitAlertAdmins,
|
||||
AdminLogType = LogType.ChatRateLimited,
|
||||
});
|
||||
}
|
||||
|
||||
private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
private void RateLimitPlayerLimited(ICommonSession player)
|
||||
{
|
||||
if (e.NewStatus == SessionStatus.Disconnected)
|
||||
_rateLimitData.Remove(e.Session);
|
||||
DispatchServerMessage(player, Loc.GetString("chat-manager-rate-limited"), suppressLog: true);
|
||||
}
|
||||
|
||||
private struct RateLimitDatum
|
||||
private void RateLimitAlertAdmins(ICommonSession player)
|
||||
{
|
||||
/// <summary>
|
||||
/// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) this rate limit period will expire at.
|
||||
/// </summary>
|
||||
public TimeSpan CountExpires;
|
||||
if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins))
|
||||
SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How many messages have been sent in the current rate limit period.
|
||||
/// </summary>
|
||||
public int Count;
|
||||
|
||||
/// <summary>
|
||||
/// Have we announced to the player that they've been blocked in this rate limit period?
|
||||
/// </summary>
|
||||
public bool Announced;
|
||||
|
||||
/// <summary>
|
||||
/// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) of the
|
||||
/// next time we can send an announcement to admins about rate limit breach.
|
||||
/// </summary>
|
||||
public TimeSpan NextAdminAnnounce;
|
||||
public RateLimitStatus HandleRateLimit(ICommonSession player)
|
||||
{
|
||||
return _rateLimitManager.CountAction(player, RateLimitKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,18 +5,17 @@ using Content.Server.Administration.Logs;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Administration.Systems;
|
||||
using Content.Server.MoMMI;
|
||||
using Content.Server.Players.RateLimiting;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Mind;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Replays;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Chat.Managers
|
||||
@@ -43,8 +42,7 @@ namespace Content.Server.Chat.Managers
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum length a player-sent message can be sent
|
||||
@@ -64,7 +62,7 @@ namespace Content.Server.Chat.Managers
|
||||
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
|
||||
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
|
||||
|
||||
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
|
||||
RegisterRateLimits();
|
||||
}
|
||||
|
||||
private void OnOocEnabledChanged(bool val)
|
||||
@@ -206,7 +204,7 @@ namespace Content.Server.Chat.Managers
|
||||
/// <param name="type">The type of message.</param>
|
||||
public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
|
||||
{
|
||||
if (!HandleRateLimit(player))
|
||||
if (HandleRateLimit(player) != RateLimitStatus.Allowed)
|
||||
return;
|
||||
|
||||
// Check if message exceeds the character limit
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Players.RateLimiting;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Chat;
|
||||
using Robust.Shared.Network;
|
||||
@@ -50,6 +52,6 @@ namespace Content.Server.Chat.Managers
|
||||
/// </summary>
|
||||
/// <param name="player">The player sending a chat message.</param>
|
||||
/// <returns>False if the player has violated rate limits and should be blocked from sending further messages.</returns>
|
||||
bool HandleRateLimit(ICommonSession player);
|
||||
RateLimitStatus HandleRateLimit(ICommonSession player);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Server.Administration.Managers;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Examine;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Players.RateLimiting;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Server.Station.Components;
|
||||
@@ -183,7 +184,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (player != null && !_chatManager.HandleRateLimit(player))
|
||||
if (player != null && _chatManager.HandleRateLimit(player) != RateLimitStatus.Allowed)
|
||||
return;
|
||||
|
||||
// Sus
|
||||
@@ -272,7 +273,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
if (!CanSendInGame(message, shell, player))
|
||||
return;
|
||||
|
||||
if (player != null && !_chatManager.HandleRateLimit(player))
|
||||
if (player != null && _chatManager.HandleRateLimit(player) != RateLimitStatus.Allowed)
|
||||
return;
|
||||
|
||||
// It doesn't make any sense for a non-player to send in-game OOC messages, whereas non-players may be sending
|
||||
|
||||
@@ -625,6 +625,11 @@ namespace Content.Server.Database
|
||||
return record == null ? null : MakePlayerRecord(record);
|
||||
}
|
||||
|
||||
protected async Task<bool> PlayerRecordExists(DbGuard db, NetUserId userId)
|
||||
{
|
||||
return await db.DbContext.Player.AnyAsync(p => p.UserId == userId);
|
||||
}
|
||||
|
||||
[return: NotNullIfNotNull(nameof(player))]
|
||||
protected PlayerRecord? MakePlayerRecord(Player? player)
|
||||
{
|
||||
|
||||
@@ -78,7 +78,8 @@ namespace Content.Server.Database
|
||||
await using var db = await GetDbImpl();
|
||||
|
||||
var exempt = await GetBanExemptionCore(db, userId);
|
||||
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt)
|
||||
var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value);
|
||||
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt, newPlayer)
|
||||
.OrderByDescending(b => b.BanTime);
|
||||
|
||||
var ban = await query.FirstOrDefaultAsync();
|
||||
@@ -98,7 +99,8 @@ namespace Content.Server.Database
|
||||
await using var db = await GetDbImpl();
|
||||
|
||||
var exempt = await GetBanExemptionCore(db, userId);
|
||||
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt);
|
||||
var newPlayer = !await db.PgDbContext.Player.AnyAsync(p => p.UserId == userId);
|
||||
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt, newPlayer);
|
||||
|
||||
var queryBans = await query.ToArrayAsync();
|
||||
var bans = new List<ServerBanDef>(queryBans.Length);
|
||||
@@ -122,7 +124,8 @@ namespace Content.Server.Database
|
||||
ImmutableArray<byte>? hwId,
|
||||
DbGuardImpl db,
|
||||
bool includeUnbanned,
|
||||
ServerBanExemptFlags? exemptFlags)
|
||||
ServerBanExemptFlags? exemptFlags,
|
||||
bool newPlayer)
|
||||
{
|
||||
DebugTools.Assert(!(address == null && userId == null && hwId == null));
|
||||
|
||||
@@ -141,7 +144,9 @@ namespace Content.Server.Database
|
||||
{
|
||||
var newQ = db.PgDbContext.Ban
|
||||
.Include(p => p.Unban)
|
||||
.Where(b => b.Address != null && EF.Functions.ContainsOrEqual(b.Address.Value, address));
|
||||
.Where(b => b.Address != null
|
||||
&& EF.Functions.ContainsOrEqual(b.Address.Value, address)
|
||||
&& !(b.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) && !newPlayer));
|
||||
|
||||
query = query == null ? newQ : query.Union(newQ);
|
||||
}
|
||||
@@ -167,6 +172,9 @@ namespace Content.Server.Database
|
||||
|
||||
if (exemptFlags is { } exempt)
|
||||
{
|
||||
if (exempt != ServerBanExemptFlags.None)
|
||||
exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange
|
||||
|
||||
query = query.Where(b => (b.ExemptFlags & exempt) == 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,11 +86,13 @@ namespace Content.Server.Database
|
||||
|
||||
var exempt = await GetBanExemptionCore(db, userId);
|
||||
|
||||
var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value);
|
||||
|
||||
// SQLite can't do the net masking stuff we need to match IP address ranges.
|
||||
// So just pull down the whole list into memory.
|
||||
var bans = await GetAllBans(db.SqliteDbContext, includeUnbanned: false, exempt);
|
||||
|
||||
return bans.FirstOrDefault(b => BanMatches(b, address, userId, hwId, exempt)) is { } foundBan
|
||||
return bans.FirstOrDefault(b => BanMatches(b, address, userId, hwId, exempt, newPlayer)) is { } foundBan
|
||||
? ConvertBan(foundBan)
|
||||
: null;
|
||||
}
|
||||
@@ -103,12 +105,14 @@ namespace Content.Server.Database
|
||||
|
||||
var exempt = await GetBanExemptionCore(db, userId);
|
||||
|
||||
var newPlayer = !await db.SqliteDbContext.Player.AnyAsync(p => p.UserId == userId);
|
||||
|
||||
// SQLite can't do the net masking stuff we need to match IP address ranges.
|
||||
// So just pull down the whole list into memory.
|
||||
var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned, exempt);
|
||||
|
||||
return queryBans
|
||||
.Where(b => BanMatches(b, address, userId, hwId, exempt))
|
||||
.Where(b => BanMatches(b, address, userId, hwId, exempt, newPlayer))
|
||||
.Select(ConvertBan)
|
||||
.ToList()!;
|
||||
}
|
||||
@@ -137,10 +141,18 @@ namespace Content.Server.Database
|
||||
IPAddress? address,
|
||||
NetUserId? userId,
|
||||
ImmutableArray<byte>? hwId,
|
||||
ServerBanExemptFlags? exemptFlags)
|
||||
ServerBanExemptFlags? exemptFlags,
|
||||
bool newPlayer)
|
||||
{
|
||||
// Any flag to bypass BlacklistedRange bans.
|
||||
var exemptFromBlacklistedRange = exemptFlags != null && exemptFlags.Value != ServerBanExemptFlags.None;
|
||||
|
||||
if (!exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP)
|
||||
&& address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value))
|
||||
&& address != null
|
||||
&& ban.Address is not null
|
||||
&& address.IsInSubnet(ban.Address.ToTuple().Value)
|
||||
&& (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) ||
|
||||
newPlayer && !exemptFromBlacklistedRange))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.DeviceNetwork.Systems;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
namespace Content.Server.DeviceNetwork.Systems;
|
||||
|
||||
public sealed class DeviceNetworkJammerSystem : EntitySystem
|
||||
/// <inheritdoc/>
|
||||
public sealed class DeviceNetworkJammerSystem : SharedDeviceNetworkJammerSystem
|
||||
{
|
||||
[Dependency] private TransformSystem _transform = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -14,20 +17,20 @@ public sealed class DeviceNetworkJammerSystem : EntitySystem
|
||||
SubscribeLocalEvent<TransformComponent, BeforePacketSentEvent>(BeforePacketSent);
|
||||
}
|
||||
|
||||
private void BeforePacketSent(EntityUid uid, TransformComponent xform, BeforePacketSentEvent ev)
|
||||
private void BeforePacketSent(Entity<TransformComponent> xform, ref BeforePacketSentEvent ev)
|
||||
{
|
||||
if (ev.Cancelled)
|
||||
return;
|
||||
|
||||
var query = EntityQueryEnumerator<DeviceNetworkJammerComponent, TransformComponent>();
|
||||
|
||||
while (query.MoveNext(out _, out var jammerComp, out var jammerXform))
|
||||
while (query.MoveNext(out var uid, out var jammerComp, out var jammerXform))
|
||||
{
|
||||
if (!jammerComp.JammableNetworks.Contains(ev.NetworkId))
|
||||
if (!_jammer.GetJammableNetworks((uid, jammerComp)).Contains(ev.NetworkId))
|
||||
continue;
|
||||
|
||||
if (jammerXform.Coordinates.InRange(EntityManager, _transform, ev.SenderTransform.Coordinates, jammerComp.Range)
|
||||
|| jammerXform.Coordinates.InRange(EntityManager, _transform, xform.Coordinates, jammerComp.Range))
|
||||
if (_transform.InRange(jammerXform.Coordinates, ev.SenderTransform.Coordinates, jammerComp.Range)
|
||||
|| _transform.InRange(jammerXform.Coordinates, xform.Comp.Coordinates, jammerComp.Range))
|
||||
{
|
||||
ev.Cancel();
|
||||
return;
|
||||
|
||||
@@ -14,8 +14,10 @@ using Content.Server.Info;
|
||||
using Content.Server.IoC;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Players.JobWhitelist;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Players.RateLimiting;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.ServerInfo;
|
||||
using Content.Server.ServerUpdates;
|
||||
@@ -108,6 +110,7 @@ namespace Content.Server.Entry
|
||||
_updateManager.Initialize();
|
||||
_playTimeTracking.Initialize();
|
||||
IoCManager.Resolve<JobWhitelistManager>().Initialize();
|
||||
IoCManager.Resolve<PlayerRateLimitManager>().Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@ public sealed partial class PuddleSystem
|
||||
SubscribeLocalEvent<SpillableComponent, LandEvent>(SpillOnLand);
|
||||
// Openable handles the event if it's closed
|
||||
SubscribeLocalEvent<SpillableComponent, MeleeHitEvent>(SplashOnMeleeHit, after: [typeof(OpenableSystem)]);
|
||||
SubscribeLocalEvent<SpillableComponent, ClothingGotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<SpillableComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
|
||||
SubscribeLocalEvent<SpillableComponent, SolutionContainerOverflowEvent>(OnOverflow);
|
||||
SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<SpillableComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow);
|
||||
@@ -97,33 +95,6 @@ public sealed partial class PuddleSystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGotEquipped(Entity<SpillableComponent> entity, ref ClothingGotEquippedEvent args)
|
||||
{
|
||||
if (!entity.Comp.SpillWorn)
|
||||
return;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))
|
||||
return;
|
||||
|
||||
// block access to the solution while worn
|
||||
AddComp<BlockSolutionAccessComponent>(entity);
|
||||
|
||||
if (solution.Volume == 0)
|
||||
return;
|
||||
|
||||
// spill all solution on the player
|
||||
var drainedSolution = _solutionContainerSystem.Drain(entity.Owner, soln.Value, solution.Volume);
|
||||
TrySplashSpillAt(entity.Owner, Transform(args.Wearer).Coordinates, drainedSolution, out _);
|
||||
}
|
||||
|
||||
private void OnGotUnequipped(Entity<SpillableComponent> entity, ref ClothingGotUnequippedEvent args)
|
||||
{
|
||||
if (!entity.Comp.SpillWorn)
|
||||
return;
|
||||
|
||||
RemCompDeferred<BlockSolutionAccessComponent>(entity);
|
||||
}
|
||||
|
||||
private void SpillOnLand(Entity<SpillableComponent> entity, ref LandEvent args)
|
||||
{
|
||||
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))
|
||||
|
||||
@@ -160,6 +160,12 @@ namespace Content.Server.GameTicking
|
||||
// whereas the command can also be used on an existing map.
|
||||
var loadOpts = loadOptions ?? new MapLoadOptions();
|
||||
|
||||
if (map.MaxRandomOffset != 0f)
|
||||
loadOpts.Offset = _robustRandom.NextVector2(map.MaxRandomOffset);
|
||||
|
||||
if (map.RandomRotation)
|
||||
loadOpts.Rotation = _robustRandom.NextAngle();
|
||||
|
||||
var ev = new PreGameMapLoad(targetMapId, map, loadOpts);
|
||||
RaiseLocalEvent(ev);
|
||||
|
||||
@@ -358,6 +364,7 @@ namespace Content.Server.GameTicking
|
||||
var listOfPlayerInfo = new List<RoundEndMessageEvent.RoundEndPlayerInfo>();
|
||||
// Grab the great big book of all the Minds, we'll need them for this.
|
||||
var allMinds = EntityQueryEnumerator<MindComponent>();
|
||||
var pvsOverride = _configurationManager.GetCVar(CCVars.RoundEndPVSOverrides);
|
||||
while (allMinds.MoveNext(out var mindId, out var mind))
|
||||
{
|
||||
// TODO don't list redundant observer roles?
|
||||
@@ -388,7 +395,7 @@ namespace Content.Server.GameTicking
|
||||
else if (mind.CurrentEntity != null && TryName(mind.CurrentEntity.Value, out var icName))
|
||||
playerIcName = icName;
|
||||
|
||||
if (TryGetEntity(mind.OriginalOwnedEntity, out var entity))
|
||||
if (TryGetEntity(mind.OriginalOwnedEntity, out var entity) && pvsOverride)
|
||||
{
|
||||
_pvsOverride.AddGlobalOverride(GetNetEntity(entity.Value), recursive: true);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using Content.Server.Antag;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
@@ -35,9 +36,27 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<InitialInfectedRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
||||
SubscribeLocalEvent<ZombieRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
||||
SubscribeLocalEvent<IncurableZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
|
||||
}
|
||||
|
||||
private void OnGetBriefing(EntityUid uid, InitialInfectedRoleComponent component, ref GetBriefingEvent args)
|
||||
{
|
||||
if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
|
||||
return;
|
||||
if (HasComp<ZombieRoleComponent>(uid)) // don't show both briefings
|
||||
return;
|
||||
args.Append(Loc.GetString("zombie-patientzero-role-greeting"));
|
||||
}
|
||||
|
||||
private void OnGetBriefing(EntityUid uid, ZombieRoleComponent component, ref GetBriefingEvent args)
|
||||
{
|
||||
if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
|
||||
return;
|
||||
args.Append(Loc.GetString("zombie-infection-greeting"));
|
||||
}
|
||||
|
||||
protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule,
|
||||
ref RoundEndTextAppendEvent args)
|
||||
{
|
||||
|
||||
@@ -24,6 +24,9 @@ public sealed partial class ToggleableGhostRoleComponent : Component
|
||||
[DataField("roleDescription")]
|
||||
public string RoleDescription = string.Empty;
|
||||
|
||||
[DataField("roleRules")]
|
||||
public string RoleRules = string.Empty;
|
||||
|
||||
[DataField("wipeVerbText")]
|
||||
public string WipeVerbText = string.Empty;
|
||||
|
||||
|
||||
@@ -53,13 +53,14 @@ public sealed class ToggleableGhostRoleSystem : EntitySystem
|
||||
EnsureComp<GhostTakeoverAvailableComponent>(uid);
|
||||
ghostRole.RoleName = Loc.GetString(component.RoleName);
|
||||
ghostRole.RoleDescription = Loc.GetString(component.RoleDescription);
|
||||
ghostRole.RoleRules = Loc.GetString(component.RoleRules);
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, ToggleableGhostRoleComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange)
|
||||
return;
|
||||
|
||||
|
||||
if (TryComp<MindContainerComponent>(uid, out var mind) && mind.HasMind)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString(component.ExamineTextMindPresent));
|
||||
|
||||
@@ -13,8 +13,10 @@ using Content.Server.Info;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.MoMMI;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Players.JobWhitelist;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Players.RateLimiting;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.ServerInfo;
|
||||
using Content.Server.ServerUpdates;
|
||||
@@ -63,6 +65,7 @@ namespace Content.Server.IoC
|
||||
IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
|
||||
IoCManager.Register<ServerApi>();
|
||||
IoCManager.Register<JobWhitelistManager>();
|
||||
IoCManager.Register<PlayerRateLimitManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Server.Maps;
|
||||
|
||||
@@ -21,16 +22,22 @@ public sealed partial class GameMapPrototype : IPrototype
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
[DataField]
|
||||
public float MaxRandomOffset = 1000f;
|
||||
|
||||
[DataField]
|
||||
public bool RandomRotation = true;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the map to use in generic messages, like the map vote.
|
||||
/// </summary>
|
||||
[DataField("mapName", required: true)]
|
||||
[DataField(required: true)]
|
||||
public string MapName { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Relative directory path to the given map, i.e. `/Maps/saltern.yml`
|
||||
/// </summary>
|
||||
[DataField("mapPath", required: true)]
|
||||
[DataField(required: true)]
|
||||
public ResPath MapPath { get; private set; } = default!;
|
||||
|
||||
[DataField("stations", required: true)]
|
||||
|
||||
@@ -127,10 +127,6 @@ public sealed class PullController : VirtualController
|
||||
if (_container.IsEntityInContainer(player))
|
||||
return false;
|
||||
|
||||
// Cooldown buddy
|
||||
if (_timing.CurTime < pullerComp.NextThrow)
|
||||
return false;
|
||||
|
||||
pullerComp.NextThrow = _timing.CurTime + pullerComp.ThrowCooldown;
|
||||
|
||||
// Cap the distance
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Content.Server.Buckle.Systems;
|
||||
using Content.Shared.Buckle.Components;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
|
||||
|
||||
public sealed partial class UnbuckleOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
private BuckleSystem _buckle = default!;
|
||||
|
||||
[DataField("shutdownState")]
|
||||
@@ -21,10 +19,7 @@ public sealed partial class UnbuckleOperator : HTNOperator
|
||||
{
|
||||
base.Startup(blackboard);
|
||||
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
if (!_entManager.TryGetComponent<BuckleComponent>(owner, out var buckle) || !buckle.Buckled)
|
||||
return;
|
||||
|
||||
_buckle.TryUnbuckle(owner, owner, true, buckle);
|
||||
_buckle.Unbuckle(owner, null);
|
||||
}
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
|
||||
@@ -55,8 +55,6 @@ public sealed class ConveyorController : SharedConveyorController
|
||||
if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating)
|
||||
return;
|
||||
|
||||
RemComp<ActiveConveyorComponent>(uid);
|
||||
|
||||
if (!TryComp<PhysicsComponent>(uid, out var physics))
|
||||
return;
|
||||
|
||||
|
||||
254
Content.Server/Players/RateLimiting/PlayerRateLimitManager.cs
Normal file
254
Content.Server/Players/RateLimiting/PlayerRateLimitManager.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Shared.Database;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Players.RateLimiting;
|
||||
|
||||
/// <summary>
|
||||
/// General-purpose system to rate limit actions taken by clients, such as chat messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Different categories of rate limits must be registered ahead of time by calling <see cref="Register"/>.
|
||||
/// Once registered, you can simply call <see cref="CountAction"/> to count a rate-limited action for a player.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This system is intended for rate limiting player actions over short periods,
|
||||
/// to ward against spam that can cause technical issues such as admin client load.
|
||||
/// It should not be used for in-game actions or similar.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Rate limits are reset when a client reconnects.
|
||||
/// This should not be an issue for the reasonably short rate limit periods this system is intended for.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="RateLimitRegistration"/>
|
||||
public sealed class PlayerRateLimitManager
|
||||
{
|
||||
[Dependency] private readonly IAdminLogManager _adminLog = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
private readonly Dictionary<string, RegistrationData> _registrations = new();
|
||||
private readonly Dictionary<ICommonSession, Dictionary<string, RateLimitDatum>> _rateLimitData = new();
|
||||
|
||||
/// <summary>
|
||||
/// Count and validate an action performed by a player against rate limits.
|
||||
/// </summary>
|
||||
/// <param name="player">The player performing the action.</param>
|
||||
/// <param name="key">The key string that was previously used to register a rate limit category.</param>
|
||||
/// <returns>Whether the action counted should be blocked due to surpassing rate limits or not.</returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="player"/> is not a connected player
|
||||
/// OR <paramref name="key"/> is not a registered rate limit category.
|
||||
/// </exception>
|
||||
/// <seealso cref="Register"/>
|
||||
public RateLimitStatus CountAction(ICommonSession player, string key)
|
||||
{
|
||||
if (player.Status == SessionStatus.Disconnected)
|
||||
throw new ArgumentException("Player is not connected");
|
||||
if (!_registrations.TryGetValue(key, out var registration))
|
||||
throw new ArgumentException($"Unregistered key: {key}");
|
||||
|
||||
var playerData = _rateLimitData.GetOrNew(player);
|
||||
ref var datum = ref CollectionsMarshal.GetValueRefOrAddDefault(playerData, key, out _);
|
||||
var time = _gameTiming.RealTime;
|
||||
if (datum.CountExpires < time)
|
||||
{
|
||||
// Period expired, reset it.
|
||||
datum.CountExpires = time + registration.LimitPeriod;
|
||||
datum.Count = 0;
|
||||
datum.Announced = false;
|
||||
}
|
||||
|
||||
datum.Count += 1;
|
||||
|
||||
if (datum.Count <= registration.LimitCount)
|
||||
return RateLimitStatus.Allowed;
|
||||
|
||||
// Breached rate limits, inform admins if configured.
|
||||
if (registration.AdminAnnounceDelay is { } cvarAnnounceDelay)
|
||||
{
|
||||
if (datum.NextAdminAnnounce < time)
|
||||
{
|
||||
registration.Registration.AdminAnnounceAction!(player);
|
||||
datum.NextAdminAnnounce = time + cvarAnnounceDelay;
|
||||
}
|
||||
}
|
||||
|
||||
if (!datum.Announced)
|
||||
{
|
||||
registration.Registration.PlayerLimitedAction(player);
|
||||
_adminLog.Add(
|
||||
registration.Registration.AdminLogType,
|
||||
LogImpact.Medium,
|
||||
$"Player {player} breached '{key}' rate limit ");
|
||||
|
||||
datum.Announced = true;
|
||||
}
|
||||
|
||||
return RateLimitStatus.Blocked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a new rate limit category.
|
||||
/// </summary>
|
||||
/// <param name="key">
|
||||
/// The key string that will be referred to later with <see cref="CountAction"/>.
|
||||
/// Must be unique and should probably just be a constant somewhere.
|
||||
/// </param>
|
||||
/// <param name="registration">The data specifying the rate limit's parameters.</param>
|
||||
/// <exception cref="InvalidOperationException"><paramref name="key"/> has already been registered.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="registration"/> is invalid.</exception>
|
||||
public void Register(string key, RateLimitRegistration registration)
|
||||
{
|
||||
if (_registrations.ContainsKey(key))
|
||||
throw new InvalidOperationException($"Key already registered: {key}");
|
||||
|
||||
var data = new RegistrationData
|
||||
{
|
||||
Registration = registration,
|
||||
};
|
||||
|
||||
if ((registration.AdminAnnounceAction == null) != (registration.CVarAdminAnnounceDelay == null))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Must set either both {nameof(registration.AdminAnnounceAction)} and {nameof(registration.CVarAdminAnnounceDelay)} or neither");
|
||||
}
|
||||
|
||||
_cfg.OnValueChanged(
|
||||
registration.CVarLimitCount,
|
||||
i => data.LimitCount = i,
|
||||
invokeImmediately: true);
|
||||
_cfg.OnValueChanged(
|
||||
registration.CVarLimitPeriodLength,
|
||||
i => data.LimitPeriod = TimeSpan.FromSeconds(i),
|
||||
invokeImmediately: true);
|
||||
|
||||
if (registration.CVarAdminAnnounceDelay != null)
|
||||
{
|
||||
_cfg.OnValueChanged(
|
||||
registration.CVarLimitCount,
|
||||
i => data.AdminAnnounceDelay = TimeSpan.FromSeconds(i),
|
||||
invokeImmediately: true);
|
||||
}
|
||||
|
||||
_registrations.Add(key, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the manager's functionality at game startup.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
|
||||
}
|
||||
|
||||
private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
if (e.NewStatus == SessionStatus.Disconnected)
|
||||
_rateLimitData.Remove(e.Session);
|
||||
}
|
||||
|
||||
private sealed class RegistrationData
|
||||
{
|
||||
public required RateLimitRegistration Registration { get; init; }
|
||||
public TimeSpan LimitPeriod { get; set; }
|
||||
public int LimitCount { get; set; }
|
||||
public TimeSpan? AdminAnnounceDelay { get; set; }
|
||||
}
|
||||
|
||||
private struct RateLimitDatum
|
||||
{
|
||||
/// <summary>
|
||||
/// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) this rate limit period will expire at.
|
||||
/// </summary>
|
||||
public TimeSpan CountExpires;
|
||||
|
||||
/// <summary>
|
||||
/// How many actions have been done in the current rate limit period.
|
||||
/// </summary>
|
||||
public int Count;
|
||||
|
||||
/// <summary>
|
||||
/// Have we announced to the player that they've been blocked in this rate limit period?
|
||||
/// </summary>
|
||||
public bool Announced;
|
||||
|
||||
/// <summary>
|
||||
/// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) of the
|
||||
/// next time we can send an announcement to admins about rate limit breach.
|
||||
/// </summary>
|
||||
public TimeSpan NextAdminAnnounce;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains all data necessary to register a rate limit with <see cref="PlayerRateLimitManager.Register"/>.
|
||||
/// </summary>
|
||||
public sealed class RateLimitRegistration
|
||||
{
|
||||
/// <summary>
|
||||
/// CVar that controls the period over which the rate limit is counted, measured in seconds.
|
||||
/// </summary>
|
||||
public required CVarDef<int> CVarLimitPeriodLength { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVar that controls how many actions are allowed in a single rate limit period.
|
||||
/// </summary>
|
||||
public required CVarDef<int> CVarLimitCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// An action that gets invoked when this rate limit has been breached by a player.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This can be used for informing players or taking administrative action.
|
||||
/// </remarks>
|
||||
public required Action<ICommonSession> PlayerLimitedAction { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVar that controls the minimum delay between admin notifications, measured in seconds.
|
||||
/// This can be omitted to have no admin notification system.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If set, <see cref="AdminAnnounceAction"/> must be set too.
|
||||
/// </remarks>
|
||||
public CVarDef<int>? CVarAdminAnnounceDelay { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// An action that gets invoked when a rate limit was breached and admins should be notified.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If set, <see cref="CVarAdminAnnounceDelay"/> must be set too.
|
||||
/// </remarks>
|
||||
public Action<ICommonSession>? AdminAnnounceAction { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Log type used to log rate limit violations to the admin logs system.
|
||||
/// </summary>
|
||||
public LogType AdminLogType { get; init; } = LogType.RateLimited;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a rate-limited operation.
|
||||
/// </summary>
|
||||
/// <seealso cref="PlayerRateLimitManager.CountAction"/>
|
||||
public enum RateLimitStatus : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// The action was not blocked by the rate limit.
|
||||
/// </summary>
|
||||
Allowed,
|
||||
|
||||
/// <summary>
|
||||
/// The action was blocked by the rate limit.
|
||||
/// </summary>
|
||||
Blocked,
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Content.Server.Radio.EntitySystems;
|
||||
|
||||
namespace Content.Server.Radio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Prevents all radio in range from sending messages
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(JammerSystem))]
|
||||
public sealed partial class ActiveRadioJammerComponent : Component
|
||||
{
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Server.Radio.Components;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.PowerCell.Components;
|
||||
using Content.Shared.RadioJammer;
|
||||
using Content.Shared.Radio.EntitySystems;
|
||||
using Content.Shared.Radio.Components;
|
||||
using Content.Shared.DeviceNetwork.Systems;
|
||||
|
||||
namespace Content.Server.Radio.EntitySystems;
|
||||
|
||||
@@ -17,6 +15,7 @@ public sealed class JammerSystem : SharedJammerSystem
|
||||
[Dependency] private readonly PowerCellSystem _powerCell = default!;
|
||||
[Dependency] private readonly BatterySystem _battery = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -24,7 +23,6 @@ public sealed class JammerSystem : SharedJammerSystem
|
||||
|
||||
SubscribeLocalEvent<RadioJammerComponent, ActivateInWorldEvent>(OnActivate);
|
||||
SubscribeLocalEvent<ActiveRadioJammerComponent, PowerCellChangedEvent>(OnPowerCellChanged);
|
||||
SubscribeLocalEvent<RadioJammerComponent, ExaminedEvent>(OnExamine);
|
||||
SubscribeLocalEvent<RadioSendAttemptEvent>(OnRadioSendAttempt);
|
||||
}
|
||||
|
||||
@@ -37,27 +35,22 @@ public sealed class JammerSystem : SharedJammerSystem
|
||||
|
||||
if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery))
|
||||
{
|
||||
if (!_battery.TryUseCharge(batteryUid.Value, GetCurrentWattage(jam) * frameTime, battery))
|
||||
if (!_battery.TryUseCharge(batteryUid.Value, GetCurrentWattage((uid, jam)) * frameTime, battery))
|
||||
{
|
||||
ChangeLEDState(false, uid);
|
||||
ChangeLEDState(uid, false);
|
||||
RemComp<ActiveRadioJammerComponent>(uid);
|
||||
RemComp<DeviceNetworkJammerComponent>(uid);
|
||||
}
|
||||
else
|
||||
{
|
||||
var percentCharged = battery.CurrentCharge / battery.MaxCharge;
|
||||
if (percentCharged > .50)
|
||||
var chargeLevel = percentCharged switch
|
||||
{
|
||||
ChangeChargeLevel(RadioJammerChargeLevel.High, uid);
|
||||
}
|
||||
else if (percentCharged < .15)
|
||||
{
|
||||
ChangeChargeLevel(RadioJammerChargeLevel.Low, uid);
|
||||
}
|
||||
else
|
||||
{
|
||||
ChangeChargeLevel(RadioJammerChargeLevel.Medium, uid);
|
||||
}
|
||||
> 0.50f => RadioJammerChargeLevel.High,
|
||||
< 0.15f => RadioJammerChargeLevel.Low,
|
||||
_ => RadioJammerChargeLevel.Medium,
|
||||
};
|
||||
ChangeChargeLevel(uid, chargeLevel);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -65,28 +58,27 @@ public sealed class JammerSystem : SharedJammerSystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnActivate(EntityUid uid, RadioJammerComponent comp, ActivateInWorldEvent args)
|
||||
private void OnActivate(Entity<RadioJammerComponent> ent, ref ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || !args.Complex)
|
||||
return;
|
||||
|
||||
var activated = !HasComp<ActiveRadioJammerComponent>(uid) &&
|
||||
_powerCell.TryGetBatteryFromSlot(uid, out var battery) &&
|
||||
battery.CurrentCharge > GetCurrentWattage(comp);
|
||||
var activated = !HasComp<ActiveRadioJammerComponent>(ent) &&
|
||||
_powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery) &&
|
||||
battery.CurrentCharge > GetCurrentWattage(ent);
|
||||
if (activated)
|
||||
{
|
||||
ChangeLEDState(true, uid);
|
||||
EnsureComp<ActiveRadioJammerComponent>(uid);
|
||||
EnsureComp<DeviceNetworkJammerComponent>(uid, out var jammingComp);
|
||||
jammingComp.Range = GetCurrentRange(comp);
|
||||
jammingComp.JammableNetworks.Add(DeviceNetworkComponent.DeviceNetIdDefaults.Wireless.ToString());
|
||||
Dirty(uid, jammingComp);
|
||||
ChangeLEDState(ent.Owner, true);
|
||||
EnsureComp<ActiveRadioJammerComponent>(ent);
|
||||
EnsureComp<DeviceNetworkJammerComponent>(ent, out var jammingComp);
|
||||
_jammer.SetRange((ent, jammingComp), GetCurrentRange(ent));
|
||||
_jammer.AddJammableNetwork((ent, jammingComp), DeviceNetworkComponent.DeviceNetIdDefaults.Wireless.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
ChangeLEDState(false, uid);
|
||||
RemCompDeferred<ActiveRadioJammerComponent>(uid);
|
||||
RemCompDeferred<DeviceNetworkJammerComponent>(uid);
|
||||
ChangeLEDState(ent.Owner, false);
|
||||
RemCompDeferred<ActiveRadioJammerComponent>(ent);
|
||||
RemCompDeferred<DeviceNetworkJammerComponent>(ent);
|
||||
}
|
||||
var state = Loc.GetString(activated ? "radio-jammer-component-on-state" : "radio-jammer-component-off-state");
|
||||
var message = Loc.GetString("radio-jammer-component-on-use", ("state", state));
|
||||
@@ -94,27 +86,12 @@ public sealed class JammerSystem : SharedJammerSystem
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnPowerCellChanged(EntityUid uid, ActiveRadioJammerComponent comp, PowerCellChangedEvent args)
|
||||
private void OnPowerCellChanged(Entity<ActiveRadioJammerComponent> ent, ref PowerCellChangedEvent args)
|
||||
{
|
||||
if (args.Ejected)
|
||||
{
|
||||
ChangeLEDState(false, uid);
|
||||
RemCompDeferred<ActiveRadioJammerComponent>(uid);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnExamine(EntityUid uid, RadioJammerComponent comp, ExaminedEvent args)
|
||||
{
|
||||
if (args.IsInDetailsRange)
|
||||
{
|
||||
var powerIndicator = HasComp<ActiveRadioJammerComponent>(uid)
|
||||
? Loc.GetString("radio-jammer-component-examine-on-state")
|
||||
: Loc.GetString("radio-jammer-component-examine-off-state");
|
||||
args.PushMarkup(powerIndicator);
|
||||
|
||||
var powerLevel = Loc.GetString(comp.Settings[comp.SelectedPowerLevel].Name);
|
||||
var switchIndicator = Loc.GetString("radio-jammer-component-switch-setting", ("powerLevel", powerLevel));
|
||||
args.PushMarkup(switchIndicator);
|
||||
ChangeLEDState(ent.Owner, false);
|
||||
RemCompDeferred<ActiveRadioJammerComponent>(ent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,9 +108,9 @@ public sealed class JammerSystem : SharedJammerSystem
|
||||
var source = Transform(sourceUid).Coordinates;
|
||||
var query = EntityQueryEnumerator<ActiveRadioJammerComponent, RadioJammerComponent, TransformComponent>();
|
||||
|
||||
while (query.MoveNext(out _, out _, out var jam, out var transform))
|
||||
while (query.MoveNext(out var uid, out _, out var jam, out var transform))
|
||||
{
|
||||
if (source.InRange(EntityManager, _transform, transform.Coordinates, GetCurrentRange(jam)))
|
||||
if (_transform.InRange(source, transform.Coordinates, GetCurrentRange((uid, jam))))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -131,25 +131,6 @@ public sealed partial class ResearchSystem
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a lathe recipe to the specified technology database
|
||||
/// without checking if it can be unlocked.
|
||||
/// </summary>
|
||||
public void AddLatheRecipe(EntityUid uid, string recipe, TechnologyDatabaseComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
if (component.UnlockedRecipes.Contains(recipe))
|
||||
return;
|
||||
|
||||
component.UnlockedRecipes.Add(recipe);
|
||||
Dirty(uid, component);
|
||||
|
||||
var ev = new TechnologyDatabaseModifiedEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether a technology can be unlocked on this database,
|
||||
/// taking parent technologies into account.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
using Content.Shared.Random;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Research.TechnologyDisk.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class TechnologyDiskComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The recipe that will be added. If null, one will be randomly generated
|
||||
/// </summary>
|
||||
[DataField("recipes")]
|
||||
public List<string>? Recipes;
|
||||
|
||||
/// <summary>
|
||||
/// A weighted random prototype for how rare each tier should be.
|
||||
/// </summary>
|
||||
[DataField("tierWeightPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<WeightedRandomPrototype>))]
|
||||
public string TierWeightPrototype = "TechDiskTierWeights";
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Research.Systems;
|
||||
using Content.Server.Research.TechnologyDisk.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Research.Components;
|
||||
using Content.Shared.Research.Prototypes;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Research.TechnologyDisk.Systems;
|
||||
|
||||
public sealed class TechnologyDiskSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly ResearchSystem _research = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<TechnologyDiskComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<TechnologyDiskComponent, ExaminedEvent>(OnExamine);
|
||||
SubscribeLocalEvent<TechnologyDiskComponent, MapInitEvent>(OnMapInit);
|
||||
}
|
||||
|
||||
private void OnAfterInteract(EntityUid uid, TechnologyDiskComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled || !args.CanReach || args.Target is not { } target)
|
||||
return;
|
||||
|
||||
if (!HasComp<ResearchServerComponent>(target) || !TryComp<TechnologyDatabaseComponent>(target, out var database))
|
||||
return;
|
||||
|
||||
if (component.Recipes != null)
|
||||
{
|
||||
foreach (var recipe in component.Recipes)
|
||||
{
|
||||
_research.AddLatheRecipe(target, recipe, database);
|
||||
}
|
||||
}
|
||||
_popup.PopupEntity(Loc.GetString("tech-disk-inserted"), target, args.User);
|
||||
QueueDel(uid);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnExamine(EntityUid uid, TechnologyDiskComponent component, ExaminedEvent args)
|
||||
{
|
||||
var message = Loc.GetString("tech-disk-examine-none");
|
||||
if (component.Recipes != null && component.Recipes.Any())
|
||||
{
|
||||
var prototype = _prototype.Index<LatheRecipePrototype>(component.Recipes[0]);
|
||||
var resultPrototype = _prototype.Index<EntityPrototype>(prototype.Result);
|
||||
message = Loc.GetString("tech-disk-examine", ("result", resultPrototype.Name));
|
||||
|
||||
if (component.Recipes.Count > 1) //idk how to do this well. sue me.
|
||||
message += " " + Loc.GetString("tech-disk-examine-more");
|
||||
}
|
||||
args.PushMarkup(message);
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, TechnologyDiskComponent component, MapInitEvent args)
|
||||
{
|
||||
if (component.Recipes != null)
|
||||
return;
|
||||
|
||||
var weightedRandom = _prototype.Index<WeightedRandomPrototype>(component.TierWeightPrototype);
|
||||
var tier = int.Parse(weightedRandom.Pick(_random));
|
||||
|
||||
//get a list of every distinct recipe in all the technologies.
|
||||
var techs = new List<ProtoId<LatheRecipePrototype>>();
|
||||
foreach (var tech in _prototype.EnumeratePrototypes<TechnologyPrototype>())
|
||||
{
|
||||
if (tech.Tier != tier)
|
||||
continue;
|
||||
|
||||
techs.AddRange(tech.RecipeUnlocks);
|
||||
}
|
||||
techs = techs.Distinct().ToList();
|
||||
|
||||
if (!techs.Any())
|
||||
return;
|
||||
|
||||
//pick one
|
||||
component.Recipes = new();
|
||||
component.Recipes.Add(_random.Pick(techs));
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,8 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
|
||||
/// </summary>
|
||||
private void HandleGeneratorCollide(Entity<ContainmentFieldGeneratorComponent> generator, ref StartCollideEvent args)
|
||||
{
|
||||
if (_tags.HasTag(args.OtherEntity, generator.Comp.IDTag))
|
||||
if (args.OtherFixtureId == generator.Comp.SourceFixtureId &&
|
||||
_tags.HasTag(args.OtherEntity, generator.Comp.IDTag))
|
||||
{
|
||||
ReceivePower(generator.Comp.PowerReceived, generator);
|
||||
generator.Comp.Accumulator = 0f;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Applies any accent components on this item to the name of the wearer while worn.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(AccentWearerNameClothingSystem))]
|
||||
public sealed partial class AccentWearerNameClothingComponent : Component;
|
||||
@@ -0,0 +1,39 @@
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
/// <inheritdoc cref="AccentWearerNameClothingComponent"/>
|
||||
public sealed class AccentWearerNameClothingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly NameModifierSystem _nameMod = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AccentWearerNameClothingComponent, ClothingGotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<AccentWearerNameClothingComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
|
||||
SubscribeLocalEvent<AccentWearerNameClothingComponent, InventoryRelayedEvent<RefreshNameModifiersEvent>>(OnRefreshNameModifiers);
|
||||
}
|
||||
|
||||
private void OnGotEquipped(Entity<AccentWearerNameClothingComponent> ent, ref ClothingGotEquippedEvent args)
|
||||
{
|
||||
_nameMod.RefreshNameModifiers(args.Wearer);
|
||||
}
|
||||
|
||||
private void OnGotUnequipped(Entity<AccentWearerNameClothingComponent> ent, ref ClothingGotUnequippedEvent args)
|
||||
{
|
||||
_nameMod.RefreshNameModifiers(args.Wearer);
|
||||
}
|
||||
|
||||
private void OnRefreshNameModifiers(Entity<AccentWearerNameClothingComponent> ent, ref InventoryRelayedEvent<RefreshNameModifiersEvent> args)
|
||||
{
|
||||
var ev = new AccentGetEvent(ent, args.Args.BaseName);
|
||||
RaiseLocalEvent(ent, ev);
|
||||
// Use a negative priority since we're going to bulldoze any earlier changes
|
||||
args.Args.AddModifier("comp-accent-wearer-name-clothing-format", -1, ("accentedName", ev.Message));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using Content.Server.Station.Systems;
|
||||
|
||||
namespace Content.Server.Station.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Stores station parameters that can be randomized by the roundstart
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(StationSystem))]
|
||||
public sealed partial class StationRandomTransformComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public float? MaxStationOffset = 100.0f;
|
||||
|
||||
[DataField]
|
||||
public bool EnableStationRotation = true;
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Events;
|
||||
using Content.Shared.Fax;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Station;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
@@ -28,10 +27,12 @@ namespace Content.Server.Station.Systems;
|
||||
[PublicAPI]
|
||||
public sealed class StationSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
||||
[Dependency] private readonly GameTicker _ticker = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly MapSystem _map = default!;
|
||||
@@ -282,51 +283,11 @@ public sealed class StationSystem : EntitySystem
|
||||
var data = Comp<StationDataComponent>(station);
|
||||
name ??= MetaData(station).EntityName;
|
||||
|
||||
var entry = gridIds ?? Array.Empty<EntityUid>();
|
||||
|
||||
foreach (var grid in entry)
|
||||
foreach (var grid in gridIds ?? Array.Empty<EntityUid>())
|
||||
{
|
||||
AddGridToStation(station, grid, null, data, name);
|
||||
}
|
||||
|
||||
if (TryComp<StationRandomTransformComponent>(station, out var random))
|
||||
{
|
||||
Angle? rotation = null;
|
||||
Vector2? offset = null;
|
||||
|
||||
if (random.MaxStationOffset != null)
|
||||
offset = _random.NextVector2(-random.MaxStationOffset.Value, random.MaxStationOffset.Value);
|
||||
|
||||
if (random.EnableStationRotation)
|
||||
rotation = _random.NextAngle();
|
||||
|
||||
foreach (var grid in entry)
|
||||
{
|
||||
//planetary maps give an error when trying to change from position or rotation.
|
||||
//This is still the case, but it will be irrelevant after the https://github.com/space-wizards/space-station-14/pull/26510
|
||||
if (rotation != null && offset != null)
|
||||
{
|
||||
var pos = _transform.GetWorldPosition(grid);
|
||||
_transform.SetWorldPositionRotation(grid, pos + offset.Value, rotation.Value);
|
||||
continue;
|
||||
}
|
||||
if (rotation != null)
|
||||
{
|
||||
_transform.SetWorldRotation(grid, rotation.Value);
|
||||
continue;
|
||||
}
|
||||
if (offset != null)
|
||||
{
|
||||
var pos = _transform.GetWorldPosition(grid);
|
||||
_transform.SetWorldPosition(grid, pos + offset.Value);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (LifeStage(station) < EntityLifeStage.MapInitialized)
|
||||
throw new Exception($"Station must be man-initialized");
|
||||
|
||||
var ev = new StationPostInitEvent((station, data));
|
||||
RaiseLocalEvent(station, ref ev, true);
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
||||
private void OnSubnetRequest(EntityUid uid, SurveillanceCameraMonitorComponent component,
|
||||
SurveillanceCameraMonitorSubnetRequestMessage args)
|
||||
{
|
||||
if (args.Actor != null)
|
||||
if (args.Actor is { Valid: true } actor && !Deleted(actor))
|
||||
{
|
||||
SetActiveSubnet(uid, args.Subnet, component);
|
||||
}
|
||||
@@ -146,6 +146,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
||||
break;
|
||||
case SurveillanceCameraSystem.CameraSubnetData:
|
||||
if (args.Data.TryGetValue(SurveillanceCameraSystem.CameraSubnetData, out string? subnet)
|
||||
&& !string.IsNullOrEmpty(subnet)
|
||||
&& !component.KnownSubnets.ContainsKey(subnet))
|
||||
{
|
||||
component.KnownSubnets.Add(subnet, args.SenderAddress);
|
||||
@@ -217,6 +218,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
||||
{
|
||||
if (!Resolve(uid, ref monitor)
|
||||
|| monitor.LastHeartbeatSent < _heartbeatDelay
|
||||
|| string.IsNullOrEmpty(monitor.ActiveSubnet)
|
||||
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var subnetAddress))
|
||||
{
|
||||
return;
|
||||
@@ -278,6 +280,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
||||
SurveillanceCameraMonitorComponent? monitor = null)
|
||||
{
|
||||
if (!Resolve(uid, ref monitor)
|
||||
|| string.IsNullOrEmpty(subnet)
|
||||
|| !monitor.KnownSubnets.ContainsKey(subnet))
|
||||
{
|
||||
return;
|
||||
@@ -295,6 +298,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
||||
private void RequestActiveSubnetInfo(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
|
||||
{
|
||||
if (!Resolve(uid, ref monitor)
|
||||
|| string.IsNullOrEmpty(monitor.ActiveSubnet)
|
||||
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var address))
|
||||
{
|
||||
return;
|
||||
@@ -310,6 +314,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
||||
private void ConnectToSubnet(EntityUid uid, string subnet, SurveillanceCameraMonitorComponent? monitor = null)
|
||||
{
|
||||
if (!Resolve(uid, ref monitor)
|
||||
|| string.IsNullOrEmpty(subnet)
|
||||
|| !monitor.KnownSubnets.TryGetValue(subnet, out var address))
|
||||
{
|
||||
return;
|
||||
@@ -327,6 +332,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
||||
private void DisconnectFromSubnet(EntityUid uid, string subnet, SurveillanceCameraMonitorComponent? monitor = null)
|
||||
{
|
||||
if (!Resolve(uid, ref monitor)
|
||||
|| string.IsNullOrEmpty(subnet)
|
||||
|| !monitor.KnownSubnets.TryGetValue(subnet, out var address))
|
||||
{
|
||||
return;
|
||||
@@ -415,6 +421,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
||||
SurveillanceCameraMonitorComponent? monitor = null)
|
||||
{
|
||||
if (!Resolve(uid, ref monitor)
|
||||
|| string.IsNullOrEmpty(monitor.ActiveSubnet)
|
||||
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var subnetAddress))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -23,4 +23,10 @@ public sealed partial class ThiefUndeterminedBackpackComponent : Component
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier ApproveSound = new SoundPathSpecifier("/Audio/Effects/rustle1.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Max number of sets you can select.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MaxSelectedSets = 2;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
private const int MaxSelectedSets = 2;
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -35,7 +34,7 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
|
||||
|
||||
private void OnApprove(Entity<ThiefUndeterminedBackpackComponent> backpack, ref ThiefBackpackApproveMessage args)
|
||||
{
|
||||
if (backpack.Comp.SelectedSets.Count != MaxSelectedSets)
|
||||
if (backpack.Comp.SelectedSets.Count != backpack.Comp.MaxSelectedSets)
|
||||
return;
|
||||
|
||||
foreach (var i in backpack.Comp.SelectedSets)
|
||||
@@ -79,6 +78,6 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
|
||||
data.Add(i, info);
|
||||
}
|
||||
|
||||
_ui.SetUiState(uid, ThiefBackpackUIKey.Key, new ThiefBackpackBoundUserInterfaceState(data, MaxSelectedSets));
|
||||
_ui.SetUiState(uid, ThiefBackpackUIKey.Key, new ThiefBackpackBoundUserInterfaceState(data, component.MaxSelectedSets));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Weather;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.Server.Weather;
|
||||
|
||||
public sealed class WeatherSystem : SharedWeatherSystem
|
||||
{
|
||||
[Dependency] private readonly IConsoleHost _console = default!;
|
||||
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -30,7 +29,7 @@ public sealed class WeatherSystem : SharedWeatherSystem
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Fun)]
|
||||
private void WeatherTwo(IConsoleShell shell, string argstr, string[] args)
|
||||
private void WeatherTwo(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length < 2)
|
||||
{
|
||||
@@ -60,7 +59,8 @@ public sealed class WeatherSystem : SharedWeatherSystem
|
||||
var maxTime = TimeSpan.MaxValue;
|
||||
|
||||
// If it's already running then just fade out with how much time we're into the weather.
|
||||
if (TryComp<WeatherComponent>(MapManager.GetMapEntityId(mapId), out var weatherComp) &&
|
||||
if (_mapSystem.TryGetMap(mapId, out var mapUid) &&
|
||||
TryComp<WeatherComponent>(mapUid, out var weatherComp) &&
|
||||
weatherComp.Weather.TryGetValue(args[1], out var existing))
|
||||
{
|
||||
maxTime = curTime - existing.StartTime;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Content.Server.Zombies;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class InitialInfectedExemptComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user