Merge branch 'master' into ion-storm-refactor

This commit is contained in:
ScarKy0
2024-11-14 14:45:55 +01:00
committed by GitHub
1254 changed files with 59475 additions and 45491 deletions

View File

@@ -67,7 +67,7 @@ namespace Content.Server.Access.Systems
if (!TryComp<IdCardComponent>(uid, out var idCard))
return;
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.JobTitle ?? "", idCard.JobIcon);
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.LocalizedJobTitle ?? "", idCard.JobIcon);
_uiSystem.SetUiState(uid, AgentIDCardUiKey.Key, state);
}

View File

@@ -96,7 +96,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
PrivilegedIdIsAuthorized(uid, component),
true,
targetIdComponent.FullName,
targetIdComponent.JobTitle,
targetIdComponent.LocalizedJobTitle,
targetAccessComponent.Tags.ToList(),
possibleAccess,
jobProto,

View File

@@ -14,13 +14,13 @@ using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Asynchronous;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Managers;
@@ -45,14 +45,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
public const string SawmillId = "admin.bans";
public const string JobPrefix = "Job:";
private readonly Dictionary<NetUserId, HashSet<ServerRoleBanDef>> _cachedRoleBans = new();
private readonly Dictionary<ICommonSession, List<ServerRoleBanDef>> _cachedRoleBans = new();
// Cached ban exemption flags are used to handle
private readonly Dictionary<ICommonSession, ServerBanExemptFlags> _cachedBanExemptions = new();
public void Initialize()
{
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_netManager.RegisterNetMessage<MsgRoleBans>();
_db.SubscribeToNotifications(OnDatabaseNotification);
@@ -63,12 +61,23 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
private async Task CachePlayerData(ICommonSession player, CancellationToken cancel)
{
// Yeah so role ban loading code isn't integrated with exempt flag loading code.
// Have you seen how garbage role ban code code is? I don't feel like refactoring it right now.
var flags = await _db.GetBanExemption(player.UserId, cancel);
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 userRoleBans = new List<ServerRoleBanDef>();
foreach (var ban in roleBans)
{
userRoleBans.Add(ban);
}
cancel.ThrowIfCancellationRequested();
_cachedBanExemptions[player] = flags;
_cachedRoleBans[player] = userRoleBans;
SendRoleBans(player);
}
private void ClearPlayerData(ICommonSession player)
@@ -76,25 +85,15 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
_cachedBanExemptions.Remove(player);
}
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus != SessionStatus.Connected || _cachedRoleBans.ContainsKey(e.Session.UserId))
return;
var netChannel = e.Session.Channel;
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
await CacheDbRoleBans(e.Session.UserId, netChannel.RemoteEndPoint.Address, hwId);
SendRoleBans(e.Session);
}
private async Task<bool> AddRoleBan(ServerRoleBanDef banDef)
{
banDef = await _db.AddServerRoleBanAsync(banDef);
if (banDef.UserId != null)
if (banDef.UserId != null
&& _playerManager.TryGetSessionById(banDef.UserId, out var player)
&& _cachedRoleBans.TryGetValue(player, out var cachedBans))
{
_cachedRoleBans.GetOrNew(banDef.UserId.Value).Add(banDef);
cachedBans.Add(banDef);
}
return true;
@@ -102,31 +101,21 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
public HashSet<string>? GetRoleBans(NetUserId playerUserId)
{
return _cachedRoleBans.TryGetValue(playerUserId, out var roleBans)
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
return null;
return _cachedRoleBans.TryGetValue(session, out var roleBans)
? roleBans.Select(banDef => banDef.Role).ToHashSet()
: null;
}
private async Task CacheDbRoleBans(NetUserId userId, IPAddress? address = null, ImmutableArray<byte>? hwId = null)
{
var roleBans = await _db.GetServerRoleBansAsync(address, userId, hwId, false);
var userRoleBans = new HashSet<ServerRoleBanDef>();
foreach (var ban in roleBans)
{
userRoleBans.Add(ban);
}
_cachedRoleBans[userId] = userRoleBans;
}
public void Restart()
{
// Clear out players that have disconnected.
var toRemove = new List<NetUserId>();
var toRemove = new ValueList<ICommonSession>();
foreach (var player in _cachedRoleBans.Keys)
{
if (!_playerManager.TryGetSessionById(player, out _))
if (player.Status == SessionStatus.Disconnected)
toRemove.Add(player);
}
@@ -138,7 +127,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
// Check for expired bans
foreach (var roleBans in _cachedRoleBans.Values)
{
roleBans.RemoveWhere(ban => DateTimeOffset.Now > ban.ExpirationTime);
roleBans.RemoveAll(ban => DateTimeOffset.Now > ban.ExpirationTime);
}
}
@@ -281,9 +270,9 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires));
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-success", ("target", targetUsername ?? "null"), ("role", role), ("reason", reason), ("length", length)));
if (target != null)
if (target != null && _playerManager.TryGetSessionById(target.Value, out var session))
{
SendRoleBans(target.Value);
SendRoleBans(session);
}
}
@@ -311,10 +300,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
await _db.AddServerRoleUnbanAsync(new ServerRoleUnbanDef(banId, unbanningAdmin, DateTimeOffset.Now));
if (ban.UserId is { } player && _cachedRoleBans.TryGetValue(player, out var roleBans))
if (ban.UserId is { } player
&& _playerManager.TryGetSessionById(player, out var session)
&& _cachedRoleBans.TryGetValue(session, out var roleBans))
{
roleBans.RemoveWhere(roleBan => roleBan.Id == ban.Id);
SendRoleBans(player);
roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id);
SendRoleBans(session);
}
return $"Pardoned ban with id {banId}";
@@ -322,8 +313,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId)
{
if (!_cachedRoleBans.TryGetValue(playerUserId, out var roleBans))
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
return null;
if (!_cachedRoleBans.TryGetValue(session, out var roleBans))
return null;
return roleBans
.Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal))
.Select(ban => new ProtoId<JobPrototype>(ban.Role[JobPrefix.Length..]))
@@ -331,19 +326,9 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
}
#endregion
public void SendRoleBans(NetUserId userId)
{
if (!_playerManager.TryGetSessionById(userId, out var player))
{
return;
}
SendRoleBans(player);
}
public void SendRoleBans(ICommonSession pSession)
{
var roleBans = _cachedRoleBans.GetValueOrDefault(pSession.UserId) ?? new HashSet<ServerRoleBanDef>();
var roleBans = _cachedRoleBans.GetValueOrDefault(pSession) ?? new List<ServerRoleBanDef>();
var bans = new MsgRoleBans()
{
Bans = roleBans.Select(o => o.Role).ToList()

View File

@@ -47,12 +47,6 @@ public interface IBanManager
/// <param name="unbanTime">The time at which this role ban was pardoned.</param>
public Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime);
/// <summary>
/// Sends role bans to the target
/// </summary>
/// <param name="pSession">Player's user ID</param>
public void SendRoleBans(NetUserId userId);
/// <summary>
/// Sends role bans to the target
/// </summary>

View File

@@ -95,9 +95,10 @@ public sealed partial class AdminVerbSystem
if (HasComp<MapComponent>(args.Target) || HasComp<MapGridComponent>(args.Target))
return;
var explodeName = Loc.GetString("admin-smite-explode-name").ToLowerInvariant();
Verb explode = new()
{
Text = "admin-smite-explode-name",
Text = explodeName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/smite.svg.192dpi.png")),
Act = () =>
@@ -111,13 +112,14 @@ public sealed partial class AdminVerbSystem
_bodySystem.GibBody(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-explode-description")
Message = string.Join(": ", explodeName, Loc.GetString("admin-smite-explode-description")) // we do this so the description tells admins the Text to run it via console.
};
args.Verbs.Add(explode);
var chessName = Loc.GetString("admin-smite-chess-dimension-name").ToLowerInvariant();
Verb chess = new()
{
Text = "admin-smite-chess-dimension-name",
Text = chessName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Tabletop/chessboard.rsi"), "chessboard"),
Act = () =>
@@ -137,12 +139,13 @@ public sealed partial class AdminVerbSystem
xform.WorldRotation = Angle.Zero;
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-chess-dimension-description")
Message = string.Join(": ", chessName, Loc.GetString("admin-smite-chess-dimension-description"))
};
args.Verbs.Add(chess);
if (TryComp<FlammableComponent>(args.Target, out var flammable))
{
var flamesName = Loc.GetString("admin-smite-set-alight-name").ToLowerInvariant();
Verb flames = new()
{
Text = "admin-smite-set-alight-name",
@@ -160,14 +163,15 @@ public sealed partial class AdminVerbSystem
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-set-alight-description")
Message = string.Join(": ", flamesName, Loc.GetString("admin-smite-set-alight-description"))
};
args.Verbs.Add(flames);
}
var monkeyName = Loc.GetString("admin-smite-monkeyify-name").ToLowerInvariant();
Verb monkey = new()
{
Text = "admin-smite-monkeyify-name",
Text = monkeyName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Animals/monkey.rsi"), "monkey"),
Act = () =>
@@ -175,13 +179,14 @@ public sealed partial class AdminVerbSystem
_polymorphSystem.PolymorphEntity(args.Target, "AdminMonkeySmite");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-monkeyify-description")
Message = string.Join(": ", monkeyName, Loc.GetString("admin-smite-monkeyify-description"))
};
args.Verbs.Add(monkey);
var disposalBinName = Loc.GetString("admin-smite-garbage-can-name").ToLowerInvariant();
Verb disposalBin = new()
{
Text = "admin-smite-electrocute-name",
Text = disposalBinName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Piping/disposal.rsi"), "disposal"),
Act = () =>
@@ -189,16 +194,17 @@ public sealed partial class AdminVerbSystem
_polymorphSystem.PolymorphEntity(args.Target, "AdminDisposalsSmite");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-garbage-can-description")
Message = string.Join(": ", disposalBinName, Loc.GetString("admin-smite-garbage-can-description"))
};
args.Verbs.Add(disposalBin);
if (TryComp<DamageableComponent>(args.Target, out var damageable) &&
HasComp<MobStateComponent>(args.Target))
{
var hardElectrocuteName = Loc.GetString("admin-smite-electrocute-name").ToLowerInvariant();
Verb hardElectrocute = new()
{
Text = "admin-smite-creampie-name",
Text = hardElectrocuteName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Hands/Gloves/Color/yellow.rsi"), "icon"),
Act = () =>
@@ -234,16 +240,17 @@ public sealed partial class AdminVerbSystem
TimeSpan.FromSeconds(30), refresh: true, ignoreInsulation: true);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-electrocute-description")
Message = string.Join(": ", hardElectrocuteName, Loc.GetString("admin-smite-electrocute-description"))
};
args.Verbs.Add(hardElectrocute);
}
if (TryComp<CreamPiedComponent>(args.Target, out var creamPied))
{
var creamPieName = Loc.GetString("admin-smite-creampie-name").ToLowerInvariant();
Verb creamPie = new()
{
Text = "admin-smite-remove-blood-name",
Text = creamPieName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Consumable/Food/Baked/pie.rsi"), "plain-slice"),
Act = () =>
@@ -251,16 +258,17 @@ public sealed partial class AdminVerbSystem
_creamPieSystem.SetCreamPied(args.Target, creamPied, true);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-creampie-description")
Message = string.Join(": ", creamPieName, Loc.GetString("admin-smite-creampie-description"))
};
args.Verbs.Add(creamPie);
}
if (TryComp<BloodstreamComponent>(args.Target, out var bloodstream))
{
var bloodRemovalName = Loc.GetString("admin-smite-remove-blood-name").ToLowerInvariant();
Verb bloodRemoval = new()
{
Text = "admin-smite-vomit-organs-name",
Text = bloodRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Fluids/tomato_splat.rsi"), "puddle-1"),
Act = () =>
@@ -273,7 +281,7 @@ public sealed partial class AdminVerbSystem
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-remove-blood-description")
Message = string.Join(": ", bloodRemovalName, Loc.GetString("admin-smite-remove-blood-description"))
};
args.Verbs.Add(bloodRemoval);
}
@@ -281,9 +289,10 @@ public sealed partial class AdminVerbSystem
// bobby...
if (TryComp<BodyComponent>(args.Target, out var body))
{
var vomitOrgansName = Loc.GetString("admin-smite-vomit-organs-name").ToLowerInvariant();
Verb vomitOrgans = new()
{
Text = "admin-smite-remove-hands-name",
Text = vomitOrgansName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Fluids/vomit_toxin.rsi"), "vomit_toxin-1"),
Act = () =>
@@ -305,13 +314,14 @@ public sealed partial class AdminVerbSystem
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-vomit-organs-description")
Message = string.Join(": ", vomitOrgansName, Loc.GetString("admin-smite-vomit-organs-description"))
};
args.Verbs.Add(vomitOrgans);
var handsRemovalName = Loc.GetString("admin-smite-remove-hands-name").ToLowerInvariant();
Verb handsRemoval = new()
{
Text = "admin-smite-remove-hand-name",
Text = handsRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/remove-hands.png")),
Act = () =>
@@ -327,13 +337,14 @@ public sealed partial class AdminVerbSystem
Filter.PvsExcept(args.Target), true, PopupType.Medium);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-remove-hands-description")
Message = string.Join(": ", handsRemovalName, Loc.GetString("admin-smite-remove-hands-description"))
};
args.Verbs.Add(handsRemoval);
var handRemovalName = Loc.GetString("admin-smite-remove-hand-name").ToLowerInvariant();
Verb handRemoval = new()
{
Text = "admin-smite-pinball-name",
Text = handRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/remove-hand.png")),
Act = () =>
@@ -350,13 +361,14 @@ public sealed partial class AdminVerbSystem
Filter.PvsExcept(args.Target), true, PopupType.Medium);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-remove-hand-description")
Message = string.Join(": ", handRemovalName, Loc.GetString("admin-smite-remove-hand-description"))
};
args.Verbs.Add(handRemoval);
var stomachRemovalName = Loc.GetString("admin-smite-stomach-removal-name").ToLowerInvariant();
Verb stomachRemoval = new()
{
Text = "admin-smite-yeet-name",
Text = stomachRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "stomach"),
Act = () =>
@@ -370,13 +382,14 @@ public sealed partial class AdminVerbSystem
args.Target, PopupType.LargeCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-stomach-removal-description"),
Message = string.Join(": ", stomachRemovalName, Loc.GetString("admin-smite-stomach-removal-description"))
};
args.Verbs.Add(stomachRemoval);
var lungRemovalName = Loc.GetString("admin-smite-lung-removal-name").ToLowerInvariant();
Verb lungRemoval = new()
{
Text = "admin-smite-become-bread-name",
Text = lungRemovalName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "lung-r"),
Act = () =>
@@ -390,16 +403,17 @@ public sealed partial class AdminVerbSystem
args.Target, PopupType.LargeCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-lung-removal-description"),
Message = string.Join(": ", lungRemovalName, Loc.GetString("admin-smite-lung-removal-description"))
};
args.Verbs.Add(lungRemoval);
}
if (TryComp<PhysicsComponent>(args.Target, out var physics))
{
var pinballName = Loc.GetString("admin-smite-pinball-name").ToLowerInvariant();
Verb pinball = new()
{
Text = "admin-smite-ghostkick-name",
Text = pinballName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "basketball"),
Act = () =>
@@ -427,13 +441,14 @@ public sealed partial class AdminVerbSystem
_physics.SetAngularDamping(args.Target, physics, 0f);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-pinball-description")
Message = string.Join(": ", pinballName, Loc.GetString("admin-smite-pinball-description"))
};
args.Verbs.Add(pinball);
var yeetName = Loc.GetString("admin-smite-yeet-name").ToLowerInvariant();
Verb yeet = new()
{
Text = "admin-smite-nyanify-name",
Text = yeetName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")),
Act = () =>
@@ -457,11 +472,12 @@ public sealed partial class AdminVerbSystem
_physics.SetAngularDamping(args.Target, physics, 0f);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-yeet-description")
Message = string.Join(": ", yeetName, Loc.GetString("admin-smite-yeet-description"))
};
args.Verbs.Add(yeet);
}
var breadName = Loc.GetString("admin-smite-become-bread-name").ToLowerInvariant(); // Will I get cancelled for breadName-ing you?
Verb bread = new()
{
Text = "admin-smite-kill-sign-name",
@@ -472,10 +488,11 @@ public sealed partial class AdminVerbSystem
_polymorphSystem.PolymorphEntity(args.Target, "AdminBreadSmite");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-become-bread-description")
Message = string.Join(": ", breadName, Loc.GetString("admin-smite-become-bread-description"))
};
args.Verbs.Add(bread);
var mouseName = Loc.GetString("admin-smite-become-mouse-name").ToLowerInvariant();
Verb mouse = new()
{
Text = "admin-smite-cluwne-name",
@@ -486,15 +503,16 @@ public sealed partial class AdminVerbSystem
_polymorphSystem.PolymorphEntity(args.Target, "AdminMouseSmite");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-become-mouse-description")
Message = string.Join(": ", mouseName, Loc.GetString("admin-smite-become-mouse-description"))
};
args.Verbs.Add(mouse);
if (TryComp<ActorComponent>(args.Target, out var actorComponent))
{
var ghostKickName = Loc.GetString("admin-smite-ghostkick-name").ToLowerInvariant();
Verb ghostKick = new()
{
Text = "admin-smite-anger-pointing-arrows-name",
Text = ghostKickName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/gavel.svg.192dpi.png")),
Act = () =>
@@ -502,15 +520,18 @@ public sealed partial class AdminVerbSystem
_ghostKickManager.DoDisconnect(actorComponent.PlayerSession.Channel, "Smitten.");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-ghostkick-description")
Message = string.Join(": ", ghostKickName, Loc.GetString("admin-smite-ghostkick-description"))
};
args.Verbs.Add(ghostKick);
}
if (TryComp<InventoryComponent>(args.Target, out var inventory)) {
if (TryComp<InventoryComponent>(args.Target, out var inventory))
{
var nyanifyName = Loc.GetString("admin-smite-nyanify-name").ToLowerInvariant();
Verb nyanify = new()
{
Text = "admin-smite-dust-name",
Text = nyanifyName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Head/Hats/catears.rsi"), "icon"),
Act = () =>
@@ -521,13 +542,14 @@ public sealed partial class AdminVerbSystem
_inventorySystem.TryEquip(args.Target, ears, "head", true, true, false, inventory);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-nyanify-description")
Message = string.Join(": ", nyanifyName, Loc.GetString("admin-smite-nyanify-description"))
};
args.Verbs.Add(nyanify);
var killSignName = Loc.GetString("admin-smite-kill-sign-name").ToLowerInvariant();
Verb killSign = new()
{
Text = "admin-smite-buffering-name",
Text = killSignName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Misc/killsign.rsi"), "icon"),
Act = () =>
@@ -535,13 +557,14 @@ public sealed partial class AdminVerbSystem
EnsureComp<KillSignComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-kill-sign-description")
Message = string.Join(": ", killSignName, Loc.GetString("admin-smite-kill-sign-description"))
};
args.Verbs.Add(killSign);
var cluwneName = Loc.GetString("admin-smite-cluwne-name").ToLowerInvariant();
Verb cluwne = new()
{
Text = "admin-smite-become-instrument-name",
Text = cluwneName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Mask/cluwne.rsi"), "icon"),
@@ -551,13 +574,14 @@ public sealed partial class AdminVerbSystem
EnsureComp<CluwneComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-cluwne-description")
Message = string.Join(": ", cluwneName, Loc.GetString("admin-smite-cluwne-description"))
};
args.Verbs.Add(cluwne);
var maidenName = Loc.GetString("admin-smite-maid-name").ToLowerInvariant();
Verb maiden = new()
{
Text = "admin-smite-remove-gravity-name",
Text = maidenName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Uniforms/Jumpskirt/janimaid.rsi"), "icon"),
Act = () =>
@@ -570,14 +594,15 @@ public sealed partial class AdminVerbSystem
});
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-maid-description")
Message = string.Join(": ", maidenName, Loc.GetString("admin-smite-maid-description"))
};
args.Verbs.Add(maiden);
}
var angerPointingArrowsName = Loc.GetString("admin-smite-anger-pointing-arrows-name").ToLowerInvariant();
Verb angerPointingArrows = new()
{
Text = "admin-smite-reptilian-species-swap-name",
Text = angerPointingArrowsName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Interface/Misc/pointing.rsi"), "pointing"),
Act = () =>
@@ -585,13 +610,14 @@ public sealed partial class AdminVerbSystem
EnsureComp<PointingArrowAngeringComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-anger-pointing-arrows-description")
Message = string.Join(": ", angerPointingArrowsName, Loc.GetString("admin-smite-anger-pointing-arrows-description"))
};
args.Verbs.Add(angerPointingArrows);
var dustName = Loc.GetString("admin-smite-dust-name").ToLowerInvariant();
Verb dust = new()
{
Text = "admin-smite-locker-stuff-name",
Text = dustName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Materials/materials.rsi"), "ash"),
Act = () =>
@@ -601,13 +627,14 @@ public sealed partial class AdminVerbSystem
_popupSystem.PopupEntity(Loc.GetString("admin-smite-turned-ash-other", ("name", args.Target)), args.Target, PopupType.LargeCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-dust-description"),
Message = string.Join(": ", dustName, Loc.GetString("admin-smite-dust-description"))
};
args.Verbs.Add(dust);
var youtubeVideoSimulationName = Loc.GetString("admin-smite-buffering-name").ToLowerInvariant();
Verb youtubeVideoSimulation = new()
{
Text = "admin-smite-headstand-name",
Text = youtubeVideoSimulationName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Misc/buffering_smite_icon.png")),
Act = () =>
@@ -615,27 +642,29 @@ public sealed partial class AdminVerbSystem
EnsureComp<BufferingComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-buffering-description"),
Message = string.Join(": ", youtubeVideoSimulationName, Loc.GetString("admin-smite-buffering-description"))
};
args.Verbs.Add(youtubeVideoSimulation);
var instrumentationName = Loc.GetString("admin-smite-become-instrument-name").ToLowerInvariant();
Verb instrumentation = new()
{
Text = "admin-smite-become-mouse-name",
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "icon"),
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "supersynth"),
Act = () =>
{
_polymorphSystem.PolymorphEntity(args.Target, "AdminInstrumentSmite");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-become-instrument-description"),
Message = string.Join(": ", instrumentationName, Loc.GetString("admin-smite-become-instrument-description"))
};
args.Verbs.Add(instrumentation);
var noGravityName = Loc.GetString("admin-smite-remove-gravity-name").ToLowerInvariant();
Verb noGravity = new()
{
Text = "admin-smite-maid-name",
Text = noGravityName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Machines/gravity_generator.rsi"), "off"),
Act = () =>
@@ -646,13 +675,14 @@ public sealed partial class AdminVerbSystem
Dirty(args.Target, grav);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-remove-gravity-description"),
Message = string.Join(": ", noGravityName, Loc.GetString("admin-smite-remove-gravity-description"))
};
args.Verbs.Add(noGravity);
var reptilianName = Loc.GetString("admin-smite-reptilian-species-swap-name").ToLowerInvariant();
Verb reptilian = new()
{
Text = "admin-smite-zoom-in-name",
Text = reptilianName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "plushie_lizard"),
Act = () =>
@@ -660,13 +690,14 @@ public sealed partial class AdminVerbSystem
_polymorphSystem.PolymorphEntity(args.Target, "AdminLizardSmite");
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-reptilian-species-swap-description"),
Message = string.Join(": ", reptilianName, Loc.GetString("admin-smite-reptilian-species-swap-description"))
};
args.Verbs.Add(reptilian);
var lockerName = Loc.GetString("admin-smite-locker-stuff-name").ToLowerInvariant();
Verb locker = new()
{
Text = "admin-smite-flip-eye-name",
Text = lockerName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Storage/closet.rsi"), "generic"),
Act = () =>
@@ -682,10 +713,11 @@ public sealed partial class AdminVerbSystem
_weldableSystem.SetWeldedState(locker, true);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-locker-stuff-description"),
Message = string.Join(": ", lockerName, Loc.GetString("admin-smite-locker-stuff-description"))
};
args.Verbs.Add(locker);
var headstandName = Loc.GetString("admin-smite-headstand-name").ToLowerInvariant();
Verb headstand = new()
{
Text = "admin-smite-run-walk-swap-name",
@@ -696,13 +728,14 @@ public sealed partial class AdminVerbSystem
EnsureComp<HeadstandComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-headstand-description"),
Message = string.Join(": ", headstandName, Loc.GetString("admin-smite-headstand-description"))
};
args.Verbs.Add(headstand);
var zoomInName = Loc.GetString("admin-smite-zoom-in-name").ToLowerInvariant();
Verb zoomIn = new()
{
Text = "admin-smite-super-speed-name",
Text = zoomInName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/zoom.png")),
Act = () =>
@@ -711,13 +744,14 @@ public sealed partial class AdminVerbSystem
_eyeSystem.SetZoom(args.Target, eye.TargetZoom * 0.2f, ignoreLimits: true);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-zoom-in-description"),
Message = string.Join(": ", zoomInName, Loc.GetString("admin-smite-zoom-in-description"))
};
args.Verbs.Add(zoomIn);
var flipEyeName = Loc.GetString("admin-smite-flip-eye-name").ToLowerInvariant();
Verb flipEye = new()
{
Text = "admin-smite-stomach-removal-name",
Text = flipEyeName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/flip.png")),
Act = () =>
@@ -726,13 +760,14 @@ public sealed partial class AdminVerbSystem
_eyeSystem.SetZoom(args.Target, eye.TargetZoom * -1, ignoreLimits: true);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-flip-eye-description"),
Message = string.Join(": ", flipEyeName, Loc.GetString("admin-smite-flip-eye-description"))
};
args.Verbs.Add(flipEye);
var runWalkSwapName = Loc.GetString("admin-smite-run-walk-swap-name").ToLowerInvariant();
Verb runWalkSwap = new()
{
Text = "admin-smite-speak-backwards-name",
Text = runWalkSwapName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/run-walk-swap.png")),
Act = () =>
@@ -746,13 +781,14 @@ public sealed partial class AdminVerbSystem
args.Target, PopupType.LargeCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-run-walk-swap-description"),
Message = string.Join(": ", runWalkSwapName, Loc.GetString("admin-smite-run-walk-swap-description"))
};
args.Verbs.Add(runWalkSwap);
var backwardsAccentName = Loc.GetString("admin-smite-speak-backwards-name").ToLowerInvariant();
Verb backwardsAccent = new()
{
Text = "admin-smite-lung-removal-name",
Text = backwardsAccentName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/help-backwards.png")),
Act = () =>
@@ -760,13 +796,14 @@ public sealed partial class AdminVerbSystem
EnsureComp<BackwardsAccentComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-speak-backwards-description"),
Message = string.Join(": ", backwardsAccentName, Loc.GetString("admin-smite-speak-backwards-description"))
};
args.Verbs.Add(backwardsAccent);
var disarmProneName = Loc.GetString("admin-smite-disarm-prone-name").ToLowerInvariant();
Verb disarmProne = new()
{
Text = "admin-smite-disarm-prone-name",
Text = disarmProneName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Actions/disarm.png")),
Act = () =>
@@ -774,10 +811,11 @@ public sealed partial class AdminVerbSystem
EnsureComp<DisarmProneComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-disarm-prone-description"),
Message = string.Join(": ", disarmProneName, Loc.GetString("admin-smite-disarm-prone-description"))
};
args.Verbs.Add(disarmProne);
var superSpeedName = Loc.GetString("admin-smite-super-speed-name").ToLowerInvariant();
Verb superSpeed = new()
{
Text = "admin-smite-garbage-can-name",
@@ -792,41 +830,45 @@ public sealed partial class AdminVerbSystem
args.Target, PopupType.LargeCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-super-speed-description"),
Message = string.Join(": ", superSpeedName, Loc.GetString("admin-smite-super-speed-description"))
};
args.Verbs.Add(superSpeed);
//Bonk
var superBonkLiteName = Loc.GetString("admin-smite-super-bonk-lite-name").ToLowerInvariant();
Verb superBonkLite = new()
{
Text = "admin-smite-super-bonk-name",
Text = superBonkLiteName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("Structures/Furniture/Tables/glass.rsi"), "full"),
Act = () =>
{
_superBonkSystem.StartSuperBonk(args.Target, stopWhenDead: true);
},
Message = Loc.GetString("admin-smite-super-bonk-lite-description"),
Impact = LogImpact.Extreme,
Message = string.Join(": ", superBonkLiteName, Loc.GetString("admin-smite-super-bonk-lite-description"))
};
args.Verbs.Add(superBonkLite);
var superBonkName = Loc.GetString("admin-smite-super-bonk-name").ToLowerInvariant();
Verb superBonk= new()
{
Text = "admin-smite-super-bonk-lite-name",
Text = superBonkName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("Structures/Furniture/Tables/generic.rsi"), "full"),
Act = () =>
{
_superBonkSystem.StartSuperBonk(args.Target);
},
Message = Loc.GetString("admin-smite-super-bonk-description"),
Impact = LogImpact.Extreme,
Message = string.Join(": ", superBonkName, Loc.GetString("admin-smite-super-bonk-description"))
};
args.Verbs.Add(superBonk);
var superslipName = Loc.GetString("admin-smite-super-slip-name").ToLowerInvariant();
Verb superslip = new()
{
Text = "admin-smite-super-slip-name",
Text = superslipName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("Objects/Specific/Janitorial/soap.rsi"), "omega-4"),
Act = () =>
@@ -846,7 +888,7 @@ public sealed partial class AdminVerbSystem
}
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-super-slip-description")
Message = string.Join(": ", superslipName, Loc.GetString("admin-smite-super-slip-description"))
};
args.Verbs.Add(superslip);
}

