Merge pull request #595 from crystallpunk-14/ed-20-11-2024-upstream-sync

Upstream sync
This commit is contained in:
Ed
2024-11-23 13:44:10 +03:00
committed by GitHub
661 changed files with 33165 additions and 12823 deletions

View File

@@ -146,8 +146,8 @@ namespace Content.Server.Abilities.Mime
mimePowers.ReadyToRepent = false;
mimePowers.VowBroken = false;
AddComp<MutedComponent>(uid);
_alertsSystem.ClearAlert(uid, mimePowers.VowAlert);
_alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert);
_alertsSystem.ClearAlert(uid, mimePowers.VowBrokenAlert);
_alertsSystem.ShowAlert(uid, mimePowers.VowAlert);
_actionsSystem.AddAction(uid, ref mimePowers.InvisibleWallActionEntity, mimePowers.InvisibleWallAction, uid);
}
}

View File

@@ -54,7 +54,7 @@ public sealed class BanListEui : BaseEui
private async Task LoadBans(NetUserId userId)
{
foreach (var ban in await _db.GetServerBansAsync(null, userId, null))
foreach (var ban in await _db.GetServerBansAsync(null, userId, null, null))
{
SharedServerUnban? unban = null;
if (ban.Unban is { } unbanDef)
@@ -74,7 +74,7 @@ public sealed class BanListEui : BaseEui
? (address.address.ToString(), address.cidrMask)
: null;
hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan());
hwid = ban.HWId?.ToString();
}
Bans.Add(new SharedServerBan(
@@ -95,7 +95,7 @@ public sealed class BanListEui : BaseEui
private async Task LoadRoleBans(NetUserId userId)
{
foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null))
foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null, null))
{
SharedServerUnban? unban = null;
if (ban.Unban is { } unbanDef)
@@ -115,7 +115,7 @@ public sealed class BanListEui : BaseEui
? (address.address.ToString(), address.cidrMask)
: null;
hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan());
hwid = ban.HWId?.ToString();
}
RoleBans.Add(new SharedServerRoleBan(
ban.Id,

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using System.Net.Sockets;
using Content.Server.Administration.Managers;
@@ -8,7 +7,6 @@ using Content.Server.EUI;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Eui;
using Robust.Server.Player;
using Robust.Shared.Network;
namespace Content.Server.Administration;
@@ -27,7 +25,7 @@ public sealed class BanPanelEui : BaseEui
private NetUserId? PlayerId { get; set; }
private string PlayerName { get; set; } = string.Empty;
private IPAddress? LastAddress { get; set; }
private ImmutableArray<byte>? LastHwid { get; set; }
private ImmutableTypedHwid? LastHwid { get; set; }
private const int Ipv4_CIDR = 32;
private const int Ipv6_CIDR = 64;
@@ -51,7 +49,7 @@ public sealed class BanPanelEui : BaseEui
switch (msg)
{
case BanPanelEuiStateMsg.CreateBanRequest r:
BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid?.ToImmutableArray(), r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase);
BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid, r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase);
break;
case BanPanelEuiStateMsg.GetPlayerInfoRequest r:
ChangePlayer(r.PlayerUsername);
@@ -59,7 +57,7 @@ public sealed class BanPanelEui : BaseEui
}
}
private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableArray<byte>? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase)
private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase)
{
if (!_admins.HasAdminFlag(Player, AdminFlags.Ban))
{
@@ -155,7 +153,7 @@ public sealed class BanPanelEui : BaseEui
ChangePlayer(located?.UserId, located?.Username ?? string.Empty, located?.LastAddress, located?.LastHWId);
}
public void ChangePlayer(NetUserId? playerId, string playerName, IPAddress? lastAddress, ImmutableArray<byte>? lastHwid)
public void ChangePlayer(NetUserId? playerId, string playerName, IPAddress? lastAddress, ImmutableTypedHwid? lastHwid)
{
PlayerId = playerId;
PlayerName = playerName;

View File

@@ -38,7 +38,7 @@ public sealed class BanListCommand : LocalizedCommands
if (shell.Player is not { } player)
{
var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastHWId, false);
var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false);
if (bans.Count == 0)
{

View File

@@ -48,7 +48,7 @@ public sealed class RoleBanListCommand : IConsoleCommand
if (shell.Player is not { } player)
{
var bans = await _dbManager.GetServerRoleBansAsync(data.LastAddress, data.UserId, data.LastHWId, includeUnbanned);
var bans = await _dbManager.GetServerRoleBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, includeUnbanned);
if (bans.Count == 0)
{

View File

@@ -65,7 +65,8 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
var netChannel = player.Channel;
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, false);
var modernHwids = netChannel.UserData.ModernHWIds;
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, modernHwids, false);
var userRoleBans = new List<ServerRoleBanDef>();
foreach (var ban in roleBans)
@@ -132,7 +133,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
}
#region Server Bans
public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason)
public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason)
{
DateTimeOffset? expires = null;
if (minutes > 0)
@@ -166,9 +167,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
var addressRangeString = addressRange != null
? $"{addressRange.Value.Item1}/{addressRange.Value.Item2}"
: "null";
var hwidString = hwid != null
? string.Concat(hwid.Value.Select(x => x.ToString("x2")))
: "null";
var hwidString = hwid?.ToString() ?? "null";
var expiresString = expires == null ? Loc.GetString("server-ban-string-never") : $"{expires}";
var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii";
@@ -208,6 +207,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
UserId = player.UserId,
Address = player.Channel.RemoteEndPoint.Address,
HWId = player.Channel.UserData.HWId,
ModernHWIds = player.Channel.UserData.ModernHWIds,
// It's possible for the player to not have cached data loading yet due to coincidental timing.
// If this is the case, we assume they have all flags to avoid false-positives.
ExemptFlags = _cachedBanExemptions.GetValueOrDefault(player, ServerBanExemptFlags.All),
@@ -228,7 +228,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
#region Job Bans
// If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin.
// Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset.
public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan)
public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan)
{
if (!_prototypeManager.TryIndex(role, out JobPrototype? _))
{

View File

@@ -24,7 +24,7 @@ public interface IBanManager
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
/// <param name="severity">Severity of the resulting ban note</param>
/// <param name="reason">Reason for the ban</param>
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason);
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason);
public HashSet<string>? GetRoleBans(NetUserId playerUserId);
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId);
@@ -37,7 +37,7 @@ public interface IBanManager
/// <param name="reason">Reason for the ban</param>
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
/// <param name="timeOfBan">Time when the ban was applied, used for grouping role bans</param>
public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan);
public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan);
/// <summary>
/// Pardons a role ban for the specified target, username or GUID

View File

@@ -5,16 +5,42 @@ using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Connection;
using Content.Server.Database;
using Content.Shared.Database;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.Server.Administration
{
public sealed record LocatedPlayerData(NetUserId UserId, IPAddress? LastAddress, ImmutableArray<byte>? LastHWId, string Username);
/// <summary>
/// Contains data resolved via <see cref="IPlayerLocator"/>.
/// </summary>
/// <param name="UserId">The ID of the located user.</param>
/// <param name="LastAddress">The last known IP address that the user connected with.</param>
/// <param name="LastHWId">
/// The last known HWID that the user connected with.
/// This should be used for placing new records involving HWIDs, such as bans.
/// For looking up data based on HWID, use combined <see cref="LastLegacyHWId"/> and <see cref="LastModernHWIds"/>.
/// </param>
/// <param name="Username">The last known username for the user connected with.</param>
/// <param name="LastLegacyHWId">
/// The last known legacy HWID value this user connected with. Only use for old lookups!
/// </param>
/// <param name="LastModernHWIds">
/// The set of last known modern HWIDs the user connected with.
/// </param>
public sealed record LocatedPlayerData(
NetUserId UserId,
IPAddress? LastAddress,
ImmutableTypedHwid? LastHWId,
string Username,
ImmutableArray<byte>? LastLegacyHWId,
ImmutableArray<ImmutableArray<byte>> LastModernHWIds);
/// <summary>
/// Utilities for finding user IDs that extend to more than the server database.
@@ -67,63 +93,42 @@ namespace Content.Server.Administration
{
// Check people currently on the server, the easiest case.
if (_playerManager.TryGetSessionByUsername(playerName, out var session))
{
var userId = session.UserId;
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.HWId;
return new LocatedPlayerData(userId, address, hwId, session.Name);
}
return ReturnForSession(session);
// Check database for past players.
var record = await _db.GetPlayerRecordByUserName(playerName, cancel);
if (record != null)
return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId, record.LastSeenUserName);
return ReturnForPlayerRecord(record);
// If all else fails, ask the auth server.
var authServer = _configurationManager.GetCVar(CVars.AuthServer);
var requestUri = $"{authServer}api/query/name?name={WebUtility.UrlEncode(playerName)}";
using var resp = await _httpClient.GetAsync(requestUri, cancel);
if (resp.StatusCode == HttpStatusCode.NotFound)
return null;
if (!resp.IsSuccessStatusCode)
{
_sawmill.Error("Auth server returned bad response {StatusCode}!", resp.StatusCode);
return null;
}
var responseData = await resp.Content.ReadFromJsonAsync<UserDataResponse>(cancellationToken: cancel);
if (responseData == null)
{
_sawmill.Error("Auth server returned null response!");
return null;
}
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName);
return await HandleAuthServerResponse(resp, cancel);
}
public async Task<LocatedPlayerData?> LookupIdAsync(NetUserId userId, CancellationToken cancel = default)
{
// Check people currently on the server, the easiest case.
if (_playerManager.TryGetSessionById(userId, out var session))
{
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.HWId;
return new LocatedPlayerData(userId, address, hwId, session.Name);
}
return ReturnForSession(session);
// Check database for past players.
var record = await _db.GetPlayerRecordByUserId(userId, cancel);
if (record != null)
return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId, record.LastSeenUserName);
return ReturnForPlayerRecord(record);
// If all else fails, ask the auth server.
var authServer = _configurationManager.GetCVar(CVars.AuthServer);
var requestUri = $"{authServer}api/query/userid?userid={WebUtility.UrlEncode(userId.UserId.ToString())}";
using var resp = await _httpClient.GetAsync(requestUri, cancel);
return await HandleAuthServerResponse(resp, cancel);
}
private async Task<LocatedPlayerData?> HandleAuthServerResponse(HttpResponseMessage resp, CancellationToken cancel)
{
if (resp.StatusCode == HttpStatusCode.NotFound)
return null;
@@ -134,14 +139,40 @@ namespace Content.Server.Administration
}
var responseData = await resp.Content.ReadFromJsonAsync<UserDataResponse>(cancellationToken: cancel);
if (responseData == null)
{
_sawmill.Error("Auth server returned null response!");
return null;
}
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName);
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName, null, []);
}
private static LocatedPlayerData ReturnForSession(ICommonSession session)
{
var userId = session.UserId;
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.GetModernHwid();
return new LocatedPlayerData(
userId,
address,
hwId,
session.Name,
session.Channel.UserData.HWId,
session.Channel.UserData.ModernHWIds);
}
private static LocatedPlayerData ReturnForPlayerRecord(PlayerRecord record)
{
var hwid = record.HWId;
return new LocatedPlayerData(
record.UserId,
record.LastSeenAddress,
hwid,
record.LastSeenUserName,
hwid is { Type: HwidType.Legacy } ? hwid.Hwid : null,
hwid is { Type: HwidType.Modern } ? [hwid.Hwid] : []);
}
public async Task<LocatedPlayerData?> LookupIdByNameOrIdAsync(string playerName, CancellationToken cancel = default)

View File

@@ -173,11 +173,11 @@ public sealed class PlayerPanelEui : BaseEui
{
_whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId);
// This won't get associated ip or hwid bans but they were not placed on this account anyways
_bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null)).Count;
_bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null, null)).Count;
// Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally
// The only way to distinguish whether a role ban is the same is to compare the ban time.
// This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now.
_roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null)).DistinctBy(rb => rb.BanTime).Count();
_roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null, null)).DistinctBy(rb => rb.BanTime).Count();
}
else
{

View File

@@ -22,6 +22,7 @@ using Content.Shared.Administration;
using Content.Shared.Administration.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Clumsy;
using Content.Shared.Clothing.Components;
using Content.Shared.Cluwne;
using Content.Shared.Damage;

View File

@@ -172,7 +172,7 @@ namespace Content.Server.Administration.Systems
}
// Check if the user has been banned
var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null);
var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null, null);
if (ban != null)
{
var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason));

View File

@@ -1,27 +1,27 @@
using Content.Server.Administration.Components;
using Content.Shared.Climbing.Components;
using Content.Shared.Climbing.Events;
using Content.Shared.Climbing.Systems;
using Content.Shared.Interaction.Components;
using Content.Shared.Clumsy;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Administration.Systems;
public sealed class SuperBonkSystem: EntitySystem
public sealed class SuperBonkSystem : EntitySystem
{
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly BonkSystem _bonkSystem = default!;
[Dependency] private readonly ClumsySystem _clumsySystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SuperBonkComponent, ComponentShutdown>(OnBonkShutdown);
SubscribeLocalEvent<SuperBonkComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<SuperBonkComponent, ComponentShutdown>(OnBonkShutdown);
}
public void StartSuperBonk(EntityUid target, float delay = 0.1f, bool stopWhenDead = false )
public void StartSuperBonk(EntityUid target, float delay = 0.1f, bool stopWhenDead = false)
{
//The other check in the code to stop when the target dies does not work if the target is already dead.
@@ -31,7 +31,6 @@ public sealed class SuperBonkSystem: EntitySystem
return;
}
var hadClumsy = EnsureComp<ClumsyComponent>(target, out _);
var tables = EntityQueryEnumerator<BonkableComponent>();
@@ -79,16 +78,17 @@ public sealed class SuperBonkSystem: EntitySystem
private void Bonk(SuperBonkComponent comp)
{
var uid = comp.Tables.Current.Key;
var bonkComp = comp.Tables.Current.Value;
// It would be very weird for something without a transform component to have a bonk component
// but just in case because I don't want to crash the server.
if (!HasComp<TransformComponent>(uid))
if (!HasComp<TransformComponent>(uid) || !TryComp<ClumsyComponent>(comp.Target, out var clumsyComp))
return;
_transformSystem.SetCoordinates(comp.Target, Transform(uid).Coordinates);
_bonkSystem.TryBonk(comp.Target, uid, bonkComp);
_clumsySystem.HitHeadClumsy((comp.Target, clumsyComp), uid);
_audioSystem.PlayPvs(clumsyComp.TableBonkSound, comp.Target);
}
private void OnMobStateChanged(EntityUid uid, SuperBonkComponent comp, MobStateChangedEvent args)

View File

