Merge remote-tracking branch 'upstream/stable' into ed-29-10-2024-upstream

# Conflicts:
#	Content.Server/Chat/Managers/ChatSanitizationManager.cs
#	Content.Server/Temperature/Systems/TemperatureSystem.cs
#	Content.Shared/Localizations/ContentLocalizationManager.cs
This commit is contained in:
Ed
2024-10-29 11:16:56 +03:00
388 changed files with 34535 additions and 18822 deletions

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,10 +642,11 @@ 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",
@@ -629,13 +657,14 @@ public sealed partial class AdminVerbSystem
_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

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

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

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

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

View File

@@ -1,15 +1,18 @@
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()
{
// CP14-RU-Localization-Start
{ "лол", "chatsan-laughs" },
@@ -60,7 +63,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" },
@@ -102,7 +105,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" },
@@ -115,9 +118,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()
@@ -125,29 +131,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

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

@@ -746,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);
@@ -756,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

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

@@ -875,10 +875,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

@@ -26,9 +26,17 @@ public sealed partial class JobCondition : EntityEffectCondition
if(!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
continue;
if(!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole)
|| mindRole.JobPrototype is null)
if (!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole))
{
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;

View File

@@ -192,9 +192,6 @@ namespace Content.Server.GameTicking
if (!_playerManager.TryGetSessionById(userId, out _))
continue;
if (_banManager.GetRoleBans(userId) == null)
continue;
total++;
}
@@ -238,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))

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

@@ -155,8 +155,8 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
if (_mind.TryGetMind(ev.User.Value, out var revMindId, out _))
{
if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out _, out var role))
role.Value.Comp.ConvertedCount++;
if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out var role))
role.Value.Comp2.ConvertedCount++;
}
}

View File

@@ -7,6 +7,7 @@ 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;
@@ -75,38 +76,46 @@ 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 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 briefing = "";
if (component.GiveCodewords)
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 (giveUplink)
if (component.GiveUplink)
{
// Calculate the amount of currency on the uplink.
var startingBalance = component.StartingBalance;
if (_jobs.MindTryGetJob(mindId, out var prototype))
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
{
if (startingBalance < prototype.AntagAdvantage) // Can't use Math functions on FixedPoint2
startingBalance = 0;
else
startingBalance = startingBalance - prototype.AntagAdvantage;
}
// 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", "#"))));
// Choose and generate an Uplink, and return the uplink code if applicable
var uplinkParams = RequestUplink(traitor, startingBalance, briefing);
code = uplinkParams.Item1;
briefing = uplinkParams.Item2;
}
_antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code, issuer), null, component.GreetSoundNotification);
string[]? codewords = null;
if (component.GiveCodewords)
codewords = component.Codewords;
if (component.GiveBriefing)
_antag.SendBriefing(traitor, GenerateBriefing(codewords, code, issuer), null, component.GreetSoundNotification);
component.TraitorMinds.Add(mindId);
@@ -134,6 +143,32 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
return true;
}
private (Note[]?, string) RequestUplink(EntityUid traitor, FixedPoint2 startingBalance, string briefing)
{
var pda = _uplink.FindUplinkTarget(traitor);
Note[]? code = null;
var uplinked = _uplink.AddUplink(traitor, startingBalance, pda, true);
if (pda is not null && uplinked)
{
// 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)
{
briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short");
}
return (null, briefing);
}
// TODO: AntagCodewordsComponent
private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args)
{
@@ -141,13 +176,17 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
}
// 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

@@ -516,8 +516,8 @@ public sealed class GhostRoleSystem : EntitySystem
_roleSystem.MindAddRole(newMind, "MindRoleGhostMarker");
if(_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind, out _, out var markerRole))
markerRole.Value.Comp.Name = role.RoleName;
if(_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind!, out var markerRole))
markerRole.Value.Comp2.Name = role.RoleName;
_mindSystem.SetUserId(newMind, player.UserId);
_mindSystem.TransferTo(newMind, mob);

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

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

View File

@@ -317,7 +317,7 @@ public sealed class DrinkSystem : SharedDrinkSystem
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} drank {ToPrettyString(entity.Owner):drink}");
}
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-2f));
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-2f).WithVariation(0.25f));
_reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion);
_stomach.TryTransferSolution(firstStomach.Value.Owner, drained, firstStomach.Value.Comp1);

View File