View File

@@ -52,7 +52,6 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly StationJobsSystem _stationJobsSystem = default!;
[Dependency] private readonly JointSystem _jointSystem = default!;
[Dependency] private readonly BatterySystem _batterySystem = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly GunSystem _gun = default!;
@@ -327,7 +326,7 @@ public sealed partial class AdminVerbSystem
Act = () =>
{
var (mapUid, gridUid) = _adminTestArenaSystem.AssertArenaLoaded(player);
_xformSystem.SetCoordinates(args.Target, new EntityCoordinates(gridUid ?? mapUid, Vector2.One));
_transformSystem.SetCoordinates(args.Target, new EntityCoordinates(gridUid ?? mapUid, Vector2.One));
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-send-to-test-arena-description"),
@@ -533,7 +532,7 @@ public sealed partial class AdminVerbSystem
if (shuttle is null)
return;
_xformSystem.SetCoordinates(args.User, new EntityCoordinates(shuttle.Value, Vector2.Zero));
_transformSystem.SetCoordinates(args.User, new EntityCoordinates(shuttle.Value, Vector2.Zero));
},
Impact = LogImpact.Low,
Message = Loc.GetString("admin-trick-locate-cargo-shuttle-description"),

View File

@@ -35,10 +35,10 @@ using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
using System.Linq;
using Content.Server.Silicons.Laws;
using Content.Shared.Silicons.Laws;
using Content.Shared.Movement.Components;
using Content.Shared.Silicons.Laws.Components;
using Robust.Server.Player;
using Content.Shared.Mind;
using Content.Shared.Silicons.StationAi;
using Robust.Shared.Physics.Components;
using static Content.Shared.Configurable.ConfigurationComponent;
@@ -63,7 +63,6 @@ namespace Content.Server.Administration.Systems
[Dependency] private readonly ArtifactSystem _artifactSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly PrayerSystem _prayerSystem = default!;
[Dependency] private readonly EuiManager _eui = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly ToolshedManager _toolshed = default!;
[Dependency] private readonly RejuvenateSystem _rejuvenate = default!;
@@ -294,7 +293,7 @@ namespace Content.Server.Administration.Systems
Act = () =>
{
var ui = new AdminLogsEui();
_eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
ui.SetLogFilter(search:args.Target.Id.ToString());
},
Impact = LogImpact.Low
@@ -348,7 +347,30 @@ namespace Content.Server.Administration.Systems
Impact = LogImpact.Low
});
if (TryComp<SiliconLawBoundComponent>(args.Target, out var lawBoundComponent))
// This logic is needed to be able to modify the AI's laws through its core and eye.
EntityUid? target = null;
SiliconLawBoundComponent? lawBoundComponent = null;
if (TryComp(args.Target, out lawBoundComponent))
{
target = args.Target;
}
// When inspecting the core we can find the entity with its laws by looking at the AiHolderComponent.
else if (TryComp<StationAiHolderComponent>(args.Target, out var holder) && holder.Slot.Item != null
&& TryComp(holder.Slot.Item, out lawBoundComponent))
{
target = holder.Slot.Item.Value;
// For the eye we can find the entity with its laws as the source of the movement relay since the eye
// is just a proxy for it to move around and look around the station.
}
else if (TryComp<MovementRelayTargetComponent>(args.Target, out var relay)
&& TryComp(relay.Source, out lawBoundComponent))
{
target = relay.Source;
}
if (lawBoundComponent != null && target != null)
{
args.Verbs.Add(new Verb()
{
@@ -362,7 +384,7 @@ namespace Content.Server.Administration.Systems
return;
}
_euiManager.OpenEui(ui, session);
ui.UpdateLaws(lawBoundComponent, args.Target);
ui.UpdateLaws(lawBoundComponent, target.Value);
},
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"),
});

View File

