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:
Ed
2024-06-21 18:30:31 +03:00
443 changed files with 8337 additions and 2540 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
namespace Content.Server.Bed.Components
{
// TODO rename this component
[RegisterComponent]
public sealed partial class HealOnBuckleHealingComponent : Component
{}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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