@@ -296,7 +296,7 @@ public sealed class FoodSystem : EntitySystem
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity.Owner):food}");
}
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-1f));
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
// Try to break all used utensils
foreach (var utensil in utensils)

View File

@@ -1,9 +1,9 @@
using Content.Server.Objectives.Components;
using Content.Server.Revolutionary.Components;
using Content.Server.Shuttles.Systems;
using Content.Shared.CCVar;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Configuration;
using Robust.Shared.Random;
@@ -17,7 +17,6 @@ public sealed class KillPersonConditionSystem : EntitySystem
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedJobSystem _job = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
@@ -86,11 +85,10 @@ public sealed class KillPersonConditionSystem : EntitySystem
}
var allHeads = new List<EntityUid>();
foreach (var mind in allHumans)
foreach (var person in allHumans)
{
// RequireAdminNotify used as a cheap way to check for command department
if (_job.MindTryGetJob(mind, out var prototype) && prototype.RequireAdminNotify)
allHeads.Add(mind);
if (TryComp<MindComponent>(person, out var mind) && mind.OwnedEntity is { } ent && HasComp<CommandStaffComponent>(ent))
allHeads.Add(person);
}
if (allHeads.Count == 0)

View File

@@ -1,13 +1,11 @@
using Content.Server.Objectives.Components;
using Content.Server.Revolutionary.Components;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Jobs;
namespace Content.Server.Objectives.Systems;
public sealed class NotCommandRequirementSystem : EntitySystem
{
[Dependency] private readonly SharedJobSystem _job = default!;
public override void Initialize()
{
base.Initialize();
@@ -20,8 +18,7 @@ public sealed class NotCommandRequirementSystem : EntitySystem
if (args.Cancelled)
return;
// cheap equivalent to checking that job department is command, since all command members require admin notification when leaving
if (_job.MindTryGetJob(args.MindId, out var prototype) && prototype.RequireAdminNotify)
if (args.Mind.OwnedEntity is { } ent && HasComp<CommandStaffComponent>(ent))
args.Cancelled = true;
}
}

View File

@@ -25,9 +25,6 @@ public sealed partial class ApcComponent : BaseApcNetComponent
[DataField("enabled")]
public bool MainBreakerEnabled = true;
// TODO: remove this since it probably breaks when 2 people use it
[DataField("hasAccess")]
public bool HasAccess = false;
/// <summary>
/// APC state needs to always be updated after first processing tick.

View File

@@ -2,7 +2,6 @@ using Content.Server.Emp;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.Pow3r;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.APC;
using Content.Shared.Emag.Components;
@@ -71,11 +70,8 @@ public sealed class ApcSystem : EntitySystem
component.NeedStateUpdate = true;
}
//Update the HasAccess var for UI to read
private void OnBoundUiOpen(EntityUid uid, ApcComponent component, BoundUIOpenedEvent args)
{
// TODO: this should be per-player not stored on the apc
component.HasAccess = _accessReader.IsAllowed(args.Actor, uid);
UpdateApcState(uid, component);
}
@@ -165,7 +161,7 @@ public sealed class ApcSystem : EntitySystem
// TODO: Fix ContentHelpers or make a new one coz this is cooked.
var charge = ContentHelpers.RoundToNearestLevels(battery.CurrentStorage / battery.Capacity, 1.0, 100 / ChargeAccuracy) / 100f * ChargeAccuracy;
var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled, apc.HasAccess,
var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled,
(int) MathF.Ceiling(battery.CurrentSupply), apc.LastExternalState,
charge);

View File