@@ -15,6 +15,7 @@ using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Players.RateLimiting;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared;
@@ -46,20 +47,23 @@ namespace Content.Server.Administration.Systems
[GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")]
private static partial Regex DiscordRegex();
private ISawmill _sawmill = default!;
private readonly HttpClient _httpClient = new();
private string _webhookUrl = string.Empty;
private WebhookData? _webhookData;
private string _onCallUrl = string.Empty;
private WebhookData? _onCallData;
private ISawmill _sawmill = default!;
private readonly HttpClient _httpClient = new();
private string _footerIconUrl = string.Empty;
private string _avatarUrl = string.Empty;
private string _serverName = string.Empty;
private readonly
Dictionary<NetUserId, (string? id, string username, string description, string? characterName, GameRunLevel
lastRunLevel)> _relayMessages = new();
private readonly Dictionary<NetUserId, DiscordRelayInteraction> _relayMessages = new();
private Dictionary<NetUserId, string> _oldMessageIds = new();
private readonly Dictionary<NetUserId, Queue<string>> _messageQueues = new();
private readonly Dictionary<NetUserId, Queue<DiscordRelayedData>> _messageQueues = new();
private readonly HashSet<NetUserId> _processingChannels = new();
private readonly Dictionary<NetUserId, (TimeSpan Timestamp, bool Typing)> _typingUpdateTimestamps = new();
private string _overrideClientName = string.Empty;
@@ -81,12 +85,16 @@ namespace Content.Server.Administration.Systems
public override void Initialize()
{
base.Initialize();
Subs.CVar(_config, CCVars.DiscordOnCallWebhook, OnCallChanged, true);
Subs.CVar(_config, CCVars.DiscordAHelpWebhook, OnWebhookChanged, true);
Subs.CVar(_config, CCVars.DiscordAHelpFooterIcon, OnFooterIconChanged, true);
Subs.CVar(_config, CCVars.DiscordAHelpAvatar, OnAvatarChanged, true);
Subs.CVar(_config, CVars.GameHostName, OnServerNameChanged, true);
Subs.CVar(_config, CCVars.AdminAhelpOverrideClientName, OnOverrideChanged, true);
_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("AHELP");
var defaultParams = new AHelpMessageParams(
string.Empty,
string.Empty,
@@ -95,7 +103,7 @@ namespace Content.Server.Administration.Systems
_gameTicker.RunLevel,
playedSound: false
);
_maxAdditionalChars = GenerateAHelpMessage(defaultParams).Length;
_maxAdditionalChars = GenerateAHelpMessage(defaultParams).Message.Length;
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
@@ -104,12 +112,37 @@ namespace Content.Server.Administration.Systems
_rateLimit.Register(
RateLimitKey,
new RateLimitRegistration
{
CVarLimitPeriodLength = CCVars.AhelpRateLimitPeriod,
CVarLimitCount = CCVars.AhelpRateLimitCount,
PlayerLimitedAction = PlayerRateLimitedAction
});
new RateLimitRegistration(CCVars.AhelpRateLimitPeriod,
CCVars.AhelpRateLimitCount,
PlayerRateLimitedAction)
);
}
private async void OnCallChanged(string url)
{
_onCallUrl = url;
if (url == string.Empty)
return;
var match = DiscordRegex().Match(url);
if (!match.Success)
{
Log.Error("On call URL does not appear to be valid.");
return;
}
if (match.Groups.Count <= 2)
{
Log.Error("Could not get webhook ID or token for on call URL.");
return;
}
var webhookId = match.Groups[1].Value;
var webhookToken = match.Groups[2].Value;
_onCallData = await GetWebhookData(webhookId, webhookToken);
}
private void PlayerRateLimitedAction(ICommonSession obj)
@@ -260,13 +293,13 @@ namespace Content.Server.Administration.Systems
// Store the Discord message IDs of the previous round
_oldMessageIds = new Dictionary<NetUserId, string>();
foreach (var message in _relayMessages)
foreach (var (user, interaction) in _relayMessages)
{
var id = message.Value.id;
var id = interaction.Id;
if (id == null)
return;
_oldMessageIds[message.Key] = id;
_oldMessageIds[user] = id;
}
_relayMessages.Clear();
@@ -331,10 +364,10 @@ namespace Content.Server.Administration.Systems
var webhookToken = match.Groups[2].Value;
// Fire and forget
await SetWebhookData(webhookId, webhookToken);
_webhookData = await GetWebhookData(webhookId, webhookToken);
}
private async Task SetWebhookData(string id, string token)
private async Task<WebhookData?> GetWebhookData(string id, string token)
{
var response = await _httpClient.GetAsync($"https://discord.com/api/v10/webhooks/{id}/{token}");
@@ -343,10 +376,10 @@ namespace Content.Server.Administration.Systems
{
_sawmill.Log(LogLevel.Error,
$"Discord returned bad status code when trying to get webhook data (perhaps the webhook URL is invalid?): {response.StatusCode}\nResponse: {content}");
return;
return null;
}
_webhookData = JsonSerializer.Deserialize<WebhookData>(content);
return JsonSerializer.Deserialize<WebhookData>(content);
}
private void OnFooterIconChanged(string url)
@@ -359,14 +392,14 @@ namespace Content.Server.Administration.Systems
_avatarUrl = url;
}
private async void ProcessQueue(NetUserId userId, Queue<string> messages)
private async void ProcessQueue(NetUserId userId, Queue<DiscordRelayedData> messages)
{
// Whether an embed already exists for this player
var exists = _relayMessages.TryGetValue(userId, out var existingEmbed);
// Whether the message will become too long after adding these new messages
var tooLong = exists && messages.Sum(msg => Math.Min(msg.Length, MessageLengthCap) + "\n".Length)
+ existingEmbed.description.Length > DescriptionMax;
var tooLong = exists && messages.Sum(msg => Math.Min(msg.Message.Length, MessageLengthCap) + "\n".Length)
+ existingEmbed?.Description.Length > DescriptionMax;
// If there is no existing embed, or it is getting too long, we create a new embed
if (!exists || tooLong)
@@ -386,10 +419,10 @@ namespace Content.Server.Administration.Systems
// If we have all the data required, we can link to the embed of the previous round or embed that was too long
if (_webhookData is { GuildId: { } guildId, ChannelId: { } channelId })
{
if (tooLong && existingEmbed.id != null)
if (tooLong && existingEmbed?.Id != null)
{
linkToPrevious =
$"**[Go to previous embed of this round](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.id})**\n";
$"**[Go to previous embed of this round](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.Id})**\n";
}
else if (_oldMessageIds.TryGetValue(userId, out var id) && !string.IsNullOrEmpty(id))
{
@@ -399,13 +432,22 @@ namespace Content.Server.Administration.Systems
}
var characterName = _minds.GetCharacterName(userId);
existingEmbed = (null, lookup.Username, linkToPrevious, characterName, _gameTicker.RunLevel);
existingEmbed = new DiscordRelayInteraction()
{
Id = null,
CharacterName = characterName,
Description = linkToPrevious,
Username = lookup.Username,
LastRunLevel = _gameTicker.RunLevel,
};
_relayMessages[userId] = existingEmbed;
}
// Previous message was in another RunLevel, so show that in the embed
if (existingEmbed.lastRunLevel != _gameTicker.RunLevel)
if (existingEmbed!.LastRunLevel != _gameTicker.RunLevel)
{
existingEmbed.description += _gameTicker.RunLevel switch
existingEmbed.Description += _gameTicker.RunLevel switch
{
GameRunLevel.PreRoundLobby => "\n\n:arrow_forward: _**Pre-round lobby started**_\n",
GameRunLevel.InRound => "\n\n:arrow_forward: _**Round started**_\n",
@@ -414,26 +456,35 @@ namespace Content.Server.Administration.Systems
$"{_gameTicker.RunLevel} was not matched."),
};
existingEmbed.lastRunLevel = _gameTicker.RunLevel;
existingEmbed.LastRunLevel = _gameTicker.RunLevel;
}
// If last message of the new batch is SOS then relay it to on-call.
// ... as long as it hasn't been relayed already.
var discordMention = messages.Last();
var onCallRelay = !discordMention.Receivers && !existingEmbed.OnCall;
// Add available messages to the embed description
while (messages.TryDequeue(out var message))
{
// In case someone thinks they're funny
if (message.Length > MessageLengthCap)
message = message[..(MessageLengthCap - TooLongText.Length)] + TooLongText;
string text;
existingEmbed.description += $"\n{message}";
// In case someone thinks they're funny
if (message.Message.Length > MessageLengthCap)
text = message.Message[..(MessageLengthCap - TooLongText.Length)] + TooLongText;
else
text = message.Message;
existingEmbed.Description += $"\n{text}";
}
var payload = GeneratePayload(existingEmbed.description,
existingEmbed.username,
existingEmbed.characterName);
var payload = GeneratePayload(existingEmbed.Description,
existingEmbed.Username,
existingEmbed.CharacterName);
// If there is no existing embed, create a new one
// Otherwise patch (edit) it
if (existingEmbed.id == null)
if (existingEmbed.Id == null)
{
var request = await _httpClient.PostAsync($"{_webhookUrl}?wait=true",
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
@@ -456,11 +507,11 @@ namespace Content.Server.Administration.Systems
return;
}
existingEmbed.id = id.ToString();
existingEmbed.Id = id.ToString();
}
else
{
var request = await _httpClient.PatchAsync($"{_webhookUrl}/messages/{existingEmbed.id}",
var request = await _httpClient.PatchAsync($"{_webhookUrl}/messages/{existingEmbed.Id}",
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
if (!request.IsSuccessStatusCode)
@@ -475,6 +526,43 @@ namespace Content.Server.Administration.Systems
_relayMessages[userId] = existingEmbed;
// Actually do the on call relay last, we just need to grab it before we dequeue every message above.
if (onCallRelay &&
_onCallData != null)
{
existingEmbed.OnCall = true;
var roleMention = _config.GetCVar(CCVars.DiscordAhelpMention);
if (!string.IsNullOrEmpty(roleMention))
{
var message = new StringBuilder();
message.AppendLine($"<@&{roleMention}>");
message.AppendLine("Unanswered SOS");
// Need webhook data to get the correct link for that channel rather than on-call data.
if (_webhookData is { GuildId: { } guildId, ChannelId: { } channelId })
{
message.AppendLine(
$"**[Go to ahelp](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.Id})**");
}
payload = GeneratePayload(message.ToString(), existingEmbed.Username, existingEmbed.CharacterName);
var request = await _httpClient.PostAsync($"{_onCallUrl}?wait=true",
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
var content = await request.Content.ReadAsStringAsync();
if (!request.IsSuccessStatusCode)
{
_sawmill.Log(LogLevel.Error, $"Discord returned bad status code when posting relay message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}");
}
}
}
else
{
existingEmbed.OnCall = false;
}
_processingChannels.Remove(userId);
}
@@ -653,7 +741,7 @@ namespace Content.Server.Administration.Systems
if (sendsWebhook)
{
if (!_messageQueues.ContainsKey(msg.UserId))
_messageQueues[msg.UserId] = new Queue<string>();
_messageQueues[msg.UserId] = new Queue<DiscordRelayedData>();
var str = message.Text;
var unameLength = senderSession.Name.Length;
@@ -702,7 +790,7 @@ namespace Content.Server.Administration.Systems
.ToList();
}
private static string GenerateAHelpMessage(AHelpMessageParams parameters)
private static DiscordRelayedData GenerateAHelpMessage(AHelpMessageParams parameters)
{
var stringbuilder = new StringBuilder();
@@ -719,13 +807,57 @@ namespace Content.Server.Administration.Systems
stringbuilder.Append($" **{parameters.RoundTime}**");
if (!parameters.PlayedSound)
stringbuilder.Append(" **(S)**");
if (parameters.Icon == null)
stringbuilder.Append($" **{parameters.Username}:** ");
else
stringbuilder.Append($" **{parameters.Username}** ");
stringbuilder.Append(parameters.Message);
return stringbuilder.ToString();
return new DiscordRelayedData()
{
Receivers = !parameters.NoReceivers,
Message = stringbuilder.ToString(),
};
}
private record struct DiscordRelayedData
{
/// <summary>
/// Was anyone online to receive it.
/// </summary>
public bool Receivers;
/// <summary>
/// What's the payload to send to discord.
/// </summary>
public string Message;
}
/// <summary>
/// Class specifically for holding information regarding existing Discord embeds
/// </summary>
private sealed class DiscordRelayInteraction
{
public string? Id;
public string Username = String.Empty;
public string? CharacterName;
/// <summary>
/// Contents for the discord message.
/// </summary>
public string Description = string.Empty;
/// <summary>
/// Run level of the last interaction. If different we'll link to the last Id.
/// </summary>
public GameRunLevel LastRunLevel;
/// <summary>
/// Did we relay this interaction to OnCall previously.
/// </summary>
public bool OnCall;
}
}

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

@@ -55,7 +55,7 @@ public sealed partial class AmeControllerComponent : SharedAmeControllerComponen
/// </summary>
[DataField("injectSound")]
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier InjectSound = new SoundCollectionSpecifier("MetalThud");
public SoundSpecifier InjectSound = new SoundPathSpecifier("/Audio/Machines/ame_fuelinjection.ogg");
/// <summary>
/// The last time this could have injected fuel into the AME.

View File

@@ -22,11 +22,17 @@ public sealed class TechAnomalySystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<TechAnomalyComponent, MapInitEvent>(OnTechMapInit);
SubscribeLocalEvent<TechAnomalyComponent, AnomalyPulseEvent>(OnPulse);
SubscribeLocalEvent<TechAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical);
SubscribeLocalEvent<TechAnomalyComponent, AnomalyStabilityChangedEvent>(OnStabilityChanged);
}
private void OnTechMapInit(Entity<TechAnomalyComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextTimer = _timing.CurTime;
}
public override void Update(float frameTime)
{
base.Update(frameTime);

View File

@@ -11,7 +11,6 @@ using Content.Server.Preferences.Managers;
using Content.Server.Roles;
using Content.Server.Roles.Jobs;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Systems;
using Content.Shared.Antag;
using Content.Shared.Clothing;
using Content.Shared.GameTicking;
@@ -20,7 +19,6 @@ using Content.Shared.Ghost;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Whitelist;
using Robust.Server.Audio;
@@ -37,14 +35,14 @@ namespace Content.Server.Antag;
public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
{
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerPreferencesManager _pref = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly GhostRoleSystem _ghostRole = default!;
[Dependency] private readonly JobSystem _jobs = default!;
[Dependency] private readonly LoadoutSystem _loadout = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerPreferencesManager _pref = default!;
[Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
@@ -57,6 +55,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
base.Initialize();
Log.Level = LogLevel.Debug;
SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
SubscribeLocalEvent<AntagSelectionComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
@@ -193,6 +193,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary>
/// Chooses antagonists from the given selection of players
/// </summary>
/// <param name="ent">The antagonist rule entity</param>
/// <param name="pool">The players to choose from</param>
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
{
if (ent.Comp.SelectionsComplete)
@@ -209,8 +212,14 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary>
/// Chooses antagonists from the given selection of players for the given antag definition.
/// </summary>
/// <param name="ent">The antagonist rule entity</param>
/// <param name="pool">The players to choose from</param>
/// <param name="def">The antagonist selection parameters and criteria</param>
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, AntagSelectionDefinition def, bool midround = false)
public void ChooseAntags(Entity<AntagSelectionComponent> ent,
IList<ICommonSession> pool,
AntagSelectionDefinition def,
bool midround = false)
{
var playerPool = GetPlayerPool(ent, pool, def);
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def);
@@ -331,7 +340,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
EntityManager.AddComponents(player, def.Components);
// Equip the entity's RoleLoadout and LoadoutGroup
List<ProtoId<StartingGearPrototype>>? gear = new();
List<ProtoId<StartingGearPrototype>> gear = new();
if (def.StartingGear is not null)
gear.Add(def.StartingGear.Value);
@@ -340,8 +349,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (session != null)
{
var curMind = session.GetMind();
if (curMind == null ||
if (curMind == null ||
!TryComp<MindComponent>(curMind.Value, out var mindComp) ||
mindComp.OwnedEntity != antagEnt)
{
@@ -350,9 +359,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
}
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
_role.MindAddRoles(curMind.Value, def.MindComponents, null, true);
_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

@@ -3,7 +3,6 @@ using Content.Shared.Antag;
using Content.Shared.Destructible.Thresholds;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Storage;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Player;
@@ -145,10 +144,17 @@ public partial struct AntagSelectionDefinition()
/// <summary>
/// Components added to the player's mind.
/// Do NOT use this to add role-type components. Add those as MindRoles instead
/// </summary>
[DataField]
public ComponentRegistry MindComponents = new();
/// <summary>
/// List of Mind Role Prototypes to be added to the player's mind.
/// </summary>
[DataField]
public List<ProtoId<EntityPrototype>>? MindRoles;
/// <summary>
/// A set of starting gear that's equipped to the player.
/// </summary>

View File

@@ -1,9 +1,11 @@
using Robust.Shared.Prototypes;
namespace Content.Server.Atmos.Components
{
/// <summary>
/// Used by FixGridAtmos. Entities with this may get magically auto-deleted on map initialization in future.
/// </summary>
[RegisterComponent]
[RegisterComponent, EntityCategory("Mapping")]
public sealed partial class AtmosFixMarkerComponent : Component
{
// See FixGridAtmos for more details

View File

@@ -1,15 +1,19 @@
using Content.Server.Atmos.Monitor.Components;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Pinpointer;
using Content.Server.Power.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Consoles;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Pinpointer;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@@ -21,6 +25,12 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
[Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!;
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly NavMapSystem _navMapSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly DeviceListSystem _deviceListSystem = default!;
private const float UpdateTime = 1.0f;
@@ -38,6 +48,9 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
// Grid events
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
// Alarm events
SubscribeLocalEvent<AtmosAlertsDeviceComponent, EntityTerminatingEvent>(OnDeviceTerminatingEvent);
SubscribeLocalEvent<AtmosAlertsDeviceComponent, AnchorStateChangedEvent>(OnDeviceAnchorChanged);
}
@@ -81,6 +94,16 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
}
private void OnDeviceAnchorChanged(EntityUid uid, AtmosAlertsDeviceComponent component, AnchorStateChangedEvent args)
{
OnDeviceAdditionOrRemoval(uid, component, args.Anchored);
}
private void OnDeviceTerminatingEvent(EntityUid uid, AtmosAlertsDeviceComponent component, ref EntityTerminatingEvent args)
{
OnDeviceAdditionOrRemoval(uid, component, false);
}
private void OnDeviceAdditionOrRemoval(EntityUid uid, AtmosAlertsDeviceComponent component, bool isAdding)
{
var xform = Transform(uid);
var gridUid = xform.GridUid;
@@ -88,10 +111,13 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
if (gridUid == null)
return;
if (!TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data))
if (!TryComp<NavMapComponent>(xform.GridUid, out var navMap))
return;
var netEntity = EntityManager.GetNetEntity(uid);
if (!TryGetAtmosDeviceNavMapData(uid, component, xform, out var data))
return;
var netEntity = GetNetEntity(uid);
var query = AllEntityQuery<AtmosAlertsComputerComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
@@ -99,11 +125,18 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
if (gridUid != entXform.GridUid)
continue;
if (args.Anchored)
if (isAdding)
{
entConsole.AtmosDevices.Add(data.Value);
}
else if (!args.Anchored)
else
{
entConsole.AtmosDevices.RemoveWhere(x => x.NetEntity == netEntity);
_navMapSystem.RemoveNavMapRegion(gridUid.Value, navMap, netEntity);
}
Dirty(ent, entConsole);
}
}
@@ -209,6 +242,12 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
if (entDevice.Group != group)
continue;
if (!TryComp<MapGridComponent>(entXform.GridUid, out var mapGrid))
continue;
if (!TryComp<NavMapComponent>(entXform.GridUid, out var navMap))
continue;
// If emagged, change the alarm type to normal
var alarmState = (entAtmosAlarmable.LastAlarmState == AtmosAlarmType.Emagged) ? AtmosAlarmType.Normal : entAtmosAlarmable.LastAlarmState;
@@ -216,14 +255,45 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
if (TryComp<ApcPowerReceiverComponent>(ent, out var entAPCPower) && !entAPCPower.Powered)
alarmState = AtmosAlarmType.Invalid;
// Create entry
var netEnt = GetNetEntity(ent);
var entry = new AtmosAlertsComputerEntry
(GetNetEntity(ent),
(netEnt,
GetNetCoordinates(entXform.Coordinates),
entDevice.Group,
alarmState,
MetaData(ent).EntityName,
entDeviceNetwork.Address);
// Get the list of sensors attached to the alarm
var sensorList = TryComp<DeviceListComponent>(ent, out var entDeviceList) ? _deviceListSystem.GetDeviceList(ent, entDeviceList) : null;
if (sensorList?.Any() == true)
{
var alarmRegionSeeds = new HashSet<Vector2i>();
// If valid and anchored, use the position of sensors as seeds for the region
foreach (var (address, sensorEnt) in sensorList)
{
if (!sensorEnt.IsValid() || !HasComp<AtmosMonitorComponent>(sensorEnt))
continue;
var sensorXform = Transform(sensorEnt);
if (sensorXform.Anchored && sensorXform.GridUid == entXform.GridUid)
alarmRegionSeeds.Add(_mapSystem.CoordinatesToTile(entXform.GridUid.Value, mapGrid, _transformSystem.GetMapCoordinates(sensorEnt, sensorXform)));
}
var regionProperties = new SharedNavMapSystem.NavMapRegionProperties(netEnt, AtmosAlertsComputerUiKey.Key, alarmRegionSeeds);
_navMapSystem.AddOrUpdateNavMapRegion(gridUid, navMap, netEnt, regionProperties);
}
else
{
_navMapSystem.RemoveNavMapRegion(entXform.GridUid.Value, navMap, netEnt);
}
alarmStateData.Add(entry);
}
@@ -306,7 +376,10 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
var query = AllEntityQuery<AtmosAlertsDeviceComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entComponent, out var entXform))
{
if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, gridUid, out var data))
if (entXform.GridUid != gridUid)
continue;
if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, out var data))
atmosDeviceNavMapData.Add(data.Value);
}
@@ -317,14 +390,10 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
(EntityUid uid,
AtmosAlertsDeviceComponent component,
TransformComponent xform,
EntityUid gridUid,
[NotNullWhen(true)] out AtmosAlertsDeviceNavMapData? output)
{
output = null;
if (xform.GridUid != gridUid)
return false;
if (!xform.Anchored)
return false;

View File

@@ -281,6 +281,17 @@ namespace Content.Server.Atmos.EntitySystems
return true;
}
/// <summary>
/// Compares two TileAtmospheres to see if they are within acceptable ranges for group processing to be enabled.
/// </summary>
public GasCompareResult CompareExchange(TileAtmosphere sample, TileAtmosphere otherSample)
{
if (sample.AirArchived == null || otherSample.AirArchived == null)
return GasCompareResult.NoExchange;
return CompareExchange(sample.AirArchived, otherSample.AirArchived);
}
/// <summary>
/// Compares two gas mixtures to see if they are within acceptable ranges for group processing to be enabled.
/// </summary>

View File