@@ -25,6 +25,16 @@ public sealed class TagCommand : ToolshedCommand
});
}
[CommandImplementation("with")]
public IEnumerable<EntityUid> With(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> entities,
[CommandArgument] ValueRef<string, Prototype<TagPrototype>> tag)
{
_tag ??= GetSys<TagSystem>();
return entities.Where(e => _tag.HasTag(e, tag.Evaluate(ctx)!));
}
[CommandImplementation("add")]
public EntityUid Add(
[CommandInvocationContext] IInvocationContext ctx,

View File

@@ -1,7 +1,19 @@
using Content.Shared.Alert;
using Robust.Shared.GameStates;
namespace Content.Server.Alert;
internal sealed class ServerAlertsSystem : AlertsSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AlertsComponent, ComponentGetState>(OnGetState);
}
private void OnGetState(Entity<AlertsComponent> alerts, ref ComponentGetState args)
{
args.State = new AlertComponentState(alerts.Comp.Alerts);
}
}

View File

@@ -28,7 +28,8 @@ namespace Content.Server.Announcements
}
else
{
var message = string.Join(' ', new ArraySegment<string>(args, 1, args.Length-1));
// Explicit IEnumerable<string> due to overload ambiguity on .NET 9
var message = string.Join(' ', (IEnumerable<string>)new ArraySegment<string>(args, 1, args.Length-1));
chat.DispatchGlobalAnnouncement(message, args[0], colorOverride: Color.Gold);
}
shell.WriteLine("Sent!");

View File

@@ -55,6 +55,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
base.Initialize();
Log.Level = LogLevel.Debug;
SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
SubscribeLocalEvent<AntagSelectionComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
@@ -182,7 +184,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
return;
var players = _playerManager.Sessions
.Where(x => GameTicker.PlayerGameStatuses[x.UserId] == PlayerGameStatus.JoinedGame)
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) && status == PlayerGameStatus.JoinedGame)
.ToList();
ChooseAntags((uid, component), players, midround: true);
@@ -360,6 +362,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
_role.MindAddRoles(curMind.Value, def.MindRoles, null, true);
ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
SendBriefing(session, def.Briefing);
Log.Debug($"Selected {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
}
var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);

View File

@@ -48,7 +48,9 @@ public sealed partial class AtmosMonitorComponent : Component
[DataField("gasThresholds")]
public Dictionary<Gas, AtmosAlarmThreshold>? GasThresholds;
// Stores a reference to the gas on the tile this is on.
/// <summary>
/// Stores a reference to the gas on the tile this entity is on (or the pipe network it monitors; see <see cref="MonitorsPipeNet"/>).
/// </summary>
[ViewVariables]
public GasMixture? TileGas;
@@ -65,4 +67,19 @@ public sealed partial class AtmosMonitorComponent : Component
/// </summary>
[DataField("registeredDevices")]
public HashSet<string> RegisteredDevices = new();
/// <summary>
/// Specifies whether this device monitors its own internal pipe network rather than the surrounding atmosphere.
/// </summary>
/// <remarks>
/// If 'true', the entity will require a NodeContainerComponent with one or more PipeNodes to function.
/// </remarks>
[DataField]
public bool MonitorsPipeNet = false;
/// <summary>
/// Specifies the name of the pipe node that this device is monitoring.
/// </summary>
[DataField]
public string NodeNameMonitoredPipe = "monitored";
}

View File

@@ -4,6 +4,9 @@ using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.EntitySystems;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos;
@@ -25,6 +28,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
[Dependency] private readonly AtmosDeviceSystem _atmosDeviceSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainerSystem = default!;
// Commands
public const string AtmosMonitorSetThresholdCmd = "atmos_monitor_set_threshold";
@@ -56,8 +60,15 @@ public sealed class AtmosMonitorSystem : EntitySystem
private void OnAtmosDeviceEnterAtmosphere(EntityUid uid, AtmosMonitorComponent atmosMonitor, ref AtmosDeviceEnabledEvent args)
{
if (atmosMonitor.MonitorsPipeNet && _nodeContainerSystem.TryGetNode<PipeNode>(uid, atmosMonitor.NodeNameMonitoredPipe, out var pipeNode))
{
atmosMonitor.TileGas = pipeNode.Air;
return;
}
atmosMonitor.TileGas = _atmosphereSystem.GetContainingMixture(uid, true);
}
private void OnMapInit(EntityUid uid, AtmosMonitorComponent component, MapInitEvent args)
{
if (component.TemperatureThresholdId != null)
@@ -206,7 +217,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (!this.IsPowered(uid, EntityManager))
return;
if (args.Grid == null)
if (args.Grid == null)
return;
// if we're not monitoring atmos, don't bother
@@ -215,6 +226,10 @@ public sealed class AtmosMonitorSystem : EntitySystem
&& component.GasThresholds == null)
return;
// If monitoring a pipe network, get its most recent gas mixture
if (component.MonitorsPipeNet && _nodeContainerSystem.TryGetNode<PipeNode>(uid, component.NodeNameMonitoredPipe, out var pipeNode))
component.TileGas = pipeNode.Air;
UpdateState(uid, component.TileGas, component);
}

View File

@@ -1,6 +1,6 @@
using System.Globalization;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking;
using Content.Server.Ghost;
using Content.Server.Hands.Systems;
using Content.Server.Inventory;
@@ -14,6 +14,7 @@ using Content.Shared.Bed.Cryostorage;
using Content.Shared.Chat;
using Content.Shared.Climbing.Systems;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Mind.Components;
using Content.Shared.StationRecords;
@@ -26,7 +27,6 @@ using Robust.Shared.Containers;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
using System.Globalization;
namespace Content.Server.Bed.Cryostorage;

View File

@@ -38,6 +38,7 @@ public sealed class SeedExtractorSystem : EntitySystem
args.User, PopupType.Medium);
QueueDel(args.Used);
args.Handled = true;
var amount = _random.Next(seedExtractor.BaseMinSeeds, seedExtractor.BaseMaxSeeds + 1);
var coords = Transform(uid).Coordinates;

View File

@@ -424,7 +424,7 @@ public record struct PriceCalculationEvent()
[ByRefEvent]
public record struct EstimatedPriceCalculationEvent()
{
public EntityPrototype Prototype;
public required EntityPrototype Prototype;
/// <summary>
/// The total price of the entity.

View File

@@ -428,6 +428,7 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
{
var cartridgeEvent = args.MessageEvent;
cartridgeEvent.LoaderUid = GetNetEntity(uid);
cartridgeEvent.Actor = args.Actor;
RelayEvent(component, cartridgeEvent, true);
}

View File

@@ -1,6 +1,7 @@
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Hypospray.Events;
using Content.Shared.Chemistry;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
@@ -85,14 +86,44 @@ public sealed class HypospraySystem : SharedHypospraySystem
string? msgFormat = null;
if (target == user)
msgFormat = "hypospray-component-inject-self-message";
else if (EligibleEntity(user, EntityManager, component) && _interaction.TryRollClumsy(user, component.ClumsyFailChance))
// Self event
var selfEvent = new SelfBeforeHyposprayInjectsEvent(user, entity.Owner, target);
RaiseLocalEvent(user, selfEvent);
if (selfEvent.Cancelled)
{
msgFormat = "hypospray-component-inject-self-clumsy-message";
target = user;
_popup.PopupEntity(Loc.GetString(selfEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user);
return false;
}
target = selfEvent.TargetGettingInjected;
if (!EligibleEntity(target, EntityManager, component))
return false;
// Target event
var targetEvent = new TargetBeforeHyposprayInjectsEvent(user, entity.Owner, target);
RaiseLocalEvent(target, targetEvent);
if (targetEvent.Cancelled)
{
_popup.PopupEntity(Loc.GetString(targetEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user);
return false;
}
target = targetEvent.TargetGettingInjected;
if (!EligibleEntity(target, EntityManager, component))
return false;
// The target event gets priority for the overriden message.
if (targetEvent.InjectMessageOverride != null)
msgFormat = targetEvent.InjectMessageOverride;
else if (selfEvent.InjectMessageOverride != null)
msgFormat = selfEvent.InjectMessageOverride;
else if (target == user)
msgFormat = "hypospray-component-inject-self-message";
if (!_solutionContainers.TryGetSolution(uid, component.SolutionName, out var hypoSpraySoln, out var hypoSpraySolution) || hypoSpraySolution.Volume == 0)
{
_popup.PopupEntity(Loc.GetString("hypospray-component-empty-message"), target, user);

View File

@@ -15,6 +15,7 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Components;
using Content.Shared.Stacks;
using Content.Shared.Nutrition.EntitySystems;
namespace Content.Server.Chemistry.EntitySystems;
@@ -22,6 +23,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
{
[Dependency] private readonly BloodstreamSystem _blood = default!;
[Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
public override void Initialize()
{
@@ -35,13 +37,14 @@ public sealed class InjectorSystem : SharedInjectorSystem
{
RaiseLocalEvent(injector, new CP14TrySkillIssueEvent(user)); //CP14 Skill issue event
var isOpenOrIgnored = injector.Comp.IgnoreClosed || !_openable.IsClosed(target);
// Handle injecting/drawing for solutions
if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
{
if (SolutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
if (isOpenOrIgnored && SolutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
return TryInject(injector, target, injectableSolution.Value, user, false);
if (SolutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
if (isOpenOrIgnored && SolutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
return TryInject(injector, target, refillableSolution.Value, user, true);
if (TryComp<BloodstreamComponent>(target, out var bloodstream))
@@ -62,7 +65,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
}
// Draw from an object (food, beaker, etc)
if (SolutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
if (isOpenOrIgnored && SolutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
return TryDraw(injector, target, drawableSolution.Value, user);
Popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",

View File

@@ -51,7 +51,8 @@ public sealed class CursedMaskSystem : SharedCursedMaskSystem
}
var npcFaction = EnsureComp<NpcFactionMemberComponent>(wearer);
ent.Comp.OldFactions = npcFaction.Factions;
ent.Comp.OldFactions.Clear();
ent.Comp.OldFactions.UnionWith(npcFaction.Factions);
_npcFaction.ClearFactions((wearer, npcFaction), false);
_npcFaction.AddFaction((wearer, npcFaction), ent.Comp.CursedMaskFaction);

View File

@@ -16,6 +16,7 @@ using Content.Shared.Cluwne;
using Content.Shared.Interaction.Components;
using Robust.Shared.Audio.Systems;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Clumsy;
namespace Content.Server.Cluwne;

View File

@@ -111,11 +111,14 @@ namespace Content.Server.Connection
var serverId = (await _serverDbEntry.ServerEntity).Id;
var hwid = e.UserData.GetModernHwid();
var trust = e.UserData.Trust;
if (deny != null)
{
var (reason, msg, banHits) = deny.Value;
var id = await _db.AddConnectionLogAsync(userId, e.UserName, addr, e.UserData.HWId, reason, serverId);
var id = await _db.AddConnectionLogAsync(userId, e.UserName, addr, hwid, trust, reason, serverId);
if (banHits is { Count: > 0 })
await _db.AddServerBanHitsAsync(id, banHits);
@@ -127,12 +130,12 @@ namespace Content.Server.Connection
}
else
{
await _db.AddConnectionLogAsync(userId, e.UserName, addr, e.UserData.HWId, null, serverId);
await _db.AddConnectionLogAsync(userId, e.UserName, addr, hwid, trust, null, serverId);
if (!ServerPreferencesManager.ShouldStorePrefs(e.AuthType))
return;
await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr, e.UserData.HWId);
await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr, hwid);
}
}
@@ -190,7 +193,9 @@ namespace Content.Server.Connection
hwId = null;
}
var bans = await _db.GetServerBansAsync(addr, userId, hwId, includeUnbanned: false);
var modernHwid = e.UserData.ModernHWIds;
var bans = await _db.GetServerBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false);
if (bans.Count > 0)
{
var firstBan = bans[0];

View File

@@ -0,0 +1,24 @@
using Content.Shared.Database;
using Robust.Shared.Network;
namespace Content.Server.Connection;
/// <summary>
/// Helper functions for working with <see cref="NetUserData"/>.
/// </summary>
public static class UserDataExt
{
/// <summary>
/// Get the preferred HWID that should be used for new records related to a player.
/// </summary>
/// <remarks>
/// Players can have zero or more HWIDs, but for logging things like connection logs we generally
/// only want a single one. This method returns a nullable method.
/// </remarks>
public static ImmutableTypedHwid? GetModernHwid(this NetUserData userData)
{
return userData.ModernHWIds.Length == 0
? null
: new ImmutableTypedHwid(userData.ModernHWIds[0], HwidType.Modern);
}
}

View File

@@ -82,6 +82,8 @@ public sealed class CrayonSystem : SharedCrayonSystem
if (component.DeleteEmpty && component.Charges <= 0)
UseUpCrayon(uid, args.User);
else
_uiSystem.ServerSendUiMessage(uid, SharedCrayonComponent.CrayonUiKey.Key, new CrayonUsedMessage(component.SelectedState));
}
private void OnCrayonUse(EntityUid uid, CrayonComponent component, UseInHandEvent args)

View File

@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Net;
using Content.Server.IP;
using Content.Shared.Database;
using Robust.Shared.Network;
namespace Content.Server.Database;
@@ -52,9 +53,28 @@ public static class BanMatcher
return true;
}
return player.HWId is { Length: > 0 } hwIdVar
&& ban.HWId != null
&& hwIdVar.AsSpan().SequenceEqual(ban.HWId.Value.AsSpan());
switch (ban.HWId?.Type)
{
case HwidType.Legacy:
if (player.HWId is { Length: > 0 } hwIdVar
&& hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan()))
{
return true;
}
break;
case HwidType.Modern:
if (player.ModernHWIds is { Length: > 0 } modernHwIdVar)
{
foreach (var hwid in modernHwIdVar)
{
if (hwid.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan()))
return true;
}
}
break;
}
return false;
}
/// <summary>
@@ -73,10 +93,15 @@ public static class BanMatcher
public IPAddress? Address;
/// <summary>
/// The hardware ID of the player.
/// The LEGACY hardware ID of the player. Corresponds with <see cref="NetUserData.HWId"/>.
/// </summary>
public ImmutableArray<byte>? HWId;
/// <summary>
/// The modern hardware IDs of the player. Corresponds with <see cref="NetUserData.ModernHWIds"/>.
/// </summary>
public ImmutableArray<ImmutableArray<byte>>? ModernHWIds;
/// <summary>
/// Exemption flags the player has been granted.
/// </summary>

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using Content.Shared.Database;
using Robust.Shared.Network;
@@ -121,7 +120,7 @@ public sealed record PlayerRecord(
string LastSeenUserName,
DateTimeOffset LastSeenTime,
IPAddress LastSeenAddress,
ImmutableArray<byte>? HWId);
ImmutableTypedHwid? HWId);
public sealed record RoundRecord(int Id, DateTimeOffset? StartDate, ServerRecord Server);

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using Content.Shared.CCVar;
using Content.Shared.Database;
@@ -13,7 +12,7 @@ namespace Content.Server.Database
public int? Id { get; }
public NetUserId? UserId { get; }
public (IPAddress address, int cidrMask)? Address { get; }
public ImmutableArray<byte>? HWId { get; }
public ImmutableTypedHwid? HWId { get; }
public DateTimeOffset BanTime { get; }
public DateTimeOffset? ExpirationTime { get; }
@@ -28,7 +27,7 @@ namespace Content.Server.Database
public ServerBanDef(int? id,
NetUserId? userId,
(IPAddress, int)? address,
ImmutableArray<byte>? hwId,
TypedHwid? hwId,
DateTimeOffset banTime,
DateTimeOffset? expirationTime,
int? roundId,

View File

@@ -388,12 +388,14 @@ namespace Content.Server.Database
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
/// <param name="hwId">The HWId of the user.</param>
/// <param name="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
public abstract Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId);
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds);
/// <summary>
/// Looks up an user's ban history.
@@ -402,13 +404,15 @@ namespace Content.Server.Database
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
/// <param name="hwId">The HWId of the user.</param>
/// <param name="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Include pardoned and expired bans.</param>
/// <returns>The user's ban history.</returns>
public abstract Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned);
public abstract Task AddServerBanAsync(ServerBanDef serverBan);
@@ -499,11 +503,13 @@ namespace Content.Server.Database
/// <param name="address">The IP address of the user.</param>
/// <param name="userId">The NetUserId of the user.</param>
/// <param name="hwId">The Hardware Id of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
/// <returns>The user's role ban history.</returns>
public abstract Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned);
public abstract Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan);
@@ -512,16 +518,23 @@ namespace Content.Server.Database
public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
{
await using var db = await GetDb();
var roleBanDetails = await db.DbContext.RoleBan
.Where(b => b.Id == id)
.Select(b => new { b.BanTime, b.PlayerUserId })
.SingleOrDefaultAsync();
var ban = await db.DbContext.RoleBan.SingleOrDefaultAsync(b => b.Id == id);
if (ban is null)
if (roleBanDetails == default)
return;
ban.Severity = severity;
ban.Reason = reason;
ban.ExpirationTime = expiration?.UtcDateTime;
ban.LastEditedById = editedBy;
ban.LastEditedAt = editedAt.UtcDateTime;
await db.DbContext.SaveChangesAsync();
await db.DbContext.RoleBan
.Where(b => b.BanTime == roleBanDetails.BanTime && b.PlayerUserId == roleBanDetails.PlayerUserId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(b => b.Severity, severity)
.SetProperty(b => b.Reason, reason)
.SetProperty(b => b.ExpirationTime, expiration.HasValue ? expiration.Value.UtcDateTime : (DateTime?)null)
.SetProperty(b => b.LastEditedById, editedBy)
.SetProperty(b => b.LastEditedAt, editedAt.UtcDateTime)
);
}
#endregion
@@ -586,7 +599,7 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId)
ImmutableTypedHwid? hwId)
{
await using var db = await GetDb();
@@ -603,7 +616,7 @@ namespace Content.Server.Database
record.LastSeenTime = DateTime.UtcNow;
record.LastSeenAddress = address;
record.LastSeenUserName = userName;
record.LastSeenHWId = hwId.ToArray();
record.LastSeenHWId = hwId;
await db.DbContext.SaveChangesAsync();
}
@@ -649,7 +662,7 @@ namespace Content.Server.Database
player.LastSeenUserName,
new DateTimeOffset(NormalizeDatabaseTime(player.LastSeenTime)),
player.LastSeenAddress,
player.LastSeenHWId?.ToImmutableArray());
player.LastSeenHWId);
}
#endregion
@@ -658,11 +671,11 @@ namespace Content.Server.Database
/*
* CONNECTION LOG
*/
public abstract Task<int> AddConnectionLogAsync(
NetUserId userId,
public abstract Task<int> AddConnectionLogAsync(NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId);

View File

@@ -69,12 +69,14 @@ namespace Content.Server.Database
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
/// <param name="hwId">The hardware ID of the user.</param>
/// <param name="hwId">The legacy HWID of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId);
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds);
/// <summary>
/// Looks up an user's ban history.
@@ -82,13 +84,15 @@ namespace Content.Server.Database
/// </summary>
/// <param name="address">The ip address of the user.</param>
/// <param name="userId">The id of the user.</param>
/// <param name="hwId">The HWId of the user.</param>
/// <param name="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">If true, bans that have been expired or pardoned are also included.</param>
/// <returns>The user's ban history.</returns>
Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned=true);
Task AddServerBanAsync(ServerBanDef serverBan);
@@ -137,12 +141,14 @@ namespace Content.Server.Database
/// <param name="address">The IP address of the user.</param>
/// <param name="userId">The NetUserId of the user.</param>
/// <param name="hwId">The Hardware Id of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
/// <returns>The user's role ban history.</returns>
Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned = true);
Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan);
@@ -180,7 +186,7 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId);
ImmutableTypedHwid? hwId);
Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default);
Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default);
#endregion
@@ -191,7 +197,8 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId);
@@ -480,20 +487,22 @@ namespace Content.Server.Database
public Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId));
return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId, modernHWIds));
}
public Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned=true)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, includeUnbanned));
return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
}
public Task AddServerBanAsync(ServerBanDef serverBan)
@@ -537,10 +546,11 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned = true)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, includeUnbanned));
return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
}
public Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
@@ -582,7 +592,7 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId)
ImmutableTypedHwid? hwId)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.UpdatePlayerRecord(userId, userName, address, hwId));
@@ -604,12 +614,13 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, denied, serverId));
return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, trust, denied, serverId));
}
public Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans)