@@ -1,11 +1,9 @@
using Content.Server.GameTicking.Rules;
namespace Content.Server.Revolutionary.Components;
/// <summary>
/// Given to heads at round start for Revs. Used for tracking if heads died or not.
/// Given to heads at round start. Used for assigning traitors to kill heads and for revs to check if the heads died or not.
/// </summary>
[RegisterComponent, Access(typeof(RevolutionaryRuleSystem))]
[RegisterComponent]
public sealed partial class CommandStaffComponent : Component
{

View File

@@ -12,9 +12,13 @@ using Robust.Shared.Timing;
namespace Content.Server.ServerUpdates;
/// <summary>
/// Responsible for restarting the server for update, when not disruptive.
/// Responsible for restarting the server periodically or for update, when not disruptive.
/// </summary>
public sealed class ServerUpdateManager
/// <remarks>
/// This was originally only designed for restarting on *update*,
/// but now also handles periodic restarting to keep server uptime via <see cref="CCVars.ServerUptimeRestartMinutes"/>.
/// </remarks>
public sealed class ServerUpdateManager : IPostInjectInit
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IWatchdogApi _watchdog = default!;
@@ -22,23 +26,43 @@ public sealed class ServerUpdateManager
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IBaseServer _server = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
[ViewVariables]
private bool _updateOnRoundEnd;
private TimeSpan? _restartTime;
private TimeSpan _uptimeRestart;
public void Initialize()
{
_watchdog.UpdateReceived += WatchdogOnUpdateReceived;
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
_cfg.OnValueChanged(
CCVars.ServerUptimeRestartMinutes,
minutes => _uptimeRestart = TimeSpan.FromMinutes(minutes),
true);
}
public void Update()
{
if (_restartTime != null && _restartTime < _gameTiming.RealTime)
if (_restartTime != null)
{
DoShutdown();
if (_restartTime < _gameTiming.RealTime)
{
DoShutdown();
}
}
else
{
if (ShouldShutdownDueToUptime())
{
ServerEmptyUpdateRestartCheck("uptime");
}
}
}
@@ -48,7 +72,7 @@ public sealed class ServerUpdateManager
/// <returns>True if the server is going to restart.</returns>
public bool RoundEnded()
{
if (_updateOnRoundEnd)
if (_updateOnRoundEnd || ShouldShutdownDueToUptime())
{
DoShutdown();
return true;
@@ -61,11 +85,14 @@ public sealed class ServerUpdateManager
{
switch (e.NewStatus)
{
case SessionStatus.Connecting:
case SessionStatus.Connected:
if (_restartTime != null)
_sawmill.Debug("Aborting server restart timer due to player connection");
_restartTime = null;
break;
case SessionStatus.Disconnected:
ServerEmptyUpdateRestartCheck();
ServerEmptyUpdateRestartCheck("last player disconnect");
break;
}
}
@@ -74,20 +101,20 @@ public sealed class ServerUpdateManager
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("server-updates-received"));
_updateOnRoundEnd = true;
ServerEmptyUpdateRestartCheck();
ServerEmptyUpdateRestartCheck("update notification");
}
/// <summary>
/// Checks whether there are still players on the server,
/// and if not starts a timer to automatically reboot the server if an update is available.
/// </summary>
private void ServerEmptyUpdateRestartCheck()
private void ServerEmptyUpdateRestartCheck(string reason)
{
// Can't simple check the current connected player count since that doesn't update
// before PlayerStatusChanged gets fired.
// So in the disconnect handler we'd still see a single player otherwise.
var playersOnline = _playerManager.Sessions.Any(p => p.Status != SessionStatus.Disconnected);
if (playersOnline || !_updateOnRoundEnd)
if (playersOnline || !(_updateOnRoundEnd || ShouldShutdownDueToUptime()))
{
// Still somebody online.
return;
@@ -95,16 +122,30 @@ public sealed class ServerUpdateManager
if (_restartTime != null)
{
// Do nothing because I guess we already have a timer running..?
// Do nothing because we already have a timer running.
return;
}
var restartDelay = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.UpdateRestartDelay));
_restartTime = restartDelay + _gameTiming.RealTime;
_sawmill.Debug("Started server-empty restart timer due to {Reason}", reason);
}
private void DoShutdown()
{
_server.Shutdown(Loc.GetString("server-updates-shutdown"));
_sawmill.Debug($"Shutting down via {nameof(ServerUpdateManager)}!");
var reason = _updateOnRoundEnd ? "server-updates-shutdown" : "server-updates-shutdown-uptime";
_server.Shutdown(Loc.GetString(reason));
}
private bool ShouldShutdownDueToUptime()
{
return _uptimeRestart != TimeSpan.Zero && _gameTiming.RealTime > _uptimeRestart;
}
void IPostInjectInit.PostInject()
{
_sawmill = _logManager.GetSawmill("restart");
}
}

View File