@@ -270,7 +270,7 @@ public sealed partial class AtmosphereSystem
{
DebugTools.AssertNotNull(tile.Air);
DebugTools.Assert(tile.Air?.Immutable == false);
Array.Clear(tile.MolesArchived);
tile.AirArchived = null;
tile.ArchivedCycle = 0;
var count = 0;

View File

@@ -210,7 +210,7 @@ namespace Content.Server.Atmos.EntitySystems
MovedByPressureComponent.ProbabilityOffset);
// Can we yeet the thing (due to probability, strength, etc.)
if (moveProb > MovedByPressureComponent.ProbabilityOffset && _robustRandom.Prob(MathF.Min(moveProb / 100f, 1f))
if (moveProb > MovedByPressureComponent.ProbabilityOffset && _random.Prob(MathF.Min(moveProb / 100f, 1f))
&& !float.IsPositiveInfinity(component.MoveResist)
&& (physics.BodyType != BodyType.Static
&& (maxForce >= (component.MoveResist * MovedByPressureComponent.MoveForcePushRatio)))

View File

@@ -54,7 +54,7 @@ namespace Content.Server.Atmos.EntitySystems
}
shouldShareAir = true;
} else if (CompareExchange(tile.Air, enemyTile.Air) != GasCompareResult.NoExchange)
} else if (CompareExchange(tile, enemyTile) != GasCompareResult.NoExchange)
{
AddActiveTile(gridAtmosphere, enemyTile);
if (ExcitedGroups)
@@ -117,9 +117,8 @@ namespace Content.Server.Atmos.EntitySystems
private void Archive(TileAtmosphere tile, int fireCount)
{
if (tile.Air != null)
tile.Air.Moles.AsSpan().CopyTo(tile.MolesArchived.AsSpan());
tile.AirArchived = new GasMixture(tile.Air);
tile.TemperatureArchived = tile.Temperature;
tile.ArchivedCycle = fireCount;
}
@@ -184,10 +183,10 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float GetHeatCapacityArchived(TileAtmosphere tile)
{
if (tile.Air == null)
if (tile.AirArchived == null)
return tile.HeatCapacity;
return GetHeatCapacityCalculation(tile.MolesArchived!, tile.Space);
return GetHeatCapacity(tile.AirArchived);
}
/// <summary>
@@ -195,10 +194,11 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float Share(TileAtmosphere tileReceiver, TileAtmosphere tileSharer, int atmosAdjacentTurfs)
{
if (tileReceiver.Air is not {} receiver || tileSharer.Air is not {} sharer)
if (tileReceiver.Air is not {} receiver || tileSharer.Air is not {} sharer ||
tileReceiver.AirArchived == null || tileSharer.AirArchived == null)
return 0f;
var temperatureDelta = tileReceiver.TemperatureArchived - tileSharer.TemperatureArchived;
var temperatureDelta = tileReceiver.AirArchived.Temperature - tileSharer.AirArchived.Temperature;
var absTemperatureDelta = Math.Abs(temperatureDelta);
var oldHeatCapacity = 0f;
var oldSharerHeatCapacity = 0f;
@@ -249,12 +249,12 @@ namespace Content.Server.Atmos.EntitySystems
// Transfer of thermal energy (via changed heat capacity) between self and sharer.
if (!receiver.Immutable && newHeatCapacity > Atmospherics.MinimumHeatCapacity)
{
receiver.Temperature = ((oldHeatCapacity * receiver.Temperature) - (heatCapacityToSharer * tileReceiver.TemperatureArchived) + (heatCapacitySharerToThis * tileSharer.TemperatureArchived)) / newHeatCapacity;
receiver.Temperature = ((oldHeatCapacity * receiver.Temperature) - (heatCapacityToSharer * tileReceiver.AirArchived.Temperature) + (heatCapacitySharerToThis * tileSharer.AirArchived.Temperature)) / newHeatCapacity;
}
if (!sharer.Immutable && newSharerHeatCapacity > Atmospherics.MinimumHeatCapacity)
{
sharer.Temperature = ((oldSharerHeatCapacity * sharer.Temperature) - (heatCapacitySharerToThis * tileSharer.TemperatureArchived) + (heatCapacityToSharer * tileReceiver.TemperatureArchived)) / newSharerHeatCapacity;
sharer.Temperature = ((oldSharerHeatCapacity * sharer.Temperature) - (heatCapacitySharerToThis * tileSharer.AirArchived.Temperature) + (heatCapacityToSharer * tileReceiver.AirArchived.Temperature)) / newSharerHeatCapacity;
}
// Thermal energy of the system (self and sharer) is unchanged.
@@ -273,7 +273,7 @@ namespace Content.Server.Atmos.EntitySystems
var moles = receiver.TotalMoles;
var theirMoles = sharer.TotalMoles;
return (tileReceiver.TemperatureArchived * (moles + movedMoles)) - (tileSharer.TemperatureArchived * (theirMoles - movedMoles)) * Atmospherics.R / receiver.Volume;
return (tileReceiver.AirArchived.Temperature * (moles + movedMoles)) - (tileSharer.AirArchived.Temperature * (theirMoles - movedMoles)) * Atmospherics.R / receiver.Volume;
}
/// <summary>
@@ -281,10 +281,11 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float TemperatureShare(TileAtmosphere tileReceiver, TileAtmosphere tileSharer, float conductionCoefficient)
{
if (tileReceiver.Air is not { } receiver || tileSharer.Air is not { } sharer)
if (tileReceiver.Air is not { } receiver || tileSharer.Air is not { } sharer ||
tileReceiver.AirArchived == null || tileSharer.AirArchived == null)
return 0f;
var temperatureDelta = tileReceiver.TemperatureArchived - tileSharer.TemperatureArchived;
var temperatureDelta = tileReceiver.AirArchived.Temperature - tileSharer.AirArchived.Temperature;
if (MathF.Abs(temperatureDelta) > Atmospherics.MinimumTemperatureDeltaToConsider)
{
var heatCapacity = GetHeatCapacityArchived(tileReceiver);
@@ -310,10 +311,10 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public float TemperatureShare(TileAtmosphere tileReceiver, float conductionCoefficient, float sharerTemperature, float sharerHeatCapacity)
{
if (tileReceiver.Air is not {} receiver)
if (tileReceiver.Air is not {} receiver || tileReceiver.AirArchived == null)
return 0;
var temperatureDelta = tileReceiver.TemperatureArchived - sharerTemperature;
var temperatureDelta = tileReceiver.AirArchived.Temperature - sharerTemperature;
if (MathF.Abs(temperatureDelta) > Atmospherics.MinimumTemperatureDeltaToConsider)
{
var heatCapacity = GetHeatCapacityArchived(tileReceiver);

View File

@@ -355,7 +355,7 @@ namespace Content.Server.Atmos.EntitySystems
continue;
DebugTools.Assert(otherTile2.AdjacentBits.IsFlagSet(direction.GetOpposite()));
if (otherTile2.Air != null && CompareExchange(otherTile2.Air, tile.Air) == GasCompareResult.NoExchange)
if (otherTile2.Air != null && CompareExchange(otherTile2, tile) == GasCompareResult.NoExchange)
continue;
AddActiveTile(gridAtmosphere, otherTile2);
@@ -689,7 +689,7 @@ namespace Content.Server.Atmos.EntitySystems
var chance = MathHelper.Clamp(0.01f + (sum / SpacingMaxWind) * 0.3f, 0.003f, 0.3f);
if (sum > 20 && _robustRandom.Prob(chance))
if (sum > 20 && _random.Prob(chance))
PryTile(mapGrid, tile.GridIndices);
}

View File

@@ -231,7 +231,7 @@ namespace Content.Server.Atmos.EntitySystems
tile.MapAtmosphere = false;
atmos.MapTiles.Remove(tile);
tile.Air = null;
Array.Clear(tile.MolesArchived);
tile.AirArchived = null;
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Space = false;
@@ -261,7 +261,7 @@ namespace Content.Server.Atmos.EntitySystems
return;
tile.Air = null;
Array.Clear(tile.MolesArchived);
tile.AirArchived = null;
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Hotspot = new Hotspot();

View File

@@ -131,7 +131,10 @@ namespace Content.Server.Atmos.EntitySystems
private void TemperatureShareMutualSolid(TileAtmosphere tile, TileAtmosphere other, float conductionCoefficient)
{
var deltaTemperature = (tile.TemperatureArchived - other.TemperatureArchived);
if (tile.AirArchived == null || other.AirArchived == null)
return;
var deltaTemperature = (tile.AirArchived.Temperature - other.AirArchived.Temperature);
if (MathF.Abs(deltaTemperature) > Atmospherics.MinimumTemperatureDeltaToConsider
&& tile.HeatCapacity != 0f && other.HeatCapacity != 0f)
{
@@ -145,11 +148,14 @@ namespace Content.Server.Atmos.EntitySystems
public void RadiateToSpace(TileAtmosphere tile)
{
if (tile.AirArchived == null)
return;
// Considering 0ºC as the break even point for radiation in and out.
if (tile.Temperature > Atmospherics.T0C)
{
// Hardcoded space temperature.
var deltaTemperature = (tile.TemperatureArchived - Atmospherics.TCMB);
var deltaTemperature = (tile.AirArchived.Temperature - Atmospherics.TCMB);
if ((tile.HeatCapacity > 0) && (MathF.Abs(deltaTemperature) > Atmospherics.MinimumTemperatureDeltaToConsider))
{
var heat = tile.ThermalConductivity * deltaTemperature * (tile.HeatCapacity *

View File

@@ -14,7 +14,6 @@ using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Atmos.EntitySystems;
@@ -27,7 +26,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly InternalsSystem _internals = default!;
@@ -39,7 +37,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly TileSystem _tile = default!;
[Dependency] private readonly MapSystem _map = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] public readonly PuddleSystem Puddle = default!;
private const float ExposedUpdateDelay = 1f;
@@ -124,6 +121,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
private void CacheDecals()
{
_burntDecals = _prototypeManager.EnumeratePrototypes<DecalPrototype>().Where(x => x.Tags.Contains("burnt")).Select(x => x.ID).ToArray();
_burntDecals = _protoMan.EnumeratePrototypes<DecalPrototype>().Where(x => x.Tags.Contains("burnt")).Select(x => x.ID).ToArray();
}
}

View File

@@ -22,9 +22,6 @@ namespace Content.Server.Atmos
[ViewVariables]
public float Temperature { get; set; } = Atmospherics.T20C;
[ViewVariables]
public float TemperatureArchived { get; set; } = Atmospherics.T20C;
[ViewVariables]
public TileAtmosphere? PressureSpecificTarget { get; set; }
@@ -93,12 +90,16 @@ namespace Content.Server.Atmos
[Access(typeof(AtmosphereSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public GasMixture? Air { get; set; }
/// <summary>
/// Like Air, but a copy stored each atmos tick before tile processing takes place. This lets us update Air
/// in-place without affecting the results based on update order.
/// </summary>
[ViewVariables]
public GasMixture? AirArchived;
[DataField("lastShare")]
public float LastShare;
[ViewVariables]
public readonly float[] MolesArchived = new float[Atmospherics.AdjustedNumberOfGases];
GasMixture IGasMixtureHolder.Air
{
get => Air ?? new GasMixture(Atmospherics.CellVolume){ Temperature = Temperature };
@@ -139,6 +140,7 @@ namespace Content.Server.Atmos
GridIndex = gridIndex;
GridIndices = gridIndices;
Air = mixture;
AirArchived = Air != null ? Air.Clone() : null;
Space = space;
if(immutable)
@@ -153,7 +155,7 @@ namespace Content.Server.Atmos
NoGridTile = other.NoGridTile;
MapAtmosphere = other.MapAtmosphere;
Air = other.Air?.Clone();
Array.Copy(other.MolesArchived, MolesArchived, MolesArchived.Length);
AirArchived = Air != null ? Air.Clone() : null;
}
public TileAtmosphere()

View File

@@ -239,7 +239,7 @@ public sealed class CryostorageSystem : SharedCryostorageSystem
Loc.GetString(
"earlyleave-cryo-announcement",
("character", name),
("entity", ent.Owner),
("entity", ent.Owner), // gender things for supporting downstreams with other languages
("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))
), Loc.GetString("earlyleave-cryo-sender"),
playDefaultSound: false

View File

@@ -472,7 +472,7 @@ public sealed class BloodstreamSystem : EntitySystem
return;
}
var currentVolume = bloodSolution.RemoveReagent(component.BloodReagent, bloodSolution.Volume);
var currentVolume = bloodSolution.RemoveReagent(component.BloodReagent, bloodSolution.Volume, ignoreReagentData: true);
component.BloodReagent = reagent;

View File

@@ -14,7 +14,6 @@ public sealed class LungSystem : EntitySystem
[Dependency] private readonly AtmosphereSystem _atmos = default!;
[Dependency] private readonly InternalsSystem _internals = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
public static string LungSolutionName = "Lung";
@@ -29,7 +28,7 @@ public sealed class LungSystem : EntitySystem
private void OnGotUnequipped(Entity<BreathToolComponent> ent, ref GotUnequippedEvent args)
{
_atmosphereSystem.DisconnectInternals(ent);
_atmos.DisconnectInternals(ent);
}
private void OnGotEquipped(Entity<BreathToolComponent> ent, ref GotEquippedEvent args)
@@ -93,7 +92,7 @@ public sealed class LungSystem : EntitySystem
if (moles <= 0)
continue;
var reagent = _atmosphereSystem.GasReagents[i];
var reagent = _atmos.GasReagents[i];
if (reagent is null)
continue;

View File

@@ -1,5 +1,6 @@
using Content.Shared.Chemistry.Components;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Audio;
namespace Content.Server.Botany.Components;
@@ -23,6 +24,9 @@ public sealed partial class PlantHolderComponent : Component
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan LastCycle = TimeSpan.Zero;
[DataField]
public SoundSpecifier? WateringSound;
[DataField]
public bool UpdateSpriteAfterUpdate;

View File

@@ -27,7 +27,7 @@ public sealed class MutationSystem : EntitySystem
{
foreach (var mutation in _randomMutations.mutations)
{
if (Random(mutation.BaseOdds * severity))
if (Random(Math.Min(mutation.BaseOdds * severity, 1.0f)))
{
if (mutation.AppliesToPlant)
{

View File

@@ -1,6 +1,5 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Botany.Components;
using Content.Server.Fluids.Components;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.EntitySystems;
@@ -18,7 +17,6 @@ using Content.Shared.Popups;
using Content.Shared.Random;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@@ -37,7 +35,6 @@ public sealed class PlantHolderSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
@@ -53,6 +50,7 @@ public sealed class PlantHolderSystem : EntitySystem
SubscribeLocalEvent<PlantHolderComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<PlantHolderComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<PlantHolderComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<PlantHolderComponent, SolutionTransferredEvent>(OnSolutionTransferred);
}
public override void Update(float frameTime)
@@ -158,6 +156,7 @@ public sealed class PlantHolderSystem : EntitySystem
if (!_botany.TryGetSeed(seeds, out var seed))
return;
args.Handled = true;
var name = Loc.GetString(seed.Name);
var noun = Loc.GetString(seed.Noun);
_popup.PopupCursor(Loc.GetString("plant-holder-component-plant-success-message",
@@ -185,6 +184,7 @@ public sealed class PlantHolderSystem : EntitySystem
return;
}
args.Handled = true;
_popup.PopupCursor(Loc.GetString("plant-holder-component-already-seeded-message",
("name", Comp<MetaDataComponent>(uid).EntityName)), args.User, PopupType.Medium);
return;
@@ -192,6 +192,7 @@ public sealed class PlantHolderSystem : EntitySystem
if (_tagSystem.HasTag(args.Used, "Hoe"))
{
args.Handled = true;
if (component.WeedLevel > 0)
{
_popup.PopupCursor(Loc.GetString("plant-holder-component-remove-weeds-message",
@@ -211,6 +212,7 @@ public sealed class PlantHolderSystem : EntitySystem
if (HasComp<ShovelComponent>(args.Used))
{
args.Handled = true;
if (component.Seed != null)
{
_popup.PopupCursor(Loc.GetString("plant-holder-component-remove-plant-message",
@@ -228,39 +230,9 @@ public sealed class PlantHolderSystem : EntitySystem
return;
}
if (_solutionContainerSystem.TryGetDrainableSolution(args.Used, out var solution, out _)
&& _solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution)
&& TryComp(args.Used, out SprayComponent? spray))
{
var amount = FixedPoint2.New(1);
var targetEntity = uid;
var solutionEntity = args.Used;
_audio.PlayPvs(spray.SpraySound, args.Used, AudioParams.Default.WithVariation(0.125f));
var split = _solutionContainerSystem.Drain(solutionEntity, solution.Value, amount);
if (split.Volume == 0)
{
_popup.PopupCursor(Loc.GetString("plant-holder-component-no-plant-message",
("owner", args.Used)), args.User);
return;
}
_popup.PopupCursor(Loc.GetString("plant-holder-component-spray-message",
("owner", uid),
("amount", split.Volume)), args.User, PopupType.Medium);
_solutionContainerSystem.TryAddSolution(component.SoilSolution.Value, split);
ForceUpdateByExternalCause(uid, component);
return;
}
if (_tagSystem.HasTag(args.Used, "PlantSampleTaker"))
{
args.Handled = true;
if (component.Seed == null)
{
_popup.PopupCursor(Loc.GetString("plant-holder-component-nothing-to-sample-message"), args.User);
@@ -316,10 +288,15 @@ public sealed class PlantHolderSystem : EntitySystem
}
if (HasComp<SharpComponent>(args.Used))
{
args.Handled = true;
DoHarvest(uid, args.User, component);
return;
}
if (TryComp<ProduceComponent>(args.Used, out var produce))
{
args.Handled = true;
_popup.PopupCursor(Loc.GetString("plant-holder-component-compost-message",
("owner", uid),
("usingItem", args.Used)), args.User, PopupType.Medium);
@@ -351,6 +328,10 @@ public sealed class PlantHolderSystem : EntitySystem
}
}
private void OnSolutionTransferred(Entity<PlantHolderComponent> ent, ref SolutionTransferredEvent args)
{
_audio.PlayPvs(ent.Comp.WateringSound, ent.Owner);
}
private void OnInteractHand(Entity<PlantHolderComponent> entity, ref InteractHandEvent args)
{
DoHarvest(entity, args.User, entity.Comp);
@@ -699,7 +680,10 @@ public sealed class PlantHolderSystem : EntitySystem
if (TryComp<HandsComponent>(user, out var hands))
{
if (!_botany.CanHarvest(component.Seed, hands.ActiveHandEntity))
{
_popup.PopupCursor(Loc.GetString("plant-holder-component-ligneous-cant-harvest-message"), user);
return false;
}
}
else if (!_botany.CanHarvest(component.Seed))
{

View File

@@ -1,10 +0,0 @@
namespace Content.Server.Cargo.Components;
/// <summary>
/// This is used for the price gun, which calculates the price of any object it appraises.
/// </summary>
[RegisterComponent]
public sealed partial class PriceGunComponent : Component
{
}

View File

@@ -420,6 +420,13 @@ public sealed partial class CargoSystem
return false;
_nameIdentifier.GenerateUniqueName(uid, BountyNameIdentifierGroup, out var randomVal);
var newBounty = new CargoBountyData(bounty, randomVal);
// This bounty id already exists! Probably because NameIdentifierSystem ran out of ids.
if (component.Bounties.Any(b => b.Id == newBounty.Id))
{
Log.Error("Failed to add bounty {ID} because another one with the same ID already existed!", newBounty.Id);
return false;
}
component.Bounties.Add(new CargoBountyData(bounty, randomVal));
_adminLogger.Add(LogType.Action, LogImpact.Low, $"Added bounty \"{bounty.ID}\" (id:{component.TotalBounties}) to station {ToPrettyString(uid)}");
component.TotalBounties++;

View File

@@ -1,73 +1,34 @@
using Content.Server.Cargo.Components;
using Content.Server.Popups;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Timing;
using Content.Shared.Verbs;
using Content.Shared.Cargo.Systems;
namespace Content.Server.Cargo.Systems;
/// <summary>
/// This handles...
/// </summary>
public sealed class PriceGunSystem : EntitySystem
public sealed class PriceGunSystem : SharedPriceGunSystem
{
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly PricingSystem _pricingSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly CargoSystem _bountySystem = default!;
/// <inheritdoc/>
public override void Initialize()
protected override bool GetPriceOrBounty(EntityUid priceGunUid, EntityUid target, EntityUid user)
{
SubscribeLocalEvent<PriceGunComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<PriceGunComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
}
private void OnUtilityVerb(EntityUid uid, PriceGunComponent component, GetVerbsEvent<UtilityVerb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Using == null)
return;
if (!TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay)))
return;
var price = _pricingSystem.GetPrice(args.Target);
var verb = new UtilityVerb()
{
Act = () =>
{
_popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result", ("object", Identity.Entity(args.Target, EntityManager)), ("price", $"{price:F2}")), args.User, args.User);
_useDelay.TryResetDelay((uid, useDelay));
},
Text = Loc.GetString("price-gun-verb-text"),
Message = Loc.GetString("price-gun-verb-message", ("object", Identity.Entity(args.Target, EntityManager)))
};
args.Verbs.Add(verb);
}
private void OnAfterInteract(EntityUid uid, PriceGunComponent component, AfterInteractEvent args)
{
if (!args.CanReach || args.Target == null || args.Handled)
return;
if (!TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay)))
return;
if (!TryComp(priceGunUid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((priceGunUid, useDelay)))
return false;
// Check if we're scanning a bounty crate
if (_bountySystem.IsBountyComplete(args.Target.Value, out _))
if (_bountySystem.IsBountyComplete(target, out _))
{
_popupSystem.PopupEntity(Loc.GetString("price-gun-bounty-complete"), args.User, args.User);
_popupSystem.PopupEntity(Loc.GetString("price-gun-bounty-complete"), user, user);
}
else // Otherwise appraise the price
{
double price = _pricingSystem.GetPrice(args.Target.Value);
_popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result", ("object", Identity.Entity(args.Target.Value, EntityManager)), ("price", $"{price:F2}")), args.User, args.User);
var price = _pricingSystem.GetPrice(target);
_popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result", ("object", Identity.Entity(target, EntityManager)), ("price", $"{price:F2}")), user, user);
}
_useDelay.TryResetDelay((uid, useDelay));
args.Handled = true;
_useDelay.TryResetDelay((priceGunUid, useDelay));
return true;
}
}

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

@@ -0,0 +1,6 @@
namespace Content.Server.CartridgeLoader.Cartridges;
[RegisterComponent]
public sealed partial class MedTekCartridgeComponent : Component
{
}

View File

@@ -0,0 +1,31 @@
using Content.Server.Medical.Components;
using Content.Shared.CartridgeLoader;
namespace Content.Server.CartridgeLoader.Cartridges;
public sealed class MedTekCartridgeSystem : EntitySystem
{
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoaderSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MedTekCartridgeComponent, CartridgeAddedEvent>(OnCartridgeAdded);
SubscribeLocalEvent<MedTekCartridgeComponent, CartridgeRemovedEvent>(OnCartridgeRemoved);
}
private void OnCartridgeAdded(Entity<MedTekCartridgeComponent> ent, ref CartridgeAddedEvent args)
{
var healthAnalyzer = EnsureComp<HealthAnalyzerComponent>(args.Loader);
}
private void OnCartridgeRemoved(Entity<MedTekCartridgeComponent> ent, ref CartridgeRemovedEvent args)
{
// only remove when the program itself is removed
if (!_cartridgeLoaderSystem.HasProgram<MedTekCartridgeComponent>(args.Loader))
{
RemComp<HealthAnalyzerComponent>(args.Loader);
}
}
}

View File

@@ -1,6 +1,6 @@
using Content.Server.Players.RateLimiting;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Players.RateLimiting;
using Robust.Shared.Player;
namespace Content.Server.Chat.Managers;
@@ -12,15 +12,13 @@ internal sealed partial class ChatManager
private void RegisterRateLimits()
{
_rateLimitManager.Register(RateLimitKey,
new RateLimitRegistration
{
CVarLimitPeriodLength = CCVars.ChatRateLimitPeriod,
CVarLimitCount = CCVars.ChatRateLimitCount,
CVarAdminAnnounceDelay = CCVars.ChatRateLimitAnnounceAdminsDelay,
PlayerLimitedAction = RateLimitPlayerLimited,
AdminAnnounceAction = RateLimitAlertAdmins,
AdminLogType = LogType.ChatRateLimited,
});
new RateLimitRegistration(CCVars.ChatRateLimitPeriod,
CCVars.ChatRateLimitCount,
RateLimitPlayerLimited,
CCVars.ChatRateLimitAnnounceAdminsDelay,
RateLimitAlertAdmins,
LogType.ChatRateLimited)
);
}
private void RateLimitPlayerLimited(ICommonSession player)
@@ -30,8 +28,7 @@ internal sealed partial class ChatManager
private void RateLimitAlertAdmins(ICommonSession player)
{
if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins))
SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
}
public RateLimitStatus HandleRateLimit(ICommonSession player)

View File

@@ -12,6 +12,7 @@ using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Mind;
using Content.Shared.Players.RateLimiting;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;

View File

@@ -1,17 +1,19 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.RegularExpressions;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
namespace Content.Server.Chat.Managers;
/// <summary>
/// Sanitizes messages!
/// It currently ony removes the shorthands for emotes (like "lol" or "^-^") from a chat message and returns the last
/// emote in their message
/// </summary>
public sealed class ChatSanitizationManager : IChatSanitizationManager
{
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
private static readonly Dictionary<string, string> SmileyToEmote = new()
private static readonly Dictionary<string, string> ShorthandToEmote = new()
{
// I could've done this with regex, but felt it wasn't the right idea.
{ ":)", "chatsan-smiles" },
{ ":]", "chatsan-smiles" },
{ "=)", "chatsan-smiles" },
@@ -33,7 +35,7 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
{ ":D", "chatsan-smiles-widely" },
{ "D:", "chatsan-frowns-deeply" },
{ ":O", "chatsan-surprised" },
{ ":3", "chatsan-smiles" }, //nope
{ ":3", "chatsan-smiles" },
{ ":S", "chatsan-uncertain" },
{ ":>", "chatsan-grins" },
{ ":<", "chatsan-pouts" },
@@ -75,7 +77,7 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
{ "kek", "chatsan-laughs" },
{ "rofl", "chatsan-laughs" },
{ "o7", "chatsan-salutes" },
{ ";_;7", "chatsan-tearfully-salutes"},
{ ";_;7", "chatsan-tearfully-salutes" },
{ "idk", "chatsan-shrugs" },
{ ";)", "chatsan-winks" },
{ ";]", "chatsan-winks" },
@@ -88,9 +90,12 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
{ "(':", "chatsan-tearfully-smiles" },
{ "[':", "chatsan-tearfully-smiles" },
{ "('=", "chatsan-tearfully-smiles" },
{ "['=", "chatsan-tearfully-smiles" },
{ "['=", "chatsan-tearfully-smiles" }
};
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
private bool _doSanitize;
public void Initialize()
@@ -98,29 +103,60 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
_configurationManager.OnValueChanged(CCVars.ChatSanitizerEnabled, x => _doSanitize = x, true);
}
public bool TrySanitizeOutSmilies(string input, EntityUid speaker, out string sanitized, [NotNullWhen(true)] out string? emote)
/// <summary>
/// Remove the shorthands from the message, returning the last one found as the emote
/// </summary>
/// <param name="message">The pre-sanitized message</param>
/// <param name="speaker">The speaker</param>
/// <param name="sanitized">The sanitized message with shorthands removed</param>
/// <param name="emote">The localized emote</param>
/// <returns>True if emote has been sanitized out</returns>
public bool TrySanitizeEmoteShorthands(string message,
EntityUid speaker,
out string sanitized,
[NotNullWhen(true)] out string? emote)
{
if (!_doSanitize)
{
sanitized = input;
emote = null;
return false;
}
input = input.TrimEnd();
foreach (var (smiley, replacement) in SmileyToEmote)
{
if (input.EndsWith(smiley, true, CultureInfo.InvariantCulture))
{
sanitized = input.Remove(input.Length - smiley.Length).TrimEnd();
emote = Loc.GetString(replacement, ("ent", speaker));
return true;
}
}
sanitized = input;
emote = null;
return false;
sanitized = message;
if (!_doSanitize)
return false;
// -1 is just a canary for nothing found yet
var lastEmoteIndex = -1;
foreach (var (shorthand, emoteKey) in ShorthandToEmote)
{
// We have to escape it because shorthands like ":)" or "-_-" would break the regex otherwise.
var escaped = Regex.Escape(shorthand);
// So there are 2 cases:
// - If there is whitespace before it and after it is either punctuation, whitespace, or the end of the line
// Delete the word and the whitespace before
// - If it is at the start of the string and is followed by punctuation, whitespace, or the end of the line
// Delete the word and the punctuation if it exists.
var pattern =
$@"\s{escaped}(?=\p{{P}}|\s|$)|^{escaped}(?:\p{{P}}|(?=\s|$))";
var r = new Regex(pattern, RegexOptions.RightToLeft | RegexOptions.IgnoreCase);
// We're using sanitized as the original message until the end so that we can make sure the indices of
// the emotes are accurate.
var lastMatch = r.Match(sanitized);
if (!lastMatch.Success)
continue;
if (lastMatch.Index > lastEmoteIndex)
{
lastEmoteIndex = lastMatch.Index;
emote = _loc.GetString(emoteKey, ("ent", speaker));
}
message = r.Replace(message, string.Empty);
}
sanitized = message.Trim();
return emote is not null;
}
}

View File

@@ -1,17 +1,14 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Players;
using Content.Server.Players.RateLimiting;
using Content.Shared.Administration;
using Content.Shared.Chat;
using Content.Shared.Players.RateLimiting;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.Server.Chat.Managers
{
public interface IChatManager
public interface IChatManager : ISharedChatManager
{
void Initialize();
/// <summary>
/// Dispatch a server announcement to every connected player.
/// </summary>
@@ -26,8 +23,6 @@ namespace Content.Server.Chat.Managers
void SendHookOOC(string sender, string message);
void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist = null, AdminFlags? flagWhitelist = null);
void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true);
void SendAdminAlert(string message);
void SendAdminAlert(EntityUid player, string message);
void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat,
INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, NetUserId? author = null);

View File

@@ -6,5 +6,8 @@ public interface IChatSanitizationManager
{
public void Initialize();
public bool TrySanitizeOutSmilies(string input, EntityUid speaker, out string sanitized, [NotNullWhen(true)] out string? emote);
public bool TrySanitizeEmoteShorthands(string input,
EntityUid speaker,
out string sanitized,
[NotNullWhen(true)] out string? emote);
}

View File

@@ -20,6 +20,7 @@ using Content.Shared.Ghost;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs.Systems;
using Content.Shared.Players;
using Content.Shared.Players.RateLimiting;
using Content.Shared.Radio;
using Content.Shared.Whitelist;
using Robust.Server.Player;
@@ -745,8 +746,12 @@ public sealed partial class ChatSystem : SharedChatSystem
// ReSharper disable once InconsistentNaming
private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true, bool punctuate = false, bool capitalizeTheWordI = true)
{
var newMessage = message.Trim();
newMessage = SanitizeMessageReplaceWords(newMessage);
var newMessage = SanitizeMessageReplaceWords(message.Trim());
GetRadioKeycodePrefix(source, newMessage, out newMessage, out var prefix);
// Sanitize it first as it might change the word order
_sanitizer.TrySanitizeEmoteShorthands(newMessage, source, out newMessage, out emoteStr);
if (capitalize)
newMessage = SanitizeMessageCapital(newMessage);
@@ -755,9 +760,7 @@ public sealed partial class ChatSystem : SharedChatSystem
if (punctuate)
newMessage = SanitizeMessagePeriod(newMessage);
_sanitizer.TrySanitizeOutSmilies(newMessage, source, out newMessage, out emoteStr);
return newMessage;
return prefix + newMessage;
}
private string SanitizeInGameOOCMessage(string message)

View File

@@ -0,0 +1,24 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Chemistry.Components;
/// <summary>
/// Used for embeddable entities that should try to inject a
/// contained solution into a target over time while they are embbeded into.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class SolutionInjectWhileEmbeddedComponent : BaseSolutionInjectOnEventComponent {
///<summary>
///The time at which the injection will happen.
///</summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextUpdate;
///<summary>
///The delay between each injection in seconds.
///</summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(3);
}

View File

@@ -13,6 +13,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;
@@ -20,6 +21,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()
{
@@ -31,13 +33,14 @@ public sealed class InjectorSystem : SharedInjectorSystem
private bool TryUseInjector(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
{
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))
@@ -58,7 +61,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

@@ -2,6 +2,7 @@ using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Events;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using Content.Shared.Projectiles;
@@ -29,6 +30,7 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
SubscribeLocalEvent<SolutionInjectOnProjectileHitComponent, ProjectileHitEvent>(HandleProjectileHit);
SubscribeLocalEvent<SolutionInjectOnEmbedComponent, EmbedEvent>(HandleEmbed);
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(HandleMeleeHit);
SubscribeLocalEvent<SolutionInjectWhileEmbeddedComponent, InjectOverTimeEvent>(OnInjectOverTime);
}
private void HandleProjectileHit(Entity<SolutionInjectOnProjectileHitComponent> entity, ref ProjectileHitEvent args)
@@ -49,6 +51,11 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
TryInjectTargets((entity.Owner, entity.Comp), args.HitEntities, args.User);
}
private void OnInjectOverTime(Entity<SolutionInjectWhileEmbeddedComponent> entity, ref InjectOverTimeEvent args)
{
DoInjection((entity.Owner, entity.Comp), args.EmbeddedIntoUid);
}
private void DoInjection(Entity<BaseSolutionInjectOnEventComponent> injectorEntity, EntityUid target, EntityUid? source = null)
{
TryInjectTargets(injectorEntity, [target], source);

View File

@@ -0,0 +1,60 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Events;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using Content.Shared.Projectiles;
using Content.Shared.Tag;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Collections;
using Robust.Shared.Timing;
namespace Content.Server.Chemistry.EntitySystems;
/// <summary>
/// System for handling injecting into an entity while a projectile is embedded.
/// </summary>
public sealed class SolutionInjectWhileEmbeddedSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly TagSystem _tag = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SolutionInjectWhileEmbeddedComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<SolutionInjectWhileEmbeddedComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<SolutionInjectWhileEmbeddedComponent, EmbeddableProjectileComponent>();
while (query.MoveNext(out var uid, out var injectComponent, out var projectileComponent))
{
if (_gameTiming.CurTime < injectComponent.NextUpdate)
continue;
injectComponent.NextUpdate += injectComponent.UpdateInterval;
if(projectileComponent.EmbeddedIntoUid == null)
continue;
var ev = new InjectOverTimeEvent(projectileComponent.EmbeddedIntoUid.Value);
RaiseLocalEvent(uid, ref ev);
}
}
}

View File

@@ -231,7 +231,7 @@ namespace Content.Server.Cloning
// TODO: Ideally, components like this should be components on the mind entity so this isn't necessary.
// Add on special job components to the mob.
if (_jobs.MindTryGetJob(mindEnt, out _, out var prototype))
if (_jobs.MindTryGetJob(mindEnt, out var prototype))
{
foreach (var special in prototype.Special)
{

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

@@ -46,7 +46,6 @@ namespace Content.Server.Connection
/// </summary>
public sealed partial class ConnectionManager : IConnectionManager
{
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly IPlayerManager _plyMgr = default!;
[Dependency] private readonly IServerNetManager _netMgr = default!;
[Dependency] private readonly IServerDbManager _db = default!;
@@ -205,7 +204,7 @@ namespace Content.Server.Connection
return null;
}
var adminData = await _dbManager.GetAdminDataForAsync(e.UserId);
var adminData = await _db.GetAdminDataForAsync(e.UserId);
if (_cfg.GetCVar(CCVars.PanicBunkerEnabled) && adminData == null)
{
@@ -213,7 +212,7 @@ namespace Content.Server.Connection
var customReason = _cfg.GetCVar(CCVars.PanicBunkerCustomReason);
var minMinutesAge = _cfg.GetCVar(CCVars.PanicBunkerMinAccountAge);
var record = await _dbManager.GetPlayerRecordByUserId(userId);
var record = await _db.GetPlayerRecordByUserId(userId);
var validAccountAge = record != null &&
record.FirstSeenTime.CompareTo(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(minMinutesAge)) <= 0;
var bypassAllowed = _cfg.GetCVar(CCVars.BypassBunkerWhitelist) && await _db.GetWhitelistStatusAsync(userId);
@@ -317,7 +316,7 @@ namespace Content.Server.Connection
var maxPlaytimeMinutes = _cfg.GetCVar(CCVars.BabyJailMaxOverallMinutes);
// Wait some time to lookup data
var record = await _dbManager.GetPlayerRecordByUserId(userId);
var record = await _db.GetPlayerRecordByUserId(userId);
// No player record = new account or the DB is having a skill issue
if (record == null)

View File

@@ -39,7 +39,7 @@ public sealed class ExaminableDamageSystem : EntitySystem
var level = GetDamageLevel(uid, component);
var msg = Loc.GetString(messages[level]);
args.PushMarkup(msg);
args.PushMarkup(msg,-99);
}
private int GetDamageLevel(EntityUid uid, ExaminableDamageComponent? component = null,

View File

@@ -512,16 +512,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
@@ -875,10 +882,41 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
public async Task AddAdminLogs(List<AdminLog> logs)
{
const int maxRetryAttempts = 5;
var initialRetryDelay = TimeSpan.FromSeconds(5);
DebugTools.Assert(logs.All(x => x.RoundId > 0), "Adding logs with invalid round ids.");
await using var db = await GetDb();
db.DbContext.AdminLog.AddRange(logs);
await db.DbContext.SaveChangesAsync();
var attempt = 0;
var retryDelay = initialRetryDelay;
while (attempt < maxRetryAttempts)
{
try
{
await using var db = await GetDb();
db.DbContext.AdminLog.AddRange(logs);
await db.DbContext.SaveChangesAsync();
_opsLog.Debug($"Successfully saved {logs.Count} admin logs.");
break;
}
catch (Exception ex)
{
attempt += 1;
_opsLog.Error($"Attempt {attempt} failed to save logs: {ex}");
if (attempt >= maxRetryAttempts)
{
_opsLog.Error($"Max retry attempts reached. Failed to save {logs.Count} admin logs.");
return;
}
_opsLog.Warning($"Retrying in {retryDelay.TotalSeconds} seconds...");
await Task.Delay(retryDelay);
retryDelay *= 2;
}
}
}
protected abstract IQueryable<AdminLog> StartAdminLogsQuery(ServerDbContext db, LogFilter? filter = null);

View File

@@ -35,7 +35,7 @@ namespace Content.Server.Decals
[Dependency] private readonly IConfigurationManager _conf = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
private readonly Dictionary<NetEntity, HashSet<Vector2i>> _dirtyChunks = new();
private readonly Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> _previousSentChunks = new();
@@ -106,7 +106,7 @@ namespace Content.Server.Decals
return;
// Transfer decals over to the new grid.
var enumerator = Comp<MapGridComponent>(ev.Grid).GetAllTilesEnumerator();
var enumerator = _mapSystem.GetAllTilesEnumerator(ev.Grid, Comp<MapGridComponent>(ev.Grid));
var oldChunkCollection = oldComp.ChunkCollection.ChunkCollection;
var chunkCollection = newComp.ChunkCollection.ChunkCollection;

View File

@@ -0,0 +1,10 @@
namespace Content.Server.Destructible.Thresholds.Behaviors;
[DataDefinition]
public sealed partial class TimerStartBehavior : IThresholdBehavior
{
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
{
system.TriggerSystem.StartTimer(owner, cause);
}
}

View File

@@ -0,0 +1,183 @@
using Content.Server.GameTicking;
using Content.Server.Voting;
using Robust.Server;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace Content.Server.Discord.WebhookMessages;
public sealed class VoteWebhooks : IPostInjectInit
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IEntitySystemManager _entSys = default!;
[Dependency] private readonly DiscordWebhook _discord = default!;
[Dependency] private readonly IBaseServer _baseServer = default!;
private ISawmill _sawmill = default!;
public WebhookState? CreateWebhookIfConfigured(VoteOptions voteOptions, string? webhookUrl = null, string? customVoteName = null, string? customVoteMessage = null)
{
// All this webhook code is complete garbage.
// I tried to clean it up somewhat, at least to fix the glaring bugs in it.
// Jesus christ man what is with our code review process.
if (string.IsNullOrEmpty(webhookUrl))
return null;
// Set up the webhook payload
var serverName = _baseServer.ServerName;
var fields = new List<WebhookEmbedField>();
foreach (var voteOption in voteOptions.Options)
{
var newVote = new WebhookEmbedField
{
Name = voteOption.text,
Value = Loc.GetString("custom-vote-webhook-option-pending")
};
fields.Add(newVote);
}
var gameTicker = _entSys.GetEntitySystemOrNull<GameTicker>();
_sawmill = Logger.GetSawmill("discord");
var runLevel = gameTicker != null ? Loc.GetString($"game-run-level-{gameTicker.RunLevel}") : "";
var runId = gameTicker != null ? gameTicker.RoundId : 0;
var voteName = customVoteName ?? Loc.GetString("custom-vote-webhook-name");
var description = customVoteMessage ?? voteOptions.Title;
var payload = new WebhookPayload()
{
Username = voteName,
Embeds = new List<WebhookEmbed>
{
new()
{
Title = voteOptions.InitiatorText,
Color = 13438992, // #CD1010
Description = description,
Footer = new WebhookEmbedFooter
{
Text = Loc.GetString(
"custom-vote-webhook-footer",
("serverName", serverName),
("roundId", runId),
("runLevel", runLevel)),
},
Fields = fields,
},
},
};
var state = new WebhookState
{
WebhookUrl = webhookUrl,
Payload = payload,
};
CreateWebhookMessage(state, payload);
return state;
}
public void UpdateWebhookIfConfigured(WebhookState? state, VoteFinishedEventArgs finished)
{
if (state == null)
return;
var embed = state.Payload.Embeds![0];
embed.Color = 2353993; // #23EB49
for (var i = 0; i < finished.Votes.Count; i++)
{
var oldName = embed.Fields[i].Name;
var newValue = finished.Votes[i].ToString();
embed.Fields[i] = new WebhookEmbedField { Name = oldName, Value = newValue, Inline = true };
}
state.Payload.Embeds[0] = embed;
UpdateWebhookMessage(state, state.Payload, state.MessageId);
}
public void UpdateCancelledWebhookIfConfigured(WebhookState? state, string? customCancelReason = null)
{
if (state == null)
return;
var embed = state.Payload.Embeds![0];
embed.Color = 13356304; // #CBCD10
if (customCancelReason == null)
embed.Description += "\n\n" + Loc.GetString("custom-vote-webhook-cancelled");
else
embed.Description += "\n\n" + customCancelReason;
for (var i = 0; i < embed.Fields.Count; i++)
{
var oldName = embed.Fields[i].Name;
embed.Fields[i] = new WebhookEmbedField { Name = oldName, Value = Loc.GetString("custom-vote-webhook-option-cancelled"), Inline = true };
}
state.Payload.Embeds[0] = embed;
UpdateWebhookMessage(state, state.Payload, state.MessageId);
}
// Sends the payload's message.
public async void CreateWebhookMessage(WebhookState state, WebhookPayload payload)
{
try
{
if (await _discord.GetWebhook(state.WebhookUrl) is not { } identifier)
return;
state.Identifier = identifier.ToIdentifier();
_sawmill.Debug(JsonSerializer.Serialize(payload));
var request = await _discord.CreateMessage(identifier.ToIdentifier(), payload);
var content = await request.Content.ReadAsStringAsync();
state.MessageId = ulong.Parse(JsonNode.Parse(content)?["id"]!.GetValue<string>()!);
}
catch (Exception e)
{
_sawmill.Error($"Error while sending vote webhook to Discord: {e}");
}
}
// Edits a pre-existing payload message, given an ID
public async void UpdateWebhookMessage(WebhookState state, WebhookPayload payload, ulong id)
{
if (state.MessageId == 0)
{
_sawmill.Warning("Failed to deliver update to custom vote webhook: message ID was zero. This likely indicates a previous connection error sending the original message.");
return;
}
DebugTools.Assert(state.Identifier != default);
try
{
await _discord.EditMessage(state.Identifier, id, payload);
}
catch (Exception e)
{
_sawmill.Error($"Error while updating vote webhook on Discord: {e}");
}
}
public sealed class WebhookState
{
public required string WebhookUrl;
public required WebhookPayload Payload;
public WebhookIdentifier Identifier;
public ulong MessageId;
}
void IPostInjectInit.PostInject() { }
}

View File

@@ -46,8 +46,8 @@ public sealed class DoorSystem : SharedDoorSystem
SetBoltsDown(ent, true);
}
UpdateBoltLightStatus(ent);
ent.Comp.Powered = args.Powered;
Dirty(ent, ent.Comp);
UpdateBoltLightStatus(ent);
}
}

View File

@@ -1,49 +1,53 @@
using System.Linq;
using Content.Shared.EntityEffects;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Localizations;
using Robust.Shared.Prototypes;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Station;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
namespace Content.Server.EntityEffects.EffectConditions;
public sealed partial class JobCondition : EntityEffectCondition
{
[DataField(required: true)] public List<ProtoId<JobPrototype>> Job;
public override bool Condition(EntityEffectBaseArgs args)
{
{
args.EntityManager.TryGetComponent<MindContainerComponent>(args.TargetEntity, out var mindContainer);
if (mindContainer != null && mindContainer.Mind != null)
if ( mindContainer is null
|| !args.EntityManager.TryGetComponent<MindComponent>(mindContainer.Mind, out var mind))
return false;
foreach (var roleId in mind.MindRoles)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
if (args.EntityManager.TryGetComponent<JobComponent>(mindContainer?.Mind, out var comp) && prototypeManager.TryIndex(comp.Prototype, out var prototype))
if(!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
continue;
if (!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole))
{
foreach (var jobId in Job)
{
if (prototype.ID == jobId)
{
return true;
}
}
Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(MindRoleComponent)}");
continue;
}
if (mindRole.JobPrototype == null)
{
Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(JobPrototype)}");
continue;
}
if (Job.Contains(mindRole.JobPrototype.Value))
return true;
}
return false;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
var localizedNames = Job.Select(jobId => prototype.Index(jobId).LocalizedName).ToList();
return Loc.GetString("reagent-effect-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames)));
}
}

View File

@@ -0,0 +1,82 @@
using Content.Shared.EntityEffects;
using Content.Server.Flash;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.EntityEffects.Effects;
[DataDefinition]
public sealed partial class FlashReactionEffect : EntityEffect
{
/// <summary>
/// Flash range per unit of reagent.
/// </summary>
[DataField]
public float RangePerUnit = 0.2f;
/// <summary>
/// Maximum flash range.
/// </summary>
[DataField]
public float MaxRange = 10f;
/// <summary>
/// How much to entities are slowed down.
/// </summary>
[DataField]
public float SlowTo = 0.5f;
/// <summary>
/// The time entities will be flashed in seconds.
/// The default is chosen to be better than the hand flash so it is worth using it for grenades etc.
/// </summary>
[DataField]
public float Duration = 4f;
/// <summary>
/// The prototype ID used for the visual effect.
/// </summary>
[DataField]
public EntProtoId? FlashEffectPrototype = "ReactionFlash";
/// <summary>
/// The sound the flash creates.
/// </summary>
[DataField]
public SoundSpecifier? Sound = new SoundPathSpecifier("/Audio/Weapons/flash.ogg");
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-flash-reaction-effect", ("chance", Probability));
public override void Effect(EntityEffectBaseArgs args)
{
var transform = args.EntityManager.GetComponent<TransformComponent>(args.TargetEntity);
var transformSystem = args.EntityManager.System<SharedTransformSystem>();
var range = 1f;
if (args is EntityEffectReagentArgs reagentArgs)
range = MathF.Min((float)(reagentArgs.Quantity * RangePerUnit), MaxRange);
args.EntityManager.System<FlashSystem>().FlashArea(
args.TargetEntity,
null,
range,
Duration * 1000,
slowTo: SlowTo,
sound: Sound);
if (FlashEffectPrototype == null)
return;
var uid = args.EntityManager.SpawnEntity(FlashEffectPrototype, transformSystem.GetMapCoordinates(transform));
transformSystem.AttachToGridOrMap(uid);
if (!args.EntityManager.TryGetComponent<PointLightComponent>(uid, out var pointLightComp))
return;
var pointLightSystem = args.EntityManager.System<SharedPointLightSystem>();
// PointLights with a radius lower than 1.1 are too small to be visible, so this is hardcoded
pointLightSystem.SetRadius(uid, MathF.Max(1.1f, range), pointLightComp);
}
}

View File

@@ -0,0 +1,15 @@
using Content.Shared.DeviceLinking;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Explosion.Components
{
/// <summary>
/// Sends a trigger when signal is received.
/// </summary>
[RegisterComponent]
public sealed partial class TimerStartOnSignalComponent : Component
{
[DataField("port", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
public string Port = "Timer";
}
}

View File

@@ -29,7 +29,7 @@ public sealed partial class ExplosionSystem
Dictionary<Vector2i, NeighborFlag> edges = new();
_gridEdges[ev.EntityUid] = edges;
foreach (var tileRef in grid.GetAllTiles())
foreach (var tileRef in _map.GetAllTiles(ev.EntityUid, grid))
{
if (IsEdge(grid, tileRef.GridIndices, out var dir))
edges.Add(tileRef.GridIndices, dir);

View File

@@ -488,9 +488,12 @@ public sealed partial class ExplosionSystem
&& physics.BodyType == BodyType.Dynamic)
{
var pos = _transformSystem.GetWorldPosition(xform);
var dir = pos - epicenter.Position;
if (dir.IsLengthZero())
dir = _robustRandom.NextVector2().Normalized();
_throwingSystem.TryThrow(
uid,
pos - epicenter.Position,
dir,
physics,
xform,
_projectileQuery,
@@ -666,6 +669,7 @@ sealed class Explosion
private readonly IEntityManager _entMan;
private readonly ExplosionSystem _system;
private readonly SharedMapSystem _mapSystem;
public readonly EntityUid VisualEnt;
@@ -688,11 +692,13 @@ sealed class Explosion
IEntityManager entMan,
IMapManager mapMan,
EntityUid visualEnt,
EntityUid? cause)
EntityUid? cause,
SharedMapSystem mapSystem)
{
VisualEnt = visualEnt;
Cause = cause;
_system = system;
_mapSystem = mapSystem;
ExplosionType = explosionType;
_tileSetIntensity = tileSetIntensity;
Epicenter = epicenter;
@@ -899,7 +905,7 @@ sealed class Explosion
{
if (list.Count > 0 && _entMan.EntityExists(grid.Owner))
{
grid.SetTiles(list);
_mapSystem.SetTiles(grid.Owner, grid, list);
}
}
_tileUpdateDict.Clear();

View File

@@ -389,7 +389,8 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
EntityManager,
_mapManager,
visualEnt,
queued.Cause);
queued.Cause,
_map);
}
private void CameraShake(float range, MapCoordinates epicenter, float totalIntensity)

View File

@@ -11,6 +11,9 @@ namespace Content.Server.Explosion.EntitySystems
{
SubscribeLocalEvent<TriggerOnSignalComponent,SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<TriggerOnSignalComponent,ComponentInit>(OnInit);
SubscribeLocalEvent<TimerStartOnSignalComponent,SignalReceivedEvent>(OnTimerSignalReceived);
SubscribeLocalEvent<TimerStartOnSignalComponent,ComponentInit>(OnTimerSignalInit);
}
private void OnSignalReceived(EntityUid uid, TriggerOnSignalComponent component, ref SignalReceivedEvent args)
@@ -24,5 +27,17 @@ namespace Content.Server.Explosion.EntitySystems
{
_signalSystem.EnsureSinkPorts(uid, component.Port);
}
private void OnTimerSignalReceived(EntityUid uid, TimerStartOnSignalComponent component, ref SignalReceivedEvent args)
{
if (args.Port != component.Port)
return;
StartTimer(uid, args.Trigger);
}
private void OnTimerSignalInit(EntityUid uid, TimerStartOnSignalComponent component, ComponentInit args)
{
_signalSystem.EnsureSinkPorts(uid, component.Port);
}
}
}

View File

@@ -1,5 +1,4 @@
using Content.Server.DoAfter;
using Content.Server.Fluids.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Audio;
@@ -13,9 +12,7 @@ using Content.Shared.Fluids.Components;
using Content.Shared.Interaction;
using Content.Shared.Tag;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Collections;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
@@ -32,19 +29,27 @@ public sealed class DrainSystem : SharedDrainSystem
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly HashSet<Entity<PuddleComponent>> _puddles = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DrainComponent, MapInitEvent>(OnDrainMapInit);
SubscribeLocalEvent<DrainComponent, GetVerbsEvent<Verb>>(AddEmptyVerb);
SubscribeLocalEvent<DrainComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<DrainComponent, AfterInteractUsingEvent>(OnInteract);
SubscribeLocalEvent<DrainComponent, DrainDoAfterEvent>(OnDoAfter);
}
private void OnDrainMapInit(Entity<DrainComponent> ent, ref MapInitEvent args)
{
// Randomise puddle drains so roundstart ones don't all dump at the same time.
ent.Comp.Accumulator = _random.NextFloat(ent.Comp.DrainFrequency);
}
private void AddEmptyVerb(Entity<DrainComponent> entity, ref GetVerbsEvent<Verb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Using == null)
@@ -118,9 +123,6 @@ public sealed class DrainSystem : SharedDrainSystem
{
base.Update(frameTime);
var managerQuery = GetEntityQuery<SolutionContainerManagerComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var puddleQuery = GetEntityQuery<PuddleComponent>();
var puddles = new ValueList<(Entity<PuddleComponent> Entity, string Solution)>();
var query = EntityQueryEnumerator<DrainComponent>();
while (query.MoveNext(out var uid, out var drain))
@@ -158,22 +160,10 @@ public sealed class DrainSystem : SharedDrainSystem
// This will ensure that UnitsPerSecond is per second...
var amount = drain.UnitsPerSecond * drain.DrainFrequency;
if (!xformQuery.TryGetComponent(uid, out var xform))
continue;
_puddles.Clear();
_lookup.GetEntitiesInRange(Transform(uid).Coordinates, drain.Range, _puddles);
puddles.Clear();
foreach (var entity in _lookup.GetEntitiesInRange(_transform.GetMapCoordinates(uid, xform), drain.Range))
{
// No InRangeUnobstructed because there's no collision group that fits right now
// and these are placed by mappers and not buildable/movable so shouldnt really be a problem...
if (puddleQuery.TryGetComponent(entity, out var puddle))
{
puddles.Add(((entity, puddle), puddle.SolutionName));
}
}
if (puddles.Count == 0)
if (_puddles.Count == 0)
{
_ambientSoundSystem.SetAmbience(uid, false);
continue;
@@ -181,13 +171,13 @@ public sealed class DrainSystem : SharedDrainSystem
_ambientSoundSystem.SetAmbience(uid, true);
amount /= puddles.Count;
amount /= _puddles.Count;
foreach (var (puddle, solution) in puddles)
foreach (var puddle in _puddles)
{
// Queue the solution deletion if it's empty. EvaporationSystem might also do this
// but queuedelete should be pretty safe.
if (!_solutionContainerSystem.ResolveSolution(puddle.Owner, solution, ref puddle.Comp.Solution, out var puddleSolution))
if (!_solutionContainerSystem.ResolveSolution(puddle.Owner, puddle.Comp.SolutionName, ref puddle.Comp.Solution, out var puddleSolution))
{
EntityManager.QueueDeleteEntity(puddle);
continue;

View File

@@ -14,6 +14,7 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using System.Numerics;
using Robust.Shared.Map;
namespace Content.Server.Fluids.EntitySystems;
@@ -35,6 +36,19 @@ public sealed class SpraySystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<SprayComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<SprayComponent, UserActivateInWorldEvent>(OnActivateInWorld);
}
private void OnActivateInWorld(Entity<SprayComponent> entity, ref UserActivateInWorldEvent args)
{
if (args.Handled)
return;
args.Handled = true;
var targetMapPos = _transform.GetMapCoordinates(GetEntityQuery<TransformComponent>().GetComponent(args.Target));
Spray(entity, args.User, targetMapPos);
}
private void OnAfterInteract(Entity<SprayComponent> entity, ref AfterInteractEvent args)
@@ -44,29 +58,36 @@ public sealed class SpraySystem : EntitySystem
args.Handled = true;
var clickPos = _transform.ToMapCoordinates(args.ClickLocation);
Spray(entity, args.User, clickPos);
}
public void Spray(Entity<SprayComponent> entity, EntityUid user, MapCoordinates mapcoord)
{
if (!_solutionContainer.TryGetSolution(entity.Owner, SprayComponent.SolutionName, out var soln, out var solution))
return;
var ev = new SprayAttemptEvent(args.User);
var ev = new SprayAttemptEvent(user);
RaiseLocalEvent(entity, ref ev);
if (ev.Cancelled)
return;
if (!TryComp<UseDelayComponent>(entity, out var useDelay)
|| _useDelay.IsDelayed((entity, useDelay)))
if (TryComp<UseDelayComponent>(entity, out var useDelay)
&& _useDelay.IsDelayed((entity, useDelay)))
return;
if (solution.Volume <= 0)
{
_popupSystem.PopupEntity(Loc.GetString("spray-component-is-empty-message"), entity.Owner, args.User);
_popupSystem.PopupEntity(Loc.GetString("spray-component-is-empty-message"), entity.Owner, user);
return;
}
var xformQuery = GetEntityQuery<TransformComponent>();
var userXform = xformQuery.GetComponent(args.User);
var userXform = xformQuery.GetComponent(user);
var userMapPos = _transform.GetMapCoordinates(userXform);
var clickMapPos = args.ClickLocation.ToMap(EntityManager, _transform);
var clickMapPos = mapcoord;
var diffPos = clickMapPos.Position - userMapPos.Position;
if (diffPos == Vector2.Zero || diffPos == Vector2Helpers.NaN)
@@ -88,8 +109,6 @@ public sealed class SpraySystem : EntitySystem
var amount = Math.Max(Math.Min((solution.Volume / entity.Comp.TransferAmount).Int(), entity.Comp.VaporAmount), 1);
var spread = entity.Comp.VaporSpread / amount;
// TODO: Just use usedelay homie.
var cooldownTime = 0f;
for (var i = 0; i < amount; i++)
{
@@ -131,20 +150,19 @@ public sealed class SpraySystem : EntitySystem
// impulse direction is defined in world-coordinates, not local coordinates
var impulseDirection = rotation.ToVec();
var time = diffLength / entity.Comp.SprayVelocity;
cooldownTime = MathF.Max(time, cooldownTime);
_vapor.Start(ent, vaporXform, impulseDirection * diffLength, entity.Comp.SprayVelocity, target, time, args.User);
_vapor.Start(ent, vaporXform, impulseDirection * diffLength, entity.Comp.SprayVelocity, target, time, user);
if (TryComp<PhysicsComponent>(args.User, out var body))
if (TryComp<PhysicsComponent>(user, out var body))
{
if (_gravity.IsWeightless(args.User, body))
_physics.ApplyLinearImpulse(args.User, -impulseDirection.Normalized() * entity.Comp.PushbackAmount, body: body);
if (_gravity.IsWeightless(user, body))
_physics.ApplyLinearImpulse(user, -impulseDirection.Normalized() * entity.Comp.PushbackAmount, body: body);
}
}
_audio.PlayPvs(entity.Comp.SpraySound, entity, entity.Comp.SpraySound.Params.WithVariation(0.125f));
_useDelay.SetLength(entity.Owner, TimeSpan.FromSeconds(cooldownTime));
_useDelay.TryResetDelay((entity, useDelay));
if (useDelay != null)
_useDelay.TryResetDelay((entity, useDelay));
}
}

View File

@@ -36,7 +36,7 @@ namespace Content.Server.GameTicking
private void InitializeCVars()
{
Subs.CVar(_configurationManager, CCVars.GameLobbyEnabled, value =>
Subs.CVar(_cfg, CCVars.GameLobbyEnabled, value =>
{
LobbyEnabled = value;
foreach (var (userId, status) in _playerGameStatuses)
@@ -47,23 +47,23 @@ namespace Content.Server.GameTicking
LobbyEnabled ? PlayerGameStatus.NotReadyToPlay : PlayerGameStatus.ReadyToPlay;
}
}, true);
Subs.CVar(_configurationManager, CCVars.GameDummyTicker, value => DummyTicker = value, true);
Subs.CVar(_configurationManager, CCVars.GameLobbyDuration, value => LobbyDuration = TimeSpan.FromSeconds(value), true);
Subs.CVar(_configurationManager, CCVars.GameDisallowLateJoins,
Subs.CVar(_cfg, CCVars.GameDummyTicker, value => DummyTicker = value, true);
Subs.CVar(_cfg, CCVars.GameLobbyDuration, value => LobbyDuration = TimeSpan.FromSeconds(value), true);
Subs.CVar(_cfg, CCVars.GameDisallowLateJoins,
value => { DisallowLateJoin = value; UpdateLateJoinStatus(); }, true);
Subs.CVar(_configurationManager, CCVars.AdminLogsServerName, value =>
Subs.CVar(_cfg, CCVars.AdminLogsServerName, value =>
{
// TODO why tf is the server name on admin logs
ServerName = value;
}, true);
Subs.CVar(_configurationManager, CCVars.DiscordRoundUpdateWebhook, value =>
Subs.CVar(_cfg, CCVars.DiscordRoundUpdateWebhook, value =>
{
if (!string.IsNullOrWhiteSpace(value))
{
_discord.GetWebhook(value, data => _webhookIdentifier = data.ToIdentifier());
}
}, true);
Subs.CVar(_configurationManager, CCVars.DiscordRoundEndRoleWebhook, value =>
Subs.CVar(_cfg, CCVars.DiscordRoundEndRoleWebhook, value =>
{
DiscordRoundEndRole = value;
@@ -72,9 +72,9 @@ namespace Content.Server.GameTicking
DiscordRoundEndRole = null;
}
}, true);
Subs.CVar(_configurationManager, CCVars.RoundEndSoundCollection, value => RoundEndSoundCollection = value, true);
Subs.CVar(_cfg, CCVars.RoundEndSoundCollection, value => RoundEndSoundCollection = value, true);
#if EXCEPTION_TOLERANCE
Subs.CVar(_configurationManager, CCVars.RoundStartFailShutdownCount, value => RoundStartFailShutdownCount = value, true);
Subs.CVar(_cfg, CCVars.RoundStartFailShutdownCount, value => RoundStartFailShutdownCount = value, true);
#endif
}
}

View File

@@ -41,9 +41,9 @@ namespace Content.Server.GameTicking
DelayStart(TimeSpan.FromSeconds(PresetFailedCooldownIncrease));
}
if (_configurationManager.GetCVar(CCVars.GameLobbyFallbackEnabled))
if (_cfg.GetCVar(CCVars.GameLobbyFallbackEnabled))
{
var fallbackPresets = _configurationManager.GetCVar(CCVars.GameLobbyFallbackPreset).Split(",");
var fallbackPresets = _cfg.GetCVar(CCVars.GameLobbyFallbackPreset).Split(",");
var startFailed = true;
foreach (var preset in fallbackPresets)
@@ -86,7 +86,7 @@ namespace Content.Server.GameTicking
private void InitializeGamePreset()
{
SetGamePreset(LobbyEnabled ? _configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset) : "sandbox");
SetGamePreset(LobbyEnabled ? _cfg.GetCVar(CCVars.GameLobbyDefaultPreset) : "sandbox");
}
public void SetGamePreset(GamePresetPrototype? preset, bool force = false)
@@ -190,7 +190,7 @@ namespace Content.Server.GameTicking
private void IncrementRoundNumber()
{
var playerIds = _playerGameStatuses.Keys.Select(player => player.UserId).ToArray();
var serverName = _configurationManager.GetCVar(CCVars.AdminLogsServerName);
var serverName = _cfg.GetCVar(CCVars.AdminLogsServerName);
// TODO FIXME AAAAAAAAAAAAAAAAAAAH THIS IS BROKEN
// Task.Run as a terrible dirty workaround to avoid synchronization context deadlock from .Result here.

View File

@@ -1,5 +1,3 @@
using System.Linq;
using Content.Server.Database;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
@@ -9,7 +7,6 @@ using Content.Shared.Preferences;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Enums;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -21,8 +18,6 @@ namespace Content.Server.GameTicking
public sealed partial class GameTicker
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
private void InitializePlayer()
{
@@ -64,7 +59,7 @@ namespace Content.Server.GameTicking
// timer time must be > tick length
Timer.Spawn(0, () => _playerManager.JoinGame(args.Session));
var record = await _dbManager.GetPlayerRecordByUserId(args.Session.UserId);
var record = await _db.GetPlayerRecordByUserId(args.Session.UserId);
var firstConnection = record != null &&
Math.Abs((record.FirstSeenTime - record.LastSeenTime).TotalMinutes) < 1;
@@ -74,8 +69,8 @@ namespace Content.Server.GameTicking
RaiseNetworkEvent(GetConnectionStatusMsg(), session.Channel);
if (firstConnection && _configurationManager.GetCVar(CCVars.AdminNewPlayerJoinSound))
_audioSystem.PlayGlobal(new SoundPathSpecifier("/Audio/Effects/newplayerping.ogg"),
if (firstConnection && _cfg.GetCVar(CCVars.AdminNewPlayerJoinSound))
_audio.PlayGlobal(new SoundPathSpecifier("/Audio/Effects/newplayerping.ogg"),
Filter.Empty().AddPlayers(_adminManager.ActiveAdmins), false,
audioParams: new AudioParams { Volume = -5f });

View File

@@ -127,8 +127,8 @@ public sealed partial class GameTicker
metadata["gamemode"] = new ValueDataNode(CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty);
metadata["roundEndPlayers"] = _serialman.WriteValue(_replayRoundPlayerInfo);
metadata["roundEndText"] = new ValueDataNode(_replayRoundText);
metadata["server_id"] = new ValueDataNode(_configurationManager.GetCVar(CCVars.ServerId));
metadata["server_name"] = new ValueDataNode(_configurationManager.GetCVar(CCVars.AdminLogsServerName));
metadata["server_id"] = new ValueDataNode(_cfg.GetCVar(CCVars.ServerId));
metadata["server_name"] = new ValueDataNode(_cfg.GetCVar(CCVars.AdminLogsServerName));
metadata["roundId"] = new ValueDataNode(RoundId.ToString());
}

View File

@@ -4,6 +4,7 @@ using Content.Server.Discord;
using Content.Server.GameTicking.Events;
using Content.Server.Ghost;
using Content.Server.Maps;
using Content.Server.Roles;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.GameTicking;
@@ -26,6 +27,7 @@ namespace Content.Server.GameTicking
public sealed partial class GameTicker
{
[Dependency] private readonly DiscordWebhook _discord = default!;
[Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
private static readonly Counter RoundNumberMetric = Metrics.CreateCounter(
@@ -190,9 +192,6 @@ namespace Content.Server.GameTicking
if (!_playerManager.TryGetSessionById(userId, out _))
continue;
if (_banManager.GetRoleBans(userId) == null)
continue;
total++;
}
@@ -236,11 +235,7 @@ namespace Content.Server.GameTicking
#if DEBUG
DebugTools.Assert(_userDb.IsLoadComplete(session), $"Player was readied up but didn't have user DB data loaded yet??");
#endif
if (_banManager.GetRoleBans(userId) == null)
{
Logger.ErrorS("RoleBans", $"Role bans for player {session} {userId} have not been loaded yet.");
continue;
}
readyPlayers.Add(session);
HumanoidCharacterProfile profile;
if (_prefsManager.TryGetCachedPreferences(userId, out var preferences))
@@ -339,8 +334,23 @@ namespace Content.Server.GameTicking
RunLevel = GameRunLevel.PostRound;
ShowRoundEndScoreboard(text);
SendRoundEndDiscordMessage();
try
{
ShowRoundEndScoreboard(text);
}
catch (Exception e)
{
Log.Error($"Error while showing round end scoreboard: {e}");
}
try
{
SendRoundEndDiscordMessage();
}
catch (Exception e)
{
Log.Error($"Error while sending round end Discord message: {e}");
}
}
public void ShowRoundEndScoreboard(string text = "")
@@ -364,7 +374,7 @@ namespace Content.Server.GameTicking
var listOfPlayerInfo = new List<RoundEndMessageEvent.RoundEndPlayerInfo>();
// Grab the great big book of all the Minds, we'll need them for this.
var allMinds = EntityQueryEnumerator<MindComponent>();
var pvsOverride = _configurationManager.GetCVar(CCVars.RoundEndPVSOverrides);
var pvsOverride = _cfg.GetCVar(CCVars.RoundEndPVSOverrides);
while (allMinds.MoveNext(out var mindId, out var mind))
{
// TODO don't list redundant observer roles?
@@ -373,7 +383,7 @@ namespace Content.Server.GameTicking
var userId = mind.UserId ?? mind.OriginalOwnerUserId;
var connected = false;
var observer = HasComp<ObserverRoleComponent>(mindId);
var observer = _role.MindHasRole<ObserverRoleComponent>(mindId);
// Continuing
if (userId != null && _playerManager.ValidSessionId(userId.Value))
{
@@ -400,7 +410,7 @@ namespace Content.Server.GameTicking
_pvsOverride.AddGlobalOverride(GetNetEntity(entity.Value), recursive: true);
}
var roles = _roles.MindGetAllRoles(mindId);
var roles = _roles.MindGetAllRoleInfo(mindId);
var playerEndRoundInfo = new RoundEndMessageEvent.RoundEndPlayerInfo()
{

View File

@@ -3,8 +3,6 @@ using System.Linq;
using System.Numerics;
using Content.Server.Administration.Managers;
using Content.Server.GameTicking.Events;
using Content.Server.Ghost;
using Content.Server.Shuttles.Components;
using Content.Server.Spawners.Components;
using Content.Server.Speech.Components;
using Content.Server.Station.Components;
@@ -224,13 +222,12 @@ namespace Content.Server.GameTicking
_mind.SetUserId(newMind, data.UserId);
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
var job = new JobComponent {Prototype = jobId};
_roles.MindAddRole(newMind, job, silent: silent);
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
var jobName = _jobs.MindTryGetJobName(newMind);
_playTimeTrackings.PlayerRolesChanged(player);
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, job, character);
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character);
DebugTools.AssertNotNull(mobMaybe);
var mob = mobMaybe!.Value;
@@ -269,13 +266,17 @@ namespace Content.Server.GameTicking
_stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
if (lateJoin)
{
_adminLogger.Add(LogType.LateJoin,
LogImpact.Medium,
$"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
}
else
{
_adminLogger.Add(LogType.RoundStartJoin,
LogImpact.Medium,
$"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
}
// Make sure they're aware of extended access.
if (Comp<StationJobsComponent>(station).ExtendedAccess
@@ -361,7 +362,7 @@ namespace Content.Server.GameTicking
var (mindId, mindComp) = _mind.CreateMind(player.UserId, name);
mind = (mindId, mindComp);
_mind.SetUserId(mind.Value, player.UserId);
_roles.MindAddRole(mind.Value, new ObserverRoleComponent());
_roles.MindAddRole(mind.Value, "MindRoleObserver");
}
var ghost = _ghost.SpawnGhost(mind.Value);

View File

@@ -8,19 +8,15 @@ using Content.Server.Maps;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Preferences.Managers;
using Content.Server.ServerUpdates;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Chat;
using Content.Shared.Damage;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Roles;
using Robust.Server;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
@@ -39,7 +35,6 @@ namespace Content.Server.GameTicking
[Dependency] private readonly IBanManager _banManager = default!;
[Dependency] private readonly IBaseServer _baseServer = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IConsoleHost _consoleHost = default!;
[Dependency] private readonly IGameMapManager _gameMapManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;

View File

@@ -1,4 +1,5 @@
using Content.Shared.Dataset;
using Content.Shared.FixedPoint;
using Content.Shared.NPC.Prototypes;
using Content.Shared.Random;
using Content.Shared.Roles;
@@ -31,6 +32,24 @@ public sealed partial class TraitorRuleComponent : Component
[DataField]
public ProtoId<DatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
/// <summary>
/// Give this traitor an Uplink on spawn.
/// </summary>
[DataField]
public bool GiveUplink = true;
/// <summary>
/// Give this traitor the codewords.
/// </summary>
[DataField]
public bool GiveCodewords = true;
/// <summary>
/// Give this traitor a briefing in chat.
/// </summary>
[DataField]
public bool GiveBriefing = true;
public int TotalTraitors => TraitorMinds.Count;
public string[] Codewords = new string[3];
@@ -68,5 +87,5 @@ public sealed partial class TraitorRuleComponent : Component
/// The amount of TC traitors start with.
/// </summary>
[DataField]
public int StartingBalance = 20;
public FixedPoint2 StartingBalance = 20;
}

View File

@@ -9,7 +9,6 @@ using Content.Server.RoundEnd;
using Content.Server.Shuttles.Events;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mobs;
@@ -31,9 +30,9 @@ namespace Content.Server.GameTicking.Rules;
public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergency = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
[Dependency] private readonly StoreSystem _store = default!;
@@ -57,6 +56,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
SubscribeLocalEvent<NukeOperativeComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<NukeOperativeComponent, EntityZombifiedEvent>(OnOperativeZombified);
SubscribeLocalEvent<NukeopsRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<ConsoleFTLAttemptEvent>(OnShuttleFTLAttempt);
SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared);
SubscribeLocalEvent<CommunicationConsoleCallShuttleAttemptEvent>(OnShuttleCallAttempt);
@@ -65,7 +66,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
SubscribeLocalEvent<NukeopsRuleComponent, RuleLoadedGridsEvent>(OnRuleLoadedGrids);
}
protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
protected override void Started(EntityUid uid,
NukeopsRuleComponent component,
GameRuleComponent gameRule,
GameRuleStartedEvent args)
{
var eligible = new List<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
@@ -85,7 +88,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
}
#region Event Handlers
protected override void AppendRoundEndText(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
protected override void AppendRoundEndText(EntityUid uid,
NukeopsRuleComponent component,
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
var winText = Loc.GetString($"nukeops-{component.WinType.ToString().ToLower()}");
@@ -227,7 +232,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
// If the disk is currently at Central Command, the crew wins - just slightly.
// This also implies that some nuclear operatives have died.
SetWinType(ent, diskAtCentCom
SetWinType(ent,
diskAtCentCom
? WinType.CrewMinor
: WinType.OpsMinor);
ent.Comp.WinConditions.Add(diskAtCentCom
@@ -456,8 +462,11 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
: WinCondition.AllNukiesDead);
SetWinType(ent, WinType.CrewMajor, false);
_roundEndSystem.DoRoundEndBehavior(
nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement);
_roundEndSystem.DoRoundEndBehavior(nukeops.RoundEndBehavior,
nukeops.EvacShuttleTime,
nukeops.RoundEndTextSender,
nukeops.RoundEndTextShuttleCall,
nukeops.RoundEndTextAnnouncement);
// prevent it called multiple times
nukeops.RoundEndBehavior = RoundEndBehavior.Nothing;
@@ -465,16 +474,22 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
private void OnAfterAntagEntSelected(Entity<NukeopsRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (ent.Comp.TargetStation is not { } station)
return;
var target = (ent.Comp.TargetStation is not null) ? Name(ent.Comp.TargetStation.Value) : "the target";
_antag.SendBriefing(args.Session, Loc.GetString("nukeops-welcome",
("station", station),
_antag.SendBriefing(args.Session,
Loc.GetString("nukeops-welcome",
("station", target),
("name", Name(ent))),
Color.Red,
ent.Comp.GreetSoundNotification);
}
private void OnGetBriefing(Entity<NukeopsRoleComponent> role, ref GetBriefingEvent args)
{
// TODO Different character screen briefing for the 3 nukie types
args.Append(Loc.GetString("nukeops-briefing"));
}
/// <remarks>
/// Is this method the shitty glue holding together the last of my sanity? yes.
/// Do i have a better solution? not presently.

View File

@@ -15,7 +15,6 @@ using Content.Shared.Database;
using Content.Shared.GameTicking.Components;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mindshield.Components;
using Content.Shared.Mobs;
@@ -38,8 +37,8 @@ namespace Content.Server.GameTicking.Rules;
public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleComponent>
{
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly EuiManager _euiMan = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
@@ -49,7 +48,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
[Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly IGameTiming _timing = default!;
//Used in OnPostFlash, no reference to the rule component is available
public readonly ProtoId<NpcFactionPrototype> RevolutionaryNpcFaction = "Revolutionary";
@@ -59,9 +58,12 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
{
base.Initialize();
SubscribeLocalEvent<CommandStaffComponent, MobStateChangedEvent>(OnCommandMobStateChanged);
SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(OnPostFlash);
SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
}
protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
@@ -85,7 +87,9 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
}
}
protected override void AppendRoundEndText(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule,
protected override void AppendRoundEndText(EntityUid uid,
RevolutionaryRuleComponent component,
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
base.AppendRoundEndText(uid, component, gameRule, ref args);
@@ -101,7 +105,9 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
args.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", sessionData.Count)));
foreach (var (mind, data, name) in sessionData)
{
var count = CompOrNull<RevolutionaryRoleComponent>(mind)?.ConvertedCount ?? 0;
_role.MindHasRole<RevolutionaryRoleComponent>(mind, out var role);
var count = CompOrNull<RevolutionaryRoleComponent>(role)?.ConvertedCount ?? 0;
args.AddLine(Loc.GetString("rev-headrev-name-user",
("name", name),
("username", data.UserName),
@@ -113,10 +119,8 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
private void OnGetBriefing(EntityUid uid, RevolutionaryRoleComponent comp, ref GetBriefingEvent args)
{
if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
return;
var head = HasComp<HeadRevolutionaryComponent>(mind.OwnedEntity);
var ent = args.Mind.Comp.OwnedEntity;
var head = HasComp<HeadRevolutionaryComponent>(ent);
args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing"));
}
@@ -145,15 +149,20 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
if (ev.User != null)
{
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
_adminLogManager.Add(LogType.Mind,
LogImpact.Medium,
$"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
if (_mind.TryGetRole<RevolutionaryRoleComponent>(ev.User.Value, out var headrev))
headrev.ConvertedCount++;
if (_mind.TryGetMind(ev.User.Value, out var revMindId, out _))
{
if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out var role))
role.Value.Comp2.ConvertedCount++;
}
}
if (mindId == default || !_role.MindHasRole<RevolutionaryRoleComponent>(mindId))
{
_role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = RevPrototypeId });
_role.MindAddRole(mindId, "MindRoleRevolutionary");
}
if (mind?.Session != null)

View File

@@ -1,18 +1,12 @@
using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Objectives;
using Content.Server.Roles;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
{
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
public override void Initialize()
@@ -24,32 +18,33 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
}
private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
// Greeting upon thief activation
private void AfterAntagSelected(Entity<ThiefRuleComponent> mindId, ref AfterAntagEntitySelectedEvent args)
{
if (!_mindSystem.TryGetMind(args.EntityUid, out var mindId, out var mind))
return;
//Generate objectives
_antag.SendBriefing(args.EntityUid, MakeBriefing(args.EntityUid), null, null);
var ent = args.EntityUid;
_antag.SendBriefing(ent, MakeBriefing(ent), null, null);
}
//Add mind briefing
private void OnGetBriefing(Entity<ThiefRoleComponent> thief, ref GetBriefingEvent args)
// Character screen briefing
private void OnGetBriefing(Entity<ThiefRoleComponent> role, ref GetBriefingEvent args)
{
if (!TryComp<MindComponent>(thief.Owner, out var mind) || mind.OwnedEntity == null)
return;
var ent = args.Mind.Comp.OwnedEntity;
args.Append(MakeBriefing(mind.OwnedEntity.Value));
if (ent is null)
return;
args.Append(MakeBriefing(ent.Value));
}
private string MakeBriefing(EntityUid thief)
private string MakeBriefing(EntityUid ent)
{
var isHuman = HasComp<HumanoidAppearanceComponent>(thief);
var isHuman = HasComp<HumanoidAppearanceComponent>(ent);
var briefing = isHuman
? Loc.GetString("thief-role-greeting-human")
: Loc.GetString("thief-role-greeting-animal");
briefing += "\n \n" + Loc.GetString("thief-role-greeting-equipment") + "\n";
if (isHuman)
briefing += "\n \n" + Loc.GetString("thief-role-greeting-equipment") + "\n";
return briefing;
}
}

View File

@@ -1,3 +1,4 @@
using Content.Server.Administration.Logs;
using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
@@ -5,12 +6,12 @@ using Content.Server.Objectives;
using Content.Server.PDA.Ringer;
using Content.Server.Roles;
using Content.Server.Traitor.Uplink;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.NPC.Systems;
using Content.Shared.Objectives.Components;
using Content.Shared.PDA;
using Content.Shared.Radio;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Roles.RoleCodeword;
@@ -25,39 +26,43 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
{
private static readonly Color TraitorCodewordColor = Color.FromHex("#cc3b3b");
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly SharedRoleCodewordSystem _roleCodewordSystem = default!;
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
Log.Level = LogLevel.Debug;
SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
}
protected override void Added(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, component, gameRule, args);
SetCodewords(component);
SetCodewords(component, args.RuleEntity);
}
private void AfterEntitySelected(Entity<TraitorRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
Log.Debug($"AfterAntagEntitySelected {ToPrettyString(ent)}");
MakeTraitor(args.EntityUid, ent);
}
private void SetCodewords(TraitorRuleComponent component)
private void SetCodewords(TraitorRuleComponent component, EntityUid ruleEntity)
{
component.Codewords = GenerateTraitorCodewords(component);
_adminLogger.Add(LogType.EventStarted, LogImpact.Low, $"Codewords generated for game rule {ToPrettyString(ruleEntity)}: {string.Join(", ", component.Codewords)}");
}
public string[] GenerateTraitorCodewords(TraitorRuleComponent component)
@@ -74,47 +79,82 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
return codewords;
}
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true)
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component)
{
//Grab the mind if it wasnt provided
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - start");
//Grab the mind if it wasn't provided
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
return false;
var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers).Values);
Note[]? code = null;
if (giveUplink)
{
// Calculate the amount of currency on the uplink.
var startingBalance = component.StartingBalance;
if (_jobs.MindTryGetJob(mindId, out _, out var prototype))
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
// creadth: we need to create uplink for the antag.
// PDA should be in place already
var pda = _uplink.FindUplinkTarget(traitor);
if (pda == null || !_uplink.AddUplink(traitor, startingBalance, giveDiscounts: true))
return false;
// Give traitors their codewords and uplink code to keep in their character info menu
code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
// If giveUplink is false the uplink code part is omitted
briefing = string.Format("{0}\n{1}", briefing,
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - failed, no Mind found");
return false;
}
_antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code, issuer), null, component.GreetSoundNotification);
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);
// Uplink code will go here if applicable, but we still need the variable if there aren't any
Note[]? code = null;
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))
{
if (startingBalance < prototype.AntagAdvantage) // Can't use Math functions on FixedPoint2
startingBalance = 0;
else
startingBalance = startingBalance - prototype.AntagAdvantage;
}
// 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
_roleSystem.MindAddRole(mindId, new RoleBriefingComponent
//Since this provides neither an antag/job prototype, nor antag status/roletype,
//and is intrinsically related to the traitor role
//it does not need to be a separate Mind Role Entity
_roleSystem.MindHasRole<TraitorRoleComponent>(mindId, out var traitorRole);
if (traitorRole is not null)
{
Briefing = briefing
}, mind, true);
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
@@ -123,26 +163,62 @@ 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;
}
private (Note[]?, string) RequestUplink(EntityUid traitor, FixedPoint2 startingBalance, string briefing)
{
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;
// If giveUplink is false the uplink code part is omitted
briefing = string.Format("{0}\n{1}",
briefing,
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
return (code, briefing);
}
else if (pda is null && uplinked)
{
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is implant");
briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short");
}
return (null, briefing);
}
// TODO: AntagCodewordsComponent
private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args)
{
args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
if(comp.GiveCodewords)
args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
}
// TODO: figure out how to handle this? add priority to briefing event?
private string GenerateBriefing(string[] codewords, Note[]? uplinkCode, string? objectiveIssuer = null)
private string GenerateBriefing(string[]? codewords, Note[]? uplinkCode, string? objectiveIssuer = null)
{
var sb = new StringBuilder();
sb.AppendLine(Loc.GetString("traitor-role-greeting", ("corporation", objectiveIssuer ?? Loc.GetString("objective-issuer-unknown"))));
sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords))));
if (codewords != null)
sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords))));
if (uplinkCode != null)
sb.AppendLine(Loc.GetString("traitor-role-uplink-code", ("code", string.Join("-", uplinkCode).Replace("sharp", "#"))));
else
sb.AppendLine(Loc.GetString("traitor-role-uplink-implant"));
return sb.ToString();
}