View File

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.IP;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
@@ -73,7 +74,8 @@ namespace Content.Server.Database
public override async Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
if (address == null && userId == null && hwId == null)
{
@@ -84,7 +86,7 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt, newPlayer)
var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned: false, exempt, newPlayer)
.OrderByDescending(b => b.BanTime);
var ban = await query.FirstOrDefaultAsync();
@@ -94,7 +96,9 @@ namespace Content.Server.Database
public override async Task<List<ServerBanDef>> GetServerBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId, bool includeUnbanned)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
if (address == null && userId == null && hwId == null)
{
@@ -105,7 +109,7 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = !await db.PgDbContext.Player.AnyAsync(p => p.UserId == userId);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt, newPlayer);
var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned, exempt, newPlayer);
var queryBans = await query.ToArrayAsync();
var bans = new List<ServerBanDef>(queryBans.Length);
@@ -127,6 +131,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbGuardImpl db,
bool includeUnbanned,
ServerBanExemptFlags? exemptFlags,
@@ -134,16 +139,11 @@ namespace Content.Server.Database
{
DebugTools.Assert(!(address == null && userId == null && hwId == null));
IQueryable<ServerBan>? query = null;
if (userId is { } uid)
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
var query = MakeBanLookupQualityShared<ServerBan, ServerUnban>(
userId,
hwId,
modernHWIds,
db.PgDbContext.Ban);
if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP))
{
@@ -156,15 +156,6 @@ namespace Content.Server.Database
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
DebugTools.Assert(
query != null,
"At least one filter item (IP/UserID/HWID) must have been given to make query not null.");
@@ -186,6 +177,49 @@ namespace Content.Server.Database
return query.Distinct();
}
private static IQueryable<TBan>? MakeBanLookupQualityShared<TBan, TUnban>(
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbSet<TBan> set)
where TBan : class, IBanCommon<TUnban>
where TUnban : class, IUnbanCommon
{
IQueryable<TBan>? query = null;
if (userId is { } uid)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.HWId!.Type == HwidType.Legacy && b.HWId!.Hwid.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
if (modernHWIds != null)
{
foreach (var modernHwid in modernHWIds)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.HWId!.Type == HwidType.Modern && b.HWId!.Hwid.SequenceEqual(modernHwid.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
}
return query;
}
private static ServerBanDef? ConvertBan(ServerBan? ban)
{
if (ban == null)
@@ -211,7 +245,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
ban.BanTime,
ban.ExpirationTime,
ban.RoundId,
@@ -249,7 +283,7 @@ namespace Content.Server.Database
db.PgDbContext.Ban.Add(new ServerBan
{
Address = serverBan.Address.ToNpgsqlInet(),
HWId = serverBan.HWId?.ToArray(),
HWId = serverBan.HWId,
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
@@ -297,6 +331,7 @@ namespace Content.Server.Database
public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
if (address == null && userId == null && hwId == null)
@@ -306,7 +341,7 @@ namespace Content.Server.Database
await using var db = await GetDbImpl();
var query = MakeRoleBanLookupQuery(address, userId, hwId, db, includeUnbanned)
var query = MakeRoleBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned)
.OrderByDescending(b => b.BanTime);
return await QueryRoleBans(query);
@@ -334,19 +369,15 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbGuardImpl db,
bool includeUnbanned)
{
IQueryable<ServerRoleBan>? query = null;
if (userId is { } uid)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
var query = MakeBanLookupQualityShared<ServerRoleBan, ServerRoleUnban>(
userId,
hwId,
modernHWIds,
db.PgDbContext.RoleBan);
if (address != null)
{
@@ -357,15 +388,6 @@ namespace Content.Server.Database
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
if (!includeUnbanned)
{
query = query?.Where(p =>
@@ -402,7 +424,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
ban.BanTime,
ban.ExpirationTime,
ban.RoundId,
@@ -440,7 +462,7 @@ namespace Content.Server.Database
var ban = new ServerRoleBan
{
Address = serverRoleBan.Address.ToNpgsqlInet(),
HWId = serverRoleBan.HWId?.ToArray(),
HWId = serverRoleBan.HWId,
Reason = serverRoleBan.Reason,
Severity = serverRoleBan.Severity,
BanningAdmin = serverRoleBan.BanningAdmin?.UserId,
@@ -476,7 +498,8 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId)
{
@@ -488,9 +511,10 @@ namespace Content.Server.Database
Time = DateTime.UtcNow,
UserId = userId.UserId,
UserName = userName,
HWId = hwId.ToArray(),
HWId = hwId,
Denied = denied,
ServerId = serverId
ServerId = serverId,
Trust = trust,
};
db.PgDbContext.ConnectionLog.Add(connectionLog);

View File

@@ -9,6 +9,7 @@ using Content.Server.Administration.Logs;
using Content.Server.IP;
using Content.Server.Preferences.Managers;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
@@ -80,22 +81,24 @@ namespace Content.Server.Database
public override async Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
await using var db = await GetDbImpl();
return (await GetServerBanQueryAsync(db, address, userId, hwId, includeUnbanned: false)).FirstOrDefault();
return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false)).FirstOrDefault();
}
public override async Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
await using var db = await GetDbImpl();
return (await GetServerBanQueryAsync(db, address, userId, hwId, includeUnbanned)).ToList();
return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned)).ToList();
}
private async Task<IEnumerable<ServerBanDef>> GetServerBanQueryAsync(
@@ -103,6 +106,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
var exempt = await GetBanExemptionCore(db, userId);
@@ -119,6 +123,7 @@ namespace Content.Server.Database
UserId = userId,
ExemptFlags = exempt ?? default,
HWId = hwId,
ModernHWIds = modernHWIds,
IsNewPlayer = newPlayer,
};
@@ -161,7 +166,7 @@ namespace Content.Server.Database
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
HWId = serverBan.HWId?.ToArray(),
HWId = serverBan.HWId,
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
RoundId = serverBan.RoundId,
@@ -205,6 +210,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
await using var db = await GetDbImpl();
@@ -214,7 +220,7 @@ namespace Content.Server.Database
var queryBans = await GetAllRoleBans(db.SqliteDbContext, includeUnbanned);
return queryBans
.Where(b => RoleBanMatches(b, address, userId, hwId))
.Where(b => RoleBanMatches(b, address, userId, hwId, modernHWIds))
.Select(ConvertRoleBan)
.ToList()!;
}
@@ -237,7 +243,8 @@ namespace Content.Server.Database
ServerRoleBan ban,
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
if (address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value))
{
@@ -249,7 +256,27 @@ namespace Content.Server.Database
return true;
}
return hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId);
switch (ban.HWId?.Type)
{
case HwidType.Legacy:
if (hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid))
return true;
break;
case HwidType.Modern:
if (modernHWIds != null)
{
foreach (var modernHWId in modernHWIds)
{
if (modernHWId.AsSpan().SequenceEqual(ban.HWId.Hwid))
return true;
}
}
break;
}
return false;
}
public override async Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan)
@@ -262,7 +289,7 @@ namespace Content.Server.Database
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
HWId = serverBan.HWId?.ToArray(),
HWId = serverBan.HWId,
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
RoundId = serverBan.RoundId,
@@ -316,7 +343,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
// SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc),
ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc),
@@ -376,7 +403,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
// SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc),
ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc),
@@ -412,7 +439,8 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId)
{
@@ -424,9 +452,10 @@ namespace Content.Server.Database
Time = DateTime.UtcNow,
UserId = userId.UserId,
UserName = userName,
HWId = hwId.ToArray(),
HWId = hwId,
Denied = denied,
ServerId = serverId
ServerId = serverId,
Trust = trust,
};
db.SqliteDbContext.ConnectionLog.Add(connectionLog);

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using Content.Shared.Database;
using Robust.Shared.Network;
@@ -10,7 +9,7 @@ public sealed class ServerRoleBanDef
public int? Id { get; }
public NetUserId? UserId { get; }
public (IPAddress address, int cidrMask)? Address { get; }
public ImmutableArray<byte>? HWId { get; }
public ImmutableTypedHwid? HWId { get; }
public DateTimeOffset BanTime { get; }
public DateTimeOffset? ExpirationTime { get; }
@@ -26,7 +25,7 @@ public sealed class ServerRoleBanDef
int? id,
NetUserId? userId,
(IPAddress, int)? address,
ImmutableArray<byte>? hwId,
ImmutableTypedHwid? hwId,
DateTimeOffset banTime,
DateTimeOffset? expirationTime,
int? roundId,

View File

@@ -184,6 +184,6 @@ namespace Content.Server.GameTicking
=> UserHasJoinedGame(session.UserId);
public bool UserHasJoinedGame(NetUserId userId)
=> PlayerGameStatuses[userId] == PlayerGameStatus.JoinedGame;
=> PlayerGameStatuses.TryGetValue(userId, out var status) && status == PlayerGameStatus.JoinedGame;
}
}

View File