@@ -0,0 +1,183 @@
using Content.Server.Administration;
using Content.Server.Labels;
using Content.Shared.Administration;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Shuttles.Components;
using Content.Shared.Storage;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.Console;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Shuttles.Commands;
/// <summary>
/// Creates FTL disks, to maps, grids, or entities.
/// </summary>
[AdminCommand(AdminFlags.Fun)]
public sealed class FTLDiskCommand : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IEntitySystemManager _entSystemManager = default!;
public override string Command => "ftldisk";
[ValidatePrototypeId<EntityPrototype>]
public const string CoordinatesDisk = "CoordinatesDisk";
[ValidatePrototypeId<EntityPrototype>]
public const string DiskCase = "DiskCase";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)
{
shell.WriteError(Loc.GetString("shell-need-minimum-one-argument"));
return;
}
var player = shell.Player;
if (player == null)
{
shell.WriteLine(Loc.GetString("shell-only-players-can-run-this-command"));
return;
}
if (player.AttachedEntity == null)
{
shell.WriteLine(Loc.GetString("shell-must-be-attached-to-entity"));
return;
}
EntityUid entity = player.AttachedEntity.Value;
var coords = _entManager.GetComponent<TransformComponent>(entity).Coordinates;
var handsSystem = _entSystemManager.GetEntitySystem<SharedHandsSystem>();
var labelSystem = _entSystemManager.GetEntitySystem<LabelSystem>();
var mapSystem = _entSystemManager.GetEntitySystem<SharedMapSystem>();
var storageSystem = _entSystemManager.GetEntitySystem<SharedStorageSystem>();
foreach (var destinations in args)
{
DebugTools.AssertNotNull(destinations);
// make sure destination is an id.
EntityUid dest;
if (_entManager.TryParseNetEntity(destinations, out var nullableDest))
{
DebugTools.AssertNotNull(nullableDest);
dest = (EntityUid) nullableDest;
// we need to go to a map, so check if the EntID is something else then try for its map
if (!_entManager.HasComponent<MapComponent>(dest))
{
if (!_entManager.TryGetComponent<TransformComponent>(dest, out var entTransform))
{
shell.WriteLine(Loc.GetString("cmd-ftldisk-no-transform", ("destination", destinations)));
continue;
}
if (!mapSystem.TryGetMap(entTransform.MapID, out var mapDest))
{
shell.WriteLine(Loc.GetString("cmd-ftldisk-no-map", ("destination", destinations)));
continue;
}
DebugTools.AssertNotNull(mapDest);
dest = mapDest!.Value; // explicit cast here should be fine since the previous if should catch it.
}
// find and verify the map is not somehow unusable.
if (!_entManager.TryGetComponent<MapComponent>(dest, out var mapComp)) // We have to check for a MapComponent here and above since we could have changed our dest entity.
{
shell.WriteLine(Loc.GetString("cmd-ftldisk-no-map-comp", ("destination", destinations), ("map", dest)));
continue;
}
if (mapComp.MapInitialized == false)
{
shell.WriteLine(Loc.GetString("cmd-ftldisk-map-not-init", ("destination", destinations), ("map", dest)));
continue;
}
if (mapComp.MapPaused == true)
{
shell.WriteLine(Loc.GetString("cmd-ftldisk-map-paused", ("destination", destinations), ("map", dest)));
continue;
}
// check if our destination works already, if not, make it.
if (!_entManager.TryGetComponent<FTLDestinationComponent>(dest, out var ftlDestComp))
{
FTLDestinationComponent ftlDest = _entManager.AddComponent<FTLDestinationComponent>(dest);
ftlDest.RequireCoordinateDisk = true;
if (_entManager.HasComponent<MapGridComponent>(dest))
{
ftlDest.BeaconsOnly = true;
shell.WriteLine(Loc.GetString("cmd-ftldisk-planet", ("destination", destinations), ("map", dest)));
}
}
else
{
// we don't do these automatically, since it isn't clear what the correct resolution is. Instead we provide feedback to the user and carry on like they know what theyre doing.
if (ftlDestComp.Enabled == false)
shell.WriteLine(Loc.GetString("cmd-ftldisk-already-dest-not-enabled", ("destination", destinations), ("map", dest)));
if (ftlDestComp.BeaconsOnly == true)
shell.WriteLine(Loc.GetString("cmd-ftldisk-requires-ftl-point", ("destination", destinations), ("map", dest)));
}
// create the FTL disk
EntityUid cdUid = _entManager.SpawnEntity(CoordinatesDisk, coords);
var cd = _entManager.EnsureComponent<ShuttleDestinationCoordinatesComponent>(cdUid);
cd.Destination = dest;
_entManager.Dirty(cdUid, cd);
// create disk case
EntityUid cdCaseUid = _entManager.SpawnEntity(DiskCase, coords);
// apply labels
if (_entManager.TryGetComponent<MetaDataComponent>(dest, out var meta) && meta != null && meta.EntityName != null)
{
labelSystem.Label(cdUid, meta.EntityName);
labelSystem.Label(cdCaseUid, meta.EntityName);
}
// if the case has a storage, try to place the disk in there and then the case inhand
if (_entManager.TryGetComponent<StorageComponent>(cdCaseUid, out var storage) && storageSystem.Insert(cdCaseUid, cdUid, out _, storageComp: storage, playSound: false))
{
if (_entManager.TryGetComponent<HandsComponent>(entity, out var handsComponent) && handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
{
handsSystem.TryPickup(entity, cdCaseUid, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
}
}
else // the case was messed up, put disk inhand
{
_entManager.DeleteEntity(cdCaseUid); // something went wrong so just yeet the chaf
if (_entManager.TryGetComponent<HandsComponent>(entity, out var handsComponent) && handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
{
handsSystem.TryPickup(entity, cdUid, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
}
}
}
else
{
shell.WriteLine(Loc.GetString("shell-invalid-entity-uid", ("uid", destinations)));
}
}
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length >= 1)
return CompletionResult.FromHintOptions(CompletionHelper.MapUids(_entManager), Loc.GetString("cmd-ftldisk-hint"));
return CompletionResult.Empty;
}
}

View File

@@ -60,6 +60,10 @@ public sealed partial class BorgSystem
if (_actions.AddAction(chassis, ref component.ModuleSwapActionEntity, out var action, component.ModuleSwapActionId, uid))
{
if(TryComp<BorgModuleIconComponent>(uid, out var moduleIconComp))
{
action.Icon = moduleIconComp.Icon;
};
action.EntityIcon = uid;
Dirty(component.ModuleSwapActionEntity.Value, action);
}

View File

@@ -293,6 +293,8 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
while (query.MoveNext(out var update))
{
SetLaws(lawset, update);
if (provider.LawUploadSound != null && _mind.TryGetMind(update, out var mindId, out _))
_roles.MindPlaySound(mindId, provider.LawUploadSound);
}
}
}

