From 877ccd6e8f5fe8a692110b9e01762f2f90b927a2 Mon Sep 17 00:00:00 2001 From: beck-thompson <107373427+beck-thompson@users.noreply.github.com> Date: Sun, 21 Apr 2024 11:57:39 -0700 Subject: [PATCH 01/61] Fixed grammar when drinking empty drink (#27218) Fix --- .../Locale/en-US/nutrition/components/drink-component.ftl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Resources/Locale/en-US/nutrition/components/drink-component.ftl b/Resources/Locale/en-US/nutrition/components/drink-component.ftl index 9a388744b0..e80787c8d5 100644 --- a/Resources/Locale/en-US/nutrition/components/drink-component.ftl +++ b/Resources/Locale/en-US/nutrition/components/drink-component.ftl @@ -1,4 +1,4 @@ -drink-component-on-use-is-empty = {$owner} is empty! +drink-component-on-use-is-empty = {CAPITALIZE(THE($owner))} is empty! drink-component-on-examine-is-empty = [color=gray]Empty[/color] drink-component-on-examine-is-opened = [color=yellow]Opened[/color] drink-component-on-examine-is-sealed = The seal is intact. @@ -10,7 +10,7 @@ drink-component-on-examine-is-half-empty = Halfway Empty drink-component-on-examine-is-mostly-empty = Mostly Empty drink-component-on-examine-exact-volume = It contains {$amount}u. drink-component-try-use-drink-not-open = Open {$owner} first! -drink-component-try-use-drink-is-empty = {$entity} is empty! +drink-component-try-use-drink-is-empty = {CAPITALIZE(THE($entity))} is empty! drink-component-try-use-drink-cannot-drink = You can't drink anything! drink-component-try-use-drink-had-enough = You can't drink more! drink-component-try-use-drink-cannot-drink-other = They can't drink anything! From c93052c7553e5a495f70cffe435febc6157ab8b3 Mon Sep 17 00:00:00 2001 From: "Wrexbe (Josh)" <81056464+wrexbe@users.noreply.github.com> Date: Sun, 21 Apr 2024 17:37:50 -0700 Subject: [PATCH 02/61] Escape markup for in-game chat, and labeler (#26359) * Escape markup for in-game chat, and labeler * different way * remove a using --------- Co-authored-by: wrexbe --- .../SharedHandsSystem.Interactions.cs | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs index 32339eb03a..6d4d332479 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs @@ -8,6 +8,7 @@ using Content.Shared.Localizations; using Robust.Shared.Input.Binding; using Robust.Shared.Map; using Robust.Shared.Player; +using Robust.Shared.Utility; namespace Content.Shared.Hands.EntitySystems; @@ -181,27 +182,21 @@ public abstract partial class SharedHandsSystem : EntitySystem } //TODO: Actually shows all items/clothing/etc. - private void HandleExamined(EntityUid uid, HandsComponent handsComp, ExaminedEvent args) + private void HandleExamined(EntityUid examinedUid, HandsComponent handsComp, ExaminedEvent args) { - var held = EnumerateHeld(uid, handsComp) - .Where(x => !HasComp(x)).ToList(); + var heldItemNames = EnumerateHeld(examinedUid, handsComp) + .Where(entity => !HasComp(entity)) + .Select(item => FormattedMessage.EscapeText(Identity.Name(item, EntityManager))) + .Select(itemName => Loc.GetString("comp-hands-examine-wrapper", ("item", itemName))) + .ToList(); + + var locKey = heldItemNames.Count != 0 ? "comp-hands-examine" : "comp-hands-examine-empty"; + var locUser = ("user", Identity.Entity(examinedUid, EntityManager)); + var locItems = ("items", ContentLocalizationManager.FormatList(heldItemNames)); using (args.PushGroup(nameof(HandsComponent))) { - if (!held.Any()) - { - args.PushText(Loc.GetString("comp-hands-examine-empty", - ("user", Identity.Entity(uid, EntityManager)))); - return; - } - - var heldList = ContentLocalizationManager.FormatList(held - .Select(x => Loc.GetString("comp-hands-examine-wrapper", - ("item", Identity.Entity(x, EntityManager)))).ToList()); - - args.PushMarkup(Loc.GetString("comp-hands-examine", - ("user", Identity.Entity(uid, EntityManager)), - ("items", heldList))); + args.PushMarkup(Loc.GetString(locKey, locUser, locItems)); } } } From 6b1866625cf20d7b64f1dba9c711193d44380da2 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 22 Apr 2024 05:51:29 +0200 Subject: [PATCH 03/61] Make sterile masks a tiny item (#27213) This minorly annoyed me because breath masks are tiny too, so if I swapped them I may run out of inventory space. --- Resources/Prototypes/Entities/Clothing/Masks/masks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml index 850050b2d3..3a86464048 100644 --- a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml +++ b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml @@ -278,7 +278,7 @@ sprite: Clothing/Mask/sterile.rsi - type: IngestionBlocker - type: Item - storedRotation: -90 + size: Tiny - type: IdentityBlocker coverage: MOUTH From 81f2dc7cf90de43c6a854c28ae9243be1c8c7d4d Mon Sep 17 00:00:00 2001 From: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com> Date: Sun, 21 Apr 2024 20:57:13 -0700 Subject: [PATCH 04/61] Add SharedPopupSystem.PopupClient for cursor and coordinates (#27231) --- Content.Client/Popups/PopupSystem.cs | 19 ++++++++++++++++++- Content.Server/Popups/PopupSystem.cs | 8 ++++++++ Content.Shared/Popups/SharedPopupSystem.cs | 12 ++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs index 3faa392e58..1ef8dfba2d 100644 --- a/Content.Client/Popups/PopupSystem.cs +++ b/Content.Client/Popups/PopupSystem.cs @@ -5,7 +5,6 @@ using Content.Shared.Popups; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.Player; -using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Shared.Configuration; using Robust.Shared.Map; @@ -163,6 +162,15 @@ namespace Content.Client.Popups PopupEntity(message, uid, type); } + public override void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small) + { + if (recipient == null) + return; + + if (_timing.IsFirstTimePredicted) + PopupCursor(message, recipient.Value, type); + } + public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) { if (recipient == null) @@ -172,6 +180,15 @@ namespace Content.Client.Popups PopupEntity(message, uid, recipient.Value, type); } + public override void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small) + { + if (recipient == null) + return; + + if (_timing.IsFirstTimePredicted) + PopupCoordinates(message, coordinates, recipient.Value, type); + } + public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small) { if (TryComp(uid, out TransformComponent? transform)) diff --git a/Content.Server/Popups/PopupSystem.cs b/Content.Server/Popups/PopupSystem.cs index d1163a2be1..4aa3d39224 100644 --- a/Content.Server/Popups/PopupSystem.cs +++ b/Content.Server/Popups/PopupSystem.cs @@ -88,11 +88,19 @@ namespace Content.Server.Popups RaiseNetworkEvent(new PopupEntityEvent(message, type, GetNetEntity(uid)), actor.PlayerSession); } + public override void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small) + { + } + public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) { // do nothing duh its for client only } + public override void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small) + { + } + public override void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small) { if (message == null) diff --git a/Content.Shared/Popups/SharedPopupSystem.cs b/Content.Shared/Popups/SharedPopupSystem.cs index 10e8ca9be1..38d2030cd5 100644 --- a/Content.Shared/Popups/SharedPopupSystem.cs +++ b/Content.Shared/Popups/SharedPopupSystem.cs @@ -82,12 +82,24 @@ namespace Content.Shared.Popups /// public abstract void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small); + /// + /// Variant of that only runs on the client, outside of prediction. + /// Useful for shared code that is always ran by both sides to avoid duplicate popups. + /// + public abstract void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small); + /// /// Variant of that only runs on the client, outside of prediction. /// Useful for shared code that is always ran by both sides to avoid duplicate popups. /// public abstract void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small); + /// + /// Variant of that only runs on the client, outside of prediction. + /// Useful for shared code that is always ran by both sides to avoid duplicate popups. + /// + public abstract void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small); + /// /// Variant of for use with prediction. The local client will show /// the popup to the recipient, and the server will show it to every other player in PVS range. If recipient is null, the local client From 210e6f64447fe586241998532c97c4c03df0b88a Mon Sep 17 00:00:00 2001 From: keronshb <54602815+keronshb@users.noreply.github.com> Date: Mon, 22 Apr 2024 04:39:07 -0400 Subject: [PATCH 05/61] Polymorphs spawn using the parents world rotation (#27223) changes polymorph spawn to use angle --- Content.Server/Polymorph/Systems/PolymorphSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs index 8cae15d70d..e6ba1d02af 100644 --- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs +++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs @@ -199,7 +199,7 @@ public sealed partial class PolymorphSystem : EntitySystem var targetTransformComp = Transform(uid); - var child = Spawn(configuration.Entity, targetTransformComp.Coordinates); + var child = Spawn(configuration.Entity, _transform.GetMapCoordinates(uid, targetTransformComp), rotation: _transform.GetWorldRotation(uid)); MakeSentientCommand.MakeSentient(child, EntityManager); From b0e6ff67793723f1fd23e62eccde731d8aec5f4c Mon Sep 17 00:00:00 2001 From: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com> Date: Mon, 22 Apr 2024 01:39:50 -0700 Subject: [PATCH 06/61] Add events for custom action target validation (#27230) --- .../Systems/Actions/ActionUIController.cs | 4 +-- .../Actions/ActionOnInteractSystem.cs | 4 +-- .../Events/ValidateActionEntityTargetEvent.cs | 4 +++ .../Events/ValidateActionWorldTargetEvent.cs | 6 ++++ Content.Shared/Actions/SharedActionsSystem.cs | 31 +++++++++++++++---- 5 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 Content.Shared/Actions/Events/ValidateActionEntityTargetEvent.cs create mode 100644 Content.Shared/Actions/Events/ValidateActionWorldTargetEvent.cs diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs index 09663ba82c..c79f0f80f9 100644 --- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs +++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs @@ -200,7 +200,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged(component.ActionEntities, args.CanReach); for (var i = entOptions.Count - 1; i >= 0; i--) { - var action = entOptions[i].Comp; + var action = entOptions[i]; if (!_actions.ValidateEntityTarget(args.User, args.Target.Value, action)) entOptions.RemoveAt(i); } @@ -88,7 +88,7 @@ public sealed class ActionOnInteractSystem : EntitySystem var options = GetValidActions(component.ActionEntities, args.CanReach); for (var i = options.Count - 1; i >= 0; i--) { - var action = options[i].Comp; + var action = options[i]; if (!_actions.ValidateWorldTarget(args.User, args.ClickLocation, action)) options.RemoveAt(i); } diff --git a/Content.Shared/Actions/Events/ValidateActionEntityTargetEvent.cs b/Content.Shared/Actions/Events/ValidateActionEntityTargetEvent.cs new file mode 100644 index 0000000000..9f22e7973a --- /dev/null +++ b/Content.Shared/Actions/Events/ValidateActionEntityTargetEvent.cs @@ -0,0 +1,4 @@ +namespace Content.Shared.Actions.Events; + +[ByRefEvent] +public record struct ValidateActionEntityTargetEvent(EntityUid User, EntityUid Target, bool Cancelled = false); diff --git a/Content.Shared/Actions/Events/ValidateActionWorldTargetEvent.cs b/Content.Shared/Actions/Events/ValidateActionWorldTargetEvent.cs new file mode 100644 index 0000000000..43e398aad4 --- /dev/null +++ b/Content.Shared/Actions/Events/ValidateActionWorldTargetEvent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.Map; + +namespace Content.Shared.Actions.Events; + +[ByRefEvent] +public record struct ValidateActionWorldTargetEvent(EntityUid User, EntityCoordinates Target, bool Cancelled = false); diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index 9f3fb96410..e1b76f517e 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -8,14 +8,13 @@ using Content.Shared.Hands; using Content.Shared.Interaction; using Content.Shared.Inventory.Events; using Content.Shared.Mind; -using Content.Shared.Mobs.Components; +using Content.Shared.Rejuvenate; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Timing; using Robust.Shared.Utility; -using Content.Shared.Rejuvenate; namespace Content.Shared.Actions; @@ -389,7 +388,7 @@ public abstract class SharedActionsSystem : EntitySystem var targetWorldPos = _transformSystem.GetWorldPosition(entityTarget); _rotateToFaceSystem.TryFaceCoordinates(user, targetWorldPos); - if (!ValidateEntityTarget(user, entityTarget, entityAction)) + if (!ValidateEntityTarget(user, entityTarget, (actionEnt, entityAction))) return; _adminLogger.Add(LogType.Action, @@ -413,7 +412,7 @@ public abstract class SharedActionsSystem : EntitySystem var entityCoordinatesTarget = GetCoordinates(netCoordinatesTarget); _rotateToFaceSystem.TryFaceCoordinates(user, entityCoordinatesTarget.ToMapPos(EntityManager, _transformSystem)); - if (!ValidateWorldTarget(user, entityCoordinatesTarget, worldAction)) + if (!ValidateWorldTarget(user, entityCoordinatesTarget, (actionEnt, worldAction))) return; _adminLogger.Add(LogType.Action, @@ -445,7 +444,17 @@ public abstract class SharedActionsSystem : EntitySystem PerformAction(user, component, actionEnt, action, performEvent, curTime); } - public bool ValidateEntityTarget(EntityUid user, EntityUid target, EntityTargetActionComponent action) + public bool ValidateEntityTarget(EntityUid user, EntityUid target, Entity actionEnt) + { + if (!ValidateEntityTargetBase(user, target, actionEnt)) + return false; + + var ev = new ValidateActionEntityTargetEvent(user, target); + RaiseLocalEvent(actionEnt, ref ev); + return !ev.Cancelled; + } + + private bool ValidateEntityTargetBase(EntityUid user, EntityUid target, EntityTargetActionComponent action) { if (!target.IsValid() || Deleted(target)) return false; @@ -484,7 +493,17 @@ public abstract class SharedActionsSystem : EntitySystem return _interactionSystem.CanAccessViaStorage(user, target); } - public bool ValidateWorldTarget(EntityUid user, EntityCoordinates coords, WorldTargetActionComponent action) + public bool ValidateWorldTarget(EntityUid user, EntityCoordinates coords, Entity action) + { + if (!ValidateWorldTargetBase(user, coords, action)) + return false; + + var ev = new ValidateActionWorldTargetEvent(user, coords); + RaiseLocalEvent(action, ref ev); + return !ev.Cancelled; + } + + private bool ValidateWorldTargetBase(EntityUid user, EntityCoordinates coords, WorldTargetActionComponent action) { if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null)) return false; From fcf5057b61321920826b630384e0dc6ff5de55e8 Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Mon, 22 Apr 2024 08:42:26 +0000 Subject: [PATCH 07/61] make fire spreading scale with mass (#27202) * make fire spreading scale with mass * realer --------- Co-authored-by: deltanedas <@deltanedas:kde.org> --- .../Atmos/EntitySystems/FlammableSystem.cs | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs index 53fcb72076..058faf443e 100644 --- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs +++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs @@ -47,6 +47,8 @@ namespace Content.Server.Atmos.EntitySystems [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly IRobustRandom _random = default!; + private EntityQuery _physicsQuery; + public const float MinimumFireStacks = -10f; public const float MaximumFireStacks = 20f; private const float UpdateTime = 1f; @@ -62,6 +64,8 @@ namespace Content.Server.Atmos.EntitySystems { UpdatesAfter.Add(typeof(AtmosphereSystem)); + _physicsQuery = GetEntityQuery(); + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnInteractUsing); SubscribeLocalEvent(OnCollide); @@ -203,7 +207,17 @@ namespace Content.Server.Atmos.EntitySystems if (flammable.OnFire && otherFlammable.OnFire) { // Both are on fire -> equalize fire stacks. - var avg = (flammable.FireStacks + otherFlammable.FireStacks) / 2; + // Weight each thing's firestacks by its mass + var mass1 = 1f; + var mass2 = 1f; + if (_physicsQuery.TryComp(uid, out var physics) && _physicsQuery.TryComp(otherUid, out var otherPhys)) + { + mass1 = physics.Mass; + mass2 = otherPhys.Mass; + } + + var total = mass1 + mass2; + var avg = (flammable.FireStacks * mass1 + otherFlammable.FireStacks * mass2) / total; flammable.FireStacks = flammable.CanExtinguish ? avg : Math.Max(flammable.FireStacks, avg); otherFlammable.FireStacks = otherFlammable.CanExtinguish ? avg : Math.Max(otherFlammable.FireStacks, avg); UpdateAppearance(uid, flammable); @@ -212,25 +226,24 @@ namespace Content.Server.Atmos.EntitySystems } // Only one is on fire -> attempt to spread the fire. - if (flammable.OnFire) + var (srcUid, srcFlammable, destUid, destFlammable) = flammable.OnFire + ? (uid, flammable, otherUid, otherFlammable) + : (otherUid, otherFlammable, uid, flammable); + + // if the thing on fire has less mass, spread less firestacks and vice versa + var ratio = 0.5f; + if (_physicsQuery.TryComp(srcUid, out var srcPhysics) && _physicsQuery.TryComp(destUid, out var destPhys)) { - otherFlammable.FireStacks += flammable.FireStacks / 2; - Ignite(otherUid, uid, otherFlammable); - if (flammable.CanExtinguish) - { - flammable.FireStacks /= 2; - UpdateAppearance(uid, flammable); - } + ratio *= srcPhysics.Mass / destPhys.Mass; } - else + + var lost = srcFlammable.FireStacks * ratio; + destFlammable.FireStacks += lost; + Ignite(destUid, srcUid, destFlammable); + if (srcFlammable.CanExtinguish) { - flammable.FireStacks += otherFlammable.FireStacks / 2; - Ignite(uid, otherUid, flammable); - if (otherFlammable.CanExtinguish) - { - otherFlammable.FireStacks /= 2; - UpdateAppearance(otherUid, otherFlammable); - } + srcFlammable.FireStacks -= lost; + UpdateAppearance(srcUid, srcFlammable); } } From bffad8053aff57672110dc4f303a1664627eacb9 Mon Sep 17 00:00:00 2001 From: PJBot Date: Mon, 22 Apr 2024 08:43:33 +0000 Subject: [PATCH 08/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index d623b48d44..8d96e4aea8 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Ubaser - changes: - - message: Food Service research is now roundstart. - type: Tweak - id: 5916 - time: '2024-02-11T06:37:12.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25046 - author: SlamBamActionman changes: - message: Added setting to toggle chat name color in Options. @@ -3846,3 +3839,10 @@ id: 6415 time: '2024-04-21T16:09:26.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26545 +- author: deltanedas + changes: + - message: Flaming mice no longer completely engulf people they touch. + type: Tweak + id: 6416 + time: '2024-04-22T08:42:26.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27202 From b5bcf86b74e76ba15aa0cb2a2fe39f81cb8991cd Mon Sep 17 00:00:00 2001 From: Weax <59857479+ImWeax@users.noreply.github.com> Date: Mon, 22 Apr 2024 18:44:14 +1000 Subject: [PATCH 09/61] Make CLF3 reaction require heating first. (#27187) * heat your bombs * Removing comment --- Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml b/Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml index 318490931f..3591ce7008 100644 --- a/Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml +++ b/Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml @@ -31,6 +31,7 @@ - type: reaction id: ChlorineTrifluoride + minTemp: 370 priority: 20 reactants: Chlorine: @@ -38,7 +39,6 @@ Fluorine: amount: 3 effects: - # TODO solution temperature!! - !type:ExplosionReactionEffect explosionType: Default # 15 damage per intensity. maxIntensity: 200 From 35ac720d19e2f5a470809e14c325786c5b706dbb Mon Sep 17 00:00:00 2001 From: PJBot Date: Mon, 22 Apr 2024 08:45:19 +0000 Subject: [PATCH 10/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 8d96e4aea8..b0a548d8c4 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: SlamBamActionman - changes: - - message: Added setting to toggle chat name color in Options. - type: Add - id: 5917 - time: '2024-02-11T06:38:55.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/24625 - author: Emisse changes: - message: Ambuzol now requires zombie blood @@ -3846,3 +3839,11 @@ id: 6416 time: '2024-04-22T08:42:26.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27202 +- author: Weax + changes: + - message: The CLF3 reaction now requires heating first before you can engulf chemistry + in fiery death. + type: Tweak + id: 6417 + time: '2024-04-22T08:44:14.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27187 From e2b260d894812194f444e209dfc1d1d2ea0ab645 Mon Sep 17 00:00:00 2001 From: potato1234_x <79580518+potato1234x@users.noreply.github.com> Date: Mon, 22 Apr 2024 18:45:38 +1000 Subject: [PATCH 11/61] Psicodine, Mannitol, Lipolicide and Happiness (#27134) * reagents * Update Resources/Locale/en-US/reagents/meta/narcotics.ftl --------- Co-authored-by: Kara --- .../ReagentEffectConditions/TotalHunger.cs | 35 ++++++++++ .../Locale/en-US/flavors/flavor-profiles.ftl | 2 + .../en-US/guidebook/chemistry/conditions.ftl | 9 +++ Resources/Locale/en-US/reagents/mannitol.ftl | 1 + .../Locale/en-US/reagents/meta/medicine.ftl | 6 ++ .../Locale/en-US/reagents/meta/narcotics.ftl | 3 + .../Locale/en-US/reagents/meta/toxins.ftl | 3 + Resources/Locale/en-US/reagents/psicodine.ftl | 3 + Resources/Prototypes/Flavors/flavors.yml | 10 +++ Resources/Prototypes/Reagents/medicine.yml | 64 +++++++++++++++++++ Resources/Prototypes/Reagents/narcotics.yml | 50 +++++++++++++++ Resources/Prototypes/Reagents/toxins.yml | 20 ++++++ .../Prototypes/Recipes/Reactions/medicine.yml | 52 +++++++++++++++ 13 files changed, 258 insertions(+) create mode 100644 Content.Server/Chemistry/ReagentEffectConditions/TotalHunger.cs create mode 100644 Resources/Locale/en-US/reagents/mannitol.ftl create mode 100644 Resources/Locale/en-US/reagents/psicodine.ftl diff --git a/Content.Server/Chemistry/ReagentEffectConditions/TotalHunger.cs b/Content.Server/Chemistry/ReagentEffectConditions/TotalHunger.cs new file mode 100644 index 0000000000..1dd12e632a --- /dev/null +++ b/Content.Server/Chemistry/ReagentEffectConditions/TotalHunger.cs @@ -0,0 +1,35 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Nutrition.Components; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Server.Chemistry.ReagentEffectConditions +{ + public sealed partial class Hunger : ReagentEffectCondition + { + [DataField] + public float Max = float.PositiveInfinity; + + [DataField] + public float Min = 0; + + public override bool Condition(ReagentEffectArgs args) + { + if (args.EntityManager.TryGetComponent(args.SolutionEntity, out HungerComponent? hunger)) + { + var total = hunger.CurrentHunger; + if (total > Min && total < Max) + return true; + } + + return false; + } + + public override string GuidebookExplanation(IPrototypeManager prototype) + { + return Loc.GetString("reagent-effect-condition-guidebook-total-hunger", + ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), + ("min", Min)); + } + } +} diff --git a/Resources/Locale/en-US/flavors/flavor-profiles.ftl b/Resources/Locale/en-US/flavors/flavor-profiles.ftl index 61567d8695..41b575b7d6 100644 --- a/Resources/Locale/en-US/flavors/flavor-profiles.ftl +++ b/Resources/Locale/en-US/flavors/flavor-profiles.ftl @@ -168,6 +168,8 @@ flavor-complex-light = like a light gone out flavor-complex-profits = like profits flavor-complex-fishops = like the dreaded fishops flavor-complex-violets = like violets +flavor-complex-mothballs = like mothballs +flavor-complex-paint-thinner = like paint thinner # Drink-specific flavors. diff --git a/Resources/Locale/en-US/guidebook/chemistry/conditions.ftl b/Resources/Locale/en-US/guidebook/chemistry/conditions.ftl index 807b5591a8..7748ab9893 100644 --- a/Resources/Locale/en-US/guidebook/chemistry/conditions.ftl +++ b/Resources/Locale/en-US/guidebook/chemistry/conditions.ftl @@ -7,6 +7,15 @@ } } +reagent-effect-condition-guidebook-total-hunger = + { $max -> + [2147483648] the target has at least {NATURALFIXED($min, 2)} total hunger + *[other] { $min -> + [0] the target has at most {NATURALFIXED($max, 2)} total hunger + *[other] the target has between {NATURALFIXED($min, 2)} and {NATURALFIXED($max, 2)} total hunger + } + } + reagent-effect-condition-guidebook-reagent-threshold = { $max -> [2147483648] there's at least {NATURALFIXED($min, 2)}u of {$reagent} diff --git a/Resources/Locale/en-US/reagents/mannitol.ftl b/Resources/Locale/en-US/reagents/mannitol.ftl new file mode 100644 index 0000000000..1d35aff587 --- /dev/null +++ b/Resources/Locale/en-US/reagents/mannitol.ftl @@ -0,0 +1 @@ +mannitol-effect-enlightened = You feel ENLIGHTENED! diff --git a/Resources/Locale/en-US/reagents/meta/medicine.ftl b/Resources/Locale/en-US/reagents/meta/medicine.ftl index e02d428082..a0b557e28f 100644 --- a/Resources/Locale/en-US/reagents/meta/medicine.ftl +++ b/Resources/Locale/en-US/reagents/meta/medicine.ftl @@ -132,3 +132,9 @@ reagent-desc-necrosol = A necrotic substance that seems to be able to heal froze reagent-name-aloxadone = aloxadone reagent-desc-aloxadone = A cryogenics chemical. Used to treat severe third degree burns via regeneration of the burnt tissue. Works regardless of the patient being alive or dead. + +reagent-name-mannitol = mannitol +reagent-desc-mannitol = Efficiently restores brain damage. + +reagent-name-psicodine = psicodine +reagent-desc-psicodine = Suppresses anxiety and other various forms of mental distress. Overdose causes hallucinations and minor toxin damage. diff --git a/Resources/Locale/en-US/reagents/meta/narcotics.ftl b/Resources/Locale/en-US/reagents/meta/narcotics.ftl index ea115bf962..a7cffb7f6b 100644 --- a/Resources/Locale/en-US/reagents/meta/narcotics.ftl +++ b/Resources/Locale/en-US/reagents/meta/narcotics.ftl @@ -39,3 +39,6 @@ reagent-desc-norepinephric-acid = A smooth chemical that blocks the optical rece reagent-name-tear-gas = tear gas reagent-desc-tear-gas = A chemical that causes severe irritation and crying, commonly used in riot control. + +reagent-name-happiness = happiness +reagent-desc-happiness = Fills you with ecstatic numbness and causes minor brain damage. Highly addictive. If overdosed causes sudden mood swings. diff --git a/Resources/Locale/en-US/reagents/meta/toxins.ftl b/Resources/Locale/en-US/reagents/meta/toxins.ftl index 660da9c271..09b135e7f5 100644 --- a/Resources/Locale/en-US/reagents/meta/toxins.ftl +++ b/Resources/Locale/en-US/reagents/meta/toxins.ftl @@ -75,3 +75,6 @@ reagent-desc-vestine = Has an adverse reaction within the body causing major jit reagent-name-tazinide = tazinide reagent-desc-tazinide = A highly dangerous metallic mixture which can interfere with most movement through an electrifying current. + +reagent-name-lipolicide = lipolicide +reagent-desc-lipolicide = A powerful toxin that will destroy fat cells, massively reducing body weight in a short time. Deadly to those without nutriment in their body. diff --git a/Resources/Locale/en-US/reagents/psicodine.ftl b/Resources/Locale/en-US/reagents/psicodine.ftl new file mode 100644 index 0000000000..c9795b11a9 --- /dev/null +++ b/Resources/Locale/en-US/reagents/psicodine.ftl @@ -0,0 +1,3 @@ +psicodine-effect-fearless = You feel totally fearless! +psicodine-effect-anxieties-wash-away = All of your anxieties wash away! +psicodine-effect-at-peace = You feel completely at peace. diff --git a/Resources/Prototypes/Flavors/flavors.yml b/Resources/Prototypes/Flavors/flavors.yml index 2b55efc21b..25ed9d3372 100644 --- a/Resources/Prototypes/Flavors/flavors.yml +++ b/Resources/Prototypes/Flavors/flavors.yml @@ -1058,3 +1058,13 @@ id: violets flavorType: Complex description: flavor-complex-violets + +- type: flavor + id: mothballs + flavorType: Complex + description: flavor-complex-mothballs + +- type: flavor + id: paintthinner + flavorType: Complex + description: flavor-complex-paint-thinner diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml index 5df80543a7..aae367d9eb 100644 --- a/Resources/Prototypes/Reagents/medicine.yml +++ b/Resources/Prototypes/Reagents/medicine.yml @@ -1199,3 +1199,67 @@ Heat: -3.0 Shock: -3.0 Caustic: -1.0 + +- type: reagent + id : Mannitol # currently this is just a way to create psicodine + name: reagent-name-mannitol + group: Medicine + desc: reagent-desc-mannitol + physicalDesc: reagent-physical-desc-opaque + flavor: sweet + color: "#A0A0A0" + metabolisms: + Medicine: + effects: + - !type:PopupMessage + conditions: + - !type:ReagentThreshold + min: 15 + type: Local + visualType: Medium + messages: [ "mannitol-effect-enlightened" ] + probability: 0.2 + +- type: reagent + id: Psicodine + name: reagent-name-psicodine + group: Medicine + desc: reagent-desc-psicodine + physicalDesc: reagent-physical-desc-shiny + flavor: bitter + color: "#07E79E" + metabolisms: + Medicine: + effects: + - !type:HealthChange + conditions: + - !type:ReagentThreshold + min: 30 + damage: + types: + Poison: 2 + - !type:GenericStatusEffect + conditions: + - !type:ReagentThreshold + min: 30 + key: SeeingRainbows + component: SeeingRainbows + type: Add + time: 8 + refresh: false + - !type:GenericStatusEffect + key: Jitter + time: 2.0 + type: Remove + - !type:GenericStatusEffect + key: Drunk + time: 6.0 + type: Remove + - !type:PopupMessage # we dont have sanity/mood so this will have to do + type: Local + visualType: Medium + messages: + - "psicodine-effect-fearless" + - "psicodine-effect-anxieties-wash-away" + - "psicodine-effect-at-peace" + probability: 0.2 diff --git a/Resources/Prototypes/Reagents/narcotics.yml b/Resources/Prototypes/Reagents/narcotics.yml index cefc8043b0..9b14fa2bc8 100644 --- a/Resources/Prototypes/Reagents/narcotics.yml +++ b/Resources/Prototypes/Reagents/narcotics.yml @@ -407,3 +407,53 @@ conditions: - !type:ReagentThreshold min: 20 + +- type: reagent + id: Happiness + name: reagent-name-happiness + group: Narcotics + desc: reagent-desc-happiness + physicalDesc: reagent-physical-desc-soothing + flavor: paintthinner + color: "#EE35FF" + metabolisms: + Narcotic: + effects: + - !type:Emote + emote: Laugh + showInChat: true + probability: 0.1 + conditions: + - !type:ReagentThreshold + max: 20 + - !type:Emote + emote: Whistle + showInChat: true + probability: 0.1 + conditions: + - !type:ReagentThreshold + max: 20 + - !type:Emote + emote: Crying + showInChat: true + probability: 0.1 + conditions: + - !type:ReagentThreshold + min: 20 + - !type:PopupMessage # we dont have sanity/mood so this will have to do + type: Local + visualType: Medium + messages: + - "psicodine-effect-fearless" + - "psicodine-effect-anxieties-wash-away" + - "psicodine-effect-at-peace" + probability: 0.2 + conditions: + - !type:ReagentThreshold + max: 20 + - !type:GenericStatusEffect + key: SeeingRainbows + component: SeeingRainbows + type: Add + time: 5 + refresh: false diff --git a/Resources/Prototypes/Reagents/toxins.yml b/Resources/Prototypes/Reagents/toxins.yml index 8c91c5f226..f5b196acf6 100644 --- a/Resources/Prototypes/Reagents/toxins.yml +++ b/Resources/Prototypes/Reagents/toxins.yml @@ -641,3 +641,23 @@ - !type:Electrocute probability: 0.8 +- type: reagent + id: Lipolicide + name: reagent-name-lipolicide + group: Toxins + desc: reagent-desc-lipolicide + physicalDesc: reagent-physical-desc-strong-smelling + flavor: mothballs #why does weightloss juice taste like mothballs + color: "#F0FFF0" + metabolisms: + Poison: + effects: + - !type:HealthChange + conditions: + - !type:Hunger + max: 50 + damage: + types: + Poison: 2 + - !type:SatiateHunger + factor: -6 diff --git a/Resources/Prototypes/Recipes/Reactions/medicine.yml b/Resources/Prototypes/Recipes/Reactions/medicine.yml index 60cb8a21f3..b13c6bb71b 100644 --- a/Resources/Prototypes/Recipes/Reactions/medicine.yml +++ b/Resources/Prototypes/Recipes/Reactions/medicine.yml @@ -298,6 +298,18 @@ products: Lipozine: 3 +- type: reaction + id: Mannitol + reactants: + Hydrogen: + amount: 1 + Water: + amount: 1 + Sugar: + amount: 1 + products: + Mannitol: 3 + - type: reaction id: MindbreakerToxin minTemp: 370 @@ -571,3 +583,43 @@ amount: 2 products: Aloxadone: 4 + +- type: reaction + id: Psicodine + impact: Medium + reactants: + Mannitol: + amount: 2 + Impedrezene: + amount: 1 + Water: + amount: 2 + products: + Psicodine: 4 + +- type: reaction + id: Lipolicide + reactants: + Ephedrine: + amount: 1 + Diethylamine: + amount: 1 + Mercury: + amount: 1 + products: + Lipolicide: 3 + +- type: reaction + id: Happiness + reactants: + Laughter: + amount: 2 + Epinephrine: + amount: 1 + Ethanol: + amount: 1 + Plasma: + amount: 5 + catalyst: true + products: + Happiness: 4 From 52204dfa35a7d2390d7a0fa614e8357d2a56badd Mon Sep 17 00:00:00 2001 From: PJBot Date: Mon, 22 Apr 2024 08:46:44 +0000 Subject: [PATCH 12/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index b0a548d8c4..756625850e 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Emisse - changes: - - message: Ambuzol now requires zombie blood - type: Tweak - id: 5918 - time: '2024-02-11T20:47:58.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25119 - author: Plykiya changes: - message: Door remotes no longer have their 5G signals absorbed by mobs, machines, @@ -3847,3 +3840,10 @@ id: 6417 time: '2024-04-22T08:44:14.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27187 +- author: Potato1234_x + changes: + - message: Added Psicodine, Mannitol, Lipolicide and Happiness. + type: Add + id: 6418 + time: '2024-04-22T08:45:39.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27134 From c5ab839b2c2e7401bfc2b1b7fea0f9535f0bef56 Mon Sep 17 00:00:00 2001 From: Terraspark4941 Date: Mon, 22 Apr 2024 13:58:54 +0500 Subject: [PATCH 13/61] Engineering guidebook update (#26851) * RTG page update * old commits from #22272 * Small fixes over most pages * TEG tweaks * Fixed a thing in airlock security * fixed an italics tag * singularity page now includes tesla * Added a burn chamber to TEG page * Prettyprinted TEG page, fixed some more issues with details * Moved ONE singular comma * Change Singularity page heading to include tesla * Singularity page pretty printing + fixes * TIL its [italic] NOT [italics] * RTG Prettyprinting * Access Config Prettyprinting + Netconfig tweak * Atmospherics prettyprinting * Construction keybind fix * Apply suggestions from code review --------- Co-authored-by: Kara --- Resources/Locale/en-US/guidebook/guides.ftl | 2 +- .../ServerInfo/Guidebook/Engineering/AME.xml | 35 ++-- .../Engineering/AccessConfigurator.xml | 46 ++-- .../Guidebook/Engineering/AirlockSecurity.xml | 130 ++++++------ .../Guidebook/Engineering/Atmospherics.xml | 104 ++++----- .../Guidebook/Engineering/Construction.xml | 12 +- .../Guidebook/Engineering/Engineering.xml | 33 ++- .../Guidebook/Engineering/Fires.xml | 22 +- .../Engineering/NetworkConfigurator.xml | 57 ++--- .../Guidebook/Engineering/Networking.xml | 37 ++-- .../Engineering/PortableGenerator.xml | 15 +- .../Guidebook/Engineering/Power.xml | 82 ++++---- .../ServerInfo/Guidebook/Engineering/RTG.xml | 29 +-- .../Guidebook/Engineering/Shuttlecraft.xml | 63 +++--- .../Guidebook/Engineering/Singularity.xml | 198 ++++++++++-------- .../ServerInfo/Guidebook/Engineering/TEG.xml | 73 +++++-- 16 files changed, 505 insertions(+), 433 deletions(-) diff --git a/Resources/Locale/en-US/guidebook/guides.ftl b/Resources/Locale/en-US/guidebook/guides.ftl index 496e38b9a0..ff1ffbf5ea 100644 --- a/Resources/Locale/en-US/guidebook/guides.ftl +++ b/Resources/Locale/en-US/guidebook/guides.ftl @@ -11,7 +11,7 @@ guide-entry-access-configurator = Access Configurator guide-entry-power = Power guide-entry-portable-generator = Portable Generators guide-entry-ame = Antimatter Engine (AME) -guide-entry-singularity = Singularity +guide-entry-singularity = Singularity / Tesla guide-entry-teg = Thermo-electric Generator (TEG) guide-entry-rtg = RTG guide-entry-controls = Controls diff --git a/Resources/ServerInfo/Guidebook/Engineering/AME.xml b/Resources/ServerInfo/Guidebook/Engineering/AME.xml index 202ed16d3d..4b55ce85c5 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/AME.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/AME.xml @@ -1,26 +1,25 @@ -# Antimatter Engine (AME) + # Antimatter Engine (AME) -The AME is one of the simplest engines available. You put together the multi-tile structure, stick some fuel into it, and you're all set. This doesn't mean it isn't potentially dangerous with overheating though. + The AME is one of the simplest engines available. You put together the multi-tile structure, stick some fuel into it, and you're all set. This doesn't mean it is perfectly safe though; you may need to deal with the AME overheating. -## Construction -Required parts: - - - - - + ## Construction + Required parts: + + + + + -To assemble an AME, start by wrenching down the controller on the far end of a HV wire. On most stations, there's catwalks to assist with this. From there, start putting down a 3x3 or larger square of AME parts in preparation for construction, making sure to maximize the number of "center" pieces that are surrounded on all 8 sides. + To assemble an AME, start by wrenching down the controller on the near end of a HV wire. On most stations, there's catwalks to assist with this. From there, start putting down a 3x3 or larger square of AME parts in preparation for construction, making sure to maximize the number of "center" pieces that are surrounded on all 8 sides. -Once this is done, you can use a multitool to convert each AME part into shielding, which should form a finished AME configuration. From there, insert a fuel jar, set the fuel rate to [color=#a4885c]twice the core count or less[/color], and turn on injection. + Once this is done, you can use a multitool to convert each AME part into shielding, which should form a finished AME configuration. From there, insert a fuel jar, set the fuel rate to [color=#a4885c]twice the core count or less[/color], and turn on injection. Any more than this ratio will eventually result in the engine [color=#ff0000]overheating and[/color], shortly afterwards, [color=#ff0000]exploding[/color]. -## Fuel Economy -The closer you are to the perfect ratio of [color=#a4885c]1:2[/color] (1 AME core to 2 fuel rate) the more efficient you'll be. You're cutting fuel efficiency to [color=#a4885c]50% and less[/color] if you're using more cores, but less fuel injection rate. -For an example [color=#76db91]3 core and 6 fuel rate[/color] will generate [color=#76db91]240kW[/color], while [color=#f0684d]8 core 8 fuel rate[/color] will generate [color=#f0684d]160kW[/color]. Generating 80kW less while spending 2 more fuel each injection. + ## Fuel Economy + The closer you are to the perfect ratio of [color=#a4885c]1:2[/color] AME cores to fuel injection rate, the more efficient you'll be. You're cutting fuel efficiency to [color=#a4885c]50% and less[/color] if you're using more cores, but a lower fuel injection rate. + For an example, [color=#76db91]3 cores and 6 fuel injected[/color] will generate [color=#76db91]240kW[/color], while [color=#f0684d]8 cores and 8 fuel injected[/color] will generate [color=#f0684d]160kW[/color]; you'd be generating 80kW less while spending 2 more fuel per injection. -## Upgrading the AME + ## Upgrading the AME -You can generally only upgrade the AME by getting more cores, which can be done by ordering more AME packages from [color=#a4885c]cargo[/color]. - - \ No newline at end of file + You can generally only upgrade the AME by installing more cores, which can be done by ordering more AME flatpacks from [color=#a4885c]Cargo[/color]. + diff --git a/Resources/ServerInfo/Guidebook/Engineering/AccessConfigurator.xml b/Resources/ServerInfo/Guidebook/Engineering/AccessConfigurator.xml index f22ea40a0d..6399521baf 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/AccessConfigurator.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/AccessConfigurator.xml @@ -1,32 +1,34 @@ -# Access Configurator -The access configurator is a tool used to specify what type of personnel may use certain devices. + # Access Configurator + The access configurator is a tool used to specify what type of personnel may use certain devices. - - - + + + -Configurable devices can include airlocks, secure crates and lockers, as well as access restricted machines. + Configurable devices can include airlocks, secure crates and lockers, as well as access restricted machines. + Note: Airlocks can have their accesses configured by the [color=#a4885c]Network Configurator[/color] (or multitool), for convenience. -## Where to find access configurators -Each station is equipped with up to two access configurators. The first is in the possession of the Chief Engineer, while the second can be found with the Head of Personnel. + ## Where to find Access Configurators + Each station is equipped with up to two access configurators. The first is in the possession of the Chief Engineer, while the second can be found with the Head of Personnel. -## How to use the access configurator -To modify a device using the access configurator -- First, use the access configurator on the chosen device to link them together. This will automatically open the configurator UI. -- Next, insert an ID card into the access configurator. -- Set the access requirements of the connected device. What requirements can be added or removed will depend upon the access privileges of the inserted ID card. -- Any changes made will be applied immediately - simply eject the ID card from the access configurator and close the UI when you are done. + ## How to use the access configurator + To modify a device using the access configurator: + - First, use the access configurator on the chosen device to link them together. This will automatically open the configurator UI. + - Next, insert an ID card into the access configurator. + - Set the access requirements of the connected device. What requirements can be added or removed will depend upon the access privileges of the inserted ID card. + - Any changes made will be applied [color=#a4885c]immediately[/color] - simply eject the ID card from the access configurator and close the UI when you are done. -## Restrictions on changing access -As a safety precaution, the inserted ID must possess *all* of the access requirements that are currently active on the connected device in order to modify it. + ## Restrictions on changing access + As a safety precaution, the inserted ID must possess [bold]all[/bold] of the access requirements that are currently active on the connected device in order to modify it. -For example, a device which can be access by both 'Science' and 'Medical' personnel can only by modified using an ID card that has access to both of these departments. The access configurator will warn the user if the inserted ID card does not have sufficient privileges to modify a device. + For example, a device which can be access by both 'Science' and 'Medical' personnel can only by modified using an ID card that has access to [color=#a4885c]both[/color] of these departments. + The access configurator will warn the user if the inserted ID card does not have sufficient privileges to modify a device. -A device with no access requirements set, like a public access airlock, can be modified using any valid station ID card. + A device with no access requirements set, like a public access airlock, can be modified using any valid station ID card. -## Repairing damaged ID card readers -Syndicate agents may attempt to hack access restricted devices through the use of a Cryptographic Sequencer (EMAG). This nefarious tool will completely short out any ID card readers that are attached to the device. + ## Repairing damaged ID card readers + Syndicate agents may attempt to hack access restricted devices through the use of a [color=#a4885c]Cryptographic Sequencer (EMAG)[/color]. This nefarious tool will completely short out any ID card readers that are attached to the device. -Crew members will need to partially de/reconstruct affected devices, and then set appropriate access permissions afterwards using the access configurator, to re-establish access restrictions. - \ No newline at end of file + Engineers will need to partially de/reconstruct affected devices, and then set appropriate access permissions afterwards using the access configurator (or network configurator, for airlocks), to re-establish access restrictions. + diff --git a/Resources/ServerInfo/Guidebook/Engineering/AirlockSecurity.xml b/Resources/ServerInfo/Guidebook/Engineering/AirlockSecurity.xml index 125833a0a1..8bfd3902cc 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/AirlockSecurity.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/AirlockSecurity.xml @@ -1,76 +1,76 @@ -# Airlock Upgrades -It is not uncommon for plucky individuals to try and bypass an airlock by meddling with its internal wiring. + # Airlock Upgrades + It is not uncommon for plucky individuals to try and bypass an airlock by meddling with its internal wiring. -Fortunately, certain countermeasures can installed into airlocks to inconvenience any would be trespassers. + Fortunately, certain countermeasures can installed into airlocks to inconvenience any would-be trespassers. -## Medium security airlocks -The most basic form of intrusion deterrence is to install internal steel plating that will prevent access to internal wiring of the airlock. + ## Medium security airlocks + The most basic form of intrusion deterrence is to install a secured steel plating that will prevent access to internal wiring of the airlock. -To upgrade a basic airlock to a medium security airlock, you will require the following materials - - - - - - - - - - - + To upgrade a basic airlock to a medium security airlock, you will require the following materials: + + + + + + + + + + + -To upgrade the basic airlock, -- Use the screwdriver to open the airlock maintenance panel. -- Add the steel sheets to the airlock. -- Weld the steel sheets into place. -- Close the maintenance panel using the screwdriver. + To upgrade a basic airlock: + - Use the screwdriver to open the airlock maintenance panel. + - Add the steel sheets to the airlock. + - Weld the steel sheets into place. + - Close the maintenance panel using the screwdriver. -## High security airlocks -For airlocks leading to the more sensitive areas of the space station, the use of stronger deterrents are advised. High security airlocks have improved armor plating to protect its internal wiring, along with an electrified security grille. + ## High security airlocks + For airlocks leading to the more sensitive areas of the space station, the use of stronger deterrents are advised. High security airlocks have improved armor plating to protect its internal wiring, along with an electrified security grille. -To upgrade a basic airlock to a high security airlock, you will require the following materials - - - - - - - - - - - - - - + To upgrade a medium security airlock to a high security airlock, you will require the following materials: + + + + + + + + + + + + + + -To upgrade the basic airlock, -- Use the screwdriver to open the airlock maintenance panel. -- Add the plasteel sheets to the airlock. -- Weld the plasteel sheets into place. -- Add the metal rods to the airlock. -- Close the maintenance panel using the screwdriver. + To upgrade a medium security airlock: + - Use the screwdriver to open the airlock maintenance panel. + - Add the plasteel sheets to the airlock. + - Weld the plasteel sheets into place. + - Add the metal rods to the airlock. + - Close the maintenance panel using the screwdriver. -## Maximum security airlocks -You can optionally upgrade a high security airlock to a maximum security airlock. Maximum security airlocks possess an additional layer of plasteel plating on top of its other protections. + ## Maximum security airlocks + You can optionally upgrade a high security airlock to a maximum security airlock. Maximum security airlocks possess an additional layer of plasteel plating on top of its other protections. -To upgrade a high security airlock to a maximum security airlock, you will require the following materials - - - - - - - - - - - + To upgrade a high security airlock to a maximum security airlock, you will require the following materials: + + + + + + + + + + + -To upgrade the high security airlock, -- Use the screwdriver to open the airlock maintenance panel. -- Add the plasteel sheets to the airlock. -- Weld the plasteel sheets into place. -- Close the maintenance panel using the screwdriver. - \ No newline at end of file + To upgrade a high security airlock: + - Use the screwdriver to open the airlock maintenance panel. + - Add the plasteel sheets to the airlock. + - Weld the plasteel sheets into place. + - Close the maintenance panel using the screwdriver. + diff --git a/Resources/ServerInfo/Guidebook/Engineering/Atmospherics.xml b/Resources/ServerInfo/Guidebook/Engineering/Atmospherics.xml index 693e3a0209..48d0d9415e 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Atmospherics.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Atmospherics.xml @@ -1,61 +1,67 @@ - -# Atmospherics + + # Atmospherics -Atmospherics setups are a necessity for your long-term comfort but are generally undocumented, resulting in them being a bit tricky to set up. The following attempts to cover the basics. + Atmospherics setups are a necessity for your long-term comfort, but are generally underdocumented, resulting in them being a bit tricky to set up. The following attempts to cover the basics. -## Standard Mix -Breathing pure O2 or pure N2 is generally bad for the health of your crew, and it is recommended to instead aim for a mix of [color=#a4885c]78% N2 and 22% O2 at 101.24kPa.[/color] It's recommended that your gas mixer setup be set to output at least 1000kPa for faster re-pressurization of rooms. - - - - - -Variations on this mix may be necessary for the long-term comfort of atypical crew, for example crew who require a plasma gas mix to survive. For atypical crew, it is recommended to try and give them their own personal space, isolated by either airlock or disposals section. Keep in mind both methods are leaky and you will need scrubbers on both sides of the lock to clean up any leaked gasses. - - - - -## Vents and Scrubbers -Vents and scrubbers are core atmospherics devices that fill and cleanse rooms, respectively. By default, they are configured for filling rooms to standard pressure (101.24kPa) and to remove all non-O2/N2 gasses from a room. They can be reconfigured from their default settings, allowing you to configure how they respond to various types of gasses or pressure levels. This can be done by interacting with an existing air alarm nearby, or installing and connecting them to a new one. + ## Standard Mix + Breathing pure O2 or pure N2 is generally bad for the health of your crew, and it is recommended to instead aim for a mix of [color=#a4885c]78% N2 and 22% O2 at 101.24kPa.[/color] It's recommended that your gas mixer setup be set to output at least 300kPA for faster re-pressurization of rooms, without posing too much of an overpressurization risk, should traitors sabotage the distro. + + + + + + Variations on this mix may be necessary for the long-term comfort of atypical crew, (for example, Voxes, who are poisoned by Oxygen and breathe Nitrogen). For atypical crew (to be implemented), it is recommended to try and give them their own personal space, isolated by either an airlock or disposals section. Keep in mind that both methods are leaky and you will need scrubbers on both sides of the lock to clean up any leaked gasses. + + + + + ## Vents and Scrubbers + Vents and scrubbers are core atmospherics devices that fill and cleanse rooms, respectively. By default, they are configured for filling rooms to standard pressure (101.24kPa) and to remove all non-O2/N2 gasses from a room. They can be reconfigured from their default settings by linking them to an Air Alarm, allowing you to configure how they respond to various types of gasses or pressure levels. - - - - -During standard operation, if a vent detects that the outside environment is space, it will automatically cease operation until a minimum pressure is reached to avoid destruction of necessary gasses. This can be fixed by pressurizing the room up to that minimum pressure by refilling it with gas canister (potentially multiple, if the room is of significant size). + + + + + During standard operation, if a normal vent detects that the outside environment is space, it will automatically cease operation until a minimum pressure is reached to avoid destruction of useful gasses. This can be fixed by pressurizing the room up to that minimum pressure by refilling it with gas canister (potentially multiple, if the room is of significant size). -Should you encounter a situation where scrubbers aren't cleaning a room fast enough, employ portable scrubbers by dragging them to the affected location and wrenching them down. They work much faster than typical scrubbers and can clean up a room quite quickly. Large spills may require you to employ multiple. - - - -# Gas mixes and Burn chambers -In the event you finish all the tasks at hand, you can make some extra power or money by creating new chemical gasses. + Should you encounter a situation where scrubbers aren't cleaning a room fast enough (and the "Siphon" functionality still cannot keep up), employ portable scrubbers by dragging them to the affected location and wrenching them down. They work much faster than typical scrubbers and can clean up a room quite quickly. Large spills may require you to employ multiple. + + + + # Gas mixes and Burn chambers + In the event you finish all the tasks at hand, you can make some extra money by creating new chemical gasses. -##Tritium -Tritium is a clear, green gas that is highly flammable, radioactive, and combusts when in contact with oxygen making it very helpful when running the [color=#a4885c]TEG.[/color] -It can be made by burning 1% Plasma and 96% or more Oxygen in the Burn Chamber. You can extract this gas through scrubbers. + ##Tritium + Tritium is a clear, green gas that is highly flammable, radioactive, and combusts when in contact with oxygen, making it very helpful when running the [color=#a4885c]TEG[/color]. + It can be made by burning 1% Plasma and 96% or more Oxygen in the Burn Chamber (Ideal ratio is 3% Plasma to 97% Oxygen). You can extract this gas through scrubbers. - - - - - - + + + + + + -##Frezon -Frezon is a bluish-green gas that is very complex and very dangerous. To obtain frezon, you must mix Tritium, Oxygen, and Nitrogen in a 70K room to start the reaction, as well as prevent the Tritium from combusting with the oxygen. + ##Frezon + Frezon is a bluish-green gas that is very complex and very dangerous. To obtain frezon, you must mix Tritium, Oxygen, and Nitrogen in a 70K room to start the reaction, and prevent the Tritium from combusting with the oxygen. - - - - - - + + + + + + -It is critical to understand that a frezon leak can devastate the station, causing a wintery hell filled with itchy sweaters and cold burns. Frezon is very cold, and can freeze the station to death if even a few moles get out, so make sure that you lock your canisters or just move your Frezon straight into a storage room. + It is critical to understand that a frezon leak can devastate the station, causing a wintery hell filled with itchy sweaters and cold burns. Frezon is very cold, and can freeze the station to death if even a few moles get out, so make sure that you lock your canisters or just move your Frezon straight into a storage room. -## Reference Sheet -- Standard atmospheric mix is [color=#a4885c]78% N2 and 22% O2 at 101.24kPa.[/color] -- Gas obeys real math. You can use the equation PV = nRT (Pressure kPa * Volume L = Moles * R * Temperature K) to derive information you might need to know about a gas. R is approximately 8.31446 + ## Reference Sheet + - Standard atmospheric mix is [color=#a4885c]78% N2 and 22% O2 at 101.24kPa.[/color] + - Gas obeys real math. You can use the equation: + + [color=cyan]PV = nRT[/color] + + + ([color=#a4885c]Pressure kPa * Volume L = Moles * R * Temperature K[/color]) + to derive information you might need to know about a gas. R is approximately 8.31446. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Construction.xml b/Resources/ServerInfo/Guidebook/Engineering/Construction.xml index 832b831d8e..15f2f15539 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Construction.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Construction.xml @@ -1,11 +1,11 @@ - -# Construction + + # Construction -By pressing [color=#a4885c]G[/color], one can open the construction menu, which allows you to craft and build a variety of objects. + By pressing [color=#a4885c][keybind="OpenCraftingMenu"][/color], one can open the construction menu, which allows you to craft and build a variety of objects. -When placing objects that "snap" to the grid, you can hold [color=#a4885c]shift[/color] to place an entire line at a time, and [color=#a4885c]ctrl[/color] to place an entire square at a time. + When placing objects that "snap" to the grid, you can hold [color=#a4885c]Shift[/color] to place an entire line at a time, and [color=#a4885c]Ctrl[/color] to place an entire grid at a time. -When crafting objects with a lot of ingredients, keep in mind you don't have to hold everything at once, you can simply place the ingredients on the floor or on a table near you and they'll be used up during crafting like normal. + When crafting objects with a lot of ingredients, keep in mind you don't have to hold everything at once; you can simply place the ingredients on the floor, in your backpack or on a table near you, and they'll be used up during crafting like normal. -When placing a "building ghost" somewhere in the world press [color=#a4885c]Middle Mouse Button[/color] to rotate the ghost clockwise. + When placing a "building ghost" somewhere in the world, press [color=#a4885c][keybind="EditorRotateObject"][/color] to rotate the ghost clockwise. If you are building a mirrorable component (think: Gas Mixers/Filters), you can press [color=#a4885c][keybind="EditorFlipObject"][/color] to flip the ghost. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Engineering.xml b/Resources/ServerInfo/Guidebook/Engineering/Engineering.xml index 0f53ea3042..ab48ed1d82 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Engineering.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Engineering.xml @@ -1,22 +1,21 @@ -# Engineering + # Engineering -Engineering is a combination of construction work, repair work, maintaining a death machine that happens to produce power, and making sure the station contains breathable air. + Engineering is a combination of construction work, repair work, maintaining a death machine that happens to produce power, and making sure the station contains breathable air. -## Tools - - - - - - - - - - - -Your core toolset is a small variety of tools. If you're an engineer, then you should have a belt on your waist containing one of each, if not you can likely find them in maintenance and tool storage within assorted toolboxes and vending machines. - -Most tasks will have explainers for how to perform them on examination, for example if you're constructing a wall, it'll tell you the next step if you look at it a bit closer. + ## Tools + + + + + + + + + + + + Your core toolset is a small variety of tools. If you're an engineer, then you should have a belt on your waist containing one of each; if not, you can likely find them in maintenance shafts and in tool storage within assorted toolboxes and vending machines. + Most tasks will have explainers for how to perform them on examination; for example, if you're constructing a wall, it'll tell you the next step if you look at it a bit closer. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Fires.xml b/Resources/ServerInfo/Guidebook/Engineering/Fires.xml index a1c54059b7..e2c83956cc 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Fires.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Fires.xml @@ -1,15 +1,15 @@ - -# Fires & Space + + # Fires & Space -Fires and spacings are an inevitability due to the highly flammable plasma gas and endless vacuum of space present in and around the station, so it's important to know how to manage them. + Fires and spacings are an inevitability due to the highly flammable plasma gas and the endless vacuum of space present in and around the station, so it's important to know how to manage them. -## Spacing -Space is arguably the easier of the two to handle. -While it does render an area uninhabitable, it can be trivially solved by simply sealing the hole that resulted in the vacuum. After that, assuming distro vents and pipes have not been destroyed in some unfortunate accident, the room will slowly begin to repressurize. -Be aware, that active spacings will slowly siphon the air out of the station's air reserves. If you find it impossible to fix structural damage due to some other hazard - make sure to limit the airflow to that room. + ## Spacing + Space is arguably the easier of the two to handle. + While it does render an area uninhabitable, it can be trivially solved by simply sealing the hole that resulted in the vacuum. After that, assuming distro vents and pipes have not been destroyed in some unfortunate accident, the room will slowly begin to repressurize. + Be aware; active spacings will slowly siphon the air out of the station's air reserves. If you find it impossible to fix structural damage due to some other hazard, make sure to limit the airflow to that room. (Currently only half-valid due to the Gas Miners infinitely replenishing most of the useful gases) -## Fires -Fires can be dealt with through a multitude of ways, but some of the most effective methods include: - - Spacing the enflamed area if possible. This will destroy all of the gasses in the room, which may be a problem if you're already straining life support. - - Dumping a Frezon canister into the enflamed area. This will ice over the flames and halt any ongoing reaction, provided you use enough Frezon. Additionally does not result in destruction of material, so you can simply scrub the room afterwards. + ## Fires + Fires can be dealt with through a multitude of ways, but some of the most effective methods include: + - Spacing the enflamed area if possible. This will destroy all of the gasses in the room, which may be a problem if you're already straining life support. + - Dumping a Frezon canister into the enflamed area. This will ice over the flames and halt any ongoing reaction, provided you use enough Frezon. Additionally, this does not result in destruction of material, so you can simply scrub the room afterwards. diff --git a/Resources/ServerInfo/Guidebook/Engineering/NetworkConfigurator.xml b/Resources/ServerInfo/Guidebook/Engineering/NetworkConfigurator.xml index 445d182ab8..ab95dd2e29 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/NetworkConfigurator.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/NetworkConfigurator.xml @@ -1,39 +1,40 @@ -# Network Configurator -The network configurator allows you to manipulate device lists and link devices together. - - - -The configurator has two modes: List and Link. You can press [color=gray]Alt+Z[/color] or [color=gray]Alt+Y[/color] to switch between them. + # Network Configurator + The network configurator allows you to manipulate device lists, link devices together and configure accesses for airlocks through door electronics. + + + + The configurator has two modes: List and Link. You can press [color=gray]Alt+Z[/color] or [color=gray]Alt+Y[/color] to switch between them. -## List Mode -In list mode you can click on network devices to save them on the configurator and then on a network device that has a device list like the [color=#a4885c]Air Alarm[/color]. + ## List Mode + In list mode you can click on network devices to save them on the configurator and then on a network device that has a device list like the [color=#a4885c]Air Alarm[/color]. -When clicking on a device like the Air Alarm, a UI will open displaying the list currently saved on the device and buttons to manipulate that list. + When clicking on a device like the Air Alarm, a UI will open displaying the list currently saved on the device and buttons to manipulate that list. -You can: -- Replace the current list with the one saved on the configurator -- Add the list on the configurator to the current one -- Clear the current list -- Copy the current list to the configurator -- Visualize the connections to the devices on the current list + You can: + - Replace the current list with the one saved on the configurator + - Add the list on the configurator to the current one + - Clear the current list + - Copy the current list to the configurator + - Visualize the connections to the devices on the current list -Pressing [color=gray]z[/color] or [color=gray]y[/color] opens the list saved on the configurator where you can remove saved devices. + Pressing [color=gray][keybind="ActivateItemInHand"][/color] opens the list saved on the configurator where you can remove saved devices. -## Link Mode -With link mode you can click on a device that is capable of device linking and click on any other device that is either -a sink or source. + ## Link Mode + With link mode, you can click on a device that is capable of device linking and then click on any other device that is either a sink or source. -For example, first clicking on a source like a [color=#a4885c]signal button[/color] and then on sink like a -[color=#a4885c]small light[/color] opens a UI that displays the source ports on the left side and the sink ports on the right. + For example, first clicking on a source, like a [color=#a4885c]signal button[/color], and then on sink, like a [color=#a4885c]small light[/color], opens a UI that displays the source ports on the left side and the sink ports on the right. -Now you can eiter click [color=gray]link defaults[/color] to link the default ports for a source + sink combination or press on a source and then a sink port to connect them. + Now, you can either click [color=gray]Link Defaults[/color] to link the default ports for a source + sink combination, or press on a source port and then a sink port to connect them. -An example of a default link for the aformentioned combinaton of devices would be: - - [color=cyan]Pressed 🠒 Toggle[/color] - -When you're done connecting the ports you want you can click on [color=gray]ok[/color] to close the UI. + An example of a default link for the aformentioned combinaton of devices would be: + + [color=cyan]Pressed 🠒 Toggle[/color] + + When you're done connecting the ports you want you can click on [color=gray]OK[/color] to close the UI. -You can quickly link multiple devices to their default port by first clicking on a device that can be linked and then using [color=gray]alt+left mouse button[/color] on the devices you want to link together. + You can quickly link multiple devices to their default port by first clicking on a device that can be linked and then using [color=gray]Alt+Left Mouse button[/color] on the devices you want to link together. + + ## Airlock Access + To configure an airlock's access, simply take the airlock's door electronics and interact with it using a network configurator (or multitool). Select the accesses you want, insert the door electronics into an airlock frame, and construct to finish! diff --git a/Resources/ServerInfo/Guidebook/Engineering/Networking.xml b/Resources/ServerInfo/Guidebook/Engineering/Networking.xml index 03576c789a..90d1f0891b 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Networking.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Networking.xml @@ -1,25 +1,24 @@ -# Networking -Some devices on the station need to communicate with each other, and they do this by utilizing device networking. -With networking machines and devices can send arbitrary data between each other. -There are multiple networks that get used, such as the wireless and wired network. -Each network device has a frequency it receives on. PDAs for example, use the frequency: [color=green]220.2[/color] + # Networking + Some devices on the station need to communicate with each other, and they do this by utilizing device networking. + With networking, machines and devices can send arbitrary data between each other. + There are multiple networks that get used, such as the wireless and wired network. + Each network device has a frequency it receives on. PDAs for example, use the frequency: [color=green]220.2[/color] -## Device Lists -Some devices need to know what other devices to communicate with specifically. - - - -Air alarms for example require you to tell it which vents, scrubbers, sensors, and firelocks to interact with. -You do that by using the Network Configurator. + Note: The following operations will require use of the Network Configurator to be performed: -## Linking -If devices basic or still more advanced but need finer control of how and what connects to each other they will generally use device linking. + ## Device Lists + Some devices need to know which other devices to communicate with specifically. - - + -With linking you can connect the outputs of a device like [color=gray]On[/color] or [color=gray]Off[/color] with the inputs of a device like the airlocks -[color=gray]Open[/color] or [color=gray]Close[/color] inputs. -The Network Configurator is also used for linking devices together. + Air alarms, for example, require you to tell it which vents, scrubbers, sensors, and firelocks to interact with. + + ## Linking + If a device, basic or advanced, needs finer controls of how and which devices it connects to, it will generally use device linking. + + + + + With linking, you can connect the outputs of a device, like [color=gray]On[/color] or [color=gray]Off[/color], with the inputs of a device, like the airlocks [color=gray]Open[/color] or [color=gray]Close[/color] inputs. diff --git a/Resources/ServerInfo/Guidebook/Engineering/PortableGenerator.xml b/Resources/ServerInfo/Guidebook/Engineering/PortableGenerator.xml index 2cf1fa44ea..b946bf041c 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/PortableGenerator.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/PortableGenerator.xml @@ -1,7 +1,7 @@ - + # Portable Generators - Need power? No engine running? The "P.A.C.M.A.N." line of portable generators has you covered. + Need power? No engines running? The "P.A.C.M.A.N." line of portable generators has you covered. @@ -16,10 +16,10 @@ - The J.R.P.A.C.M.A.N. can be found across the station in maintenance areas, and is ideal for crew to set up themselves whenever there are power issues. + The J.R.P.A.C.M.A.N. can be found across the station in maintenance shafts, and is ideal for crew to set up themselves whenever there are power issues. Setup is incredibly easy: wrench it down above an [color=green]LV[/color] power cable, give it some welding fuel, and start it up. - Welding fuel should be plentiful to find around the station. In a pinch, you can even transfer some from the big tanks with a soda can. Just remember to empty the soda can first, I don't think it likes soda as fuel. + Welding fuel should be plentiful to find around the station. In a pinch, you can even transfer some from the big tanks with a soda can or water bottle. Just remember to empty the soda can first, I don't think it likes soda as fuel. # The Big Ones @@ -33,10 +33,11 @@ - The (S.U.P.E.R.)P.A.C.M.A.N. is intended for usage by engineering for advanced power scenarios. Bootstrapping the engine, powering departments, and so on. + The (S.U.P.E.R.)P.A.C.M.A.N. is intended for usage by engineering for advanced power scenarios. Bootstrapping larger engines, powering departments, and so on. - The S.U.P.E.R.P.A.C.M.A.N. boasts larger power output and longer runtime at maximum output, but scales down to lower outputs less efficiently. + The S.U.P.E.R.P.A.C.M.A.N. boasts a larger power output and longer runtime at maximum output, but scales down to lower outputs less efficiently. - They connect directly to [color=yellow]MV[/color] or [color=orange]HV[/color] power cables, able to switch between them for flexibility. + They connect directly to [color=yellow]MV[/color] or [color=orange]HV[/color] power cables, and are able to switch between them for flexibility. + The S.U.P.E.R.P.A.C.M.A.N and P.A.C.M.A.N require uranium sheets and plasma sheets as fuel, respectively. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Power.xml b/Resources/ServerInfo/Guidebook/Engineering/Power.xml index 62b38e397d..7dd227ee9b 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Power.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Power.xml @@ -1,53 +1,53 @@ -# Power + # Power -SS14 has a fairly in-depth power system through which all devices on the station receive electricity. It's divided into three main powernets; HV, LV, and MV. - - - - - + SS14 has a fairly in-depth power system through which all devices on the station receive electricity. It's divided into three main powernets; High Voltage, Medium Voltage, and Low Voltage. + + + + + -## Cabling -The three major cable types (HV, MV, and LV) can be used to form independent powernets. Examine them for a description of their uses. - - - - - + ## Cabling + The three major cable types (HV, MV, and LV) can be used to form independent powernets. Examine them for a description of their uses. + + + + + -## Power storage -Each power storage device presented functions as the transformer for its respective power level (HV, MV, and LV) and also provides a fairly sizable backup battery to help flatten out spikes and dips in power usage. - - - - - + ## Power storage + Each power storage device presented functions as the transformer for its respective power level (HV, MV, and LV), and also provides a fairly sizable backup battery to help flatten out spikes and dips in power usage. + + + + + -## Ramping -Contrary to what one might expect from a video game electrical simulation, power is not instantly provided upon request. Generators and batteries take time to ramp up to match the draw imposed on them, which leads to brownouts when there are large changes in current draw all at once, for example when batteries run out. + ## Ramping + Contrary to what one might expect from a video game electrical simulation, power is not instantly provided upon request. Generators and batteries take time to ramp up to match the draw imposed on them, which leads to brownouts when there are large changes in current draw all at once; for example, when batteries run out. -## Installing power storage -Substations are the most self-explanatory. Simply install the machine on top of an MV and HV cable, it will draw power from the HV cable to provide to MV. + ## Installing power storage + Substations are the most self-explanatory. Simply install the machine on top of an MV and HV cable; it will draw power from the HV cable to provide to MV. -Installing APCs is similarly simple, except APCs are exclusively wallmounted machinery and cannot be installed on the floor. Make sure it has both MV and LV connections. + Installing APCs is similarly simple, except APCs are exclusively wallmounted machinery and cannot be installed on the floor. Make sure it has both MV and LV connections. -Installing a SMES requires you construct a cable terminal to use as the input. The SMES will draw power from the terminal and send power out from underneath. The terminal will ensure that the HV input and HV output do not connect. Avoid connecting a SMES to itself, this will result in a short circuit which can result in power flickering or outages depending on severity. + Installing a SMES requires you construct a cable terminal to use as the input. The SMES will draw power from the terminal and send power out from underneath. The terminal will ensure that the HV input and HV output do not connect. Avoid connecting a SMES to itself; this will result in a short circuit, which can result in power flickering or outages depending on severity. -## APC breaking -Currently the only power storage device that has a limit to its power network is APC. As soon as all connected devices and machinery demand more than [color=#a4885c]24kW[/color] it's breaker will pop and everything will turn off. - - - + ## APC breaking + Currently the only power storage device that has a limit to its power to the network is the APC. As soon as all connected devices and machinery demand more than [color=#a4885c]24kW[/color] of power, its breaker will pop and everything will turn off. In the case that you are not an engineer, call an engineer (or cyborg) to re-enable it, after reducing the load back down to [color=#a4885c]below[/color] 24kW. + + + -## Checking power grid -1. Use the [color=#a4885c]t-ray scanner[/color] in order to locate cables that are hidden under tiles. (skip this step if cables aren't hidden) -2. Pry open the tile that is blocking your access to the cable with a [color=#a4885c]crowbar[/color]. (skip this step if cables aren't hidden) -3. Equip your trusty [color=#a4885c]Multitool[/color] and click on any cable to see powergrid stats. - - - - - + ## Checking the power grid + 1. Use the [color=#a4885c]t-ray scanner[/color] in order to locate cables that are hidden under tiles. (skip this step if cables aren't hidden) + 2. Pry open the tile that is blocking your access to the cable with a [color=#a4885c]crowbar[/color]. (skip this step if cables aren't hidden) + 3. Equip your trusty [color=#a4885c]Multitool[/color] and click on any cable to see the power-grid stats. + + + + + diff --git a/Resources/ServerInfo/Guidebook/Engineering/RTG.xml b/Resources/ServerInfo/Guidebook/Engineering/RTG.xml index 1d71ee9144..6149b58049 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/RTG.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/RTG.xml @@ -1,17 +1,20 @@ -# Radioisotope Thermoelectric Generator (RTG) + # Radioisotope Thermoelectric Generator (RTG) - - - - + + + -Making power using a Radioisotope Thermoelectric Generator (RTG) is similar to making power using solar. -RTGs only provide 10 kW of power, but they provide it for free and for the entire round. -Basically, if you connect an RTG to your power grid, it'll give you free power. + Making power using a Radioisotope Thermoelectric Generator (RTG) is similar to making power using solars. + RTGs only provide [color=#a4885c]10kW[/color] of power, but they provide it for free and for the entire round. + Basically, if you connect an RTG to your power grid, it'll give you [color=#a4885c]free power[/color]. + However, they're only accessible through salvage finding one on an expedition. Should they bring some in, make sure to thank them! -Sometimes, RTGs are damaged. -Damaged RTGs behave just like regular ones, but they're radioactive. -That means they're more dangerous, but on the bright side, you can put radiation collectors next to them to turn that radiation into more power. - - \ No newline at end of file + + + + Sometimes, RTGs appear damaged. + Damaged RTGs behave just like regular ones, but they're [color=yellow]radioactive[/color]. + That means they're more dangerous, but on the bright side, you can put radiation collectors next to them to turn that radiation into more power. + This is usually more worthwhile, considering the power is still free, so long as you can find a safe spot to put the RTG(s) in. + diff --git a/Resources/ServerInfo/Guidebook/Engineering/Shuttlecraft.xml b/Resources/ServerInfo/Guidebook/Engineering/Shuttlecraft.xml index 7e743ddd68..21956d600c 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Shuttlecraft.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Shuttlecraft.xml @@ -1,40 +1,39 @@ -# Shuttle-craft + # Shuttle-craft -Shuttle construction is simple and easy, albeit rather expensive and hard to pull off within an hour. It's a good activity if you have a significant amount of spare time on your hands and want a bit of a challenge. + Shuttle construction is simple and easy, albeit rather expensive and hard to pull off within an hour. It's a good activity if you have a significant amount of spare time on your hands and want a bit of a challenge. -## Getting started -Required parts: - - - - - - - - - - - - - + ## Getting started + Required parts: + + + + + + + + + + + + + -Optional parts: - - - - - - - - - - + Optional parts: + + + + + + + + + + -Head out into space with steel sheets and metal rods in hand, and once you're three or more meters away from the station, click near or under you with the rods in hand. This will place some lattice, which can then be turned into plating with the steel sheets. Expand your lattice platform by clicking just off the edge with rods in hand. + Head out into space with steel sheets and metal rods in hand, and once you're three or more tiles away from the station, click near or under you with the rods in hand. This will place some lattice, which can then be turned into plating with steel sheets or floor tiles. Expand your lattice platform by clicking just off the edge with some rods in hand. -From there, once you have the shape you want, bring out and install thrusters at the edges. They must be pointing outward into space to function and will not fire if there's a tile in the way of the nozzle. Install a gyroscope where convenient, and use your substation and generator to set up power. Construct a wall on top of an MV cable and then install an APC on that to power the devices onboard. - -Finally, install the shuttle computer wherever is convenient and ensure all your thrusters and gyroscopes are receiving power. If they are, congratulations, you should have a functional shuttle! Making it livable and good looking is left as an exercise to the reader. + From there, once you have the shape you want, bring out and install thrusters at the edges. They must be pointing outward into space to function and will not fire if there's a tile in the way of the nozzle. Install a gyroscope where convenient, and use your substation and generator to set up power. Construct a wall on top of an MV cable and then install an APC on that to power the devices onboard. + Finally, install the shuttle computer wherever is convenient and ensure all your thrusters and gyroscopes are receiving power (remember to wire the MV and LV networks!). If they are; congratulations, you should have a functional shuttle! Making it livable and good looking is left as an exercise to the reader. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Singularity.xml b/Resources/ServerInfo/Guidebook/Engineering/Singularity.xml index 3553d43e6f..3c0dd665e2 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Singularity.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Singularity.xml @@ -1,109 +1,141 @@ -# Gravitational Singularity Engine + # Singularity / Tesla Engine -The Gravitational Singularity Engine can yield infinite power, with no fueling required. It can also destroy the whole station with equal ease. It uses a Particle Accelerator to fire high energy particles at a Singularity Generator to form a singularity. The singularity then pulses radiation which is absorbed by Radiation Collectors. + The Singularity Engine / Tesla Engine can yield [color=#a4885c]infinite power[/color], with no fueling required. It can also [color=red]destroy the whole station[/color] with equal ease. It uses a Particle Accelerator to fire high energy particles at a Singularity Generator to form a singularity or ball lightning. + The singularity then pulses radiation which is absorbed by Radiation Collectors, or the ball lightning then zaps nearby tesla coils and grounding rods to provide power. -## Setting it up + # Setting it up -The Gravitational Singularity Engine requires 4 subsystems to work properly: + Both engines requires 4 subsystems to work properly; two are shared between both engines: -## Gravitational singularity generator - - - -The generator should be anchored at the center of the containment area since this is where the singularity will appear at. + ## Containment field generators and Emitters + + + + + + The emitters connect to MV cables and fire lasers as long as they have power and are turned on. + Fire the emitters at enabled containment field generators to activate them. + If two containment field generators are active, in range and are in the same cardinal axis, a containment field will appear. + The containment field will repel the singularity or tesla, keeping it from escaping, and yield a little bit of power every time anything bounces off of them. -## Containment field generators and emitters - - - - - -The emitters connect to MV cables and fire lasers as long as they have power and are turned on. -Fire emitters at containment field generators to activate them. -If two containment field generators are active, in range and in the same cardinal axis, a containment field will appear. -The containment field will repell the singularity, keeping it from escaping, and yield a little bit of power every time anything bounces off of them. -Emitter lasers and containment field can cause damage, avoid touching them when active. + The emitter lasers and the containment fields can also cause damage and/or cause you to be sent flying into deep space; [color=#a4885c]avoid touching them[/color] when active. + It is recommended to [color=#a4885c]lock the emitters[/color] with [keybind="AltActivateItemInWorld"/], to prevent any break-in no-gooders from loosing the singularity or tesla by simply switching off the field. -## Radiation collectors - - - - -They connect to HV cables and generate power from nearby radiation sources when turned on. -Radiation collectors require a tank full of gaseous plasma in order to operate. -Continous radiation exposure will gradually consume the stored plasma, so replace depleted tanks with fresh ones to maintain a high power output. + Teslas can have significantly smaller containment fields than singularity containment fields; adjusting field size is recommended, as the tesla becomes easier to keep watch on in a simply 3x3 field setup. -## Particle accelerator + ## Particle accelerator - - - + + + - - - - - + + + + + - - - + + + - - - - - + + + + + -The Particle Accelerator (PA) is a multi-tile structure that launches accelerated particles from its emitters. Its emitters should always face the gravitational singularity generator. -Some stations already have an unfinished PA. To complete, first ensure there is MV cable beneath the PA power box, anchor all parts, then add LV cable to each part. - - - -Then use a screwdriver to screw back the panels. -Scan parts using the PA control computer to check if it's operational. If it shows up as incomplete, examine for what's missing. - - - + The Particle Accelerator (PA) is a multi-tile structure that launches accelerated particles from its emitters. Its emitters should always face the generator. + Some stations already have an unfinished PA. To complete it, first ensure there is a MV cable beneath the PA power box, anchor all the parts, and then add an LV cable to each part. + + + + Then use a screwdriver to screw back the panels. + [color=#a4885c]Scan parts[/color] using the PA control computer to check if it's operational (the PA will not function if you do not scan it!). If it shows up as incomplete, examine what's missing. + + + -## Turing on the Gravitational Singularity Engine + The other two subsystems are unique to each other: -[color=#a4885c]Do not[/color] turn the PA on unless all other subsystems are working properly. + ## Gravitational singularity generator or Ball lightning generator + + + + + The generator should be anchored at the center of the containment area, since this is where the singularity/tesla should appear at. -Turn power on using the PA control computer. Set strength to an appropiate level. Currently the only appropriate level is [color=#f0684d]1[/color], anything above that will ensure that singularity grows too strong to handle. -The higher the output stength is set on PA control computer, the bigger the singularity will be. + ## Radiation collectors or Tesla coils + + + + + The radition collectors connect to HV cables and generate power from nearby radiation sources when turned on. + Radiation collectors require a tank full of gaseous plasma in order to operate. + Continous radiation exposure will gradually convert the stored plasma into tritium, so replace depleted plasma tanks with fresh ones regularly to maintain a high power output. -The PA will now draw power from the power net and start firing particles at the Gravitational singularity generator. + + + + + The tesla coils connect to HV cables and provide a stream of power after being zapped by the ball lightning. + However, tesla coils usually do not fully absorb the lightning strike, and the grounding rods are required to prevent lighting from arcing to and obliterating nearby machines. + Do note that one grounding rod is not a foolproof solution; get [color=#a4885c]atleast 4 rods[/color] around the containment field to make it mathematically unlikely for the tesla to escape. + As the ball lightning zaps tesla coils, they will degrade from wear; make sure to [color=#a4885c]weld them[/color] every now and then to keep generating power. - - - - - + ## Turing on the Engines -A singularity will soon appear at the position of the Gravitational singularity generator. - - - + [color=red]Do not[/color] turn the PA on unless all the other subsystems are working properly and there is enough power to start the engine. -If no particle is hitting the singularity generator, the singularity will start to slowly decay until it disappear. + Turn power on using the PA control computer. Set the strength to an appropiate level. Currently the only appropriate level is [color=#f0684d]1[/color]; anything above that will ensure that singularity grows too strong to handle. + The higher the output stength is set on PA control computer, the bigger the singularity will be. -## Safety -Singularity emits radiation around it, so always keep a distance. Consider getting radiation shielding gear beforehand. Seek medical attention if experiencing health issues. + Currently, the output power does not affect the ball lightning, beyond giving the ball lightning extra orbs around it. - - - + The PA will now draw power from the power net and start firing particles at the generators. - - - - - + + + + + -A singularity might move around, but the containment field will repel it. -If a singularity escapes its containment field, often referred to as a "singuloose," it will attract and then consume everything in its way. + A singularity or ball lightning will soon appear at the position of the Gravitational singularity generator. + + + or + + -In such circumstances, there is little to be done other than running in the opposite direction. + If no particles are hitting the singularity, the singularity will start to slowly decay until it disappears. + This is not the case for the tesla; feel free to disconnect the PA after the tesla has been set up. + + ## Safety + The singularity emits a large amount of radiation around it, so always keep a distance from it. Consider getting [color=yellow]radiation shielding gear[/color] beforehand. Seek medical attention if you are experiencing health issues. + + + + + + + + + + + + The singularity might move around, but the containment field will repel it. + + The tesla creates large bolts of lightning around it, so make sure to wear insuls before approaching it. If you aren't, and it zaps you, pray that the ball lightning doesn't stunlock you and eventually send you into crit. + + + + If a singularity or tesla escapes its containment field, often referred to as a "singuloose" or "tesloose" respectively, it will attract and then consume everything in its way, growing larger as it does so, or it will begin to obliterate every machine in its path, and shock all crew personnel. + + In such circumstances, there is little to be done other than running in the opposite direction. + + + + However, if science has happened to research [color=#D381C9]Portable Particle Decelerators[/color], or if cargo can order them in time, you may be able to stop the singularity from eating the whole station. + Good luck on the tesla, though; it is merely too powerful to recontain after breaching. diff --git a/Resources/ServerInfo/Guidebook/Engineering/TEG.xml b/Resources/ServerInfo/Guidebook/Engineering/TEG.xml index 9e8697a9e1..7739181f94 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/TEG.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/TEG.xml @@ -19,17 +19,17 @@ Note that the circulators are [color=#a4885c]directional[/color]: they will only let gas through one way. You can see this direction in-game by examining the circulator itself. A pressure difference is required across the input and output, so pumps are generally provided and must be turned on. - There is no preference for which side must be hot or cold, there need only be a difference in temperature between them. The gases in the two "loops" are never mixed, only energy is exchanged between them. The hot side will cool down, the cold side will heat up. + There is no preference for which side must be hot or cold, there need only be a difference in temperature between them. The gases in the two "loops" are never mixed, [color=#a4885c]only energy is exchanged between them[/color]. The hot side will cool down, the cold side will heat up. ## The Pipes - There are 2 major pipenets to worry about here: The Hot Loop (where gas will be burnt for heat), and The Cold Loop (where circulated, heated waste gas will either be removed into space or cooled back down). Make sure that [bold]both pipenets do NOT mix[/bold], as only heat should be transferred between the two through the TEG. + There are 2 major pipenets to worry about here: [color=red]The Hot Loop[/color] (where gas will be burnt for heat), and [color=cyan]The Cold Loop[/color] (where circulated, heated waste gas will either be removed into space or cooled back down). Make sure that [color=#a4885c][bold]both pipenets do NOT mix[/bold][/color], as only heat should be transferred between the two through the TEG. # The Hot Loop As I'm sure a wise person once said: the best way to make something hot is to light it on fire. Well, depending on context, that may not be very wise, but luckily your engineering department has just what's needed to do it wisely after all. - As stated above, there are many different layouts one can follow to heat up (or cool down) gases; this part of the guide will cover 2 common methods one will often see for the hot loop when the TEG is setup: The Pipe Burn, and the Burn chamber. + As stated above, there are many different layouts one can follow to heat up (or cool down) gases; this part of the guide will cover 2 common methods one will often see for the hot loop when the TEG is setup: [color=#a4885c]The Pipe Burn[/color], and [color=red]the Burn Chamber[/color]. Side note: Plasma fires burn relatively cool compared to, for example, Tritium fires. It may be viable to extract Tritium from an extraction setup (using a 97/3 ratio of O2/Plasma) and react it with Oxygen to get truly hellish temperatures for power. Although, this is just a recommendation; I'm not ya mum. @@ -37,7 +37,7 @@ Also known as the naive method, this is generally discouraged when working for efficiency. However, if all you need is a smidge of power to run the station, and you don't feel like setting up the burn chamber, this method will do. - TODO: Remove this section when atmos pipes are updated to have pressure/temperature limits in a future atmos refactor. + [color=#444444]TODO: Remove this section when atmos pipes are updated to have pressure/temperature limits in a future atmos refactor.[/color] Most (if not all) pipe burns follow this general layout: @@ -55,8 +55,8 @@ - The Gas input is pretty self-explanatory; this is where you will input the O2-Plasma mix to be burnt. A 2:1 (67/33) ratio of Oxygen to Plasma is recommended for the hottest burn. - The High-pressure pump serves 2 purposes; first, it prevents the burn from backwashing into the supply pipe, which would be.. bad, for many reasons. Second, it maintains a positive pressure in the following pipe segment, which is important to allow the burn to continue, especially since hot gases expand. - - The Pipe segment is where the burn actually occurs; to start it off, one can use a heater to increase the temperature up to the ignition temperature of Plasma. Afterwards, the reaction should be self-sustaining, so long as the Pressure and Moles supplied remains high enough. Be warned; if you wish to remove the heater, it will carry some of this superheated gas with it, transferring it to the next pipenet you connect it to. Best to space the gas through a space vent, if you must. - - The Low-pressure pump (whose pressure should be [italics]slightly lower[/italics] than the input pump) prevents [italics]all[/italics] the gas from passing through the circulator, which could result in the loss of the Moles required to sustain a burn. + - The Pipe segment is where the burn actually occurs; to start it off, one can use a heater to increase the temperature up to the ignition temperature of Plasma. Afterwards, the reaction should be self-sustaining, so long as the Pressure and Moles supplied remains high enough. [color=#a4885c]Be warned[/color]; if you wish to remove the heater, it will carry some of this superheated gas with it, transferring it to the next pipenet you connect it to. Best to space the gas through a space vent, if you must. + - The Low-pressure pump (whose pressure should be [italic]slightly lower[/italic] than the input pump) prevents [italic]all[/italic] the gas from passing through the circulator, which could result in the loss of the Moles required to sustain a burn. - The Circulator is where this generated heat will flow to the cold loop; afterwards, feel free to space the waste gases. Note: Pressure pumps are used here as, while they pump on pressure (not flow-rate, which is comparatively faster), they are a bit easier to control when it comes to the limited Plasma supply on-station. However, the steps shown can be followed with volumetric pumps too. @@ -68,25 +68,53 @@ Most (if not all) stations have the burn chamber separated from the main atmospherics block by a 1-wide spaced grid, presumably to prevent conduction. The chambers consist of 3(+1) important parts: - The Air Injector/Passive Vent - The Space Vent - - The Radiator Loop + - The Scrubber Array + + Here is one layer of an example setup: (pipes can and do need to be layered under the scrubbers below to connect them!) + + + + + + + + + + + + + + + + + + + + + + + + + + + + - Most normal burn chambers don't come with Heat-Exchangers; instead, they have air scrubbers (and optionally, an air alarm) to help filter for Tritium, which is a highly reactive, hot-burning gas that can also be used to heat the TEG efficiently. - However, this is a slightly more advanced setup than just burning plasma, as it needs 2 burn chambers instead of 1 (one for tritium production, one for burning said tritium), so remove the scrubbers and retrofit the burn chamber with a parallel array of heat-exchangers instead. - The air injector (or Passive Vent) injects air (or allows air to flow) into the burn chamber. Either should be supplemented with a pump before it, to keep pressures high. - There is a notable difference between the passive vent and the air injector; the air injector can only keep injecting air up to 9MPa, which can be reached very easily with a good burn. Ideally, switch out the air injector for a passive vent connected to a volume pump. + There is a notable difference between the passive vent and the air injector; the air injector can only keep injecting air up to [color=#a4885c]9MPa[/color], which can be reached very easily with a good burn. Ideally, switch out the air injector for a passive vent connected to a volume pump. - The space vent (designated as a blast door to space on one side of the burn chamber) allows waste gases to be expelled and destroyed. Open this to keep the pressure under control. + The space vent (designated as a blast door to space on one side of the burn chamber) allows waste gases to be expelled and destroyed. Open this every now and then to keep the pressure under control, or to space excess input gas. - The radiator loop collects heat from the burnt gases and brings it to the TEG. To maximize efficiency, hook up the heat-exchangers [italics]in parallel to each other[/italics], with a pressure pump at max pressure after the array and a volumetric pump before the array. - The pressure of the volumetric pump should be set to ( 200 / number of heat-exchangers ) L/s. For example, having 2 heat-exchangers would mean you should set the pressure to 100 L/s. - Finally, fill the whole loop with (ideally) a high heat capacity gas, like Frezon or Plasma. (Yes, Frezon =/= Cold. Frezon has one of the highest heat capacities in the game; so long as it isn't reacting with Nitrogen, it can actually be heated and can store heat really well!) + The scrubber array filters out all the burnt gasses and sends them through the TEG. Note that using default settings on the scrubbers is a bad idea, as valuable plasma will be filtered out too. + Instead, use a network configurator to connect all the scrubbers to a nearby air alarm, and set the air alarm's scrubber settings to scrub everything except Oxygen and Plasma, and to siphon air aswell. This ensures that as much heat as available can be collected and sent to the TEG. + + Note that these are just two of many ways you can setup the hot loop; [color=#a4885c]feel free to mix and match setups as needed![/color] Volume pumps in replacement of pressure pumps, radiator loops for heat collection, or even a Pyroclastic anomaly to provide said heat! The stars are the limit! # The Cold Loop As with the Hot Loop, the Cold Loop must also be setup in order to operate the TEG. However, the Cold Loop is usually a lot more low-tech than the Hot Loop; in reality, the Cold Loop only has to be "relatively" cooler -- hey, room temperature is technically cooler than the surface of the sun, right? - There are 3 main methods you will see used for the Cold Loop: The Water Cooler (see: Liltenhead's video on the TEG), the Coolant Array and the Freezer Loop. + There are 3 main methods you will see used for the Cold Loop: [color=#a4885c]The Water Cooler[/color] (see: Liltenhead's video on the TEG), [color=cyan]the Coolant Array[/color] and [color=#a4885c]the Freezer Loop[/color]. ## The Water Cooler @@ -103,14 +131,17 @@ - TODO: Remove this section when gas miners are removed in a future atmos refactor. + [color=#444444]TODO: Remove this section when gas miners are removed in a future atmos refactor.[/color] ## Coolant Array - This is the default method for the Cold Loop you will see on a variety of stations. Being of moderate complexity and having no losses of any resource, this [italics]should[/italics] be the main method of cooling down the TEG. However, every station at the moment somehow has their heat exchangers hooked up wrong, reducing efficiency greatly. (Thanks a bunch, NT!) + This is the default method for the Cold Loop you will see on a variety of stations. Being of moderate complexity and having no losses of any resource, this [color=#a4885c]should[/color] be the main method of cooling down the TEG. However, most stations at the moment somehow have their heat exchangers hooked up wrong (or suggest incorrect piping), reducing efficiency greatly. [color=#444444](Thanks a bunch, NT!)[/color] - To use heat-exchangers properly, they must be setup in [italics]parallel[/italics], not in series (like what you see on most stations). A gas pump at max pressure should be placed after, and a volumetric pump before the heat-exchangers. - The flow-rate of the volumetric pump should be set to ( 200 / number of heat-exchangers ) L/s. + To use heat-exchangers properly, they must be setup in [color=#a4885c]parallel[/color], not in series (like what you see on most stations). A gas pump at max pressure should be placed after, and a volumetric pump before the heat-exchangers. + The flow-rate of the volumetric pump should be set using the following formula: + + [color=cyan]( 200 / number of heat-exchangers )[/color] L/s. + Simply speaking, the Coolant Array consists of 3 major parts: An input connector port, a few pumps and the heat-exchanger array out in space. It can be setup like so: @@ -161,7 +192,7 @@ - Connector Port: Use this to input a gas with high heat capacity; most of the time, Plasma or Frezon is used to do so, as they both have very high specific heat capacities (although most any gas will do). (Yes, Plasma =/= Hot. You can cool it down, and it acts as a really good heat exchange medium.) - Input/Output Pumps: Used to make sure gas keeps flowing through both the Circulator and the Heat-Exchanger array. As the gas cools down and heats up (and as it flows through the Exchanger), pressure must be applied for it to keep flowing. - - Heat-Exchanger: Basically, just a bunch of heat-exchanger pipes in space. Not much to say, besides the fact that it cools down the gas inside it. Make sure the heat-exchangers are placed on lattice, not plating! Otherwise, the heat-exchange efficiency will be greatly reduced, as the heat-exchangers aren't directly exposed to space below them. + - Heat-Exchanger: Basically, just a bunch of heat-exchanger pipes in space. Not much to say, besides the fact that it cools down the gas inside it. Make sure the heat-exchangers are [color=#a4885c]placed on lattice, not plating[/color]! Otherwise, the heat-exchange efficiency will be greatly reduced, as the heat-exchangers aren't directly exposed to space below them. ## The Freezer Loop From af516cff614fb69b29d4c5e764d3c3cecd5740ac Mon Sep 17 00:00:00 2001 From: PJBot Date: Mon, 22 Apr 2024 09:00:00 +0000 Subject: [PATCH 14/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 756625850e..447a02e092 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: Plykiya - changes: - - message: Door remotes no longer have their 5G signals absorbed by mobs, machines, - or Ian's cute butt. - type: Fix - id: 5919 - time: '2024-02-12T02:34:13.532550+00:00' - url: null - author: FungiFellow changes: - message: Lowered Reoccurence Delay for Ion Storm @@ -3847,3 +3839,10 @@ id: 6418 time: '2024-04-22T08:45:39.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27134 +- author: Terraspark4941 + changes: + - message: Updated the engineering section of the guidebook! + type: Tweak + id: 6419 + time: '2024-04-22T08:58:54.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26851 From fd0ca42c58315b4c4f91fe9364c3dc3d88ba5624 Mon Sep 17 00:00:00 2001 From: Just-a-Unity-Dev <67359748+Just-a-Unity-Dev@users.noreply.github.com> Date: Mon, 22 Apr 2024 10:03:03 +0000 Subject: [PATCH 15/61] General slime improvements (#23425) * General slime improvements * Finish morphing * oops 2x2 not 3x3 * actually lets ball - 2x3 inventory * Last two things on the todo list * .\RobustToolbox\ * JUST COMPILE * fix tests 2.0 * fix tests 3.0 * Do reviews * minor change * guideboob * more --------- Co-authored-by: Kara --- Content.Server/Geras/GerasComponent.cs | 18 ++++++ Content.Server/Geras/GerasSystem.cs | 40 +++++++++++++ .../Storage/EntitySystems/StorageSystem.cs | 4 +- Content.Shared/Geras/SharedGerasSystem.cs | 16 ++++++ .../en-US/components/storage-component.ftl | 2 + Resources/Locale/en-US/geras/geras.ftl | 2 + Resources/Prototypes/Actions/types.yml | 17 +++++- .../Prototypes/Entities/Mobs/NPCs/slimes.yml | 57 +++++++++++++++---- .../Entities/Mobs/Species/slime.yml | 34 +++++++++++ Resources/Prototypes/Polymorphs/polymorph.yml | 11 ++++ .../ServerInfo/Guidebook/Mobs/SlimePerson.xml | 16 +++++- 11 files changed, 201 insertions(+), 16 deletions(-) create mode 100644 Content.Server/Geras/GerasComponent.cs create mode 100644 Content.Server/Geras/GerasSystem.cs create mode 100644 Content.Shared/Geras/SharedGerasSystem.cs create mode 100644 Resources/Locale/en-US/geras/geras.ftl diff --git a/Content.Server/Geras/GerasComponent.cs b/Content.Server/Geras/GerasComponent.cs new file mode 100644 index 0000000000..eaf792502f --- /dev/null +++ b/Content.Server/Geras/GerasComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Actions; +using Content.Shared.Polymorph; +using Robust.Shared.Prototypes; + +namespace Content.Server.Geras; + +/// +/// This component assigns the entity with a polymorph action. +/// +[RegisterComponent] +public sealed partial class GerasComponent : Component +{ + [DataField] public ProtoId GerasPolymorphId = "SlimeMorphGeras"; + + [DataField] public ProtoId GerasAction = "ActionMorphGeras"; + + [DataField] public EntityUid? GerasActionEntity; +} diff --git a/Content.Server/Geras/GerasSystem.cs b/Content.Server/Geras/GerasSystem.cs new file mode 100644 index 0000000000..a5377a021e --- /dev/null +++ b/Content.Server/Geras/GerasSystem.cs @@ -0,0 +1,40 @@ +using Content.Server.Actions; +using Content.Server.Polymorph.Systems; +using Content.Server.Popups; +using Content.Shared.Actions; +using Content.Shared.Geras; +using Robust.Shared.Player; + +namespace Content.Server.Geras; + +/// +public sealed class GerasSystem : SharedGerasSystem +{ + [Dependency] private readonly ActionsSystem _actionsSystem = default!; + [Dependency] private readonly PolymorphSystem _polymorphSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnMorphIntoGeras); + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(EntityUid uid, GerasComponent component, MapInitEvent args) + { + // try to add geras action + _actionsSystem.AddAction(uid, ref component.GerasActionEntity, component.GerasAction); + } + + private void OnMorphIntoGeras(EntityUid uid, GerasComponent component, MorphIntoGeras args) + { + var ent = _polymorphSystem.PolymorphEntity(uid, component.GerasPolymorphId); + + if (!ent.HasValue) + return; + + _popupSystem.PopupEntity(Loc.GetString("geras-popup-morph-message-others", ("entity", ent.Value)), ent.Value, Filter.PvsExcept(ent.Value), true); + _popupSystem.PopupEntity(Loc.GetString("geras-popup-morph-message-user"), ent.Value, ent.Value); + } +} diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index 27694ee61c..5d41e0a521 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -76,13 +76,13 @@ public sealed partial class StorageSystem : SharedStorageSystem }; if (uiOpen) { - verb.Text = Loc.GetString("verb-common-close-ui"); + verb.Text = Loc.GetString("comp-storage-verb-close-storage"); verb.Icon = new SpriteSpecifier.Texture( new("/Textures/Interface/VerbIcons/close.svg.192dpi.png")); } else { - verb.Text = Loc.GetString("verb-common-open-ui"); + verb.Text = Loc.GetString("comp-storage-verb-open-storage"); verb.Icon = new SpriteSpecifier.Texture( new("/Textures/Interface/VerbIcons/open.svg.192dpi.png")); } diff --git a/Content.Shared/Geras/SharedGerasSystem.cs b/Content.Shared/Geras/SharedGerasSystem.cs new file mode 100644 index 0000000000..f5dea466a2 --- /dev/null +++ b/Content.Shared/Geras/SharedGerasSystem.cs @@ -0,0 +1,16 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Geras; + +/// +/// A Geras is the small morph of a slime. This system handles exactly that. +/// +public abstract class SharedGerasSystem : EntitySystem +{ + +} + +public sealed partial class MorphIntoGeras : InstantActionEvent +{ + +} diff --git a/Resources/Locale/en-US/components/storage-component.ftl b/Resources/Locale/en-US/components/storage-component.ftl index 29c858891a..e742c83f6a 100644 --- a/Resources/Locale/en-US/components/storage-component.ftl +++ b/Resources/Locale/en-US/components/storage-component.ftl @@ -8,3 +8,5 @@ comp-storage-cant-drop = You can't let go of { THE($entity) }! comp-storage-window-title = Storage Item comp-storage-window-weight = { $weight }/{ $maxWeight }, Max Size: {$size} comp-storage-window-slots = Slots: { $itemCount }/{ $maxCount }, Max Size: {$size} +comp-storage-verb-open-storage = Open Storage +comp-storage-verb-close-storage = Close Storage diff --git a/Resources/Locale/en-US/geras/geras.ftl b/Resources/Locale/en-US/geras/geras.ftl new file mode 100644 index 0000000000..3cd3f101ff --- /dev/null +++ b/Resources/Locale/en-US/geras/geras.ftl @@ -0,0 +1,2 @@ +geras-popup-morph-message-user = You shift and morph into a small version of you! +geras-popup-morph-message-others = {CAPITALIZE(THE($entity))} shifts and morphs into a blob of slime! diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index b91b26e357..aeffd5b8c5 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -92,7 +92,6 @@ state: gib event: !type:ActivateImplantEvent - - type: entity id: ActionActivateFreedomImplant name: Break Free @@ -171,6 +170,22 @@ state: icon event: !type:UseDnaScramblerImplantEvent +- type: entity + id: ActionMorphGeras + name: Morph into Geras + description: Morphs you into a Geras - a miniature version of you which allows you to move fast, at the cost of your inventory. + noSpawn: true + components: + - type: InstantAction + charges: 1 + itemIconStyle: BigAction + useDelay: 10 # prevent spam + priority: -20 + icon: + sprite: Mobs/Aliens/slimes.rsi + state: blue_adult_slime + event: !type:MorphIntoGeras + - type: entity id: ActionToggleSuitPiece name: Toggle Suit Piece diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index 10e9218d6e..95c30a174f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -1,16 +1,10 @@ - type: entity name: basic slime - id: MobAdultSlimes + id: BaseMobAdultSlimes parent: [ SimpleMobBase, MobCombat ] abstract: true description: It looks so much like jelly. I wonder what it tastes like? components: - - type: NpcFactionMember - factions: - - SimpleNeutral - - type: HTN - rootTask: - task: SimpleHostileCompound - type: Sprite drawdepth: Mobs sprite: Mobs/Aliens/slimes.rsi @@ -111,6 +105,19 @@ successChance: 0.5 interactSuccessString: petting-success-slimes interactFailureString: petting-failure-generic + - type: Speech + speechVerb: Slime + speechSounds: Slime + - type: TypingIndicator + proto: slime + +- type: entity + name: basic slime + id: MobAdultSlimes + parent: BaseMobAdultSlimes + abstract: true + description: It looks so much like jelly. I wonder what it tastes like? + components: - type: ReplacementAccent accent: slimes - type: GhostTakeoverAvailable @@ -118,11 +125,37 @@ makeSentient: true name: ghost-role-information-slimes-name description: ghost-role-information-slimes-description - - type: Speech - speechVerb: Slime - speechSounds: Slime - - type: TypingIndicator - proto: slime + - type: NpcFactionMember + factions: + - SimpleNeutral + - type: HTN + rootTask: + task: SimpleHostileCompound + +- type: entity + name: geras + description: A geras of a slime - the name is ironic, isn't it? + id: MobSlimesGeras + parent: BaseMobAdultSlimes + noSpawn: true + components: + # they portable... + - type: MultiHandedItem + - type: Item + size: Huge + - type: Sprite + color: "#FFFFFF55" + - type: MeleeWeapon + attackRate: 2 + damage: + types: + Blunt: 4 + - type: DamageStateVisuals + states: + Alive: + Base: blue_adult_slime + Dead: + Base: blue_adult_slime_dead - type: entity name: blue slime diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml index 3eabb7dc07..5599825a0c 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml @@ -12,6 +12,30 @@ - type: Body prototype: Slime requiredLegs: 2 + # they like eat it idk lol + - type: Storage + grid: + - 0,0,1,2 + maxItemSize: Large + storageInsertSound: + path: /Audio/Voice/Slime/slime_squish.ogg + - type: ContainerContainer + containers: + storagebase: !type:Container + ents: [] + - type: UserInterface + interfaces: + - key: enum.StorageUiKey.Key + type: StorageBoundUserInterface + - key: enum.VoiceMaskUIKey.Key + type: VoiceMaskBoundUserInterface + - key: enum.HumanoidMarkingModifierKey.Key + type: HumanoidMarkingModifierBoundUserInterface + - key: enum.StrippingUiKey.Key + type: StrippableBoundUserInterface + # to prevent bag open/honk spam + - type: UseDelay + delay: 0.5 - type: HumanoidAppearance species: SlimePerson - type: Speech @@ -27,6 +51,16 @@ - type: Damageable damageContainer: Biological damageModifierSet: Slime + - type: Geras + - type: PassiveDamage # Around 8 damage a minute healed + allowedStates: + - Alive + damageCap: 65 + damage: + types: + Heat: -0.14 + groups: + Brute: -0.14 - type: DamageVisuals damageOverlayGroups: Brute: diff --git a/Resources/Prototypes/Polymorphs/polymorph.yml b/Resources/Prototypes/Polymorphs/polymorph.yml index b4249f8a3e..ce89f41d37 100644 --- a/Resources/Prototypes/Polymorphs/polymorph.yml +++ b/Resources/Prototypes/Polymorphs/polymorph.yml @@ -75,6 +75,17 @@ inventory: Transfer revertOnDeath: true +- type: polymorph + id: SlimeMorphGeras + configuration: + entity: MobSlimesGeras + transferName: false + transferHumanoidAppearance: false + inventory: Drop + transferDamage: true + revertOnDeath: true + revertOnCrit: true + # this is a test for transferring some visual appearance stuff - type: polymorph id: TestHumanMorph diff --git a/Resources/ServerInfo/Guidebook/Mobs/SlimePerson.xml b/Resources/ServerInfo/Guidebook/Mobs/SlimePerson.xml index fc72c60dbf..0374d4cb95 100644 --- a/Resources/ServerInfo/Guidebook/Mobs/SlimePerson.xml +++ b/Resources/ServerInfo/Guidebook/Mobs/SlimePerson.xml @@ -10,7 +10,21 @@ They exhale nitrous oxide and are unaffected by it. Their body can process 6 reagents at the same time instead of just 2. - Their Slime "blood" can not be regenerated from Iron. Slime Blood is technically a source of + Slimepeople can morph into a [bold]"geras"[/bold] (an archaic slimefolk term), which is a smaller slime form that can [bold]pass through grilles[/bold], + but forces them to drop their inventory and held items. It's handy for a quick getaway. A geras is small enough to pick up (with two hands) + and fits in a duffelbag. + + + + + + Slimepeople have an [bold]internal 2x2 storage inventory[/bold] inside of their slime membrane. Anyone can see what's inside and take it out of you without asking, + so be careful. They [bold]don't drop their internal storage when they morph into a geras, however![/bold] + + Slimepeople have slight accelerated regeneration compared to other humanoids. They're also capable of hardening their fists, and as such have stronger punches, + although they punch a little slower. + + Their slime "blood" can not be regenerated from Iron. Slime Blood is technically a source of moderately filling food for other species, although drinking the blood of your coworkers is usually frowned upon. They suffocate 80% slower, but take pressure damage 9% faster. This makes them by far the species most capable to survive in hard vacuum. For a while. From 289bbc70d774dd00f4ae535d12d14501edb45927 Mon Sep 17 00:00:00 2001 From: PJBot Date: Mon, 22 Apr 2024 10:04:08 +0000 Subject: [PATCH 16/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 447a02e092..43c84ce447 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: FungiFellow - changes: - - message: Lowered Reoccurence Delay for Ion Storm - type: Tweak - id: 5920 - time: '2024-02-12T02:35:10.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25135 - author: PoorMansDreams changes: - message: Ammo is now tipped! @@ -3846,3 +3839,18 @@ id: 6419 time: '2024-04-22T08:58:54.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26851 +- author: eclips_e + changes: + - message: Slimepeople can now morph into a "geras"--a smaller slime form that can + pass under grilles, at the cost of dropping all of their inventory. They can + also be picked up with two hands and placed into duffelbags. + type: Add + - message: Slimepeople now have an internal 2x2 storage that they (and anyone around + them) can access. It is not dropped when morphing into a geras! + type: Add + - message: Slimepeople now have slightly increased regeneration and a slightly meatier + punch, but slower attacks. + type: Add + id: 6420 + time: '2024-04-22T10:03:03.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/23425 From 85c6383cc025289d3376ef0e76f358483b888b18 Mon Sep 17 00:00:00 2001 From: Kara Date: Mon, 22 Apr 2024 03:18:36 -0700 Subject: [PATCH 17/61] Minor slime fix (#27237) --- Content.Server/Geras/GerasSystem.cs | 1 + Resources/Prototypes/Actions/types.yml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/Geras/GerasSystem.cs b/Content.Server/Geras/GerasSystem.cs index a5377a021e..e25ea8f028 100644 --- a/Content.Server/Geras/GerasSystem.cs +++ b/Content.Server/Geras/GerasSystem.cs @@ -36,5 +36,6 @@ public sealed class GerasSystem : SharedGerasSystem _popupSystem.PopupEntity(Loc.GetString("geras-popup-morph-message-others", ("entity", ent.Value)), ent.Value, Filter.PvsExcept(ent.Value), true); _popupSystem.PopupEntity(Loc.GetString("geras-popup-morph-message-user"), ent.Value, ent.Value); + args.Handled = true; } } diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index aeffd5b8c5..be66691564 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -177,7 +177,6 @@ noSpawn: true components: - type: InstantAction - charges: 1 itemIconStyle: BigAction useDelay: 10 # prevent spam priority: -20 From 32a90120ebaa9e013594432f9fd1e1025cb321d8 Mon Sep 17 00:00:00 2001 From: FungiFellow <151778459+FungiFellow@users.noreply.github.com> Date: Mon, 22 Apr 2024 07:18:28 -0500 Subject: [PATCH 18/61] Syndi-Cat Tweaks (#27222) * Syndicat Price 10 -> 6 * Update animals.yml * Update animals.yml * Update uplink_catalog.yml * Update uplink_catalog.yml * Update animals.yml --- Resources/Prototypes/Catalog/uplink_catalog.yml | 11 +++-------- Resources/Prototypes/Entities/Mobs/NPCs/animals.yml | 10 ++++++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index 6375ac37f7..a7c2dbc7a4 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -122,7 +122,7 @@ name: uplink-sniper-bundle-name description: uplink-sniper-bundle-desc icon: { sprite: /Textures/Objects/Weapons/Guns/Snipers/heavy_sniper.rsi, state: base } - productEntity: BriefcaseSyndieSniperBundleFilled + productEntity: BriefcaseSyndieSniperBundleFilled cost: Telecrystal: 12 categories: @@ -990,15 +990,10 @@ icon: { sprite: /Textures/Mobs/Pets/cat.rsi, state: syndicat } productEntity: MobCatSyndy cost: - Telecrystal: 10 + Telecrystal: 6 categories: - UplinkAllies - conditions: - - !type:StoreWhitelistCondition - whitelist: - tags: - - NukeOpsUplink - + - type: listing id: UplinkSyndicatePersonalAI name: uplink-syndicate-pai-name diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index aeae35efbf..819c7beacf 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -2731,6 +2731,16 @@ - type: NpcFactionMember factions: - Syndicate + - type: MeleeWeapon + damage: + types: + Piercing: 10 + Structural: 10 + - type: Insulated + - type: Tag + tags: + - DoorBumpOpener + - type: MovementAlwaysTouching - type: entity name: space cat From 8bc2ee81dd6fd72675e08009379523d2a3ff97f8 Mon Sep 17 00:00:00 2001 From: PJBot Date: Mon, 22 Apr 2024 12:19:34 +0000 Subject: [PATCH 19/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 43c84ce447..77b8f7a4b1 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: PoorMansDreams - changes: - - message: Ammo is now tipped! - type: Tweak - id: 5921 - time: '2024-02-12T03:02:52.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25103 - author: PoorMansDreams changes: - message: Buyable Janitorial Trolley! @@ -3854,3 +3847,11 @@ id: 6420 time: '2024-04-22T10:03:03.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/23425 +- author: FungiFellow + changes: + - message: Syndi-Cats are now 6TC, Insulated, Available to Syndies, Can Move in + Space, Open Doors, and Hit Harder + type: Tweak + id: 6421 + time: '2024-04-22T12:18:28.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27222 From 5b0c3ecf738c630df5e3909163fde3e494ec85bc Mon Sep 17 00:00:00 2001 From: lzk <124214523+lzk228@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:00:13 +0200 Subject: [PATCH 20/61] Little analysis console rearrangement (#27244) --- .../Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml index 15cc7a82ff..29f4a54847 100644 --- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml +++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml @@ -22,11 +22,6 @@ ToolTip="{Loc 'analysis-console-print-tooltip-info'}"> - - + + From 8eecd9cc2db16f584471571432c9f0b3a8c292d1 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Mon, 22 Apr 2024 18:46:22 -0400 Subject: [PATCH 21/61] Prevent ghosts from triggering examine artifact triggers (#27249) --- .../Triggers/Systems/ArtifactExamineTriggerSystem.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactExamineTriggerSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactExamineTriggerSystem.cs index cbade1682e..b7afbcfc8b 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactExamineTriggerSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactExamineTriggerSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components; using Content.Shared.Examine; +using Content.Shared.Ghost; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems; @@ -15,6 +16,10 @@ public sealed class ArtifactExamineTriggerSystem : EntitySystem private void OnExamine(EntityUid uid, ArtifactExamineTriggerComponent component, ExaminedEvent args) { + // Prevent ghosts from activating this trigger unless they have CanGhostInteract + if (TryComp(args.Examiner, out var ghost) && !ghost.CanGhostInteract) + return; + _artifact.TryActivateArtifact(uid); } } From ee0f63d2736936e4bdeae0043f42a25de0779733 Mon Sep 17 00:00:00 2001 From: PJBot Date: Mon, 22 Apr 2024 22:47:28 +0000 Subject: [PATCH 22/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 77b8f7a4b1..d0553e3c93 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: PoorMansDreams - changes: - - message: Buyable Janitorial Trolley! - type: Add - id: 5922 - time: '2024-02-12T06:35:19.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25139 - author: Plykiya changes: - message: Stun batons and stun prods now display their hits remaining. @@ -3855,3 +3848,10 @@ id: 6421 time: '2024-04-22T12:18:28.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27222 +- author: Tayrtahn + changes: + - message: Ghosts can no longer trigger artifacts by examining them. + type: Fix + id: 6422 + time: '2024-04-22T22:46:22.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27249 From 06a5e00c99ae0eff70ef29b68a432a9159fe55cb Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:28:09 +1200 Subject: [PATCH 23/61] Update engine to v219.1.3 (#27257) --- RobustToolbox | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RobustToolbox b/RobustToolbox index 73da147b88..4e87d93009 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit 73da147b8811c8d032e0f119ef507a1c11ff4073 +Subproject commit 4e87d93009748778c27ae835b16297596e5b1563 From 0e2f5d6bf730c67cc5adeeca2f0ba5fbe6f1f9f4 Mon Sep 17 00:00:00 2001 From: Tyzemol <85772526+Tyzemol@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:48:26 +0500 Subject: [PATCH 24/61] Fix slime storage issue (#27260) fix_slime_storage --- Resources/Prototypes/Entities/Mobs/Species/slime.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml index 5599825a0c..de2f88e59d 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml @@ -14,6 +14,7 @@ requiredLegs: 2 # they like eat it idk lol - type: Storage + clickInsert: false grid: - 0,0,1,2 maxItemSize: Large From c4a6bcca5c4b0f5ee14fd6d556df181ba5016e39 Mon Sep 17 00:00:00 2001 From: PJBot Date: Tue, 23 Apr 2024 08:49:33 +0000 Subject: [PATCH 25/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index d0553e3c93..2ae8a2fb59 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Plykiya - changes: - - message: Stun batons and stun prods now display their hits remaining. - type: Tweak - id: 5923 - time: '2024-02-12T06:36:06.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25141 - author: Ilya246 changes: - message: Autolathes may now make empty air tanks. @@ -3855,3 +3848,10 @@ id: 6422 time: '2024-04-22T22:46:22.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27249 +- author: Tyzemol + changes: + - message: Slimes no longer absorb all items used on them + type: Fix + id: 6423 + time: '2024-04-23T08:48:26.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27260 From 32c1f8f8cd2cff03d8de11be139cfd46f552c2ea Mon Sep 17 00:00:00 2001 From: Skye <57879983+Rainbeon@users.noreply.github.com> Date: Tue, 23 Apr 2024 04:57:09 -0400 Subject: [PATCH 26/61] Fix: Suit Sensors Vitals (#27259) Vitals now functions correctly --- Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs b/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs index e3ca466b08..27539dd22b 100644 --- a/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs +++ b/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs @@ -24,7 +24,7 @@ public sealed class SuitSensorStatus public bool IsAlive; public int? TotalDamage; public int? TotalDamageThreshold; - public float? DamagePercentage => TotalDamageThreshold == null || TotalDamage == null ? null : TotalDamage / TotalDamageThreshold; + public float? DamagePercentage => TotalDamageThreshold == null || TotalDamage == null ? null : TotalDamage / (float) TotalDamageThreshold; public NetCoordinates? Coordinates; } From 31b9f98e6d2a44b6d2700b5399b47a261fb8771c Mon Sep 17 00:00:00 2001 From: PJBot Date: Tue, 23 Apr 2024 08:58:15 +0000 Subject: [PATCH 27/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 2ae8a2fb59..77c4e376df 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Ilya246 - changes: - - message: Autolathes may now make empty air tanks. - type: Add - id: 5924 - time: '2024-02-12T06:37:28.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25130 - author: Ubaser changes: - message: Resprited the CE's jetpack. @@ -3855,3 +3848,10 @@ id: 6423 time: '2024-04-23T08:48:26.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27260 +- author: Rainbeon + changes: + - message: Suit sensor vitals now function again. + type: Fix + id: 6424 + time: '2024-04-23T08:57:09.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27259 From 88b56efba102c0f537c268f3b23e21715d5c34d6 Mon Sep 17 00:00:00 2001 From: Ghagliiarghii <68826635+Ghagliiarghii@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:24:58 -0400 Subject: [PATCH 28/61] Increase availibility of combat knives to security (#27224) * Increase availibility of combat knives to security * whitelisted combatknife in secbelt for Diona * Revert SecDrobe change for MGS --- .../Catalog/VendingMachines/Inventories/sec.yml | 1 + Resources/Prototypes/Entities/Clothing/Belt/belts.yml | 1 + Resources/Prototypes/Recipes/Lathes/security.yml | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml index 0aa814196a..b54c2cdbcf 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml @@ -14,6 +14,7 @@ ClothingEyesHudSecurity: 2 ClothingEyesEyepatchHudSecurity: 2 ClothingBeltSecurityWebbing: 5 + CombatKnife: 3 Zipties: 12 RiotShield: 2 RiotLaserShield: 2 diff --git a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml index 1f90b42152..0b0d9e018a 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml @@ -472,6 +472,7 @@ - Sidearm - MagazinePistol - MagazineMagnum + - CombatKnife components: - Stunbaton - FlashOnTrigger diff --git a/Resources/Prototypes/Recipes/Lathes/security.yml b/Resources/Prototypes/Recipes/Lathes/security.yml index b3100ed70b..f536242238 100644 --- a/Resources/Prototypes/Recipes/Lathes/security.yml +++ b/Resources/Prototypes/Recipes/Lathes/security.yml @@ -30,6 +30,15 @@ Steel: 300 Plastic: 300 +- type: latheRecipe + id: CombatKnife + result: CombatKnife + category: Weapons + completetime: 2 + materials: + Steel: 250 + Plastic: 100 + - type: latheRecipe id: WeaponLaserCarbine result: WeaponLaserCarbine From 2f0c02ef3548be651fbb2ed90d79ba87d87dfb9b Mon Sep 17 00:00:00 2001 From: PJBot Date: Tue, 23 Apr 2024 11:26:04 +0000 Subject: [PATCH 29/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 77c4e376df..6b6d251e99 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Ubaser - changes: - - message: Resprited the CE's jetpack. - type: Tweak - id: 5925 - time: '2024-02-12T20:44:23.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25150 - author: EdenTheLiznerd changes: - message: Deathnettles are no longer able to bypass armor and don't do as much @@ -3855,3 +3848,11 @@ id: 6424 time: '2024-04-23T08:57:09.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27259 +- author: Ghagliiarghii + changes: + - message: Security can now find replacements for their trusty Combat Knife in the + SecVend, or craft them with the Sec Lathe. + type: Tweak + id: 6425 + time: '2024-04-23T11:24:58.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27224 From eee056eaf063caa690516bf8b4eb7e65a0199e9c Mon Sep 17 00:00:00 2001 From: Whisper <121047731+QuietlyWhisper@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:30:01 -0400 Subject: [PATCH 30/61] lower max firestacks to 10, refactor flammable (#27159) * lower max firestacks to 10, refactor flammable * fix * uncap fire stack damage, lower fire stack damage --- .../Systems/AdminVerbSystem.Smites.cs | 2 +- .../Atmos/Components/FlammableComponent.cs | 34 ++++++++++++++----- .../Atmos/EntitySystems/FlammableSystem.cs | 20 ++++------- Resources/Prototypes/Entities/Mobs/base.yml | 2 +- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index 942882f7ae..042bac3956 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -152,7 +152,7 @@ public sealed partial class AdminVerbSystem Act = () => { // Fuck you. Burn Forever. - flammable.FireStacks = FlammableSystem.MaximumFireStacks; + flammable.FireStacks = flammable.MaximumFireStacks; _flammableSystem.Ignite(args.Target, args.User); var xform = Transform(args.Target); _popupSystem.PopupEntity(Loc.GetString("admin-smite-set-alight-self"), args.Target, diff --git a/Content.Server/Atmos/Components/FlammableComponent.cs b/Content.Server/Atmos/Components/FlammableComponent.cs index 679b551058..e00f5efbdc 100644 --- a/Content.Server/Atmos/Components/FlammableComponent.cs +++ b/Content.Server/Atmos/Components/FlammableComponent.cs @@ -11,49 +11,65 @@ namespace Content.Server.Atmos.Components [ViewVariables(VVAccess.ReadWrite)] [DataField] - public bool OnFire { get; set; } + public bool OnFire; [ViewVariables(VVAccess.ReadWrite)] [DataField] - public float FireStacks { get; set; } + public float FireStacks; [ViewVariables(VVAccess.ReadWrite)] - [DataField("fireSpread")] + [DataField] + public float MaximumFireStacks = 10f; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float MinimumFireStacks = -10f; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public string FlammableFixtureID = "flammable"; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float MinIgnitionTemperature = 373.15f; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField] public bool FireSpread { get; private set; } = false; [ViewVariables(VVAccess.ReadWrite)] - [DataField("canResistFire")] + [DataField] public bool CanResistFire { get; private set; } = false; - [DataField("damage", required: true)] + [DataField(required: true)] [ViewVariables(VVAccess.ReadWrite)] public DamageSpecifier Damage = new(); // Empty by default, we don't want any funny NREs. /// /// Used for the fixture created to handle passing firestacks when two flammable objects collide. /// - [DataField("flammableCollisionShape")] + [DataField] public IPhysShape FlammableCollisionShape = new PhysShapeCircle(0.35f); /// /// Should the component be set on fire by interactions with isHot entities /// [ViewVariables(VVAccess.ReadWrite)] - [DataField("alwaysCombustible")] + [DataField] public bool AlwaysCombustible = false; /// /// Can the component anyhow lose its FireStacks? /// [ViewVariables(VVAccess.ReadWrite)] - [DataField("canExtinguish")] + [DataField] public bool CanExtinguish = true; /// /// How many firestacks should be applied to component when being set on fire? /// [ViewVariables(VVAccess.ReadWrite)] - [DataField("firestacksOnIgnite")] + [DataField] public float FirestacksOnIgnite = 2.0f; /// diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs index 058faf443e..c569997ffe 100644 --- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs +++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs @@ -49,13 +49,9 @@ namespace Content.Server.Atmos.EntitySystems private EntityQuery _physicsQuery; - public const float MinimumFireStacks = -10f; - public const float MaximumFireStacks = 20f; + // This should probably be moved to the component, requires a rewrite, all fires tick at the same time private const float UpdateTime = 1f; - public const float MinIgnitionTemperature = 373.15f; - public const string FlammableFixtureID = "flammable"; - private float _timer; private readonly Dictionary, float> _fireEvents = new(); @@ -134,7 +130,7 @@ namespace Content.Server.Atmos.EntitySystems if (!TryComp(uid, out var body)) return; - _fixture.TryCreateFixture(uid, component.FlammableCollisionShape, FlammableFixtureID, hard: false, + _fixture.TryCreateFixture(uid, component.FlammableCollisionShape, component.FlammableFixtureID, hard: false, collisionMask: (int) CollisionGroup.FullTileLayer, body: body); } @@ -192,7 +188,7 @@ namespace Content.Server.Atmos.EntitySystems // Normal hard collisions, though this isn't generally possible since most flammable things are mobs // which don't collide with one another, shouldn't work here. - if (args.OtherFixtureId != FlammableFixtureID && args.OurFixtureId != FlammableFixtureID) + if (args.OtherFixtureId != flammable.FlammableFixtureID && args.OurFixtureId != flammable.FlammableFixtureID) return; if (!flammable.FireSpread) @@ -254,7 +250,7 @@ namespace Content.Server.Atmos.EntitySystems private void OnTileFire(Entity ent, ref TileFireEvent args) { - var tempDelta = args.Temperature - MinIgnitionTemperature; + var tempDelta = args.Temperature - ent.Comp.MinIgnitionTemperature; _fireEvents.TryGetValue(ent, out var maxTemp); @@ -287,7 +283,7 @@ namespace Content.Server.Atmos.EntitySystems if (!Resolve(uid, ref flammable)) return; - flammable.FireStacks = MathF.Min(MathF.Max(MinimumFireStacks, flammable.FireStacks + relativeFireStacks), MaximumFireStacks); + flammable.FireStacks = MathF.Min(MathF.Max(flammable.MinimumFireStacks, flammable.FireStacks + relativeFireStacks), flammable.MaximumFireStacks); if (flammable.OnFire && flammable.FireStacks <= 0) Extinguish(uid, flammable); @@ -439,12 +435,10 @@ namespace Content.Server.Atmos.EntitySystems EnsureComp(uid); _ignitionSourceSystem.SetIgnited(uid); - var damageScale = MathF.Min( flammable.FireStacks, 5); - if (TryComp(uid, out TemperatureComponent? temp)) - _temperatureSystem.ChangeHeat(uid, 12500 * damageScale, false, temp); + _temperatureSystem.ChangeHeat(uid, 12500 * flammable.FireStacks, false, temp); - _damageableSystem.TryChangeDamage(uid, flammable.Damage * damageScale, interruptsDoAfters: false); + _damageableSystem.TryChangeDamage(uid, flammable.Damage * flammable.FireStacks, interruptsDoAfters: false); AdjustFireStacks(uid, flammable.FirestackFade * (flammable.Resisting ? 10f : 1f), flammable); } diff --git a/Resources/Prototypes/Entities/Mobs/base.yml b/Resources/Prototypes/Entities/Mobs/base.yml index 065d62c748..0a2b68d0a1 100644 --- a/Resources/Prototypes/Entities/Mobs/base.yml +++ b/Resources/Prototypes/Entities/Mobs/base.yml @@ -192,7 +192,7 @@ canResistFire: true damage: #per second, scales with number of fire 'stacks' types: - Heat: 3 + Heat: 1.5 - type: FireVisuals sprite: Mobs/Effects/onfire.rsi normalState: Generic_mob_burning From 401911b7347ea483b21b5c197fc0bef6b8483c73 Mon Sep 17 00:00:00 2001 From: PJBot Date: Tue, 23 Apr 2024 11:31:07 +0000 Subject: [PATCH 31/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 6b6d251e99..0d430fc5be 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: EdenTheLiznerd - changes: - - message: Deathnettles are no longer able to bypass armor and don't do as much - damage - type: Tweak - id: 5926 - time: '2024-02-13T01:20:02.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25068 - author: Vasilis changes: - message: You can now inspect peoples id's and health though glass if you are within @@ -3856,3 +3848,11 @@ id: 6425 time: '2024-04-23T11:24:58.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27224 +- author: Whisper + changes: + - message: Fire stack limit reduced from 20 to 10. Fire transfers will be less effective, + and fires will not last as long. + type: Tweak + id: 6426 + time: '2024-04-23T11:30:01.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27159 From 905102064cd1311943093c9feeecca8bbd8bc358 Mon Sep 17 00:00:00 2001 From: brainfood1183 <113240905+brainfood1183@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:31:48 +0100 Subject: [PATCH 32/61] Maints Exit Sign (#26831) * Maints Exit Sign * change description * id, sprites changed from maints to exit prototype now has an unshaded layer so it can be seen in the dark. --- .../Structures/Wallmounts/Signs/signs.yml | 12 ++++++ .../Wallmounts/signs.rsi/direction_exit.png | Bin 0 -> 15393 bytes .../signs.rsi/direction_exit_unshaded.png | Bin 0 -> 15747 bytes .../Structures/Wallmounts/signs.rsi/meta.json | 38 +++++++++++++++++- 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 Resources/Textures/Structures/Wallmounts/signs.rsi/direction_exit.png create mode 100644 Resources/Textures/Structures/Wallmounts/signs.rsi/direction_exit_unshaded.png diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml index 9aac49a6bd..18af2ec06c 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml @@ -169,6 +169,18 @@ - type: Sprite state: direction_library +- type: entity + parent: BaseSignDirectional + id: SignDirectionalExit + name: exit sign + description: A directional sign pointing toward the nearest exit. + components: + - type: Sprite + layers: + - state: direction_exit + - state: direction_exit_unshaded + shader: unshaded + - type: entity parent: BaseSignDirectional id: SignDirectionalMed diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/direction_exit.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/direction_exit.png new file mode 100644 index 0000000000000000000000000000000000000000..d2b60cb5fed39d1feea375101b56198381c7aa59 GIT binary patch literal 15393 zcmeI3Yitx%6o7BhAXZU~4~QCF7f?~AGqXFp+Z}ed#a*B^EYxX&pG#g_nfoyXI9qDn=`)r;&K4M_}ZFCJ^HS;-s8ri&n@chpQ5ibQ#A_>0M0t! zdY6GGo|*)J|AZQAG#jIHMOjZ)NQ&MJD>BIxnhijpDwC4rHfTDUVT-B-o!{^7b~;og z=v?55a?w;6wyHHt(y(F4yqLVCP4+9!s!(|#BO(V$XiAPuGNBn_Cg{xi713{Nm~}d` zQ_QxYbEXy0(HO0Bg!MFZcq+V%%z1qdpTC0l_nZjN!eMS&9qmm~jj zhRV^eKw625^^w{6a7YR|TTL@1vTSE(XGN#GLQl7_yx;Fgb69?*X1(1EI-ORa!YjY8WU3HIGxF>ZMK&X)SiXW|OBpG0!6wz7 zPGpy+$Sh32B-Bg;+3_XqQmwkF8?E{XAq(me1EVvEMhme^t~Z%18QL&sbsz+Jq$Mql zSXT@T#eW3bLBvTrd7yDeBA4L}9dO9hY zAvGzrKsKed1lZ!BZDt{>#IT;w(`Z>R8*wg!0n(9uCfLd|xl zl$180RewR}U;!3dTA^cEHrYBBCE4n2A=xSqs07#{qs3(vEc0gS@Z%gEf}DSw`N%Ok zTcJiOubivwtdX4VI$$hSc%Tj$w3F^*4@T zqz?7}9YZmT`+NkmnQGD^x;4eLKOG)7d^hYhQrc!j(Tt|)1^a3n*UB5#;GD%)*Ywgz*f=%x?dZ?c6O z&SB+Q=wkookK&I0O|O_Q8UzDE7AY<=A52SeVL-?t#YN_WX(=uY2w9}K$b2v@#f1SO zixd}`52mHKFd$@+;v)0Gv=kQxge+2AWImXd;=+KCMT(2e2h&nq7!a~Zagq68T8aw; zLKZ15G9OG!abZBnBE?1KgJ~%)3Vor!L$?? z281k9Tx33&mg2&IkVT4%%m>p_To@3tNO6(*U|Na`140%lE;1iXOL1X9$Rfo>=7VV| zE({1+q`1g@FfGM}0U?V-T;;__IH87~+w4RSXdYdEeG)y~>5yycqW~4t z^nDC~b`F4V7Xu)!17M>5NYk6M0Vvy68<`o)^qqLJO1qc4;H|0!hh}hldQM*aw448V z?8~RsKeWc*FhyIlqhZB4b8m5+ad$({yN|bBws_B!+kOvD+q*My*ZitOUmsk0!=m0R z;!CglayB@<+ICIY{N2YZrtkMIopIaYo1(weerURX-=sd)Tb}8f-y?kX^^3>D3wCd~ zO#k)$?_TeizwEr~A5WeQ-dQ!K>xOl&nD4FH`}2;&FKpj-e^2)!7A!yfi)%~wCC2mD z>~C5fgX-za-a7QviR1FgN5((9?Rfaqa}R91=%YtluiA8={e!oU^_)7nZ_VkB#1)rf(bY%&FVQ&ezzHYF=vz@lTIL(@a9i|@VfqBS+- zTIh*#(NrDi)arZFV1937tJ2%01XS1ba8)oX!2n5M$#gcE&`l{Da^>Pm@YrrnM_T_U1Ov>SUwO4u$;gO0t0(6W{+;kSw=S}=7SXD zM1ZNJwUnh9I&H_5V@9_Xa=Gk8rLAyX$y6zkZWh=5E%}B0!B%FQ&XK{qW~S3pA~3>#`RO_ewx1I6Qu zGd!mR7#|S5jNcPb89wgzDSputi~H3a#Awv%J|J|&P`d5a0z>Bui)tt^eEGCEAb9+$ z8ermHo@00(04An-eM~&&=TzVq+_5-cOymeQAW>6VgLO|%IQrPvqQYc8cTDugVRV`E zco~rsVG14yjN(;zS@g>uUWBCbC@O>*kg8rY;cV@3%wjl~bCqpASk}L_C?vF8g-Xe3 z6WHr7fv8lK!e>|{s$wMUy1!|VzJH+tf4{su|k6N7uEtfSNCAuAJA5wGx`2) zb{$Q0=lc5u4sI-O{}x=GGV0oncXi6BYuR&wu32HBkPT^ec-hZB`5W_m_?QmDUXRD^ z39@K2>d~xaL82|9!8@pF-wC{8!3Py8I<^ny=nx8y`Jr+=Q0J;=z7;;M!+#2F>94|o z!dkljT+f5@6_aHChBKK{o+a_VdrBbM{GEJPJjC1vEA#l-#oi%($Ed#Uz#@8cyR0CMV%)vpKkg5 z^zrVaZ=N`qk=twUQePRz4?TbRobmXvrtyzB&Z__6oT_Du={sj%-85yzfHG!^6+N5uWkPp2hUW&vtF!vMsc9 Date: Tue, 23 Apr 2024 11:32:54 +0000 Subject: [PATCH 33/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 0d430fc5be..afb19b11fe 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: Vasilis - changes: - - message: You can now inspect peoples id's and health though glass if you are within - details range. - type: Tweak - id: 5927 - time: '2024-02-13T06:41:56.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25163 - author: metalgearsloth changes: - message: Fix decal error spam in console due to a missing overlay. @@ -3856,3 +3848,10 @@ id: 6426 time: '2024-04-23T11:30:01.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27159 +- author: brainfood1183 + changes: + - message: Directional Exit signs for maints! + type: Add + id: 6427 + time: '2024-04-23T11:31:48.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26831 From c129bb177c8f4e7eec0ae43c09a48aa5bc0f7268 Mon Sep 17 00:00:00 2001 From: pigeonpeas <147350443+pigeonpeas@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:34:09 -0400 Subject: [PATCH 34/61] Allow the purchase of emitters from cargo. (#27229) add emitter crates back to cargo + changes the contents of an emittercrate from emitter to emitterflatpack :+1: --- .../Catalog/Cargo/cargo_engines.yml | 22 +++++++++--------- .../Catalog/Fills/Crates/engines.yml | 2 +- .../Entities/Objects/Devices/flatpack.yml | 14 +++++++++++ .../Objects/Devices/flatpack.rsi/emitter.png | Bin 0 -> 326 bytes .../Objects/Devices/flatpack.rsi/meta.json | 5 +++- 5 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 Resources/Textures/Objects/Devices/flatpack.rsi/emitter.png diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_engines.yml b/Resources/Prototypes/Catalog/Cargo/cargo_engines.yml index bd00b0c2d4..8d3bea5075 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_engines.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_engines.yml @@ -28,17 +28,17 @@ category: cargoproduct-category-name-engineering group: market -#- type: cargoProduct -# name: "emitter crate" -# id: EngineSingularityEmitter -# description: "Contains an emitter. Used only for dangerous applications." -# icon: -# sprite: Structures/Power/Generation/Singularity/emitter.rsi -# state: emitter2 -# product: CrateEngineeringSingularityEmitter -# cost: 3000 -# category: cargoproduct-category-name-engineering -# group: market +- type: cargoProduct + name: "emitter crate" + id: EngineSingularityEmitter + description: "Contains an emitter. Used only for dangerous applications." + icon: + sprite: Structures/Power/Generation/Singularity/emitter.rsi + state: emitter2 + product: CrateEngineeringSingularityEmitter + cost: 3000 + category: cargoproduct-category-name-engineering + group: market - type: cargoProduct id: EngineSingularityCollector diff --git a/Resources/Prototypes/Catalog/Fills/Crates/engines.yml b/Resources/Prototypes/Catalog/Fills/Crates/engines.yml index 9b47036b01..79698b550a 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/engines.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/engines.yml @@ -42,7 +42,7 @@ components: - type: StorageFill contents: - - id: Emitter # TODO change to flatpack + - id: EmitterFlatpack # TODO change to flatpack - type: entity id: CrateEngineeringSingularityCollector diff --git a/Resources/Prototypes/Entities/Objects/Devices/flatpack.yml b/Resources/Prototypes/Entities/Objects/Devices/flatpack.yml index 5499348bd1..2aecd13288 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/flatpack.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/flatpack.yml @@ -110,6 +110,20 @@ - type: GuideHelp guides: [ Singularity, Power ] +- type: entity + parent: BaseFlatpack + id: EmitterFlatpack + name: emitter flatpack + description: A flatpack used for constructing an emitter. + components: + - type: Flatpack + entity: Emitter + - type: Sprite + layers: + - state: emitter + - type: GuideHelp + guides: [ Singularity, Power ] + - type: entity parent: BaseFlatpack id: TeslaGeneratorFlatpack diff --git a/Resources/Textures/Objects/Devices/flatpack.rsi/emitter.png b/Resources/Textures/Objects/Devices/flatpack.rsi/emitter.png new file mode 100644 index 0000000000000000000000000000000000000000..c663886f9cce33df769cbe389312c741626769c3 GIT binary patch literal 326 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCijK0(?ST4UBZ%J)P4s5(({UE>gxK>zz`c7 zYiDQ2(q;zKq!wP11f&E?g8YJkYLEfL1Lasrpd4p`M`SSr1K(i~W;~w1A_XYe?CIhd zV&VUG;%mMJ1rE1^-et!3|K~QloL%~jp|sB8TcH`7Q^iAZCD#Ti&u)ea_JBt^4VN-> zk^{77$T4Y^aITVWDl9%Ub6P@v(_2n%L!oOP@i)p2$u|qm{=Uz;Y1;!e`?tC#MZcR~ zO5e%;aV@W|%=jJ2@yWZX#U;?X#);!0%VY~9-rbe4j6mlwc)I$ztaD0e0sv(1 Bdo%z5 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Devices/flatpack.rsi/meta.json b/Resources/Textures/Objects/Devices/flatpack.rsi/meta.json index 2d1ca37141..8f46a0ca53 100644 --- a/Resources/Textures/Objects/Devices/flatpack.rsi/meta.json +++ b/Resources/Textures/Objects/Devices/flatpack.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC0-1.0", - "copyright": "Created by EmoGarbage404 (github) for SS14, solar-assembly-part taken from tgstation and modified at https://tgstation13.org/wiki/Guide_to_construction#Solar_Panels_and_Trackers, ame-part taken from vgstation at https://github.com/vgstation-coders/vgstation13/commit/1b7952787c06c21ef1623e494dcfe7cb1f46e041; singularity-generator, tesla-generator, radiation-collector, containment-field-generator, tesla-coil, grounding-rod inner icons made by lzk228", + "copyright": "Created by EmoGarbage404 (github) for SS14, solar-assembly-part taken from tgstation and modified at https://tgstation13.org/wiki/Guide_to_construction#Solar_Panels_and_Trackers, ame-part taken from vgstation at https://github.com/vgstation-coders/vgstation13/commit/1b7952787c06c21ef1623e494dcfe7cb1f46e041; singularity-generator, tesla-generator, radiation-collector, containment-field-generator, tesla-coil, grounding-rod inner icons made by lzk228; emitter made by pigeonpeas", "size": { "x": 32, "y": 32 @@ -39,6 +39,9 @@ }, { "name": "containment-field-generator" + }, + { + "name": "emitter" } ] } From 8117131c91a9cb7eab0f9d565bec3e9ebf27500f Mon Sep 17 00:00:00 2001 From: PJBot Date: Tue, 23 Apr 2024 11:35:15 +0000 Subject: [PATCH 35/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index afb19b11fe..996b3d5d01 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: metalgearsloth - changes: - - message: Fix decal error spam in console due to a missing overlay. - type: Fix - id: 5928 - time: '2024-02-13T07:10:44.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25170 - author: jamessimo changes: - message: Can now "wink" with the text emote ;) or ;] @@ -3855,3 +3848,10 @@ id: 6427 time: '2024-04-23T11:31:48.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26831 +- author: pigeonpeas + changes: + - message: Adds the ability to purchase emitters in cargo. + type: Add + id: 6428 + time: '2024-04-23T11:34:09.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27229 From 1bfc63c546617ee2e9be9960b14abf16eee19ad5 Mon Sep 17 00:00:00 2001 From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Date: Tue, 23 Apr 2024 08:07:12 -0400 Subject: [PATCH 36/61] fix cargo teleporter (#27255) * fix cargo teleporter * don't delete orders * basado --- .../StationCargoOrderDatabaseComponent.cs | 16 ++++ .../Cargo/Systems/CargoSystem.Orders.cs | 33 +++++--- .../Cargo/Systems/CargoSystem.Telepad.cs | 82 +++++++++++++++---- .../StationEvents/Events/CargoGiftsRule.cs | 2 +- Content.Shared/Cargo/CargoOrderData.cs | 24 ++++-- .../Components/SharedCargoTelepadComponent.cs | 5 +- .../Entities/Structures/cargo_telepad.yml | 2 + .../Prototypes/name_identifier_groups.yml | 6 ++ 8 files changed, 133 insertions(+), 37 deletions(-) diff --git a/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs b/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs index c30db08bbe..2e3b2c2115 100644 --- a/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs +++ b/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs @@ -1,4 +1,6 @@ +using Content.Server.Station.Components; using Content.Shared.Cargo; +using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Prototypes; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -38,3 +40,17 @@ public sealed partial class StationCargoOrderDatabaseComponent : Component [DataField] public EntProtoId PrinterOutput = "PaperCargoInvoice"; } + +/// +/// Event broadcast before a cargo order is fulfilled, allowing alternate systems to fulfill the order. +/// +[ByRefEvent] +public record struct FulfillCargoOrderEvent(Entity Station, CargoOrderData Order, Entity OrderConsole) +{ + public Entity OrderConsole = OrderConsole; + public Entity Station = Station; + public CargoOrderData Order = Order; + + public EntityUid? FulfillmentEntity; + public bool Handled = false; +} diff --git a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs index 7a81e1a424..13a1d3d565 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs @@ -170,13 +170,20 @@ namespace Content.Server.Cargo.Systems return; } - var tradeDestination = TryFulfillOrder(stationData, order, orderDatabase); + var ev = new FulfillCargoOrderEvent((station.Value, stationData), order, (uid, component)); + RaiseLocalEvent(ref ev); + ev.FulfillmentEntity ??= station.Value; - if (tradeDestination == null) + if (!ev.Handled) { - ConsolePopup(args.Session, Loc.GetString("cargo-console-unfulfilled")); - PlayDenySound(uid, component); - return; + ev.FulfillmentEntity = TryFulfillOrder((station.Value, stationData), order, orderDatabase); + + if (ev.FulfillmentEntity == null) + { + ConsolePopup(args.Session, Loc.GetString("cargo-console-unfulfilled")); + PlayDenySound(uid, component); + return; + } } _idCardSystem.TryFindIdCard(player, out var idCard); @@ -186,14 +193,14 @@ namespace Content.Server.Cargo.Systems var approverName = idCard.Comp?.FullName ?? Loc.GetString("access-reader-unknown-id"); var approverJob = idCard.Comp?.JobTitle ?? Loc.GetString("access-reader-unknown-id"); - var message = Loc.GetString("cargo-console-unlock-approved-order-broadcast", + var message = Loc.GetString("cargo-console-unlock-approved-order-broadcast", ("productName", Loc.GetString(order.ProductName)), ("orderAmount", order.OrderQuantity), ("approverName", approverName), ("approverJob", approverJob), ("cost", cost)); _radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false); - ConsolePopup(args.Session, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(tradeDestination.Value).EntityName))); + ConsolePopup(args.Session, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(ev.FulfillmentEntity.Value).EntityName))); // Log order approval _adminLogger.Add(LogType.Action, LogImpact.Low, @@ -201,10 +208,10 @@ namespace Content.Server.Cargo.Systems orderDatabase.Orders.Remove(order); DeductFunds(bank, cost); - UpdateOrders(station.Value, orderDatabase); + UpdateOrders(station.Value); } - private EntityUid? TryFulfillOrder(StationDataComponent stationData, CargoOrderData order, StationCargoOrderDatabaseComponent orderDatabase) + private EntityUid? TryFulfillOrder(Entity stationData, CargoOrderData order, StationCargoOrderDatabaseComponent orderDatabase) { // No slots at the trade station _listEnts.Clear(); @@ -357,7 +364,7 @@ namespace Content.Server.Cargo.Systems /// Updates all of the cargo-related consoles for a particular station. /// This should be called whenever orders change. /// - private void UpdateOrders(EntityUid dbUid, StationCargoOrderDatabaseComponent _) + private void UpdateOrders(EntityUid dbUid) { // Order added so all consoles need updating. var orderQuery = AllEntityQuery(); @@ -392,7 +399,7 @@ namespace Content.Server.Cargo.Systems string description, string dest, StationCargoOrderDatabaseComponent component, - StationDataComponent stationData + Entity stationData ) { DebugTools.Assert(_protoMan.HasIndex(spawnId)); @@ -414,7 +421,7 @@ namespace Content.Server.Cargo.Systems private bool TryAddOrder(EntityUid dbUid, CargoOrderData data, StationCargoOrderDatabaseComponent component) { component.Orders.Add(data); - UpdateOrders(dbUid, component); + UpdateOrders(dbUid); return true; } @@ -432,7 +439,7 @@ namespace Content.Server.Cargo.Systems { orderDB.Orders.RemoveAt(sequenceIdx); } - UpdateOrders(dbUid, orderDB); + UpdateOrders(dbUid); } public void ClearOrders(StationCargoOrderDatabaseComponent component) diff --git a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs index 42aabf2578..f83ec1a512 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs @@ -1,9 +1,13 @@ +using System.Linq; using Content.Server.Cargo.Components; using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; +using Content.Server.Station.Components; using Content.Shared.Cargo; using Content.Shared.Cargo.Components; using Content.Shared.DeviceLinking; using Robust.Shared.Audio; +using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Server.Cargo.Systems; @@ -13,10 +17,44 @@ public sealed partial class CargoSystem private void InitializeTelepad() { SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnTelepadPowerChange); // Shouldn't need re-anchored event SubscribeLocalEvent(OnTelepadAnchorChange); + SubscribeLocalEvent(OnTelepadFulfillCargoOrder); } + + private void OnTelepadFulfillCargoOrder(ref FulfillCargoOrderEvent args) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var tele, out var xform)) + { + if (tele.CurrentState != CargoTelepadState.Idle) + continue; + + if (!this.IsPowered(uid, EntityManager)) + continue; + + if (_station.GetOwningStation(uid, xform) != args.Station) + continue; + + // todo cannot be fucking asked to figure out device linking rn but this shouldn't just default to the first port. + if (!TryComp(uid, out var sinkComponent) || + sinkComponent.LinkedSources.FirstOrNull() is not { } console || + console != args.OrderConsole.Owner) + continue; + + for (var i = 0; i < args.Order.OrderQuantity; i++) + { + tele.CurrentOrders.Add(args.Order); + } + tele.Accumulator = tele.Delay; + args.Handled = true; + args.FulfillmentEntity = uid; + return; + } + } + private void UpdateTelepad(float frameTime) { var query = EntityQueryEnumerator(); @@ -33,14 +71,6 @@ public sealed partial class CargoSystem continue; } - if (!TryComp(uid, out var sinkComponent) || - sinkComponent.LinkedSources.FirstOrNull() is not { } console || - !HasComp(console)) - { - comp.Accumulator = comp.Delay; - continue; - } - comp.Accumulator -= frameTime; // Uhh listen teleporting takes time and I just want the 1 float. @@ -51,21 +81,22 @@ public sealed partial class CargoSystem continue; } - var station = _station.GetOwningStation(console); - - if (!TryComp(station, out var orderDatabase) || - orderDatabase.Orders.Count == 0) + if (comp.CurrentOrders.Count == 0) { comp.Accumulator += comp.Delay; continue; } var xform = Transform(uid); - if (FulfillNextOrder(orderDatabase, xform.Coordinates, comp.PrinterOutput)) + var currentOrder = comp.CurrentOrders.First(); + if (FulfillOrder(currentOrder, xform.Coordinates, comp.PrinterOutput)) { _audio.PlayPvs(_audio.GetSound(comp.TeleportSound), uid, AudioParams.Default.WithVolume(-8f)); - UpdateOrders(station.Value, orderDatabase); + if (_station.GetOwningStation(uid) is { } station) + UpdateOrders(station); + + comp.CurrentOrders.Remove(currentOrder); comp.CurrentState = CargoTelepadState.Teleporting; _appearance.SetData(uid, CargoTelepadVisuals.State, CargoTelepadState.Teleporting, appearance); } @@ -79,6 +110,29 @@ public sealed partial class CargoSystem _linker.EnsureSinkPorts(uid, telepad.ReceiverPort); } + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Comp.CurrentOrders.Count == 0) + return; + + if (_station.GetStations().Count == 0) + return; + + if (_station.GetOwningStation(ent) is not { } station) + { + station = _random.Pick(_station.GetStations().Where(HasComp).ToList()); + } + + if (!TryComp(station, out var db) || + !TryComp(station, out var data)) + return; + + foreach (var order in ent.Comp.CurrentOrders) + { + TryFulfillOrder((station, data), order, db); + } + } + private void SetEnabled(EntityUid uid, CargoTelepadComponent component, ApcPowerReceiverComponent? receiver = null, TransformComponent? xform = null) { diff --git a/Content.Server/StationEvents/Events/CargoGiftsRule.cs b/Content.Server/StationEvents/Events/CargoGiftsRule.cs index 194786fca7..0c8c9b6dc5 100644 --- a/Content.Server/StationEvents/Events/CargoGiftsRule.cs +++ b/Content.Server/StationEvents/Events/CargoGiftsRule.cs @@ -69,7 +69,7 @@ public sealed class CargoGiftsRule : StationEventSystem Loc.GetString(component.Description), Loc.GetString(component.Dest), cargoDb, - stationData! + (station.Value, stationData) )) { break; diff --git a/Content.Shared/Cargo/CargoOrderData.cs b/Content.Shared/Cargo/CargoOrderData.cs index 831010cedd..ce05d92236 100644 --- a/Content.Shared/Cargo/CargoOrderData.cs +++ b/Content.Shared/Cargo/CargoOrderData.cs @@ -1,47 +1,55 @@ using Robust.Shared.Serialization; -using Content.Shared.Access.Components; using System.Text; namespace Content.Shared.Cargo { - [NetSerializable, Serializable] - public sealed class CargoOrderData + [DataDefinition, NetSerializable, Serializable] + public sealed partial class CargoOrderData { /// /// Price when the order was added. /// + [DataField] public int Price; /// /// A unique (arbitrary) ID which identifies this order. /// - public readonly int OrderId; + [DataField] + public int OrderId { get; private set; } /// /// Prototype Id for the item to be created /// - public readonly string ProductId; + [DataField] + public string ProductId { get; private set; } /// /// Prototype Name /// - public readonly string ProductName; + [DataField] + public string ProductName { get; private set; } /// /// The number of items in the order. Not readonly, as it might change /// due to caps on the amount of orders that can be placed. /// + [DataField] public int OrderQuantity; /// /// How many instances of this order that we've already dispatched /// + [DataField] public int NumDispatched = 0; - public readonly string Requester; + [DataField] + public string Requester { get; private set; } // public String RequesterRank; // TODO Figure out how to get Character ID card data // public int RequesterId; - public readonly string Reason; + [DataField] + public string Reason { get; private set; } public bool Approved => Approver is not null; + [DataField] public string? Approver; public CargoOrderData(int orderId, string productId, string productName, int price, int amount, string requester, string reason) diff --git a/Content.Shared/Cargo/Components/SharedCargoTelepadComponent.cs b/Content.Shared/Cargo/Components/SharedCargoTelepadComponent.cs index 911ea41cca..5bc3934768 100644 --- a/Content.Shared/Cargo/Components/SharedCargoTelepadComponent.cs +++ b/Content.Shared/Cargo/Components/SharedCargoTelepadComponent.cs @@ -12,11 +12,14 @@ namespace Content.Shared.Cargo.Components; [RegisterComponent, NetworkedComponent, Access(typeof(SharedCargoSystem))] public sealed partial class CargoTelepadComponent : Component { + [DataField] + public List CurrentOrders = new(); + /// /// The actual amount of time it takes to teleport from the telepad /// [DataField("delay"), ViewVariables(VVAccess.ReadWrite)] - public float Delay = 10f; + public float Delay = 5f; /// /// How much time we've accumulated until next teleport. diff --git a/Resources/Prototypes/Entities/Structures/cargo_telepad.yml b/Resources/Prototypes/Entities/Structures/cargo_telepad.yml index d395235a53..9dc9f77cff 100644 --- a/Resources/Prototypes/Entities/Structures/cargo_telepad.yml +++ b/Resources/Prototypes/Entities/Structures/cargo_telepad.yml @@ -47,3 +47,5 @@ board: CargoTelepadMachineCircuitboard - type: Appearance - type: CollideOnAnchor + - type: NameIdentifier + group: CargoTelepads diff --git a/Resources/Prototypes/name_identifier_groups.yml b/Resources/Prototypes/name_identifier_groups.yml index 82c2f3bce9..4823e31f55 100644 --- a/Resources/Prototypes/name_identifier_groups.yml +++ b/Resources/Prototypes/name_identifier_groups.yml @@ -37,3 +37,9 @@ id: Bounty minValue: 0 maxValue: 999 + +- type: nameIdentifierGroup + id: CargoTelepads + prefix: TELE + minValue: 0 + maxValue: 999 From 0c851d256d0c5b90cbe7d8c85a47cef4fcff1a51 Mon Sep 17 00:00:00 2001 From: PJBot Date: Tue, 23 Apr 2024 12:08:18 +0000 Subject: [PATCH 37/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 996b3d5d01..37b7aa84dc 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,15 +1,4 @@ Entries: -- author: jamessimo - changes: - - message: Can now "wink" with the text emote ;) or ;] - type: Add - - message: Can now "tearfully smile" with the text emote :') and similar variants - type: Add - - message: Added more ways to "cry" with the emote :'( and similar variants - type: Tweak - id: 5929 - time: '2024-02-13T15:43:20.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25129 - author: icekot8 changes: - message: Medical berets added to MediDrobe @@ -3855,3 +3844,10 @@ id: 6428 time: '2024-04-23T11:34:09.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27229 +- author: EmoGarbage404 + changes: + - message: Fixed cargo telepads not teleporting in orders from linked consoles. + type: Fix + id: 6429 + time: '2024-04-23T12:07:12.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27255 From 10ad53df7296506485033a3dae572e4016553630 Mon Sep 17 00:00:00 2001 From: Flareguy <78941145+Flareguy@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:40:26 -0500 Subject: [PATCH 38/61] Revert "Maints Exit Sign" (#27271) Revert "Maints Exit Sign (#26831)" This reverts commit 905102064cd1311943093c9feeecca8bbd8bc358. --- .../Structures/Wallmounts/Signs/signs.yml | 12 ------ .../Wallmounts/signs.rsi/direction_exit.png | Bin 15393 -> 0 bytes .../signs.rsi/direction_exit_unshaded.png | Bin 15747 -> 0 bytes .../Structures/Wallmounts/signs.rsi/meta.json | 38 +----------------- 4 files changed, 1 insertion(+), 49 deletions(-) delete mode 100644 Resources/Textures/Structures/Wallmounts/signs.rsi/direction_exit.png delete mode 100644 Resources/Textures/Structures/Wallmounts/signs.rsi/direction_exit_unshaded.png diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml index 18af2ec06c..9aac49a6bd 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml @@ -169,18 +169,6 @@ - type: Sprite state: direction_library -- type: entity - parent: BaseSignDirectional - id: SignDirectionalExit - name: exit sign - description: A directional sign pointing toward the nearest exit. - components: - - type: Sprite - layers: - - state: direction_exit - - state: direction_exit_unshaded - shader: unshaded - - type: entity parent: BaseSignDirectional id: SignDirectionalMed diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/direction_exit.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/direction_exit.png deleted file mode 100644 index d2b60cb5fed39d1feea375101b56198381c7aa59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15393 zcmeI3Yitx%6o7BhAXZU~4~QCF7f?~AGqXFp+Z}ed#a*B^EYxX&pG#g_nfoyXI9qDn=`)r;&K4M_}ZFCJ^HS;-s8ri&n@chpQ5ibQ#A_>0M0t! zdY6GGo|*)J|AZQAG#jIHMOjZ)NQ&MJD>BIxnhijpDwC4rHfTDUVT-B-o!{^7b~;og z=v?55a?w;6wyHHt(y(F4yqLVCP4+9!s!(|#BO(V$XiAPuGNBn_Cg{xi713{Nm~}d` zQ_QxYbEXy0(HO0Bg!MFZcq+V%%z1qdpTC0l_nZjN!eMS&9qmm~jj zhRV^eKw625^^w{6a7YR|TTL@1vTSE(XGN#GLQl7_yx;Fgb69?*X1(1EI-ORa!YjY8WU3HIGxF>ZMK&X)SiXW|OBpG0!6wz7 zPGpy+$Sh32B-Bg;+3_XqQmwkF8?E{XAq(me1EVvEMhme^t~Z%18QL&sbsz+Jq$Mql zSXT@T#eW3bLBvTrd7yDeBA4L}9dO9hY zAvGzrKsKed1lZ!BZDt{>#IT;w(`Z>R8*wg!0n(9uCfLd|xl zl$180RewR}U;!3dTA^cEHrYBBCE4n2A=xSqs07#{qs3(vEc0gS@Z%gEf}DSw`N%Ok zTcJiOubivwtdX4VI$$hSc%Tj$w3F^*4@T zqz?7}9YZmT`+NkmnQGD^x;4eLKOG)7d^hYhQrc!j(Tt|)1^a3n*UB5#;GD%)*Ywgz*f=%x?dZ?c6O z&SB+Q=wkookK&I0O|O_Q8UzDE7AY<=A52SeVL-?t#YN_WX(=uY2w9}K$b2v@#f1SO zixd}`52mHKFd$@+;v)0Gv=kQxge+2AWImXd;=+KCMT(2e2h&nq7!a~Zagq68T8aw; zLKZ15G9OG!abZBnBE?1KgJ~%)3Vor!L$?? z281k9Tx33&mg2&IkVT4%%m>p_To@3tNO6(*U|Na`140%lE;1iXOL1X9$Rfo>=7VV| zE({1+q`1g@FfGM}0U?V-T;;__IH87~+w4RSXdYdEeG)y~>5yycqW~4t z^nDC~b`F4V7Xu)!17M>5NYk6M0Vvy68<`o)^qqLJO1qc4;H|0!hh}hldQM*aw448V z?8~RsKeWc*FhyIlqhZB4b8m5+ad$({yN|bBws_B!+kOvD+q*My*ZitOUmsk0!=m0R z;!CglayB@<+ICIY{N2YZrtkMIopIaYo1(weerURX-=sd)Tb}8f-y?kX^^3>D3wCd~ zO#k)$?_TeizwEr~A5WeQ-dQ!K>xOl&nD4FH`}2;&FKpj-e^2)!7A!yfi)%~wCC2mD z>~C5fgX-za-a7QviR1FgN5((9?Rfaqa}R91=%YtluiA8={e!oU^_)7nZ_VkB#1)rf(bY%&FVQ&ezzHYF=vz@lTIL(@a9i|@VfqBS+- zTIh*#(NrDi)arZFV1937tJ2%01XS1ba8)oX!2n5M$#gcE&`l{Da^>Pm@YrrnM_T_U1Ov>SUwO4u$;gO0t0(6W{+;kSw=S}=7SXD zM1ZNJwUnh9I&H_5V@9_Xa=Gk8rLAyX$y6zkZWh=5E%}B0!B%FQ&XK{qW~S3pA~3>#`RO_ewx1I6Qu zGd!mR7#|S5jNcPb89wgzDSputi~H3a#Awv%J|J|&P`d5a0z>Bui)tt^eEGCEAb9+$ z8ermHo@00(04An-eM~&&=TzVq+_5-cOymeQAW>6VgLO|%IQrPvqQYc8cTDugVRV`E zco~rsVG14yjN(;zS@g>uUWBCbC@O>*kg8rY;cV@3%wjl~bCqpASk}L_C?vF8g-Xe3 z6WHr7fv8lK!e>|{s$wMUy1!|VzJH+tf4{su|k6N7uEtfSNCAuAJA5wGx`2) zb{$Q0=lc5u4sI-O{}x=GGV0oncXi6BYuR&wu32HBkPT^ec-hZB`5W_m_?QmDUXRD^ z39@K2>d~xaL82|9!8@pF-wC{8!3Py8I<^ny=nx8y`Jr+=Q0J;=z7;;M!+#2F>94|o z!dkljT+f5@6_aHChBKK{o+a_VdrBbM{GEJPJjC1vEA#l-#oi%($Ed#Uz#@8cyR0CMV%)vpKkg5 z^zrVaZ=N`qk=twUQePRz4?TbRobmXvrtyzB&Z__6oT_Du={sj%-85yzfHG!^6+N5uWkPp2hUW&vtF!vMsc9 Date: Tue, 23 Apr 2024 19:42:34 -0700 Subject: [PATCH 39/61] Do-afters belonging to other players are now shaded (#27273) Make do-afters belonging to other players shaded Co-authored-by: Plykiya --- Content.Client/DoAfter/DoAfterOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Content.Client/DoAfter/DoAfterOverlay.cs b/Content.Client/DoAfter/DoAfterOverlay.cs index 2e23dd44ca..45981159f0 100644 --- a/Content.Client/DoAfter/DoAfterOverlay.cs +++ b/Content.Client/DoAfter/DoAfterOverlay.cs @@ -21,7 +21,7 @@ public sealed class DoAfterOverlay : Overlay private readonly ProgressColorSystem _progressColor; private readonly Texture _barTexture; - private readonly ShaderInstance _shader; + private readonly ShaderInstance _unshadedShader; /// /// Flash time for cancelled DoAfters @@ -45,7 +45,7 @@ public sealed class DoAfterOverlay : Overlay var sprite = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/progress_bar.rsi"), "icon"); _barTexture = _entManager.EntitySysManager.GetEntitySystem().Frame0(sprite); - _shader = protoManager.Index("unshaded").Instance(); + _unshadedShader = protoManager.Index("unshaded").Instance(); } protected override void Draw(in OverlayDrawArgs args) @@ -58,7 +58,6 @@ public sealed class DoAfterOverlay : Overlay const float scale = 1f; var scaleMatrix = Matrix3.CreateScale(new Vector2(scale, scale)); var rotationMatrix = Matrix3.CreateRotation(-rotation); - handle.UseShader(_shader); var curTime = _timing.CurTime; @@ -79,6 +78,13 @@ public sealed class DoAfterOverlay : Overlay if (!bounds.Contains(worldPosition)) continue; + // shades the do-after bar if the do-after bar belongs to other players + // does not shade do-afters belonging to the local player + if (uid != localEnt) + handle.UseShader(null); + else + handle.UseShader(_unshadedShader); + // If the entity is paused, we will draw the do-after as it was when the entity got paused. var meta = metaQuery.GetComponent(uid); var time = meta.EntityPaused From 829b453e3ca786ae09edb0ad1d23f6a4f642b214 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 24 Apr 2024 02:43:41 +0000 Subject: [PATCH 40/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 37b7aa84dc..31587e2074 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: icekot8 - changes: - - message: Medical berets added to MediDrobe - type: Add - id: 5930 - time: '2024-02-13T21:37:23.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25176 - author: PoorMansDreams changes: - message: Alternate ammo now has the proper appearance when spent. @@ -3851,3 +3844,11 @@ id: 6429 time: '2024-04-23T12:07:12.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27255 +- author: Plykiya + changes: + - message: Do-after bars of other players are now shaded and harder to see in the + dark. + type: Tweak + id: 6430 + time: '2024-04-24T02:42:34.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27273 From 49f1c8b197687024d572e61807050fea42518b35 Mon Sep 17 00:00:00 2001 From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Date: Wed, 24 Apr 2024 01:35:16 -0400 Subject: [PATCH 41/61] fix analysis console not saving bias button state (#27275) --- Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs index 2acf35da25..ecbb8e9662 100644 --- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs +++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs @@ -73,6 +73,7 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow { ScanButton.Disabled = !state.CanScan; PrintButton.Disabled = !state.CanPrint; + DownBiasButton.Pressed = state.IsTraversalDown; var disabled = !state.ServerConnected || !state.CanScan || state.PointAmount <= 0; From 91aa16f08a642ae463bcd875393dd47f1657813d Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Wed, 24 Apr 2024 17:38:43 +1200 Subject: [PATCH 42/61] Add NukeOps Test (#27207) * Add NukeOps Test * Update EvacShuttleTest to also check mapinit * Update RuleMaxTimeRestartTest * Fix cvar cleanup * A * Revert some changes * comments * Add MappingTests * Finally fix the test * A --- .../Pair/TestPair.Timing.cs | 19 +- Content.IntegrationTests/PoolManager.Cvars.cs | 1 + .../Tests/GameRules/NukeOpsTest.cs | 187 ++++++++++++++++++ .../Tests/GameRules/RuleMaxTimeRestartTest.cs | 9 + .../Interaction/InteractionTest.Helpers.cs | 7 +- .../Tests/Interaction/InteractionTest.cs | 1 - .../Tests/Mapping/MappingTests.cs | 102 ++++++++++ .../Tests/Station/EvacShuttleTest.cs | 34 ++-- .../Systems/CriminalRecordsConsoleSystem.cs | 2 + .../GameTicking/GameTicker.GamePreset.cs | 2 +- .../GameTicking/GameTicker.RoundFlow.cs | 2 +- .../Rules/Components/NukeopsRuleComponent.cs | 11 +- .../Rules/MaxTimeRestartRuleSystem.cs | 2 + Content.Server/Mapping/MappingCommand.cs | 2 +- .../Systems/StationRecordsSystem.cs | 2 +- Content.Shared/Roles/SharedRoleSystem.cs | 3 + 16 files changed, 355 insertions(+), 31 deletions(-) create mode 100644 Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs create mode 100644 Content.IntegrationTests/Tests/Mapping/MappingTests.cs diff --git a/Content.IntegrationTests/Pair/TestPair.Timing.cs b/Content.IntegrationTests/Pair/TestPair.Timing.cs index 3487ea6801..e0859660d4 100644 --- a/Content.IntegrationTests/Pair/TestPair.Timing.cs +++ b/Content.IntegrationTests/Pair/TestPair.Timing.cs @@ -1,5 +1,4 @@ #nullable enable -using Robust.Shared.Timing; namespace Content.IntegrationTests.Pair; @@ -19,6 +18,22 @@ public sealed partial class TestPair } } + /// + /// Convert a time interval to some number of ticks. + /// + public int SecondsToTicks(float seconds) + { + return (int) Math.Ceiling(seconds / Server.Timing.TickPeriod.TotalSeconds); + } + + /// + /// Run the server & client in sync for some amount of time + /// + public async Task RunSeconds(float seconds) + { + await RunTicksSync(SecondsToTicks(seconds)); + } + /// /// Runs the server-client pair in sync, but also ensures they are both idle each tick. /// @@ -59,4 +74,4 @@ public sealed partial class TestPair delta = cTick - sTick; Assert.That(delta, Is.EqualTo(targetDelta)); } -} \ No newline at end of file +} diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs index 327ec627f5..d39c7284d0 100644 --- a/Content.IntegrationTests/PoolManager.Cvars.cs +++ b/Content.IntegrationTests/PoolManager.Cvars.cs @@ -32,6 +32,7 @@ public static partial class PoolManager (CCVars.GameLobbyEnabled.Name, "false"), (CCVars.ConfigPresetDevelopment.Name, "false"), (CCVars.AdminLogsEnabled.Name, "false"), + (CCVars.AutosaveEnabled.Name, "false"), (CVars.NetBufferSize.Name, "0") }; diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs new file mode 100644 index 0000000000..5833db0a10 --- /dev/null +++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs @@ -0,0 +1,187 @@ +#nullable enable +using System.Linq; +using Content.Server.Body.Components; +using Content.Server.GameTicking; +using Content.Server.GameTicking.Presets; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Mind; +using Content.Server.Pinpointer; +using Content.Server.Roles; +using Content.Server.Shuttles.Components; +using Content.Server.Station.Components; +using Content.Shared.CCVar; +using Content.Shared.Damage; +using Content.Shared.FixedPoint; +using Content.Shared.GameTicking; +using Content.Shared.Hands.Components; +using Content.Shared.Inventory; +using Content.Shared.NPC.Systems; +using Content.Shared.NukeOps; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Map.Components; + +namespace Content.IntegrationTests.Tests.GameRules; + +[TestFixture] +public sealed class NukeOpsTest +{ + /// + /// Check that a nuke ops game mode can start without issue. I.e., that the nuke station and such all get loaded. + /// + [Test] + public async Task TryStopNukeOpsFromConstantlyFailing() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Dirty = true, + DummyTicker = false, + Connected = true, + InLobby = true + }); + + var server = pair.Server; + var client = pair.Client; + var entMan = server.EntMan; + var mapSys = server.System(); + var ticker = server.System(); + var mindSys = server.System(); + var roleSys = server.System(); + var invSys = server.System(); + var factionSys = server.System(); + + Assert.That(server.CfgMan.GetCVar(CCVars.GridFill), Is.False); + server.CfgMan.SetCVar(CCVars.GridFill, true); + + // Initially in the lobby + Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); + Assert.That(client.AttachedEntity, Is.Null); + Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay)); + + // There are no grids or maps + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + + // And no nukie related components + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + + // Ready up and start nukeops + await pair.WaitClientCommand("toggleready True"); + Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.ReadyToPlay)); + await pair.WaitCommand("forcepreset Nukeops"); + await pair.RunTicksSync(10); + + // Game should have started + Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound)); + Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.JoinedGame)); + Assert.That(client.EntMan.EntityExists(client.AttachedEntity)); + var player = pair.Player!.AttachedEntity!.Value; + Assert.That(entMan.EntityExists(player)); + + // Maps now exist + Assert.That(entMan.Count(), Is.GreaterThan(0)); + Assert.That(entMan.Count(), Is.GreaterThan(0)); + Assert.That(entMan.Count(), Is.EqualTo(2)); // The main station & nukie station + Assert.That(entMan.Count(), Is.GreaterThan(3)); // Each station has at least 1 grid, plus some shuttles + Assert.That(entMan.Count(), Is.EqualTo(1)); + + // And we now have nukie related components + Assert.That(entMan.Count(), Is.EqualTo(1)); + Assert.That(entMan.Count(), Is.EqualTo(1)); + Assert.That(entMan.Count(), Is.EqualTo(1)); + Assert.That(entMan.Count(), Is.EqualTo(1)); + + // The player entity should be the nukie commander + var mind = mindSys.GetMind(player)!.Value; + Assert.That(entMan.HasComponent(player)); + Assert.That(roleSys.MindIsAntagonist(mind)); + Assert.That(roleSys.MindHasRole(mind)); + Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True); + Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False); + + var roles = roleSys.MindGetAllRoles(mind); + var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander" && x.Component is NukeopsRoleComponent); + Assert.That(cmdRoles.Count(), Is.EqualTo(1)); + + // The game rule exists, and all the stations/shuttles/maps are properly initialized + var rule = entMan.AllComponents().Single().Component; + Assert.That(entMan.EntityExists(rule.NukieOutpost)); + Assert.That(entMan.EntityExists(rule.NukieShuttle)); + Assert.That(entMan.EntityExists(rule.TargetStation)); + + Assert.That(entMan.HasComponent(rule.NukieOutpost)); + Assert.That(entMan.HasComponent(rule.NukieShuttle)); + + Assert.That(entMan.HasComponent(rule.NukieOutpost)); + Assert.That(entMan.HasComponent(rule.TargetStation)); + + var nukieStation = entMan.GetComponent(rule.NukieOutpost!.Value); + Assert.That(entMan.EntityExists(nukieStation.Station)); + Assert.That(nukieStation.Station, Is.Not.EqualTo(rule.TargetStation)); + + Assert.That(server.MapMan.MapExists(rule.NukiePlanet)); + var nukieMap = mapSys.GetMap(rule.NukiePlanet!.Value); + + var targetStation = entMan.GetComponent(rule.TargetStation!.Value); + var targetGrid = targetStation.Grids.First(); + var targetMap = entMan.GetComponent(targetGrid).MapUid!.Value; + Assert.That(targetMap, Is.Not.EqualTo(nukieMap)); + + Assert.That(entMan.GetComponent(player).MapUid, Is.EqualTo(nukieMap)); + Assert.That(entMan.GetComponent(rule.NukieOutpost!.Value).MapUid, Is.EqualTo(nukieMap)); + Assert.That(entMan.GetComponent(rule.NukieShuttle!.Value).MapUid, Is.EqualTo(nukieMap)); + + // The maps are all map-initialized, including the player + // Yes, this is necessary as this has repeatedly been broken somehow. + Assert.That(mapSys.IsInitialized(nukieMap)); + Assert.That(mapSys.IsInitialized(targetMap)); + Assert.That(mapSys.IsPaused(nukieMap), Is.False); + Assert.That(mapSys.IsPaused(targetMap), Is.False); + + EntityLifeStage LifeStage(EntityUid? uid) => entMan.GetComponent(uid!.Value).EntityLifeStage; + Assert.That(LifeStage(player), Is.GreaterThan(EntityLifeStage.Initialized)); + Assert.That(LifeStage(nukieMap), Is.GreaterThan(EntityLifeStage.Initialized)); + Assert.That(LifeStage(targetMap), Is.GreaterThan(EntityLifeStage.Initialized)); + Assert.That(LifeStage(rule.NukieOutpost), Is.GreaterThan(EntityLifeStage.Initialized)); + Assert.That(LifeStage(rule.NukieShuttle), Is.GreaterThan(EntityLifeStage.Initialized)); + Assert.That(LifeStage(rule.TargetStation), Is.GreaterThan(EntityLifeStage.Initialized)); + + // Make sure the player has hands. We've had fucking disarmed nukies before. + Assert.That(entMan.HasComponent(player)); + Assert.That(entMan.GetComponent(player).Hands.Count, Is.GreaterThan(0)); + + // While we're at it, lets make sure they aren't naked. I don't know how many inventory slots all mobs will be + // likely to have in the future. But nukies should probably have at least 3 slots with something in them. + var enumerator = invSys.GetSlotEnumerator(player); + int total = 0; + while (enumerator.NextItem(out _)) + { + total++; + } + Assert.That(total, Is.GreaterThan(3)); + + // Finally lets check the nukie commander passed basic training and figured out how to breathe. + var totalSeconds = 30; + var totalTicks = (int) Math.Ceiling(totalSeconds / server.Timing.TickPeriod.TotalSeconds); + int increment = 5; + var resp = entMan.GetComponent(player); + var damage = entMan.GetComponent(player); + for (var tick = 0; tick < totalTicks; tick += increment) + { + await pair.RunTicksSync(increment); + Assert.That(resp.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold)); + Assert.That(damage.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); + } + + ticker.SetGamePreset((GamePresetPrototype?)null); + server.CfgMan.SetCVar(CCVars.GridFill, false); + await pair.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs index 1e3f9c9854..0707bd64c6 100644 --- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs @@ -19,6 +19,9 @@ namespace Content.IntegrationTests.Tests.GameRules await using var pair = await PoolManager.GetServerClient(new PoolSettings { InLobby = true }); var server = pair.Server; + Assert.That(server.EntMan.Count(), Is.Zero); + Assert.That(server.EntMan.Count(), Is.Zero); + var entityManager = server.ResolveDependency(); var sGameTicker = server.ResolveDependency().GetEntitySystem(); var sGameTiming = server.ResolveDependency(); @@ -26,6 +29,9 @@ namespace Content.IntegrationTests.Tests.GameRules sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity); Assert.That(entityManager.TryGetComponent(ruleEntity, out var maxTime)); + Assert.That(server.EntMan.Count(), Is.EqualTo(1)); + Assert.That(server.EntMan.Count(), Is.EqualTo(1)); + await server.WaitAssertion(() => { Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); @@ -33,6 +39,9 @@ namespace Content.IntegrationTests.Tests.GameRules sGameTicker.StartRound(); }); + Assert.That(server.EntMan.Count(), Is.EqualTo(1)); + Assert.That(server.EntMan.Count(), Is.EqualTo(1)); + await server.WaitAssertion(() => { Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.InRound)); diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index 480fd9cde6..d45290c866 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -767,14 +767,9 @@ public abstract partial class InteractionTest await Pair.RunTicksSync(ticks); } - protected int SecondsToTicks(float seconds) - { - return (int) Math.Ceiling(seconds / TickPeriod); - } - protected async Task RunSeconds(float seconds) { - await RunTicks(SecondsToTicks(seconds)); + await Pair.RunSeconds(seconds); } #endregion diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index a4ed31e998..42f64b344c 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -12,7 +12,6 @@ using Content.Shared.Body.Part; using Content.Shared.DoAfter; using Content.Shared.Hands.Components; using Content.Shared.Interaction; -using Content.Server.Item; using Content.Shared.Mind; using Content.Shared.Players; using Robust.Client.Input; diff --git a/Content.IntegrationTests/Tests/Mapping/MappingTests.cs b/Content.IntegrationTests/Tests/Mapping/MappingTests.cs new file mode 100644 index 0000000000..287e30eb8b --- /dev/null +++ b/Content.IntegrationTests/Tests/Mapping/MappingTests.cs @@ -0,0 +1,102 @@ +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; + +namespace Content.IntegrationTests.Tests.Mapping; + +[TestFixture] +public sealed class MappingTests +{ + /// + /// Checks that the mapping command creates paused & uninitialized maps. + /// + [Test] + public async Task MappingTest() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings {Dirty = true, Connected = true, DummyTicker = false}); + + var server = pair.Server; + var entMan = server.EntMan; + var mapSys = server.System(); + + await pair.RunTicksSync(5); + var mapId = 1; + while (mapSys.MapExists(new(mapId))) + { + mapId++; + } + + await pair.WaitClientCommand($"mapping {mapId}"); + var map = mapSys.GetMap(new MapId(mapId)); + + var mapXform = server.Transform(map); + Assert.That(mapXform.MapUid, Is.EqualTo(map)); + Assert.That(mapXform.MapID, Is.EqualTo(new MapId(mapId))); + + var xform = server.Transform(pair.Player!.AttachedEntity!.Value); + + Assert.That(xform.MapUid, Is.EqualTo(map)); + Assert.That(mapSys.IsInitialized(map), Is.False); + Assert.That(mapSys.IsPaused(map), Is.True); + Assert.That(server.MetaData(map).EntityLifeStage, Is.EqualTo(EntityLifeStage.Initialized)); + Assert.That(server.MetaData(map).EntityPaused, Is.True); + + // Spawn a new entity + EntityUid ent = default; + await server.WaitPost(() => + { + ent = entMan.Spawn(null, new MapCoordinates(default, new(mapId))); + }); + await pair.RunTicksSync(5); + Assert.That(server.MetaData(ent).EntityLifeStage, Is.EqualTo(EntityLifeStage.Initialized)); + Assert.That(server.MetaData(ent).EntityPaused, Is.True); + + // Save the map + var file = $"{nameof(MappingTest)}.yml"; + await pair.WaitClientCommand($"savemap {mapId} {file}"); + + // Mapinitialize it + await pair.WaitClientCommand($"mapinit {mapId}"); + Assert.That(mapSys.IsInitialized(map), Is.True); + Assert.That(mapSys.IsPaused(map), Is.False); + Assert.That(server.MetaData(map).EntityLifeStage, Is.EqualTo(EntityLifeStage.MapInitialized)); + Assert.That(server.MetaData(map).EntityPaused, Is.False); + Assert.That(server.MetaData(ent).EntityLifeStage, Is.EqualTo(EntityLifeStage.MapInitialized)); + Assert.That(server.MetaData(ent).EntityPaused, Is.False); + + await server.WaitPost(() => entMan.DeleteEntity(map)); + + // Load the saved map + mapId++; + while (mapSys.MapExists(new(mapId))) + { + mapId++; + } + + await pair.WaitClientCommand($"mapping {mapId} {file}"); + map = mapSys.GetMap(new MapId(mapId)); + + // And it should all be paused and un-initialized + xform = server.Transform(pair.Player!.AttachedEntity!.Value); + Assert.That(xform.MapUid, Is.EqualTo(map)); + Assert.That(mapSys.IsInitialized(map), Is.False); + Assert.That(mapSys.IsPaused(map), Is.True); + Assert.That(server.MetaData(map).EntityLifeStage, Is.EqualTo(EntityLifeStage.Initialized)); + Assert.That(server.MetaData(map).EntityPaused, Is.True); + + mapXform = server.Transform(map); + Assert.That(mapXform.MapUid, Is.EqualTo(map)); + Assert.That(mapXform.MapID, Is.EqualTo(new MapId(mapId))); + Assert.That(mapXform.ChildCount, Is.EqualTo(2)); + + mapXform.ChildEnumerator.MoveNext(out ent); + if (ent == pair.Player.AttachedEntity) + mapXform.ChildEnumerator.MoveNext(out ent); + + Assert.That(server.MetaData(ent).EntityLifeStage, Is.EqualTo(EntityLifeStage.Initialized)); + Assert.That(server.MetaData(ent).EntityPaused, Is.True); + + await server.WaitPost(() => entMan.DeleteEntity(map)); + await pair.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs index 5750be09c2..532e481ac2 100644 --- a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs +++ b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs @@ -28,10 +28,11 @@ public sealed class EvacShuttleTest // Dummy ticker tests should not have centcomm Assert.That(entMan.Count(), Is.Zero); - var shuttleEnabled = pair.Server.CfgMan.GetCVar(CCVars.EmergencyShuttleEnabled); - pair.Server.CfgMan.SetCVar(CCVars.GameMap, "Saltern"); - pair.Server.CfgMan.SetCVar(CCVars.GameDummyTicker, false); + Assert.That(pair.Server.CfgMan.GetCVar(CCVars.GridFill), Is.False); pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, true); + pair.Server.CfgMan.SetCVar(CCVars.GameDummyTicker, false); + var gameMap = pair.Server.CfgMan.GetCVar(CCVars.GameMap); + pair.Server.CfgMan.SetCVar(CCVars.GameMap, "Saltern"); await server.WaitPost(() => ticker.RestartRound()); await pair.RunTicksSync(25); @@ -71,6 +72,20 @@ public sealed class EvacShuttleTest Assert.That(shuttleXform.MapUid, Is.Not.Null); Assert.That(shuttleXform.MapUid, Is.EqualTo(centcommMap)); + // All of these should have been map-initialized. + var mapSys = entMan.System(); + Assert.That(mapSys.IsInitialized(centcommMap), Is.True); + Assert.That(mapSys.IsInitialized(salternXform.MapUid), Is.True); + Assert.That(mapSys.IsPaused(centcommMap), Is.False); + Assert.That(mapSys.IsPaused(salternXform.MapUid!.Value), Is.False); + + EntityLifeStage LifeStage(EntityUid uid) => entMan.GetComponent(uid).EntityLifeStage; + Assert.That(LifeStage(saltern), Is.EqualTo(EntityLifeStage.MapInitialized)); + Assert.That(LifeStage(shuttle), Is.EqualTo(EntityLifeStage.MapInitialized)); + Assert.That(LifeStage(centcomm), Is.EqualTo(EntityLifeStage.MapInitialized)); + Assert.That(LifeStage(centcommMap), Is.EqualTo(EntityLifeStage.MapInitialized)); + Assert.That(LifeStage(salternXform.MapUid.Value), Is.EqualTo(EntityLifeStage.MapInitialized)); + // Set up shuttle timing var evacSys = server.System(); evacSys.TransitTime = ShuttleSystem.DefaultTravelTime; // Absolute minimum transit time, so the test has to run for at least this long @@ -78,19 +93,15 @@ public sealed class EvacShuttleTest var dockTime = server.CfgMan.GetCVar(CCVars.EmergencyShuttleDockTime); server.CfgMan.SetCVar(CCVars.EmergencyShuttleDockTime, 2); - async Task RunSeconds(float seconds) - { - await pair.RunTicksSync((int) Math.Ceiling(seconds / server.Timing.TickPeriod.TotalSeconds)); - } // Call evac shuttle. await pair.WaitCommand("callshuttle 0:02"); - await RunSeconds(3); + await pair.RunSeconds(3); // Shuttle should have arrived on the station Assert.That(shuttleXform.MapUid, Is.EqualTo(salternXform.MapUid)); - await RunSeconds(2); + await pair.RunSeconds(2); // Shuttle should be FTLing back to centcomm Assert.That(entMan.Count(), Is.EqualTo(1)); @@ -101,14 +112,15 @@ public sealed class EvacShuttleTest Assert.That(shuttleXform.MapUid, Is.EqualTo(ftl.Owner)); // Shuttle should have arrived at centcomm - await RunSeconds(ShuttleSystem.DefaultTravelTime); + await pair.RunSeconds(ShuttleSystem.DefaultTravelTime); Assert.That(shuttleXform.MapUid, Is.EqualTo(centcommMap)); // Round should be ending now Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PostRound)); server.CfgMan.SetCVar(CCVars.EmergencyShuttleDockTime, dockTime); - pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, shuttleEnabled); + pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, false); + pair.Server.CfgMan.SetCVar(CCVars.GameMap, gameMap); await pair.CleanReturnAsync(); } } diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs index 4290726cc4..fe53ea268c 100644 --- a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs +++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs @@ -253,6 +253,8 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS { var name = Identity.Name(uid, EntityManager); var xform = Transform(uid); + + // TODO use the entity's station? Not the station of the map that it happens to currently be on? var station = _station.GetStationInMap(xform.MapID); if (station != null && _stationRecords.GetRecordByName(station.Value, name) is { } id) diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs index b97a16ab99..a1946d34a0 100644 --- a/Content.Server/GameTicking/GameTicker.GamePreset.cs +++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs @@ -100,7 +100,7 @@ namespace Content.Server.GameTicking SetGamePreset(LobbyEnabled ? _configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset) : "sandbox"); } - public void SetGamePreset(GamePresetPrototype preset, bool force = false) + public void SetGamePreset(GamePresetPrototype? preset, bool force = false) { // Do nothing if this game ticker is a dummy! if (DummyTicker) diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 202daf256d..792d838169 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -165,7 +165,7 @@ namespace Content.Server.GameTicking var gridIds = _map.LoadMap(targetMapId, ev.GameMap.MapPath.ToString(), ev.Options); - _metaData.SetEntityName(_mapManager.GetMapEntityId(targetMapId), "Station map"); + _metaData.SetEntityName(_mapManager.GetMapEntityId(targetMapId), $"station map - {map.MapName}"); var gridUids = gridIds.ToList(); RaiseLocalEvent(new PostGameMapLoad(map, targetMapId, gridUids, stationName)); diff --git a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs index c66a9d12a1..8f11e70560 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs @@ -6,11 +6,7 @@ using Content.Shared.NPC.Prototypes; using Content.Shared.Roles; using Robust.Shared.Map; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Utility; - namespace Content.Server.GameTicking.Rules.Components; @@ -116,13 +112,14 @@ public sealed partial class NukeopsRuleComponent : Component [DataField] public List WinConditions = new (); - public MapId? NukiePlanet; - + // TODO full game save // TODO: use components, don't just cache entity UIDs // There have been (and probably still are) bugs where these refer to deleted entities from old rounds. + // Whenever this gets fixed, update NukiesTest. public EntityUid? NukieOutpost; public EntityUid? NukieShuttle; public EntityUid? TargetStation; + public MapId? NukiePlanet; /// /// Data to be used in for an operative once the Mind has been added. @@ -131,7 +128,7 @@ public sealed partial class NukeopsRuleComponent : Component public Dictionary OperativeMindPendingData = new(); [DataField(required: true)] - public ProtoId Faction = default!; + public ProtoId Faction; [DataField] public NukeopSpawnPreset CommanderSpawnDetails = new() { AntagRoleProto = "NukeopsCommander", GearProto = "SyndicateCommanderGearFull", NamePrefix = "nukeops-role-commander", NameList = "SyndicateNamesElite" }; diff --git a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs index e792a004df..2522ebb53b 100644 --- a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs @@ -33,6 +33,7 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem TimerFired(component), component.TimerCancel.Token); @@ -49,6 +50,7 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem GameTicker.RestartRound()); } diff --git a/Content.Server/Mapping/MappingCommand.cs b/Content.Server/Mapping/MappingCommand.cs index 08f3dcccf9..46534f7059 100644 --- a/Content.Server/Mapping/MappingCommand.cs +++ b/Content.Server/Mapping/MappingCommand.cs @@ -53,7 +53,7 @@ namespace Content.Server.Mapping } #if DEBUG - shell.WriteError(Loc.GetString("cmd-mapping-warning")); + shell.WriteLine(Loc.GetString("cmd-mapping-warning")); #endif MapId mapId; diff --git a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs index 67f50d7a4e..58c4c876c5 100644 --- a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs +++ b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs @@ -211,7 +211,7 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem /// public uint? GetRecordByName(EntityUid station, string name, StationRecordsComponent? records = null) { - if (!Resolve(station, ref records)) + if (!Resolve(station, ref records, false)) return null; foreach (var (id, record) in GetRecordsOfType(station, records)) diff --git a/Content.Shared/Roles/SharedRoleSystem.cs b/Content.Shared/Roles/SharedRoleSystem.cs index e8053e4c67..c25ac1968d 100644 --- a/Content.Shared/Roles/SharedRoleSystem.cs +++ b/Content.Shared/Roles/SharedRoleSystem.cs @@ -137,11 +137,13 @@ public abstract class SharedRoleSystem : EntitySystem public bool MindHasRole(EntityUid mindId) where T : IComponent { + DebugTools.Assert(HasComp(mindId)); return HasComp(mindId); } public List MindGetAllRoles(EntityUid mindId) { + DebugTools.Assert(HasComp(mindId)); var ev = new MindGetAllRolesEvent(new List()); RaiseLocalEvent(mindId, ref ev); return ev.Roles; @@ -152,6 +154,7 @@ public abstract class SharedRoleSystem : EntitySystem if (mindId == null) return false; + DebugTools.Assert(HasComp(mindId)); var ev = new MindIsAntagonistEvent(); RaiseLocalEvent(mindId.Value, ref ev); return ev.IsAntagonist; From 480d26aba64865f1460c4ca82b8d341d98268670 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Wed, 24 Apr 2024 09:02:43 -0400 Subject: [PATCH 43/61] Fix climbing and bonking simultaneously (#27268) * Properly network ClumsyComponent * Fix being able to climb and bonk at the same time. Also properly subscribe to AttemptClimbEvent by-ref. * Update Content.Shared/Interaction/Components/ClumsyComponent.cs --- Content.Shared/Climbing/Systems/BonkSystem.cs | 11 ++++--- .../Climbing/Systems/ClimbSystem.cs | 3 +- .../Interaction/Components/ClumsyComponent.cs | 30 ++++++++++--------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Content.Shared/Climbing/Systems/BonkSystem.cs b/Content.Shared/Climbing/Systems/BonkSystem.cs index ea4e04c621..f59fe92573 100644 --- a/Content.Shared/Climbing/Systems/BonkSystem.cs +++ b/Content.Shared/Climbing/Systems/BonkSystem.cs @@ -107,17 +107,16 @@ public sealed partial class BonkSystem : EntitySystem var doAfterArgs = new DoAfterArgs(EntityManager, user, bonkableComponent.BonkDelay, new BonkDoAfterEvent(), uid, target: uid, used: climber) { BreakOnMove = true, - BreakOnDamage = true + BreakOnDamage = true, + DuplicateCondition = DuplicateConditions.SameTool | DuplicateConditions.SameTarget }; - _doAfter.TryStartDoAfter(doAfterArgs); - - return true; + return _doAfter.TryStartDoAfter(doAfterArgs); } - private void OnAttemptClimb(EntityUid uid, BonkableComponent component, AttemptClimbEvent args) + private void OnAttemptClimb(EntityUid uid, BonkableComponent component, ref AttemptClimbEvent args) { - if (args.Cancelled || !HasComp(args.Climber) || !HasComp(args.User)) + if (args.Cancelled) return; if (TryStartBonk(uid, args.User, args.Climber, component)) diff --git a/Content.Shared/Climbing/Systems/ClimbSystem.cs b/Content.Shared/Climbing/Systems/ClimbSystem.cs index 1bdaecf730..ac01c4e9ac 100644 --- a/Content.Shared/Climbing/Systems/ClimbSystem.cs +++ b/Content.Shared/Climbing/Systems/ClimbSystem.cs @@ -222,7 +222,8 @@ public sealed partial class ClimbSystem : VirtualController used: entityToMove) { BreakOnMove = true, - BreakOnDamage = true + BreakOnDamage = true, + DuplicateCondition = DuplicateConditions.SameTool | DuplicateConditions.SameTarget }; _audio.PlayPredicted(comp.StartClimbSound, climbable, user); diff --git a/Content.Shared/Interaction/Components/ClumsyComponent.cs b/Content.Shared/Interaction/Components/ClumsyComponent.cs index 5b72fc224c..824696c838 100644 --- a/Content.Shared/Interaction/Components/ClumsyComponent.cs +++ b/Content.Shared/Interaction/Components/ClumsyComponent.cs @@ -1,22 +1,24 @@ using Content.Shared.Damage; using Robust.Shared.Audio; +using Robust.Shared.GameStates; -namespace Content.Shared.Interaction.Components +namespace Content.Shared.Interaction.Components; + +/// +/// A simple clumsy tag-component. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ClumsyComponent : Component { /// - /// A simple clumsy tag-component. + /// Damage dealt to a clumsy character when they try to fire a gun. /// - [RegisterComponent] - public sealed partial class ClumsyComponent : Component - { - [DataField("clumsyDamage", required: true)] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier ClumsyDamage = default!; + [DataField(required: true), AutoNetworkedField] + public DamageSpecifier ClumsyDamage = default!; - /// - /// Sound to play when clumsy interactions fail - /// - [DataField("clumsySound")] - public SoundSpecifier ClumsySound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg"); - } + /// + /// Sound to play when clumsy interactions fail. + /// + [DataField] + public SoundSpecifier ClumsySound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg"); } From 4290d5f13e16f01fd6a33af897665e0ba7795b3b Mon Sep 17 00:00:00 2001 From: Alzore <140123969+Blackern5000@users.noreply.github.com> Date: Wed, 24 Apr 2024 08:21:28 -0500 Subject: [PATCH 44/61] Make the cargo telepad T2 civilian (#26270) therearetwowaystodothis --- Resources/Prototypes/Research/civilianservices.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Resources/Prototypes/Research/civilianservices.yml b/Resources/Prototypes/Research/civilianservices.yml index afb1f0ff50..b990eb6ae4 100644 --- a/Resources/Prototypes/Research/civilianservices.yml +++ b/Resources/Prototypes/Research/civilianservices.yml @@ -191,8 +191,6 @@ - WeaponSprayNozzle - ClothingBackpackWaterTank -# Tier 3 - - type: technology id: BluespaceCargoTransport name: research-technology-bluespace-cargo-transport @@ -200,11 +198,13 @@ sprite: Structures/cargo_telepad.rsi state: display discipline: CivilianServices - tier: 3 + tier: 2 cost: 15000 recipeUnlocks: - CargoTelepadMachineCircuitboard +# Tier 3 + - type: technology id: QuantumFiberWeaving name: research-technology-quantum-fiber-weaving From 7f6f9c12093fde119aed6cc458301b5b02ddc139 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 24 Apr 2024 13:22:36 +0000 Subject: [PATCH 45/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 31587e2074..c4053e1b32 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: PoorMansDreams - changes: - - message: Alternate ammo now has the proper appearance when spent. - type: Fix - id: 5931 - time: '2024-02-13T21:40:15.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25167 - author: Agoichi changes: - message: All hoods have tag "Hides Hair" @@ -3852,3 +3845,10 @@ id: 6430 time: '2024-04-24T02:42:34.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27273 +- author: Blackern5000 + changes: + - message: The cargo telepad is now tier 2 technology rather than tier 3. + type: Tweak + id: 6431 + time: '2024-04-24T13:21:29.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26270 From 5fa424a43b9a04c4dc6839a9e5244f80d8e576e7 Mon Sep 17 00:00:00 2001 From: FungiFellow <151778459+FungiFellow@users.noreply.github.com> Date: Wed, 24 Apr 2024 08:43:56 -0500 Subject: [PATCH 46/61] Add Truncheon to Secbelt Whitelist (#27281) * Add Truncheon to Secbelt Whitelist * Update belts.yml * Added Truncehon Tag in Tags.yml * Update security.yml * Update security.yml --- Resources/Prototypes/Entities/Clothing/Belt/belts.yml | 1 + Resources/Prototypes/Entities/Objects/Weapons/security.yml | 3 +++ Resources/Prototypes/tags.yml | 3 +++ 3 files changed, 7 insertions(+) diff --git a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml index 0b0d9e018a..1bae86a022 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml @@ -473,6 +473,7 @@ - MagazinePistol - MagazineMagnum - CombatKnife + - Truncheon components: - Stunbaton - FlashOnTrigger diff --git a/Resources/Prototypes/Entities/Objects/Weapons/security.yml b/Resources/Prototypes/Entities/Objects/Weapons/security.yml index b9d409fb3d..101314a1fb 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/security.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/security.yml @@ -102,6 +102,9 @@ bluntStaminaDamageFactor: 1.5 - type: Item size: Normal + - type: Tag + tags: + - Truncheon - type: Clothing sprite: Objects/Weapons/Melee/truncheon.rsi quickEquip: false diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index f6cedeb937..91b79f3d8e 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -1235,6 +1235,9 @@ - type: Tag id: TrashBag +- type: Tag + id: Truncheon + - type: Tag id: Unimplantable From 38f490e5ebd3fe945464eafaa20f6fe7a8328920 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 24 Apr 2024 13:45:03 +0000 Subject: [PATCH 47/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index c4053e1b32..c1bdcacff3 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,13 +1,4 @@ Entries: -- author: Agoichi - changes: - - message: All hoods have tag "Hides Hair" - type: Tweak - - message: Plague Doctor's hat and Witch hat (with red hair) have tag "Hides Hair" - type: Tweak - id: 5932 - time: '2024-02-13T21:43:19.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25142 - author: Blackern5000 changes: - message: Advanced topical meds now cost significantly less chemicals @@ -3852,3 +3843,10 @@ id: 6431 time: '2024-04-24T13:21:29.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26270 +- author: FungiFellow + changes: + - message: Truncheon now fits in SecBelt + type: Tweak + id: 6432 + time: '2024-04-24T13:43:56.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27281 From 7b90c08a2c7315b2c5a6cb17dfd82a5ac9863a53 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Wed, 24 Apr 2024 16:01:31 +0200 Subject: [PATCH 48/61] Various item status fixes/tweaks (#27267) * Always display item status panel fully Initial feedback from the UI changes seems to be that a lot of people go "why is there empty space" so let's fix that. * Fix item status middle hand being on the wrong side I think I switched this around when fixing the left/right being inverted in the UI code. * Minor status panel UI tweaks Bottom-align contents now that the panel itself doesn't dynamically expand, prevent weird gaps. Clip contents for panel * Fix clipping on implanters and network configurators. Made them take less space. For implanters the name has to be cut off, which I did by adding a new ClipControl to achieve that in rich text. * Update visibility of item status panels based on whether you have hands at all. This avoids UI for borgs looking silly. Added a new "HandUILocation" enum that doesn't have middle hands to avoid confusion in UI code. * Use BulletRender for laser guns too. Provides all the benefits like fixing layout overflow and allowing multi-line stuff. Looks great now. This involved generalizing BulletRender a bit so it can be used for not-just-bullets. * Fix geiger word wrapping if you're really fucked --- .../Implants/UI/ImplanterStatusControl.cs | 16 +- Content.Client/Stylesheets/StyleNano.cs | 7 + .../UserInterface/Controls/ClipControl.cs | 55 +++++ .../Systems/Hands/Controls/HandButton.cs | 3 + .../Systems/Hands/HandsUIController.cs | 30 ++- .../Systems/Hotbar/Widgets/HotbarGui.xaml | 4 +- .../Systems/Hotbar/Widgets/HotbarGui.xaml.cs | 16 +- .../Inventory/Controls/ItemStatusPanel.xaml | 43 ++-- .../Controls/ItemStatusPanel.xaml.cs | 22 +- .../Weapons/Ranged/ItemStatus/BulletRender.cs | 222 ++++++++++++------ .../Ranged/Systems/GunSystem.AmmoCounter.cs | 59 +---- .../Hands/Components/HandsComponent.cs | 34 +++ .../en-US/devices/network-configurator.ftl | 4 +- Resources/Locale/en-US/implant/implant.ftl | 5 +- .../Locale/en-US/inventory/item-status.ftl | 1 + .../en-US/radiation/geiger-component.ftl | 2 +- 16 files changed, 338 insertions(+), 185 deletions(-) create mode 100644 Content.Client/UserInterface/Controls/ClipControl.cs create mode 100644 Resources/Locale/en-US/inventory/item-status.ftl diff --git a/Content.Client/Implants/UI/ImplanterStatusControl.cs b/Content.Client/Implants/UI/ImplanterStatusControl.cs index f3f0cdea7d..e2ffabd17d 100644 --- a/Content.Client/Implants/UI/ImplanterStatusControl.cs +++ b/Content.Client/Implants/UI/ImplanterStatusControl.cs @@ -1,5 +1,6 @@ using Content.Client.Message; using Content.Client.Stylesheets; +using Content.Client.UserInterface.Controls; using Content.Shared.Implants.Components; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; @@ -17,7 +18,7 @@ public sealed class ImplanterStatusControl : Control _parent = parent; _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; _label.MaxWidth = 350; - AddChild(_label); + AddChild(new ClipControl { Children = { _label } }); Update(); } @@ -42,17 +43,12 @@ public sealed class ImplanterStatusControl : Control _ => Loc.GetString("injector-invalid-injector-toggle-mode") }; - var (implantName, implantDescription) = _parent.ImplanterSlot.HasItem switch - { - false => (Loc.GetString("implanter-empty-text"), ""), - true => (_parent.ImplantData.Item1, _parent.ImplantData.Item2), - }; - + var implantName = _parent.ImplanterSlot.HasItem + ? _parent.ImplantData.Item1 + : Loc.GetString("implanter-empty-text"); _label.SetMarkup(Loc.GetString("implanter-label", ("implantName", implantName), - ("implantDescription", implantDescription), - ("modeString", modeStringLocalized), - ("lineBreak", "\n"))); + ("modeString", modeStringLocalized))); } } diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index 5fc17447c3..8707d70766 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -136,6 +136,8 @@ namespace Content.Client.Stylesheets public const string StyleClassPowerStateGood = "PowerStateGood"; public const string StyleClassItemStatus = "ItemStatus"; + public const string StyleClassItemStatusNotHeld = "ItemStatusNotHeld"; + public static readonly Color ItemStatusNotHeldColor = Color.Gray; //Background public const string StyleClassBackgroundBaseDark = "PanelBackgroundBaseDark"; @@ -1234,6 +1236,11 @@ namespace Content.Client.Stylesheets new StyleProperty("font", notoSans10), }), + Element() + .Class(StyleClassItemStatusNotHeld) + .Prop("font", notoSansItalic10) + .Prop("font-color", ItemStatusNotHeldColor), + Element() .Class(StyleClassItemStatus) .Prop(nameof(RichTextLabel.LineHeightScale), 0.7f) diff --git a/Content.Client/UserInterface/Controls/ClipControl.cs b/Content.Client/UserInterface/Controls/ClipControl.cs new file mode 100644 index 0000000000..1fca3c0f47 --- /dev/null +++ b/Content.Client/UserInterface/Controls/ClipControl.cs @@ -0,0 +1,55 @@ +using System.Numerics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client.UserInterface.Controls; + +/// +/// Pretends to child controls that there's infinite space. +/// This can be used to make something like a clip instead of wrapping. +/// +public sealed class ClipControl : Control +{ + private bool _clipHorizontal = true; + private bool _clipVertical = true; + + public bool ClipHorizontal + { + get => _clipHorizontal; + set + { + _clipHorizontal = value; + InvalidateMeasure(); + } + } + + public bool ClipVertical + { + get => _clipVertical; + set + { + _clipVertical = value; + InvalidateMeasure(); + } + } + + protected override Vector2 MeasureOverride(Vector2 availableSize) + { + if (ClipHorizontal) + availableSize = availableSize with { X = float.PositiveInfinity }; + if (ClipVertical) + availableSize = availableSize with { Y = float.PositiveInfinity }; + + return base.MeasureOverride(availableSize); + } + + protected override Vector2 ArrangeOverride(Vector2 finalSize) + { + foreach (var child in Children) + { + child.Arrange(UIBox2.FromDimensions(Vector2.Zero, child.DesiredSize)); + } + + return finalSize; + } +} diff --git a/Content.Client/UserInterface/Systems/Hands/Controls/HandButton.cs b/Content.Client/UserInterface/Systems/Hands/Controls/HandButton.cs index 574e0c4707..d5794f7195 100644 --- a/Content.Client/UserInterface/Systems/Hands/Controls/HandButton.cs +++ b/Content.Client/UserInterface/Systems/Hands/Controls/HandButton.cs @@ -5,8 +5,11 @@ namespace Content.Client.UserInterface.Systems.Hands.Controls; public sealed class HandButton : SlotControl { + public HandLocation HandLocation { get; } + public HandButton(string handName, HandLocation handLocation) { + HandLocation = handLocation; Name = "hand_" + handName; SlotName = handName; SetBackground(handLocation); diff --git a/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs b/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs index e57c15462e..99d7bc77b8 100644 --- a/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs +++ b/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs @@ -256,7 +256,8 @@ public sealed class HandsUIController : UIController, IOnStateEntered + SetHeight="60"/> + SetHeight="60"/> (); hotbarController.Setup(HandContainer, StoragePanel); @@ -29,9 +29,15 @@ public sealed partial class HotbarGui : UIWidget StatusPanelRight.Update(entity); } - public void SetHighlightHand(HandLocation? hand) + public void SetHighlightHand(HandUILocation? hand) { - StatusPanelLeft.UpdateHighlight(hand is HandLocation.Left); - StatusPanelRight.UpdateHighlight(hand is HandLocation.Middle or HandLocation.Right); + StatusPanelLeft.UpdateHighlight(hand is HandUILocation.Left); + StatusPanelRight.UpdateHighlight(hand is HandUILocation.Right); + } + + public void UpdateStatusVisibility(bool left, bool right) + { + StatusPanelLeft.Visible = left; + StatusPanelRight.Visible = right; } } diff --git a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml index 81142d64d2..3b1257b44c 100644 --- a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml +++ b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml @@ -4,25 +4,26 @@ xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client" VerticalAlignment="Bottom" HorizontalAlignment="Center"> - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs index e1fe6ab246..95951fa1b0 100644 --- a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs +++ b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs @@ -1,17 +1,13 @@ -using System.Numerics; using Content.Client.Items; -using Content.Client.Resources; using Content.Shared.Hands.Components; using Content.Shared.IdentityManagement; using Content.Shared.Inventory.VirtualItem; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Timing; using Robust.Shared.Utility; -using static Content.Client.IoC.StaticIoC; namespace Content.Client.UserInterface.Systems.Inventory.Controls; @@ -23,17 +19,15 @@ public sealed partial class ItemStatusPanel : Control [ViewVariables] private EntityUid? _entity; // Tracked so we can re-run SetSide() if the theme changes. - private HandLocation _side; + private HandUILocation _side; public ItemStatusPanel() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - - SetSide(HandLocation.Middle); } - public void SetSide(HandLocation location) + public void SetSide(HandUILocation location) { // AN IMPORTANT REMINDER ABOUT THIS CODE: // In the UI, the RIGHT hand is on the LEFT on the screen. @@ -47,15 +41,14 @@ public sealed partial class ItemStatusPanel : Control switch (location) { - case HandLocation.Right: + case HandUILocation.Right: texture = Theme.ResolveTexture("item_status_right"); textureHighlight = Theme.ResolveTexture("item_status_right_highlight"); cutOut = StyleBox.Margin.Left; flat = StyleBox.Margin.Right; contentMargin = MarginFromThemeColor("_itemstatus_content_margin_right"); break; - case HandLocation.Middle: - case HandLocation.Left: + case HandUILocation.Left: texture = Theme.ResolveTexture("item_status_left"); textureHighlight = Theme.ResolveTexture("item_status_left_highlight"); cutOut = StyleBox.Margin.Right; @@ -104,11 +97,14 @@ public sealed partial class ItemStatusPanel : Control public void Update(EntityUid? entity) { + ItemNameLabel.Visible = entity != null; + NoItemLabel.Visible = entity == null; + if (entity == null) { + ItemNameLabel.Text = ""; ClearOldStatus(); _entity = null; - VisWrapper.Visible = false; return; } @@ -119,8 +115,6 @@ public sealed partial class ItemStatusPanel : Control UpdateItemName(); } - - VisWrapper.Visible = true; } public void UpdateHighlight(bool highlight) diff --git a/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs index 492fad3872..8aea5d7ee6 100644 --- a/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs +++ b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs @@ -6,40 +6,10 @@ using Robust.Client.UserInterface; namespace Content.Client.Weapons.Ranged.ItemStatus; -/// -/// Renders one or more rows of bullets for item status. -/// -/// -/// This is a custom control to allow complex responsive layout logic. -/// -public sealed class BulletRender : Control +public abstract class BaseBulletRenderer : Control { - private static readonly Color ColorA = Color.FromHex("#b68f0e"); - private static readonly Color ColorB = Color.FromHex("#d7df60"); - private static readonly Color ColorGoneA = Color.FromHex("#000000"); - private static readonly Color ColorGoneB = Color.FromHex("#222222"); - - /// - /// Try to ensure there's at least this many bullets on one row. - /// - /// - /// For example, if there are two rows and the second row has only two bullets, - /// we "steal" some bullets from the row below it to make it look nicer. - /// - public const int MinCountPerRow = 7; - - public const int BulletHeight = 12; - public const int BulletSeparationNormal = 3; - public const int BulletSeparationTiny = 2; - public const int BulletWidthNormal = 5; - public const int BulletWidthTiny = 2; - public const int VerticalSeparation = 2; - - private readonly Texture _bulletTiny; - private readonly Texture _bulletNormal; - private int _capacity; - private BulletType _type = BulletType.Normal; + private LayoutParameters _params; public int Rows { get; set; } = 2; public int Count { get; set; } @@ -49,35 +19,31 @@ public sealed class BulletRender : Control get => _capacity; set { + if (_capacity == value) + return; + _capacity = value; InvalidateMeasure(); } } - public BulletType Type + protected LayoutParameters Parameters { - get => _type; + get => _params; set { - _type = value; + _params = value; InvalidateMeasure(); } } - public BulletRender() - { - var resC = IoCManager.Resolve(); - _bulletTiny = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/tiny.png"); - _bulletNormal = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/normal.png"); - } - protected override Vector2 MeasureOverride(Vector2 availableSize) { var countPerRow = Math.Min(Capacity, CountPerRow(availableSize.X)); var rows = Math.Min((int) MathF.Ceiling(Capacity / (float) countPerRow), Rows); - var height = BulletHeight * rows + (BulletSeparationNormal * rows - 1); + var height = _params.ItemHeight * rows + (_params.VerticalSeparation * rows - 1); var width = RowWidth(countPerRow); return new Vector2(width, height); @@ -91,13 +57,8 @@ public sealed class BulletRender : Control var countPerRow = CountPerRow(Size.X); - var (separation, _) = BulletParams(); - var texture = Type == BulletType.Normal ? _bulletNormal : _bulletTiny; - var pos = new Vector2(); - var altColor = false; - var spent = Capacity - Count; var bulletsDone = 0; @@ -105,7 +66,7 @@ public sealed class BulletRender : Control // Draw by rows, bottom to top. for (var row = 0; row < Rows; row++) { - altColor = false; + var altColor = false; var thisRowCount = Math.Min(countPerRow, Capacity - bulletsDone); if (thisRowCount <= 0) @@ -116,9 +77,10 @@ public sealed class BulletRender : Control // 1. The next row would have less than MinCountPerRow bullets. // 2. The next row is actually visible (we aren't the last row). // 3. MinCountPerRow is actually smaller than the count per row (avoid degenerate cases). + // 4. There's enough bullets that at least one will end up on the next row. var nextRowCount = Capacity - bulletsDone - thisRowCount; - if (nextRowCount < MinCountPerRow && row != Rows - 1 && MinCountPerRow < countPerRow) - thisRowCount -= MinCountPerRow - nextRowCount; + if (nextRowCount < _params.MinCountPerRow && row != Rows - 1 && _params.MinCountPerRow < countPerRow && nextRowCount > 0) + thisRowCount -= _params.MinCountPerRow - nextRowCount; // Account for row width to right-align. var rowWidth = RowWidth(thisRowCount); @@ -128,46 +90,130 @@ public sealed class BulletRender : Control for (var bullet = 0; bullet < thisRowCount; bullet++) { var absIdx = Capacity - bulletsDone - thisRowCount + bullet; - Color color; - if (absIdx >= spent) - color = altColor ? ColorA : ColorB; - else - color = altColor ? ColorGoneA : ColorGoneB; var renderPos = pos; - renderPos.Y = Size.Y - renderPos.Y - BulletHeight; - handle.DrawTexture(texture, renderPos, color); - pos.X += separation; + renderPos.Y = Size.Y - renderPos.Y - _params.ItemHeight; + + DrawItem(handle, renderPos, absIdx < spent, altColor); + + pos.X += _params.ItemSeparation; altColor ^= true; } bulletsDone += thisRowCount; pos.X = 0; - pos.Y += BulletHeight + VerticalSeparation; + pos.Y += _params.ItemHeight + _params.VerticalSeparation; } } + protected abstract void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor); + private int CountPerRow(float width) { - var (separation, bulletWidth) = BulletParams(); - return (int) ((width - bulletWidth + separation) / separation); - } - - private (int separation, int width) BulletParams() - { - return Type switch - { - BulletType.Normal => (BulletSeparationNormal, BulletWidthNormal), - BulletType.Tiny => (BulletSeparationTiny, BulletWidthTiny), - _ => throw new ArgumentOutOfRangeException() - }; + return (int) ((width - _params.ItemWidth + _params.ItemSeparation) / _params.ItemSeparation); } private int RowWidth(int count) { - var (separation, bulletWidth) = BulletParams(); + return (count - 1) * _params.ItemSeparation + _params.ItemWidth; + } - return (count - 1) * separation + bulletWidth; + protected struct LayoutParameters + { + public int ItemHeight; + public int ItemSeparation; + public int ItemWidth; + public int VerticalSeparation; + + /// + /// Try to ensure there's at least this many bullets on one row. + /// + /// + /// For example, if there are two rows and the second row has only two bullets, + /// we "steal" some bullets from the row below it to make it look nicer. + /// + public int MinCountPerRow; + } +} + +/// +/// Renders one or more rows of bullets for item status. +/// +/// +/// This is a custom control to allow complex responsive layout logic. +/// +public sealed class BulletRender : BaseBulletRenderer +{ + public const int MinCountPerRow = 7; + + public const int BulletHeight = 12; + public const int VerticalSeparation = 2; + + private static readonly LayoutParameters LayoutNormal = new LayoutParameters + { + ItemHeight = BulletHeight, + ItemSeparation = 3, + ItemWidth = 5, + VerticalSeparation = VerticalSeparation, + MinCountPerRow = MinCountPerRow + }; + + private static readonly LayoutParameters LayoutTiny = new LayoutParameters + { + ItemHeight = BulletHeight, + ItemSeparation = 2, + ItemWidth = 2, + VerticalSeparation = VerticalSeparation, + MinCountPerRow = MinCountPerRow + }; + + private static readonly Color ColorA = Color.FromHex("#b68f0e"); + private static readonly Color ColorB = Color.FromHex("#d7df60"); + private static readonly Color ColorGoneA = Color.FromHex("#000000"); + private static readonly Color ColorGoneB = Color.FromHex("#222222"); + + private readonly Texture _bulletTiny; + private readonly Texture _bulletNormal; + + private BulletType _type = BulletType.Normal; + + public BulletType Type + { + get => _type; + set + { + if (_type == value) + return; + + Parameters = _type switch + { + BulletType.Normal => LayoutNormal, + BulletType.Tiny => LayoutTiny, + _ => throw new ArgumentOutOfRangeException() + }; + + _type = value; + } + } + + public BulletRender() + { + var resC = IoCManager.Resolve(); + _bulletTiny = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/tiny.png"); + _bulletNormal = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/normal.png"); + Parameters = LayoutNormal; + } + + protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor) + { + Color color; + if (spent) + color = altColor ? ColorGoneA : ColorGoneB; + else + color = altColor ? ColorA : ColorB; + + var texture = _type == BulletType.Tiny ? _bulletTiny : _bulletNormal; + handle.DrawTexture(texture, renderPos, color); } public enum BulletType @@ -176,3 +222,31 @@ public sealed class BulletRender : Control Tiny } } + +public sealed class BatteryBulletRenderer : BaseBulletRenderer +{ + private static readonly Color ItemColor = Color.FromHex("#E00000"); + private static readonly Color ItemColorGone = Color.Black; + + private const int SizeH = 10; + private const int SizeV = 10; + private const int Separation = 4; + + public BatteryBulletRenderer() + { + Parameters = new LayoutParameters + { + ItemWidth = SizeH, + ItemHeight = SizeV, + ItemSeparation = SizeH + Separation, + MinCountPerRow = 3, + VerticalSeparation = Separation + }; + } + + protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor) + { + var color = spent ? ItemColorGone : ItemColor; + handle.DrawRect(UIBox2.FromDimensions(renderPos, new Vector2(SizeH, SizeV)), color); + } +} diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs index cc7405b047..84eaa9af1b 100644 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs @@ -116,7 +116,7 @@ public sealed partial class GunSystem public sealed class BoxesStatusControl : Control { - private readonly BoxContainer _bulletsList; + private readonly BatteryBulletRenderer _bullets; private readonly Label _ammoCount; public BoxesStatusControl() @@ -128,27 +128,18 @@ public sealed partial class GunSystem AddChild(new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal, - HorizontalExpand = true, Children = { - new Control + (_bullets = new BatteryBulletRenderer { - HorizontalExpand = true, - Children = - { - (_bulletsList = new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Horizontal, - VerticalAlignment = VAlignment.Center, - SeparationOverride = 4 - }), - } - }, - new Control() { MinSize = new Vector2(5, 0) }, + Margin = new Thickness(0, 0, 5, 0), + HorizontalExpand = true + }), (_ammoCount = new Label { StyleClasses = { StyleNano.StyleClassItemStatus }, HorizontalAlignment = HAlignment.Right, + VerticalAlignment = VAlignment.Bottom }), } }); @@ -156,46 +147,12 @@ public sealed partial class GunSystem public void Update(int count, int max) { - _bulletsList.RemoveAllChildren(); - _ammoCount.Visible = true; _ammoCount.Text = $"x{count:00}"; - max = Math.Min(max, 8); - FillBulletRow(_bulletsList, count, max); - } - private static void FillBulletRow(Control container, int count, int capacity) - { - var colorGone = Color.FromHex("#000000"); - var color = Color.FromHex("#E00000"); - - // Draw the empty ones - for (var i = count; i < capacity; i++) - { - container.AddChild(new PanelContainer - { - PanelOverride = new StyleBoxFlat() - { - BackgroundColor = colorGone, - }, - MinSize = new Vector2(10, 15), - }); - } - - // Draw the full ones, but limit the count to the capacity - count = Math.Min(count, capacity); - for (var i = 0; i < count; i++) - { - container.AddChild(new PanelContainer - { - PanelOverride = new StyleBoxFlat() - { - BackgroundColor = color, - }, - MinSize = new Vector2(10, 15), - }); - } + _bullets.Capacity = max; + _bullets.Count = count; } } diff --git a/Content.Shared/Hands/Components/HandsComponent.cs b/Content.Shared/Hands/Components/HandsComponent.cs index f1f25a69f7..919d55f294 100644 --- a/Content.Shared/Hands/Components/HandsComponent.cs +++ b/Content.Shared/Hands/Components/HandsComponent.cs @@ -126,9 +126,43 @@ public sealed class HandsComponentState : ComponentState /// /// What side of the body this hand is on. /// +/// +/// public enum HandLocation : byte { Left, Middle, Right } + +/// +/// What side of the UI a hand is on. +/// +/// +/// +public enum HandUILocation : byte +{ + Left, + Right +} + +/// +/// Helper functions for working with . +/// +public static class HandLocationExt +{ + /// + /// Convert a into the appropriate . + /// This maps "middle" hands to . + /// + public static HandUILocation GetUILocation(this HandLocation location) + { + return location switch + { + HandLocation.Left => HandUILocation.Left, + HandLocation.Middle => HandUILocation.Right, + HandLocation.Right => HandUILocation.Right, + _ => throw new ArgumentOutOfRangeException(nameof(location), location, null) + }; + } +} diff --git a/Resources/Locale/en-US/devices/network-configurator.ftl b/Resources/Locale/en-US/devices/network-configurator.ftl index e1bcbc4c94..cd4955ed36 100644 --- a/Resources/Locale/en-US/devices/network-configurator.ftl +++ b/Resources/Locale/en-US/devices/network-configurator.ftl @@ -41,5 +41,5 @@ network-configurator-examine-current-mode = Current mode: {$mode} network-configurator-examine-switch-modes = Press {$key} to switch modes # item status -network-configurator-item-status-label = Current mode: {$mode} -{$keybinding} to switch mode +network-configurator-item-status-label = Mode: {$mode} + Switch: {$keybinding} diff --git a/Resources/Locale/en-US/implant/implant.ftl b/Resources/Locale/en-US/implant/implant.ftl index b93d43105a..c3002a73ae 100644 --- a/Resources/Locale/en-US/implant/implant.ftl +++ b/Resources/Locale/en-US/implant/implant.ftl @@ -10,9 +10,10 @@ implanter-component-implant-already = {$target} already has the {$implant}! implanter-draw-text = Draw implanter-inject-text = Inject -implanter-empty-text = None +implanter-empty-text = Empty -implanter-label = Implant: [color=green]{$implantName}[/color] | [color=white]{$modeString}[/color]{$lineBreak}{$implantDescription} +implanter-label = [color=green]{$implantName}[/color] + Mode: [color=white]{$modeString}[/color] implanter-contained-implant-text = [color=green]{$desc}[/color] diff --git a/Resources/Locale/en-US/inventory/item-status.ftl b/Resources/Locale/en-US/inventory/item-status.ftl new file mode 100644 index 0000000000..a53ba8be7d --- /dev/null +++ b/Resources/Locale/en-US/inventory/item-status.ftl @@ -0,0 +1 @@ +item-status-not-held = No held item diff --git a/Resources/Locale/en-US/radiation/geiger-component.ftl b/Resources/Locale/en-US/radiation/geiger-component.ftl index 0e7d2a8a35..726c7190f2 100644 --- a/Resources/Locale/en-US/radiation/geiger-component.ftl +++ b/Resources/Locale/en-US/radiation/geiger-component.ftl @@ -1,3 +1,3 @@ -geiger-item-control-status = Radiation: [color={$color}]{$rads} rads[/color] +geiger-item-control-status = [color={$color}]{$rads} rads[/color] geiger-item-control-disabled = Disabled geiger-component-examine = Current radiation: [color={$color}]{$rads} rads[/color] From 8aabb1c2e5f006ca604daa92f2da0b8856f02ee7 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Thu, 25 Apr 2024 02:27:13 +1200 Subject: [PATCH 49/61] Content changes & tests for engine prototype validation PR (#27188) * Content changes & tests for engine prototype validation PR * A --- .../Tests/Linter/StaticFieldValidationTest.cs | 150 ++++++++++++++++++ .../Components/IdentityBlockerComponent.cs | 1 + Content.YAMLLinter/Program.cs | 2 +- 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs diff --git a/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs new file mode 100644 index 0000000000..30724b50a6 --- /dev/null +++ b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using System.Linq; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; +using Robust.Shared.Reflection; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.IntegrationTests.Tests.Linter; + +/// +/// Verify that the yaml linter successfully validates static fields +/// +[TestFixture] +public sealed class StaticFieldValidationTest +{ + [Test] + public async Task TestStaticFieldValidation() + { + await using var pair = await PoolManager.GetServerClient(); + var protoMan = pair.Server.ProtoMan; + + var protos = new Dictionary>(); + foreach (var kind in protoMan.EnumeratePrototypeKinds()) + { + var ids = protoMan.EnumeratePrototypes(kind).Select(x => x.ID).ToHashSet(); + protos.Add(kind, ids); + } + + Assert.That(protoMan.ValidateStaticFields(typeof(StringValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdSetValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(PrivateProtoIdArrayValid), protos).Count, Is.Zero); + + Assert.That(protoMan.ValidateStaticFields(typeof(StringInvalid), protos).Count, Is.EqualTo(1)); + Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayInvalid), protos).Count, Is.EqualTo(2)); + Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdInvalid), protos).Count, Is.EqualTo(1)); + Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayInvalid), protos).Count, Is.EqualTo(2)); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestInvalid), protos).Count, Is.EqualTo(1)); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayInvalid), protos).Count, Is.EqualTo(2)); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListInvalid), protos).Count, Is.EqualTo(2)); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdSetInvalid), protos).Count, Is.EqualTo(2)); + Assert.That(protoMan.ValidateStaticFields(typeof(PrivateProtoIdArrayInvalid), protos).Count, Is.EqualTo(2)); + + await pair.CleanReturnAsync(); + } + + [TestPrototypes] + private const string TestPrototypes = @" +- type: entity + id: StaticFieldTestEnt + +- type: Tag + id: StaticFieldTestTag +"; + + [Reflect(false)] private sealed class StringValid + { + [ValidatePrototypeId] public static string Tag = "StaticFieldTestTag"; + } + + [Reflect(false)] private sealed class StringInvalid + { + [ValidatePrototypeId] public static string Tag = string.Empty; + } + + [Reflect(false)] private sealed class StringArrayValid + { + [ValidatePrototypeId] public static string[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"}; + } + + [Reflect(false)] private sealed class StringArrayInvalid + { + [ValidatePrototypeId] public static string[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty}; + } + + [Reflect(false)] private sealed class EntProtoIdValid + { + public static EntProtoId Tag = "StaticFieldTestEnt"; + } + + [Reflect(false)] private sealed class EntProtoIdInvalid + { + public static EntProtoId Tag = string.Empty; + } + + [Reflect(false)] private sealed class EntProtoIdArrayValid + { + public static EntProtoId[] Tag = {"StaticFieldTestEnt", "StaticFieldTestEnt"}; + } + + [Reflect(false)] private sealed class EntProtoIdArrayInvalid + { + public static EntProtoId[] Tag = {string.Empty, "StaticFieldTestEnt", string.Empty}; + } + + [Reflect(false)] private sealed class ProtoIdTestValid + { + public static ProtoId Tag = "StaticFieldTestTag"; + } + + [Reflect(false)] private sealed class ProtoIdTestInvalid + { + public static ProtoId Tag = string.Empty; + } + + [Reflect(false)] private sealed class ProtoIdArrayValid + { + public static ProtoId[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"}; + } + + [Reflect(false)] private sealed class ProtoIdArrayInvalid + { + public static ProtoId[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty}; + } + + [Reflect(false)] private sealed class ProtoIdListValid + { + public static List> Tag = new() {"StaticFieldTestTag", "StaticFieldTestTag"}; + } + + [Reflect(false)] private sealed class ProtoIdListInvalid + { + public static List> Tag = new() {string.Empty, "StaticFieldTestTag", string.Empty}; + } + + [Reflect(false)] private sealed class ProtoIdSetValid + { + public static HashSet> Tag = new() {"StaticFieldTestTag", "StaticFieldTestTag"}; + } + + [Reflect(false)] private sealed class ProtoIdSetInvalid + { + public static HashSet> Tag = new() {string.Empty, "StaticFieldTestTag", string.Empty, " "}; + } + + [Reflect(false)] private sealed class PrivateProtoIdArrayValid + { + private static ProtoId[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"}; + } + + [Reflect(false)] private sealed class PrivateProtoIdArrayInvalid + { + private static ProtoId[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty}; + } +} diff --git a/Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs b/Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs index 3857063783..e7a88b6ef2 100644 --- a/Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs +++ b/Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs @@ -6,6 +6,7 @@ namespace Content.Shared.IdentityManagement.Components; [RegisterComponent, NetworkedComponent] public sealed partial class IdentityBlockerComponent : Component { + [DataField] public bool Enabled = true; /// diff --git a/Content.YAMLLinter/Program.cs b/Content.YAMLLinter/Program.cs index b23faa48fc..7f0b740fe8 100644 --- a/Content.YAMLLinter/Program.cs +++ b/Content.YAMLLinter/Program.cs @@ -99,7 +99,7 @@ namespace Content.YAMLLinter yamlErrors[kind] = set; } - fieldErrors = protoMan.ValidateFields(prototypes); + fieldErrors = protoMan.ValidateStaticFields(prototypes); }); return (yamlErrors, fieldErrors); From 17160fad8393193bbf76c7dbd119f263a7faaeb2 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 25 Apr 2024 00:35:56 +1000 Subject: [PATCH 50/61] Update submodule to 219.2.0 (#27294) --- RobustToolbox | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RobustToolbox b/RobustToolbox index 4e87d93009..eb63809999 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit 4e87d93009748778c27ae835b16297596e5b1563 +Subproject commit eb638099999dce3a43d90772ca976ae010d649c0 From d797e3753f4da64de6912b1087fa2fd341552d69 Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:40:54 +0000 Subject: [PATCH 51/61] saltern bar stool (#27263) saltern stool Co-authored-by: deltanedas <@deltanedas:kde.org> --- Resources/Maps/saltern.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Resources/Maps/saltern.yml b/Resources/Maps/saltern.yml index 22b08b60b6..b622f69799 100644 --- a/Resources/Maps/saltern.yml +++ b/Resources/Maps/saltern.yml @@ -3273,12 +3273,13 @@ entities: - type: MetaData - type: Transform - type: Map + mapPaused: True - type: PhysicsMap + - type: GridTree + - type: MovedGrids - type: Broadphase - type: OccluderTree - type: LoadedMap - - type: GridTree - - type: MovedGrids - proto: AcousticGuitarInstrument entities: - uid: 3146 @@ -6119,8 +6120,6 @@ entities: - data: null ReagentId: Leporazine Quantity: 40 - - type: MixableSolution - solution: beaker - uid: 10800 components: - type: Transform @@ -32842,9 +32841,6 @@ entities: - type: Transform pos: 38.5,-5.5 parent: 31 - - type: Door - secondsUntilStateChange: -9978.97 - state: Closing - uid: 4019 components: - type: Transform @@ -33106,9 +33102,6 @@ entities: - type: Transform pos: 1.5,-2.5 parent: 31 - - type: Door - secondsUntilStateChange: -80.957985 - state: Closing - uid: 3944 components: - type: Transform @@ -58376,6 +58369,11 @@ entities: - type: Transform pos: 42.5,-7.5 parent: 31 + - uid: 11452 + components: + - type: Transform + pos: -4.5,-3.5 + parent: 31 - proto: StorageCanister entities: - uid: 1108 From ef0a4d64c82ae10b2039e82ea06a28765499e540 Mon Sep 17 00:00:00 2001 From: pigeonpeas <147350443+pigeonpeas@users.noreply.github.com> Date: Wed, 24 Apr 2024 17:27:34 -0400 Subject: [PATCH 52/61] Add a trash bag to the advanced cleaning borg module. (#27226) add the trash bag puts the trash bag in the advanced cleaning module --- .../Entities/Objects/Specific/Robotics/borg_modules.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml index d37523bd73..daa0d9bc20 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml @@ -318,6 +318,7 @@ - HoloprojectorBorg - SprayBottleSpaceCleaner - Dropper + - TrashBag # medical modules - type: entity From eb648dd0eb4b4cec3f85a59040ccd66cbe4f3617 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 24 Apr 2024 21:28:40 +0000 Subject: [PATCH 53/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index c1bdcacff3..b40dc7fbe6 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Blackern5000 - changes: - - message: Advanced topical meds now cost significantly less chemicals - type: Tweak - id: 5933 - time: '2024-02-13T22:03:13.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/24948 - author: Tayrtahn changes: - message: Drink bottles can now be opened and closed from the verbs menu, and some @@ -3850,3 +3843,10 @@ id: 6432 time: '2024-04-24T13:43:56.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27281 +- author: pigeonpeas + changes: + - message: Adds a trash bag to the advanced cleaning module. + type: Add + id: 6433 + time: '2024-04-24T21:27:34.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27226 From f6ac494ad3edb584d29f5bd71d4045d93ea9f55c Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:19:17 +1000 Subject: [PATCH 54/61] Fix .ftl keys (#27289) Required for https://github.com/space-wizards/RobustToolbox/pull/4885 to not kill content --- Content.Server/Speech/Components/PirateAccentComponent.cs | 3 ++- Resources/Locale/en-US/accent/italian.ftl | 3 --- Resources/Locale/en-US/accent/pirate.ftl | 2 +- Resources/Locale/en-US/administration/ui/admin-logs.ftl | 1 - Resources/Locale/en-US/administration/ui/admin-notes.ftl | 1 - Resources/Locale/en-US/alerts/alerts.ftl | 3 --- .../en-US/ame/components/ame-controller-component.ftl | 1 - .../arcade/components/space-villain-game-component.ftl | 3 +-- .../Locale/en-US/construction/ui/construction-menu.ftl | 3 +-- .../disposal/tube/components/disposal-router-component.ftl | 4 ---- .../disposal/tube/components/disposal-tagger-window.ftl | 3 --- Resources/Locale/en-US/escape-menu/ui/options-menu.ftl | 1 - Resources/Locale/en-US/guidebook/chemistry/effects.ftl | 2 +- .../en-US/interaction/interaction-popup-component.ftl | 6 ------ Resources/Locale/en-US/reagents/meta/physical-desc.ftl | 2 -- Resources/Locale/en-US/ui/verbs.ftl | 3 --- 16 files changed, 6 insertions(+), 35 deletions(-) delete mode 100644 Resources/Locale/en-US/ui/verbs.ftl diff --git a/Content.Server/Speech/Components/PirateAccentComponent.cs b/Content.Server/Speech/Components/PirateAccentComponent.cs index 0559d9854b..b5b292775d 100644 --- a/Content.Server/Speech/Components/PirateAccentComponent.cs +++ b/Content.Server/Speech/Components/PirateAccentComponent.cs @@ -15,6 +15,7 @@ public sealed partial class PirateAccentComponent : Component { "accent-pirate-prefix-1", "accent-pirate-prefix-2", - "accent-pirate-prefix-3" + "accent-pirate-prefix-3", + "accent-pirate-prefix-4", }; } diff --git a/Resources/Locale/en-US/accent/italian.ftl b/Resources/Locale/en-US/accent/italian.ftl index d0ef4e8f72..cc8641417f 100644 --- a/Resources/Locale/en-US/accent/italian.ftl +++ b/Resources/Locale/en-US/accent/italian.ftl @@ -78,9 +78,6 @@ accent-italian-words-replace-23 = greek accent-italian-words-24 = operatives accent-italian-words-replace-24 = greeks -accent-italian-words-24 = ops -accent-italian-words-replace-24 = greeks - accent-italian-words-25 = sec accent-italian-words-replace-25 = polizia diff --git a/Resources/Locale/en-US/accent/pirate.ftl b/Resources/Locale/en-US/accent/pirate.ftl index 8da975df40..b6db7c803b 100644 --- a/Resources/Locale/en-US/accent/pirate.ftl +++ b/Resources/Locale/en-US/accent/pirate.ftl @@ -1,7 +1,7 @@ accent-pirate-prefix-1 = Arrgh accent-pirate-prefix-2 = Garr accent-pirate-prefix-3 = Yarr -accent-pirate-prefix-3 = Yarrgh +accent-pirate-prefix-4 = Yarrgh accent-pirate-replaced-1 = my accent-pirate-replacement-1 = me diff --git a/Resources/Locale/en-US/administration/ui/admin-logs.ftl b/Resources/Locale/en-US/administration/ui/admin-logs.ftl index 549e9587d7..377bea6e84 100644 --- a/Resources/Locale/en-US/administration/ui/admin-logs.ftl +++ b/Resources/Locale/en-US/administration/ui/admin-logs.ftl @@ -14,7 +14,6 @@ admin-logs-select-none = None # Players admin-logs-search-players-placeholder = Search Players (OR) -admin-logs-select-none = None admin-logs-include-non-player = Include Non-players # Logs diff --git a/Resources/Locale/en-US/administration/ui/admin-notes.ftl b/Resources/Locale/en-US/administration/ui/admin-notes.ftl index ca5348a940..03e1290257 100644 --- a/Resources/Locale/en-US/administration/ui/admin-notes.ftl +++ b/Resources/Locale/en-US/administration/ui/admin-notes.ftl @@ -35,7 +35,6 @@ admin-notes-message-seen = Seen admin-notes-banned-from = Banned from admin-notes-the-server = the server admin-notes-permanently = permanently -admin-notes-for = for {$player} admin-notes-days = {$days} days admin-notes-hours = {$hours} hours admin-notes-minutes = {$minutes} minutes diff --git a/Resources/Locale/en-US/alerts/alerts.ftl b/Resources/Locale/en-US/alerts/alerts.ftl index 795d740141..319809da40 100644 --- a/Resources/Locale/en-US/alerts/alerts.ftl +++ b/Resources/Locale/en-US/alerts/alerts.ftl @@ -57,9 +57,6 @@ alerts-no-battery-desc = You don't have a battery, rendering you unable to charg alerts-internals-name = Toggle internals alerts-internals-desc = Toggles your gas tank internals on or off. -alerts-internals-name = Toggle internals -alerts-internals-desc = Toggles your gas tank internals on or off. - alerts-piloting-name = Piloting Shuttle alerts-piloting-desc = You are piloting a shuttle. Click the alert to stop. diff --git a/Resources/Locale/en-US/ame/components/ame-controller-component.ftl b/Resources/Locale/en-US/ame/components/ame-controller-component.ftl index ee1f7f42e7..f15141ebcc 100644 --- a/Resources/Locale/en-US/ame/components/ame-controller-component.ftl +++ b/Resources/Locale/en-US/ame/components/ame-controller-component.ftl @@ -16,7 +16,6 @@ ame-window-refresh-parts-button = Refresh Parts ame-window-core-count-label = Core count: ame-window-power-currentsupply-label = Current power supply: ame-window-power-targetsupply-label = Targeted power supply: -ame-window-toggle-injection-button = Toggle Injection ame-window-eject-button = Eject ame-window-increase-fuel-button = Increase ame-window-decrease-fuel-button = Decrease diff --git a/Resources/Locale/en-US/arcade/components/space-villain-game-component.ftl b/Resources/Locale/en-US/arcade/components/space-villain-game-component.ftl index af005ae62d..75e18c8d71 100644 --- a/Resources/Locale/en-US/arcade/components/space-villain-game-component.ftl +++ b/Resources/Locale/en-US/arcade/components/space-villain-game-component.ftl @@ -11,5 +11,4 @@ space-villain-game-enemy-dies-with-player-message = {$enemyName} dies, but takes space-villain-game-enemy-throws-bomb-message = {$enemyName} throws a bomb, exploding you for {$damageReceived} damage! space-villain-game-enemy-steals-player-power-message = {$enemyName} steals {$stolenAmount} of your power! space-villain-game-enemy-heals-message = {$enemyName} heals for {$healedAmount} health! -space-villain-game-enemy-steals-player-power-message = {$enemyName} steals {$stolenAmount} of your power! -space-villain-game-enemy-attacks-message = {$enemyName} attacks you for {$damageDealt} damage! \ No newline at end of file +space-villain-game-enemy-attacks-message = {$enemyName} attacks you for {$damageDealt} damage! diff --git a/Resources/Locale/en-US/construction/ui/construction-menu.ftl b/Resources/Locale/en-US/construction/ui/construction-menu.ftl index f4b7f3559a..82ebc01bc9 100644 --- a/Resources/Locale/en-US/construction/ui/construction-menu.ftl +++ b/Resources/Locale/en-US/construction/ui/construction-menu.ftl @@ -4,5 +4,4 @@ construction-menu-title = Construction construction-menu-place-ghost = Place construction ghost construction-menu-clear-all = Clear All construction-menu-eraser-mode = Eraser Mode -construction-menu-title = Construction -construction-menu-craft = Craft \ No newline at end of file +construction-menu-craft = Craft diff --git a/Resources/Locale/en-US/disposal/tube/components/disposal-router-component.ftl b/Resources/Locale/en-US/disposal/tube/components/disposal-router-component.ftl index 64fbfdf66f..4fe24b7853 100644 --- a/Resources/Locale/en-US/disposal/tube/components/disposal-router-component.ftl +++ b/Resources/Locale/en-US/disposal/tube/components/disposal-router-component.ftl @@ -4,7 +4,3 @@ disposal-router-window-title = Disposal Router disposal-router-window-tags-label = Tags: disposal-router-window-tag-input-tooltip = A comma separated list of tags disposal-router-window-tag-input-confirm-button = Confirm - -## ConfigureVerb - -configure-verb-get-data-text = Open Configuration diff --git a/Resources/Locale/en-US/disposal/tube/components/disposal-tagger-window.ftl b/Resources/Locale/en-US/disposal/tube/components/disposal-tagger-window.ftl index dc4b40fc7f..55523c4b95 100644 --- a/Resources/Locale/en-US/disposal/tube/components/disposal-tagger-window.ftl +++ b/Resources/Locale/en-US/disposal/tube/components/disposal-tagger-window.ftl @@ -1,6 +1,3 @@ disposal-tagger-window-title = Disposal Tagger disposal-tagger-window-tag-input-label = Tag: disposal-tagger-window-tag-confirm-button = Confirm - -## ConfigureVerb -configure-verb-get-data-text = Open Configuration diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index 6173977285..416d87f7c5 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -196,7 +196,6 @@ ui-options-function-editor-rotate-object = Rotate ui-options-function-editor-flip-object = Flip ui-options-function-editor-copy-object = Copy -ui-options-function-open-abilities-menu = Open action menu ui-options-function-show-debug-console = Open Console ui-options-function-show-debug-monitors = Show Debug Monitors ui-options-function-inspect-entity = Inspect Entity diff --git a/Resources/Locale/en-US/guidebook/chemistry/effects.ftl b/Resources/Locale/en-US/guidebook/chemistry/effects.ftl index b6f45d2386..94c1376083 100644 --- a/Resources/Locale/en-US/guidebook/chemistry/effects.ftl +++ b/Resources/Locale/en-US/guidebook/chemistry/effects.ftl @@ -43,7 +43,7 @@ reagent-effect-guidebook-foam-area-reaction-effect = *[other] create } large quantities of foam -reagent-effect-guidebook-foam-area-reaction-effect = +reagent-effect-guidebook-smoke-area-reaction-effect = { $chance -> [1] Creates *[other] create diff --git a/Resources/Locale/en-US/interaction/interaction-popup-component.ftl b/Resources/Locale/en-US/interaction/interaction-popup-component.ftl index bb56233ff1..4929b11b1c 100644 --- a/Resources/Locale/en-US/interaction/interaction-popup-component.ftl +++ b/Resources/Locale/en-US/interaction/interaction-popup-component.ftl @@ -42,7 +42,6 @@ petting-failure-crab = You reach out to pet {THE($target)}, but {SUBJECT($target petting-failure-dehydrated-carp = You pet {THE($target)} on {POSS-ADJ($target)} dry little head. petting-failure-goat = You reach out to pet {THE($target)}, but {SUBJECT($target)} stubbornly refuses! petting-failure-goose = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} too horrible! -petting-failure-goose = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} too australian! petting-failure-possum = You reach out to pet {THE($target)}, but are met with hisses and snarls! petting-failure-pig = You reach out to pet {THE($target)}, but are met with irritated oinks and squeals! petting-failure-raccoon = You reach out to pet {THE($target)}, but {THE($target)} is busy raccooning around. @@ -68,11 +67,6 @@ petting-failure-cleanbot = You reach out to pet {THE($target)}, but {SUBJECT($ta petting-failure-mimebot = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} busy miming! petting-failure-medibot = You reach out to pet {THE($target)}, but {POSS-ADJ($target)} syringe nearly stabs your hand! -## Knocking on windows - -# Shown when knocking on a window -comp-window-knock = *knock knock* - ## Rattling fences fence-rattle-success = *rattle* diff --git a/Resources/Locale/en-US/reagents/meta/physical-desc.ftl b/Resources/Locale/en-US/reagents/meta/physical-desc.ftl index 2e959bf14e..355b76c2a4 100644 --- a/Resources/Locale/en-US/reagents/meta/physical-desc.ftl +++ b/Resources/Locale/en-US/reagents/meta/physical-desc.ftl @@ -64,7 +64,6 @@ reagent-physical-desc-sticky = sticky reagent-physical-desc-bubbly = bubbly reagent-physical-desc-rocky = rocky reagent-physical-desc-lemony-fresh = lemony fresh -reagent-physical-desc-soapy = soapy reagent-physical-desc-crisp = crisp reagent-physical-desc-citric = citric reagent-physical-desc-acidic = acidic @@ -76,7 +75,6 @@ reagent-physical-desc-overpowering = overpowering reagent-physical-desc-sour = sour reagent-physical-desc-pungent = pungent reagent-physical-desc-clumpy = clumpy -reagent-physical-desc-strong-smelling = strong-smelling reagent-physical-desc-odorless = odorless reagent-physical-desc-gloopy = gloopy reagent-physical-desc-cloudy = cloudy diff --git a/Resources/Locale/en-US/ui/verbs.ftl b/Resources/Locale/en-US/ui/verbs.ftl deleted file mode 100644 index 1471261dcb..0000000000 --- a/Resources/Locale/en-US/ui/verbs.ftl +++ /dev/null @@ -1,3 +0,0 @@ -### Loc for the various UI-related verbs -ui-verb-toggle-open = Toggle UI -verb-instrument-openui = Play Music From 771661f478f5be07472434d32532f22aa7a8d8ab Mon Sep 17 00:00:00 2001 From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Date: Wed, 24 Apr 2024 21:26:55 -0400 Subject: [PATCH 55/61] fix analysis console bias button for real (#27299) fix bias button for real --- .../Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs index ecbb8e9662..59cf34944c 100644 --- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs +++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs @@ -73,7 +73,10 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow { ScanButton.Disabled = !state.CanScan; PrintButton.Disabled = !state.CanPrint; - DownBiasButton.Pressed = state.IsTraversalDown; + if (state.IsTraversalDown) + DownBiasButton.Pressed = true; + else + UpBiasButton.Pressed = true; var disabled = !state.ServerConnected || !state.CanScan || state.PointAmount <= 0; From 161fd6c83c87f7deb1a06aad66c9ac0ef329c641 Mon Sep 17 00:00:00 2001 From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Date: Wed, 24 Apr 2024 21:31:45 -0400 Subject: [PATCH 56/61] Mega Antag Refactor (#25786) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Mega Antag Refactor * last minute delta save * more workshopping * more shit * ok tested this for once * okkkkk sure * generic delays for starting rules * well darn * nukies partially * ouagh * ballin' faded and smonkin wed * obliterated the diff * Spread my arms and soak up congratulations * I've got plenty of love, but nothing to show for it * but there’s too much sunlight Shining on my laptop monitor, so I Can’t see anything with any amount of clarity * ok this junk * OOK! * fubar * most of sloth's review * oh boy * eek * hell yea! * ASDFJASDJFvsakcvjkzjnhhhyh --- .../Humanoid/HumanoidAppearanceSystem.cs | 5 +- .../Tests/GameRules/NukeOpsTest.cs | 42 +- .../Tests/GameRules/RuleMaxTimeRestartTest.cs | 1 + .../Tests/GameRules/SecretStartsTest.cs | 6 +- Content.Server/Administration/ServerApi.cs | 1 + .../Systems/AdminVerbSystem.Antags.cs | 42 +- .../Antag/AntagSelectionPlayerPool.cs | 29 + .../Antag/AntagSelectionSystem.API.cs | 302 +++++++ Content.Server/Antag/AntagSelectionSystem.cs | 685 ++++++++------- .../Components/AntagSelectionComponent.cs | 189 +++++ .../GhostRoleAntagSpawnerComponent.cs | 14 + .../Antag/MobReplacementRuleSystem.cs | 1 + .../Destructible/Thresholds/MinMax.cs | 15 +- .../Components/ActiveGameRuleComponent.cs | 2 +- .../Components/DelayedStartRuleComponent.cs | 16 + .../Components/EndedGameRuleComponent.cs | 2 +- .../Components/GameRuleComponent.cs | 9 +- .../GameTicking/GameTicker.GameRule.cs | 65 +- Content.Server/GameTicking/GameTicker.cs | 1 + .../Rules/Components/LoadMapRuleComponent.cs | 29 + .../Rules/Components/NinjaRuleComponent.cs | 2 +- .../NukeOperativeSpawnerComponent.cs | 11 +- .../Components/NukeOpsShuttleComponent.cs | 2 + .../Rules/Components/NukeopsRuleComponent.cs | 62 +- .../Rules/Components/PiratesRuleComponent.cs | 24 - .../Components/RevolutionaryRuleComponent.cs | 37 - .../Rules/Components/ThiefRuleComponent.cs | 36 +- .../Rules/Components/TraitorRuleComponent.cs | 15 + .../Rules/Components/ZombieRuleComponent.cs | 58 -- .../GameTicking/Rules/DeathMatchRuleSystem.cs | 24 +- .../Rules/GameRuleSystem.Utility.cs | 26 +- .../GameTicking/Rules/GameRuleSystem.cs | 38 +- .../Rules/InactivityTimeRestartRuleSystem.cs | 1 + .../Rules/KillCalloutRuleSystem.cs | 1 + .../GameTicking/Rules/LoadMapRuleSystem.cs | 80 ++ .../Rules/MaxTimeRestartRuleSystem.cs | 1 + .../GameTicking/Rules/NukeopsRuleSystem.cs | 777 +++++------------- .../GameTicking/Rules/PiratesRuleSystem.cs | 321 -------- .../GameTicking/Rules/RespawnRuleSystem.cs | 1 + .../Rules/RevolutionaryRuleSystem.cs | 135 +-- .../RoundstartStationVariationRuleSystem.cs | 1 + .../GameTicking/Rules/SandboxRuleSystem.cs | 1 + .../GameTicking/Rules/SecretRuleSystem.cs | 1 + .../GameTicking/Rules/SubGamemodesSystem.cs | 1 + .../GameTicking/Rules/ThiefRuleSystem.cs | 98 +-- .../GameTicking/Rules/TraitorRuleSystem.cs | 207 +---- .../GameTicking/Rules/ZombieRuleSystem.cs | 209 ++--- Content.Server/Objectives/ObjectivesSystem.cs | 19 +- .../PowerMonitoringConsoleSystem.cs | 5 +- .../Managers/IServerPreferencesManager.cs | 1 + .../Managers/ServerPreferencesManager.cs | 14 + .../RandomMetadata/RandomMetadataSystem.cs | 11 +- .../EntitySystems/ConditionalSpawnerSystem.cs | 1 + .../BasicStationEventSchedulerSystem.cs | 1 + .../Components/LoneOpsSpawnRuleComponent.cs | 18 - .../StationEvents/Events/AnomalySpawnRule.cs | 1 + .../Events/BluespaceArtifactRule.cs | 3 +- .../Events/BluespaceLockerRule.cs | 3 +- .../StationEvents/Events/BreakerFlipRule.cs | 3 +- .../Events/BureaucraticErrorRule.cs | 1 + .../StationEvents/Events/CargoGiftsRule.cs | 1 + .../StationEvents/Events/ClericalErrorRule.cs | 3 +- .../StationEvents/Events/FalseAlarmRule.cs | 1 + .../StationEvents/Events/GasLeakRule.cs | 1 + .../StationEvents/Events/ImmovableRodRule.cs | 1 + .../StationEvents/Events/IonStormRule.cs | 2 +- .../StationEvents/Events/KudzuGrowthRule.cs | 1 + .../StationEvents/Events/LoneOpsSpawnRule.cs | 47 -- .../Events/MassHallucinationsRule.cs | 1 + .../StationEvents/Events/MeteorSwarmRule.cs | 1 + .../StationEvents/Events/NinjaSpawnRule.cs | 1 + .../Events/PowerGridCheckRule.cs | 1 + .../Events/RandomEntityStorageSpawnRule.cs | 1 + .../Events/RandomSentienceRule.cs | 1 + .../StationEvents/Events/RandomSpawnRule.cs | 1 + .../StationEvents/Events/SolarFlareRule.cs | 1 + .../Events/StationEventSystem.cs | 1 + .../StationEvents/Events/VentClogRule.cs | 1 + .../StationEvents/Events/VentCrittersRule.cs | 1 + .../RampingStationEventSchedulerSystem.cs | 1 + .../Traitor/Systems/AutoTraitorSystem.cs | 48 +- .../Uplink/Commands/AddUplinkCommand.cs | 5 +- .../Zombies/PendingZombieComponent.cs | 16 + Content.Server/Zombies/ZombieSystem.cs | 4 + Content.Shared/Antag/AntagAcceptability.cs | 5 + Content.Shared/CCVar/CCVars.cs | 85 -- .../SharedHumanoidAppearanceSystem.cs | 5 +- .../Inventory/InventorySystem.Helpers.cs | 4 +- .../NukeOps/NukeOperativeComponent.cs | 7 +- Content.Shared/Roles/SharedRoleSystem.cs | 31 + .../Station/SharedStationSpawningSystem.cs | 19 +- .../game-presets/preset-pirates.ftl | 10 - Resources/Maps/Shuttles/striker.yml | 2 +- .../Entities/Markers/Spawners/ghost_roles.yml | 3 +- Resources/Prototypes/GameRules/events.yml | 51 +- Resources/Prototypes/GameRules/midround.yml | 17 + Resources/Prototypes/GameRules/roundstart.yml | 127 ++- .../Roles/Jobs/Fun/misc_startinggear.yml | 14 +- Resources/Prototypes/game_presets.yml | 12 - 99 files changed, 1931 insertions(+), 2310 deletions(-) create mode 100644 Content.Server/Antag/AntagSelectionPlayerPool.cs create mode 100644 Content.Server/Antag/AntagSelectionSystem.API.cs create mode 100644 Content.Server/Antag/Components/AntagSelectionComponent.cs create mode 100644 Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs rename Content.Server/GameTicking/{Rules => }/Components/ActiveGameRuleComponent.cs (84%) create mode 100644 Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs rename Content.Server/GameTicking/{Rules => }/Components/EndedGameRuleComponent.cs (81%) rename Content.Server/GameTicking/{Rules => }/Components/GameRuleComponent.cs (83%) create mode 100644 Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs delete mode 100644 Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs create mode 100644 Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs delete mode 100644 Content.Server/GameTicking/Rules/PiratesRuleSystem.cs delete mode 100644 Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs delete mode 100644 Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs delete mode 100644 Resources/Locale/en-US/game-ticking/game-presets/preset-pirates.ftl diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs index 5bae35da5b..6eb5dd9ec9 100644 --- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs +++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs @@ -108,8 +108,11 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem /// This should not be used if the entity is owned by the server. The server will otherwise /// override this with the appearance data it sends over. /// - public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null) + public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null) { + if (profile == null) + return; + if (!Resolve(uid, ref humanoid)) { return; diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs index 5833db0a10..5bada98a3a 100644 --- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs @@ -112,22 +112,38 @@ public sealed class NukeOpsTest // The game rule exists, and all the stations/shuttles/maps are properly initialized var rule = entMan.AllComponents().Single().Component; - Assert.That(entMan.EntityExists(rule.NukieOutpost)); - Assert.That(entMan.EntityExists(rule.NukieShuttle)); + var mapRule = entMan.AllComponents().Single().Component; + foreach (var grid in mapRule.MapGrids) + { + Assert.That(entMan.EntityExists(grid)); + Assert.That(entMan.HasComponent(grid)); + Assert.That(entMan.HasComponent(grid)); + } Assert.That(entMan.EntityExists(rule.TargetStation)); - Assert.That(entMan.HasComponent(rule.NukieOutpost)); - Assert.That(entMan.HasComponent(rule.NukieShuttle)); - - Assert.That(entMan.HasComponent(rule.NukieOutpost)); Assert.That(entMan.HasComponent(rule.TargetStation)); - var nukieStation = entMan.GetComponent(rule.NukieOutpost!.Value); + var nukieShuttlEnt = entMan.AllComponents().FirstOrDefault().Uid; + Assert.That(entMan.EntityExists(nukieShuttlEnt)); + + EntityUid? nukieStationEnt = null; + foreach (var grid in mapRule.MapGrids) + { + if (entMan.HasComponent(grid)) + { + nukieStationEnt = grid; + break; + } + } + + Assert.That(entMan.EntityExists(nukieStationEnt)); + var nukieStation = entMan.GetComponent(nukieStationEnt!.Value); + Assert.That(entMan.EntityExists(nukieStation.Station)); Assert.That(nukieStation.Station, Is.Not.EqualTo(rule.TargetStation)); - Assert.That(server.MapMan.MapExists(rule.NukiePlanet)); - var nukieMap = mapSys.GetMap(rule.NukiePlanet!.Value); + Assert.That(server.MapMan.MapExists(mapRule.Map)); + var nukieMap = mapSys.GetMap(mapRule.Map!.Value); var targetStation = entMan.GetComponent(rule.TargetStation!.Value); var targetGrid = targetStation.Grids.First(); @@ -135,8 +151,8 @@ public sealed class NukeOpsTest Assert.That(targetMap, Is.Not.EqualTo(nukieMap)); Assert.That(entMan.GetComponent(player).MapUid, Is.EqualTo(nukieMap)); - Assert.That(entMan.GetComponent(rule.NukieOutpost!.Value).MapUid, Is.EqualTo(nukieMap)); - Assert.That(entMan.GetComponent(rule.NukieShuttle!.Value).MapUid, Is.EqualTo(nukieMap)); + Assert.That(entMan.GetComponent(nukieStationEnt.Value).MapUid, Is.EqualTo(nukieMap)); + Assert.That(entMan.GetComponent(nukieShuttlEnt).MapUid, Is.EqualTo(nukieMap)); // The maps are all map-initialized, including the player // Yes, this is necessary as this has repeatedly been broken somehow. @@ -149,8 +165,8 @@ public sealed class NukeOpsTest Assert.That(LifeStage(player), Is.GreaterThan(EntityLifeStage.Initialized)); Assert.That(LifeStage(nukieMap), Is.GreaterThan(EntityLifeStage.Initialized)); Assert.That(LifeStage(targetMap), Is.GreaterThan(EntityLifeStage.Initialized)); - Assert.That(LifeStage(rule.NukieOutpost), Is.GreaterThan(EntityLifeStage.Initialized)); - Assert.That(LifeStage(rule.NukieShuttle), Is.GreaterThan(EntityLifeStage.Initialized)); + Assert.That(LifeStage(nukieStationEnt.Value), Is.GreaterThan(EntityLifeStage.Initialized)); + Assert.That(LifeStage(nukieShuttlEnt), Is.GreaterThan(EntityLifeStage.Initialized)); Assert.That(LifeStage(rule.TargetStation), Is.GreaterThan(EntityLifeStage.Initialized)); // Make sure the player has hands. We've had fucking disarmed nukies before. diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs index 0707bd64c6..20a157e33e 100644 --- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs @@ -1,5 +1,6 @@ using Content.Server.GameTicking; using Content.Server.GameTicking.Commands; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; using Content.Shared.CCVar; diff --git a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs index 0f665a63de..5d7ae8efbf 100644 --- a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs @@ -17,6 +17,7 @@ public sealed class SecretStartsTest var server = pair.Server; await server.WaitIdleAsync(); + var entMan = server.ResolveDependency(); var gameTicker = server.ResolveDependency().GetEntitySystem(); await server.WaitAssertion(() => @@ -32,10 +33,7 @@ public sealed class SecretStartsTest await server.WaitAssertion(() => { - foreach (var rule in gameTicker.GetAddedGameRules()) - { - Assert.That(gameTicker.GetActiveGameRules(), Does.Contain(rule)); - } + Assert.That(gameTicker.GetAddedGameRules().Count(), Is.GreaterThan(1), $"No additional rules started by secret rule."); // End all rules gameTicker.ClearGameRules(); diff --git a/Content.Server/Administration/ServerApi.cs b/Content.Server/Administration/ServerApi.cs index 6f10ef9b47..04fd38598f 100644 --- a/Content.Server/Administration/ServerApi.cs +++ b/Content.Server/Administration/ServerApi.cs @@ -8,6 +8,7 @@ using System.Text.Json.Nodes; using System.Threading.Tasks; using Content.Server.Administration.Systems; using Content.Server.GameTicking; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Presets; using Content.Server.GameTicking.Rules.Components; using Content.Server.Maps; diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs index eff97136d0..599485150a 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs @@ -1,23 +1,37 @@ -using Content.Server.GameTicking.Rules; +using Content.Server.Administration.Commands; +using Content.Server.Antag; +using Content.Server.GameTicking.Rules.Components; using Content.Server.Zombies; using Content.Shared.Administration; using Content.Shared.Database; -using Content.Shared.Humanoid; using Content.Shared.Mind.Components; +using Content.Shared.Roles; using Content.Shared.Verbs; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Server.Administration.Systems; public sealed partial class AdminVerbSystem { + [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly ZombieSystem _zombie = default!; - [Dependency] private readonly ThiefRuleSystem _thief = default!; - [Dependency] private readonly TraitorRuleSystem _traitorRule = default!; - [Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!; - [Dependency] private readonly PiratesRuleSystem _piratesRule = default!; - [Dependency] private readonly RevolutionaryRuleSystem _revolutionaryRule = default!; + + [ValidatePrototypeId] + private const string DefaultTraitorRule = "Traitor"; + + [ValidatePrototypeId] + private const string DefaultNukeOpRule = "LoneOpsSpawn"; + + [ValidatePrototypeId] + private const string DefaultRevsRule = "Revolutionary"; + + [ValidatePrototypeId] + private const string DefaultThiefRule = "Thief"; + + [ValidatePrototypeId] + private const string PirateGearId = "PirateGear"; // All antag verbs have names so invokeverb works. private void AddAntagVerbs(GetVerbsEvent args) @@ -40,9 +54,7 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"), Act = () => { - // if its a monkey or mouse or something dont give uplink or objectives - var isHuman = HasComp(args.Target); - _traitorRule.MakeTraitorAdmin(args.Target, giveUplink: isHuman, giveObjectives: isHuman); + _antag.ForceMakeAntag(player, DefaultTraitorRule); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-traitor"), @@ -71,7 +83,7 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"), Act = () => { - _nukeopsRule.MakeLoneNukie(args.Target); + _antag.ForceMakeAntag(player, DefaultNukeOpRule); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-nuclear-operative"), @@ -85,14 +97,14 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"), Act = () => { - _piratesRule.MakePirate(args.Target); + // pirates just get an outfit because they don't really have logic associated with them + SetOutfitCommand.SetOutfit(args.Target, PirateGearId, EntityManager); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-pirate"), }; args.Verbs.Add(pirate); - //todo come here at some point dear lort. Verb headRev = new() { Text = Loc.GetString("admin-verb-text-make-head-rev"), @@ -100,7 +112,7 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"), Act = () => { - _revolutionaryRule.OnHeadRevAdmin(args.Target); + _antag.ForceMakeAntag(player, DefaultRevsRule); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-head-rev"), @@ -114,7 +126,7 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"), "icon"), Act = () => { - _thief.AdminMakeThief(args.Target, false); //Midround add pacified is bad + _antag.ForceMakeAntag(player, DefaultThiefRule); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-thief"), diff --git a/Content.Server/Antag/AntagSelectionPlayerPool.cs b/Content.Server/Antag/AntagSelectionPlayerPool.cs new file mode 100644 index 0000000000..054292dcf9 --- /dev/null +++ b/Content.Server/Antag/AntagSelectionPlayerPool.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Robust.Shared.Player; +using Robust.Shared.Random; + +namespace Content.Server.Antag; + +public sealed class AntagSelectionPlayerPool(params List[] sessions) +{ + private readonly List> _orderedPools = sessions.ToList(); + + public bool TryPickAndTake(IRobustRandom random, [NotNullWhen(true)] out ICommonSession? session) + { + session = null; + + foreach (var pool in _orderedPools) + { + if (pool.Count == 0) + continue; + + session = random.PickAndTake(pool); + break; + } + + return session != null; + } + + public int Count => _orderedPools.Sum(p => p.Count); +} diff --git a/Content.Server/Antag/AntagSelectionSystem.API.cs b/Content.Server/Antag/AntagSelectionSystem.API.cs new file mode 100644 index 0000000000..f8ec5bcafc --- /dev/null +++ b/Content.Server/Antag/AntagSelectionSystem.API.cs @@ -0,0 +1,302 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Server.Antag.Components; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Objectives; +using Content.Shared.Chat; +using Content.Shared.Mind; +using JetBrains.Annotations; +using Robust.Shared.Audio; +using Robust.Shared.Player; + +namespace Content.Server.Antag; + +public sealed partial class AntagSelectionSystem +{ + /// + /// Tries to get the next non-filled definition based on the current amount of selected minds and other factors. + /// + public bool TryGetNextAvailableDefinition(Entity ent, + [NotNullWhen(true)] out AntagSelectionDefinition? definition) + { + definition = null; + + var totalTargetCount = GetTargetAntagCount(ent); + var mindCount = ent.Comp.SelectedMinds.Count; + if (mindCount >= totalTargetCount) + return false; + + foreach (var def in ent.Comp.Definitions) + { + var target = GetTargetAntagCount(ent, null, def); + + if (mindCount < target) + { + definition = def; + return true; + } + + mindCount -= target; + } + + return false; + } + + /// + /// Gets the number of antagonists that should be present for a given rule based on the provided pool. + /// A null pool will simply use the player count. + /// + public int GetTargetAntagCount(Entity ent, AntagSelectionPlayerPool? pool = null) + { + var count = 0; + foreach (var def in ent.Comp.Definitions) + { + count += GetTargetAntagCount(ent, pool, def); + } + + return count; + } + + /// + /// Gets the number of antagonists that should be present for a given antag definition based on the provided pool. + /// A null pool will simply use the player count. + /// + public int GetTargetAntagCount(Entity ent, AntagSelectionPlayerPool? pool, AntagSelectionDefinition def) + { + var poolSize = pool?.Count ?? _playerManager.Sessions.Length; + // factor in other definitions' affect on the count. + var countOffset = 0; + foreach (var otherDef in ent.Comp.Definitions) + { + countOffset += Math.Clamp(poolSize / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio; + } + // make sure we don't double-count the current selection + countOffset -= Math.Clamp((poolSize + countOffset) / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio; + + return Math.Clamp((poolSize - countOffset) / def.PlayerRatio, def.Min, def.Max); + } + + /// + /// Returns identifiable information for all antagonists to be used in a round end summary. + /// + /// + /// A list containing, in order, the antag's mind, the session data, and the original name stored as a string. + /// + public List<(EntityUid, SessionData, string)> GetAntagIdentifiers(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return new List<(EntityUid, SessionData, string)>(); + + var output = new List<(EntityUid, SessionData, string)>(); + foreach (var (mind, name) in ent.Comp.SelectedMinds) + { + if (!TryComp(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null) + continue; + + if (!_playerManager.TryGetPlayerData(mindComp.OriginalOwnerUserId.Value, out var data)) + continue; + + output.Add((mind, data, name)); + } + return output; + } + + /// + /// Returns all the minds of antagonists. + /// + public List> GetAntagMinds(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return new(); + + var output = new List>(); + foreach (var (mind, _) in ent.Comp.SelectedMinds) + { + if (!TryComp(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null) + continue; + + output.Add((mind, mindComp)); + } + return output; + } + + /// + /// Helper specifically for + /// + public List GetAntagMindEntityUids(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return new(); + + return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList(); + } + + /// + /// Returns all the antagonists for this rule who are currently alive + /// + public IEnumerable GetAliveAntags(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + yield break; + + var minds = GetAntagMinds(ent); + foreach (var mind in minds) + { + if (_mind.IsCharacterDeadIc(mind)) + continue; + + if (mind.Comp.OriginalOwnedEntity != null) + yield return GetEntity(mind.Comp.OriginalOwnedEntity.Value); + } + } + + /// + /// Returns the number of alive antagonists for this rule. + /// + public int GetAliveAntagCount(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return 0; + + var numbah = 0; + var minds = GetAntagMinds(ent); + foreach (var mind in minds) + { + if (_mind.IsCharacterDeadIc(mind)) + continue; + + numbah++; + } + + return numbah; + } + + /// + /// Returns if there are any remaining antagonists alive for this rule. + /// + public bool AnyAliveAntags(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + return GetAliveAntags(ent).Any(); + } + + /// + /// Checks if all the antagonists for this rule are alive. + /// + public bool AllAntagsAlive(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + return GetAliveAntagCount(ent) == ent.Comp.SelectedMinds.Count; + } + + /// + /// Helper method to send the briefing text and sound to a player entity + /// + /// The entity chosen to be antag + /// The briefing text to send + /// The color the briefing should be, null for default + /// The sound to briefing/greeting sound to play + public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) + { + if (!_mind.TryGetMind(entity, out _, out var mindComponent)) + return; + + if (mindComponent.Session == null) + return; + + SendBriefing(mindComponent.Session, briefing, briefingColor, briefingSound); + } + + /// + /// Helper method to send the briefing text and sound to a list of sessions + /// + /// The sessions that will be sent the briefing + /// The briefing text to send + /// The color the briefing should be, null for default + /// The sound to briefing/greeting sound to play + [PublicAPI] + public void SendBriefing(List sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) + { + foreach (var session in sessions) + { + SendBriefing(session, briefing, briefingColor, briefingSound); + } + } + + /// + /// Helper method to send the briefing text and sound to a session + /// + /// The player chosen to be an antag + /// The briefing data + public void SendBriefing( + ICommonSession? session, + BriefingData? data) + { + if (session == null || data == null) + return; + + var text = data.Value.Text == null ? string.Empty : Loc.GetString(data.Value.Text); + SendBriefing(session, text, data.Value.Color, data.Value.Sound); + } + + /// + /// Helper method to send the briefing text and sound to a session + /// + /// The player chosen to be an antag + /// The briefing text to send + /// The color the briefing should be, null for default + /// The sound to briefing/greeting sound to play + public void SendBriefing( + ICommonSession? session, + string briefing, + Color? briefingColor, + SoundSpecifier? briefingSound) + { + if (session == null) + return; + + _audio.PlayGlobal(briefingSound, session); + if (!string.IsNullOrEmpty(briefing)) + { + var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing)); + _chat.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel, + briefingColor); + } + } + + /// + /// This technically is a gamerule-ent-less way to make an entity an antag. + /// You should almost never be using this. + /// + public void ForceMakeAntag(ICommonSession? player, string defaultRule) where T : Component + { + var rule = ForceGetGameRuleEnt(defaultRule); + + if (!TryGetNextAvailableDefinition(rule, out var def)) + def = rule.Comp.Definitions.Last(); + MakeAntag(rule, player, def.Value); + } + + /// + /// Tries to grab one of the weird specific antag gamerule ents or starts a new one. + /// This is gross code but also most of this is pretty gross to begin with. + /// + public Entity ForceGetGameRuleEnt(string id) where T : Component + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var comp)) + { + return (uid, comp); + } + var ruleEnt = GameTicker.AddGameRule(id); + RemComp(ruleEnt); + var antag = Comp(ruleEnt); + antag.SelectionsComplete = true; // don't do normal selection. + GameTicker.StartGameRule(ruleEnt); + return (ruleEnt, antag); + } +} diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs index b11c562df5..eb68e077b1 100644 --- a/Content.Server/Antag/AntagSelectionSystem.cs +++ b/Content.Server/Antag/AntagSelectionSystem.cs @@ -1,347 +1,444 @@ +using System.Linq; +using Content.Server.Antag.Components; +using Content.Server.Chat.Managers; +using Content.Server.GameTicking; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; +using Content.Server.Ghost.Roles; +using Content.Server.Ghost.Roles.Components; using Content.Server.Mind; using Content.Server.Preferences.Managers; +using Content.Server.Roles; using Content.Server.Roles.Jobs; using Content.Server.Shuttles.Components; +using Content.Server.Station.Systems; using Content.Shared.Antag; +using Content.Shared.Ghost; using Content.Shared.Humanoid; using Content.Shared.Players; using Content.Shared.Preferences; -using Content.Shared.Roles; using Robust.Server.Audio; -using Robust.Shared.Audio; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using System.Linq; -using Content.Shared.Chat; +using Robust.Server.GameObjects; +using Robust.Server.Player; using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Player; +using Robust.Shared.Random; namespace Content.Server.Antag; -public sealed class AntagSelectionSystem : GameRuleSystem +public sealed partial class AntagSelectionSystem : GameRuleSystem { - [Dependency] private readonly IServerPreferencesManager _prefs = default!; - [Dependency] private readonly AudioSystem _audioSystem = default!; + [Dependency] private readonly IChatManager _chat = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IServerPreferencesManager _pref = default!; + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly GhostRoleSystem _ghostRole = default!; [Dependency] private readonly JobSystem _jobs = default!; - [Dependency] private readonly MindSystem _mindSystem = default!; - [Dependency] private readonly SharedRoleSystem _roleSystem = default!; + [Dependency] private readonly MapSystem _map = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly RoleSystem _role = default!; + [Dependency] private readonly StationSpawningSystem _stationSpawning = default!; + [Dependency] private readonly TransformSystem _transform = default!; - #region Eligible Player Selection - /// - /// Get all players that are eligible for an antag role - /// - /// All sessions from which to select eligible players - /// The prototype to get eligible players for - /// Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included - /// Should players already selected as antags be eligible - /// Should we ignore if the player has enabled this specific role - /// A custom condition that each player is tested against, if it returns true the player is excluded from eligibility - /// List of all player entities that match the requirements - public List GetEligiblePlayers(IEnumerable playerSessions, - ProtoId antagPrototype, - bool includeAllJobs = false, - AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive, - bool ignorePreferences = false, - bool allowNonHumanoids = false, - Func? customExcludeCondition = null) + // arbitrary random number to give late joining some mild interest. + public const float LateJoinRandomChance = 0.5f; + + /// + public override void Initialize() { - var eligiblePlayers = new List(); + base.Initialize(); - foreach (var player in playerSessions) + SubscribeLocalEvent(OnTakeGhostRole); + + SubscribeLocalEvent(OnPlayerSpawning); + SubscribeLocalEvent(OnJobsAssigned); + SubscribeLocalEvent(OnSpawnComplete); + } + + private void OnTakeGhostRole(Entity ent, ref TakeGhostRoleEvent args) + { + if (args.TookRole) + return; + + if (ent.Comp.Rule is not { } rule || ent.Comp.Definition is not { } def) + return; + + if (!Exists(rule) || !TryComp(rule, out var select)) + return; + + MakeAntag((rule, select), args.Player, def, ignoreSpawner: true); + args.TookRole = true; + _ghostRole.UnregisterGhostRole((ent, Comp(ent))); + } + + private void OnPlayerSpawning(RulePlayerSpawningEvent args) + { + var pool = args.PlayerPool; + + var query = QueryActiveRules(); + while (query.MoveNext(out var uid, out _, out var comp, out _)) { - if (IsPlayerEligible(player, antagPrototype, includeAllJobs, acceptableAntags, ignorePreferences, allowNonHumanoids, customExcludeCondition)) - eligiblePlayers.Add(player.AttachedEntity!.Value); - } - - return eligiblePlayers; - } - - /// - /// Get all sessions that are eligible for an antag role, can be run prior to sessions being attached to an entity - /// This does not exclude sessions that have already been chosen as antags - that must be handled manually - /// - /// All sessions from which to select eligible players - /// The prototype to get eligible players for - /// Should we ignore if the player has enabled this specific role - /// List of all player sessions that match the requirements - public List GetEligibleSessions(IEnumerable playerSessions, ProtoId antagPrototype, bool ignorePreferences = false) - { - var eligibleSessions = new List(); - - foreach (var session in playerSessions) - { - if (IsSessionEligible(session, antagPrototype, ignorePreferences)) - eligibleSessions.Add(session); - } - - return eligibleSessions; - } - - /// - /// Test eligibility of the player for a specific antag role - /// - /// The player session to test - /// The prototype to get eligible players for - /// Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included - /// Should players already selected as antags be eligible - /// Should we ignore if the player has enabled this specific role - /// A function, accepting an EntityUid and returning bool. Each player is tested against this, returning truw will exclude the player from eligibility - /// True if the player session matches the requirements, false otherwise - public bool IsPlayerEligible(ICommonSession session, - ProtoId antagPrototype, - bool includeAllJobs = false, - AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive, - bool ignorePreferences = false, - bool allowNonHumanoids = false, - Func? customExcludeCondition = null) - { - if (!IsSessionEligible(session, antagPrototype, ignorePreferences)) - return false; - - //Ensure the player has a mind - if (session.GetMind() is not { } playerMind) - return false; - - //Ensure the player has an attached entity - if (session.AttachedEntity is not { } playerEntity) - return false; - - //Ignore latejoined players, ie those on the arrivals station - if (HasComp(playerEntity)) - return false; - - //Exclude jobs that cannot be antag, unless explicitly allowed - if (!includeAllJobs && !_jobs.CanBeAntag(session)) - return false; - - //Check if the entity is already an antag - switch (acceptableAntags) - { - //If we dont want to select any antag roles - case AntagAcceptability.None: - { - if (_roleSystem.MindIsAntagonist(playerMind)) - return false; - break; - } - //If we dont want to select exclusive antag roles - case AntagAcceptability.NotExclusive: - { - if (_roleSystem.MindIsExclusiveAntagonist(playerMind)) - return false; - break; - } - } - - //Unless explictly allowed, ignore non humanoids (eg pets) - if (!allowNonHumanoids && !HasComp(playerEntity)) - return false; - - //If a custom condition was provided, test it and exclude the player if it returns true - if (customExcludeCondition != null && customExcludeCondition(playerEntity)) - return false; - - - return true; - } - - /// - /// Check if the session is eligible for a role, can be run prior to the session being attached to an entity - /// - /// Player session to check - /// Which antag prototype to check for - /// Ignore if the player has enabled this antag - /// True if the session matches the requirements, false otherwise - public bool IsSessionEligible(ICommonSession session, ProtoId antagPrototype, bool ignorePreferences = false) - { - //Exclude disconnected or zombie sessions - //No point giving antag roles to them - if (session.Status == SessionStatus.Disconnected || - session.Status == SessionStatus.Zombie) - return false; - - //Check the player has this antag preference selected - //Unless we are ignoring preferences, in which case add them anyway - var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(session.UserId).SelectedCharacter; - if (!pref.AntagPreferences.Contains(antagPrototype.Id) && !ignorePreferences) - return false; - - return true; - } - #endregion - - /// - /// Helper method to calculate the number of antags to select based upon the number of players - /// - /// How many players there are on the server - /// How many players should there be for an additional antag - /// Maximum number of antags allowed - /// The number of antags that should be chosen - public int CalculateAntagCount(int playerCount, int playersPerAntag, int maxAntags) - { - return Math.Clamp(playerCount / playersPerAntag, 1, maxAntags); - } - - #region Antag Selection - /// - /// Selects a set number of entities from several lists, prioritising the first list till its empty, then second list etc - /// - /// Array of lists, which are chosen from in order until the correct number of items are selected - /// How many items to select - /// Up to the specified count of elements from all provided lists - public List ChooseAntags(int count, params List[] eligiblePlayerLists) - { - var chosenPlayers = new List(); - foreach (var playerList in eligiblePlayerLists) - { - //Remove all chosen players from this list, to prevent duplicates - foreach (var chosenPlayer in chosenPlayers) - { - playerList.Remove(chosenPlayer); - } - - //If we have reached the desired number of players, skip - if (chosenPlayers.Count >= count) + if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn) continue; - //Pick and choose a random number of players from this list - chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList)); + if (comp.SelectionsComplete) + return; + + ChooseAntags((uid, comp), pool); + comp.SelectionsComplete = true; + + foreach (var session in comp.SelectedSessions) + { + args.PlayerPool.Remove(session); + GameTicker.PlayerJoinGame(session); + } } - return chosenPlayers; } - /// - /// Helper method to choose antags from a list - /// - /// List of eligible players - /// How many to choose - /// Up to the specified count of elements from the provided list - public List ChooseAntags(int count, List eligiblePlayers) + + private void OnJobsAssigned(RulePlayerJobsAssignedEvent args) { - var chosenPlayers = new List(); + var query = QueryActiveRules(); + while (query.MoveNext(out var uid, out _, out var comp, out _)) + { + if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn) + continue; + + if (comp.SelectionsComplete) + continue; + + ChooseAntags((uid, comp)); + comp.SelectionsComplete = true; + } + } + + private void OnSpawnComplete(PlayerSpawnCompleteEvent args) + { + if (!args.LateJoin) + return; + + // TODO: this really doesn't handle multiple latejoin definitions well + // eventually this should probably store the players per definition with some kind of unique identifier. + // something to figure out later. + + var query = QueryActiveRules(); + while (query.MoveNext(out var uid, out _, out var antag, out _)) + { + if (!RobustRandom.Prob(LateJoinRandomChance)) + continue; + + if (!antag.Definitions.Any(p => p.LateJoinAdditional)) + continue; + + if (!TryGetNextAvailableDefinition((uid, antag), out var def)) + continue; + + MakeAntag((uid, antag), args.Player, def.Value); + } + } + + protected override void Added(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) + { + base.Added(uid, component, gameRule, args); + + for (var i = 0; i < component.Definitions.Count; i++) + { + var def = component.Definitions[i]; + + if (def.MinRange != null) + { + def.Min = def.MinRange.Value.Next(RobustRandom); + } + + if (def.MaxRange != null) + { + def.Max = def.MaxRange.Value.Next(RobustRandom); + } + } + } + + protected override void Started(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, component, gameRule, args); + + if (component.SelectionsComplete) + return; + + if (GameTicker.RunLevel != GameRunLevel.InRound) + return; + + if (GameTicker.RunLevel == GameRunLevel.InRound && component.SelectionTime == AntagSelectionTime.PrePlayerSpawn) + return; + + ChooseAntags((uid, component)); + component.SelectionsComplete = true; + } + + /// + /// Chooses antagonists from the current selection of players + /// + public void ChooseAntags(Entity ent) + { + var sessions = _playerManager.Sessions.ToList(); + ChooseAntags(ent, sessions); + } + + /// + /// Chooses antagonists from the given selection of players + /// + public void ChooseAntags(Entity ent, List pool) + { + foreach (var def in ent.Comp.Definitions) + { + ChooseAntags(ent, pool, def); + } + } + + /// + /// Chooses antagonists from the given selection of players for the given antag definition. + /// + public void ChooseAntags(Entity ent, List pool, AntagSelectionDefinition def) + { + var playerPool = GetPlayerPool(ent, pool, def); + var count = GetTargetAntagCount(ent, playerPool, def); for (var i = 0; i < count; i++) { - if (eligiblePlayers.Count == 0) - break; - - chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers)); - } - - return chosenPlayers; - } - - /// - /// Selects a set number of sessions from several lists, prioritising the first list till its empty, then second list etc - /// - /// Array of lists, which are chosen from in order until the correct number of items are selected - /// How many items to select - /// Up to the specified count of elements from all provided lists - public List ChooseAntags(int count, params List[] eligiblePlayerLists) - { - var chosenPlayers = new List(); - foreach (var playerList in eligiblePlayerLists) - { - //Remove all chosen players from this list, to prevent duplicates - foreach (var chosenPlayer in chosenPlayers) + var session = (ICommonSession?) null; + if (def.PickPlayer) { - playerList.Remove(chosenPlayer); + if (!playerPool.TryPickAndTake(RobustRandom, out session)) + break; + + if (ent.Comp.SelectedSessions.Contains(session)) + continue; } - //If we have reached the desired number of players, skip - if (chosenPlayers.Count >= count) - continue; - - //Pick and choose a random number of players from this list - chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList)); + MakeAntag(ent, session, def); } - return chosenPlayers; } - /// - /// Helper method to choose sessions from a list - /// - /// List of eligible sessions - /// How many to choose - /// Up to the specified count of elements from the provided list - public List ChooseAntags(int count, List eligiblePlayers) - { - var chosenPlayers = new List(); - for (int i = 0; i < count; i++) + /// + /// Makes a given player into the specified antagonist. + /// + public void MakeAntag(Entity ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false) + { + var antagEnt = (EntityUid?) null; + var isSpawner = false; + + if (session != null) { - if (eligiblePlayers.Count == 0) - break; + ent.Comp.SelectedSessions.Add(session); - chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers)); + // we shouldn't be blocking the entity if they're just a ghost or smth. + if (!HasComp(session.AttachedEntity)) + antagEnt = session.AttachedEntity; } - - return chosenPlayers; - } - #endregion - - #region Briefings - /// - /// Helper method to send the briefing text and sound to a list of entities - /// - /// The players chosen to be antags - /// The briefing text to send - /// The color the briefing should be, null for default - /// The sound to briefing/greeting sound to play - public void SendBriefing(List entities, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) - { - foreach (var entity in entities) + else if (!ignoreSpawner && def.SpawnerPrototype != null) // don't add spawners if we have a player, dummy. { - SendBriefing(entity, briefing, briefingColor, briefingSound); + antagEnt = Spawn(def.SpawnerPrototype); + isSpawner = true; } - } - /// - /// Helper method to send the briefing text and sound to a player entity - /// - /// The entity chosen to be antag - /// The briefing text to send - /// The color the briefing should be, null for default - /// The sound to briefing/greeting sound to play - public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) - { - if (!_mindSystem.TryGetMind(entity, out _, out var mindComponent)) + if (!antagEnt.HasValue) + { + var getEntEv = new AntagSelectEntityEvent(session, ent); + RaiseLocalEvent(ent, ref getEntEv, true); + + if (!getEntEv.Handled) + { + throw new InvalidOperationException($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player."); + } + + antagEnt = getEntEv.Entity; + } + + if (antagEnt is not { } player) return; - if (mindComponent.Session == null) - return; + var getPosEv = new AntagSelectLocationEvent(session, ent); + RaiseLocalEvent(ent, ref getPosEv, true); + if (getPosEv.Handled) + { + var playerXform = Transform(player); + var pos = RobustRandom.Pick(getPosEv.Coordinates); + var mapEnt = _map.GetMap(pos.MapId); + _transform.SetMapCoordinates((player, playerXform), pos); + } - SendBriefing(mindComponent.Session, briefing, briefingColor, briefingSound); + if (isSpawner) + { + if (!TryComp(player, out var spawnerComp)) + { + Log.Error("Antag spawner with GhostRoleAntagSpawnerComponent."); + return; + } + + spawnerComp.Rule = ent; + spawnerComp.Definition = def; + return; + } + + EntityManager.AddComponents(player, def.Components); + _stationSpawning.EquipStartingGear(player, def.StartingGear); + + if (session != null) + { + var curMind = session.GetMind(); + if (curMind == null) + { + curMind = _mind.CreateMind(session.UserId, Name(antagEnt.Value)); + _mind.SetUserId(curMind.Value, session.UserId); + } + + EntityManager.AddComponents(curMind.Value, def.MindComponents); + _mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true); + ent.Comp.SelectedMinds.Add((curMind.Value, Name(player))); + } + + if (def.Briefing is { } briefing) + { + SendBriefing(session, briefing); + } + + var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def); + RaiseLocalEvent(ent, ref afterEv, true); } /// - /// Helper method to send the briefing text and sound to a list of sessions + /// Gets an ordered player pool based on player preferences and the antagonist definition. /// - /// - /// - /// - /// - - public void SendBriefing(List sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) + public AntagSelectionPlayerPool GetPlayerPool(Entity ent, List sessions, AntagSelectionDefinition def) { + var primaryList = new List(); + var secondaryList = new List(); + var fallbackList = new List(); + var rawList = new List(); foreach (var session in sessions) { - SendBriefing(session, briefing, briefingColor, briefingSound); - } - } - /// - /// Helper method to send the briefing text and sound to a session - /// - /// The player chosen to be an antag - /// The briefing text to send - /// The color the briefing should be, null for default - /// The sound to briefing/greeting sound to play + if (!IsSessionValid(ent, session, def) || + !IsEntityValid(session.AttachedEntity, def)) + { + rawList.Add(session); + continue; + } - public void SendBriefing(ICommonSession session, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) - { - _audioSystem.PlayGlobal(briefingSound, session); - var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing)); - ChatManager.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel, briefingColor); + var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter; + if (def.PrefRoles.Count == 0 || pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p))) + { + primaryList.Add(session); + } + else if (def.PrefRoles.Count == 0 || pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p))) + { + secondaryList.Add(session); + } + else + { + fallbackList.Add(session); + } + } + + return new AntagSelectionPlayerPool(primaryList, secondaryList, fallbackList, rawList); + } + + /// + /// Checks if a given session is valid for an antagonist. + /// + public bool IsSessionValid(Entity ent, ICommonSession session, AntagSelectionDefinition def, EntityUid? mind = null) + { + mind ??= session.GetMind(); + + if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie) + return false; + + if (ent.Comp.SelectedSessions.Contains(session)) + return false; + + //todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds) + + switch (def.MultiAntagSetting) + { + case AntagAcceptability.None: + { + if (_role.MindIsAntagonist(mind)) + return false; + break; + } + case AntagAcceptability.NotExclusive: + { + if (_role.MindIsExclusiveAntagonist(mind)) + return false; + break; + } + } + + // todo: expand this to allow for more fine antag-selection logic for game rules. + if (!_jobs.CanBeAntag(session)) + return false; + + return true; + } + + /// + /// Checks if a given entity (mind/session not included) is valid for a given antagonist. + /// + private bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def) + { + if (entity == null) + return false; + + if (HasComp(entity)) + return false; + + if (!def.AllowNonHumans && !HasComp(entity)) + return false; + + if (def.Whitelist != null) + { + if (!def.Whitelist.IsValid(entity.Value, EntityManager)) + return false; + } + + if (def.Blacklist != null) + { + if (def.Blacklist.IsValid(entity.Value, EntityManager)) + return false; + } + + return true; } - #endregion } + +/// +/// Event raised on a game rule entity in order to determine what the antagonist entity will be. +/// Only raised if the selected player's current entity is invalid. +/// +[ByRefEvent] +public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity GameRule) +{ + public readonly ICommonSession? Session = Session; + + public bool Handled => Entity != null; + + public EntityUid? Entity; +} + +/// +/// Event raised on a game rule entity to determine the location for the antagonist. +/// +[ByRefEvent] +public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity GameRule) +{ + public readonly ICommonSession? Session = Session; + + public bool Handled => Coordinates.Any(); + + public List Coordinates = new(); +} + +/// +/// Event raised on a game rule entity after the setup logic for an antag is complete. +/// Used for applying additional more complex setup logic. +/// +[ByRefEvent] +public readonly record struct AfterAntagEntitySelectedEvent(ICommonSession? Session, EntityUid EntityUid, Entity GameRule, AntagSelectionDefinition Def); diff --git a/Content.Server/Antag/Components/AntagSelectionComponent.cs b/Content.Server/Antag/Components/AntagSelectionComponent.cs new file mode 100644 index 0000000000..096be14049 --- /dev/null +++ b/Content.Server/Antag/Components/AntagSelectionComponent.cs @@ -0,0 +1,189 @@ +using Content.Server.Administration.Systems; +using Content.Server.Destructible.Thresholds; +using Content.Shared.Antag; +using Content.Shared.Roles; +using Content.Shared.Storage; +using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server.Antag.Components; + +[RegisterComponent, Access(typeof(AntagSelectionSystem), typeof(AdminVerbSystem))] +public sealed partial class AntagSelectionComponent : Component +{ + /// + /// Has the primary selection of antagonists finished yet? + /// + [DataField] + public bool SelectionsComplete; + + /// + /// The definitions for the antagonists + /// + [DataField] + public List Definitions = new(); + + /// + /// The minds and original names of the players selected to be antagonists. + /// + [DataField] + public List<(EntityUid, string)> SelectedMinds = new(); + + /// + /// When the antag selection will occur. + /// + [DataField] + public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn; + + /// + /// Cached sessions of players who are chosen. Used so we don't have to rebuild the pool multiple times in a tick. + /// Is not serialized. + /// + public HashSet SelectedSessions = new(); +} + +[DataDefinition] +public partial struct AntagSelectionDefinition() +{ + /// + /// A list of antagonist roles that are used for selecting which players will be antagonists. + /// + [DataField] + public List> PrefRoles = new(); + + /// + /// Fallback for . Useful if you need multiple role preferences for a team antagonist. + /// + [DataField] + public List> FallbackRoles = new(); + + /// + /// Should we allow people who already have an antagonist role? + /// + [DataField] + public AntagAcceptability MultiAntagSetting = AntagAcceptability.None; + + /// + /// The minimum number of this antag. + /// + [DataField] + public int Min = 1; + + /// + /// The maximum number of this antag. + /// + [DataField] + public int Max = 1; + + /// + /// A range used to randomly select + /// + [DataField] + public MinMax? MinRange; + + /// + /// A range used to randomly select + /// + [DataField] + public MinMax? MaxRange; + + /// + /// a player to antag ratio: used to determine the amount of antags that will be present. + /// + [DataField] + public int PlayerRatio = 10; + + /// + /// Whether or not players should be picked to inhabit this antag or not. + /// + [DataField] + public bool PickPlayer = true; + + /// + /// If true, players that latejoin into a round have a chance of being converted into antagonists. + /// + [DataField] + public bool LateJoinAdditional = false; + + //todo: find out how to do this with minimal boilerplate: filler department, maybe? + //public HashSet> JobBlacklist = new() + + /// + /// Mostly just here for legacy compatibility and reducing boilerplate + /// + [DataField] + public bool AllowNonHumans = false; + + /// + /// A whitelist for selecting which players can become this antag. + /// + [DataField] + public EntityWhitelist? Whitelist; + + /// + /// A blacklist for selecting which players can become this antag. + /// + [DataField] + public EntityWhitelist? Blacklist; + + /// + /// Components added to the player. + /// + [DataField] + public ComponentRegistry Components = new(); + + /// + /// Components added to the player's mind. + /// + [DataField] + public ComponentRegistry MindComponents = new(); + + /// + /// A set of starting gear that's equipped to the player. + /// + [DataField] + public ProtoId? StartingGear; + + /// + /// A briefing shown to the player. + /// + [DataField] + public BriefingData? Briefing; + + /// + /// A spawner used to defer the selection of this particular definition. + /// + /// + /// Not the cleanest way of doing this code but it's just an odd specific behavior. + /// Sue me. + /// + [DataField] + public EntProtoId? SpawnerPrototype; +} + +/// +/// Contains data used to generate a briefing. +/// +[DataDefinition] +public partial struct BriefingData +{ + /// + /// The text shown + /// + [DataField] + public LocId? Text; + + /// + /// The color of the text. + /// + [DataField] + public Color? Color; + + /// + /// The sound played. + /// + [DataField] + public SoundSpecifier? Sound; +} diff --git a/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs b/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs new file mode 100644 index 0000000000..fcaa4d4267 --- /dev/null +++ b/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.Antag.Components; + +/// +/// Ghost role spawner that creates an antag for the associated gamerule. +/// +[RegisterComponent, Access(typeof(AntagSelectionSystem))] +public sealed partial class GhostRoleAntagSpawnerComponent : Component +{ + [DataField] + public EntityUid? Rule; + + [DataField] + public AntagSelectionDefinition? Definition; +} diff --git a/Content.Server/Antag/MobReplacementRuleSystem.cs b/Content.Server/Antag/MobReplacementRuleSystem.cs index 2446b976e1..18837b5a7c 100644 --- a/Content.Server/Antag/MobReplacementRuleSystem.cs +++ b/Content.Server/Antag/MobReplacementRuleSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Antag.Mimic; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; using Content.Shared.VendingMachines; diff --git a/Content.Server/Destructible/Thresholds/MinMax.cs b/Content.Server/Destructible/Thresholds/MinMax.cs index b438e7c0e8..c44864183a 100644 --- a/Content.Server/Destructible/Thresholds/MinMax.cs +++ b/Content.Server/Destructible/Thresholds/MinMax.cs @@ -1,4 +1,6 @@ -namespace Content.Server.Destructible.Thresholds +using Robust.Shared.Random; + +namespace Content.Server.Destructible.Thresholds { [Serializable] [DataDefinition] @@ -9,5 +11,16 @@ [DataField("max")] public int Max; + + public MinMax(int min, int max) + { + Min = min; + Max = max; + } + + public int Next(IRobustRandom random) + { + return random.Next(Min, Max + 1); + } } } diff --git a/Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs b/Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs similarity index 84% rename from Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs rename to Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs index 956768bdd9..b9e6fa5d4b 100644 --- a/Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs +++ b/Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Server.GameTicking.Components; /// /// Added to game rules before and removed before . diff --git a/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs b/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs new file mode 100644 index 0000000000..de4be83627 --- /dev/null +++ b/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server.GameTicking.Components; + +/// +/// Generic component used to track a gamerule that's start has been delayed. +/// +[RegisterComponent, AutoGenerateComponentPause] +public sealed partial class DelayedStartRuleComponent : Component +{ + /// + /// The time at which the rule will start properly. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField] + public TimeSpan RuleStartTime; +} diff --git a/Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs b/Content.Server/GameTicking/Components/EndedGameRuleComponent.cs similarity index 81% rename from Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs rename to Content.Server/GameTicking/Components/EndedGameRuleComponent.cs index 4484abd4d0..3234bfff3a 100644 --- a/Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs +++ b/Content.Server/GameTicking/Components/EndedGameRuleComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Server.GameTicking.Components; /// /// Added to game rules before . diff --git a/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs b/Content.Server/GameTicking/Components/GameRuleComponent.cs similarity index 83% rename from Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs rename to Content.Server/GameTicking/Components/GameRuleComponent.cs index 6309b97402..1e6c3f0ab1 100644 --- a/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs +++ b/Content.Server/GameTicking/Components/GameRuleComponent.cs @@ -1,6 +1,7 @@ +using Content.Server.Destructible.Thresholds; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Server.GameTicking.Components; /// /// Component attached to all gamerule entities. @@ -20,6 +21,12 @@ public sealed partial class GameRuleComponent : Component /// [DataField] public int MinPlayers; + + /// + /// A delay for when the rule the is started and when the starting logic actually runs. + /// + [DataField] + public MinMax? Delay; } /// diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs index 4ebe946af4..f52a3cb296 100644 --- a/Content.Server/GameTicking/GameTicker.GameRule.cs +++ b/Content.Server/GameTicking/GameTicker.GameRule.cs @@ -1,6 +1,6 @@ using System.Linq; using Content.Server.Administration; -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Components; using Content.Shared.Administration; using Content.Shared.Database; using Content.Shared.Prototypes; @@ -102,6 +102,22 @@ public sealed partial class GameTicker if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up return false; + // If we already have it, then we just skip the delay as it has already happened. + if (!RemComp(ruleEntity) && ruleData.Delay != null) + { + var delayTime = TimeSpan.FromSeconds(ruleData.Delay.Value.Next(_robustRandom)); + + if (delayTime > TimeSpan.Zero) + { + _sawmill.Info($"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}"); + _adminLogger.Add(LogType.EventStarted, $"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}"); + + var delayed = EnsureComp(ruleEntity); + delayed.RuleStartTime = _gameTiming.CurTime + (delayTime); + return true; + } + } + _allPreviousGameRules.Add((RoundDuration(), id)); _sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}"); _adminLogger.Add(LogType.EventStarted, $"Started game rule {ToPrettyString(ruleEntity)}"); @@ -255,6 +271,18 @@ public sealed partial class GameTicker } } + private void UpdateGameRules() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var delay, out var rule)) + { + if (_gameTiming.CurTime < delay.RuleStartTime) + continue; + + StartGameRule(uid, rule); + } + } + #region Command Implementations [AdminCommand(AdminFlags.Fun)] @@ -323,38 +351,3 @@ public sealed partial class GameTicker #endregion } - -/* -/// -/// Raised broadcast when a game rule is selected, but not started yet. -/// -public sealed class GameRuleAddedEvent -{ - public GameRulePrototype Rule { get; } - - public GameRuleAddedEvent(GameRulePrototype rule) - { - Rule = rule; - } -} - -public sealed class GameRuleStartedEvent -{ - public GameRulePrototype Rule { get; } - - public GameRuleStartedEvent(GameRulePrototype rule) - { - Rule = rule; - } -} - -public sealed class GameRuleEndedEvent -{ - public GameRulePrototype Rule { get; } - - public GameRuleEndedEvent(GameRulePrototype rule) - { - Rule = rule; - } -} -*/ diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index efda3df0ca..fa23312268 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -133,6 +133,7 @@ namespace Content.Server.GameTicking return; base.Update(frameTime); UpdateRoundFlow(frameTime); + UpdateGameRules(); } } } diff --git a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs new file mode 100644 index 0000000000..463aecbff5 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs @@ -0,0 +1,29 @@ +using Content.Server.Maps; +using Content.Shared.Whitelist; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// This is used for a game rule that loads a map when activated. +/// +[RegisterComponent] +public sealed partial class LoadMapRuleComponent : Component +{ + [DataField] + public MapId? Map; + + [DataField] + public ProtoId? GameMap ; + + [DataField] + public ResPath? MapPath; + + [DataField] + public List MapGrids = new(); + + [DataField] + public EntityWhitelist? SpawnerWhitelist; +} diff --git a/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs index e6966c1e37..fa352eb320 100644 --- a/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs @@ -8,7 +8,7 @@ namespace Content.Server.GameTicking.Rules.Components; /// /// Stores some configuration used by the ninja system. -/// Objectives and roundend summary are handled by . +/// Objectives and roundend summary are handled by . /// [RegisterComponent, Access(typeof(SpaceNinjaSystem))] public sealed partial class NinjaRuleComponent : Component diff --git a/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs index e02d90c18b..bb1b7c8746 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs @@ -1,6 +1,3 @@ -using Content.Shared.Roles; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - namespace Content.Server.GameTicking.Rules.Components; /// @@ -9,11 +6,5 @@ namespace Content.Server.GameTicking.Rules.Components; /// TODO: Remove once systems can request spawns from the ghost role system directly. /// [RegisterComponent] -public sealed partial class NukeOperativeSpawnerComponent : Component -{ - [DataField("name", required:true)] - public string OperativeName = default!; +public sealed partial class NukeOperativeSpawnerComponent : Component; - [DataField] - public NukeopSpawnPreset SpawnDetails = default!; -} diff --git a/Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs index 358b157cdf..3d097cd7c7 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs @@ -6,4 +6,6 @@ [RegisterComponent] public sealed partial class NukeOpsShuttleComponent : Component { + [DataField] + public EntityUid AssociatedRule; } diff --git a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs index 8f11e70560..e05c3e5db6 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs @@ -1,27 +1,16 @@ -using Content.Server.Maps; using Content.Server.RoundEnd; -using Content.Server.StationEvents.Events; using Content.Shared.Dataset; using Content.Shared.NPC.Prototypes; using Content.Shared.Roles; -using Robust.Shared.Map; +using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Server.GameTicking.Rules.Components; -[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))] +[RegisterComponent, Access(typeof(NukeopsRuleSystem))] public sealed partial class NukeopsRuleComponent : Component { - /// - /// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative - /// - [DataField] - public int PlayersPerOperative = 10; - - [DataField] - public int MaxOps = 5; - /// /// What will happen if all of the nuclear operatives will die. Used by LoneOpsSpawn event. /// @@ -52,12 +41,6 @@ public sealed partial class NukeopsRuleComponent : Component [DataField] public TimeSpan EvacShuttleTime = TimeSpan.FromMinutes(3); - /// - /// Whether or not to spawn the nuclear operative outpost. Used by LoneOpsSpawn event. - /// - [DataField] - public bool SpawnOutpost = true; - /// /// Whether or not nukie left their outpost /// @@ -80,7 +63,7 @@ public sealed partial class NukeopsRuleComponent : Component /// This amount of TC will be given to each nukie /// [DataField] - public int WarTCAmountPerNukie = 40; + public int WarTcAmountPerNukie = 40; /// /// Delay between war declaration and nuke ops arrival on station map. Gives crew time to prepare @@ -94,50 +77,23 @@ public sealed partial class NukeopsRuleComponent : Component [DataField] public int WarDeclarationMinOps = 4; - [DataField] - public EntProtoId SpawnPointProto = "SpawnPointNukies"; - - [DataField] - public EntProtoId GhostSpawnPointProto = "SpawnPointGhostNukeOperative"; - - [DataField] - public string OperationName = "Test Operation"; - - [DataField] - public ProtoId OutpostMapPrototype = "NukieOutpost"; - [DataField] public WinType WinType = WinType.Neutral; [DataField] public List WinConditions = new (); - // TODO full game save - // TODO: use components, don't just cache entity UIDs - // There have been (and probably still are) bugs where these refer to deleted entities from old rounds. - // Whenever this gets fixed, update NukiesTest. - public EntityUid? NukieOutpost; - public EntityUid? NukieShuttle; + [DataField] public EntityUid? TargetStation; - public MapId? NukiePlanet; + + [DataField] + public ProtoId Faction = "Syndicate"; /// - /// Data to be used in for an operative once the Mind has been added. + /// Path to antagonist alert sound. /// [DataField] - public Dictionary OperativeMindPendingData = new(); - - [DataField(required: true)] - public ProtoId Faction; - - [DataField] - public NukeopSpawnPreset CommanderSpawnDetails = new() { AntagRoleProto = "NukeopsCommander", GearProto = "SyndicateCommanderGearFull", NamePrefix = "nukeops-role-commander", NameList = "SyndicateNamesElite" }; - - [DataField] - public NukeopSpawnPreset AgentSpawnDetails = new() { AntagRoleProto = "NukeopsMedic", GearProto = "SyndicateOperativeMedicFull", NamePrefix = "nukeops-role-agent", NameList = "SyndicateNamesNormal" }; - - [DataField] - public NukeopSpawnPreset OperativeSpawnDetails = new(); + public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg"); } /// diff --git a/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs deleted file mode 100644 index 1d03b41d77..0000000000 --- a/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Robust.Shared.Audio; - -namespace Content.Server.GameTicking.Rules.Components; - -[RegisterComponent, Access(typeof(PiratesRuleSystem))] -public sealed partial class PiratesRuleComponent : Component -{ - [ViewVariables] - public List Pirates = new(); - [ViewVariables] - public EntityUid PirateShip = EntityUid.Invalid; - [ViewVariables] - public HashSet InitialItems = new(); - [ViewVariables] - public double InitialShipValue; - - /// - /// Path to antagonist alert sound. - /// - [DataField("pirateAlertSound")] - public SoundSpecifier PirateAlertSound = new SoundPathSpecifier( - "/Audio/Ambience/Antag/pirate_start.ogg", - AudioParams.Default.WithVolume(4)); -} diff --git a/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs index 2ce3f1f9a6..3b19bbffb6 100644 --- a/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs @@ -22,43 +22,6 @@ public sealed partial class RevolutionaryRuleComponent : Component [DataField] public TimeSpan TimerWait = TimeSpan.FromSeconds(20); - /// - /// Stores players minds - /// - [DataField] - public Dictionary HeadRevs = new(); - - [DataField] - public ProtoId HeadRevPrototypeId = "HeadRev"; - - /// - /// Min players needed for Revolutionary gamemode to start. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public int MinPlayers = 15; - - /// - /// Max Head Revs allowed during selection. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public int MaxHeadRevs = 3; - - /// - /// The amount of Head Revs that will spawn per this amount of players. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public int PlayersPerHeadRev = 15; - - /// - /// The gear head revolutionaries are given on spawn. - /// - [DataField] - public List StartingGear = new() - { - "Flash", - "ClothingEyesGlassesSunglasses" - }; - /// /// The time it takes after the last head is killed for the shuttle to arrive. /// diff --git a/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs index 9dfd6e6627..01a078625a 100644 --- a/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs @@ -1,12 +1,11 @@ using Content.Shared.Random; -using Content.Shared.Roles; using Robust.Shared.Audio; using Robust.Shared.Prototypes; namespace Content.Server.GameTicking.Rules.Components; /// -/// Stores data for . +/// Stores data for . /// [RegisterComponent, Access(typeof(ThiefRuleSystem))] public sealed partial class ThiefRuleComponent : Component @@ -23,42 +22,9 @@ public sealed partial class ThiefRuleComponent : Component [DataField] public float BigObjectiveChance = 0.7f; - /// - /// Add a Pacified comp to thieves - /// - [DataField] - public bool PacifistThieves = true; - - [DataField] - public ProtoId ThiefPrototypeId = "Thief"; - [DataField] public float MaxObjectiveDifficulty = 2.5f; [DataField] public int MaxStealObjectives = 10; - - /// - /// Things that will be given to thieves - /// - [DataField] - public List StarterItems = new() { "ToolboxThief", "ClothingHandsChameleonThief" }; - - /// - /// All Thieves created by this rule - /// - [DataField] - public List ThievesMinds = new(); - - /// - /// Max Thiefs created by rule on roundstart - /// - [DataField] - public int MaxAllowThief = 3; - - /// - /// Sound played when making the player a thief via antag control or ghost role - /// - [DataField] - public SoundSpecifier? GreetingSound = new SoundPathSpecifier("/Audio/Misc/thief_greeting.ogg"); } diff --git a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs index e904d8a7c2..ea5c9a830b 100644 --- a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs @@ -57,4 +57,19 @@ public sealed partial class TraitorRuleComponent : Component /// [DataField] public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg"); + + /// + /// The amount of codewords that are selected. + /// + [DataField] + public int CodewordCount = 4; + + /// + /// The amount of TC traitors start with. + /// + [DataField] + public int StartingBalance = 20; + + [DataField] + public int MaxDifficulty = 20; } diff --git a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs index 4fe91e3a5f..59d1940eaf 100644 --- a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs @@ -8,12 +8,6 @@ namespace Content.Server.GameTicking.Rules.Components; [RegisterComponent, Access(typeof(ZombieRuleSystem))] public sealed partial class ZombieRuleComponent : Component { - [DataField] - public Dictionary InitialInfectedNames = new(); - - [DataField] - public ProtoId PatientZeroPrototypeId = "InitialInfected"; - /// /// When the round will next check for round end. /// @@ -26,61 +20,9 @@ public sealed partial class ZombieRuleComponent : Component [DataField] public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(30); - /// - /// The time at which the initial infected will be chosen. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] - public TimeSpan? StartTime; - - /// - /// The minimum amount of time after the round starts that the initial infected will be chosen. - /// - [DataField] - public TimeSpan MinStartDelay = TimeSpan.FromMinutes(10); - - /// - /// The maximum amount of time after the round starts that the initial infected will be chosen. - /// - [DataField] - public TimeSpan MaxStartDelay = TimeSpan.FromMinutes(15); - - /// - /// The sound that plays when someone becomes an initial infected. - /// todo: this should have a unique sound instead of reusing the zombie one. - /// - [DataField] - public SoundSpecifier InitialInfectedSound = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg"); - - /// - /// The minimum amount of time initial infected have before they start taking infection damage. - /// - [DataField] - public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f); - - /// - /// The maximum amount of time initial infected have before they start taking damage. - /// - [DataField] - public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f); - - /// - /// How many players for each initial infected. - /// - [DataField] - public int PlayersPerInfected = 10; - - /// - /// The maximum number of initial infected. - /// - [DataField] - public int MaxInitialInfected = 6; - /// /// After this amount of the crew become zombies, the shuttle will be automatically called. /// [DataField] public float ZombieShuttleCallPercentage = 0.7f; - - [DataField] - public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead"; } diff --git a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs index 82ac755592..78b8a8a85c 100644 --- a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Administration.Commands; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.KillTracking; using Content.Server.Mind; @@ -33,7 +34,6 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem(OnSpawnComplete); SubscribeLocalEvent(OnKillReported); SubscribeLocalEvent(OnPointChanged); - SubscribeLocalEvent(OnRoundEndTextAppend); } private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev) @@ -113,21 +113,17 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem(); - while (query.MoveNext(out var uid, out var dm, out var point, out var rule)) - { - if (!GameTicker.IsGameRuleAdded(uid, rule)) - continue; + if (!TryComp(uid, out var point)) + return; - if (dm.Victor != null && _player.TryGetPlayerData(dm.Victor.Value, out var data)) - { - ev.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName))); - ev.AddLine(""); - } - ev.AddLine(Loc.GetString("point-scoreboard-header")); - ev.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup()); + if (component.Victor != null && _player.TryGetPlayerData(component.Victor.Value, out var data)) + { + args.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName))); + args.AddLine(""); } + args.AddLine(Loc.GetString("point-scoreboard-header")); + args.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup()); } } diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs index a60a2bfe22..4534333417 100644 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Components; using Robust.Shared.Collections; @@ -15,31 +16,6 @@ public abstract partial class GameRuleSystem where T: IComponent return EntityQueryEnumerator(); } - protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out _, out _, out _, out var gameRule)) - { - var minPlayers = gameRule.MinPlayers; - if (!ev.Forced && ev.Players.Length < minPlayers) - { - ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players", - ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers), - ("presetName", localizedPresetName))); - ev.Cancel(); - continue; - } - - if (ev.Players.Length == 0) - { - ChatManager.DispatchServerAnnouncement(Loc.GetString("preset-no-one-ready")); - ev.Cancel(); - } - } - - return !ev.Cancelled; - } - /// /// Utility function for finding a random event-eligible station entity /// diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.cs index 363c2ad7f7..bcad146c22 100644 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.cs @@ -1,6 +1,6 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Chat.Managers; -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Components; using Robust.Server.GameObjects; using Robust.Shared.Random; using Robust.Shared.Timing; @@ -22,9 +22,31 @@ public abstract partial class GameRuleSystem : EntitySystem where T : ICompon { base.Initialize(); + SubscribeLocalEvent(OnStartAttempt); SubscribeLocalEvent(OnGameRuleAdded); SubscribeLocalEvent(OnGameRuleStarted); SubscribeLocalEvent(OnGameRuleEnded); + SubscribeLocalEvent(OnRoundEndTextAppend); + } + + private void OnStartAttempt(RoundStartAttemptEvent args) + { + if (args.Forced || args.Cancelled) + return; + + var query = QueryActiveRules(); + while (query.MoveNext(out var uid, out _, out _, out var gameRule)) + { + var minPlayers = gameRule.MinPlayers; + if (args.Players.Length >= minPlayers) + continue; + + ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players", + ("readyPlayersCount", args.Players.Length), + ("minimumPlayers", minPlayers), + ("presetName", ToPrettyString(uid)))); + args.Cancel(); + } } private void OnGameRuleAdded(EntityUid uid, T component, ref GameRuleAddedEvent args) @@ -48,6 +70,12 @@ public abstract partial class GameRuleSystem : EntitySystem where T : ICompon Ended(uid, component, ruleData, args); } + private void OnRoundEndTextAppend(Entity ent, ref RoundEndTextAppendEvent args) + { + if (!TryComp(ent, out var ruleData)) + return; + AppendRoundEndText(ent, ent, ruleData, ref args); + } /// /// Called when the gamerule is added @@ -73,6 +101,14 @@ public abstract partial class GameRuleSystem : EntitySystem where T : ICompon } + /// + /// Called at the end of a round when text needs to be added for a game rule. + /// + protected virtual void AppendRoundEndText(EntityUid uid, T component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args) + { + + } + /// /// Called on an active gamerule entity in the Update function /// diff --git a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs index b775b7af56..01fa387595 100644 --- a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs @@ -1,5 +1,6 @@ using System.Threading; using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Robust.Server.Player; using Robust.Shared.Player; diff --git a/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs b/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs index 01fd97d9a7..3da55e30c9 100644 --- a/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.KillTracking; using Content.Shared.Chat; diff --git a/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs new file mode 100644 index 0000000000..aba9ed9e58 --- /dev/null +++ b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs @@ -0,0 +1,80 @@ +using Content.Server.Antag; +using Content.Server.GameTicking.Components; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Spawners.Components; +using Robust.Server.GameObjects; +using Robust.Server.Maps; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameTicking.Rules; + +public sealed class LoadMapRuleSystem : GameRuleSystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly MapSystem _map = default!; + [Dependency] private readonly MapLoaderSystem _mapLoader = default!; + [Dependency] private readonly TransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnSelectLocation); + SubscribeLocalEvent(OnGridSplit); + } + + private void OnGridSplit(ref GridSplitEvent args) + { + var rule = QueryActiveRules(); + while (rule.MoveNext(out _, out var mapComp, out _)) + { + if (!mapComp.MapGrids.Contains(args.Grid)) + continue; + + mapComp.MapGrids.AddRange(args.NewGrids); + break; + } + } + + protected override void Added(EntityUid uid, LoadMapRuleComponent comp, GameRuleComponent rule, GameRuleAddedEvent args) + { + if (comp.Map != null) + return; + + _map.CreateMap(out var mapId); + comp.Map = mapId; + + if (comp.GameMap != null) + { + var gameMap = _prototypeManager.Index(comp.GameMap.Value); + comp.MapGrids.AddRange(GameTicker.LoadGameMap(gameMap, comp.Map.Value, new MapLoadOptions())); + } + else if (comp.MapPath != null) + { + if (_mapLoader.TryLoad(comp.Map.Value, comp.MapPath.Value.ToString(), out var roots, new MapLoadOptions { LoadMap = true })) + comp.MapGrids.AddRange(roots); + } + else + { + Log.Error($"No valid map prototype or map path associated with the rule {ToPrettyString(uid)}"); + } + } + + private void OnSelectLocation(Entity ent, ref AntagSelectLocationEvent args) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var xform)) + { + if (xform.MapID != ent.Comp.Map) + continue; + + if (xform.GridUid == null || !ent.Comp.MapGrids.Contains(xform.GridUid.Value)) + continue; + + if (ent.Comp.SpawnerWhitelist != null && !ent.Comp.SpawnerWhitelist.IsValid(uid, EntityManager)) + continue; + + args.Coordinates.Add(_transform.GetMapCoordinates(xform)); + } + } +} diff --git a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs index 2522ebb53b..cae99fee9f 100644 --- a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs @@ -1,5 +1,6 @@ using System.Threading; using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Timer = Robust.Shared.Timing.Timer; diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index d521e26396..2f8b9dc927 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -1,31 +1,20 @@ -using Content.Server.Administration.Commands; -using Content.Server.Administration.Managers; using Content.Server.Antag; using Content.Server.Communications; using Content.Server.GameTicking.Rules.Components; -using Content.Server.Ghost.Roles.Components; -using Content.Server.Ghost.Roles.Events; using Content.Server.Humanoid; -using Content.Server.Mind; using Content.Server.Nuke; using Content.Server.NukeOps; using Content.Server.Popups; using Content.Server.Preferences.Managers; -using Content.Server.RandomMetadata; using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Server.Shuttles.Events; using Content.Server.Shuttles.Systems; -using Content.Server.Spawners.Components; using Content.Server.Station.Components; -using Content.Server.Station.Systems; using Content.Server.Store.Components; using Content.Server.Store.Systems; -using Content.Shared.CCVar; -using Content.Shared.Dataset; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Prototypes; -using Content.Shared.Mind.Components; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.NPC.Components; @@ -33,45 +22,30 @@ using Content.Shared.NPC.Systems; using Content.Shared.Nuke; using Content.Shared.NukeOps; using Content.Shared.Preferences; -using Content.Shared.Roles; using Content.Shared.Store; using Content.Shared.Tag; using Content.Shared.Zombies; -using Robust.Server.Player; -using Robust.Shared.Configuration; using Robust.Shared.Map; -using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; using System.Linq; +using Content.Server.GameTicking.Components; namespace Content.Server.GameTicking.Rules; public sealed class NukeopsRuleSystem : GameRuleSystem { - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IServerPreferencesManager _prefs = default!; - [Dependency] private readonly IAdminManager _adminManager = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly EmergencyShuttleSystem _emergency = default!; [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; - [Dependency] private readonly RandomMetadataSystem _randomMetadata = default!; - [Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly NpcFactionSystem _npcFaction = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly RoundEndSystem _roundEndSystem = default!; - [Dependency] private readonly SharedRoleSystem _roles = default!; - [Dependency] private readonly StationSpawningSystem _stationSpawning = default!; [Dependency] private readonly StoreSystem _store = default!; [Dependency] private readonly TagSystem _tag = default!; - [Dependency] private readonly AntagSelectionSystem _antagSelection = default!; - - private ISawmill _sawmill = default!; [ValidatePrototypeId] private const string TelecrystalCurrencyPrototype = "Telecrystal"; @@ -79,141 +53,67 @@ public sealed class NukeopsRuleSystem : GameRuleSystem [ValidatePrototypeId] private const string NukeOpsUplinkTagPrototype = "NukeOpsUplink"; - [ValidatePrototypeId] - public const string NukeopsId = "Nukeops"; - - [ValidatePrototypeId] - private const string OperationPrefixDataset = "operationPrefix"; - - [ValidatePrototypeId] - private const string OperationSuffixDataset = "operationSuffix"; - public override void Initialize() { base.Initialize(); - _sawmill = _logManager.GetSawmill("NukeOps"); - - SubscribeLocalEvent(OnStartAttempt); - SubscribeLocalEvent(OnPlayersSpawning); - SubscribeLocalEvent(OnRoundEndText); SubscribeLocalEvent(OnNukeExploded); SubscribeLocalEvent(OnRunLevelChanged); SubscribeLocalEvent(OnNukeDisarm); SubscribeLocalEvent(OnComponentRemove); SubscribeLocalEvent(OnMobStateChanged); - SubscribeLocalEvent(OnPlayersGhostSpawning); - SubscribeLocalEvent(OnMindAdded); SubscribeLocalEvent(OnOperativeZombified); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShuttleFTLAttempt); SubscribeLocalEvent(OnWarDeclared); SubscribeLocalEvent(OnShuttleCallAttempt); + + SubscribeLocalEvent(OnAntagSelectEntity); + SubscribeLocalEvent(OnAfterAntagEntSelected); } protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { - base.Started(uid, component, gameRule, args); + var eligible = new List>(); + var eligibleQuery = EntityQueryEnumerator(); + while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member)) + { + if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member))) + continue; - if (GameTicker.RunLevel == GameRunLevel.InRound) - SpawnOperativesForGhostRoles(uid, component); + eligible.Add((eligibleUid, eligibleComp, member)); + } + + if (eligible.Count == 0) + return; + + component.TargetStation = RobustRandom.Pick(eligible); } #region Event Handlers - - private void OnStartAttempt(RoundStartAttemptEvent ev) + protected override void AppendRoundEndText(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule, + ref RoundEndTextAppendEvent args) { - TryRoundStartAttempt(ev, Loc.GetString("nukeops-title")); - } + var winText = Loc.GetString($"nukeops-{component.WinType.ToString().ToLower()}"); + args.AddLine(winText); - private void OnPlayersSpawning(RulePlayerSpawningEvent ev) - { - var query = QueryActiveRules(); - while (query.MoveNext(out var uid, out _, out var nukeops, out _)) + foreach (var cond in component.WinConditions) { - if (!SpawnMap((uid, nukeops))) - { - _sawmill.Info("Failed to load map for nukeops"); - continue; - } - - //Handle there being nobody readied up - if (ev.PlayerPool.Count == 0) - continue; - - var commanderEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.CommanderSpawnDetails.AntagRoleProto); - var agentEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.AgentSpawnDetails.AntagRoleProto); - var operativeEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.OperativeSpawnDetails.AntagRoleProto); - //Calculate how large the nukeops team needs to be - var nukiesToSelect = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, nukeops.PlayersPerOperative, nukeops.MaxOps); - - //Select Nukies - //Select Commander, priority : commanderEligible, agentEligible, operativeEligible, all players - var selectedCommander = _antagSelection.ChooseAntags(1, commanderEligible, agentEligible, operativeEligible, ev.PlayerPool).FirstOrDefault(); - //Select Agent, priority : agentEligible, operativeEligible, all players - var selectedAgent = _antagSelection.ChooseAntags(1, agentEligible, operativeEligible, ev.PlayerPool).FirstOrDefault(); - //Select Operatives, priority : operativeEligible, all players - var selectedOperatives = _antagSelection.ChooseAntags(nukiesToSelect - 2, operativeEligible, ev.PlayerPool); - - //Create the team! - //If the session is null, they will be spawned as ghost roles (provided the cvar is set) - var operatives = new List { new NukieSpawn(selectedCommander, nukeops.CommanderSpawnDetails) }; - if (nukiesToSelect > 1) - operatives.Add(new NukieSpawn(selectedAgent, nukeops.AgentSpawnDetails)); - - for (var i = 0; i < nukiesToSelect - 2; i++) - { - //Use up all available sessions first, then spawn the rest as ghost roles (if enabled) - if (selectedOperatives.Count > i) - { - operatives.Add(new NukieSpawn(selectedOperatives[i], nukeops.OperativeSpawnDetails)); - } - else - { - operatives.Add(new NukieSpawn(null, nukeops.OperativeSpawnDetails)); - } - } - - SpawnOperatives(operatives, _cfg.GetCVar(CCVars.NukeopsSpawnGhostRoles), nukeops); - - foreach (var nukieSpawn in operatives) - { - if (nukieSpawn.Session == null) - continue; - - GameTicker.PlayerJoinGame(nukieSpawn.Session); - } - } - } - - private void OnRoundEndText(RoundEndTextAppendEvent ev) - { - var ruleQuery = QueryActiveRules(); - while (ruleQuery.MoveNext(out _, out _, out var nukeops, out _)) - { - var winText = Loc.GetString($"nukeops-{nukeops.WinType.ToString().ToLower()}"); - ev.AddLine(winText); - - foreach (var cond in nukeops.WinConditions) - { - var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}"); - ev.AddLine(text); - } + var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}"); + args.AddLine(text); } - ev.AddLine(Loc.GetString("nukeops-list-start")); + args.AddLine(Loc.GetString("nukeops-list-start")); - var nukiesQuery = EntityQueryEnumerator(); - while (nukiesQuery.MoveNext(out var nukeopsUid, out _, out var mindContainer)) + var antags =_antag.GetAntagIdentifiers(uid); + + foreach (var (_, sessionData, name) in antags) { - if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer)) - continue; - - ev.AddLine(mind.Session != null - ? Loc.GetString("nukeops-list-name-user", ("name", Name(nukeopsUid)), ("user", mind.Session.Name)) - : Loc.GetString("nukeops-list-name", ("name", Name(nukeopsUid)))); + args.AddLine(Loc.GetString("nukeops-list-name-user", ("name", name), ("user", sessionData.UserName))); } } @@ -224,10 +124,10 @@ public sealed class NukeopsRuleSystem : GameRuleSystem { if (ev.OwningStation != null) { - if (ev.OwningStation == nukeops.NukieOutpost) + if (ev.OwningStation == GetOutpost(uid)) { nukeops.WinConditions.Add(WinCondition.NukeExplodedOnNukieOutpost); - SetWinType(uid, WinType.CrewMajor, nukeops); + SetWinType((uid, nukeops), WinType.CrewMajor); continue; } @@ -242,7 +142,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem } nukeops.WinConditions.Add(WinCondition.NukeExplodedOnCorrectStation); - SetWinType(uid, WinType.OpsMajor, nukeops); + SetWinType((uid, nukeops), WinType.OpsMajor); correctStation = true; } @@ -263,19 +163,85 @@ public sealed class NukeopsRuleSystem : GameRuleSystem private void OnRunLevelChanged(GameRunLevelChangedEvent ev) { + if (ev.New is not GameRunLevel.PostRound) + return; + var query = QueryActiveRules(); while (query.MoveNext(out var uid, out _, out var nukeops, out _)) { - switch (ev.New) + OnRoundEnd((uid, nukeops)); + } + } + + private void OnRoundEnd(Entity ent) + { + // If the win condition was set to operative/crew major win, ignore. + if (ent.Comp.WinType == WinType.OpsMajor || ent.Comp.WinType == WinType.CrewMajor) + return; + + var nukeQuery = AllEntityQuery(); + var centcomms = _emergency.GetCentcommMaps(); + + while (nukeQuery.MoveNext(out var nuke, out var nukeTransform)) + { + if (nuke.Status != NukeStatus.ARMED) + continue; + + // UH OH + if (nukeTransform.MapUid != null && centcomms.Contains(nukeTransform.MapUid.Value)) { - case GameRunLevel.InRound: - OnRoundStart(uid, nukeops); - break; - case GameRunLevel.PostRound: - OnRoundEnd(uid, nukeops); - break; + ent.Comp.WinConditions.Add(WinCondition.NukeActiveAtCentCom); + SetWinType((ent, ent), WinType.OpsMajor); + return; + } + + if (nukeTransform.GridUid == null || ent.Comp.TargetStation == null) + continue; + + if (!TryComp(ent.Comp.TargetStation.Value, out StationDataComponent? data)) + continue; + + foreach (var grid in data.Grids) + { + if (grid != nukeTransform.GridUid) + continue; + + ent.Comp.WinConditions.Add(WinCondition.NukeActiveInStation); + SetWinType(ent, WinType.OpsMajor); + return; } } + + if (_antag.AllAntagsAlive(ent.Owner)) + { + SetWinType(ent, WinType.OpsMinor); + ent.Comp.WinConditions.Add(WinCondition.AllNukiesAlive); + return; + } + + ent.Comp.WinConditions.Add(_antag.AnyAliveAntags(ent.Owner) + ? WinCondition.SomeNukiesAlive + : WinCondition.AllNukiesDead); + + var diskAtCentCom = false; + var diskQuery = AllEntityQuery(); + while (diskQuery.MoveNext(out _, out var transform)) + { + diskAtCentCom = transform.MapUid != null && centcomms.Contains(transform.MapUid.Value); + + // TODO: The target station should be stored, and the nuke disk should store its original station. + // This is fine for now, because we can assume a single station in base SS14. + break; + } + + // If the disk is currently at Central Command, the crew wins - just slightly. + // This also implies that some nuclear operatives have died. + SetWinType(ent, diskAtCentCom + ? WinType.CrewMinor + : WinType.OpsMinor); + ent.Comp.WinConditions.Add(diskAtCentCom + ? WinCondition.NukeDiskOnCentCom + : WinCondition.NukeDiskNotOnCentCom); } private void OnNukeDisarm(NukeDisarmSuccessEvent ev) @@ -294,66 +260,31 @@ public sealed class NukeopsRuleSystem : GameRuleSystem CheckRoundShouldEnd(); } - private void OnPlayersGhostSpawning(EntityUid uid, NukeOperativeComponent component, GhostRoleSpawnerUsedEvent args) - { - var spawner = args.Spawner; - - if (!TryComp(spawner, out var nukeOpSpawner)) - return; - - HumanoidCharacterProfile? profile = null; - if (TryComp(args.Spawned, out ActorComponent? actor)) - profile = _prefs.GetPreferences(actor.PlayerSession.UserId).SelectedCharacter as HumanoidCharacterProfile; - - // TODO: this is kinda awful for multi-nukies - foreach (var nukeops in EntityQuery()) - { - SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.SpawnDetails, profile); - - nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.SpawnDetails.AntagRoleProto); - } - } - - private void OnMindAdded(EntityUid uid, NukeOperativeComponent component, MindAddedMessage args) - { - if (!_mind.TryGetMind(uid, out var mindId, out var mind)) - return; - - var query = QueryActiveRules(); - while (query.MoveNext(out _, out _, out var nukeops, out _)) - { - if (nukeops.OperativeMindPendingData.TryGetValue(uid, out var role) || !nukeops.SpawnOutpost || - nukeops.RoundEndBehavior == RoundEndBehavior.Nothing) - { - role ??= nukeops.OperativeSpawnDetails.AntagRoleProto; - _roles.MindAddRole(mindId, new NukeopsRoleComponent { PrototypeId = role }); - nukeops.OperativeMindPendingData.Remove(uid); - } - - if (mind.Session is not { } playerSession) - return; - - if (GameTicker.RunLevel != GameRunLevel.InRound) - return; - - if (nukeops.TargetStation != null && !string.IsNullOrEmpty(Name(nukeops.TargetStation.Value))) - { - NotifyNukie(playerSession, component, nukeops); - } - } - } - private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args) { RemCompDeferred(uid, component); } + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + var map = Transform(ent).MapID; + + var rules = EntityQueryEnumerator(); + while (rules.MoveNext(out var uid, out _, out var mapRule)) + { + if (map != mapRule.Map) + continue; + ent.Comp.AssociatedRule = uid; + break; + } + } + private void OnShuttleFTLAttempt(ref ConsoleFTLAttemptEvent ev) { var query = QueryActiveRules(); - while (query.MoveNext(out _, out _, out var nukeops, out _)) + while (query.MoveNext(out var uid, out _, out var nukeops, out _)) { - if (ev.Uid != nukeops.NukieShuttle) + if (ev.Uid != GetShuttle((uid, nukeops))) continue; if (nukeops.WarDeclaredTime != null) @@ -397,12 +328,12 @@ public sealed class NukeopsRuleSystem : GameRuleSystem { // TODO: this is VERY awful for multi-nukies var query = QueryActiveRules(); - while (query.MoveNext(out _, out _, out var nukeops, out _)) + while (query.MoveNext(out var uid, out _, out var nukeops, out _)) { if (nukeops.WarDeclaredTime != null) continue; - if (Transform(ev.DeclaratorEntity).MapID != nukeops.NukiePlanet) + if (TryComp(uid, out var mapComp) && Transform(ev.DeclaratorEntity).MapID != mapComp.Map) continue; var newStatus = GetWarCondition(nukeops, ev.Status); @@ -448,161 +379,22 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (!_tag.HasTag(uid, NukeOpsUplinkTagPrototype)) continue; - if (!nukieRule.NukieOutpost.HasValue) + if (GetOutpost(uid) is not {} outpost) continue; - if (Transform(uid).MapID != Transform(nukieRule.NukieOutpost.Value).MapID) // Will receive bonus TC only on their start outpost + if (Transform(uid).MapID != Transform(outpost).MapID) // Will receive bonus TC only on their start outpost continue; - _store.TryAddCurrency(new () { { TelecrystalCurrencyPrototype, nukieRule.WarTCAmountPerNukie } }, uid, component); + _store.TryAddCurrency(new () { { TelecrystalCurrencyPrototype, nukieRule.WarTcAmountPerNukie } }, uid, component); var msg = Loc.GetString("store-currency-war-boost-given", ("target", uid)); _popupSystem.PopupEntity(msg, uid); } } - private void OnRoundStart(EntityUid uid, NukeopsRuleComponent? component = null) + private void SetWinType(Entity ent, WinType type, bool endRound = true) { - if (!Resolve(uid, ref component)) - return; - - // TODO: This needs to try and target a Nanotrasen station. At the very least, - // we can only currently guarantee that NT stations are the only station to - // exist in the base game. - - var eligible = new List>(); - var eligibleQuery = EntityQueryEnumerator(); - while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member)) - { - if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member))) - continue; - - eligible.Add((eligibleUid, eligibleComp, member)); - } - - if (eligible.Count == 0) - return; - - component.TargetStation = RobustRandom.Pick(eligible); - component.OperationName = _randomMetadata.GetRandomFromSegments([OperationPrefixDataset, OperationSuffixDataset], " "); - - var filter = Filter.Empty(); - var query = EntityQueryEnumerator(); - while (query.MoveNext(out _, out var nukeops, out var actor)) - { - NotifyNukie(actor.PlayerSession, nukeops, component); - filter.AddPlayer(actor.PlayerSession); - } - } - - private void OnRoundEnd(EntityUid uid, NukeopsRuleComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - // If the win condition was set to operative/crew major win, ignore. - if (component.WinType == WinType.OpsMajor || component.WinType == WinType.CrewMajor) - return; - - var nukeQuery = AllEntityQuery(); - var centcomms = _emergency.GetCentcommMaps(); - - while (nukeQuery.MoveNext(out var nuke, out var nukeTransform)) - { - if (nuke.Status != NukeStatus.ARMED) - continue; - - // UH OH - if (nukeTransform.MapUid != null && centcomms.Contains(nukeTransform.MapUid.Value)) - { - component.WinConditions.Add(WinCondition.NukeActiveAtCentCom); - SetWinType(uid, WinType.OpsMajor, component); - return; - } - - if (nukeTransform.GridUid == null || component.TargetStation == null) - continue; - - if (!TryComp(component.TargetStation.Value, out StationDataComponent? data)) - continue; - - foreach (var grid in data.Grids) - { - if (grid != nukeTransform.GridUid) - continue; - - component.WinConditions.Add(WinCondition.NukeActiveInStation); - SetWinType(uid, WinType.OpsMajor, component); - return; - } - } - - var allAlive = true; - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var nukeopsUid, out _, out var mindContainer, out var mobState)) - { - // mind got deleted somehow so ignore it - if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer)) - continue; - - // check if player got gibbed or ghosted or something - count as dead - if (mind.OwnedEntity != null && - // if the player somehow isn't a mob anymore that also counts as dead - // have to be alive, not crit or dead - mobState.CurrentState is MobState.Alive) - { - continue; - } - - allAlive = false; - break; - } - - // If all nuke ops were alive at the end of the round, - // the nuke ops win. This is to prevent people from - // running away the moment nuke ops appear. - if (allAlive) - { - SetWinType(uid, WinType.OpsMinor, component); - component.WinConditions.Add(WinCondition.AllNukiesAlive); - return; - } - - component.WinConditions.Add(WinCondition.SomeNukiesAlive); - - var diskAtCentCom = false; - var diskQuery = AllEntityQuery(); - - while (diskQuery.MoveNext(out _, out var transform)) - { - diskAtCentCom = transform.MapUid != null && centcomms.Contains(transform.MapUid.Value); - - // TODO: The target station should be stored, and the nuke disk should store its original station. - // This is fine for now, because we can assume a single station in base SS14. - break; - } - - // If the disk is currently at Central Command, the crew wins - just slightly. - // This also implies that some nuclear operatives have died. - if (diskAtCentCom) - { - SetWinType(uid, WinType.CrewMinor, component); - component.WinConditions.Add(WinCondition.NukeDiskOnCentCom); - } - // Otherwise, the nuke ops win. - else - { - SetWinType(uid, WinType.OpsMinor, component); - component.WinConditions.Add(WinCondition.NukeDiskNotOnCentCom); - } - } - - private void SetWinType(EntityUid uid, WinType type, NukeopsRuleComponent? component = null, bool endRound = true) - { - if (!Resolve(uid, ref component)) - return; - - component.WinType = type; + ent.Comp.WinType = type; if (endRound && (type == WinType.CrewMajor || type == WinType.OpsMajor)) _roundEndSystem.EndRound(); @@ -613,243 +405,130 @@ public sealed class NukeopsRuleSystem : GameRuleSystem var query = QueryActiveRules(); while (query.MoveNext(out var uid, out _, out var nukeops, out _)) { - if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor) - continue; + CheckRoundShouldEnd((uid, nukeops)); + } + } - // If there are any nuclear bombs that are active, immediately return. We're not over yet. - var armed = false; - foreach (var nuke in EntityQuery()) - { - if (nuke.Status == NukeStatus.ARMED) - { - armed = true; - break; - } - } - if (armed) - continue; + private void CheckRoundShouldEnd(Entity ent) + { + var nukeops = ent.Comp; - MapId? shuttleMapId = Exists(nukeops.NukieShuttle) - ? Transform(nukeops.NukieShuttle.Value).MapID + if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor) + return; + + + // If there are any nuclear bombs that are active, immediately return. We're not over yet. + foreach (var nuke in EntityQuery()) + { + if (nuke.Status == NukeStatus.ARMED) + return; + } + + var shuttle = GetShuttle((ent, ent)); + + MapId? shuttleMapId = Exists(shuttle) + ? Transform(shuttle.Value).MapID + : null; + + MapId? targetStationMap = null; + if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data)) + { + var grid = data.Grids.FirstOrNull(); + targetStationMap = grid != null + ? Transform(grid.Value).MapID : null; - - MapId? targetStationMap = null; - if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data)) - { - var grid = data.Grids.FirstOrNull(); - targetStationMap = grid != null - ? Transform(grid.Value).MapID - : null; - } - - // Check if there are nuke operatives still alive on the same map as the shuttle, - // or on the same map as the station. - // If there are, the round can continue. - var operatives = EntityQuery(true); - var operativesAlive = operatives - .Where(ent => - ent.Item3.MapID == shuttleMapId - || ent.Item3.MapID == targetStationMap) - .Any(ent => ent.Item2.CurrentState == MobState.Alive && ent.Item1.Running); - - if (operativesAlive) - continue; // There are living operatives than can access the shuttle, or are still on the station's map. - - // Check that there are spawns available and that they can access the shuttle. - var spawnsAvailable = EntityQuery(true).Any(); - if (spawnsAvailable && shuttleMapId == nukeops.NukiePlanet) - continue; // Ghost spawns can still access the shuttle. Continue the round. - - // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives, - // and there are no nuclear operatives on the target station's map. - nukeops.WinConditions.Add(spawnsAvailable - ? WinCondition.NukiesAbandoned - : WinCondition.AllNukiesDead); - - SetWinType(uid, WinType.CrewMajor, nukeops, false); - _roundEndSystem.DoRoundEndBehavior( - nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement); - - // prevent it called multiple times - nukeops.RoundEndBehavior = RoundEndBehavior.Nothing; - } - } - - private bool SpawnMap(Entity ent) - { - if (!ent.Comp.SpawnOutpost - || ent.Comp.NukiePlanet != null) - return true; - - ent.Comp.NukiePlanet = _mapManager.CreateMap(); - var gameMap = _prototypeManager.Index(ent.Comp.OutpostMapPrototype); - ent.Comp.NukieOutpost = GameTicker.LoadGameMap(gameMap, ent.Comp.NukiePlanet.Value, null)[0]; - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var grid, out _, out var shuttleTransform)) - { - if (shuttleTransform.MapID != ent.Comp.NukiePlanet) - continue; - - ent.Comp.NukieShuttle = grid; - break; } - return true; + // Check if there are nuke operatives still alive on the same map as the shuttle, + // or on the same map as the station. + // If there are, the round can continue. + var operatives = EntityQuery(true); + var operativesAlive = operatives + .Where(op => + op.Item3.MapID == shuttleMapId + || op.Item3.MapID == targetStationMap) + .Any(op => op.Item2.CurrentState == MobState.Alive && op.Item1.Running); + + if (operativesAlive) + return; // There are living operatives than can access the shuttle, or are still on the station's map. + + // Check that there are spawns available and that they can access the shuttle. + var spawnsAvailable = EntityQuery(true).Any(); + if (spawnsAvailable && CompOrNull(ent)?.Map == shuttleMapId) + return; // Ghost spawns can still access the shuttle. Continue the round. + + // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives, + // and there are no nuclear operatives on the target station's map. + nukeops.WinConditions.Add(spawnsAvailable + ? WinCondition.NukiesAbandoned + : WinCondition.AllNukiesDead); + + SetWinType(ent, WinType.CrewMajor, false); + _roundEndSystem.DoRoundEndBehavior( + nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement); + + // prevent it called multiple times + nukeops.RoundEndBehavior = RoundEndBehavior.Nothing; } - /// - /// Adds missing nuke operative components, equips starting gear and renames the entity. - /// - private void SetupOperativeEntity(EntityUid mob, string name, NukeopSpawnPreset spawnDetails, HumanoidCharacterProfile? profile) + // this should really go anywhere else but im tired. + private void OnAntagSelectEntity(Entity ent, ref AntagSelectEntityEvent args) { - _metaData.SetEntityName(mob, name); - EnsureComp(mob); - - if (profile != null) - _humanoid.LoadProfile(mob, profile); - - var gear = _prototypeManager.Index(spawnDetails.GearProto); - _stationSpawning.EquipStartingGear(mob, gear); - - _npcFaction.RemoveFaction(mob, "NanoTrasen", false); - _npcFaction.AddFaction(mob, "Syndicate"); - } - - private void SpawnOperatives(List sessions, bool spawnGhostRoles, NukeopsRuleComponent component) - { - if (component.NukieOutpost is not { Valid: true } outpostUid) + if (args.Handled) return; - var spawns = new List(); - foreach (var (_, meta, xform) in EntityQuery(true)) + var profile = args.Session != null + ? _prefs.GetPreferences(args.Session.UserId).SelectedCharacter as HumanoidCharacterProfile + : HumanoidCharacterProfile.RandomWithSpecies(); + if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species)) { - if (meta.EntityPrototype?.ID != component.SpawnPointProto.Id) - continue; - - if (xform.ParentUid != component.NukieOutpost) - continue; - - spawns.Add(xform.Coordinates); - break; + species = _prototypeManager.Index(SharedHumanoidAppearanceSystem.DefaultSpecies); } - //Fallback, spawn at the centre of the map - if (spawns.Count == 0) - { - spawns.Add(Transform(outpostUid).Coordinates); - _sawmill.Warning($"Fell back to default spawn for nukies!"); - } - - //Spawn the team - foreach (var nukieSession in sessions) - { - var name = $"{Loc.GetString(nukieSession.Type.NamePrefix)} {RobustRandom.PickAndTake(_prototypeManager.Index(nukieSession.Type.NameList).Values.ToList())}"; - - var nukeOpsAntag = _prototypeManager.Index(nukieSession.Type.AntagRoleProto); - - //If a session is available, spawn mob and transfer mind into it - if (nukieSession.Session != null) - { - var profile = _prefs.GetPreferences(nukieSession.Session.UserId).SelectedCharacter as HumanoidCharacterProfile; - if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species)) - { - species = _prototypeManager.Index(SharedHumanoidAppearanceSystem.DefaultSpecies); - } - - var mob = Spawn(species.Prototype, RobustRandom.Pick(spawns)); - SetupOperativeEntity(mob, name, nukieSession.Type, profile); - - var newMind = _mind.CreateMind(nukieSession.Session.UserId, name); - _mind.SetUserId(newMind, nukieSession.Session.UserId); - _roles.MindAddRole(newMind, new NukeopsRoleComponent { PrototypeId = nukieSession.Type.AntagRoleProto }); - - _mind.TransferTo(newMind, mob); - } - //Otherwise, spawn as a ghost role - else if (spawnGhostRoles) - { - var spawnPoint = Spawn(component.GhostSpawnPointProto, RobustRandom.Pick(spawns)); - var ghostRole = EnsureComp(spawnPoint); - EnsureComp(spawnPoint); - ghostRole.RoleName = Loc.GetString(nukeOpsAntag.Name); - ghostRole.RoleDescription = Loc.GetString(nukeOpsAntag.Objective); - - var nukeOpSpawner = EnsureComp(spawnPoint); - nukeOpSpawner.OperativeName = name; - nukeOpSpawner.SpawnDetails = nukieSession.Type; - } - } + args.Entity = Spawn(species.Prototype); + _humanoid.LoadProfile(args.Entity.Value, profile); } - /// - /// Display a greeting message and play a sound for a nukie - /// - private void NotifyNukie(ICommonSession session, NukeOperativeComponent nukeop, NukeopsRuleComponent nukeopsRule) + private void OnAfterAntagEntSelected(Entity ent, ref AfterAntagEntitySelectedEvent args) { - if (nukeopsRule.TargetStation is not { } station) + if (ent.Comp.TargetStation is not { } station) return; - _antagSelection.SendBriefing(session, Loc.GetString("nukeops-welcome", ("station", station), ("name", nukeopsRule.OperationName)), Color.Red, nukeop.GreetSoundNotification); + _antag.SendBriefing(args.Session, Loc.GetString("nukeops-welcome", + ("station", station), + ("name", Name(ent))), + Color.Red, + ent.Comp.GreetSoundNotification); } - /// - /// Spawn nukie ghost roles if this gamerule was started mid round - /// - private void SpawnOperativesForGhostRoles(EntityUid uid, NukeopsRuleComponent? component = null) + /// + /// Is this method the shitty glue holding together the last of my sanity? yes. + /// Do i have a better solution? not presently. + /// + private EntityUid? GetOutpost(Entity ent) { - if (!Resolve(uid, ref component)) - return; + if (!Resolve(ent, ref ent.Comp, false)) + return null; - if (!SpawnMap((uid, component))) - { - _sawmill.Info("Failed to load map for nukeops"); - return; - } - - var numNukies = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerOperative, component.MaxOps); - - //Dont continue if we have no nukies to spawn - if (numNukies == 0) - return; - - //Fill the ranks, commander first, then agent, then operatives - //TODO: Possible alternative team compositions? Like multiple commanders or agents - var operatives = new List(); - if (numNukies >= 1) - operatives.Add(new NukieSpawn(null, component.CommanderSpawnDetails)); - if (numNukies >= 2) - operatives.Add(new NukieSpawn(null, component.AgentSpawnDetails)); - if (numNukies >= 3) - { - for (var i = 2; i < numNukies; i++) - { - operatives.Add(new NukieSpawn(null, component.OperativeSpawnDetails)); - } - } - - SpawnOperatives(operatives, true, component); + return ent.Comp.MapGrids.FirstOrNull(); } - //For admins forcing someone to nukeOps. - public void MakeLoneNukie(EntityUid entity) + /// + /// Is this method the shitty glue holding together the last of my sanity? yes. + /// Do i have a better solution? not presently. + /// + private EntityUid? GetShuttle(Entity ent) { - if (!_mind.TryGetMind(entity, out var mindId, out var mindComponent)) - return; + if (!Resolve(ent, ref ent.Comp, false)) + return null; - //ok hardcoded value bad but so is everything else here - _roles.MindAddRole(mindId, new NukeopsRoleComponent { PrototypeId = NukeopsId }, mindComponent); - SetOutfitCommand.SetOutfit(entity, "SyndicateOperativeGearFull", EntityManager); - } - - private sealed class NukieSpawn - { - public ICommonSession? Session { get; private set; } - public NukeopSpawnPreset Type { get; private set; } - - public NukieSpawn(ICommonSession? session, NukeopSpawnPreset type) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) { - Session = session; - Type = type; + if (comp.AssociatedRule == ent.Owner) + return uid; } + + return null; } } diff --git a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs deleted file mode 100644 index 0a749d2e01..0000000000 --- a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs +++ /dev/null @@ -1,321 +0,0 @@ -using System.Linq; -using System.Numerics; -using Content.Server.Administration.Commands; -using Content.Server.Cargo.Systems; -using Content.Server.Chat.Managers; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Preferences.Managers; -using Content.Server.Spawners.Components; -using Content.Server.Station.Components; -using Content.Server.Station.Systems; -using Content.Shared.CCVar; -using Content.Shared.Humanoid; -using Content.Shared.Humanoid.Prototypes; -using Content.Shared.Mind; -using Content.Shared.NPC.Prototypes; -using Content.Shared.NPC.Systems; -using Content.Shared.Preferences; -using Content.Shared.Roles; -using Robust.Server.GameObjects; -using Robust.Server.Maps; -using Robust.Server.Player; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Configuration; -using Robust.Shared.Enums; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Utility; - -namespace Content.Server.GameTicking.Rules; - -/// -/// This handles the Pirates minor antag, which is designed to coincide with other modes on occasion. -/// -public sealed class PiratesRuleSystem : GameRuleSystem -{ - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IChatManager _chatManager = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IServerPreferencesManager _prefs = default!; - [Dependency] private readonly StationSpawningSystem _stationSpawningSystem = default!; - [Dependency] private readonly PricingSystem _pricingSystem = default!; - [Dependency] private readonly MapLoaderSystem _map = default!; - [Dependency] private readonly NamingSystem _namingSystem = default!; - [Dependency] private readonly NpcFactionSystem _npcFaction = default!; - [Dependency] private readonly SharedMindSystem _mindSystem = default!; - [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; - - [ValidatePrototypeId] - private const string GameRuleId = "Pirates"; - - [ValidatePrototypeId] - private const string MobId = "MobHuman"; - - [ValidatePrototypeId] - private const string SpeciesId = "Human"; - - [ValidatePrototypeId] - private const string PirateFactionId = "Syndicate"; - - [ValidatePrototypeId] - private const string EnemyFactionId = "NanoTrasen"; - - [ValidatePrototypeId] - private const string GearId = "PirateGear"; - - [ValidatePrototypeId] - private const string SpawnPointId = "SpawnPointPirates"; - - /// - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnPlayerSpawningEvent); - SubscribeLocalEvent(OnRoundEndTextEvent); - SubscribeLocalEvent(OnStartAttempt); - } - - private void OnRoundEndTextEvent(RoundEndTextAppendEvent ev) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var pirates, out var gameRule)) - { - if (Deleted(pirates.PirateShip)) - { - // Major loss, the ship somehow got annihilated. - ev.AddLine(Loc.GetString("pirates-no-ship")); - } - else - { - List<(double, EntityUid)> mostValuableThefts = new(); - - var comp1 = pirates; - var finalValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid => - { - foreach (var mindId in comp1.Pirates) - { - if (TryComp(mindId, out MindComponent? mind) && mind.CurrentEntity == uid) - return false; // Don't appraise the pirates twice, we count them in separately. - } - - return true; - }, (uid, price) => - { - if (comp1.InitialItems.Contains(uid)) - return; - - mostValuableThefts.Add((price, uid)); - mostValuableThefts.Sort((i1, i2) => i2.Item1.CompareTo(i1.Item1)); - if (mostValuableThefts.Count > 5) - mostValuableThefts.Pop(); - }); - - foreach (var mindId in pirates.Pirates) - { - if (TryComp(mindId, out MindComponent? mind) && mind.CurrentEntity is not null) - finalValue += _pricingSystem.GetPrice(mind.CurrentEntity.Value); - } - - var score = finalValue - pirates.InitialShipValue; - - ev.AddLine(Loc.GetString("pirates-final-score", ("score", $"{score:F2}"))); - ev.AddLine(Loc.GetString("pirates-final-score-2", ("finalPrice", $"{finalValue:F2}"))); - - ev.AddLine(""); - ev.AddLine(Loc.GetString("pirates-most-valuable")); - - foreach (var (price, obj) in mostValuableThefts) - { - ev.AddLine(Loc.GetString("pirates-stolen-item-entry", ("entity", obj), ("credits", $"{price:F2}"))); - } - - if (mostValuableThefts.Count == 0) - ev.AddLine(Loc.GetString("pirates-stole-nothing")); - } - - ev.AddLine(""); - ev.AddLine(Loc.GetString("pirates-list-start")); - foreach (var pirate in pirates.Pirates) - { - if (TryComp(pirate, out MindComponent? mind)) - { - ev.AddLine($"- {mind.CharacterName} ({mind.Session?.Name})"); - } - } - } - } - - private void OnPlayerSpawningEvent(RulePlayerSpawningEvent ev) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var pirates, out var gameRule)) - { - // Forgive me for copy-pasting nukies. - if (!GameTicker.IsGameRuleAdded(uid, gameRule)) - return; - - pirates.Pirates.Clear(); - pirates.InitialItems.Clear(); - - // Between 1 and : needs at least n players per op. - var numOps = Math.Max(1, - (int) Math.Min( - Math.Floor((double) ev.PlayerPool.Count / _cfg.GetCVar(CCVars.PiratesPlayersPerOp)), - _cfg.GetCVar(CCVars.PiratesMaxOps))); - var ops = new ICommonSession[numOps]; - for (var i = 0; i < numOps; i++) - { - ops[i] = _random.PickAndTake(ev.PlayerPool); - } - - var map = "/Maps/Shuttles/pirate.yml"; - var xformQuery = GetEntityQuery(); - - var aabbs = EntityQuery().SelectMany(x => - x.Grids.Select(x => - xformQuery.GetComponent(x).WorldMatrix.TransformBox(Comp(x).LocalAABB))) - .ToArray(); - - var aabb = aabbs[0]; - - for (var i = 1; i < aabbs.Length; i++) - { - aabb.Union(aabbs[i]); - } - - // (Not commented?) - var a = MathF.Max(aabb.Height / 2f, aabb.Width / 2f) * 2.5f; - - var gridId = _map.LoadGrid(GameTicker.DefaultMap, map, new MapLoadOptions - { - Offset = aabb.Center + new Vector2(a, a), - LoadMap = false, - }); - - if (!gridId.HasValue) - { - Log.Error($"Gridid was null when loading \"{map}\", aborting."); - foreach (var session in ops) - { - ev.PlayerPool.Add(session); - } - - return; - } - - pirates.PirateShip = gridId.Value; - - // TODO: Loot table or something - var pirateGear = _prototypeManager.Index(GearId); // YARRR - - var spawns = new List(); - - // Forgive me for hardcoding prototypes - foreach (var (_, meta, xform) in - EntityQuery(true)) - { - if (meta.EntityPrototype?.ID != SpawnPointId || xform.ParentUid != pirates.PirateShip) - continue; - - spawns.Add(xform.Coordinates); - } - - if (spawns.Count == 0) - { - spawns.Add(Transform(pirates.PirateShip).Coordinates); - Log.Warning($"Fell back to default spawn for pirates!"); - } - - for (var i = 0; i < ops.Length; i++) - { - var sex = _random.Prob(0.5f) ? Sex.Male : Sex.Female; - var gender = sex == Sex.Male ? Gender.Male : Gender.Female; - - var name = _namingSystem.GetName(SpeciesId, gender); - - var session = ops[i]; - var newMind = _mindSystem.CreateMind(session.UserId, name); - _mindSystem.SetUserId(newMind, session.UserId); - - var mob = Spawn(MobId, _random.Pick(spawns)); - _metaData.SetEntityName(mob, name); - - _mindSystem.TransferTo(newMind, mob); - var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile; - _stationSpawningSystem.EquipStartingGear(mob, pirateGear); - - _npcFaction.RemoveFaction(mob, EnemyFactionId, false); - _npcFaction.AddFaction(mob, PirateFactionId); - - pirates.Pirates.Add(newMind); - - // Notificate every player about a pirate antagonist role with sound - _audioSystem.PlayGlobal(pirates.PirateAlertSound, session); - - GameTicker.PlayerJoinGame(session); - } - - pirates.InitialShipValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid => - { - pirates.InitialItems.Add(uid); - return true; - }); // Include the players in the appraisal. - } - } - - //Forcing one player to be a pirate. - public void MakePirate(EntityUid entity) - { - if (!_mindSystem.TryGetMind(entity, out var mindId, out var mind)) - return; - - SetOutfitCommand.SetOutfit(entity, GearId, EntityManager); - - var pirateRule = EntityQuery().FirstOrDefault(); - if (pirateRule == null) - { - //todo fuck me this shit is awful - GameTicker.StartGameRule(GameRuleId, out var ruleEntity); - pirateRule = Comp(ruleEntity); - } - - // Notificate every player about a pirate antagonist role with sound - if (mind.Session != null) - { - _audioSystem.PlayGlobal(pirateRule.PirateAlertSound, mind.Session); - } - } - - private void OnStartAttempt(RoundStartAttemptEvent ev) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var pirates, out var gameRule)) - { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) - return; - - var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers); - if (!ev.Forced && ev.Players.Length < minPlayers) - { - _chatManager.SendAdminAnnouncement(Loc.GetString("nukeops-not-enough-ready-players", - ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); - ev.Cancel(); - return; - } - - if (ev.Players.Length == 0) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready")); - ev.Cancel(); - } - } - } -} diff --git a/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs b/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs index b11c28fb2b..5215da96aa 100644 --- a/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Systems; using Content.Shared.Chat; diff --git a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs index ba9fb2ccbc..e89d4614ff 100644 --- a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs @@ -14,7 +14,6 @@ using Content.Server.Station.Systems; using Content.Shared.Database; using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; -using Content.Shared.Inventory; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Mindshield.Components; @@ -24,12 +23,11 @@ using Content.Shared.Mobs.Systems; using Content.Shared.NPC.Prototypes; using Content.Shared.NPC.Systems; using Content.Shared.Revolutionary.Components; -using Content.Shared.Roles; using Content.Shared.Stunnable; using Content.Shared.Zombies; using Robust.Shared.Prototypes; using Robust.Shared.Timing; -using System.Linq; +using Content.Server.GameTicking.Components; namespace Content.Server.GameTicking.Rules; @@ -40,7 +38,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem RevolutionaryNpcFaction = "Revolutionary"; @@ -60,23 +57,12 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem(OnStartAttempt); - SubscribeLocalEvent(OnPlayerJobAssigned); SubscribeLocalEvent(OnCommandMobStateChanged); SubscribeLocalEvent(OnHeadRevMobStateChanged); - SubscribeLocalEvent(OnRoundEndText); SubscribeLocalEvent(OnGetBriefing); SubscribeLocalEvent(OnPostFlash); } - //Set miniumum players - protected override void Added(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) - { - base.Added(uid, component, gameRule, args); - - gameRule.MinPlayers = component.MinPlayers; - } - protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { base.Started(uid, component, gameRule, args); @@ -98,40 +84,29 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem(); - while (query.MoveNext(out var headrev)) + // This is (revsLost, commandsLost) concatted together + // (moony wrote this comment idk what it means) + var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0); + args.AddLine(Loc.GetString(Outcomes[index])); + + var sessionData = _antag.GetAntagIdentifiers(uid); + args.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", sessionData.Count))); + foreach (var (mind, data, name) in sessionData) { - // This is (revsLost, commandsLost) concatted together - // (moony wrote this comment idk what it means) - var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0); - ev.AddLine(Loc.GetString(Outcomes[index])); + var count = CompOrNull(mind)?.ConvertedCount ?? 0; + args.AddLine(Loc.GetString("rev-headrev-name-user", + ("name", name), + ("username", data.UserName), + ("count", count))); - ev.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", headrev.HeadRevs.Count))); - foreach (var player in headrev.HeadRevs) - { - // TODO: when role entities are a thing this has to change - var count = CompOrNull(player.Value)?.ConvertedCount ?? 0; - - _mind.TryGetSession(player.Value, out var session); - var username = session?.Name; - if (username != null) - { - ev.AddLine(Loc.GetString("rev-headrev-name-user", - ("name", player.Key), - ("username", username), ("count", count))); - } - else - { - ev.AddLine(Loc.GetString("rev-headrev-name", - ("name", player.Key), ("count", count))); - } - - // TODO: someone suggested listing all alive? revs maybe implement at some point - } + // TODO: someone suggested listing all alive? revs maybe implement at some point } } @@ -144,57 +119,6 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem chosen, ProtoId antagProto, RevolutionaryRuleComponent comp) - { - foreach (var headRev in chosen) - GiveHeadRev(headRev, antagProto, comp); - } - private void GiveHeadRev(EntityUid chosen, ProtoId antagProto, RevolutionaryRuleComponent comp) - { - RemComp(chosen); - - var inCharacterName = MetaData(chosen).EntityName; - - if (!_mind.TryGetMind(chosen, out var mind, out _)) - return; - - if (!_role.MindHasRole(mind)) - { - _role.MindAddRole(mind, new RevolutionaryRoleComponent { PrototypeId = antagProto }, silent: true); - } - - comp.HeadRevs.Add(inCharacterName, mind); - _inventory.SpawnItemsOnEntity(chosen, comp.StartingGear); - var revComp = EnsureComp(chosen); - EnsureComp(chosen); - - _antagSelection.SendBriefing(chosen, Loc.GetString("head-rev-role-greeting"), Color.CornflowerBlue, revComp.RevStartSound); - } - /// /// Called when a Head Rev uses a flash in melee to convert somebody else. /// @@ -232,22 +156,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem(entity)) - return; - - var revRule = EntityQuery().FirstOrDefault(); - if (revRule == null) - { - GameTicker.StartGameRule("Revolutionary", out var ruleEnt); - revRule = Comp(ruleEnt); - } - - GiveHeadRev(entity, revRule.HeadRevPrototypeId, revRule); + _antag.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound); } //TODO: Enemies of the revolution @@ -308,7 +217,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem { [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly AntagSelectionSystem _antagSelection = default!; [Dependency] private readonly MindSystem _mindSystem = default!; - [Dependency] private readonly SharedRoleSystem _roleSystem = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly ObjectivesSystem _objectives = default!; - [Dependency] private readonly InventorySystem _inventory = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnPlayersSpawned); + SubscribeLocalEvent(AfterAntagSelected); SubscribeLocalEvent(OnGetBriefing); SubscribeLocalEvent(OnObjectivesTextGetInfo); } - private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev) + private void AfterAntagSelected(Entity ent, ref AfterAntagEntitySelectedEvent args) { - var query = QueryActiveRules(); - while (query.MoveNext(out var uid, out _, out var comp, out var gameRule)) - { - //Get all players eligible for this role, allow selecting existing antags - //TO DO: When voxes specifies are added, increase their chance of becoming a thief by 4 times >:) - var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.ThiefPrototypeId, acceptableAntags: AntagAcceptability.All, allowNonHumanoids: true); - - //Abort if there are none - if (eligiblePlayers.Count == 0) - { - Log.Warning($"No eligible thieves found, ending game rule {ToPrettyString(uid):rule}"); - GameTicker.EndGameRule(uid, gameRule); - continue; - } - - //Calculate number of thieves to choose - var thiefCount = _random.Next(1, comp.MaxAllowThief + 1); - - //Select our theives - var thieves = _antagSelection.ChooseAntags(thiefCount, eligiblePlayers); - - MakeThief(thieves, comp, comp.PacifistThieves); - } - } - - public void MakeThief(List players, ThiefRuleComponent thiefRule, bool addPacified) - { - foreach (var thief in players) - { - MakeThief(thief, thiefRule, addPacified); - } - } - - public void MakeThief(EntityUid thief, ThiefRuleComponent thiefRule, bool addPacified) - { - if (!_mindSystem.TryGetMind(thief, out var mindId, out var mind)) + if (!_mindSystem.TryGetMind(args.EntityUid, out var mindId, out var mind)) return; - if (HasComp(mindId)) - return; - - // Assign thief roles - _roleSystem.MindAddRole(mindId, new ThiefRoleComponent - { - PrototypeId = thiefRule.ThiefPrototypeId, - }, silent: true); - - //Add Pacified - //To Do: Long-term this should just be using the antag code to add components. - if (addPacified) //This check is important because some servers may want to disable the thief's pacifism. Do not remove. - { - EnsureComp(thief); - } - //Generate objectives - GenerateObjectives(mindId, mind, thiefRule); - - //Send briefing here to account for humanoid/animal - _antagSelection.SendBriefing(thief, MakeBriefing(thief), null, thiefRule.GreetingSound); - - // Give starting items - _inventory.SpawnItemsOnEntity(thief, thiefRule.StarterItems); - - thiefRule.ThievesMinds.Add(mindId); - } - - public void AdminMakeThief(EntityUid entity, bool addPacified) - { - var thiefRule = EntityQuery().FirstOrDefault(); - if (thiefRule == null) - { - GameTicker.StartGameRule("Thief", out var ruleEntity); - thiefRule = Comp(ruleEntity); - } - - if (HasComp(entity)) - return; - - MakeThief(entity, thiefRule, addPacified); + GenerateObjectives(mindId, mind, ent); } private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule) @@ -160,8 +79,7 @@ public sealed class ThiefRuleSystem : GameRuleSystem private string MakeBriefing(EntityUid thief) { var isHuman = HasComp(thief); - var briefing = "\n"; - briefing = isHuman + var briefing = isHuman ? Loc.GetString("thief-role-greeting-human") : Loc.GetString("thief-role-greeting-animal"); @@ -169,9 +87,9 @@ public sealed class ThiefRuleSystem : GameRuleSystem return briefing; } - private void OnObjectivesTextGetInfo(Entity thiefs, ref ObjectivesTextGetInfoEvent args) + private void OnObjectivesTextGetInfo(Entity ent, ref ObjectivesTextGetInfoEvent args) { - args.Minds = thiefs.Comp.ThievesMinds; + args.Minds = _antag.GetAntagMindEntityUids(ent.Owner); args.AgentName = Loc.GetString("thief-round-end-agent-name"); } } diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index 769d7e0a5b..b6bcd5ee1e 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -5,97 +5,61 @@ using Content.Server.Objectives; using Content.Server.PDA.Ringer; using Content.Server.Roles; using Content.Server.Traitor.Uplink; -using Content.Shared.CCVar; -using Content.Shared.Dataset; using Content.Shared.Mind; -using Content.Shared.Mobs.Systems; using Content.Shared.NPC.Systems; using Content.Shared.Objectives.Components; using Content.Shared.PDA; using Content.Shared.Roles; using Content.Shared.Roles.Jobs; -using Robust.Server.Player; -using Robust.Shared.Configuration; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Robust.Shared.Timing; using System.Linq; using System.Text; +using Content.Server.GameTicking.Components; namespace Content.Server.GameTicking.Rules; public sealed class TraitorRuleSystem : GameRuleSystem { - [Dependency] private readonly AntagSelectionSystem _antagSelection = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly NpcFactionSystem _npcFaction = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly UplinkSystem _uplink = default!; [Dependency] private readonly MindSystem _mindSystem = default!; [Dependency] private readonly SharedRoleSystem _roleSystem = default!; [Dependency] private readonly SharedJobSystem _jobs = default!; [Dependency] private readonly ObjectivesSystem _objectives = default!; - [Dependency] private readonly IGameTiming _timing = default!; - private int PlayersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor); - private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors); + public const int MaxPicks = 20; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnStartAttempt); - SubscribeLocalEvent(OnPlayersSpawned); - SubscribeLocalEvent(HandleLatejoin); + SubscribeLocalEvent(AfterEntitySelected); SubscribeLocalEvent(OnObjectivesTextGetInfo); SubscribeLocalEvent(OnObjectivesTextPrepend); } - //Set min players on game rule protected override void Added(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) { base.Added(uid, component, gameRule, args); - - gameRule.MinPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers); - } - - protected override void Started(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - base.Started(uid, component, gameRule, args); MakeCodewords(component); } - protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime) + private void AfterEntitySelected(Entity ent, ref AfterAntagEntitySelectedEvent args) { - base.ActiveTick(uid, component, gameRule, frameTime); - - if (component.SelectionStatus < TraitorRuleComponent.SelectionState.Started && component.AnnounceAt < _timing.CurTime) - { - DoTraitorStart(component); - component.SelectionStatus = TraitorRuleComponent.SelectionState.Started; - } - } - - /// - /// Check for enough players - /// - /// - private void OnStartAttempt(RoundStartAttemptEvent ev) - { - TryRoundStartAttempt(ev, Loc.GetString("traitor-title")); + MakeTraitor(args.EntityUid, ent); } private void MakeCodewords(TraitorRuleComponent component) { - var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount); - var adjectives = _prototypeManager.Index(component.CodewordAdjectives).Values; - var verbs = _prototypeManager.Index(component.CodewordVerbs).Values; + var adjectives = _prototypeManager.Index(component.CodewordAdjectives).Values; + var verbs = _prototypeManager.Index(component.CodewordVerbs).Values; var codewordPool = adjectives.Concat(verbs).ToList(); - var finalCodewordCount = Math.Min(codewordCount, codewordPool.Count); + var finalCodewordCount = Math.Min(component.CodewordCount, codewordPool.Count); component.Codewords = new string[finalCodewordCount]; for (var i = 0; i < finalCodewordCount; i++) { @@ -103,66 +67,19 @@ public sealed class TraitorRuleSystem : GameRuleSystem } } - private void DoTraitorStart(TraitorRuleComponent component) - { - var eligiblePlayers = _antagSelection.GetEligiblePlayers(_playerManager.Sessions, component.TraitorPrototypeId); - - if (eligiblePlayers.Count == 0) - return; - - var traitorsToSelect = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, PlayersPerTraitor, MaxTraitors); - - var selectedTraitors = _antagSelection.ChooseAntags(traitorsToSelect, eligiblePlayers); - - MakeTraitor(selectedTraitors, component); - } - - private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev) - { - //Start the timer - var query = QueryActiveRules(); - while (query.MoveNext(out _, out var comp, out var gameRuleComponent)) - { - var delay = TimeSpan.FromSeconds( - _cfg.GetCVar(CCVars.TraitorStartDelay) + - _random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance))); - - //Set the delay for choosing traitors - comp.AnnounceAt = _timing.CurTime + delay; - - comp.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToStart; - } - } - - public bool MakeTraitor(List traitors, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true) - { - foreach (var traitor in traitors) - { - MakeTraitor(traitor, component, giveUplink, giveObjectives); - } - - return true; - } - public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true) { //Grab the mind if it wasnt provided if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind)) return false; - if (HasComp(mindId)) - { - Log.Error($"Player {mind.CharacterName} is already a traitor."); - return false; - } - var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords))); Note[]? code = null; if (giveUplink) { // Calculate the amount of currency on the uplink. - var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance); + var startingBalance = component.StartingBalance; if (_jobs.MindTryGetJob(mindId, out _, out var prototype)) startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0); @@ -180,19 +97,14 @@ public sealed class TraitorRuleSystem : GameRuleSystem Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#")))); } - _antagSelection.SendBriefing(traitor, GenerateBriefing(component.Codewords, code), null, component.GreetSoundNotification); + _antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code), null, component.GreetSoundNotification); component.TraitorMinds.Add(mindId); - // Assign traitor roles - _roleSystem.MindAddRole(mindId, new TraitorRoleComponent - { - PrototypeId = component.TraitorPrototypeId - }, mind, true); // Assign briefing _roleSystem.MindAddRole(mindId, new RoleBriefingComponent { - Briefing = briefing.ToString() + Briefing = briefing }, mind, true); // Change the faction @@ -202,11 +114,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem // Give traitors their objectives if (giveObjectives) { - var maxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty); - var maxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks); var difficulty = 0f; - Log.Debug($"Attempting {maxPicks} objective picks with {maxDifficulty} difficulty"); - for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++) + for (var pick = 0; pick < MaxPicks && component.MaxDifficulty > difficulty; pick++) { var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup); if (objective == null) @@ -222,53 +131,9 @@ public sealed class TraitorRuleSystem : GameRuleSystem return true; } - private void HandleLatejoin(PlayerSpawnCompleteEvent ev) - { - var query = QueryActiveRules(); - while (query.MoveNext(out _, out var comp, out _)) - { - if (comp.TotalTraitors >= MaxTraitors) - continue; - - if (!ev.LateJoin) - continue; - - if (!_antagSelection.IsPlayerEligible(ev.Player, comp.TraitorPrototypeId)) - continue; - - //If its before we have selected traitors, continue - if (comp.SelectionStatus < TraitorRuleComponent.SelectionState.Started) - continue; - - // the nth player we adjust our probabilities around - var target = PlayersPerTraitor * comp.TotalTraitors + 1; - var chance = 1f / PlayersPerTraitor; - - // If we have too many traitors, divide by how many players below target for next traitor we are. - if (ev.JoinOrder < target) - { - chance /= (target - ev.JoinOrder); - } - else // Tick up towards 100% chance. - { - chance *= ((ev.JoinOrder + 1) - target); - } - - if (chance > 1) - chance = 1; - - // Now that we've calculated our chance, roll and make them a traitor if we roll under. - // You get one shot. - if (_random.Prob(chance)) - { - MakeTraitor(ev.Mob, comp); - } - } - } - private void OnObjectivesTextGetInfo(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextGetInfoEvent args) { - args.Minds = comp.TraitorMinds; + args.Minds = _antag.GetAntagMindEntityUids(uid); args.AgentName = Loc.GetString("traitor-round-end-agent-name"); } @@ -277,27 +142,6 @@ public sealed class TraitorRuleSystem : GameRuleSystem args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords))); } - /// - /// Start this game rule manually - /// - public TraitorRuleComponent StartGameRule() - { - var comp = EntityQuery().FirstOrDefault(); - if (comp == null) - { - GameTicker.StartGameRule("Traitor", out var ruleEntity); - comp = Comp(ruleEntity); - } - - return comp; - } - - public void MakeTraitorAdmin(EntityUid entity, bool giveUplink, bool giveObjectives) - { - var traitorRule = StartGameRule(); - MakeTraitor(entity, traitorRule, giveUplink, giveObjectives); - } - private string GenerateBriefing(string[] codewords, Note[]? uplinkCode) { var sb = new StringBuilder(); @@ -312,9 +156,11 @@ public sealed class TraitorRuleSystem : GameRuleSystem public List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind) { List<(EntityUid Id, MindComponent Mind)> allTraitors = new(); - foreach (var traitor in EntityQuery()) + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var traitor)) { - foreach (var role in GetOtherTraitorMindsAliveAndConnected(ourMind, traitor)) + foreach (var role in GetOtherTraitorMindsAliveAndConnected(ourMind, (uid, traitor))) { if (!allTraitors.Contains(role)) allTraitors.Add(role); @@ -324,20 +170,15 @@ public sealed class TraitorRuleSystem : GameRuleSystem return allTraitors; } - private List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind, TraitorRuleComponent component) + private List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind, Entity rule) { var traitors = new List<(EntityUid Id, MindComponent Mind)>(); - foreach (var traitor in component.TraitorMinds) + foreach (var mind in _antag.GetAntagMinds(rule.Owner)) { - if (TryComp(traitor, out MindComponent? mind) && - mind.OwnedEntity != null && - mind.Session != null && - mind != ourMind && - _mobStateSystem.IsAlive(mind.OwnedEntity.Value) && - mind.CurrentEntity == mind.OwnedEntity) - { - traitors.Add((traitor, mind)); - } + if (mind.Comp == ourMind) + continue; + + traitors.Add((mind, mind)); } return traitors; diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs index 54e8bcf8b7..f22c208408 100644 --- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs @@ -1,112 +1,90 @@ -using Content.Server.Actions; using Content.Server.Antag; using Content.Server.Chat.Systems; using Content.Server.GameTicking.Rules.Components; using Content.Server.Popups; -using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Server.Zombies; -using Content.Shared.CCVar; using Content.Shared.Humanoid; using Content.Shared.Mind; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; -using Content.Shared.Roles; using Content.Shared.Zombies; -using Robust.Server.Player; -using Robust.Shared.Configuration; using Robust.Shared.Player; -using Robust.Shared.Random; using Robust.Shared.Timing; using System.Globalization; +using Content.Server.GameTicking.Components; namespace Content.Server.GameTicking.Rules; public sealed class ZombieRuleSystem : GameRuleSystem { - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly RoundEndSystem _roundEnd = default!; [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly ActionsSystem _action = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly ZombieSystem _zombie = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; - [Dependency] private readonly SharedRoleSystem _roles = default!; [Dependency] private readonly StationSystem _station = default!; - [Dependency] private readonly AntagSelectionSystem _antagSelection = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly IGameTiming _timing = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnStartAttempt); - SubscribeLocalEvent(OnRoundEndText); SubscribeLocalEvent(OnZombifySelf); } - /// - /// Set the required minimum players for this gamemode to start - /// - protected override void Added(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) + protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, + ref RoundEndTextAppendEvent args) { - base.Added(uid, component, gameRule, args); + base.AppendRoundEndText(uid, component, gameRule, ref args); - gameRule.MinPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers); - } + // This is just the general condition thing used for determining the win/lose text + var fraction = GetInfectedFraction(true, true); - private void OnRoundEndText(RoundEndTextAppendEvent ev) - { - foreach (var zombie in EntityQuery()) + if (fraction <= 0) + args.AddLine(Loc.GetString("zombie-round-end-amount-none")); + else if (fraction <= 0.25) + args.AddLine(Loc.GetString("zombie-round-end-amount-low")); + else if (fraction <= 0.5) + args.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture)))); + else if (fraction < 1) + args.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture)))); + else + args.AddLine(Loc.GetString("zombie-round-end-amount-all")); + + var antags = _antag.GetAntagIdentifiers(uid); + args.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", antags.Count))); + foreach (var (_, data, entName) in antags) { - // This is just the general condition thing used for determining the win/lose text - var fraction = GetInfectedFraction(true, true); + args.AddLine(Loc.GetString("zombie-round-end-user-was-initial", + ("name", entName), + ("username", data.UserName))); + } - if (fraction <= 0) - ev.AddLine(Loc.GetString("zombie-round-end-amount-none")); - else if (fraction <= 0.25) - ev.AddLine(Loc.GetString("zombie-round-end-amount-low")); - else if (fraction <= 0.5) - ev.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture)))); - else if (fraction < 1) - ev.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture)))); - else - ev.AddLine(Loc.GetString("zombie-round-end-amount-all")); - - ev.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", zombie.InitialInfectedNames.Count))); - foreach (var player in zombie.InitialInfectedNames) + var healthy = GetHealthyHumans(); + // Gets a bunch of the living players and displays them if they're under a threshold. + // InitialInfected is used for the threshold because it scales with the player count well. + if (healthy.Count <= 0 || healthy.Count > 2 * antags.Count) + return; + args.AddLine(""); + args.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count))); + foreach (var survivor in healthy) + { + var meta = MetaData(survivor); + var username = string.Empty; + if (_mindSystem.TryGetMind(survivor, out _, out var mind) && mind.Session != null) { - ev.AddLine(Loc.GetString("zombie-round-end-user-was-initial", - ("name", player.Key), - ("username", player.Value))); + username = mind.Session.Name; } - var healthy = GetHealthyHumans(); - // Gets a bunch of the living players and displays them if they're under a threshold. - // InitialInfected is used for the threshold because it scales with the player count well. - if (healthy.Count <= 0 || healthy.Count > 2 * zombie.InitialInfectedNames.Count) - continue; - ev.AddLine(""); - ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count))); - foreach (var survivor in healthy) - { - var meta = MetaData(survivor); - var username = string.Empty; - if (_mindSystem.TryGetMind(survivor, out _, out var mind) && mind.Session != null) - { - username = mind.Session.Name; - } - - ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor", - ("name", meta.EntityName), - ("username", username))); - } + args.AddLine(Loc.GetString("zombie-round-end-user-was-survivor", + ("name", meta.EntityName), + ("username", username))); } } @@ -134,38 +112,20 @@ public sealed class ZombieRuleSystem : GameRuleSystem _roundEnd.EndRound(); } - /// - /// Check we have enough players to start this game mode, if not - cancel and announce - /// - private void OnStartAttempt(RoundStartAttemptEvent ev) - { - TryRoundStartAttempt(ev, Loc.GetString("zombie-title")); - } - protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { base.Started(uid, component, gameRule, args); - var delay = _random.Next(component.MinStartDelay, component.MaxStartDelay); - component.StartTime = _timing.CurTime + delay; + component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; } protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime) { base.ActiveTick(uid, component, gameRule, frameTime); - - if (component.StartTime.HasValue && component.StartTime < _timing.CurTime) - { - InfectInitialPlayers(component); - component.StartTime = null; - component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; - } - - if (component.NextRoundEndCheck.HasValue && component.NextRoundEndCheck < _timing.CurTime) - { - CheckRoundEnd(component); - component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; - } + if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime) + return; + CheckRoundEnd(component); + component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; } private void OnZombifySelf(EntityUid uid, PendingZombieComponent component, ZombifySelfActionEvent args) @@ -232,81 +192,4 @@ public sealed class ZombieRuleSystem : GameRuleSystem } return healthy; } - - /// - /// Infects the first players with the passive zombie virus. - /// Also records their names for the end of round screen. - /// - /// - /// The reason this code is written separately is to facilitate - /// allowing this gamemode to be started midround. As such, it doesn't need - /// any information besides just running. - /// - private void InfectInitialPlayers(ZombieRuleComponent component) - { - //Get all players with initial infected enabled, and exclude those with the ZombieImmuneComponent and roles with CanBeAntag = False - var eligiblePlayers = _antagSelection.GetEligiblePlayers( - _playerManager.Sessions, - component.PatientZeroPrototypeId, - includeAllJobs: false, - customExcludeCondition: player => HasComp(player) || HasComp(player) - ); - - //And get all players, excluding ZombieImmune and roles with CanBeAntag = False - to fill any leftover initial infected slots - var allPlayers = _antagSelection.GetEligiblePlayers( - _playerManager.Sessions, - component.PatientZeroPrototypeId, - acceptableAntags: Shared.Antag.AntagAcceptability.All, - includeAllJobs: false , - ignorePreferences: true, - customExcludeCondition: HasComp - ); - - //If there are no players to choose, abort - if (allPlayers.Count == 0) - return; - - //How many initial infected should we select - var initialInfectedCount = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerInfected, component.MaxInitialInfected); - - //Choose the required number of initial infected from the eligible players, making up any shortfall by choosing from all players - var initialInfected = _antagSelection.ChooseAntags(initialInfectedCount, eligiblePlayers, allPlayers); - - //Make brain craving - MakeZombie(initialInfected, component); - - //Send the briefing, play greeting sound - _antagSelection.SendBriefing(initialInfected, Loc.GetString("zombie-patientzero-role-greeting"), Color.Plum, component.InitialInfectedSound); - } - - private void MakeZombie(List entities, ZombieRuleComponent component) - { - foreach (var entity in entities) - { - MakeZombie(entity, component); - } - } - private void MakeZombie(EntityUid entity, ZombieRuleComponent component) - { - if (!_mindSystem.TryGetMind(entity, out var mind, out var mindComponent)) - return; - - //Add the role to the mind silently (to avoid repeating job assignment) - _roles.MindAddRole(mind, new InitialInfectedRoleComponent { PrototypeId = component.PatientZeroPrototypeId }, silent: true); - EnsureComp(entity); - - //Add the zombie components and grace period - var pending = EnsureComp(entity); - pending.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace); - EnsureComp(entity); - EnsureComp(entity); - - //Add the zombify action - _action.AddAction(entity, ref pending.Action, component.ZombifySelfActionPrototype, entity); - - //Get names for the round end screen, incase they leave mid-round - var inCharacterName = MetaData(entity).EntityName; - var accountName = mindComponent.Session == null ? string.Empty : mindComponent.Session.Name; - component.InitialInfectedNames.Add(inCharacterName, accountName); - } } diff --git a/Content.Server/Objectives/ObjectivesSystem.cs b/Content.Server/Objectives/ObjectivesSystem.cs index 20205b8b72..47fe4eb5f8 100644 --- a/Content.Server/Objectives/ObjectivesSystem.cs +++ b/Content.Server/Objectives/ObjectivesSystem.cs @@ -1,10 +1,7 @@ using Content.Server.GameTicking; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Mind; using Content.Server.Shuttles.Systems; using Content.Shared.Cuffs.Components; using Content.Shared.Mind; -using Content.Shared.Mobs.Systems; using Content.Shared.Objectives.Components; using Content.Shared.Objectives.Systems; using Content.Shared.Random; @@ -12,7 +9,9 @@ using Content.Shared.Random.Helpers; using Robust.Shared.Prototypes; using Robust.Shared.Random; using System.Linq; +using Content.Server.GameTicking.Components; using System.Text; +using Robust.Server.Player; namespace Content.Server.Objectives; @@ -20,8 +19,8 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem { [Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!; public override void Initialize() @@ -179,7 +178,9 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem .ThenByDescending(x => x.completedObjectives); foreach (var (summary, _, _) in sortedAgents) + { result.AppendLine(summary); + } } public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto) @@ -244,8 +245,14 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem return null; var name = mind.CharacterName; - _mind.TryGetSession(mindId, out var session); - var username = session?.Name; + var username = (string?) null; + + if (mind.OriginalOwnerUserId != null && + _player.TryGetPlayerData(mind.OriginalOwnerUserId.Value, out var sessionData)) + { + username = sessionData.UserName; + } + if (username != null) { diff --git a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs index 107d09c898..0e20f007d7 100644 --- a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs @@ -17,6 +17,7 @@ using Robust.Shared.Player; using Robust.Shared.Utility; using System.Linq; using System.Diagnostics.CodeAnalysis; +using Content.Server.GameTicking.Components; namespace Content.Server.Power.EntitySystems; @@ -723,8 +724,8 @@ internal sealed partial class PowerMonitoringConsoleSystem : SharedPowerMonitori } } - // Designates a supplied entity as a 'collection master'. Other entities which share this - // entities collection name and are attached on the same load network are assigned this entity + // Designates a supplied entity as a 'collection master'. Other entities which share this + // entities collection name and are attached on the same load network are assigned this entity // as the master that represents them on the console UI. This way you can have one device // represent multiple connected devices private void AssignEntityAsCollectionMaster diff --git a/Content.Server/Preferences/Managers/IServerPreferencesManager.cs b/Content.Server/Preferences/Managers/IServerPreferencesManager.cs index a36b053717..1808592ef5 100644 --- a/Content.Server/Preferences/Managers/IServerPreferencesManager.cs +++ b/Content.Server/Preferences/Managers/IServerPreferencesManager.cs @@ -16,6 +16,7 @@ namespace Content.Server.Preferences.Managers bool TryGetCachedPreferences(NetUserId userId, [NotNullWhen(true)] out PlayerPreferences? playerPreferences); PlayerPreferences GetPreferences(NetUserId userId); + PlayerPreferences? GetPreferencesOrNull(NetUserId? userId); IEnumerable> GetSelectedProfilesForPlayers(List userIds); bool HavePreferencesLoaded(ICommonSession session); } diff --git a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs index 0f8cb83f10..a1eb8aad82 100644 --- a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs +++ b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs @@ -256,6 +256,20 @@ namespace Content.Server.Preferences.Managers return prefs; } + /// + /// Retrieves preferences for the given username from storage or returns null. + /// Creates and saves default preferences if they are not found, then returns them. + /// + public PlayerPreferences? GetPreferencesOrNull(NetUserId? userId) + { + if (userId == null) + return null; + + if (_cachedPlayerPrefs.TryGetValue(userId.Value, out var pref)) + return pref.Prefs; + return null; + } + private async Task GetOrCreatePreferencesAsync(NetUserId userId) { var prefs = await _db.GetPlayerPreferencesAsync(userId); diff --git a/Content.Server/RandomMetadata/RandomMetadataSystem.cs b/Content.Server/RandomMetadata/RandomMetadataSystem.cs index c088d57fd9..0c254c52ac 100644 --- a/Content.Server/RandomMetadata/RandomMetadataSystem.cs +++ b/Content.Server/RandomMetadata/RandomMetadataSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Dataset; +using Content.Shared.Dataset; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -47,9 +47,12 @@ public sealed class RandomMetadataSystem : EntitySystem var outputSegments = new List(); foreach (var segment in segments) { - outputSegments.Add(_prototype.TryIndex(segment, out var proto) - ? Loc.GetString(_random.Pick(proto.Values)) - : Loc.GetString(segment)); + if (_prototype.TryIndex(segment, out var proto)) + outputSegments.Add(_random.Pick(proto.Values)); + else if (Loc.TryGetString(segment, out var localizedSegment)) + outputSegments.Add(localizedSegment); + else + outputSegments.Add(segment); } return string.Join(separator, outputSegments); } diff --git a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs index 506fd61d55..75f8618798 100644 --- a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs +++ b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Server.GameTicking; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Spawners.Components; using JetBrains.Annotations; diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs index 36d30f50ee..e7e0957239 100644 --- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Administration; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs deleted file mode 100644 index 92911e0858..0000000000 --- a/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Server.StationEvents.Events; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(LoneOpsSpawnRule))] -public sealed partial class LoneOpsSpawnRuleComponent : Component -{ - [DataField("loneOpsShuttlePath")] - public string LoneOpsShuttlePath = "Maps/Shuttles/striker.yml"; - - [DataField("gameRuleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string GameRuleProto = "Nukeops"; - - [DataField("additionalRule")] - public EntityUid? AdditionalRule; -} diff --git a/Content.Server/StationEvents/Events/AnomalySpawnRule.cs b/Content.Server/StationEvents/Events/AnomalySpawnRule.cs index 48a3b900c4..96633834ee 100644 --- a/Content.Server/StationEvents/Events/AnomalySpawnRule.cs +++ b/Content.Server/StationEvents/Events/AnomalySpawnRule.cs @@ -1,4 +1,5 @@ using Content.Server.Anomaly; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs b/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs index 0eed77f154..b3ed10999e 100644 --- a/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs +++ b/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs @@ -1,4 +1,5 @@ -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Components; +using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Robust.Shared.Random; diff --git a/Content.Server/StationEvents/Events/BluespaceLockerRule.cs b/Content.Server/StationEvents/Events/BluespaceLockerRule.cs index 709b750334..eef9850e73 100644 --- a/Content.Server/StationEvents/Events/BluespaceLockerRule.cs +++ b/Content.Server/StationEvents/Events/BluespaceLockerRule.cs @@ -1,4 +1,5 @@ -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Components; +using Content.Server.GameTicking.Rules.Components; using Content.Server.Resist; using Content.Server.Station.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/BreakerFlipRule.cs b/Content.Server/StationEvents/Events/BreakerFlipRule.cs index 494779fe35..16d3fd8c95 100644 --- a/Content.Server/StationEvents/Events/BreakerFlipRule.cs +++ b/Content.Server/StationEvents/Events/BreakerFlipRule.cs @@ -1,4 +1,5 @@ -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Components; +using Content.Server.GameTicking.Rules.Components; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Station.Components; diff --git a/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs b/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs index 249a14a9b8..ccfb8aee58 100644 --- a/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs +++ b/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Components; using Content.Server.Station.Systems; diff --git a/Content.Server/StationEvents/Events/CargoGiftsRule.cs b/Content.Server/StationEvents/Events/CargoGiftsRule.cs index 0c8c9b6dc5..c27cd30278 100644 --- a/Content.Server/StationEvents/Events/CargoGiftsRule.cs +++ b/Content.Server/StationEvents/Events/CargoGiftsRule.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Server.Cargo.Components; using Content.Server.Cargo.Systems; using Content.Server.GameTicking; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/ClericalErrorRule.cs b/Content.Server/StationEvents/Events/ClericalErrorRule.cs index dd4473952c..854ee685b3 100644 --- a/Content.Server/StationEvents/Events/ClericalErrorRule.cs +++ b/Content.Server/StationEvents/Events/ClericalErrorRule.cs @@ -1,4 +1,5 @@ -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Components; +using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Content.Server.StationRecords; using Content.Server.StationRecords.Systems; diff --git a/Content.Server/StationEvents/Events/FalseAlarmRule.cs b/Content.Server/StationEvents/Events/FalseAlarmRule.cs index 05e9435b40..e5317a5449 100644 --- a/Content.Server/StationEvents/Events/FalseAlarmRule.cs +++ b/Content.Server/StationEvents/Events/FalseAlarmRule.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using JetBrains.Annotations; diff --git a/Content.Server/StationEvents/Events/GasLeakRule.cs b/Content.Server/StationEvents/Events/GasLeakRule.cs index 68544e416c..1221612171 100644 --- a/Content.Server/StationEvents/Events/GasLeakRule.cs +++ b/Content.Server/StationEvents/Events/GasLeakRule.cs @@ -1,4 +1,5 @@ using Content.Server.Atmos.EntitySystems; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Robust.Shared.Audio; diff --git a/Content.Server/StationEvents/Events/ImmovableRodRule.cs b/Content.Server/StationEvents/Events/ImmovableRodRule.cs index 1b8fb6be1f..cacb839cd3 100644 --- a/Content.Server/StationEvents/Events/ImmovableRodRule.cs +++ b/Content.Server/StationEvents/Events/ImmovableRodRule.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.ImmovableRod; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/IonStormRule.cs b/Content.Server/StationEvents/Events/IonStormRule.cs index cd3cd63ae8..8361cc6048 100644 --- a/Content.Server/StationEvents/Events/IonStormRule.cs +++ b/Content.Server/StationEvents/Events/IonStormRule.cs @@ -1,5 +1,5 @@ +using Content.Server.GameTicking.Components; using System.Linq; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Silicons.Laws; using Content.Server.Station.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/KudzuGrowthRule.cs b/Content.Server/StationEvents/Events/KudzuGrowthRule.cs index 3fa12cd4e9..5b56e03846 100644 --- a/Content.Server/StationEvents/Events/KudzuGrowthRule.cs +++ b/Content.Server/StationEvents/Events/KudzuGrowthRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs b/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs deleted file mode 100644 index 4b15e59099..0000000000 --- a/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Robust.Server.GameObjects; -using Robust.Server.Maps; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; -using Content.Server.RoundEnd; - -namespace Content.Server.StationEvents.Events; - -public sealed class LoneOpsSpawnRule : StationEventSystem -{ - [Dependency] private readonly MapLoaderSystem _map = default!; - - protected override void Started(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - base.Started(uid, component, gameRule, args); - - // Loneops can only spawn if there is no nukeops active - if (GameTicker.IsGameRuleAdded()) - { - ForceEndSelf(uid, gameRule); - return; - } - - var shuttleMap = MapManager.CreateMap(); - var options = new MapLoadOptions - { - LoadMap = true, - }; - - _map.TryLoad(shuttleMap, component.LoneOpsShuttlePath, out _, options); - - var nukeopsEntity = GameTicker.AddGameRule(component.GameRuleProto); - component.AdditionalRule = nukeopsEntity; - var nukeopsComp = Comp(nukeopsEntity); - nukeopsComp.SpawnOutpost = false; - nukeopsComp.RoundEndBehavior = RoundEndBehavior.Nothing; - GameTicker.StartGameRule(nukeopsEntity); - } - - protected override void Ended(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) - { - base.Ended(uid, component, gameRule, args); - - if (component.AdditionalRule != null) - GameTicker.EndGameRule(component.AdditionalRule.Value); - } -} diff --git a/Content.Server/StationEvents/Events/MassHallucinationsRule.cs b/Content.Server/StationEvents/Events/MassHallucinationsRule.cs index 722a489541..d6f609bee1 100644 --- a/Content.Server/StationEvents/Events/MassHallucinationsRule.cs +++ b/Content.Server/StationEvents/Events/MassHallucinationsRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Content.Server.Traits.Assorted; diff --git a/Content.Server/StationEvents/Events/MeteorSwarmRule.cs b/Content.Server/StationEvents/Events/MeteorSwarmRule.cs index ad56479b37..455011259d 100644 --- a/Content.Server/StationEvents/Events/MeteorSwarmRule.cs +++ b/Content.Server/StationEvents/Events/MeteorSwarmRule.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Robust.Shared.Map; diff --git a/Content.Server/StationEvents/Events/NinjaSpawnRule.cs b/Content.Server/StationEvents/Events/NinjaSpawnRule.cs index 8ad5c8602e..d9d68a386c 100644 --- a/Content.Server/StationEvents/Events/NinjaSpawnRule.cs +++ b/Content.Server/StationEvents/Events/NinjaSpawnRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Ninja.Systems; using Content.Server.Station.Components; diff --git a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs index 5503438df8..d547fc9446 100644 --- a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs +++ b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs @@ -1,4 +1,5 @@ using System.Threading; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; diff --git a/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs b/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs index c3cd719cc4..87d50fc8b2 100644 --- a/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs +++ b/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Content.Server.Storage.Components; diff --git a/Content.Server/StationEvents/Events/RandomSentienceRule.cs b/Content.Server/StationEvents/Events/RandomSentienceRule.cs index 4b7606d01f..06bb470602 100644 --- a/Content.Server/StationEvents/Events/RandomSentienceRule.cs +++ b/Content.Server/StationEvents/Events/RandomSentienceRule.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Ghost.Roles.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/RandomSpawnRule.cs b/Content.Server/StationEvents/Events/RandomSpawnRule.cs index c514acc623..77744d44e4 100644 --- a/Content.Server/StationEvents/Events/RandomSpawnRule.cs +++ b/Content.Server/StationEvents/Events/RandomSpawnRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/SolarFlareRule.cs b/Content.Server/StationEvents/Events/SolarFlareRule.cs index a4ec74b43b..0370b4ee61 100644 --- a/Content.Server/StationEvents/Events/SolarFlareRule.cs +++ b/Content.Server/StationEvents/Events/SolarFlareRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Radio; using Robust.Shared.Random; diff --git a/Content.Server/StationEvents/Events/StationEventSystem.cs b/Content.Server/StationEvents/Events/StationEventSystem.cs index 7f05f8940d..cbdae9e9e3 100644 --- a/Content.Server/StationEvents/Events/StationEventSystem.cs +++ b/Content.Server/StationEvents/Events/StationEventSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Administration.Logs; using Content.Server.Chat.Systems; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Systems; diff --git a/Content.Server/StationEvents/Events/VentClogRule.cs b/Content.Server/StationEvents/Events/VentClogRule.cs index e263a5f4f6..867f41dccc 100644 --- a/Content.Server/StationEvents/Events/VentClogRule.cs +++ b/Content.Server/StationEvents/Events/VentClogRule.cs @@ -6,6 +6,7 @@ using JetBrains.Annotations; using Robust.Shared.Random; using System.Linq; using Content.Server.Fluids.EntitySystems; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/VentCrittersRule.cs b/Content.Server/StationEvents/Events/VentCrittersRule.cs index cdcf2bf6ff..c2605039bc 100644 --- a/Content.Server/StationEvents/Events/VentCrittersRule.cs +++ b/Content.Server/StationEvents/Events/VentCrittersRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.StationEvents.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Components; diff --git a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs index ef3b5cf18a..6c1ad4f489 100644 --- a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs @@ -1,4 +1,5 @@ using Content.Server.GameTicking; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/Traitor/Systems/AutoTraitorSystem.cs b/Content.Server/Traitor/Systems/AutoTraitorSystem.cs index 15deae2552..e9307effbc 100644 --- a/Content.Server/Traitor/Systems/AutoTraitorSystem.cs +++ b/Content.Server/Traitor/Systems/AutoTraitorSystem.cs @@ -1,6 +1,7 @@ -using Content.Server.GameTicking.Rules; +using Content.Server.Antag; using Content.Server.Traitor.Components; using Content.Shared.Mind.Components; +using Robust.Shared.Prototypes; namespace Content.Server.Traitor.Systems; @@ -9,7 +10,10 @@ namespace Content.Server.Traitor.Systems; /// public sealed class AutoTraitorSystem : EntitySystem { - [Dependency] private readonly TraitorRuleSystem _traitorRule = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; + + [ValidatePrototypeId] + private const string DefaultTraitorRule = "Traitor"; public override void Initialize() { @@ -20,44 +24,6 @@ public sealed class AutoTraitorSystem : EntitySystem private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args) { - TryMakeTraitor(uid, comp); - } - - /// - /// Sets the GiveUplink field. - /// - public void SetGiveUplink(EntityUid uid, bool giveUplink, AutoTraitorComponent? comp = null) - { - if (!Resolve(uid, ref comp)) - return; - - comp.GiveUplink = giveUplink; - } - - /// - /// Sets the GiveObjectives field. - /// - public void SetGiveObjectives(EntityUid uid, bool giveObjectives, AutoTraitorComponent? comp = null) - { - if (!Resolve(uid, ref comp)) - return; - - comp.GiveObjectives = giveObjectives; - } - - /// - /// Checks if there is a mind, then makes it a traitor using the options. - /// - public bool TryMakeTraitor(EntityUid uid, AutoTraitorComponent? comp = null) - { - if (!Resolve(uid, ref comp)) - return false; - - //Start the rule if it has not already been started - var traitorRuleComponent = _traitorRule.StartGameRule(); - _traitorRule.MakeTraitor(uid, traitorRuleComponent, giveUplink: comp.GiveUplink, giveObjectives: comp.GiveObjectives); - // prevent spamming anything if it fails - RemComp(uid); - return true; + _antag.ForceMakeAntag(args.Mind.Comp.Session, DefaultTraitorRule); } } diff --git a/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs b/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs index cdaed3f928..79192f6b49 100644 --- a/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs +++ b/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs @@ -83,12 +83,9 @@ namespace Content.Server.Traitor.Uplink.Commands uplinkEntity = eUid; } - // Get TC count - var tcCount = _cfgManager.GetCVar(CCVars.TraitorStartingBalance); - Logger.Debug(_entManager.ToPrettyString(user)); // Finally add uplink var uplinkSys = _entManager.System(); - if (!uplinkSys.AddUplink(user, FixedPoint2.New(tcCount), uplinkEntity: uplinkEntity)) + if (!uplinkSys.AddUplink(user, 20, uplinkEntity: uplinkEntity)) { shell.WriteLine(Loc.GetString("add-uplink-command-error-2")); } diff --git a/Content.Server/Zombies/PendingZombieComponent.cs b/Content.Server/Zombies/PendingZombieComponent.cs index 10b62c05dc..98eae74f06 100644 --- a/Content.Server/Zombies/PendingZombieComponent.cs +++ b/Content.Server/Zombies/PendingZombieComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Damage; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Server.Zombies; @@ -35,6 +36,21 @@ public sealed partial class PendingZombieComponent : Component [DataField("gracePeriod"), ViewVariables(VVAccess.ReadWrite)] public TimeSpan GracePeriod = TimeSpan.Zero; + /// + /// The minimum amount of time initial infected have before they start taking infection damage. + /// + [DataField] + public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f); + + /// + /// The maximum amount of time initial infected have before they start taking damage. + /// + [DataField] + public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f); + + [DataField] + public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead"; + /// /// The chance each second that a warning will be shown. /// diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs index 080bef44e7..09c8fa26db 100644 --- a/Content.Server/Zombies/ZombieSystem.cs +++ b/Content.Server/Zombies/ZombieSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.Actions; using Content.Server.Body.Systems; using Content.Server.Chat; using Content.Server.Chat.Systems; @@ -30,6 +31,7 @@ namespace Content.Server.Zombies [Dependency] private readonly BloodstreamSystem _bloodstream = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly AutoEmoteSystem _autoEmote = default!; [Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; @@ -74,6 +76,8 @@ namespace Content.Server.Zombies } component.NextTick = _timing.CurTime + TimeSpan.FromSeconds(1f); + component.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace); + _actions.AddAction(uid, ref component.Action, component.ZombifySelfActionPrototype); } public override void Update(float frameTime) diff --git a/Content.Shared/Antag/AntagAcceptability.cs b/Content.Shared/Antag/AntagAcceptability.cs index 98abe713eb..02d0b5f58f 100644 --- a/Content.Shared/Antag/AntagAcceptability.cs +++ b/Content.Shared/Antag/AntagAcceptability.cs @@ -20,3 +20,8 @@ public enum AntagAcceptability All } +public enum AntagSelectionTime : byte +{ + PrePlayerSpawn, + PostPlayerSpawn +} diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 4a7b440846..2954cfecce 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -403,91 +403,6 @@ namespace Content.Shared.CCVar public static readonly CVarDef DiscordRoundEndRoleWebhook = CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY); - - /* - * Suspicion - */ - - public static readonly CVarDef SuspicionMinPlayers = - CVarDef.Create("suspicion.min_players", 5); - - public static readonly CVarDef SuspicionMinTraitors = - CVarDef.Create("suspicion.min_traitors", 2); - - public static readonly CVarDef SuspicionPlayersPerTraitor = - CVarDef.Create("suspicion.players_per_traitor", 6); - - public static readonly CVarDef SuspicionStartingBalance = - CVarDef.Create("suspicion.starting_balance", 20); - - public static readonly CVarDef SuspicionMaxTimeSeconds = - CVarDef.Create("suspicion.max_time_seconds", 300); - - /* - * Traitor - */ - - public static readonly CVarDef TraitorMinPlayers = - CVarDef.Create("traitor.min_players", 5); - - public static readonly CVarDef TraitorMaxTraitors = - CVarDef.Create("traitor.max_traitors", 12); // Assuming average server maxes somewhere from like 50-80 people - - public static readonly CVarDef TraitorPlayersPerTraitor = - CVarDef.Create("traitor.players_per_traitor", 10); - - public static readonly CVarDef TraitorCodewordCount = - CVarDef.Create("traitor.codeword_count", 4); - - public static readonly CVarDef TraitorStartingBalance = - CVarDef.Create("traitor.starting_balance", 20); - - public static readonly CVarDef TraitorMaxDifficulty = - CVarDef.Create("traitor.max_difficulty", 5); - - public static readonly CVarDef TraitorMaxPicks = - CVarDef.Create("traitor.max_picks", 20); - - public static readonly CVarDef TraitorStartDelay = - CVarDef.Create("traitor.start_delay", 4f * 60f); - - public static readonly CVarDef TraitorStartDelayVariance = - CVarDef.Create("traitor.start_delay_variance", 3f * 60f); - - /* - * TraitorDeathMatch - */ - - public static readonly CVarDef TraitorDeathMatchStartingBalance = - CVarDef.Create("traitordm.starting_balance", 20); - - /* - * Zombie - */ - - public static readonly CVarDef ZombieMinPlayers = - CVarDef.Create("zombie.min_players", 20); - - /* - * Pirates - */ - - public static readonly CVarDef PiratesMinPlayers = - CVarDef.Create("pirates.min_players", 25); - - public static readonly CVarDef PiratesMaxOps = - CVarDef.Create("pirates.max_pirates", 6); - - public static readonly CVarDef PiratesPlayersPerOp = - CVarDef.Create("pirates.players_per_pirate", 5); - - /* - * Nukeops - */ - - public static readonly CVarDef NukeopsSpawnGhostRoles = - CVarDef.Create("nukeops.spawn_ghost_roles", false); - /* * Tips */ diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs index 2a846d7fe2..ffb78dcf1f 100644 --- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs +++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs @@ -267,8 +267,11 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem /// The mob's entity UID. /// The character profile to load. /// Humanoid component of the entity - public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null) + public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null) { + if (profile == null) + return; + if (!Resolve(uid, ref humanoid)) { return; diff --git a/Content.Shared/Inventory/InventorySystem.Helpers.cs b/Content.Shared/Inventory/InventorySystem.Helpers.cs index 811387d375..7e325abe21 100644 --- a/Content.Shared/Inventory/InventorySystem.Helpers.cs +++ b/Content.Shared/Inventory/InventorySystem.Helpers.cs @@ -1,8 +1,6 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using Content.Shared.Hands.Components; using Content.Shared.Storage.EntitySystems; -using Robust.Shared.Containers; using Robust.Shared.Prototypes; namespace Content.Shared.Inventory; @@ -96,7 +94,7 @@ public partial class InventorySystem /// /// The entity that you want to spawn an item on /// A list of prototype IDs that you want to spawn in the bag. - public void SpawnItemsOnEntity(EntityUid entity, List items) + public void SpawnItemsOnEntity(EntityUid entity, List items) { foreach (var item in items) { diff --git a/Content.Shared/NukeOps/NukeOperativeComponent.cs b/Content.Shared/NukeOps/NukeOperativeComponent.cs index cdbefece9d..d19f0ae3e9 100644 --- a/Content.Shared/NukeOps/NukeOperativeComponent.cs +++ b/Content.Shared/NukeOps/NukeOperativeComponent.cs @@ -13,14 +13,9 @@ namespace Content.Shared.NukeOps; [RegisterComponent, NetworkedComponent] public sealed partial class NukeOperativeComponent : Component { - /// - /// Path to antagonist alert sound. - /// - [DataField("greetSoundNotification")] - public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg"); /// - /// + /// /// [DataField("syndStatusIcon", customTypeSerializer: typeof(PrototypeIdSerializer))] public string SyndStatusIcon = "SyndicateFaction"; diff --git a/Content.Shared/Roles/SharedRoleSystem.cs b/Content.Shared/Roles/SharedRoleSystem.cs index c25ac1968d..1a732eb750 100644 --- a/Content.Shared/Roles/SharedRoleSystem.cs +++ b/Content.Shared/Roles/SharedRoleSystem.cs @@ -62,6 +62,32 @@ public abstract class SharedRoleSystem : EntitySystem _antagTypes.Add(typeof(T)); } + public void MindAddRole(EntityUid mindId, Component component, MindComponent? mind = null, bool silent = false) + { + if (!Resolve(mindId, ref mind)) + return; + + if (HasComp(mindId, component.GetType())) + { + throw new ArgumentException($"We already have this role: {component}"); + } + + EntityManager.AddComponent(mindId, component); + var antagonist = IsAntagonistRole(component.GetType()); + + var mindEv = new MindRoleAddedEvent(silent); + RaiseLocalEvent(mindId, ref mindEv); + + var message = new RoleAddedEvent(mindId, mind, antagonist, silent); + if (mind.OwnedEntity != null) + { + RaiseLocalEvent(mind.OwnedEntity.Value, message, true); + } + + _adminLogger.Add(LogType.Mind, LogImpact.Low, + $"'Role {component}' added to mind of {_minds.MindOwnerLoggingString(mind)}"); + } + /// /// Gives this mind a new role. /// @@ -180,6 +206,11 @@ public abstract class SharedRoleSystem : EntitySystem return _antagTypes.Contains(typeof(T)); } + public bool IsAntagonistRole(Type component) + { + return _antagTypes.Contains(component); + } + /// /// Play a sound for the mind, if it has a session attached. /// Use this for role greeting sounds. diff --git a/Content.Shared/Station/SharedStationSpawningSystem.cs b/Content.Shared/Station/SharedStationSpawningSystem.cs index 49ef8509db..363fb3f91e 100644 --- a/Content.Shared/Station/SharedStationSpawningSystem.cs +++ b/Content.Shared/Station/SharedStationSpawningSystem.cs @@ -1,16 +1,17 @@ using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Inventory; -using Content.Shared.Preferences; using Content.Shared.Roles; using Content.Shared.Storage; using Content.Shared.Storage.EntitySystems; using Robust.Shared.Collections; +using Robust.Shared.Prototypes; namespace Content.Shared.Station; public abstract class SharedStationSpawningSystem : EntitySystem { + [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; [Dependency] protected readonly InventorySystem InventorySystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly SharedStorageSystem _storage = default!; @@ -21,8 +22,22 @@ public abstract class SharedStationSpawningSystem : EntitySystem /// /// Entity to load out. /// Starting gear to use. - public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear) + public void EquipStartingGear(EntityUid entity, ProtoId? startingGear) { + PrototypeManager.TryIndex(startingGear, out var gearProto); + EquipStartingGear(entity, gearProto); + } + + /// + /// Equips starting gear onto the given entity. + /// + /// Entity to load out. + /// Starting gear to use. + public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingGear) + { + if (startingGear == null) + return; + if (InventorySystem.TryGetSlots(entity, out var slotDefinitions)) { foreach (var slot in slotDefinitions) diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-pirates.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-pirates.ftl deleted file mode 100644 index 941643dd9a..0000000000 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-pirates.ftl +++ /dev/null @@ -1,10 +0,0 @@ -pirates-title = Privateers -pirates-description = A group of privateers has approached your lowly station. Hostile or not, their sole goal is to end the round with as many knicknacks on their ship as they can get. - -pirates-no-ship = Through unknown circumstances, the privateer's ship was completely and utterly destroyed. No score. -pirates-final-score = The privateers successfully obtained {$score} spesos worth -pirates-final-score-2 = of knicknacks, with a total of {$finalPrice} spesos. -pirates-list-start = The privateers were: -pirates-most-valuable = The most valuable stolen items were: -pirates-stolen-item-entry = {$entity} ({$credits} spesos) -pirates-stole-nothing = - The pirates stole absolutely nothing at all. Point and laugh. diff --git a/Resources/Maps/Shuttles/striker.yml b/Resources/Maps/Shuttles/striker.yml index 6a450f5266..88b113d7fd 100644 --- a/Resources/Maps/Shuttles/striker.yml +++ b/Resources/Maps/Shuttles/striker.yml @@ -1771,7 +1771,7 @@ entities: - type: Transform pos: 0.5436061,-7.5129323 parent: 325 -- proto: SpawnPointLoneNukeOperative +- proto: SpawnPointNukies entities: - uid: 322 components: diff --git a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml index ddbccfe686..727b55eb4e 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml @@ -84,8 +84,7 @@ name: ghost-role-information-loneop-name description: ghost-role-information-loneop-description rules: ghost-role-information-loneop-rules - - type: GhostRoleMobSpawner - prototype: MobHumanLoneNuclearOperative + - type: GhostRoleAntagSpawner - type: Sprite sprite: Markers/jobs.rsi layers: diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 32cfd69cb0..45519e840d 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -405,11 +405,28 @@ weight: 3 duration: 1 - type: ZombieRule - minStartDelay: 0 #let them know immediately - maxStartDelay: 10 - maxInitialInfected: 3 #fewer zombies - minInitialInfectedGrace: 300 #less time to prepare - maxInitialInfectedGrace: 450 + - type: AntagSelection + definitions: + - prefRoles: [ InitialInfected ] + max: 3 + playerRatio: 10 + blacklist: + components: + - ZombieImmune + - InitialInfectedExempt + briefing: + text: zombie-patientzero-role-greeting + color: Plum + sound: "/Audio/Ambience/Antag/zombie_start.ogg" + components: + - type: PendingZombie #less time to prepare than normal + minInitialInfectedGrace: 300 + maxInitialInfectedGrace: 450 + - type: ZombifyOnDeath + - type: IncurableZombie + mindComponents: + - type: InitialInfectedRole + prototype: InitialInfected - type: entity id: LoneOpsSpawn @@ -422,7 +439,29 @@ minimumPlayers: 20 reoccurrenceDelay: 30 duration: 1 - - type: LoneOpsSpawnRule + - type: LoadMapRule + mapPath: /Maps/Shuttles/striker.yml + - type: NukeopsRule + roundEndBehavior: Nothing + - type: AntagSelection + definitions: + - spawnerPrototype: SpawnPointLoneNukeOperative + min: 1 + max: 1 + pickPlayer: false + startingGear: SyndicateLoneOperativeGearFull + components: + - type: NukeOperative + - type: RandomMetadata + nameSegments: + - SyndicateNamesPrefix + - SyndicateNamesNormal + - type: NpcFactionMember + factions: + - Syndicate + mindComponents: + - type: NukeopsRole + prototype: Nukeops - type: entity id: MassHallucinations diff --git a/Resources/Prototypes/GameRules/midround.yml b/Resources/Prototypes/GameRules/midround.yml index 37fc4b44cd..bb870f6007 100644 --- a/Resources/Prototypes/GameRules/midround.yml +++ b/Resources/Prototypes/GameRules/midround.yml @@ -34,6 +34,23 @@ id: Thief components: - type: ThiefRule + - type: AntagSelection + definitions: + - prefRoles: [ Thief ] + maxRange: + min: 1 + max: 3 + playerRatio: 1 + allowNonHumans: true + multiAntagSetting: All + startingGear: ThiefGear + components: + - type: Pacified + mindComponents: + - type: ThiefRole + prototype: Thief + briefing: + sound: "/Audio/Misc/thief_greeting.ogg" - type: entity noSpawn: true diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 6d2b1f29d1..8218e1bdd1 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -70,29 +70,114 @@ components: - type: GameRule minPlayers: 20 + - type: RandomMetadata #this generates the random operation name cuz it's cool. + nameSegments: + - operationPrefix + - operationSuffix - type: NukeopsRule - faction: Syndicate - -- type: entity - id: Pirates - parent: BaseGameRule - noSpawn: true - components: - - type: PiratesRule + - type: LoadMapRule + gameMap: NukieOutpost + - type: AntagSelection + selectionTime: PrePlayerSpawn + definitions: + - prefRoles: [ NukeopsCommander ] + fallbackRoles: [ Nukeops, NukeopsMedic ] + max: 1 + playerRatio: 10 + startingGear: SyndicateCommanderGearFull + components: + - type: NukeOperative + - type: RandomMetadata + nameSegments: + - nukeops-role-commander + - SyndicateNamesElite + - type: NpcFactionMember + factions: + - Syndicate + mindComponents: + - type: NukeopsRole + prototype: NukeopsCommander + - prefRoles: [ NukeopsMedic ] + fallbackRoles: [ Nukeops, NukeopsCommander ] + max: 1 + playerRatio: 10 + startingGear: SyndicateOperativeMedicFull + components: + - type: NukeOperative + - type: RandomMetadata + nameSegments: + - nukeops-role-agent + - SyndicateNamesNormal + - type: NpcFactionMember + factions: + - Syndicate + mindComponents: + - type: NukeopsRole + prototype: NukeopsMedic + - prefRoles: [ Nukeops ] + fallbackRoles: [ NukeopsCommander, NukeopsMedic ] + min: 0 + max: 3 + playerRatio: 10 + startingGear: SyndicateOperativeGearFull + components: + - type: NukeOperative + - type: RandomMetadata + nameSegments: + - nukeops-role-operator + - SyndicateNamesNormal + - type: NpcFactionMember + factions: + - Syndicate + mindComponents: + - type: NukeopsRole + prototype: Nukeops - type: entity id: Traitor parent: BaseGameRule noSpawn: true components: + - type: GameRule + minPlayers: 5 + delay: + min: 240 + max: 420 - type: TraitorRule + - type: AntagSelection + definitions: + - prefRoles: [ Traitor ] + max: 12 + playerRatio: 10 + lateJoinAdditional: true + mindComponents: + - type: TraitorRole + prototype: Traitor - type: entity id: Revolutionary parent: BaseGameRule noSpawn: true components: + - type: GameRule + minPlayers: 15 - type: RevolutionaryRule + - type: AntagSelection + definitions: + - prefRoles: [ HeadRev ] + max: 3 + playerRatio: 15 + briefing: + text: head-rev-role-greeting + color: CornflowerBlue + sound: "/Audio/Ambience/Antag/headrev_start.ogg" + startingGear: HeadRevGear + components: + - type: Revolutionary + - type: HeadRevolutionary + mindComponents: + - type: RevolutionaryRole + prototype: HeadRev - type: entity id: Sandbox @@ -113,7 +198,32 @@ parent: BaseGameRule noSpawn: true components: + - type: GameRule + minPlayers: 20 + delay: + min: 600 + max: 900 - type: ZombieRule + - type: AntagSelection + definitions: + - prefRoles: [ InitialInfected ] + max: 6 + playerRatio: 10 + blacklist: + components: + - ZombieImmune + - InitialInfectedExempt + briefing: + text: zombie-patientzero-role-greeting + color: Plum + sound: "/Audio/Ambience/Antag/zombie_start.ogg" + components: + - type: PendingZombie + - type: ZombifyOnDeath + - type: IncurableZombie + mindComponents: + - type: InitialInfectedRole + prototype: InitialInfected # event schedulers - type: entity @@ -142,7 +252,6 @@ - id: BasicTrashVariationPass - id: SolidWallRustingVariationPass - id: ReinforcedWallRustingVariationPass - - id: CutWireVariationPass - id: BasicPuddleMessVariationPass prob: 0.99 orGroup: puddleMess diff --git a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml index 70d74b932a..946100fb96 100644 --- a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml +++ b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml @@ -273,8 +273,18 @@ #Head Rev Gear - type: startingGear id: HeadRevGear - equipment: - pocket2: Flash + storage: + back: + - Flash + - ClothingEyesGlassesSunglasses + +#Thief Gear +- type: startingGear + id: ThiefGear + storage: + back: + - ToolboxThief + - ClothingHandsChameleonThief #Gladiator with spear - type: startingGear diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml index a5b20a3db6..3bc6ae6208 100644 --- a/Resources/Prototypes/game_presets.yml +++ b/Resources/Prototypes/game_presets.yml @@ -153,15 +153,3 @@ - Zombie - BasicStationEventScheduler - BasicRoundstartVariation - -- type: gamePreset - id: Pirates - alias: - - pirates - name: pirates-title - description: pirates-description - showInVote: false - rules: - - Pirates - - BasicStationEventScheduler - - BasicRoundstartVariation From 6d16d6b1c973dfc5da7ec7e6cc3529b39a9dbf69 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Thu, 25 Apr 2024 13:54:25 +1200 Subject: [PATCH 57/61] Fix DoAfter attempt event null error (#27280) * Fix DoAfter attempt event null error * A --- Content.Shared/DoAfter/DoAfterEvent.cs | 2 +- Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs | 1 + Content.Shared/DoAfter/SharedDoAfterSystem.cs | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Content.Shared/DoAfter/DoAfterEvent.cs b/Content.Shared/DoAfter/DoAfterEvent.cs index c01505f9b2..bc9abdab87 100644 --- a/Content.Shared/DoAfter/DoAfterEvent.cs +++ b/Content.Shared/DoAfter/DoAfterEvent.cs @@ -73,7 +73,7 @@ public sealed partial class DoAfterAttemptEvent : CancellableEntityEvent public readonly DoAfter DoAfter; /// - /// The event that the DoAfter will raise after sucesfully finishing. Given that this event has the data + /// The event that the DoAfter will raise after successfully finishing. Given that this event has the data /// required to perform the interaction, it should also contain the data required to validate/attempt the /// interaction. /// diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs index 455491f524..4f77a271b3 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs @@ -104,6 +104,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem doAfter.AttemptEvent = _factory.CreateInstance(evType, new object[] { doAfter, args.Event }); } + args.Event.DoAfter = doAfter; if (args.EventTarget != null) RaiseLocalEvent(args.EventTarget.Value, doAfter.AttemptEvent, args.Broadcast); else diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.cs index d20da49485..9ad649683d 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.cs @@ -245,8 +245,9 @@ public abstract partial class SharedDoAfterSystem : EntitySystem if (args.AttemptFrequency == AttemptFrequency.StartAndEnd && !TryAttemptEvent(doAfter)) return false; - if (args.Delay <= TimeSpan.Zero || - _tag.HasTag(args.User, "InstantDoAfters")) + // TODO DO AFTER + // Why does this tag exist? Just make this a bool on the component? + if (args.Delay <= TimeSpan.Zero || _tag.HasTag(args.User, "InstantDoAfters")) { RaiseDoAfterEvents(doAfter, comp); // We don't store instant do-afters. This is just a lazy way of hiding them from client-side visuals. From d3b1178428e9e0e714e51523558335589078a76c Mon Sep 17 00:00:00 2001 From: beck-thompson <107373427+beck-thompson@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:19:16 -0700 Subject: [PATCH 58/61] Radio jammer update! (#25912) * Added selectable power level to radio jammer * Cleaned up OnGetVerb * Settings are now stored in the .yml file. Simplified stuff a lot! * Minor fixes! * Small little baby fix :) * Added the power level switch to the examine menu and also removed the ftl file as it was in the incorrect location. * Minor code cleanup * Changed byte -> int * Update sprite * Fixed licence * Added power LED that changes if the jammer is on low power. * Removed tabs * Changed github link to the commit * Changed all the RemComp to RemComDeferred * Moved NetworkedComponent to shared * Changed radio jammer textures back with minor edits * Added a space because it was annoying me * Jammer now updates range for suit sensors properly! Thanks nikthechampiongr :) * Removed useless comment * Cleaned up code that updates the range of tracking devices. * Fixed client namespace and removed newline * Cleaned up ChangeLEDState and ChangeChargeLevel. * Added comments * Read only * Fixed another comment * Locked in * Made server inherit shared * Update Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs * Update Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs * review fixes --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: metalgearsloth --- Content.Client/DeviceNetwork/JammerSystem.cs | 8 ++ .../Radio/Components/RadioJammerComponent.cs | 20 ----- .../Radio/EntitySystems/JammerSystem.cs | 68 ++++++++++----- .../Components/SharedRadioJammerComponent.cs | 74 +++++++++++++++++ .../Radio/EntitySystems/SharedJammerSystem.cs | 78 ++++++++++++++++++ Content.Shared/Verbs/VerbCategory.cs | 2 + .../components/radio-jammer-component.ftl | 10 +++ Resources/Locale/en-US/verbs/verb-system.ftl | 1 + .../Entities/Objects/Tools/jammer.yml | 32 ++++++- .../Objects/Devices/jammer.rsi/jammer.png | Bin 495 -> 939 bytes .../Devices/jammer.rsi/jammer_high_charge.png | Bin 0 -> 609 bytes .../Devices/jammer.rsi/jammer_low_charge.png | Bin 0 -> 617 bytes .../jammer.rsi/jammer_medium_charge.png | Bin 0 -> 608 bytes .../Objects/Devices/jammer.rsi/meta.json | 22 ++++- 14 files changed, 272 insertions(+), 43 deletions(-) create mode 100644 Content.Client/DeviceNetwork/JammerSystem.cs delete mode 100644 Content.Server/Radio/Components/RadioJammerComponent.cs create mode 100644 Content.Shared/Radio/Components/SharedRadioJammerComponent.cs create mode 100644 Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs create mode 100644 Resources/Textures/Objects/Devices/jammer.rsi/jammer_high_charge.png create mode 100644 Resources/Textures/Objects/Devices/jammer.rsi/jammer_low_charge.png create mode 100644 Resources/Textures/Objects/Devices/jammer.rsi/jammer_medium_charge.png diff --git a/Content.Client/DeviceNetwork/JammerSystem.cs b/Content.Client/DeviceNetwork/JammerSystem.cs new file mode 100644 index 0000000000..c7dbf8c8fe --- /dev/null +++ b/Content.Client/DeviceNetwork/JammerSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Radio.EntitySystems; + +namespace Content.Client.DeviceNetwork; + +public sealed class JammerSystem : SharedJammerSystem +{ + +} diff --git a/Content.Server/Radio/Components/RadioJammerComponent.cs b/Content.Server/Radio/Components/RadioJammerComponent.cs deleted file mode 100644 index 93504ef957..0000000000 --- a/Content.Server/Radio/Components/RadioJammerComponent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Server.Radio.EntitySystems; - -namespace Content.Server.Radio.Components; - -/// -/// When activated () prevents from sending messages in range -/// -[RegisterComponent] -[Access(typeof(JammerSystem))] -public sealed partial class RadioJammerComponent : Component -{ - [DataField("range"), ViewVariables(VVAccess.ReadWrite)] - public float Range = 8f; - - /// - /// Power usage per second when enabled - /// - [DataField("wattage"), ViewVariables(VVAccess.ReadWrite)] - public float Wattage = 2f; -} diff --git a/Content.Server/Radio/EntitySystems/JammerSystem.cs b/Content.Server/Radio/EntitySystems/JammerSystem.cs index 5a2a854017..4f58cb21e1 100644 --- a/Content.Server/Radio/EntitySystems/JammerSystem.cs +++ b/Content.Server/Radio/EntitySystems/JammerSystem.cs @@ -1,26 +1,22 @@ using Content.Server.DeviceNetwork.Components; -using Content.Server.DeviceNetwork.Systems; -using Content.Server.Medical.CrewMonitoring; using Content.Server.Popups; using Content.Server.Power.EntitySystems; using Content.Server.PowerCell; using Content.Server.Radio.Components; -using Content.Server.Station.Systems; using Content.Shared.DeviceNetwork.Components; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.PowerCell.Components; +using Content.Shared.RadioJammer; +using Content.Shared.Radio.EntitySystems; namespace Content.Server.Radio.EntitySystems; -public sealed class JammerSystem : EntitySystem +public sealed class JammerSystem : SharedJammerSystem { [Dependency] private readonly PowerCellSystem _powerCell = default!; [Dependency] private readonly BatterySystem _battery = default!; - [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly StationSystem _stationSystem = default!; - [Dependency] private readonly SingletonDeviceNetServerSystem _singletonServerSystem = default!; public override void Initialize() { @@ -35,14 +31,37 @@ public sealed class JammerSystem : EntitySystem public override void Update(float frameTime) { var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var _, out var jam)) { - if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery) && - !_battery.TryUseCharge(batteryUid.Value, jam.Wattage * frameTime, battery)) + + if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery)) { - RemComp(uid); - RemComp(uid); + if (!_battery.TryUseCharge(batteryUid.Value, GetCurrentWattage(jam) * frameTime, battery)) + { + ChangeLEDState(false, uid); + RemComp(uid); + RemComp(uid); + } + else + { + var percentCharged = battery.CurrentCharge / battery.MaxCharge; + if (percentCharged > .50) + { + ChangeChargeLevel(RadioJammerChargeLevel.High, uid); + } + else if (percentCharged < .15) + { + ChangeChargeLevel(RadioJammerChargeLevel.Low, uid); + } + else + { + ChangeChargeLevel(RadioJammerChargeLevel.Medium, uid); + } + } + } + } } @@ -50,40 +69,49 @@ public sealed class JammerSystem : EntitySystem { var activated = !HasComp(uid) && _powerCell.TryGetBatteryFromSlot(uid, out var battery) && - battery.CurrentCharge > comp.Wattage; + battery.CurrentCharge > GetCurrentWattage(comp); if (activated) { + ChangeLEDState(true, uid); EnsureComp(uid); EnsureComp(uid, out var jammingComp); - jammingComp.Range = comp.Range; + jammingComp.Range = GetCurrentRange(comp); jammingComp.JammableNetworks.Add(DeviceNetworkComponent.DeviceNetIdDefaults.Wireless.ToString()); Dirty(uid, jammingComp); } else { - RemComp(uid); - RemComp(uid); + ChangeLEDState(false, uid); + RemCompDeferred(uid); + RemCompDeferred(uid); } var state = Loc.GetString(activated ? "radio-jammer-component-on-state" : "radio-jammer-component-off-state"); var message = Loc.GetString("radio-jammer-component-on-use", ("state", state)); - _popup.PopupEntity(message, args.User, args.User); + Popup.PopupEntity(message, args.User, args.User); args.Handled = true; } private void OnPowerCellChanged(EntityUid uid, ActiveRadioJammerComponent comp, PowerCellChangedEvent args) { if (args.Ejected) - RemComp(uid); + { + ChangeLEDState(false, uid); + RemCompDeferred(uid); + } } private void OnExamine(EntityUid uid, RadioJammerComponent comp, ExaminedEvent args) { if (args.IsInDetailsRange) { - var msg = HasComp(uid) + var powerIndicator = HasComp(uid) ? Loc.GetString("radio-jammer-component-examine-on-state") : Loc.GetString("radio-jammer-component-examine-off-state"); - args.PushMarkup(msg); + args.PushMarkup(powerIndicator); + + var powerLevel = Loc.GetString(comp.Settings[comp.SelectedPowerLevel].Name); + var switchIndicator = Loc.GetString("radio-jammer-component-switch-setting", ("powerLevel", powerLevel)); + args.PushMarkup(switchIndicator); } } @@ -102,7 +130,7 @@ public sealed class JammerSystem : EntitySystem while (query.MoveNext(out _, out _, out var jam, out var transform)) { - if (source.InRange(EntityManager, _transform, transform.Coordinates, jam.Range)) + if (source.InRange(EntityManager, _transform, transform.Coordinates, GetCurrentRange(jam))) { return true; } diff --git a/Content.Shared/Radio/Components/SharedRadioJammerComponent.cs b/Content.Shared/Radio/Components/SharedRadioJammerComponent.cs new file mode 100644 index 0000000000..e5e52a3e47 --- /dev/null +++ b/Content.Shared/Radio/Components/SharedRadioJammerComponent.cs @@ -0,0 +1,74 @@ +using Robust.Shared.Serialization; +using Robust.Shared.GameStates; + +namespace Content.Shared.RadioJammer; + +/// +/// When activated () prevents from sending messages in range +/// Suit sensors will also stop working. +/// +[NetworkedComponent, RegisterComponent] +public sealed partial class RadioJammerComponent : Component +{ + [DataDefinition] + public partial struct RadioJamSetting + { + /// + /// Power usage per second when enabled. + /// + [DataField(required: true)] + public float Wattage; + + /// + /// Range of the jammer. + /// + [DataField(required: true)] + public float Range; + + /// + /// The message that is displayed when switched + /// to this setting. + /// + [DataField(required: true)] + public LocId Message = string.Empty; + + /// + /// Name of the setting. + /// + [DataField(required: true)] + public LocId Name = string.Empty; + } + + /// + /// List of all the settings for the radio jammer. + /// + [DataField(required: true), ViewVariables(VVAccess.ReadOnly)] + public RadioJamSetting[] Settings; + + /// + /// Index of the currently selected setting. + /// + [DataField] + public int SelectedPowerLevel = 1; +} + +[Serializable, NetSerializable] +public enum RadioJammerChargeLevel : byte +{ + Low, + Medium, + High +} + +[Serializable, NetSerializable] +public enum RadioJammerLayers : byte +{ + LED +} + +[Serializable, NetSerializable] +public enum RadioJammerVisuals : byte +{ + ChargeLevel, + LEDOn +} diff --git a/Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs b/Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs new file mode 100644 index 0000000000..e1f632735c --- /dev/null +++ b/Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs @@ -0,0 +1,78 @@ +using Content.Shared.Popups; +using Content.Shared.DeviceNetwork.Components; +using Content.Shared.Verbs; +using Content.Shared.RadioJammer; + +namespace Content.Shared.Radio.EntitySystems; + +public abstract class SharedJammerSystem : EntitySystem +{ + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] protected readonly SharedPopupSystem Popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnGetVerb); + } + + private void OnGetVerb(Entity entity, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + var user = args.User; + + byte index = 0; + foreach (var setting in entity.Comp.Settings) + { + // This is because Act wont work with index. + // Needs it to be saved in the loop. + var currIndex = index; + var verb = new Verb + { + Priority = currIndex, + Category = VerbCategory.PowerLevel, + Disabled = entity.Comp.SelectedPowerLevel == currIndex, + Act = () => + { + entity.Comp.SelectedPowerLevel = currIndex; + if (TryComp(entity.Owner, out var jammerComp)) + { + // This is a little sketcy but only way to do it. + jammerComp.Range = GetCurrentRange(entity.Comp); + Dirty(entity.Owner, jammerComp); + } + Popup.PopupPredicted(Loc.GetString(setting.Message), user, user); + }, + Text = Loc.GetString(setting.Name), + }; + args.Verbs.Add(verb); + index++; + } + } + + public float GetCurrentWattage(RadioJammerComponent jammer) + { + return jammer.Settings[jammer.SelectedPowerLevel].Wattage; + } + + public float GetCurrentRange(RadioJammerComponent jammer) + { + return jammer.Settings[jammer.SelectedPowerLevel].Range; + } + + protected void ChangeLEDState(bool isLEDOn, EntityUid uid, + AppearanceComponent? appearance = null) + { + _appearance.SetData(uid, RadioJammerVisuals.LEDOn, isLEDOn, appearance); + } + + protected void ChangeChargeLevel(RadioJammerChargeLevel chargeLevel, EntityUid uid, + AppearanceComponent? appearance = null) + { + _appearance.SetData(uid, RadioJammerVisuals.ChargeLevel, chargeLevel, appearance); + } + +} diff --git a/Content.Shared/Verbs/VerbCategory.cs b/Content.Shared/Verbs/VerbCategory.cs index d22041396f..9b9197249a 100644 --- a/Content.Shared/Verbs/VerbCategory.cs +++ b/Content.Shared/Verbs/VerbCategory.cs @@ -83,5 +83,7 @@ namespace Content.Shared.Verbs public static readonly VerbCategory Lever = new("verb-categories-lever", null); public static readonly VerbCategory SelectType = new("verb-categories-select-type", null); + + public static readonly VerbCategory PowerLevel = new("verb-categories-power-level", null); } } diff --git a/Resources/Locale/en-US/radio/components/radio-jammer-component.ftl b/Resources/Locale/en-US/radio/components/radio-jammer-component.ftl index 68efbf8d4e..eb540ee971 100644 --- a/Resources/Locale/en-US/radio/components/radio-jammer-component.ftl +++ b/Resources/Locale/en-US/radio/components/radio-jammer-component.ftl @@ -4,3 +4,13 @@ radio-jammer-component-off-state = off radio-jammer-component-examine-on-state = The light is currently [color=darkgreen]on[/color]. radio-jammer-component-examine-off-state = The light is currently [color=darkred]off[/color]. + +radio-jammer-component-setting-high = High +radio-jammer-component-setting-medium = Medium +radio-jammer-component-setting-low = Low + +radio-jammer-component-set-message-high = The jammer is now operating at high power. +radio-jammer-component-set-message-medium = The jammer is now operating at medium power. +radio-jammer-component-set-message-low = The jammer is now operating at low power. + +radio-jammer-component-switch-setting = The power level switch is set to "[color=yellow]{$powerLevel}[/color]". diff --git a/Resources/Locale/en-US/verbs/verb-system.ftl b/Resources/Locale/en-US/verbs/verb-system.ftl index 2bebddca61..c626e41ce1 100644 --- a/Resources/Locale/en-US/verbs/verb-system.ftl +++ b/Resources/Locale/en-US/verbs/verb-system.ftl @@ -28,6 +28,7 @@ verb-categories-timer = Set Delay verb-categories-lever = Lever verb-categories-select-type = Select Type verb-categories-fax = Set Destination +verb-categories-power-level = Power Level verb-common-toggle-light = Toggle light verb-common-close = Close diff --git a/Resources/Prototypes/Entities/Objects/Tools/jammer.yml b/Resources/Prototypes/Entities/Objects/Tools/jammer.yml index beb3695627..b456a23f1f 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/jammer.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/jammer.yml @@ -6,8 +6,26 @@ components: - type: Sprite sprite: Objects/Devices/jammer.rsi - state: jammer + layers: + - state: jammer + - state: jammer_high_charge + map: ["enum.RadioJammerLayers.LED"] + shader: unshaded + visible: false - type: RadioJammer + settings: + - wattage: 1 + range: 2.5 + message: radio-jammer-component-set-message-low + name: radio-jammer-component-setting-low + - wattage: 2 + range: 6 + message: radio-jammer-component-set-message-medium + name: radio-jammer-component-setting-medium + - wattage: 12 + range: 12 + message: radio-jammer-component-set-message-high + name: radio-jammer-component-setting-high - type: PowerCellSlot cellSlotId: cell_slot - type: ContainerContainer @@ -18,3 +36,15 @@ cell_slot: name: power-cell-slot-component-slot-name-default startingItem: PowerCellMedium + - type: Appearance + - type: GenericVisualizer + visuals: + enum.RadioJammerVisuals.LEDOn: + RadioJammerLayers.LED: + True: { visible: True } + False: { visible: False } + enum.RadioJammerVisuals.ChargeLevel: + RadioJammerLayers.LED: + Low: {state: jammer_low_charge} + Medium: {state: jammer_medium_charge} + High: {state: jammer_high_charge} diff --git a/Resources/Textures/Objects/Devices/jammer.rsi/jammer.png b/Resources/Textures/Objects/Devices/jammer.rsi/jammer.png index 6de27ba924f109e4b6bcf08e517a0ec822575e90..e1db2d05b65b8ea456bd56b17f35d59002099610 100644 GIT binary patch delta 918 zcmV;H18My41FHv+BYy#dX+uL$Nkc;*aB^>EX>4Tx04R}tkv&MmKpe$iQ^g_`2Rn#3 zWT;M7L`8JdDionYs1;guFuC*#nlvOSE{=k0!NHHks)LKOt`4q(Aou~|?BJy6A|?JW zDYS_3;J6>}?mh0_0YbgZG^=9_&~)2OCE{WxyDE0QB8U)x8Gnq*%rfRADGA^4b&mjF z?_xa5|JTxw zGo6|zju4B5Hdfl06-|wJk~perI^_!)k5$fFoV9Y5HSft^7|!V{%Uq{9hy)g~1PLM( zR8c}1He$5uq<>gQ(SF>=Kj8W$aw+60fstbY6=;xMKlmT~?$*jrjC)DpIMDgxI3Ghm z*DlbgInMX7<1|iy;4^TgxBQhlF#Sn-t))eefZlE3;<}|Nd%)!m(Ent}rtC^VnnFGg zyr0oGWr4n1pnJ{ht+|iW2Ov#dC2xR(LtwZ-+3P;<4u7@x_V1Zye?O3)a3o6NxxVLw_vB$HgKTV|bZN008cC8F508kH=#W zLXcSirIgnUBkcON*TVyVPP+|NRWX~-0RYqKH2mj!Fu=hw;5bgia@TbctXAP%;QI)C zf3Gl0Nee5L3U8QZ_= z5PvD^Pg|`-+mPxMsEUHJrlG89kfiPAJ$qi*{lNs7gz!eaj-o81*=*vKF+2`$A}ZFc z79w`ppV|QtLff*!IbnaCi0{Y~hfs9?wpc*db#%L3k%`PGr0IU;d9W=DB8a~xU{~O> sBGo-mDt)~Hwr&5Mq8vGL{5yOCMF5?%sSj!)^8f$<07*qoM6N<$f?9c$mjD0& delta 470 zcmV;{0V)2g2k!%rBYyx1a7bBm000ie000ie0hKEb8vpAR8@)rJF92xgwHvl{N zZX!X?BpI70XxV-kFW{)C5(7r$bP&DDXosQQ>mwy>VU^gL+{!e{8hHN%Fk6vFnIgha~02CWN zO~fF?1t19A0fhr_uW956ilRu`&1N$&B476agrM#HA&CGYfPVw<4UbB;qCIR_?*IS* M07*qoM6N<$f`zNd1ONa4 diff --git a/Resources/Textures/Objects/Devices/jammer.rsi/jammer_high_charge.png b/Resources/Textures/Objects/Devices/jammer.rsi/jammer_high_charge.png new file mode 100644 index 0000000000000000000000000000000000000000..e288427e7175f4e467ff323b90478f0e8e4d8485 GIT binary patch literal 609 zcmV-n0-pVeP)EX>4Tx04R}tkv&MmKpe$iQ^g_`2Rn#3WT;M7L`8JdDionYs1;guFuC*#nlvOS zE{=k0!NHHks)LKOt`4q(Aou~|?BJy6A|?JWDYS_3;J6>}?mh0_0YbgZG^=9_&~)2O zCE{WxyDE0QB8U)x8H~!zGUg;H3E%N`j{slqVm!4LCuVzla{SV+-++{ZuQ`XzEHEX>4Tx04R}tkv&MmKpe$iQ^g_`2Rn#3WT;M7L`8JdDionYs1;guFuC*#nlvOS zE{=k0!NHHks)LKOt`4q(Aou~|?BJy6A|?JWDYS_3;J6>}?mh0_0YbgZG^=9_&~)2O zCE{WxyDE0QB8U)x8H~!zGUg;H3E%N`j{slqVm!4LCuVzla{SV+-++{ZuQ`XzEHEX>4Tx04R}tkv&MmKpe$iQ^g_`2Rn#3WT;M7L`8JdDionYs1;guFuC*#nlvOS zE{=k0!NHHks)LKOt`4q(Aou~|?BJy6A|?JWDYS_3;J6>}?mh0_0YbgZG^=9_&~)2O zCE{WxyDE0QB8U)x8H~!zGUg;H3E%N`j{slqVm!4LCuVzla{SV+-++{ZuQ`XzEH8FWQhbVF}#ZDnqB07G(RVRU6= zAa`kWXdp*PO;A^X4i^9b069rSK~z}7?a?t203ZxQ(Hs_P2LDbvQ~)&)Ja8;<|Ls3Z uBme+jWXmhVH@3z})HRhAJOKaz{;&bN4GGL2ctrmI0000 Date: Thu, 25 Apr 2024 02:20:23 +0000 Subject: [PATCH 59/61] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index b40dc7fbe6..a357848aa5 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: Tayrtahn - changes: - - message: Drink bottles can now be opened and closed from the verbs menu, and some - have been given tamper-evident seals. - type: Add - id: 5934 - time: '2024-02-13T22:08:07.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/24780 - author: TheShuEd changes: - message: Darkness is coming. A new shadow anomaly added. @@ -3850,3 +3842,11 @@ id: 6433 time: '2024-04-24T21:27:34.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27226 +- author: Beck Thompson + changes: + - message: Radio jammer now has 3 selectable power operating levels and a battery + level led indicator! + type: Tweak + id: 6434 + time: '2024-04-25T02:19:17.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25912 From 960a50c0ffb67839f213e0c223e4f96e36df7f6d Mon Sep 17 00:00:00 2001 From: SoulFN <164462467+SoulFN@users.noreply.github.com> Date: Thu, 25 Apr 2024 19:02:25 +0800 Subject: [PATCH 60/61] Buff revenant structural damage (#27309) * 15 > 50 * update 50 >> 60 --- .../Revenant/EntitySystems/RevenantSystem.Abilities.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs index ffd5f75bf2..68a2624500 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs @@ -238,7 +238,7 @@ public sealed partial class RevenantSystem { //hardcoded damage specifiers til i die. var dspec = new DamageSpecifier(); - dspec.DamageDict.Add("Structural", 15); + dspec.DamageDict.Add("Structural", 60); _damage.TryChangeDamage(ent, dspec, origin: uid); } From cf45005471bbd07facbe2a5de841c08f3b8e84f0 Mon Sep 17 00:00:00 2001 From: ShadowCommander Date: Thu, 25 Apr 2024 09:26:21 -0700 Subject: [PATCH 61/61] Add command to toggle ghost visibility on the client (#27314) * Add command to toggle ghost visibility on the client * Fix description * Fix index of arg parsing * Allow setting GhostVisibility directly --- .../Commands/ToggleGhostVisibilityCommand.cs | 26 +++++++++++++++++++ Content.Client/Ghost/GhostSystem.cs | 5 ++-- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs diff --git a/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs b/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs new file mode 100644 index 0000000000..480da6ad8d --- /dev/null +++ b/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs @@ -0,0 +1,26 @@ +using Robust.Shared.Console; + +namespace Content.Client.Ghost.Commands; + +public sealed class ToggleGhostVisibilityCommand : IConsoleCommand +{ + [Dependency] private readonly IEntitySystemManager _entSysMan = default!; + + public string Command => "toggleghostvisibility"; + public string Description => "Toggles ghost visibility on the client."; + public string Help => "toggleghostvisibility [bool]"; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + var ghostSystem = _entSysMan.GetEntitySystem(); + + if (args.Length != 0 && bool.TryParse(args[0], out var visibility)) + { + ghostSystem.ToggleGhostVisibility(visibility); + } + else + { + ghostSystem.ToggleGhostVisibility(); + } + } +} diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs index c42e7cd0e0..94872a58ef 100644 --- a/Content.Client/Ghost/GhostSystem.cs +++ b/Content.Client/Ghost/GhostSystem.cs @@ -3,7 +3,6 @@ using Content.Shared.Actions; using Content.Shared.Ghost; using Robust.Client.Console; using Robust.Client.GameObjects; -using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Player; @@ -177,9 +176,9 @@ namespace Content.Client.Ghost _console.RemoteExecuteCommand(null, "ghostroles"); } - public void ToggleGhostVisibility() + public void ToggleGhostVisibility(bool? visibility = null) { - GhostVisibility = !GhostVisibility; + GhostVisibility = visibility ?? !GhostVisibility; } } }