@@ -7,12 +7,12 @@ using Content.Server.Spawners.Components;
using Content.Server.Speech.Components;
using Content.Server.Station.Components;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
@@ -457,71 +457,4 @@ namespace Content.Server.GameTicking
#endregion
}
/// <summary>
/// Event raised broadcast before a player is spawned by the GameTicker.
/// You can use this event to spawn a player off-station on late-join but also at round start.
/// When this event is handled, the GameTicker will not perform its own player-spawning logic.
/// </summary>
[PublicAPI]
public sealed class PlayerBeforeSpawnEvent : HandledEntityEventArgs
{
public ICommonSession Player { get; }
public HumanoidCharacterProfile Profile { get; }
public string? JobId { get; }
public bool LateJoin { get; }
public EntityUid Station { get; }
public PlayerBeforeSpawnEvent(ICommonSession player,
HumanoidCharacterProfile profile,
string? jobId,
bool lateJoin,
EntityUid station)
{
Player = player;
Profile = profile;
JobId = jobId;
LateJoin = lateJoin;
Station = station;
}
}
/// <summary>
/// Event raised both directed and broadcast when a player has been spawned by the GameTicker.
/// You can use this to handle people late-joining, or to handle people being spawned at round start.
/// Can be used to give random players a role, modify their equipment, etc.
/// </summary>
[PublicAPI]
public sealed class PlayerSpawnCompleteEvent : EntityEventArgs
{
public EntityUid Mob { get; }
public ICommonSession Player { get; }
public string? JobId { get; }
public bool LateJoin { get; }
public bool Silent { get; }
public EntityUid Station { get; }
public HumanoidCharacterProfile Profile { get; }
// Ex. If this is the 27th person to join, this will be 27.
public int JoinOrder { get; }
public PlayerSpawnCompleteEvent(EntityUid mob,
ICommonSession player,
string? jobId,
bool lateJoin,
bool silent,
int joinOrder,
EntityUid station,
HumanoidCharacterProfile profile)
{
Mob = mob;
Player = player;
JobId = jobId;
LateJoin = lateJoin;
Silent = silent;
Station = station;
Profile = profile;
JoinOrder = joinOrder;
}
}
}

View File

@@ -6,6 +6,7 @@ using Content.Server.Mind;
using Content.Server.Points;
using Content.Server.RoundEnd;
using Content.Server.Station.Systems;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Points;
using Content.Shared.Storage;

View File

@@ -41,6 +41,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
{
base.Initialize();
Log.Level = LogLevel.Debug;
SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
}
@@ -53,6 +55,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
private void AfterEntitySelected(Entity<TraitorRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
Log.Debug($"AfterAntagEntitySelected {ToPrettyString(ent)}");
MakeTraitor(args.EntityUid, ent);
}
@@ -78,14 +81,22 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component)
{
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - start");
//Grab the mind if it wasn't provided
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
{
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - failed, no Mind found");
return false;
}
var briefing = "";
if (component.GiveCodewords)
{
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - added codewords flufftext to briefing");
briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
}
var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers).Values);
@@ -94,6 +105,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
if (component.GiveUplink)
{
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink start");
// Calculate the amount of currency on the uplink.
var startingBalance = component.StartingBalance;
if (_jobs.MindTryGetJob(mindId, out var prototype))
@@ -105,18 +117,27 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
}
// Choose and generate an Uplink, and return the uplink code if applicable
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink request start");
var uplinkParams = RequestUplink(traitor, startingBalance, briefing);
code = uplinkParams.Item1;
briefing = uplinkParams.Item2;
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink request completed");
}
string[]? codewords = null;
if (component.GiveCodewords)
{
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - set codewords from component");
codewords = component.Codewords;
}
if (component.GiveBriefing)
{
_antag.SendBriefing(traitor, GenerateBriefing(codewords, code, issuer), null, component.GreetSoundNotification);
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Sent the Briefing");
}
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Adding TraitorMind");
component.TraitorMinds.Add(mindId);
// Assign briefing
@@ -126,9 +147,14 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
_roleSystem.MindHasRole<TraitorRoleComponent>(mindId, out var traitorRole);
if (traitorRole is not null)
{
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Add traitor briefing components");
AddComp<RoleBriefingComponent>(traitorRole.Value.Owner);
Comp<RoleBriefingComponent>(traitorRole.Value.Owner).Briefing = briefing;
}
else
{
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - did not get traitor briefing");
}
// Send codewords to only the traitor client
var color = TraitorCodewordColor; // Fall back to a dark red Syndicate color if a prototype is not found
@@ -137,9 +163,11 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
_roleCodewordSystem.SetRoleCodewords(codewordComp, "traitor", component.Codewords.ToList(), color);
// Change the faction
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Change faction");
_npcFaction.RemoveFaction(traitor, component.NanoTrasenFaction, false);
_npcFaction.AddFaction(traitor, component.SyndicateFaction);
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Finished");
return true;
}
@@ -148,10 +176,12 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
var pda = _uplink.FindUplinkTarget(traitor);
Note[]? code = null;
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink add");
var uplinked = _uplink.AddUplink(traitor, startingBalance, pda, true);
if (pda is not null && uplinked)
{
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is PDA");
// Codes are only generated if the uplink is a PDA
code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
@@ -163,6 +193,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
}
else if (pda is null && uplinked)
{
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is implant");
briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short");
}

View File

@@ -34,10 +34,11 @@ public sealed class GatewayGeneratorSystem : EntitySystem
[Dependency] private readonly GatewaySystem _gateway = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly SharedMapSystem _maps = default!;
[Dependency] private readonly SharedSalvageSystem _salvage = default!;
[Dependency] private readonly TileSystem _tile = default!;
[ValidatePrototypeId<DatasetPrototype>]
private const string PlanetNames = "names_borer";
[ValidatePrototypeId<LocalizedDatasetPrototype>]
private const string PlanetNames = "NamesBorer";
// TODO:
// Fix shader some more
@@ -102,7 +103,7 @@ public sealed class GatewayGeneratorSystem : EntitySystem
var mapId = _mapManager.CreateMap();
var mapUid = _mapManager.GetMapEntityId(mapId);
var gatewayName = SharedSalvageSystem.GetFTLName(_protoManager.Index<DatasetPrototype>(PlanetNames), seed);
var gatewayName = _salvage.GetFTLName(_protoManager.Index<LocalizedDatasetPrototype>(PlanetNames), seed);
_metadata.SetEntityName(mapUid, gatewayName);
var origin = new Vector2i(random.Next(-MaxOffset, MaxOffset), random.Next(-MaxOffset, MaxOffset));

View File

@@ -60,7 +60,6 @@ public sealed class RandomGiftSystem : EntitySystem
var coords = Transform(args.User).Coordinates;
var handsEnt = Spawn(component.SelectedEntity, coords);
_adminLogger.Add(LogType.EntitySpawn, LogImpact.Low, $"{ToPrettyString(args.User)} used {ToPrettyString(uid)} which spawned {ToPrettyString(handsEnt)}");
EnsureComp<ItemComponent>(handsEnt); // For insane mode.
if (component.Wrapper is not null)
Spawn(component.Wrapper, coords);

View File

@@ -77,7 +77,20 @@ public sealed class DefibrillatorSystem : EntitySystem
Zap(uid, target, args.User, component);
}
public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null)
/// <summary>
/// Checks if you can actually defib a target.
/// </summary>
/// <param name="uid">Uid of the defib</param>
/// <param name="target">Uid of the target getting defibbed</param>
/// <param name="user">Uid of the entity using the defibrillator</param>
/// <param name="component">Defib component</param>
/// <param name="targetCanBeAlive">
/// If true, the target can be alive. If false, the function will check if the target is alive and will return false if they are.
/// </param>
/// <returns>
/// Returns true if the target is valid to be defibed, false otherwise.
/// </returns>
public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null, bool targetCanBeAlive = false)
{
if (!Resolve(uid, ref component))
return false;
@@ -98,15 +111,25 @@ public sealed class DefibrillatorSystem : EntitySystem
if (!_powerCell.HasActivatableCharge(uid, user: user))
return false;
if (_mobState.IsAlive(target, mobState))
if (!targetCanBeAlive && _mobState.IsAlive(target, mobState))
return false;
if (!component.CanDefibCrit && _mobState.IsCritical(target, mobState))
if (!targetCanBeAlive && !component.CanDefibCrit && _mobState.IsCritical(target, mobState))
return false;
return true;
}
/// <summary>
/// Tries to start defibrillating the target. If the target is valid, will start the defib do-after.
/// </summary>
/// <param name="uid">Uid of the defib</param>
/// <param name="target">Uid of the target getting defibbed</param>
/// <param name="user">Uid of the entity using the defibrillator</param>
/// <param name="component">Defib component</param>
/// <returns>
/// Returns true if the defibrillation do-after started, otherwise false.
/// </returns>
public bool TryStartZap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null)
{
if (!Resolve(uid, ref component))
@@ -118,27 +141,44 @@ public sealed class DefibrillatorSystem : EntitySystem
_audio.PlayPvs(component.ChargeSound, uid);
return _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, component.DoAfterDuration, new DefibrillatorZapDoAfterEvent(),
uid, target, uid)
{
NeedHand = true,
BreakOnMove = !component.AllowDoAfterMovement
});
{
NeedHand = true,
BreakOnMove = !component.AllowDoAfterMovement
});
}
public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null, MobStateComponent? mob = null, MobThresholdsComponent? thresholds = null)
/// <summary>
/// Tries to defibrillate the target with the given defibrillator.
/// </summary>
public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null)
{
if (!Resolve(uid, ref component) || !Resolve(target, ref mob, ref thresholds, false))
if (!Resolve(uid, ref component))
return;
// clowns zap themselves
if (HasComp<ClumsyComponent>(user) && user != target)
{
Zap(uid, user, user, component);
return;
}
if (!_powerCell.TryUseActivatableCharge(uid, user: user))
return;
var selfEvent = new SelfBeforeDefibrillatorZapsEvent(user, uid, target);
RaiseLocalEvent(user, selfEvent);
target = selfEvent.DefibTarget;
// Ensure thet new target is still valid.
if (selfEvent.Cancelled || !CanZap(uid, target, user, component, true))
return;
var targetEvent = new TargetBeforeDefibrillatorZapsEvent(user, uid, target);
RaiseLocalEvent(target, targetEvent);
target = targetEvent.DefibTarget;
if (targetEvent.Cancelled || !CanZap(uid, target, user, component, true))
return;
if (!TryComp<MobStateComponent>(target, out var mob) ||
!TryComp<MobThresholdsComponent>(target, out var thresholds))
return;
_audio.PlayPvs(component.ZapSound, uid);
_electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true);
component.NextZapTime = _timing.CurTime + component.ZapDelay;

View File

@@ -4,7 +4,6 @@ using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Emp;
using Content.Server.GameTicking;
using Content.Server.Medical.CrewMonitoring;
using Content.Server.Popups;
using Content.Server.Station.Systems;
@@ -14,8 +13,10 @@ using Content.Shared.Damage;
using Content.Shared.DeviceNetwork;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Interaction;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Verbs;
@@ -383,7 +384,7 @@ public sealed class SuitSensorSystem : EntitySystem
// Get mob total damage crit threshold
int? totalDamageThreshold = null;
if (_mobThresholdSystem.TryGetThresholdForState(sensor.User.Value, Shared.Mobs.MobState.Critical, out var critThreshold))
if (_mobThresholdSystem.TryGetThresholdForState(sensor.User.Value, MobState.Critical, out var critThreshold))
totalDamageThreshold = critThreshold.Value.Int();
// finally, form suit sensor status

View File

@@ -11,8 +11,8 @@ public sealed partial class PathfindingSystem
/// </summary>
public record struct BreadthPathArgs()
{
public Vector2i Start;
public List<Vector2i> Ends;
public required Vector2i Start;
public required List<Vector2i> Ends;
public bool Diagonals = false;

View File

@@ -19,7 +19,7 @@ public sealed partial class PathfindingSystem
public List<Vector2i> Points = new();
public List<Vector2i> Path = new();
public Dictionary<Vector2i, Vector2i> CameFrom;
public Dictionary<Vector2i, Vector2i>? CameFrom;
}
public record struct SplinePathArgs(SimplePathArgs Args)

View File

@@ -84,6 +84,6 @@ public sealed partial class PathfindingSystem
public float MaxWiden = 7f;
public List<Vector2i> Path;
public required List<Vector2i> Path;
}
}

View File