View File

@@ -13,6 +13,7 @@ using Content.Shared.Mind;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Roles;
using Content.Shared.Zombies;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -22,15 +23,16 @@ namespace Content.Server.GameTicking.Rules;
public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
{
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
public override void Initialize()
{
@@ -41,23 +43,20 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
SubscribeLocalEvent<IncurableZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
}
private void OnGetBriefing(EntityUid uid, InitialInfectedRoleComponent component, ref GetBriefingEvent args)
private void OnGetBriefing(Entity<InitialInfectedRoleComponent> role, ref GetBriefingEvent args)
{
if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
return;
if (HasComp<ZombieRoleComponent>(uid)) // don't show both briefings
return;
args.Append(Loc.GetString("zombie-patientzero-role-greeting"));
if (!_roles.MindHasRole<ZombieRoleComponent>(args.Mind.Owner))
args.Append(Loc.GetString("zombie-patientzero-role-greeting"));
}
private void OnGetBriefing(EntityUid uid, ZombieRoleComponent component, ref GetBriefingEvent args)
private void OnGetBriefing(Entity<ZombieRoleComponent> role, ref GetBriefingEvent args)
{
if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
return;
args.Append(Loc.GetString("zombie-infection-greeting"));
}
protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule,
protected override void AppendRoundEndText(EntityUid uid,
ZombieRuleComponent component,
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
base.AppendRoundEndText(uid, component, gameRule, ref args);

View File

@@ -46,11 +46,9 @@ namespace Content.Server.Ghost
[Dependency] private readonly JobSystem _jobs = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly MindSystem _minds = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
@@ -170,7 +168,7 @@ namespace Content.Server.Ghost
// Allow this entity to be seen by other ghosts.
var visibility = EnsureComp<VisibilityComponent>(uid);
if (_ticker.RunLevel != GameRunLevel.PostRound)
if (_gameTicker.RunLevel != GameRunLevel.PostRound)
{
_visibilitySystem.AddLayer((uid, visibility), (int) VisibilityFlags.Ghost, false);
_visibilitySystem.RemoveLayer((uid, visibility), (int) VisibilityFlags.Normal, false);
@@ -215,14 +213,7 @@ namespace Content.Server.Ghost
private void OnMapInit(EntityUid uid, GhostComponent component, MapInitEvent args)
{
if (_actions.AddAction(uid, ref component.BooActionEntity, out var act, component.BooAction)
&& act.UseDelay != null)
{
var start = _gameTiming.CurTime;
var end = start + act.UseDelay.Value;
_actions.SetCooldown(component.BooActionEntity.Value, start, end);
}
_actions.AddAction(uid, ref component.BooActionEntity, component.BooAction);
_actions.AddAction(uid, ref component.ToggleGhostHearingActionEntity, component.ToggleGhostHearingAction);
_actions.AddAction(uid, ref component.ToggleLightingActionEntity, component.ToggleLightingAction);
_actions.AddAction(uid, ref component.ToggleFoVActionEntity, component.ToggleFoVAction);
@@ -277,7 +268,7 @@ namespace Content.Server.Ghost
return;
}
_mindSystem.UnVisit(actor.PlayerSession);
_mind.UnVisit(actor.PlayerSession);
}
#region Warp
@@ -455,7 +446,7 @@ namespace Content.Server.Ghost
spawnPosition = null;
// If it's bad, look for a valid point to spawn
spawnPosition ??= _ticker.GetObserverSpawnPoint();
spawnPosition ??= _gameTicker.GetObserverSpawnPoint();
// Make sure the new point is valid too
if (!IsValidSpawnPosition(spawnPosition))

View File

@@ -1,11 +1,13 @@
namespace Content.Server.Ghost.Roles;
using Content.Shared.Roles;
namespace Content.Server.Ghost.Roles;
/// <summary>
/// This is used for round end display of ghost roles.
/// It may also be used to ensure some ghost roles count as antagonists in future.
/// </summary>
[RegisterComponent]
public sealed partial class GhostRoleMarkerRoleComponent : Component
public sealed partial class GhostRoleMarkerRoleComponent : BaseMindRoleComponent
{
[DataField("name")] public string? Name;
}

View File

@@ -71,18 +71,22 @@ public sealed class GhostRoleSystem : EntitySystem
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<GhostTakeoverAvailableComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<GhostTakeoverAvailableComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<GhostTakeoverAvailableComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<GhostTakeoverAvailableComponent, TakeGhostRoleEvent>(OnTakeoverTakeRole);
SubscribeLocalEvent<GhostRoleComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<GhostRoleComponent, ComponentStartup>(OnRoleStartup);
SubscribeLocalEvent<GhostRoleComponent, ComponentShutdown>(OnRoleShutdown);
SubscribeLocalEvent<GhostRoleComponent, EntityPausedEvent>(OnPaused);
SubscribeLocalEvent<GhostRoleComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentInit>(OnRaffleInit);
SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentShutdown>(OnRaffleShutdown);
SubscribeLocalEvent<GhostRoleMobSpawnerComponent, TakeGhostRoleEvent>(OnSpawnerTakeRole);
SubscribeLocalEvent<GhostTakeoverAvailableComponent, TakeGhostRoleEvent>(OnTakeoverTakeRole);
SubscribeLocalEvent<GhostRoleMobSpawnerComponent, GetVerbsEvent<Verb>>(OnVerb);
SubscribeLocalEvent<GhostRoleMobSpawnerComponent, GhostRoleRadioMessage>(OnGhostRoleRadioMessage);
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
@@ -509,7 +513,11 @@ public sealed class GhostRoleSystem : EntitySystem
var newMind = _mindSystem.CreateMind(player.UserId,
EntityManager.GetComponent<MetaDataComponent>(mob).EntityName);
_roleSystem.MindAddRole(newMind, new GhostRoleMarkerRoleComponent { Name = role.RoleName });
_roleSystem.MindAddRole(newMind, "MindRoleGhostMarker");
if(_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind!, out var markerRole))
markerRole.Value.Comp2.Name = role.RoleName;
_mindSystem.SetUserId(newMind, player.UserId);
_mindSystem.TransferTo(newMind, mob);
@@ -602,10 +610,7 @@ public sealed class GhostRoleSystem : EntitySystem
if (ghostRole.JobProto != null)
{
if (HasComp<JobComponent>(args.Mind))
_roleSystem.MindRemoveRole<JobComponent>(args.Mind);
_roleSystem.MindAddRole(args.Mind, new JobComponent { Prototype = ghostRole.JobProto });
_roleSystem.MindAddJobRole(args.Mind, args.Mind, silent:false,ghostRole.JobProto);
}
ghostRole.Taken = true;

View File

@@ -45,7 +45,7 @@ public sealed class HolosignSystem : EntitySystem
if (args.Handled
|| !args.CanReach // prevent placing out of range
|| HasComp<StorageComponent>(args.Target) // if it's a storage component like a bag, we ignore usage so it can be stored
|| !_powerCell.TryUseCharge(uid, component.ChargeUse) // if no battery or no charge, doesn't work
|| !_powerCell.TryUseCharge(uid, component.ChargeUse, user: args.User) // if no battery or no charge, doesn't work
)
return;

View File

@@ -166,7 +166,7 @@ public sealed class IdentitySystem : SharedIdentitySystem
if (_idCard.TryFindIdCard(target, out var id))
{
presumedName = string.IsNullOrWhiteSpace(id.Comp.FullName) ? null : id.Comp.FullName;
presumedJob = id.Comp.JobTitle?.ToLowerInvariant();
presumedJob = id.Comp.LocalizedJobTitle?.ToLowerInvariant();
}
// If it didn't find a job, that's fine.

View File

@@ -25,6 +25,7 @@ public sealed class ImmovableRodSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
public override void Update(float frameTime)
{
@@ -39,7 +40,7 @@ public sealed class ImmovableRodSystem : EntitySystem
if (!TryComp<MapGridComponent>(trans.GridUid, out var grid))
continue;
grid.SetTile(trans.Coordinates, Tile.Empty);
_map.SetTile(trans.GridUid.Value, grid, trans.Coordinates, Tile.Empty);
}
}