View File

@@ -131,7 +131,7 @@ public sealed class TemperatureSystem : EntitySystem
TemperatureComponent? temperature = null)
{
//CrystallPunk may try place on heater and entity, and solutions
//if (!Resolve(uid, ref temperature))
//if (!Resolve(uid, ref temperature, false))
// return;
if (temperature == null)
return;
@@ -315,7 +315,7 @@ public sealed class TemperatureSystem : EntitySystem
private void ChangeTemperatureOnCollide(Entity<ChangeTemperatureOnCollideComponent> ent, ref ProjectileHitEvent args)
{
_temperature.ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
_temperature.ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
}
private void OnParentChange(EntityUid uid, TemperatureComponent component,

View File

@@ -1,4 +1,5 @@
using Content.Server.Traitor.Systems;
using Robust.Shared.Prototypes;
namespace Content.Server.Traitor.Components;
@@ -9,14 +10,8 @@ namespace Content.Server.Traitor.Components;
public sealed partial class AutoTraitorComponent : Component
{
/// <summary>
/// Whether to give the traitor an uplink or not.
/// The traitor profile to use
/// </summary>
[DataField("giveUplink"), ViewVariables(VVAccess.ReadWrite)]
public bool GiveUplink = true;
/// <summary>
/// Whether to give the traitor objectives or not.
/// </summary>
[DataField("giveObjectives"), ViewVariables(VVAccess.ReadWrite)]
public bool GiveObjectives = true;
[DataField]
public EntProtoId Profile = "Traitor";
}

View File

@@ -12,9 +12,6 @@ public sealed class AutoTraitorSystem : EntitySystem
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultTraitorRule = "Traitor";
public override void Initialize()
{
base.Initialize();
@@ -24,6 +21,6 @@ public sealed class AutoTraitorSystem : EntitySystem
private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args)
{
_antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, DefaultTraitorRule);
_antag.ForceMakeAntag<AutoTraitorComponent>(args.Mind.Comp.Session, comp.Profile);
}
}

View File