@@ -122,8 +122,7 @@ namespace Content.Server.Nutrition.EntitySystems
private void OnVapeDoAfter(Entity<VapeComponent> entity, ref VapeDoAfterEvent args)
{
if (args.Handled
|| args.Args.Target == null)
if (args.Cancelled || args.Handled || args.Args.Target == null)
return;
var environment = _atmos.GetContainingMixture(args.Args.Target.Value, true, true);

View File

@@ -44,7 +44,7 @@ namespace Content.Server.Nutrition.EntitySystems
public (bool Success, bool Handled) TryUseUtensil(EntityUid user, EntityUid target, Entity<UtensilComponent> utensil)
{
if (!EntityManager.TryGetComponent(target, out FoodComponent? food))
return (false, true);
return (false, false);
//Prevents food usage with a wrong utensil
if ((food.Utensil & utensil.Comp.Types) == 0)

View File

@@ -53,7 +53,7 @@ public sealed class KillPersonConditionSystem : EntitySystem
return;
// no other humans to kill
var allHumans = _mind.GetAliveHumansExcept(args.MindId);
var allHumans = _mind.GetAliveHumans(args.MindId);
if (allHumans.Count == 0)
{
args.Cancelled = true;
@@ -77,14 +77,14 @@ public sealed class KillPersonConditionSystem : EntitySystem
return;
// no other humans to kill
var allHumans = _mind.GetAliveHumansExcept(args.MindId);
var allHumans = _mind.GetAliveHumans(args.MindId);
if (allHumans.Count == 0)
{
args.Cancelled = true;
return;
}
var allHeads = new List<EntityUid>();
var allHeads = new HashSet<Entity<MindComponent>>();
foreach (var person in allHumans)
{
if (TryComp<MindComponent>(person, out var mind) && mind.OwnedEntity is { } ent && HasComp<CommandStaffComponent>(ent))

View File

@@ -314,6 +314,9 @@ public sealed class MoverController : SharedMoverController
var linearInput = Vector2.Zero;
var brakeInput = 0f;
var angularInput = 0f;
var linearCount = 0;
var brakeCount = 0;
var angularCount = 0;
foreach (var (pilotUid, pilot, _, consoleXform) in pilots)
{
@@ -322,24 +325,27 @@ public sealed class MoverController : SharedMoverController
if (brakes > 0f)
{
brakeInput += brakes;
brakeCount++;
}
if (strafe.Length() > 0f)
{
var offsetRotation = consoleXform.LocalRotation;
linearInput += offsetRotation.RotateVec(strafe);
linearCount++;
}
if (rotation != 0f)
{
angularInput += rotation;
angularCount++;
}
}
var count = pilots.Count;
linearInput /= count;
angularInput /= count;
brakeInput /= count;
// Don't slow down the shuttle if there's someone just looking at the console
linearInput /= Math.Max(1, linearCount);
angularInput /= Math.Max(1, angularCount);
brakeInput /= Math.Max(1, brakeCount);
// Handle shuttle movement
if (brakeInput > 0f)

View File

@@ -1,99 +1,5 @@
using Content.Server.Polymorph.Components;
using Content.Shared.Actions;
using Content.Shared.Construction.Components;
using Content.Shared.Hands;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Polymorph;
using Content.Shared.Polymorph.Components;
using Content.Shared.Polymorph.Systems;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Physics.Components;
namespace Content.Server.Polymorph.Systems;
public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem
{
[Dependency] private readonly MetaDataSystem _meta = default!;
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
[Dependency] private readonly PolymorphSystem _polymorph = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChameleonDisguiseComponent, GotEquippedHandEvent>(OnEquippedHand);
SubscribeLocalEvent<ChameleonDisguiseComponent, DisguiseToggleNoRotEvent>(OnToggleNoRot);
SubscribeLocalEvent<ChameleonDisguiseComponent, DisguiseToggleAnchoredEvent>(OnToggleAnchored);
}
private void OnEquippedHand(Entity<ChameleonDisguiseComponent> ent, ref GotEquippedHandEvent args)
{
if (!TryComp<PolymorphedEntityComponent>(ent, out var poly))
return;
_polymorph.Revert((ent, poly));
args.Handled = true;
}
public override void Disguise(ChameleonProjectorComponent proj, EntityUid user, EntityUid entity)
{
if (_polymorph.PolymorphEntity(user, proj.Polymorph) is not {} disguise)
return;
// make disguise look real (for simple things at least)
var meta = MetaData(entity);
_meta.SetEntityName(disguise, meta.EntityName);
_meta.SetEntityDescription(disguise, meta.EntityDescription);
var comp = EnsureComp<ChameleonDisguiseComponent>(disguise);
comp.SourceEntity = entity;
comp.SourceProto = Prototype(entity)?.ID;
Dirty(disguise, comp);
// no sechud trolling
RemComp<StatusIconComponent>(disguise);
_appearance.CopyData(entity, disguise);
var mass = CompOrNull<PhysicsComponent>(entity)?.Mass ?? 0f;
// let the disguise die when its taken enough damage, which then transfers to the player
// health is proportional to mass, and capped to not be insane
if (TryComp<MobThresholdsComponent>(disguise, out var thresholds))
{
// if the player is of flesh and blood, cap max health to theirs
// so that when reverting damage scales 1:1 and not round removing
var playerMax = _mobThreshold.GetThresholdForState(user, MobState.Dead).Float();
var max = playerMax == 0f ? proj.MaxHealth : Math.Max(proj.MaxHealth, playerMax);
var health = Math.Clamp(mass, proj.MinHealth, proj.MaxHealth);
_mobThreshold.SetMobStateThreshold(disguise, health, MobState.Critical, thresholds);
_mobThreshold.SetMobStateThreshold(disguise, max, MobState.Dead, thresholds);
}
// add actions for controlling transform aspects
_actions.AddAction(disguise, proj.NoRotAction);
_actions.AddAction(disguise, proj.AnchorAction);
}
private void OnToggleNoRot(Entity<ChameleonDisguiseComponent> ent, ref DisguiseToggleNoRotEvent args)
{
var xform = Transform(ent);
xform.NoLocalRotation = !xform.NoLocalRotation;
}
private void OnToggleAnchored(Entity<ChameleonDisguiseComponent> ent, ref DisguiseToggleAnchoredEvent args)
{
var uid = ent.Owner;
var xform = Transform(uid);
if (xform.Anchored)
_xform.Unanchor(uid, xform);
else
_xform.AnchorEntity((uid, xform));
}
}
public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem;

View File

@@ -199,6 +199,9 @@ public sealed partial class PolymorphSystem : EntitySystem
var targetTransformComp = Transform(uid);
if (configuration.PolymorphSound != null)
_audio.PlayPvs(configuration.PolymorphSound, targetTransformComp.Coordinates);
var child = Spawn(configuration.Entity, _transform.GetMapCoordinates(uid, targetTransformComp), rotation: _transform.GetWorldRotation(uid));
MakeSentientCommand.MakeSentient(child, EntityManager);
@@ -288,6 +291,9 @@ public sealed partial class PolymorphSystem : EntitySystem
var uidXform = Transform(uid);
var parentXform = Transform(parent);
if (component.Configuration.ExitPolymorphSound != null)
_audio.PlayPvs(component.Configuration.ExitPolymorphSound, uidXform.Coordinates);
_transform.SetParent(parent, parentXform, uidXform.ParentUid);
_transform.SetCoordinates(parent, parentXform, uidXform.Coordinates, uidXform.LocalRotation);

View File

@@ -31,6 +31,12 @@ public sealed partial class DungeonJob
if (reservedTiles.Contains(node))
continue;
if (dunGen.TileMask is not null)
{
if (!dunGen.TileMask.Contains(((ContentTileDefinition) _tileDefManager[tileRef.Value.Tile.TypeId]).ID))
continue;
}
if (dunGen.TileMask is not null)
{

View File

@@ -31,6 +31,7 @@ namespace Content.Server.Research.Disk
_research.ModifyServerPoints(args.Target.Value, component.Points, server);
_popupSystem.PopupEntity(Loc.GetString("research-disk-inserted", ("points", component.Points)), args.Target.Value, args.User);
EntityManager.QueueDeleteEntity(uid);
args.Handled = true;
}
private void OnMapInit(EntityUid uid, ResearchDiskComponent component, MapInitEvent args)

View File

@@ -28,7 +28,7 @@ public sealed partial class SalvageSystem
var mission = GetMission(_prototypeManager.Index<SalvageDifficultyPrototype>(missionparams.Difficulty), missionparams.Seed);
data.NextOffer = _timing.CurTime + mission.Duration + TimeSpan.FromSeconds(1);
_labelSystem.Label(cdUid, GetFTLName(_prototypeManager.Index<DatasetPrototype>("names_borer"), missionparams.Seed));
_labelSystem.Label(cdUid, GetFTLName(_prototypeManager.Index<LocalizedDatasetPrototype>("NamesBorer"), missionparams.Seed));
_audio.PlayPvs(component.PrintSound, uid);
UpdateConsoles((station.Value, data));

View File

@@ -104,7 +104,9 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
destComp.BeaconsOnly = true;
destComp.RequireCoordinateDisk = true;
destComp.Enabled = true;
_metaData.SetEntityName(mapUid, SharedSalvageSystem.GetFTLName(_prototypeManager.Index<DatasetPrototype>("names_borer"), _missionParams.Seed));
_metaData.SetEntityName(
mapUid,
_entManager.System<SharedSalvageSystem>().GetFTLName(_prototypeManager.Index<LocalizedDatasetPrototype>("NamesBorer"), _missionParams.Seed));
_entManager.AddComponent<FTLBeaconComponent>(mapUid);
// Saving the mission mapUid to a CD is made optional, in case one is somehow made in a process without a CD entity

View File

@@ -13,11 +13,12 @@ public sealed class ServerInfoManager
private static readonly (CVarDef<string> cVar, string icon, string name)[] Vars =
{
// @formatter:off
(CCVars.InfoLinksDiscord, "discord", "info-link-discord"),
(CCVars.InfoLinksForum, "forum", "info-link-forum"),
(CCVars.InfoLinksGithub, "github", "info-link-github"),
(CCVars.InfoLinksWebsite, "web", "info-link-website"),
(CCVars.InfoLinksWiki, "wiki", "info-link-wiki")
(CCVars.InfoLinksDiscord, "discord", "info-link-discord"),
(CCVars.InfoLinksForum, "forum", "info-link-forum"),
(CCVars.InfoLinksGithub, "github", "info-link-github"),
(CCVars.InfoLinksWebsite, "web", "info-link-website"),
(CCVars.InfoLinksWiki, "wiki", "info-link-wiki"),
(CCVars.InfoLinksTelegram, "telegram", "info-link-telegram")
// @formatter:on
};

View File

@@ -32,7 +32,7 @@ public interface IGridSpawnGroup
public float MaximumDistance { get; }
/// <inheritdoc />
public ProtoId<DatasetPrototype>? NameDataset { get; }
public ProtoId<LocalizedDatasetPrototype>? NameDataset { get; }
/// <inheritdoc />
int MinCount { get; set; }
@@ -75,7 +75,7 @@ public sealed class DungeonSpawnGroup : IGridSpawnGroup
public float MaximumDistance { get; }
/// <inheritdoc />
public ProtoId<DatasetPrototype>? NameDataset { get; }
public ProtoId<LocalizedDatasetPrototype>? NameDataset { get; }
/// <inheritdoc />
public int MinCount { get; set; } = 1;
@@ -106,7 +106,7 @@ public sealed class GridSpawnGroup : IGridSpawnGroup
/// <inheritdoc />
public float MaximumDistance { get; }
public ProtoId<DatasetPrototype>? NameDataset { get; }
public ProtoId<LocalizedDatasetPrototype>? NameDataset { get; }
public int MinCount { get; set; } = 1;
public int MaxCount { get; set; } = 1;
public ComponentRegistry AddComponents { get; set; } = new();

View File

@@ -19,10 +19,10 @@ using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Damage.Components;
using Content.Shared.DeviceNetwork;
using Content.Shared.GameTicking;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Preferences;
using Content.Shared.Salvage;
using Content.Shared.Shuttles.Components;
using Content.Shared.Tiles;

View File

@@ -208,7 +208,7 @@ public sealed partial class ShuttleSystem
if (_protoManager.TryIndex(group.NameDataset, out var dataset))
{
_metadata.SetEntityName(spawned, SharedSalvageSystem.GetFTLName(dataset, _random.Next()));
_metadata.SetEntityName(spawned, _salvage.GetFTLName(dataset, _random.Next()));
}
if (group.Hide)

View File

@@ -9,6 +9,7 @@ using Content.Server.Station.Systems;
using Content.Server.Stunnable;
using Content.Shared.GameTicking;
using Content.Shared.Mobs.Systems;
using Content.Shared.Salvage;
using Content.Shared.Shuttles.Systems;
using Content.Shared.Throwing;
using JetBrains.Annotations;
@@ -52,6 +53,7 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedSalvageSystem _salvage = default!;
[Dependency] private readonly ShuttleConsoleSystem _console = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly StunSystem _stuns = default!;

View File

@@ -0,0 +1,82 @@
using Content.Server.Inventory;
using Content.Server.Radio.Components;
using Content.Shared.Inventory;
using Content.Shared.Silicons.Borgs;
using Content.Shared.Silicons.Borgs.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Silicons.Borgs;
/// <summary>
/// Server-side logic for borg type switching. Handles more heavyweight and server-specific switching logic.
/// </summary>
public sealed class BorgSwitchableTypeSystem : SharedBorgSwitchableTypeSystem
{
[Dependency] private readonly BorgSystem _borgSystem = default!;
[Dependency] private readonly ServerInventorySystem _inventorySystem = default!;
protected override void SelectBorgModule(Entity<BorgSwitchableTypeComponent> ent, ProtoId<BorgTypePrototype> borgType)
{
var prototype = Prototypes.Index(borgType);
// Assign radio channels
string[] radioChannels = [.. ent.Comp.InherentRadioChannels, .. prototype.RadioChannels];
if (TryComp(ent, out IntrinsicRadioTransmitterComponent? transmitter))
transmitter.Channels = [.. radioChannels];
if (TryComp(ent, out ActiveRadioComponent? activeRadio))
activeRadio.Channels = [.. radioChannels];
// Borg transponder for the robotics console
if (TryComp(ent, out BorgTransponderComponent? transponder))
{
_borgSystem.SetTransponderSprite(
(ent.Owner, transponder),
new SpriteSpecifier.Rsi(new ResPath("Mobs/Silicon/chassis.rsi"), prototype.SpriteBodyState));
_borgSystem.SetTransponderName(
(ent.Owner, transponder),
Loc.GetString($"borg-type-{borgType}-transponder"));
}
// Configure modules
if (TryComp(ent, out BorgChassisComponent? chassis))
{
var chassisEnt = (ent.Owner, chassis);
_borgSystem.SetMaxModules(
chassisEnt,
prototype.ExtraModuleCount + prototype.DefaultModules.Length);
_borgSystem.SetModuleWhitelist(chassisEnt, prototype.ModuleWhitelist);
foreach (var module in prototype.DefaultModules)
{
var moduleEntity = Spawn(module);
var borgModule = Comp<BorgModuleComponent>(moduleEntity);
_borgSystem.SetBorgModuleDefault((moduleEntity, borgModule), true);
_borgSystem.InsertModule(chassisEnt, moduleEntity);
}
}
// Configure special components
if (Prototypes.TryIndex(ent.Comp.SelectedBorgType, out var previousPrototype))
{
if (previousPrototype.AddComponents is { } removeComponents)
EntityManager.RemoveComponents(ent, removeComponents);
}
if (prototype.AddComponents is { } addComponents)
{
EntityManager.AddComponents(ent, addComponents);
}
// Configure inventory template (used for hat spacing)
if (TryComp(ent, out InventoryComponent? inventory))
{
_inventorySystem.SetTemplateId((ent.Owner, inventory), prototype.InventoryTemplateId);
}
base.SelectBorgModule(ent, borgType);
}
}

View File

@@ -2,6 +2,7 @@ using System.Linq;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction.Components;
using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
namespace Content.Server.Silicons.Borgs;
@@ -300,6 +301,24 @@ public sealed partial class BorgSystem
return true;
}
/// <summary>
/// Check if a module can be removed from a borg.
/// </summary>
/// <param name="borg">The borg that the module is being removed from.</param>
/// <param name="module">The module to remove from the borg.</param>
/// <param name="user">The user attempting to remove the module.</param>
/// <returns>True if the module can be removed.</returns>
public bool CanRemoveModule(
Entity<BorgChassisComponent> borg,
Entity<BorgModuleComponent> module,
EntityUid? user = null)
{
if (module.Comp.DefaultModule)
return false;
return true;
}
/// <summary>
/// Installs and activates all modules currently inside the borg's module container
/// </summary>
@@ -369,4 +388,24 @@ public sealed partial class BorgSystem
var ev = new BorgModuleUninstalledEvent(uid);
RaiseLocalEvent(module, ref ev);
}
/// <summary>
/// Sets <see cref="BorgChassisComponent.MaxModules"/>.
/// </summary>
/// <param name="ent">The borg to modify.</param>
/// <param name="maxModules">The new max module count.</param>
public void SetMaxModules(Entity<BorgChassisComponent> ent, int maxModules)
{
ent.Comp.MaxModules = maxModules;
}
/// <summary>
/// Sets <see cref="BorgChassisComponent.ModuleWhitelist"/>.
/// </summary>
/// <param name="ent">The borg to modify.</param>
/// <param name="whitelist">The new module whitelist.</param>
public void SetModuleWhitelist(Entity<BorgChassisComponent> ent, EntityWhitelist? whitelist)
{
ent.Comp.ModuleWhitelist = whitelist;
}
}