View File

@@ -74,14 +74,12 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
return;
// same as store code, but message is only shown to yourself
args.Handled = _store.TryAddCurrency(_store.GetCurrencyValue(args.Used, currency), uid, store);
if (!args.Handled)
if (!_store.TryAddCurrency((args.Used, currency), (uid, store)))
return;
args.Handled = true;
var msg = Loc.GetString("store-currency-inserted-implant", ("used", args.Used));
_popup.PopupEntity(msg, args.User, args.User);
QueueDel(args.Used);
}
private void OnFreedomImplant(EntityUid uid, SubdermalImplantComponent component, UseFreedomImplantEvent args)

View File

@@ -7,6 +7,7 @@ using Content.Server.Chat.Managers;
using Content.Server.Connection;
using Content.Server.Database;
using Content.Server.Discord;
using Content.Server.Discord.WebhookMessages;
using Content.Server.EUI;
using Content.Server.GhostKick;
using Content.Server.Info;
@@ -14,8 +15,6 @@ using Content.Server.Mapping;
using Content.Server.Maps;
using Content.Server.MoMMI;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Objectives;
using Content.Server.Players;
using Content.Server.Players.JobWhitelist;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Players.RateLimiting;
@@ -26,8 +25,10 @@ using Content.Server.Voting.Managers;
using Content.Server.Worldgen.Tools;
using Content.Shared.Administration.Logs;
using Content.Shared.Administration.Managers;
using Content.Shared.Chat;
using Content.Shared.Kitchen;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Players.RateLimiting;
namespace Content.Server.IoC
{
@@ -36,6 +37,7 @@ namespace Content.Server.IoC
public static void Register()
{
IoCManager.Register<IChatManager, ChatManager>();
IoCManager.Register<ISharedChatManager, ChatManager>();
IoCManager.Register<IChatSanitizationManager, ChatSanitizationManager>();
IoCManager.Register<IMoMMILink, MoMMILink>();
IoCManager.Register<IServerPreferencesManager, ServerPreferencesManager>();
@@ -63,11 +65,13 @@ namespace Content.Server.IoC
IoCManager.Register<ServerInfoManager>();
IoCManager.Register<PoissonDiskSampler>();
IoCManager.Register<DiscordWebhook>();
IoCManager.Register<VoteWebhooks>();
IoCManager.Register<ServerDbEntryManager>();
IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
IoCManager.Register<ServerApi>();
IoCManager.Register<JobWhitelistManager>();
IoCManager.Register<PlayerRateLimitManager>();
IoCManager.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
IoCManager.Register<MappingManager>();
}
}