@@ -1,97 +1,136 @@
using System.Linq;
using Content.Server.Store.Systems;
using Content.Server.StoreDiscount.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Implants;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.FixedPoint;
using Content.Shared.Store;
using Content.Shared.Store.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Traitor.Uplink
namespace Content.Server.Traitor.Uplink;
public sealed class UplinkSystem : EntitySystem
{
public sealed class UplinkSystem : EntitySystem
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly StoreSystem _store = default!;
[Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
[ValidatePrototypeId<CurrencyPrototype>]
public const string TelecrystalCurrencyPrototype = "Telecrystal";
private const string FallbackUplinkImplant = "UplinkImplant";
private const string FallbackUplinkCatalog = "UplinkUplinkImplanter";
/// <summary>
/// Adds an uplink to the target
/// </summary>
/// <param name="user">The person who is getting the uplink</param>
/// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param>
/// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param>
/// <param name="giveDiscounts">Marker that enables discounts for uplink items.</param>
/// <returns>Whether or not the uplink was added successfully</returns>
public bool AddUplink(
EntityUid user,
FixedPoint2 balance,
EntityUid? uplinkEntity = null,
bool giveDiscounts = false)
{
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly StoreSystem _store = default!;
// Try to find target item if none passed
[ValidatePrototypeId<CurrencyPrototype>]
public const string TelecrystalCurrencyPrototype = "Telecrystal";
uplinkEntity ??= FindUplinkTarget(user);
/// <summary>
/// Adds an uplink to the target
/// </summary>
/// <param name="user">The person who is getting the uplink</param>
/// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param>
/// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param>
/// <param name="giveDiscounts">Marker that enables discounts for uplink items.</param>
/// <returns>Whether or not the uplink was added successfully</returns>
public bool AddUplink(
EntityUid user,
FixedPoint2? balance,
EntityUid? uplinkEntity = null,
bool giveDiscounts = false
)
if (uplinkEntity == null)
return ImplantUplink(user, balance, giveDiscounts);
EnsureComp<UplinkComponent>(uplinkEntity.Value);
SetUplink(user, uplinkEntity.Value, balance, giveDiscounts);
// TODO add BUI. Currently can't be done outside of yaml -_-
// ^ What does this even mean?
return true;
}
/// <summary>
/// Configure TC for the uplink
/// </summary>
private void SetUplink(EntityUid user, EntityUid uplink, FixedPoint2 balance, bool giveDiscounts)
{
var store = EnsureComp<StoreComponent>(uplink);
store.AccountOwner = user;
store.Balance.Clear();
_store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance } },
uplink,
store);
var uplinkInitializedEvent = new StoreInitializedEvent(
TargetUser: user,
Store: uplink,
UseDiscounts: giveDiscounts,
Listings: _store.GetAvailableListings(user, uplink, store)
.ToArray());
RaiseLocalEvent(ref uplinkInitializedEvent);
}
/// <summary>
/// Implant an uplink as a fallback measure if the traitor had no PDA
/// </summary>
private bool ImplantUplink(EntityUid user, FixedPoint2 balance, bool giveDiscounts)
{
var implantProto = new string(FallbackUplinkImplant);
if (!_proto.TryIndex<ListingPrototype>(FallbackUplinkCatalog, out var catalog))
return false;
if (!catalog.Cost.TryGetValue(TelecrystalCurrencyPrototype, out var cost))
return false;
if (balance < cost) // Can't use Math functions on FixedPoint2
balance = 0;
else
balance = balance - cost;
var implant = _subdermalImplant.AddImplant(user, implantProto);
if (!HasComp<StoreComponent>(implant))
return false;
SetUplink(user, implant.Value, balance, giveDiscounts);
return true;
}
/// <summary>
/// Finds the entity that can hold an uplink for a user.
/// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.)
/// </summary>
public EntityUid? FindUplinkTarget(EntityUid user)
{
// Try to find PDA in inventory
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
{
// Try to find target item if none passed
uplinkEntity ??= FindUplinkTarget(user);
if (uplinkEntity == null)
while (containerSlotEnumerator.MoveNext(out var pdaUid))
{
return false;
if (!pdaUid.ContainedEntity.HasValue)
continue;
if (HasComp<PdaComponent>(pdaUid.ContainedEntity.Value) || HasComp<StoreComponent>(pdaUid.ContainedEntity.Value))
return pdaUid.ContainedEntity.Value;
}
EnsureComp<UplinkComponent>(uplinkEntity.Value);
var store = EnsureComp<StoreComponent>(uplinkEntity.Value);
store.AccountOwner = user;
store.Balance.Clear();
if (balance != null)
{
store.Balance.Clear();
_store.TryAddCurrency(new Dictionary<string, FixedPoint2> { { TelecrystalCurrencyPrototype, balance.Value } }, uplinkEntity.Value, store);
}
var uplinkInitializedEvent = new StoreInitializedEvent(
TargetUser: user,
Store: uplinkEntity.Value,
UseDiscounts: giveDiscounts,
Listings: _store.GetAvailableListings(user, uplinkEntity.Value, store)
.ToArray()
);
RaiseLocalEvent(ref uplinkInitializedEvent);
// TODO add BUI. Currently can't be done outside of yaml -_-
return true;
}
/// <summary>
/// Finds the entity that can hold an uplink for a user.
/// Usually this is a pda in their pda slot, but can also be in their hands. (but not pockets or inside bag, etc.)
/// </summary>
public EntityUid? FindUplinkTarget(EntityUid user)
// Also check hands
foreach (var item in _handsSystem.EnumerateHeld(user))
{
// Try to find PDA in inventory
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
{
while (containerSlotEnumerator.MoveNext(out var pdaUid))
{
if (!pdaUid.ContainedEntity.HasValue)
continue;
if (HasComp<PdaComponent>(pdaUid.ContainedEntity.Value) || HasComp<StoreComponent>(pdaUid.ContainedEntity.Value))
return pdaUid.ContainedEntity.Value;
}
}
// Also check hands
foreach (var item in _handsSystem.EnumerateHeld(user))
{
if (HasComp<PdaComponent>(item) || HasComp<StoreComponent>(item))
return item;
}
return null;
if (HasComp<PdaComponent>(item) || HasComp<StoreComponent>(item))
return item;
}
return null;
}
}