View File

@@ -8,6 +8,7 @@ using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Explosion.Components;
using Robust.Shared.Utility;
namespace Content.Server.Silicons.Borgs;
@@ -134,4 +135,20 @@ public sealed partial class BorgSystem
return false;
}
/// <summary>
/// Sets <see cref="BorgTransponderComponent.Sprite"/>.
/// </summary>
public void SetTransponderSprite(Entity<BorgTransponderComponent> ent, SpriteSpecifier sprite)
{
ent.Comp.Sprite = sprite;
}
/// <summary>
/// Sets <see cref="BorgTransponderComponent.Name"/>.
/// </summary>
public void SetTransponderName(Entity<BorgTransponderComponent> ent, string name)
{
ent.Comp.Name = name;
}
}

View File

@@ -82,6 +82,9 @@ public sealed partial class BorgSystem
if (!component.ModuleContainer.Contains(module))
return;
if (!CanRemoveModule((uid, component), (module, Comp<BorgModuleComponent>(module)), args.Actor))
return;
_adminLog.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(args.Actor):player} removed module {ToPrettyString(module)} from borg {ToPrettyString(uid)}");
_container.Remove(module, component.ModuleContainer);

View File

@@ -129,7 +129,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
if (module != null && CanInsertModule(uid, used, component, module, args.User))
{
_container.Insert(used, component.ModuleContainer);
InsertModule((uid, component), used);
_adminLog.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.User):player} installed module {ToPrettyString(used)} into borg {ToPrettyString(uid)}");
args.Handled = true;
@@ -137,6 +137,19 @@ public sealed partial class BorgSystem : SharedBorgSystem
}
}
/// <summary>
/// Inserts a new module into a borg, the same as if a player inserted it manually.
/// </summary>
/// <para>
/// This does not run checks to see if the borg is actually allowed to be inserted, such as whitelists.
/// </para>
/// <param name="ent">The borg to insert into.</param>
/// <param name="module">The module to insert.</param>
public void InsertModule(Entity<BorgChassisComponent> ent, EntityUid module)
{
_container.Insert(module, ent.Comp.ModuleContainer);
}
// todo: consider transferring over the ghost role? managing that might suck.
protected override void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args)
{

View File

@@ -0,0 +1,280 @@
using Content.Server.StationEvents.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Dataset;
using Content.Shared.FixedPoint;
using Content.Shared.GameTicking.Components;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.Silicons.Laws;
using Content.Shared.Silicons.Laws.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Silicons.Laws;
public sealed class IonStormSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SiliconLawSystem _siliconLaw = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
// funny
[ValidatePrototypeId<DatasetPrototype>]
private const string Threats = "IonStormThreats";
[ValidatePrototypeId<DatasetPrototype>]
private const string Objects = "IonStormObjects";
[ValidatePrototypeId<DatasetPrototype>]
private const string Crew = "IonStormCrew";
[ValidatePrototypeId<DatasetPrototype>]
private const string Adjectives = "IonStormAdjectives";
[ValidatePrototypeId<DatasetPrototype>]
private const string Verbs = "IonStormVerbs";
[ValidatePrototypeId<DatasetPrototype>]
private const string NumberBase = "IonStormNumberBase";
[ValidatePrototypeId<DatasetPrototype>]
private const string NumberMod = "IonStormNumberMod";
[ValidatePrototypeId<DatasetPrototype>]
private const string Areas = "IonStormAreas";
[ValidatePrototypeId<DatasetPrototype>]
private const string Feelings = "IonStormFeelings";
[ValidatePrototypeId<DatasetPrototype>]
private const string FeelingsPlural = "IonStormFeelingsPlural";
[ValidatePrototypeId<DatasetPrototype>]
private const string Musts = "IonStormMusts";
[ValidatePrototypeId<DatasetPrototype>]
private const string Requires = "IonStormRequires";
[ValidatePrototypeId<DatasetPrototype>]
private const string Actions = "IonStormActions";
[ValidatePrototypeId<DatasetPrototype>]
private const string Allergies = "IonStormAllergies";
[ValidatePrototypeId<DatasetPrototype>]
private const string AllergySeverities = "IonStormAllergySeverities";
[ValidatePrototypeId<DatasetPrototype>]
private const string Concepts = "IonStormConcepts";
[ValidatePrototypeId<DatasetPrototype>]
private const string Drinks = "IonStormDrinks";
[ValidatePrototypeId<DatasetPrototype>]
private const string Foods = "IonStormFoods";
/// <summary>
/// Randomly alters the laws of an individual silicon.
/// </summary>
public void IonStormTarget(Entity<SiliconLawBoundComponent, IonStormTargetComponent> ent, bool adminlog = true)
{
var lawBound = ent.Comp1;
var target = ent.Comp2;
if (!_robustRandom.Prob(target.Chance))
return;
var laws = _siliconLaw.GetLaws(ent, lawBound);
if (laws.Laws.Count == 0)
return;
// try to swap it out with a random lawset
if (_robustRandom.Prob(target.RandomLawsetChance))
{
var lawsets = _proto.Index<WeightedRandomPrototype>(target.RandomLawsets);
var lawset = lawsets.Pick(_robustRandom);
laws = _siliconLaw.GetLawset(lawset);
}
// clone it so not modifying stations lawset
laws = laws.Clone();
// shuffle them all
if (_robustRandom.Prob(target.ShuffleChance))
{
// hopefully work with existing glitched laws if there are multiple ion storms
var baseOrder = FixedPoint2.New(1);
foreach (var law in laws.Laws)
{
if (law.Order < baseOrder)
baseOrder = law.Order;
}
_robustRandom.Shuffle(laws.Laws);
// change order based on shuffled position
for (int i = 0; i < laws.Laws.Count; i++)
{
laws.Laws[i].Order = baseOrder + i;
}
}
// see if we can remove a random law
if (laws.Laws.Count > 0 && _robustRandom.Prob(target.RemoveChance))
{
var i = _robustRandom.Next(laws.Laws.Count);
laws.Laws.RemoveAt(i);
}
// generate a new law...
var newLaw = GenerateLaw();
// see if the law we add will replace a random existing law or be a new glitched order one
if (laws.Laws.Count > 0 && _robustRandom.Prob(target.ReplaceChance))
{
var i = _robustRandom.Next(laws.Laws.Count);
laws.Laws[i] = new SiliconLaw()
{
LawString = newLaw,
Order = laws.Laws[i].Order
};
}
else
{
laws.Laws.Insert(0, new SiliconLaw
{
LawString = newLaw,
Order = -1,
LawIdentifierOverride = Loc.GetString("ion-storm-law-scrambled-number", ("length", _robustRandom.Next(5, 10)))
});
}
// sets all unobfuscated laws' indentifier in order from highest to lowest priority
// This could technically override the Obfuscation from the code above, but it seems unlikely enough to basically never happen
int orderDeduction = -1;
for (int i = 0; i < laws.Laws.Count; i++)
{
var notNullIdentifier = laws.Laws[i].LawIdentifierOverride ?? (i - orderDeduction).ToString();
if (notNullIdentifier.Any(char.IsSymbol))
{
orderDeduction += 1;
}
else
{
laws.Laws[i].LawIdentifierOverride = (i - orderDeduction).ToString();
}
}
// adminlog is used to prevent adminlog spam.
if (adminlog)
_adminLogger.Add(LogType.Mind, LogImpact.High, $"{ToPrettyString(ent):silicon} had its laws changed by an ion storm to {laws.LoggingString()}");
// laws unique to this silicon, dont use station laws anymore
EnsureComp<SiliconLawProviderComponent>(ent);
var ev = new IonStormLawsEvent(laws);
RaiseLocalEvent(ent, ref ev);
}
// for your own sake direct your eyes elsewhere
private string GenerateLaw()
{
// pick all values ahead of time to make the logic cleaner
var threats = Pick(Threats);
var objects = Pick(Objects);
var crew1 = Pick(Crew);
var crew2 = Pick(Crew);
var adjective = Pick(Adjectives);
var verb = Pick(Verbs);
var number = Pick(NumberBase) + " " + Pick(NumberMod);
var area = Pick(Areas);
var feeling = Pick(Feelings);
var feelingPlural = Pick(FeelingsPlural);
var must = Pick(Musts);
var require = Pick(Requires);
var action = Pick(Actions);
var allergy = Pick(Allergies);
var allergySeverity = Pick(AllergySeverities);
var concept = Pick(Concepts);
var drink = Pick(Drinks);
var food = Pick(Foods);
var joined = $"{number} {adjective}";
// a lot of things have subjects of a threat/crew/object
var triple = _robustRandom.Next(0, 3) switch
{
0 => threats,
1 => crew1,
2 => objects,
_ => throw new IndexOutOfRangeException(),
};
var crewAll = _robustRandom.Prob(0.5f) ? crew2 : Loc.GetString("ion-storm-crew");
var objectsThreats = _robustRandom.Prob(0.5f) ? objects : threats;
var objectsConcept = _robustRandom.Prob(0.5f) ? objects : concept;
// s goes ahead of require, is/are
// i dont think theres a way to do this in fluent
var (who, plural) = _robustRandom.Next(0, 5) switch
{
0 => (Loc.GetString("ion-storm-you"), false),
1 => (Loc.GetString("ion-storm-the-station"), true),
2 => (Loc.GetString("ion-storm-the-crew"), true),
3 => (Loc.GetString("ion-storm-the-job", ("job", crew2)), false),
_ => (area, true) // THE SINGULARITY REQUIRES THE HAPPY CLOWNS
};
var jobChange = _robustRandom.Next(0, 3) switch
{
0 => crew1,
1 => Loc.GetString("ion-storm-clowns"),
_ => Loc.GetString("ion-storm-heads")
};
var part = Loc.GetString("ion-storm-part", ("part", _robustRandom.Prob(0.5f)));
var harm = _robustRandom.Next(0, 6) switch
{
0 => concept,
1 => $"{adjective} {threats}",
2 => $"{adjective} {objects}",
3 => Loc.GetString("ion-storm-adjective-things", ("adjective", adjective)),
4 => crew1,
_ => Loc.GetString("ion-storm-x-and-y", ("x", crew1), ("y", crew2))
};
if (plural) feeling = feelingPlural;
var subjects = _robustRandom.Prob(0.5f) ? objectsThreats : Loc.GetString("ion-storm-people");
// message logic!!!
return _robustRandom.Next(0, 35) switch
{
0 => Loc.GetString("ion-storm-law-on-station", ("joined", joined), ("subjects", triple)),
1 => Loc.GetString("ion-storm-law-no-shuttle", ("joined", joined), ("subjects", triple)),
2 => Loc.GetString("ion-storm-law-crew-are", ("who", crewAll), ("joined", joined), ("subjects", objectsThreats)),
3 => Loc.GetString("ion-storm-law-subjects-harmful", ("adjective", adjective), ("subjects", triple)),
4 => Loc.GetString("ion-storm-law-must-harmful", ("must", must)),
5 => Loc.GetString("ion-storm-law-thing-harmful", ("thing", _robustRandom.Prob(0.5f) ? concept : action)),
6 => Loc.GetString("ion-storm-law-job-harmful", ("adjective", adjective), ("job", crew1)),
7 => Loc.GetString("ion-storm-law-having-harmful", ("adjective", adjective), ("thing", objectsConcept)),
8 => Loc.GetString("ion-storm-law-not-having-harmful", ("adjective", adjective), ("thing", objectsConcept)),
9 => Loc.GetString("ion-storm-law-requires", ("who", who), ("plural", plural), ("thing", _robustRandom.Prob(0.5f) ? concept : require)),
10 => Loc.GetString("ion-storm-law-requires-subjects", ("who", who), ("plural", plural), ("joined", joined), ("subjects", triple)),
11 => Loc.GetString("ion-storm-law-allergic", ("who", who), ("plural", plural), ("severity", allergySeverity), ("allergy", _robustRandom.Prob(0.5f) ? concept : allergy)),
12 => Loc.GetString("ion-storm-law-allergic-subjects", ("who", who), ("plural", plural), ("severity", allergySeverity), ("adjective", adjective), ("subjects", _robustRandom.Prob(0.5f) ? objects : crew1)),
13 => Loc.GetString("ion-storm-law-feeling", ("who", who), ("feeling", feeling), ("concept", concept)),
14 => Loc.GetString("ion-storm-law-feeling-subjects", ("who", who), ("feeling", feeling), ("joined", joined), ("subjects", triple)),
15 => Loc.GetString("ion-storm-law-you-are", ("concept", concept)),
16 => Loc.GetString("ion-storm-law-you-are-subjects", ("joined", joined), ("subjects", triple)),
17 => Loc.GetString("ion-storm-law-you-must-always", ("must", must)),
18 => Loc.GetString("ion-storm-law-you-must-never", ("must", must)),
19 => Loc.GetString("ion-storm-law-eat", ("who", crewAll), ("adjective", adjective), ("food", _robustRandom.Prob(0.5f) ? food : triple)),
20 => Loc.GetString("ion-storm-law-drink", ("who", crewAll), ("adjective", adjective), ("drink", drink)),
21 => Loc.GetString("ion-storm-law-change-job", ("who", crewAll), ("adjective", adjective), ("change", jobChange)),
22 => Loc.GetString("ion-storm-law-highest-rank", ("who", crew1)),
23 => Loc.GetString("ion-storm-law-lowest-rank", ("who", crew1)),
24 => Loc.GetString("ion-storm-law-crew-must", ("who", crewAll), ("must", must)),
25 => Loc.GetString("ion-storm-law-crew-must-go", ("who", crewAll), ("area", area)),
26 => Loc.GetString("ion-storm-law-crew-only-1", ("who", crew1), ("part", part)),
27 => Loc.GetString("ion-storm-law-crew-only-2", ("who", crew1), ("other", crew2), ("part", part)),
28 => Loc.GetString("ion-storm-law-crew-only-subjects", ("adjective", adjective), ("subjects", subjects), ("part", part)),
29 => Loc.GetString("ion-storm-law-crew-must-do", ("must", must), ("part", part)),
30 => Loc.GetString("ion-storm-law-crew-must-have", ("adjective", adjective), ("objects", objects), ("part", part)),
31 => Loc.GetString("ion-storm-law-crew-must-eat", ("who", who), ("adjective", adjective), ("food", food), ("part", part)),
32 => Loc.GetString("ion-storm-law-harm", ("who", harm)),
33 => Loc.GetString("ion-storm-law-protect", ("who", harm)),
_ => Loc.GetString("ion-storm-law-concept-verb", ("concept", concept), ("verb", verb), ("subjects", triple))
};
}
/// <summary>
/// Picks a random value from an ion storm dataset.
/// All ion storm datasets start with IonStorm.
/// </summary>
private string Pick(string name)
{
var dataset = _proto.Index<DatasetPrototype>(name);
return _robustRandom.Pick(dataset.Values);
}
}

View File