View File

@@ -5,6 +5,7 @@ using Content.Server.Stack;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Destructible;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Kitchen;
@@ -122,6 +123,9 @@ namespace Content.Server.Kitchen.EntitySystems
if (solution.Volume > containerSolution.AvailableVolume)
continue;
var dev = new DestructionEventArgs();
RaiseLocalEvent(item, dev);
QueueDel(item);
}

View File

@@ -4,7 +4,6 @@ using Content.Server.CartridgeLoader;
using Content.Server.CartridgeLoader.Cartridges;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Interaction;
using Content.Server.MassMedia.Components;
using Content.Server.Popups;
using Content.Server.Station.Systems;
@@ -27,7 +26,6 @@ public sealed class NewsSystem : SharedNewsSystem
{
[Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly InteractionSystem _interaction = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoaderSystem = default!;
@@ -35,7 +33,6 @@ public sealed class NewsSystem : SharedNewsSystem
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
public override void Initialize()

View File

@@ -175,6 +175,10 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
var materialPerStack = composition.MaterialComposition[materialProto.ID];
var amountToSpawn = amount / materialPerStack;
overflowMaterial = amount - amountToSpawn * materialPerStack;
if (amountToSpawn == 0)
return new List<EntityUid>();
return _stackSystem.SpawnMultiple(materialProto.StackEntity, amountToSpawn, coordinates);
}

View File

@@ -54,7 +54,7 @@ public sealed partial class HealthAnalyzerComponent : Component
/// Sound played on scanning end
/// </summary>
[DataField]
public SoundSpecifier? ScanningEndSound;
public SoundSpecifier ScanningEndSound = new SoundPathSpecifier("/Audio/Items/Medical/healthscanner.ogg");
/// <summary>
/// Whether to show up the popup

View File

@@ -1,16 +1,13 @@
using Content.Server.Administration.Logs;
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Medical.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Atmos;
@@ -18,7 +15,6 @@ using Content.Shared.UserInterface;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Climbing.Systems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
@@ -46,7 +42,6 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
@@ -196,7 +191,7 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
}
// TODO: This should be a state my dude
_userInterfaceSystem.ServerSendUiMessage(
_uiSystem.ServerSendUiMessage(
entity.Owner,
HealthAnalyzerUiKey.Key,
new HealthAnalyzerScannedUserMessage(GetNetEntity(entity.Comp.BodyContainer.ContainedEntity),
@@ -206,6 +201,7 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
? bloodSolution.FillFraction
: 0,
null,
null,
null
));
}