View File

@@ -56,8 +56,14 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
_damageExamine.AddDamageExamine(args.Message, damageSpec, Loc.GetString("damage-melee"));
}
protected override bool ArcRaySuccessful(EntityUid targetUid, Vector2 position, Angle angle, Angle arcWidth, float range, MapId mapId,
EntityUid ignore, ICommonSession? session)
protected override bool ArcRaySuccessful(EntityUid targetUid,
Vector2 position,
Angle angle,
Angle arcWidth,
float range,
MapId mapId,
EntityUid ignore,
ICommonSession? session)
{
// Originally the client didn't predict damage effects so you'd intuit some level of how far
// in the future you'd need to predict, but then there was a lot of complaining like "why would you add artifical delay" as if ping is a choice.

View File

@@ -2,6 +2,8 @@ using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Mind.Components;
using Content.Shared.Teleportation.Systems;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
@@ -11,6 +13,7 @@ public sealed class PortalArtifactSystem : EntitySystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly LinkedEntitySystem _link = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
public override void Initialize()
{
@@ -21,21 +24,28 @@ public sealed class PortalArtifactSystem : EntitySystem
private void OnActivate(Entity<PortalArtifactComponent> artifact, ref ArtifactActivatedEvent args)
{
var map = Transform(artifact).MapID;
var validMinds = new ValueList<EntityUid>();
var mindQuery = EntityQueryEnumerator<MindContainerComponent, TransformComponent, MetaDataComponent>();
while (mindQuery.MoveNext(out var uid, out var mc, out var xform, out var meta))
{
// check if the MindContainer has a Mind and if the entity is not in a container (this also auto excludes AI) and if they are on the same map
if (mc.HasMind && !_container.IsEntityOrParentInContainer(uid, meta: meta, xform: xform) && xform.MapID == map)
{
validMinds.Add(uid);
}
}
//this would only be 0 if there were a station full of AIs and no one else, in that case just stop this function
if (validMinds.Count == 0)
return;
var firstPortal = Spawn(artifact.Comp.PortalProto, _transform.GetMapCoordinates(artifact));
var minds = new List<EntityUid>();
var mindQuery = EntityQueryEnumerator<MindContainerComponent, TransformComponent>();
while (mindQuery.MoveNext(out var uid, out var mc, out var xform))
{
if (mc.HasMind && xform.MapID == map)
minds.Add(uid);
}
var target = _random.Pick(validMinds);
var target = _random.Pick(minds);
var secondPortal = Spawn(artifact.Comp.PortalProto, _transform.GetMapCoordinates(target));
//Manual position swapping, because the portal that opens doesn't trigger a collision, and doesn't teleport targets the first time.
_transform.SwapPositions(target, secondPortal);
_transform.SwapPositions(target, artifact.Owner);
_link.TryLink(firstPortal, secondPortal, true);
}