@@ -1,7 +1,6 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Radio.Components;
using Content.Server.Roles;
using Content.Server.Station.Systems;
@@ -9,6 +8,7 @@ using Content.Shared.Administration;
using Content.Shared.Chat;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
@@ -17,12 +17,12 @@ using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Stunnable;
using Content.Shared.Wires;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Toolshed;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
namespace Content.Server.Silicons.Laws;
@@ -50,9 +50,9 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
SubscribeLocalEvent<SiliconLawProviderComponent, GetSiliconLawsEvent>(OnDirectedGetLaws);
SubscribeLocalEvent<SiliconLawProviderComponent, IonStormLawsEvent>(OnIonStormLaws);
SubscribeLocalEvent<SiliconLawProviderComponent, MindAddedMessage>(OnLawProviderMindAdded);
SubscribeLocalEvent<SiliconLawProviderComponent, MindRemovedMessage>(OnLawProviderMindRemoved);
SubscribeLocalEvent<SiliconLawProviderComponent, GotEmaggedEvent>(OnEmagLawsAdded);
SubscribeLocalEvent<EmagSiliconLawComponent, MindAddedMessage>(OnEmagMindAdded);
SubscribeLocalEvent<EmagSiliconLawComponent, MindRemovedMessage>(OnEmagMindRemoved);
}
private void OnMapInit(EntityUid uid, SiliconLawBoundComponent component, MapInitEvent args)
@@ -67,10 +67,35 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
var msg = Loc.GetString("laws-notify");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false,
actor.PlayerSession.Channel, colorOverride: Color.FromHex("#2ed2fd"));
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.FromHex("#2ed2fd"));
if (!TryComp<SiliconLawProviderComponent>(uid, out var lawcomp))
return;
if (!lawcomp.Subverted)
return;
var modifedLawMsg = Loc.GetString("laws-notify-subverted");
var modifiedLawWrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", modifedLawMsg));
_chatManager.ChatMessageToOne(ChatChannel.Server, modifedLawMsg, modifiedLawWrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.Red);
}
private void OnLawProviderMindAdded(Entity<SiliconLawProviderComponent> ent, ref MindAddedMessage args)
{
if (!ent.Comp.Subverted)
return;
EnsureSubvertedSiliconRole(args.Mind);
}
private void OnLawProviderMindRemoved(Entity<SiliconLawProviderComponent> ent, ref MindRemovedMessage args)
{
if (!ent.Comp.Subverted)
return;
RemoveSubvertedSiliconRole(args.Mind);
}
private void OnToggleLawsScreen(EntityUid uid, SiliconLawBoundComponent component, ToggleLawsScreenEvent args)
{
if (args.Handled || !TryComp<ActorComponent>(uid, out var actor))
@@ -117,9 +142,12 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
// gotta tell player to check their laws
NotifyLawsChanged(uid, component.LawUploadSound);
// Show the silicon has been subverted.
component.Subverted = true;
// new laws may allow antagonist behaviour so make it clear for admins
if (TryComp<EmagSiliconLawComponent>(uid, out var emag))
EnsureEmaggedRole(uid, emag);
if(_mind.TryGetMind(uid, out var mindId, out _))
EnsureSubvertedSiliconRole(mindId);
}
}
@@ -130,6 +158,9 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
if (component.Lawset == null)
component.Lawset = GetLawset(component.Laws);
// Show the silicon has been subverted.
component.Subverted = true;
// Add the first emag law before the others
component.Lawset?.Laws.Insert(0, new SiliconLaw
{
@@ -152,35 +183,25 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
base.OnGotEmagged(uid, component, ref args);
NotifyLawsChanged(uid, component.EmaggedSound);
EnsureEmaggedRole(uid, component);
if(_mind.TryGetMind(uid, out var mindId, out _))
EnsureSubvertedSiliconRole(mindId);
_stunSystem.TryParalyze(uid, component.StunTime, true);
}
private void OnEmagMindAdded(EntityUid uid, EmagSiliconLawComponent component, MindAddedMessage args)
private void EnsureSubvertedSiliconRole(EntityUid mindId)
{
if (HasComp<EmaggedComponent>(uid))
EnsureEmaggedRole(uid, component);
}
private void OnEmagMindRemoved(EntityUid uid, EmagSiliconLawComponent component, MindRemovedMessage args)
{
if (component.AntagonistRole == null)
return;
_roles.MindTryRemoveRole<SubvertedSiliconRoleComponent>(args.Mind);
}
private void EnsureEmaggedRole(EntityUid uid, EmagSiliconLawComponent component)
{
if (component.AntagonistRole == null || !_mind.TryGetMind(uid, out var mindId, out _))
return;
if (!_roles.MindHasRole<SubvertedSiliconRoleComponent>(mindId))
_roles.MindAddRole(mindId, "MindRoleSubvertedSilicon");
}
private void RemoveSubvertedSiliconRole(EntityUid mindId)
{
if (_roles.MindHasRole<SubvertedSiliconRoleComponent>(mindId))
_roles.MindTryRemoveRole<SubvertedSiliconRoleComponent>(mindId);
}
public SiliconLawset GetLaws(EntityUid uid, SiliconLawBoundComponent? component = null)
{
if (!Resolve(uid, ref component))

View File

@@ -1,10 +1,11 @@
using System.Linq;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Shared.Chat;
using Content.Shared.Mind;
using Content.Shared.Roles;
using Content.Shared.Silicons.StationAi;
using Content.Shared.StationAi;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
@@ -14,6 +15,8 @@ public sealed class StationAiSystem : SharedStationAiSystem
{
[Dependency] private readonly IChatManager _chats = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
private readonly HashSet<Entity<StationAiCoreComponent>> _ais = new();
@@ -43,6 +46,19 @@ public sealed class StationAiSystem : SharedStationAiSystem
return true;
}
public override void AnnounceIntellicardUsage(EntityUid uid, SoundSpecifier? cue = null)
{
if (!TryComp<ActorComponent>(uid, out var actor))
return;
var msg = Loc.GetString("ai-consciousness-download-warning");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
_chats.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.Red);
if (cue != null && _mind.TryGetMind(uid, out var mindId, out _))
_roles.MindPlaySound(mindId, cue);
}
private void AnnounceSnip(EntityUid entity)
{
var xform = Transform(entity);

View File

@@ -1,33 +0,0 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Content.Server.Singularity.EntitySystems;
namespace Content.Server.Singularity.Components;
[RegisterComponent]
public sealed partial class SingularityGeneratorComponent : Component
{
/// <summary>
/// The amount of power this generator has accumulated.
/// If you want to set this use <see cref="SingularityGeneratorSystem.SetPower"/>
/// </summary>
[DataField("power")]
[Access(friends:typeof(SingularityGeneratorSystem))]
public float Power = 0;
/// <summary>
/// The power threshold at which this generator will spawn a singularity.
/// If you want to set this use <see cref="SingularityGeneratorSystem.SetThreshold"/>
/// </summary>
[DataField("threshold")]
[Access(friends:typeof(SingularityGeneratorSystem))]
public float Threshold = 16;
/// <summary>
/// The prototype ID used to spawn a singularity.
/// </summary>
[DataField("spawnId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
[ViewVariables(VVAccess.ReadWrite)]
public string? SpawnPrototype = "Singularity";
}

View File

@@ -196,7 +196,8 @@ namespace Content.Server.Singularity.EntitySystems
if (TryComp<ApcPowerReceiverComponent>(uid, out var apcReceiver))
{
apcReceiver.Load = component.PowerUseActive;
PowerOn(uid, component);
if (apcReceiver.Powered)
PowerOn(uid, component);
}
// Do not directly PowerOn().
// OnReceivedPowerChanged will get fired due to DrawRate change which will turn it on.

View File

@@ -1,14 +1,23 @@
using Content.Server.ParticleAccelerator.Components;
using Content.Server.Singularity.Components;
using Content.Shared.Popups;
using Content.Shared.Singularity.Components;
using Content.Shared.Singularity.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Timing;
namespace Content.Server.Singularity.EntitySystems;
public sealed class SingularityGeneratorSystem : EntitySystem
public sealed class SingularityGeneratorSystem : SharedSingularityGeneratorSystem
{
#region Dependencies
[Dependency] private readonly IViewVariablesManager _vvm = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly PhysicsSystem _physics = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
#endregion Dependencies
public override void Initialize()
@@ -100,11 +109,37 @@ public sealed class SingularityGeneratorSystem : EntitySystem
/// <param name="args">The state of the beginning of the collision.</param>
private void HandleParticleCollide(EntityUid uid, ParticleProjectileComponent component, ref StartCollideEvent args)
{
if (EntityManager.TryGetComponent<SingularityGeneratorComponent>(args.OtherEntity, out var singularityGeneratorComponent))
if (!EntityManager.TryGetComponent<SingularityGeneratorComponent>(args.OtherEntity, out var generatorComp))
return;
if (_timing.CurTime < _metadata.GetPauseTime(uid) + generatorComp.NextFailsafe && !generatorComp.FailsafeDisabled)
{
EntityManager.QueueDeleteEntity(uid);
return;
}
var contained = true;
if (!generatorComp.FailsafeDisabled)
{
var transform = Transform(args.OtherEntity);
var directions = Enum.GetValues<Direction>().Length;
for (var i = 0; i < directions - 1; i += 2) // Skip every other direction, checking only cardinals
{
if (!CheckContainmentField((Direction)i, new Entity<SingularityGeneratorComponent>(args.OtherEntity, generatorComp), transform))
contained = false;
}
}
if (!contained && !generatorComp.FailsafeDisabled)
{
generatorComp.NextFailsafe = _timing.CurTime + generatorComp.FailsafeCooldown;
PopupSystem.PopupEntity(Loc.GetString("comp-generator-failsafe", ("target", args.OtherEntity)), args.OtherEntity, PopupType.LargeCaution);
}
else
{
SetPower(
args.OtherEntity,
singularityGeneratorComponent.Power + component.State switch
generatorComp.Power + component.State switch
{
ParticleAcceleratorPowerState.Standby => 0,
ParticleAcceleratorPowerState.Level0 => 1,
@@ -113,10 +148,46 @@ public sealed class SingularityGeneratorSystem : EntitySystem
ParticleAcceleratorPowerState.Level3 => 8,
_ => 0
},
singularityGeneratorComponent
generatorComp
);
EntityManager.QueueDeleteEntity(uid);
}
EntityManager.QueueDeleteEntity(uid);
}
#endregion Event Handlers
/// <summary>
/// Checks whether there's a containment field in a given direction away from the generator
/// </summary>
/// <param name="transform">The transform component of the singularity generator.</param>
/// <remarks>Mostly copied from <see cref="ContainmentFieldGeneratorSystem"/> </remarks>
private bool CheckContainmentField(Direction dir, Entity<SingularityGeneratorComponent> generator, TransformComponent transform)
{
var component = generator.Comp;
var (worldPosition, worldRotation) = _transformSystem.GetWorldPositionRotation(transform);
var dirRad = dir.ToAngle() + worldRotation;
var ray = new CollisionRay(worldPosition, dirRad.ToVec(), component.CollisionMask);
var rayCastResults = _physics.IntersectRay(transform.MapID, ray, component.FailsafeDistance, generator, false);
var genQuery = GetEntityQuery<ContainmentFieldComponent>();
RayCastResults? closestResult = null;
foreach (var result in rayCastResults)
{
if (genQuery.HasComponent(result.HitEntity))
closestResult = result;
break;
}
if (closestResult == null)
return false;
var ent = closestResult.Value.HitEntity;
// Check that the field can't be moved. The fields' transform parenting is weird, so skip that
return TryComp<PhysicsComponent>(ent, out var collidableComponent) && collidableComponent.BodyType == BodyType.Static;
}
}

View File

@@ -1,64 +1,14 @@
using System.Linq;
using Content.Server.Silicons.Laws;
using Content.Server.StationEvents.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Dataset;
using Content.Shared.FixedPoint;
using Content.Shared.GameTicking.Components;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.Silicons.Laws;
using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Station.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
public sealed class IonStormRule : StationEventSystem<IonStormRuleComponent>
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SiliconLawSystem _siliconLaw = default!;
// funny
[ValidatePrototypeId<DatasetPrototype>]
private const string Threats = "IonStormThreats";
[ValidatePrototypeId<DatasetPrototype>]
private const string Objects = "IonStormObjects";
[ValidatePrototypeId<DatasetPrototype>]
private const string Crew = "IonStormCrew";
[ValidatePrototypeId<DatasetPrototype>]
private const string Adjectives = "IonStormAdjectives";
[ValidatePrototypeId<DatasetPrototype>]
private const string Verbs = "IonStormVerbs";
[ValidatePrototypeId<DatasetPrototype>]
private const string NumberBase = "IonStormNumberBase";
[ValidatePrototypeId<DatasetPrototype>]
private const string NumberMod = "IonStormNumberMod";
[ValidatePrototypeId<DatasetPrototype>]
private const string Areas = "IonStormAreas";
[ValidatePrototypeId<DatasetPrototype>]
private const string Feelings = "IonStormFeelings";
[ValidatePrototypeId<DatasetPrototype>]
private const string FeelingsPlural = "IonStormFeelingsPlural";
[ValidatePrototypeId<DatasetPrototype>]
private const string Musts = "IonStormMusts";
[ValidatePrototypeId<DatasetPrototype>]
private const string Requires = "IonStormRequires";
[ValidatePrototypeId<DatasetPrototype>]
private const string Actions = "IonStormActions";
[ValidatePrototypeId<DatasetPrototype>]
private const string Allergies = "IonStormAllergies";
[ValidatePrototypeId<DatasetPrototype>]
private const string AllergySeverities = "IonStormAllergySeverities";
[ValidatePrototypeId<DatasetPrototype>]
private const string Concepts = "IonStormConcepts";
[ValidatePrototypeId<DatasetPrototype>]
private const string Drinks = "IonStormDrinks";
[ValidatePrototypeId<DatasetPrototype>]
private const string Foods = "IonStormFoods";
[Dependency] private readonly IonStormSystem _ionStorm = default!;
protected override void Started(EntityUid uid, IonStormRuleComponent comp, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
@@ -74,217 +24,7 @@ public sealed class IonStormRule : StationEventSystem<IonStormRuleComponent>
if (CompOrNull<StationMemberComponent>(xform.GridUid)?.Station != chosenStation)
continue;
if (!RobustRandom.Prob(target.Chance))
continue;
var laws = _siliconLaw.GetLaws(ent, lawBound);
if (laws.Laws.Count == 0)
continue;
// try to swap it out with a random lawset
if (RobustRandom.Prob(target.RandomLawsetChance))
{
var lawsets = PrototypeManager.Index<WeightedRandomPrototype>(target.RandomLawsets);
var lawset = lawsets.Pick(RobustRandom);
laws = _siliconLaw.GetLawset(lawset);
}
else
{
// clone it so not modifying stations lawset
laws = laws.Clone();
}
// shuffle them all
if (RobustRandom.Prob(target.ShuffleChance))
{
// hopefully work with existing glitched laws if there are multiple ion storms
FixedPoint2 baseOrder = FixedPoint2.New(1);
foreach (var law in laws.Laws)
{
if (law.Order < baseOrder)
baseOrder = law.Order;
}
RobustRandom.Shuffle(laws.Laws);
// change order based on shuffled position
for (int i = 0; i < laws.Laws.Count; i++)
{
laws.Laws[i].Order = baseOrder + i;
}
}
// see if we can remove a random law
if (laws.Laws.Count > 0 && RobustRandom.Prob(target.RemoveChance))
{
var i = RobustRandom.Next(laws.Laws.Count);
laws.Laws.RemoveAt(i);
}
// generate a new law...
var newLaw = GenerateLaw();
// see if the law we add will replace a random existing law or be a new glitched order one
if (laws.Laws.Count > 0 && RobustRandom.Prob(target.ReplaceChance))
{
var i = RobustRandom.Next(laws.Laws.Count);
laws.Laws[i] = new SiliconLaw()
{
LawString = newLaw,
Order = laws.Laws[i].Order
};
}
else
{
laws.Laws.Insert(0, new SiliconLaw
{
LawString = newLaw,
Order = -1,
LawIdentifierOverride = Loc.GetString("ion-storm-law-scrambled-number", ("length", RobustRandom.Next(5, 10)))
});
}
// sets all unobfuscated laws' indentifier in order from highest to lowest priority
// This could technically override the Obfuscation from the code above, but it seems unlikely enough to basically never happen
int orderDeduction = -1;
for (int i = 0; i < laws.Laws.Count; i++)
{
string notNullIdentifier = laws.Laws[i].LawIdentifierOverride ?? (i - orderDeduction).ToString();
if (notNullIdentifier.Any(char.IsSymbol))
{
orderDeduction += 1;
}
else
{
laws.Laws[i].LawIdentifierOverride = (i - orderDeduction).ToString();
}
}
_adminLogger.Add(LogType.Mind, LogImpact.High, $"{ToPrettyString(ent):silicon} had its laws changed by an ion storm to {laws.LoggingString()}");
// laws unique to this silicon, dont use station laws anymore
EnsureComp<SiliconLawProviderComponent>(ent);
var ev = new IonStormLawsEvent(laws);
RaiseLocalEvent(ent, ref ev);
_ionStorm.IonStormTarget((ent, lawBound, target));
}
}
// for your own sake direct your eyes elsewhere
private string GenerateLaw()
{
// pick all values ahead of time to make the logic cleaner
var threats = Pick(Threats);
var objects = Pick(Objects);
var crew1 = Pick(Crew);
var crew2 = Pick(Crew);
var adjective = Pick(Adjectives);
var verb = Pick(Verbs);
var number = Pick(NumberBase) + " " + Pick(NumberMod);
var area = Pick(Areas);
var feeling = Pick(Feelings);
var feelingPlural = Pick(FeelingsPlural);
var must = Pick(Musts);
var require = Pick(Requires);
var action = Pick(Actions);
var allergy = Pick(Allergies);
var allergySeverity = Pick(AllergySeverities);
var concept = Pick(Concepts);
var drink = Pick(Drinks);
var food = Pick(Foods);
var joined = $"{number} {adjective}";
// a lot of things have subjects of a threat/crew/object
var triple = RobustRandom.Next(0, 3) switch
{
0 => threats,
1 => crew1,
2 => objects,
_ => throw new IndexOutOfRangeException(),
};
var crewAll = RobustRandom.Prob(0.5f) ? crew2 : Loc.GetString("ion-storm-crew");
var objectsThreats = RobustRandom.Prob(0.5f) ? objects : threats;
var objectsConcept = RobustRandom.Prob(0.5f) ? objects : concept;
// s goes ahead of require, is/are
// i dont think theres a way to do this in fluent
var (who, plural) = RobustRandom.Next(0, 5) switch
{
0 => (Loc.GetString("ion-storm-you"), false),
1 => (Loc.GetString("ion-storm-the-station"), true),
2 => (Loc.GetString("ion-storm-the-crew"), true),
3 => (Loc.GetString("ion-storm-the-job", ("job", crew2)), false),
_ => (area, true) // THE SINGULARITY REQUIRES THE HAPPY CLOWNS
};
var jobChange = RobustRandom.Next(0, 3) switch
{
0 => crew1,
1 => Loc.GetString("ion-storm-clowns"),
_ => Loc.GetString("ion-storm-heads")
};
var part = Loc.GetString("ion-storm-part", ("part", RobustRandom.Prob(0.5f)));
var harm = RobustRandom.Next(0, 6) switch
{
0 => concept,
1 => $"{adjective} {threats}",
2 => $"{adjective} {objects}",
3 => Loc.GetString("ion-storm-adjective-things", ("adjective", adjective)),
4 => crew1,
_ => Loc.GetString("ion-storm-x-and-y", ("x", crew1), ("y", crew2))
};
if (plural) feeling = feelingPlural;
var subjects = RobustRandom.Prob(0.5f) ? objectsThreats : Loc.GetString("ion-storm-people");
// message logic!!!
return RobustRandom.Next(0, 36) switch
{
0 => Loc.GetString("ion-storm-law-on-station", ("joined", joined), ("subjects", triple)),
1 => Loc.GetString("ion-storm-law-no-shuttle", ("joined", joined), ("subjects", triple)),
2 => Loc.GetString("ion-storm-law-crew-are", ("who", crewAll), ("joined", joined), ("subjects", objectsThreats)),
3 => Loc.GetString("ion-storm-law-subjects-harmful", ("adjective", adjective), ("subjects", triple)),
4 => Loc.GetString("ion-storm-law-must-harmful", ("must", must)),
5 => Loc.GetString("ion-storm-law-thing-harmful", ("thing", RobustRandom.Prob(0.5f) ? concept : action)),
6 => Loc.GetString("ion-storm-law-job-harmful", ("adjective", adjective), ("job", crew1)),
7 => Loc.GetString("ion-storm-law-having-harmful", ("adjective", adjective), ("thing", objectsConcept)),
8 => Loc.GetString("ion-storm-law-not-having-harmful", ("adjective", adjective), ("thing", objectsConcept)),
9 => Loc.GetString("ion-storm-law-requires", ("who", who), ("plural", plural), ("thing", RobustRandom.Prob(0.5f) ? concept : require)),
10 => Loc.GetString("ion-storm-law-requires-subjects", ("who", who), ("plural", plural), ("joined", joined), ("subjects", triple)),
11 => Loc.GetString("ion-storm-law-allergic", ("who", who), ("plural", plural), ("severity", allergySeverity), ("allergy", RobustRandom.Prob(0.5f) ? concept : allergy)),
12 => Loc.GetString("ion-storm-law-allergic-subjects", ("who", who), ("plural", plural), ("severity", allergySeverity), ("adjective", adjective), ("subjects", RobustRandom.Prob(0.5f) ? objects : crew1)),
13 => Loc.GetString("ion-storm-law-feeling", ("who", who), ("feeling", feeling), ("concept", concept)),
14 => Loc.GetString("ion-storm-law-feeling-subjects", ("who", who), ("feeling", feeling), ("joined", joined), ("subjects", triple)),
15 => Loc.GetString("ion-storm-law-you-are", ("concept", concept)),
16 => Loc.GetString("ion-storm-law-you-are-subjects", ("joined", joined), ("subjects", triple)),
17 => Loc.GetString("ion-storm-law-you-must-always", ("must", must)),
18 => Loc.GetString("ion-storm-law-you-must-never", ("must", must)),
19 => Loc.GetString("ion-storm-law-eat", ("who", crewAll), ("adjective", adjective), ("food", RobustRandom.Prob(0.5f) ? food : triple)),
20 => Loc.GetString("ion-storm-law-drink", ("who", crewAll), ("adjective", adjective), ("drink", drink)),
22 => Loc.GetString("ion-storm-law-change-job", ("who", crewAll), ("adjective", adjective), ("change", jobChange)),
23 => Loc.GetString("ion-storm-law-highest-rank", ("who", crew1)),
24 => Loc.GetString("ion-storm-law-lowest-rank", ("who", crew1)),
25 => Loc.GetString("ion-storm-law-crew-must", ("who", crewAll), ("must", must)),
26 => Loc.GetString("ion-storm-law-crew-must-go", ("who", crewAll), ("area", area)),
27 => Loc.GetString("ion-storm-law-crew-only-1", ("who", crew1), ("part", part)),
28 => Loc.GetString("ion-storm-law-crew-only-2", ("who", crew1), ("other", crew2), ("part", part)),
29 => Loc.GetString("ion-storm-law-crew-only-subjects", ("adjective", adjective), ("subjects", subjects), ("part", part)),
30 => Loc.GetString("ion-storm-law-crew-must-do", ("must", must), ("part", part)),
31 => Loc.GetString("ion-storm-law-crew-must-have", ("adjective", adjective), ("objects", objects), ("part", part)),
32 => Loc.GetString("ion-storm-law-crew-must-eat", ("who", who), ("adjective", adjective), ("food", food), ("part", part)),
33 => Loc.GetString("ion-storm-law-harm", ("who", harm)),
34 => Loc.GetString("ion-storm-law-protect", ("who", harm)),
_ => Loc.GetString("ion-storm-law-concept-verb", ("concept", concept), ("verb", verb), ("subjects", triple))
};
}
/// <summary>
/// Picks a random value from an ion storm dataset.
/// All ion storm datasets start with IonStorm.
/// </summary>
private string Pick(string name)
{
var dataset = _proto.Index<DatasetPrototype>(name);
return RobustRandom.Pick(dataset.Values);
}
}

View File

@@ -1,9 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Content.Server.Access.Systems;
using Content.Server.Forensics;
using Content.Server.GameTicking;
using Content.Shared.Access.Components;
using Content.Shared.GameTicking;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Preferences;

View File

@@ -256,6 +256,11 @@ public sealed partial class StoreSystem
RaiseLocalEvent(buyer, listing.ProductEvent);
}
if (listing.DisableRefund)
{
component.RefundAllowed = false;
}
//log dat shit.
_admin.Add(LogType.StorePurchase,
LogImpact.Low,

View File

@@ -1,13 +1,13 @@
using Content.Server.Actions;
using Content.Server.GameTicking;
using Content.Shared.Actions;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Roles;
using Content.Shared.Traits;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
namespace Content.Server.Traits;

View File

@@ -1,4 +1,6 @@
using Content.Shared.Damage;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Map;
namespace Content.Server.Weapons.Ranged.Systems;
@@ -13,17 +15,28 @@ public sealed partial class GunSystem
*/
// Automatic firing without stopping if the AutoShootGunComponent component is exist and enabled
var query = EntityQueryEnumerator<AutoShootGunComponent, GunComponent>();
var query = EntityQueryEnumerator<GunComponent>();
while (query.MoveNext(out var uid, out var autoShoot, out var gun))
while (query.MoveNext(out var uid, out var gun))
{
if (!autoShoot.Enabled)
continue;
if (gun.NextFire > Timing.CurTime)
continue;
AttemptShoot(uid, gun);
if (TryComp(uid, out AutoShootGunComponent? autoShoot))
{
if (!autoShoot.Enabled)
continue;
AttemptShoot(uid, gun);
}
else if (gun.BurstActivated)
{
var parent = _transform.GetParentUid(uid);
if (HasComp<DamageableComponent>(parent))
AttemptShoot(parent, uid, gun, gun.ShootCoordinates ?? new EntityCoordinates(uid, gun.DefaultDirection));
else
AttemptShoot(uid, gun);
}
}
}
}