View File

@@ -2,6 +2,7 @@ using Content.Server.Body.Components;
using Content.Server.Medical.Components;
using Content.Server.PowerCell;
using Content.Server.Temperature.Components;
using Content.Server.Traits.Assorted;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
@@ -13,11 +14,9 @@ using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.MedicalScanner;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
using Content.Shared.PowerCell;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server.Medical;
@@ -104,7 +103,8 @@ public sealed class HealthAnalyzerSystem : EntitySystem
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
return;
_audio.PlayPvs(uid.Comp.ScanningEndSound, uid);
if (!uid.Comp.Silent)
_audio.PlayPvs(uid.Comp.ScanningEndSound, uid);
OpenUserInterface(args.User, uid);
BeginAnalyzingEntity(uid, args.Target.Value);
@@ -197,6 +197,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
var bloodAmount = float.NaN;
var bleeding = false;
var unrevivable = false;
if (TryComp<BloodstreamComponent>(target, out var bloodstream) &&
_solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName,
@@ -206,12 +207,16 @@ public sealed class HealthAnalyzerSystem : EntitySystem
bleeding = bloodstream.BleedAmount > 0;
}
if (HasComp<UnrevivableComponent>(target))
unrevivable = true;
_uiSystem.ServerSendUiMessage(healthAnalyzer, HealthAnalyzerUiKey.Key, new HealthAnalyzerScannedUserMessage(
GetNetEntity(target),
bodyTemperature,
bloodAmount,
scanMode,
bleeding
bleeding,
unrevivable
));
}
}

View File

@@ -363,8 +363,8 @@ public sealed class SuitSensorSystem : EntitySystem
{
if (card.Comp.FullName != null)
userName = card.Comp.FullName;
if (card.Comp.JobTitle != null)
userJob = card.Comp.JobTitle;
if (card.Comp.LocalizedJobTitle != null)
userJob = card.Comp.LocalizedJobTitle;
userJobIcon = card.Comp.JobIcon;
foreach (var department in card.Comp.JobDepartments)

View File

@@ -43,7 +43,7 @@ namespace Content.Server.Mind.Commands
builder.AppendFormat("player: {0}, mob: {1}\nroles: ", mind.UserId, mind.OwnedEntity);
var roles = _entities.System<SharedRoleSystem>();
foreach (var role in roles.MindGetAllRoles(mindId))
foreach (var role in roles.MindGetAllRoleInfo(mindId))
{
builder.AppendFormat("{0} ", role.Name);
}

View File

@@ -1,5 +0,0 @@
using Content.Shared.Movement.Systems;
namespace Content.Server.Movement.Systems;
public sealed class WaddleAnimationSystem : SharedWaddleAnimationSystem;

View File

@@ -0,0 +1,9 @@
namespace Content.Server.NPC.Queries.Considerations;
/// <summary>
/// Returns 1f if the target is on fire or 0f if not.
/// </summary>
public sealed partial class TargetOnFireCon : UtilityConsideration
{
}

View File

@@ -1,3 +1,4 @@
using Content.Server.Atmos.Components;
using Content.Server.Fluids.EntitySystems;
using Content.Server.NPC.Queries;
using Content.Server.NPC.Queries.Considerations;
@@ -351,6 +352,12 @@ public sealed class NPCUtilitySystem : EntitySystem
return 0f;
}
case TargetOnFireCon:
{
if (TryComp(targetUid, out FlammableComponent? fire) && fire.OnFire)
return 1f;
return 0f;
}
default:
throw new NotImplementedException();
}

View File

@@ -14,6 +14,7 @@ public sealed class NameIdentifierSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly ILogManager _logManager = default!;
/// <summary>
/// Free IDs available per <see cref="NameIdentifierGroupPrototype"/>.
@@ -118,23 +119,39 @@ public sealed class NameIdentifierSystem : EntitySystem
private void InitialSetupPrototypes()
{
foreach (var proto in _prototypeManager.EnumeratePrototypes<NameIdentifierGroupPrototype>())
{
AddGroup(proto);
}
EnsureIds();
}
private void AddGroup(NameIdentifierGroupPrototype proto)
private void FillGroup(NameIdentifierGroupPrototype proto, List<int> values)
{
var values = new List<int>(proto.MaxValue - proto.MinValue);
values.Clear();
for (var i = proto.MinValue; i < proto.MaxValue; i++)
{
values.Add(i);
}
_robustRandom.Shuffle(values);
CurrentIds.Add(proto.ID, values);
}
private List<int> GetOrCreateIdList(NameIdentifierGroupPrototype proto)
{
if (!CurrentIds.TryGetValue(proto.ID, out var ids))
{
ids = new List<int>(proto.MaxValue - proto.MinValue);
CurrentIds.Add(proto.ID, ids);
}
return ids;
}
private void EnsureIds()
{
foreach (var proto in _prototypeManager.EnumeratePrototypes<NameIdentifierGroupPrototype>())
{
var ids = GetOrCreateIdList(proto);
FillGroup(proto, ids);
}
}
private void OnReloadPrototypes(PrototypesReloadedEventArgs ev)
@@ -159,19 +176,20 @@ public sealed class NameIdentifierSystem : EntitySystem
foreach (var proto in set.Modified.Values)
{
var name_proto = (NameIdentifierGroupPrototype) proto;
// Only bother adding new ones.
if (CurrentIds.ContainsKey(proto.ID))
continue;
AddGroup((NameIdentifierGroupPrototype) proto);
var ids = GetOrCreateIdList(name_proto);
FillGroup(name_proto, ids);
}
}
private void CleanupIds(RoundRestartCleanupEvent ev)
{
foreach (var values in CurrentIds.Values)
{
_robustRandom.Shuffle(values);
}
EnsureIds();
}
}

View File

@@ -22,6 +22,9 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
// How much the cell score should be increased per 1 AutoRechargeRate.
private const int AutoRechargeValue = 100;
public override void Initialize()
{
base.Initialize();
@@ -59,15 +62,26 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
return;
// no power cell for some reason??? allow it
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery))
if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery))
return;
// can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power
if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting) || inserting.MaxCharge <= battery.MaxCharge)
if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting))
{
args.Cancel();
return;
}
var user = Transform(uid).ParentUid;
// can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power
if (GetCellScore(inserting.Owner, inserting) <= GetCellScore(battery.Owner, battery))
{
args.Cancel();
Popup.PopupEntity(Loc.GetString("ninja-cell-downgrade"), user, user);
return;
}
// tell ninja abilities that use battery to update it so they don't use charge from the old one
var user = Transform(uid).ParentUid;
if (!_ninja.IsNinja(user))
return;
@@ -76,6 +90,16 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
RaiseLocalEvent(user, ref ev);
}
// this function assigns a score to a power cell depending on the capacity, to be used when comparing which cell is better.
private float GetCellScore(EntityUid uid, BatteryComponent battcomp)
{
// if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate,
// this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high.
if (TryComp<BatterySelfRechargerComponent>(uid, out var selfcomp) && selfcomp.AutoRecharge)
return battcomp.MaxCharge + (selfcomp.AutoRechargeRate*AutoRechargeValue);
return battcomp.MaxCharge;
}
private void OnEmpAttempt(EntityUid uid, NinjaSuitComponent comp, EmpAttemptEvent args)
{
// ninja suit (battery) is immune to emp

View File

@@ -1,14 +1,12 @@
using Content.Server.Explosion.EntitySystems;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Objectives.Components;
using Content.Server.Popups;
using Content.Server.Roles;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Roles;
using Content.Shared.Sticky;
using Robust.Shared.GameObjects;
namespace Content.Server.Ninja.Systems;
@@ -19,6 +17,7 @@ public sealed class SpiderChargeSystem : SharedSpiderChargeSystem
{
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SpaceNinjaSystem _ninja = default!;
@@ -41,7 +40,10 @@ public sealed class SpiderChargeSystem : SharedSpiderChargeSystem
var user = args.User;
if (!_mind.TryGetRole<NinjaRoleComponent>(user, out var _))
if (!_mind.TryGetMind(args.User, out var mind, out _))
return;
if (!_role.MindHasRole<NinjaRoleComponent>(mind))
{
_popup.PopupEntity(Loc.GetString("spider-charge-not-ninja"), user, user);
args.Cancelled = true;

Some files were not shown because too many files have changed in this diff Show More