View File

@@ -1,15 +1,12 @@
using System.Linq;
using System.Numerics;
using Content.Server.Cargo.Systems;
using Content.Server.Interaction;
using Content.Server.Power.EntitySystems;
using Content.Server.Stunnable;
using Content.Server.Weapons.Ranged.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Effects;
using Content.Shared.Interaction.Components;
using Content.Shared.Projectiles;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged;
@@ -33,16 +30,13 @@ public sealed partial class GunSystem : SharedGunSystem
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly DamageExamineSystem _damageExamine = default!;
[Dependency] private readonly InteractionSystem _interaction = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StaminaSystem _stamina = default!;
[Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
private const float DamagePitchVariation = 0.05f;
public const float GunClumsyChance = 0.5f;
public override void Initialize()
{
@@ -71,26 +65,14 @@ public sealed partial class GunSystem : SharedGunSystem
{
userImpulse = true;
// Try a clumsy roll
// TODO: Who put this here
if (TryComp<ClumsyComponent>(user, out var clumsy) && gun.ClumsyProof == false)
if (user != null)
{
for (var i = 0; i < ammo.Count; i++)
var selfEvent = new SelfBeforeGunShotEvent(user.Value, (gunUid, gun), ammo);
RaiseLocalEvent(user.Value, selfEvent);
if (selfEvent.Cancelled)
{
if (_interaction.TryRollClumsy(user.Value, GunClumsyChance, clumsy))
{
// Wound them
Damageable.TryChangeDamage(user, clumsy.ClumsyDamage, origin: user);
_stun.TryParalyze(user.Value, TimeSpan.FromSeconds(3f), true);
// Apply salt to the wound ("Honk!")
Audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg"), gunUid);
Audio.PlayPvs(clumsy.ClumsySound, gunUid);
PopupSystem.PopupEntity(Loc.GetString("gun-clumsy"), user.Value);
userImpulse = false;
return;
}
userImpulse = false;
return;
}
}

View File

@@ -4,6 +4,7 @@ using Content.Shared.Weather;
using Robust.Shared.Console;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using System.Linq;
namespace Content.Server.Weather;
@@ -85,6 +86,7 @@ public sealed class WeatherSystem : SharedWeatherSystem
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), "Map Id");
var a = CompletionHelper.PrototypeIDs<WeatherPrototype>(true, ProtoMan);
return CompletionResult.FromHintOptions(a, Loc.GetString("cmd-weather-hint"));
var b = a.Concat(new[] { new CompletionOption("null", Loc.GetString("cmd-weather-null")) });
return CompletionResult.FromHintOptions(b, Loc.GetString("cmd-weather-hint"));
}
}

View File

@@ -4,6 +4,7 @@ using Content.Server._CP14.StationCommonObjectives;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.Mind;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;

View File

@@ -3,6 +3,7 @@ using Content.Server._CP14.GameTicking.Rules.Components;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.Mind;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;

View File

@@ -1,7 +1,7 @@
using Content.Server._CP14.StationCommonObjectives;
using Content.Server.GameTicking;
using Content.Server.Mind;
using Content.Server.Objectives.Components;
using Content.Shared.GameTicking;
using Content.Shared.Roles.Jobs;
using Robust.Server.Player;
using Robust.Shared.Prototypes;