This commit is contained in:
TytosB
2025-03-19 15:56:18 -05:00
501 changed files with 8629 additions and 2772 deletions

View File

@@ -85,10 +85,10 @@ internal sealed class AdminNameOverlay : Overlay
var currentOffset = Vector2.Zero;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
currentOffset += lineoffset;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
currentOffset += lineoffset;
if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && playTime)

View File

@@ -0,0 +1,5 @@
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Client.CartridgeLoader.Cartridges;
public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem;

View File

@@ -0,0 +1,32 @@
<Control xmlns="https://spacestation14.io" xmlns:system="clr-namespace:System;assembly=System.Runtime">
<BoxContainer Name="MainContainer"
Orientation="Horizontal"
SetWidth="250">
<Button Name="MainButton"
HorizontalExpand="True"
VerticalExpand="True"
StyleClasses="ButtonSquare"
Margin="-1 0 0 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Margin="-5 0 0 0">
<Label Name="TaskLabel"
StyleClasses="LabelSubText" />
<Label Name="TaskForLabel"
StyleClasses="LabelSubText"
Margin="0 -5 0 0" />
</BoxContainer>
</BoxContainer>
</Button>
<Button Name="DoneButton"
VerticalExpand="True"
Text="{Loc 'nano-task-ui-done'}">
<Button.StyleClasses>
<system:String>ButtonSmall</system:String>
<system:String>OpenLeft</system:String>
</Button.StyleClasses>
</Button>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,33 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Maths;
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Client.CartridgeLoader.Cartridges;
/// <summary>
/// Represents a single control for a single NanoTask item
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class NanoTaskItemControl : Control
{
public Action<int>? OnMainPressed;
public Action<int>? OnDonePressed;
public NanoTaskItemControl(NanoTaskItemAndId item)
{
RobustXamlLoader.Load(this);
TaskLabel.Text = item.Data.Description;
TaskLabel.FontColorOverride = Color.White;
TaskForLabel.Text = item.Data.TaskIsFor;
MainButton.OnPressed += _ => OnMainPressed?.Invoke(item.Id);
DoneButton.OnPressed += _ => OnDonePressed?.Invoke(item.Id);
MainButton.Disabled = item.Data.IsTaskDone;
DoneButton.Text = item.Data.IsTaskDone ? Loc.GetString("nano-task-ui-revert-done") : Loc.GetString("nano-task-ui-done");
}
}

View File

@@ -0,0 +1,67 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc nano-task-ui-item-title}"
MinSize="300 300">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" Margin="4">
<!-- Task Description Input -->
<BoxContainer Orientation="Vertical" Margin="0 4">
<Label Text="{Loc nano-task-ui-description-label}"
StyleClasses="LabelHeading" />
<PanelContainer StyleClasses="ButtonSquare">
<LineEdit Name="DescriptionInput"
PlaceHolder="{Loc nano-task-ui-description-placeholder}" />
</PanelContainer>
</BoxContainer>
<!-- Task Requester Input -->
<BoxContainer Orientation="Vertical" Margin="0 4">
<Label Text="{Loc nano-task-ui-requester-label}"
StyleClasses="LabelHeading" />
<PanelContainer StyleClasses="ButtonSquare">
<LineEdit Name="RequesterInput"
PlaceHolder="{Loc nano-task-ui-requester-placeholder}" />
</PanelContainer>
</BoxContainer>
<!-- Severity Buttons -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Center"
Margin="0 8 0 0">
<Button Name="LowButton"
Text="{Loc nano-task-ui-priority-low}"
StyleClasses="OpenRight"
MinSize="60 0" />
<Button Name="MediumButton"
Text="{Loc nano-task-ui-priority-medium}"
StyleClasses="ButtonSquare"
MinSize="60 0" />
<Button Name="HighButton"
Text="{Loc nano-task-ui-priority-high}"
StyleClasses="OpenLeft"
MinSize="60 0" />
</BoxContainer>
<!-- Verb Buttons -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0">
<Button Name="CancelButton"
Text="{Loc nano-task-ui-cancel}"
StyleClasses="OpenRight"
MinSize="60 0" />
<Button Name="DeleteButton"
Text="{Loc nano-task-ui-delete}"
StyleClasses="ButtonSquare"
MinSize="60 0" />
<Button Name="PrintButton"
Text="{Loc nano-task-ui-print}"
StyleClasses="ButtonSquare"
MinSize="60 0" />
<Button Name="SaveButton"
Text="{Loc nano-task-ui-save}"
StyleClasses="OpenLeft"
MinSize="60 0" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</DefaultWindow>

View File

@@ -0,0 +1,109 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.UserInterface.Controls;
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Client.CartridgeLoader.Cartridges;
/// <summary>
/// Popup displayed to edit a NanoTask item
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class NanoTaskItemPopup : DefaultWindow
{
private readonly ButtonGroup _priorityGroup = new();
private int? _editingTaskId = null;
public Action<int, NanoTaskItem>? TaskSaved;
public Action<int>? TaskDeleted;
public Action<NanoTaskItem>? TaskCreated;
public Action<NanoTaskItem>? TaskPrinted;
private NanoTaskItem MakeItem()
{
return new(
description: DescriptionInput.Text,
taskIsFor: RequesterInput.Text,
isTaskDone: false,
priority: _priorityGroup.Pressed switch {
var item when item == LowButton => NanoTaskPriority.Low,
var item when item == MediumButton => NanoTaskPriority.Medium,
var item when item == HighButton => NanoTaskPriority.High,
_ => NanoTaskPriority.Medium,
}
);
}
public NanoTaskItemPopup()
{
RobustXamlLoader.Load(this);
LowButton.Group = _priorityGroup;
MediumButton.Group = _priorityGroup;
HighButton.Group = _priorityGroup;
CancelButton.OnPressed += _ => Close();
DeleteButton.OnPressed += _ =>
{
if (_editingTaskId is int id)
{
TaskDeleted?.Invoke(id);
}
};
PrintButton.OnPressed += _ =>
{
TaskPrinted?.Invoke(MakeItem());
};
SaveButton.OnPressed += _ =>
{
if (_editingTaskId is int id)
{
TaskSaved?.Invoke(id, MakeItem());
}
else
{
TaskCreated?.Invoke(MakeItem());
}
};
DescriptionInput.OnTextChanged += args =>
{
if (args.Text.Length > NanoTaskItem.MaximumStringLength)
DescriptionInput.Text = args.Text[..NanoTaskItem.MaximumStringLength];
};
RequesterInput.OnTextChanged += args =>
{
if (args.Text.Length > NanoTaskItem.MaximumStringLength)
RequesterInput.Text = args.Text[..NanoTaskItem.MaximumStringLength];
};
}
public void SetEditingTaskId(int? id)
{
_editingTaskId = id;
DeleteButton.Visible = id is not null;
}
public void ResetInputs(NanoTaskItem? item)
{
if (item is NanoTaskItem task)
{
var button = task.Priority switch {
NanoTaskPriority.High => HighButton,
NanoTaskPriority.Medium => MediumButton,
NanoTaskPriority.Low => LowButton,
};
button.Pressed = true;
DescriptionInput.Text = task.Description;
RequesterInput.Text = task.TaskIsFor;
}
else
{
MediumButton.Pressed = true;
DescriptionInput.Text = "";
RequesterInput.Text = "";
}
}
}

View File

@@ -0,0 +1,82 @@
using System.Linq;
using Content.Client.UserInterface.Fragments;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.CartridgeLoader.Cartridges;
/// <summary>
/// UI fragment responsible for displaying NanoTask controls in a PDA and coordinating with the NanoTaskCartridgeSystem for state
/// </summary>
public sealed partial class NanoTaskUi : UIFragment
{
private NanoTaskUiFragment? _fragment;
private NanoTaskItemPopup? _popup;
public override Control GetUIFragmentRoot()
{
return _fragment!;
}
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
{
_fragment = new NanoTaskUiFragment();
_popup = new NanoTaskItemPopup();
_fragment.NewTask += () =>
{
_popup.ResetInputs(null);
_popup.SetEditingTaskId(null);
_popup.OpenCentered();
};
_fragment.OpenTask += id =>
{
if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task)
return;
_popup.ResetInputs(task.Data);
_popup.SetEditingTaskId(task.Id);
_popup.OpenCentered();
};
_fragment.ToggleTaskCompletion += id =>
{
if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task)
return;
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, new(
description: task.Data.Description,
taskIsFor: task.Data.TaskIsFor,
isTaskDone: !task.Data.IsTaskDone,
priority: task.Data.Priority
))))));
};
_popup.TaskSaved += (id, data) =>
{
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, data)))));
_popup.Close();
};
_popup.TaskDeleted += id =>
{
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskDeleteTask(id))));
_popup.Close();
};
_popup.TaskCreated += data =>
{
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskAddTask(data))));
_popup.Close();
};
_popup.TaskPrinted += data =>
{
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskPrintTask(data))));
};
}
public override void UpdateState(BoundUserInterfaceState state)
{
if (state is not NanoTaskUiState nanoTaskState)
return;
_fragment?.UpdateState(nanoTaskState.Tasks);
}
}

View File

@@ -0,0 +1,58 @@
<cartridges:NanoTaskUiFragment xmlns:cartridges="clr-namespace:Content.Client.CartridgeLoader.Cartridges"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns="https://spacestation14.io" Margin="1 0 2 0">
<PanelContainer StyleClasses="BackgroundDark"></PanelContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical" Margin="8" SeparationOverride="8">
<!-- Heading for High Priority Items -->
<BoxContainer Orientation="Horizontal">
<PanelContainer SetWidth="7" Margin="0 0 8 0">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#e93d58"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<Label Name="HighPriority" StyleClasses="LabelHeading"/>
</BoxContainer>
<!-- Location for High Priority Items -->
<GridContainer Name="HighContainer"
HorizontalExpand="True"
Access="Public"
Columns="2" />
<!-- Heading for Medium Priority Items -->
<BoxContainer Orientation="Horizontal">
<PanelContainer SetWidth="7" Margin="0 0 8 0">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#ef973c"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<Label Name="MediumPriority" StyleClasses="LabelHeading"/>
</BoxContainer>
<!-- Location for Medium Priority Items -->
<GridContainer Name="MediumContainer"
HorizontalExpand="True"
Access="Public"
Columns="2" />
<!-- Location for Low Priority Items -->
<BoxContainer Orientation="Horizontal">
<PanelContainer SetWidth="7" Margin="0 0 8 0">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#3dd425"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<Label Name="LowPriority" StyleClasses="LabelHeading"/>
</BoxContainer>
<!-- Location for Low Priority Items -->
<GridContainer Name="LowContainer"
HorizontalExpand="True"
Access="Public"
Columns="2" />
<Button Name="NewTaskButton" Text="{Loc 'nano-task-ui-new-task'}" HorizontalAlignment="Right"/>
</BoxContainer>
</ScrollContainer>
</BoxContainer>
</cartridges:NanoTaskUiFragment>

View File

@@ -0,0 +1,52 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Client.CartridgeLoader.Cartridges;
/// <summary>
/// Class displaying the main UI of NanoTask
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class NanoTaskUiFragment : BoxContainer
{
public Action<int>? OpenTask;
public Action<int>? ToggleTaskCompletion;
public Action? NewTask;
public List<NanoTaskItemAndId> Tasks = new();
public NanoTaskUiFragment()
{
RobustXamlLoader.Load(this);
Orientation = LayoutOrientation.Vertical;
HorizontalExpand = true;
VerticalExpand = true;
NewTaskButton.OnPressed += _ => NewTask?.Invoke();
}
public void UpdateState(List<NanoTaskItemAndId> tasks)
{
Tasks = tasks;
HighContainer.RemoveAllChildren();
MediumContainer.RemoveAllChildren();
LowContainer.RemoveAllChildren();
HighPriority.Text = Loc.GetString("nano-task-ui-heading-high-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.High)));
MediumPriority.Text = Loc.GetString("nano-task-ui-heading-medium-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.Medium)));
LowPriority.Text = Loc.GetString("nano-task-ui-heading-low-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.Low)));
foreach (var task in tasks)
{
var container = task.Data.Priority switch {
NanoTaskPriority.High => HighContainer,
NanoTaskPriority.Medium => MediumContainer,
NanoTaskPriority.Low => LowContainer,
};
var control = new NanoTaskItemControl(task);
container.AddChild(control);
control.OnMainPressed += id => OpenTask?.Invoke(id);
control.OnDonePressed += id => ToggleTaskCompletion?.Invoke(id);
}
}
}

View File

@@ -36,29 +36,6 @@ internal sealed class ShowSubFloor : LocalizedCommands
}
}
internal sealed class ShowSubFloorForever : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
public const string CommandName = "showsubfloorforever";
public override string Command => CommandName;
public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_entitySystemManager.GetEntitySystem<SubFloorHideSystem>().ShowAll = true;
var entMan = IoCManager.Resolve<IEntityManager>();
var components = entMan.EntityQuery<SubFloorHideComponent, SpriteComponent>(true);
foreach (var (_, sprite) in components)
{
sprite.DrawDepth = (int) DrawDepth.Overlays;
}
}
}
internal sealed class NotifyCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;

View File

@@ -24,7 +24,7 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands
{
_entitySystemManager.GetEntitySystem<MarkerSystem>().MarkersVisible = true;
_lightManager.Enabled = false;
shell.ExecuteCommand("showsubfloorforever");
shell.ExecuteCommand("showsubfloor");
_entitySystemManager.GetEntitySystem<ActionsSystem>().LoadActionAssignments("/mapping_actions.yml", false);
}
}

View File

@@ -3,6 +3,7 @@ using Content.Shared.Access.Systems;
using Content.Shared.Administration;
using Content.Shared.CriminalRecords;
using Content.Shared.Dataset;
using Content.Shared.Random.Helpers;
using Content.Shared.Security;
using Content.Shared.StationRecords;
using Robust.Client.AutoGenerated;
@@ -32,7 +33,7 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
public readonly EntityUid Console;
[ValidatePrototypeId<DatasetPrototype>]
[ValidatePrototypeId<LocalizedDatasetPrototype>]
private const string ReasonPlaceholders = "CriminalRecordsWantedReasonPlaceholders";
public Action<uint?>? OnKeySelected;
@@ -333,8 +334,8 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
var field = "reason";
var title = Loc.GetString("criminal-records-status-" + status.ToString().ToLower());
var placeholders = _proto.Index<DatasetPrototype>(ReasonPlaceholders);
var placeholder = Loc.GetString("criminal-records-console-reason-placeholder", ("placeholder", _random.Pick(placeholders.Values))); // just funny it doesn't actually get used
var placeholders = _proto.Index<LocalizedDatasetPrototype>(ReasonPlaceholders);
var placeholder = Loc.GetString("criminal-records-console-reason-placeholder", ("placeholder", _random.Pick(placeholders))); // just funny it doesn't actually get used
var prompt = Loc.GetString("criminal-records-console-reason");
var entry = new QuickDialogEntry(field, QuickDialogEntryType.LongText, prompt, placeholder);
var entries = new List<QuickDialogEntry>() { entry };

View File

@@ -26,6 +26,12 @@ namespace Content.Client.IconSmoothing
[ViewVariables(VVAccess.ReadWrite), DataField("key")]
public string? SmoothKey { get; private set; }
/// <summary>
/// Additional keys to smooth with.
/// </summary>
[DataField]
public List<string> AdditionalKeys = new();
/// <summary>
/// Prepended to the RSI state.
/// </summary>

View File

@@ -376,7 +376,8 @@ namespace Content.Client.IconSmoothing
while (candidates.MoveNext(out var entity))
{
if (smoothQuery.TryGetComponent(entity, out var other) &&
other.SmoothKey == smooth.SmoothKey &&
other.SmoothKey != null &&
(other.SmoothKey == smooth.SmoothKey || smooth.AdditionalKeys.Contains(other.SmoothKey)) &&
other.Enabled)
{
return true;

View File

@@ -64,9 +64,15 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
args.Sprite.Visible = hasVisibleLayer || revealed;
// allows a t-ray to show wires/pipes above carpets/puddles
if (scannerRevealed)
if (ShowAll)
{
// Allows sandbox mode to make wires visible over other stuff.
component.OriginalDrawDepth ??= args.Sprite.DrawDepth;
args.Sprite.DrawDepth = (int)Shared.DrawDepth.DrawDepth.Overdoors;
}
else if (scannerRevealed)
{
// Allows a t-ray to show wires/pipes above carpets/puddles.
if (component.OriginalDrawDepth is not null)
return;
component.OriginalDrawDepth = args.Sprite.DrawDepth;

View File

@@ -0,0 +1,94 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.DeviceLinking.Systems;
using Content.Shared.DeviceLinking;
using Content.Shared.Prototypes;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.DeviceLinking;
public sealed class DeviceLinkingTest
{
private const string PortTesterProtoId = "DeviceLinkingSinkPortTester";
[TestPrototypes]
private const string Prototypes = $@"
- type: entity
id: {PortTesterProtoId}
components:
- type: DeviceLinkSource
ports:
- Output
";
/// <summary>
/// Spawns every entity that has a <see cref="DeviceLinkSinkComponent"/>
/// and sends a signal to every port to make sure nothing causes an error.
/// </summary>
[Test]
public async Task AllDeviceLinkSinksWorkTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var compFact = server.ResolveDependency<IComponentFactory>();
var mapMan = server.ResolveDependency<IMapManager>();
var mapSys = server.System<SharedMapSystem>();
var deviceLinkSys = server.System<DeviceLinkSystem>();
var prototypes = server.ProtoMan.EnumeratePrototypes<EntityPrototype>();
await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
foreach (var proto in prototypes)
{
if (proto.Abstract || pair.IsTestPrototype(proto))
continue;
if (!proto.TryGetComponent<DeviceLinkSinkComponent>(out var protoSinkComp, compFact))
continue;
foreach (var port in protoSinkComp.Ports)
{
// Create a map for each entity/port combo so they can't interfere
mapSys.CreateMap(out var mapId);
var grid = mapMan.CreateGridEntity(mapId);
mapSys.SetTile(grid.Owner, grid.Comp, Vector2i.Zero, new Tile(1));
var coord = new EntityCoordinates(grid.Owner, 0, 0);
// Spawn the sink entity
var sinkEnt = server.EntMan.SpawnEntity(proto.ID, coord);
// Get the actual sink component, since the one we got from the prototype doesn't have its owner set up
Assert.That(server.EntMan.TryGetComponent<DeviceLinkSinkComponent>(sinkEnt, out var sinkComp),
$"Tester prototype does not have a DeviceLinkSourceComponent!");
// Spawn the tester
var sourceEnt = server.EntMan.SpawnEntity(PortTesterProtoId, coord);
Assert.That(server.EntMan.TryGetComponent<DeviceLinkSourceComponent>(sourceEnt, out var sourceComp),
$"Tester prototype does not have a DeviceLinkSourceComponent!");
// Create a link from the tester's output to the target port on the sink
deviceLinkSys.SaveLinks(null,
sourceEnt,
sinkEnt,
[("Output", port.Id)],
sourceComp,
sinkComp);
// Send a signal to the port
Assert.DoesNotThrow(() => { deviceLinkSys.InvokePort(sourceEnt, "Output", null, sourceComp); },
$"Exception thrown while triggering port {port.Id} of sink device {proto.ID}");
mapSys.DeleteMap(mapId);
}
}
});
});
await pair.CleanReturnAsync();
}
}

View File

@@ -152,10 +152,14 @@ public sealed class NukeOpsTest
Assert.That(roleSys.MindGetAllRoleInfo(mindCrew).Any(x => nukeroles.Contains(x.Prototype)), Is.False);
}
var ruleGridComps = entMan.AllComponents<RuleGridsComponent>();
Assert.That(ruleGridComps, Has.Length.EqualTo(1),
$"Unexpected RuleGrid(s) detected! {string.Join(',', ruleGridComps.Select(e => server.EntMan.ToPrettyString(e.Uid)))}");
// The game rule exists, and all the stations/shuttles/maps are properly initialized
var rule = entMan.AllComponents<NukeopsRuleComponent>().Single();
var ruleComp = rule.Component;
var gridsRule = entMan.AllComponents<RuleGridsComponent>().Single().Component;
var gridsRule = ruleGridComps.Single().Component;
foreach (var grid in gridsRule.MapGrids)
{
Assert.That(entMan.EntityExists(grid));

View File

@@ -19,16 +19,19 @@ public sealed class LocalizedDatasetPrototypeTest
var protos = protoMan.EnumeratePrototypes<LocalizedDatasetPrototype>().OrderBy(p => p.ID);
// Check each prototype
foreach (var proto in protos)
Assert.Multiple(() =>
{
// Check each value in the prototype
foreach (var locId in proto.Values)
// Check each prototype
foreach (var proto in protos)
{
// Make sure the localization manager has a string for the LocId
Assert.That(localizationMan.HasString(locId), $"LocalizedDataset {proto.ID} with prefix \"{proto.Values.Prefix}\" specifies {proto.Values.Count} entries, but no localized string was found matching {locId}!");
// Check each value in the prototype
foreach (var locId in proto.Values)
{
// Make sure the localization manager has a string for the LocId
Assert.That(localizationMan.HasString(locId), $"LocalizedDataset {proto.ID} with prefix \"{proto.Values.Prefix}\" specifies {proto.Values.Count} entries, but no localized string was found matching {locId}!");
}
}
}
});
await pair.CleanReturnAsync();
}

View File

@@ -102,6 +102,12 @@ public sealed class StoreTests
+ $"flag as 'true'. This marks the fact that cost modifier of discount is not applied properly!"
);
// The storeComponent returns discounted items with conditions randomly, so we remove these to sanitize the data.
foreach (var discountedItem in discountedListingItems)
{
discountedItem.Conditions = null;
}
// Refund action requests re-generation of listing items so we will be re-acquiring items from component a lot of times.
var itemIds = discountedListingItems.Select(x => x.ID);
foreach (var itemId in itemIds)
@@ -140,6 +146,9 @@ public sealed class StoreTests
// get refreshed item after refund re-generated items
discountedListingItem = storeComponent.FullListingsCatalog.First(x => x.ID == itemId);
// The storeComponent can give a discounted item a condition at random, so we remove it to sanitize the data.
discountedListingItem.Conditions = null;
var afterRefundBalance = storeComponent.Balance[UplinkSystem.TelecrystalCurrencyPrototype];
Assert.That(afterRefundBalance.Value, Is.EqualTo(originalBalance.Value), "Expected refund to return all discounted cost value.");
Assert.That(

View File

@@ -0,0 +1,205 @@
using System.Linq;
using Content.IntegrationTests.Tests.Interaction;
using Content.Server.VendingMachines;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.VendingMachines;
namespace Content.IntegrationTests.Tests.Vending;
public sealed class VendingInteractionTest : InteractionTest
{
private const string VendingMachineProtoId = "InteractionTestVendingMachine";
private const string VendedItemProtoId = "InteractionTestItem";
private const string RestockBoxProtoId = "InteractionTestRestockBox";
private const string RestockBoxOtherProtoId = "InteractionTestRestockBoxOther";
[TestPrototypes]
private const string TestPrototypes = $@"
- type: entity
parent: BaseItem
id: {VendedItemProtoId}
name: {VendedItemProtoId}
- type: vendingMachineInventory
id: InteractionTestVendingInventory
startingInventory:
{VendedItemProtoId}: 5
- type: vendingMachineInventory
id: InteractionTestVendingInventoryOther
startingInventory:
{VendedItemProtoId}: 5
- type: entity
parent: BaseVendingMachineRestock
id: {RestockBoxProtoId}
components:
- type: VendingMachineRestock
canRestock:
- InteractionTestVendingInventory
- type: entity
parent: BaseVendingMachineRestock
id: {RestockBoxOtherProtoId}
components:
- type: VendingMachineRestock
canRestock:
- InteractionTestVendingInventoryOther
- type: entity
id: {VendingMachineProtoId}
parent: VendingMachine
components:
- type: VendingMachine
pack: InteractionTestVendingInventory
ejectDelay: 0 # no delay to speed up tests
- type: Sprite
sprite: error.rsi
";
[Test]
public async Task InteractUITest()
{
await SpawnTarget(VendingMachineProtoId);
// Should start with no BUI open
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI was open unexpectedly.");
// Unpowered vending machine does not open BUI
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI opened without power.");
// Power the vending machine
var apc = await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
await RunTicks(1);
// Interacting with powered vending machine opens BUI
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), "BUI failed to open.");
// Interacting with it again closes the BUI
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI failed to close on interaction.");
// Reopen BUI for the next check
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), "BUI failed to reopen.");
// Remove power
await Delete(apc);
await RunTicks(1);
// The BUI should close when power is lost
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI failed to close on power loss.");
}
[Test]
public async Task DispenseItemTest()
{
await SpawnTarget(VendingMachineProtoId);
var vendorEnt = SEntMan.GetEntity(Target.Value);
var vendingSystem = SEntMan.System<VendingMachineSystem>();
var items = vendingSystem.GetAllInventory(vendorEnt);
// Verify initial item count
Assert.That(items, Is.Not.Empty, $"{VendingMachineProtoId} spawned with no items.");
Assert.That(items.First().Amount, Is.EqualTo(5), $"{VendingMachineProtoId} spawned with unexpected item count.");
// Power the vending machine
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
await RunTicks(1);
// Open the BUI
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), "BUI failed to open.");
// Request an item be dispensed
var ev = new VendingMachineEjectMessage(InventoryType.Regular, VendedItemProtoId);
await SendBui(VendingMachineUiKey.Key, ev);
// Make sure the stock decreased
Assert.That(items.First().Amount, Is.EqualTo(4), "Stocked item count did not decrease.");
// Make sure the dispensed item was spawned in to the world
await AssertEntityLookup(
("APCBasic", 1),
(VendedItemProtoId, 1)
);
}
[Test]
public async Task RestockTest()
{
var vendingSystem = SEntMan.System<VendingMachineSystem>();
await SpawnTarget(VendingMachineProtoId);
var vendorEnt = SEntMan.GetEntity(Target.Value);
var items = vendingSystem.GetAllInventory(vendorEnt);
Assert.That(items, Is.Not.Empty, $"{VendingMachineProtoId} spawned with no items.");
Assert.That(items.First().Amount, Is.EqualTo(5), $"{VendingMachineProtoId} spawned with unexpected item count.");
// Try to restock with the maintenance panel closed (nothing happens)
await InteractUsing(RestockBoxProtoId);
Assert.That(items.First().Amount, Is.EqualTo(5), "Restocked without opening maintenance panel.");
// Open the maintenance panel
await InteractUsing(Screw);
// Try to restock using the wrong restock box (nothing happens)
await InteractUsing(RestockBoxOtherProtoId);
Assert.That(items.First().Amount, Is.EqualTo(5), "Restocked with wrong restock box.");
// Restock the machine
await InteractUsing(RestockBoxProtoId);
Assert.That(items.First().Amount, Is.EqualTo(10), "Restocking resulted in unexpected item count.");
}
[Test]
public async Task RepairTest()
{
await SpawnTarget(VendingMachineProtoId);
// Power the vending machine
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
await RunTicks(1);
// Break it
await BreakVendor();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI did not close when vending machine broke.");
// Make sure we can't open the BUI while it's broken
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "Opened BUI of broken vending machine.");
// Repair the vending machine
await InteractUsing(Weld);
// Make sure the BUI can open now that the machine has been repaired
await Activate();
Assert.That(IsUiOpen(VendingMachineUiKey.Key), "Failed to open BUI after repair.");
}
private async Task BreakVendor()
{
var damageableSys = SEntMan.System<DamageableSystem>();
Assert.That(TryComp<DamageableComponent>(out var damageableComp), $"{VendingMachineProtoId} does not have DamageableComponent.");
Assert.That(damageableComp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero), $"{VendingMachineProtoId} started with unexpected damage.");
// Damage the vending machine to the point that it breaks
var damageType = ProtoMan.Index<DamageTypePrototype>("Blunt");
var damage = new DamageSpecifier(damageType, FixedPoint2.New(100));
await Server.WaitPost(() => damageableSys.TryChangeDamage(SEntMan.GetEntity(Target), damage, ignoreResistances: true));
await RunTicks(5);
Assert.That(damageableComp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero), $"{VendingMachineProtoId} did not take damage.");
}
}

View File

@@ -7,6 +7,8 @@ using Content.Shared.Database;
using Content.Shared.Roles;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Ban)]
@@ -15,6 +17,7 @@ public sealed class RoleBanCommand : IConsoleCommand
[Dependency] private readonly IPlayerLocator _locator = default!;
[Dependency] private readonly IBanManager _bans = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public string Command => "roleban";
public string Description => Loc.GetString("cmd-roleban-desc");
@@ -76,6 +79,12 @@ public sealed class RoleBanCommand : IConsoleCommand
return;
}
if (!_proto.HasIndex<JobPrototype>(job))
{
shell.WriteError(Loc.GetString("cmd-roleban-job-parse",("job", job)));
return;
}
var located = await _locator.LookupIdByNameOrIdAsync(target);
if (located == null)
{

View File

@@ -1,4 +1,5 @@
using Content.Server.Antag.Components;
using Content.Shared.GameTicking.Components;
using Content.Server.GameTicking.Rules;
namespace Content.Server.Antag;
@@ -14,9 +15,20 @@ public sealed class AntagRandomSpawnSystem : GameRuleSystem<AntagRandomSpawnComp
SubscribeLocalEvent<AntagRandomSpawnComponent, AntagSelectLocationEvent>(OnSelectLocation);
}
protected override void Added(EntityUid uid, AntagRandomSpawnComponent comp, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, comp, gameRule, args);
// we have to select this here because AntagSelectLocationEvent is raised twice because MakeAntag is called twice
// once when a ghost role spawner is created and once when someone takes the ghost role
if (TryFindRandomTile(out _, out _, out _, out var coords))
comp.Coords = coords;
}
private void OnSelectLocation(Entity<AntagRandomSpawnComponent> ent, ref AntagSelectLocationEvent args)
{
if (TryFindRandomTile(out _, out _, out _, out var coords))
args.Coordinates.Add(_transform.ToMapCoordinates(coords));
if (ent.Comp.Coords != null)
args.Coordinates.Add(_transform.ToMapCoordinates(ent.Comp.Coords.Value));
}
}

View File

@@ -3,7 +3,9 @@ using System.Linq;
using Content.Server.Antag.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Objectives;
using Content.Shared.Antag;
using Content.Shared.Chat;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.Preferences;
using JetBrains.Annotations;
@@ -25,7 +27,7 @@ public sealed partial class AntagSelectionSystem
definition = null;
var totalTargetCount = GetTargetAntagCount(ent, players);
var mindCount = ent.Comp.SelectedMinds.Count;
var mindCount = ent.Comp.AssignedMinds.Count;
if (mindCount >= totalTargetCount)
return false;
@@ -95,7 +97,7 @@ public sealed partial class AntagSelectionSystem
var countOffset = 0;
foreach (var otherDef in ent.Comp.Definitions)
{
countOffset += Math.Clamp((poolSize - countOffset) / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio;
countOffset += Math.Clamp((poolSize - countOffset) / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio; // Note: Is the PlayerRatio necessary here? Seems like it can cause issues for defs with varied PlayerRatio.
}
// make sure we don't double-count the current selection
countOffset -= Math.Clamp(poolSize / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
@@ -115,7 +117,7 @@ public sealed partial class AntagSelectionSystem
return new List<(EntityUid, SessionData, string)>();
var output = new List<(EntityUid, SessionData, string)>();
foreach (var (mind, name) in ent.Comp.SelectedMinds)
foreach (var (mind, name) in ent.Comp.AssignedMinds)
{
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
continue;
@@ -137,7 +139,7 @@ public sealed partial class AntagSelectionSystem
return new();
var output = new List<Entity<MindComponent>>();
foreach (var (mind, _) in ent.Comp.SelectedMinds)
foreach (var (mind, _) in ent.Comp.AssignedMinds)
{
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
continue;
@@ -155,7 +157,7 @@ public sealed partial class AntagSelectionSystem
if (!Resolve(ent, ref ent.Comp, false))
return new();
return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList();
return ent.Comp.AssignedMinds.Select(p => p.Item1).ToList();
}
/// <summary>
@@ -247,7 +249,7 @@ public sealed partial class AntagSelectionSystem
if (!Resolve(ent, ref ent.Comp, false))
return false;
return GetAliveAntagCount(ent) == ent.Comp.SelectedMinds.Count;
return GetAliveAntagCount(ent) == ent.Comp.AssignedMinds.Count;
}
/// <summary>
@@ -352,8 +354,66 @@ public sealed partial class AntagSelectionSystem
var ruleEnt = GameTicker.AddGameRule(id);
RemComp<LoadMapRuleComponent>(ruleEnt);
var antag = Comp<AntagSelectionComponent>(ruleEnt);
antag.SelectionsComplete = true; // don't do normal selection.
antag.AssignmentComplete = true; // don't do normal selection.
GameTicker.StartGameRule(ruleEnt);
return (ruleEnt, antag);
}
/// <summary>
/// Get all sessions that have been preselected for antag.
/// </summary>
/// <param name="except">A specific definition to be excluded from the check.</param>
public HashSet<ICommonSession> GetPreSelectedAntagSessions(AntagSelectionDefinition? except = null)
{
var result = new HashSet<ICommonSession>();
var query = QueryAllRules();
while (query.MoveNext(out var uid, out var comp, out _))
{
if (HasComp<EndedGameRuleComponent>(uid))
continue;
foreach (var def in comp.Definitions)
{
if (def.Equals(except))
continue;
if (comp.PreSelectedSessions.TryGetValue(def, out var set))
result.UnionWith(set);
}
}
return result;
}
/// <summary>
/// Get all sessions that have been preselected for antag and are exclusive, i.e. should not be paired with other antags.
/// </summary>
/// <param name="except">A specific definition to be excluded from the check.</param>
// Note: This is a bit iffy since technically this exclusive definition is defined via the MultiAntagSetting, while there's a separately tracked antagExclusive variable in the mindrole.
// We can't query that however since there's no guarantee the mindrole has been given out yet when checking pre-selected antags.
// I don't think there's any instance where they differ, but it's something to be aware of for a potential future refactor.
public HashSet<ICommonSession> GetPreSelectedExclusiveAntagSessions(AntagSelectionDefinition? except = null)
{
var result = new HashSet<ICommonSession>();
var query = QueryAllRules();
while (query.MoveNext(out var uid, out var comp, out _))
{
if (HasComp<EndedGameRuleComponent>(uid))
continue;
foreach (var def in comp.Definitions)
{
if (def.Equals(except))
continue;
if (def.MultiAntagSetting == AntagAcceptability.None && comp.PreSelectedSessions.TryGetValue(def, out var set))
{
result.UnionWith(set);
break;
}
}
}
return result;
}
}

View File

@@ -11,8 +11,11 @@ using Content.Server.Preferences.Managers;
using Content.Server.Roles;
using Content.Server.Roles.Jobs;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Events;
using Content.Shared.Administration.Logs;
using Content.Shared.Antag;
using Content.Shared.Clothing;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Ghost;
@@ -46,6 +49,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
[Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
// arbitrary random number to give late joining some mild interest.
public const float LateJoinRandomChance = 0.5f;
@@ -89,19 +93,33 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var comp, out _))
{
if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn)
if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn && comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
continue;
if (comp.SelectionsComplete)
if (comp.AssignmentComplete)
continue;
ChooseAntags((uid, comp), pool); // We choose the antags here...
if (comp.SelectionTime == AntagSelectionTime.PrePlayerSpawn)
{
AssignPreSelectedSessions((uid, comp)); // ...But only assign them if PrePlayerSpawn
foreach (var session in comp.AssignedSessions)
{
args.PlayerPool.Remove(session);
GameTicker.PlayerJoinGame(session);
}
}
}
// If IntraPlayerSpawn is selected, delayed rules should choose at this point too.
var queryDelayed = QueryDelayedRules();
while (queryDelayed.MoveNext(out var uid, out _, out var comp, out _))
{
if (comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
continue;
ChooseAntags((uid, comp), pool);
foreach (var session in comp.SelectedSessions)
{
args.PlayerPool.Remove(session);
GameTicker.PlayerJoinGame(session);
}
}
}
@@ -110,10 +128,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var comp, out _))
{
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn)
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn && comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
continue;
ChooseAntags((uid, comp), args.Players);
AssignPreSelectedSessions((uid, comp));
}
}
@@ -126,11 +145,13 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
// eventually this should probably store the players per definition with some kind of unique identifier.
// something to figure out later.
var query = QueryActiveRules();
var query = QueryAllRules();
var rules = new List<(EntityUid, AntagSelectionComponent)>();
while (query.MoveNext(out var uid, out _, out var antag, out _))
while (query.MoveNext(out var uid, out var antag, out _))
{
rules.Add((uid, antag));
if (HasComp<ActiveGameRuleComponent>(uid) ||
(HasComp<DelayedStartRuleComponent>(uid) && antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn)) //IntraPlayerSpawn selects antags before spawning, but doesn't activate until after.
rules.Add((uid, antag));
}
RobustRandom.Shuffle(rules);
@@ -142,7 +163,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!antag.Definitions.Any(p => p.LateJoinAdditional))
continue;
DebugTools.AssertEqual(antag.SelectionTime, AntagSelectionTime.PostPlayerSpawn);
DebugTools.AssertNotEqual(antag.SelectionTime, AntagSelectionTime.PrePlayerSpawn);
// do not count players in the lobby for the antag ratio
var players = _playerManager.NetworkedSessions.Count(x => x.AttachedEntity != null);
@@ -150,7 +171,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
continue;
if (TryMakeAntag((uid, antag), args.Player, def.Value))
var onlyPreSelect = (antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn && !antag.AssignmentComplete); // Don't wanna give them antag status if the rule hasn't assigned its existing ones yet
if (TryMakeAntag((uid, antag), args.Player, def.Value, onlyPreSelect: onlyPreSelect))
break;
}
}
@@ -183,14 +206,20 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (GameTicker.RunLevel != GameRunLevel.InRound)
return;
if (component.SelectionsComplete)
if (component.AssignmentComplete)
return;
var players = _playerManager.Sessions
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) && status == PlayerGameStatus.JoinedGame)
.ToList();
if (!component.PreSelectionsComplete)
{
var players = _playerManager.Sessions
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) &&
status == PlayerGameStatus.JoinedGame)
.ToList();
ChooseAntags((uid, component), players, midround: true);
ChooseAntags((uid, component), players, midround: true);
}
AssignPreSelectedSessions((uid, component));
}
/// <summary>
@@ -201,7 +230,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
{
if (ent.Comp.SelectionsComplete)
if (ent.Comp.PreSelectionsComplete)
return;
foreach (var def in ent.Comp.Definitions)
@@ -209,7 +238,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
ChooseAntags(ent, pool, def, midround: midround);
}
ent.Comp.SelectionsComplete = true;
ent.Comp.PreSelectionsComplete = true;
}
/// <summary>
@@ -250,21 +279,53 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
break;
}
if (session != null && ent.Comp.SelectedSessions.Contains(session))
if (session != null && ent.Comp.PreSelectedSessions.Values.Any(x => x.Contains(session)))
{
Log.Warning($"Somehow picked {session} for an antag when this rule already selected them previously");
continue;
}
}
MakeAntag(ent, session, def);
if (session == null)
MakeAntag(ent, null, def); // This is for spawner antags
else
{
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
set.Add(session); // Selection done!
Log.Debug($"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
_adminLogger.Add(LogType.AntagSelection, $"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
}
}
}
/// <summary>
/// Assigns antag roles to sessions selected for it.
/// </summary>
public void AssignPreSelectedSessions(Entity<AntagSelectionComponent> ent)
{
// Only assign if there's been a pre-selection, and the selection hasn't already been made
if (!ent.Comp.PreSelectionsComplete || ent.Comp.AssignmentComplete)
return;
foreach (var def in ent.Comp.Definitions)
{
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
continue;
foreach (var session in set)
{
TryMakeAntag(ent, session, def);
}
}
ent.Comp.AssignmentComplete = true;
}
/// <summary>
/// Tries to makes a given player into the specified antagonist.
/// </summary>
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true)
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true, bool onlyPreSelect = false)
{
if (checkPref && !HasPrimaryAntagPreference(session, def))
return false;
@@ -272,7 +333,19 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session?.AttachedEntity, def))
return false;
MakeAntag(ent, session, def, ignoreSpawner);
if (onlyPreSelect && session != null)
{
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
set.Add(session);
Log.Debug($"Pre-selected {session!.Name} as antagonist: {ToPrettyString(ent)}");
_adminLogger.Add(LogType.AntagSelection, $"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
}
else
{
MakeAntag(ent, session, def, ignoreSpawner);
}
return true;
}
@@ -286,7 +359,10 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (session != null)
{
ent.Comp.SelectedSessions.Add(session);
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
set.Add(session);
ent.Comp.AssignedSessions.Add(session);
// we shouldn't be blocking the entity if they're just a ghost or smth.
if (!HasComp<GhostComponent>(session.AttachedEntity))
@@ -309,10 +385,19 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
if (session != null)
ent.Comp.SelectedSessions.Remove(session);
{
ent.Comp.AssignedSessions.Remove(session);
ent.Comp.PreSelectedSessions[def].Remove(session);
}
return;
}
// TODO: This is really messy because this part runs twice for midround events.
// Once when the ghostrole spawner is created and once when a player takes it.
// Therefore any component subscribing to this has to make sure both subscriptions return the same value
// or the ghost role raffle location preview will be wrong.
var getPosEv = new AntagSelectLocationEvent(session, ent);
RaiseLocalEvent(ent, ref getPosEv, true);
if (getPosEv.Handled)
@@ -330,7 +415,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
if (session != null)
ent.Comp.SelectedSessions.Remove(session);
{
ent.Comp.AssignedSessions.Remove(session);
ent.Comp.PreSelectedSessions[def].Remove(session);
}
return;
}
@@ -363,10 +452,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
_role.MindAddRoles(curMind.Value, def.MindRoles, null, true);
ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
ent.Comp.AssignedMinds.Add((curMind.Value, Name(player)));
SendBriefing(session, def.Briefing);
Log.Debug($"Selected {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
Log.Debug($"Assigned {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
_adminLogger.Add(LogType.AntagSelection, $"Assigned {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
}
var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
@@ -412,15 +502,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
return false;
if (ent.Comp.SelectedSessions.Contains(session))
if (ent.Comp.AssignedSessions.Contains(session))
return false;
mind ??= session.GetMind();
// If the player has not spawned in as any entity (e.g., in the lobby), they can be given an antag role/entity.
if (mind == null)
return true;
//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)
@@ -429,12 +515,16 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
if (_role.MindIsAntagonist(mind))
return false;
if (GetPreSelectedAntagSessions(def).Contains(session)) // Used for rules where the antag has been selected, but not started yet
return false;
break;
}
case AntagAcceptability.NotExclusive:
{
if (_role.MindIsExclusiveAntagonist(mind))
return false;
if (GetPreSelectedExclusiveAntagSessions(def).Contains(session))
return false;
break;
}
}
@@ -481,7 +571,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (ent.Comp.AgentName is not { } name)
return;
args.Minds = ent.Comp.SelectedMinds;
args.Minds = ent.Comp.AssignedMinds;
args.AgentName = Loc.GetString(name);
}
}

View File

@@ -1,3 +1,5 @@
using Robust.Shared.Map;
namespace Content.Server.Antag.Components;
/// <summary>
@@ -5,4 +7,11 @@ namespace Content.Server.Antag.Components;
/// Requires <see cref="AntagSelectionComponent"/>.
/// </summary>
[RegisterComponent]
public sealed partial class AntagRandomSpawnComponent : Component;
public sealed partial class AntagRandomSpawnComponent : Component
{
/// <summary>
/// Location that was picked.
/// </summary>
[DataField]
public EntityCoordinates? Coords;
}

View File

@@ -14,10 +14,16 @@ namespace Content.Server.Antag.Components;
public sealed partial class AntagSelectionComponent : Component
{
/// <summary>
/// Has the primary selection of antagonists finished yet?
/// Has the primary assignment of antagonists finished yet?
/// </summary>
[DataField]
public bool SelectionsComplete;
public bool AssignmentComplete;
/// <summary>
/// Has the antagonists been preselected but yet to be fully assigned?
/// </summary>
[DataField]
public bool PreSelectionsComplete;
/// <summary>
/// The definitions for the antagonists
@@ -26,10 +32,10 @@ public sealed partial class AntagSelectionComponent : Component
public List<AntagSelectionDefinition> Definitions = new();
/// <summary>
/// The minds and original names of the players selected to be antagonists.
/// The minds and original names of the players assigned to be antagonists.
/// </summary>
[DataField]
public List<(EntityUid, string)> SelectedMinds = new();
public List<(EntityUid, string)> AssignedMinds = new();
/// <summary>
/// When the antag selection will occur.
@@ -37,11 +43,17 @@ public sealed partial class AntagSelectionComponent : Component
[DataField]
public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn;
/// <summary>
/// Cached sessions of antag definitions and selected players. Players in this dict are not guaranteed to have been assigned the role yet.
/// </summary>
[DataField]
public Dictionary<AntagSelectionDefinition, HashSet<ICommonSession>>PreSelectedSessions = new();
/// <summary>
/// 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.
/// </summary>
public HashSet<ICommonSession> SelectedSessions = new();
public HashSet<ICommonSession> AssignedSessions = new();
/// <summary>
/// Locale id for the name of the antag.

View File

@@ -0,0 +1,151 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.CartridgeLoader;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Paper;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.CartridgeLoader.Cartridges;
/// <summary>
/// Server-side class implementing the core UI logic of NanoTask
/// </summary>
public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem
{
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PaperSystem _paper = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeMessageEvent>(OnUiMessage);
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeRemovedEvent>(OnCartridgeRemoved);
SubscribeLocalEvent<NanoTaskInteractionComponent, InteractUsingEvent>(OnInteractUsing);
}
private void OnCartridgeRemoved(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeRemovedEvent args)
{
if (!_cartridgeLoader.HasProgram<NanoTaskCartridgeComponent>(args.Loader))
{
RemComp<NanoTaskInteractionComponent>(args.Loader);
}
}
private void OnInteractUsing(Entity<NanoTaskInteractionComponent> ent, ref InteractUsingEvent args)
{
if (!_cartridgeLoader.TryGetProgram<NanoTaskCartridgeComponent>(ent.Owner, out var uid, out var program))
{
return;
}
if (!EntityManager.TryGetComponent<NanoTaskPrintedComponent>(args.Used, out var printed))
{
return;
}
if (printed.Task is NanoTaskItem item)
{
program.Tasks.Add(new(program.Counter++, printed.Task));
args.Handled = true;
EntityManager.DeleteEntity(args.Used);
UpdateUiState(new Entity<NanoTaskCartridgeComponent>(uid.Value, program), ent.Owner);
}
}
/// <summary>
/// This gets called when the ui fragment needs to be updated for the first time after activating
/// </summary>
private void OnUiReady(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
{
UpdateUiState(ent, args.Loader);
}
private void SetupPrintedTask(EntityUid uid, NanoTaskItem item)
{
PaperComponent? paper = null;
NanoTaskPrintedComponent? printed = null;
if (!Resolve(uid, ref paper, ref printed))
return;
printed.Task = item;
var msg = new FormattedMessage();
msg.AddText(Loc.GetString("nano-task-printed-description", ("description", item.Description)));
msg.PushNewline();
msg.AddText(Loc.GetString("nano-task-printed-requester", ("requester", item.TaskIsFor)));
msg.PushNewline();
msg.AddText(item.Priority switch {
NanoTaskPriority.High => Loc.GetString("nano-task-printed-high-priority"),
NanoTaskPriority.Medium => Loc.GetString("nano-task-printed-medium-priority"),
NanoTaskPriority.Low => Loc.GetString("nano-task-printed-low-priority"),
_ => "",
});
_paper.SetContent((uid, paper), msg.ToMarkup());
}
/// <summary>
/// The ui messages received here get wrapped by a CartridgeMessageEvent and are relayed from the <see cref="CartridgeLoaderSystem"/>
/// </summary>
/// <remarks>
/// The cartridge specific ui message event needs to inherit from the CartridgeMessageEvent
/// </remarks>
private void OnUiMessage(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeMessageEvent args)
{
if (args is not NanoTaskUiMessageEvent message)
return;
switch (message.Payload)
{
case NanoTaskAddTask task:
if (!task.Item.Validate())
return;
ent.Comp.Tasks.Add(new(ent.Comp.Counter++, task.Item));
break;
case NanoTaskUpdateTask task:
{
if (!task.Item.Data.Validate())
return;
var idx = ent.Comp.Tasks.FindIndex(t => t.Id == task.Item.Id);
if (idx != -1)
ent.Comp.Tasks[idx] = task.Item;
break;
}
case NanoTaskDeleteTask task:
ent.Comp.Tasks.RemoveAll(t => t.Id == task.Id);
break;
case NanoTaskPrintTask task:
{
if (!task.Item.Validate())
return;
if (_timing.CurTime < ent.Comp.NextPrintAllowedAfter)
return;
ent.Comp.NextPrintAllowedAfter = _timing.CurTime + ent.Comp.PrintDelay;
var printed = Spawn("PaperNanoTaskItem", Transform(message.Actor).Coordinates);
_hands.PickupOrDrop(message.Actor, printed);
_audio.PlayPvs(new SoundPathSpecifier("/Audio/Machines/printer.ogg"), ent.Owner);
SetupPrintedTask(printed, task.Item);
break;
}
}
UpdateUiState(ent, GetEntity(args.LoaderUid));
}
private void UpdateUiState(Entity<NanoTaskCartridgeComponent> ent, EntityUid loaderUid)
{
var state = new NanoTaskUiState(ent.Comp.Tasks);
_cartridgeLoader.UpdateCartridgeUiState(loaderUid, state);
}
}

View File

@@ -5,9 +5,15 @@ using Content.Shared.Cloning.Events;
using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.NameModifier.Components;
using Content.Shared.StatusEffect;
using Content.Shared.Stacks;
using Content.Shared.Storage;
using Content.Shared.Storage.EntitySystems;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using System.Diagnostics.CodeAnalysis;
@@ -28,6 +34,10 @@ public sealed class CloningSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] private readonly SharedStackSystem _stack = default!;
[Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
/// <summary>
/// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
@@ -81,6 +91,15 @@ public sealed class CloningSystem : EntitySystem
if (settings.CopyEquipment != null)
CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
// Copy storage on the mob itself as well.
// This is needed for slime storage.
if (settings.CopyInternalStorage)
CopyStorage(original, clone.Value, settings.Whitelist, settings.Blacklist);
// copy implants and their storage contents
if (settings.CopyImplants)
CopyImplants(original, clone.Value, settings.CopyInternalStorage, settings.Whitelist, settings.Blacklist);
var originalName = Name(original);
if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
originalName = nameModComp.BaseName;
@@ -100,24 +119,122 @@ public sealed class CloningSystem : EntitySystem
/// Copies the equipment the original has to the clone.
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
/// </summary>
public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
public void CopyEquipment(Entity<InventoryComponent?> original, Entity<InventoryComponent?> clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(clone, out var cloneInventory))
if (!Resolve(original, ref original.Comp) || !Resolve(clone, ref clone.Comp))
return;
var coords = Transform(clone).Coordinates;
// Iterate over all inventory slots
var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
var slotEnumerator = _inventory.GetSlotEnumerator(original, slotFlags);
while (slotEnumerator.NextItem(out var item, out var slot))
{
// Spawn a copy of the item using the original prototype.
// This means any changes done to the item after spawning will be reset, but that should not be a problem for simple items like clothing etc.
// we use a whitelist and blacklist to be sure to exclude any problematic entities
var cloneItem = CopyItem(item, coords, whitelist, blacklist);
if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
continue;
var prototype = MetaData(item).EntityPrototype;
if (prototype != null)
_inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
if (cloneItem != null && !_inventory.TryEquip(clone, cloneItem.Value, slot.Name, silent: true, inventory: clone.Comp))
Del(cloneItem); // delete it again if the clone cannot equip it
}
}
/// <summary>
/// Copies an item and its storage recursively, placing all items at the same position in grid storage.
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
/// </summary>
/// <remarks>
/// This is not perfect and only considers item in storage containers.
/// Some components have their own additional spawn logic on map init, so we cannot just copy all containers.
/// </remarks>
public EntityUid? CopyItem(EntityUid original, EntityCoordinates coords, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
// we use a whitelist and blacklist to be sure to exclude any problematic entities
if (!_whitelist.CheckBoth(original, blacklist, whitelist))
return null;
var prototype = MetaData(original).EntityPrototype?.ID;
if (prototype == null)
return null;
var spawned = EntityManager.SpawnAtPosition(prototype, coords);
// if the original is a stack, adjust the count of the copy
if (TryComp<StackComponent>(original, out var originalStack) && TryComp<StackComponent>(spawned, out var spawnedStack))
_stack.SetCount(spawned, originalStack.Count, spawnedStack);
// if the original has items inside its storage, copy those as well
if (TryComp<StorageComponent>(original, out var originalStorage) && TryComp<StorageComponent>(spawned, out var spawnedStorage))
{
// remove all items that spawned with the entity inside its storage
// this ignores other containers, but this should be good enough for our purposes
_container.CleanContainer(spawnedStorage.Container);
// recursively replace them
// surely no one will ever create two items that contain each other causing an infinite loop, right?
foreach ((var itemUid, var itemLocation) in originalStorage.StoredItems)
{
var copy = CopyItem(itemUid, coords, whitelist, blacklist);
if (copy != null)
_storage.InsertAt((spawned, spawnedStorage), copy.Value, itemLocation, out _, playSound: false);
}
}
return spawned;
}
/// <summary>
/// Copies an item's storage recursively to another storage.
/// The storage grids should have the same shape or it will drop on the floor.
/// Basically the same as CopyItem, but we don't copy the outermost container.
/// </summary>
public void CopyStorage(Entity<StorageComponent?> original, Entity<StorageComponent?> target, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
if (!Resolve(original, ref original.Comp, false) || !Resolve(target, ref target.Comp, false))
return;
var coords = Transform(target).Coordinates;
// delete all items in the target storage
_container.CleanContainer(target.Comp.Container);
// recursively replace them
foreach ((var itemUid, var itemLocation) in original.Comp.StoredItems)
{
var copy = CopyItem(itemUid, coords, whitelist, blacklist);
if (copy != null)
_storage.InsertAt(target, copy.Value, itemLocation, out _, playSound: false);
}
}
/// <summary>
/// Copies all implants from one mob to another.
/// Might result in duplicates if the target already has them.
/// Can copy the storage inside a storage implant according to a whitelist and blacklist.
/// </summary>
/// <param name="original">Entity to copy implants from.</param>
/// <param name="target">Entity to copy implants to.</param>
/// <param name="copyStorage">If true will copy storage of the implants (E.g storage implant)</param>
/// <param name="whitelist">Whitelist for the storage copy (If copyStorage is true)</param>
/// <param name="blacklist">Blacklist for the storage copy (If copyStorage is true)</param>
public void CopyImplants(Entity<ImplantedComponent?> original, EntityUid target, bool copyStorage = false, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
if (!Resolve(original, ref original.Comp, false))
return; // they don't have any implants to copy!
foreach (var originalImplant in original.Comp.ImplantContainer.ContainedEntities)
{
if (!HasComp<SubdermalImplantComponent>(originalImplant))
continue; // not an implant (should only happen with admin shenanigans)
var implantId = MetaData(originalImplant).EntityPrototype?.ID;
if (implantId == null)
continue;
var targetImplant = _subdermalImplant.AddImplant(target, implantId);
if (copyStorage && targetImplant != null)
CopyStorage(originalImplant, targetImplant.Value, whitelist, blacklist); // only needed for storage implants
}
}
}

View File

@@ -1,12 +1,24 @@
using Content.Server.Explosion.EntitySystems;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Spawns a protoype when triggered.
/// </summary>
[RegisterComponent, Access(typeof(TriggerSystem))]
public sealed partial class SpawnOnTriggerComponent : Component
{
[ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Proto = string.Empty;
/// <summary>
/// The prototype to spawn.
/// </summary>
[DataField(required: true)]
public EntProtoId Proto = string.Empty;
/// <summary>
/// Use MapCoordinates for spawning?
/// Set to true if you don't want the new entity parented to the spawner.
/// </summary>
[DataField]
public bool mapCoords;
}

View File

@@ -1,15 +1,20 @@
namespace Content.Server.Explosion.Components
{
[RegisterComponent]
public sealed partial class TriggerOnCollideComponent : Component
{
[DataField("fixtureID", required: true)]
public string FixtureID = String.Empty;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Doesn't trigger if the other colliding fixture is nonhard.
/// </summary>
[DataField("ignoreOtherNonHard")]
public bool IgnoreOtherNonHard = true;
}
/// <summary>
/// Triggers when colliding with another entity.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnCollideComponent : Component
{
/// <summary>
/// The fixture with which to collide.
/// </summary>
[DataField(required: true)]
public string FixtureID = string.Empty;
/// <summary>
/// Doesn't trigger if the other colliding fixture is nonhard.
/// </summary>
[DataField]
public bool IgnoreOtherNonHard = true;
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Triggers on use in hand.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnUseComponent : Component { }

View File

@@ -0,0 +1,23 @@
using Content.Shared.Whitelist;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Checks if the user of a Trigger satisfies a whitelist and blacklist condition.
/// Cancels the trigger otherwise.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerWhitelistComponent : Component
{
/// <summary>
/// Whitelist for what entites can cause this trigger.
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Blacklist for what entites can cause this trigger.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
}

View File

@@ -14,6 +14,7 @@ using Content.Shared.Explosion.Components;
using Content.Shared.Explosion.Components.OnTrigger;
using Content.Shared.Implants.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
@@ -23,6 +24,7 @@ using Content.Shared.Slippery;
using Content.Shared.StepTrigger.Systems;
using Content.Shared.Trigger;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Whitelist;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
@@ -31,10 +33,7 @@ using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Player;
using Content.Shared.Coordinates;
using Robust.Shared.Utility;
using Robust.Shared.Timing;
namespace Content.Server.Explosion.EntitySystems
{
@@ -53,6 +52,12 @@ namespace Content.Server.Explosion.EntitySystems
}
}
/// <summary>
/// Raised before a trigger is activated.
/// </summary>
[ByRefEvent]
public record struct BeforeTriggerEvent(EntityUid Triggered, EntityUid? User, bool Cancelled = false);
/// <summary>
/// Raised when timer trigger becomes active.
/// </summary>
@@ -78,6 +83,7 @@ namespace Content.Server.Explosion.EntitySystems
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly ElectrocutionSystem _electrocution = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
public override void Initialize()
{
@@ -93,6 +99,7 @@ namespace Content.Server.Explosion.EntitySystems
SubscribeLocalEvent<TriggerOnSpawnComponent, MapInitEvent>(OnSpawnTriggered);
SubscribeLocalEvent<TriggerOnCollideComponent, StartCollideEvent>(OnTriggerCollide);
SubscribeLocalEvent<TriggerOnActivateComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<TriggerOnUseComponent, UseInHandEvent>(OnUse);
SubscribeLocalEvent<TriggerImplantActionComponent, ActivateImplantEvent>(OnImplantTrigger);
SubscribeLocalEvent<TriggerOnStepTriggerComponent, StepTriggeredOffEvent>(OnStepTriggered);
SubscribeLocalEvent<TriggerOnSlipComponent, SlipEvent>(OnSlipTriggered);
@@ -109,6 +116,13 @@ namespace Content.Server.Explosion.EntitySystems
SubscribeLocalEvent<SoundOnTriggerComponent, TriggerEvent>(OnSoundTrigger);
SubscribeLocalEvent<ShockOnTriggerComponent, TriggerEvent>(HandleShockTrigger);
SubscribeLocalEvent<RattleComponent, TriggerEvent>(HandleRattleTrigger);
SubscribeLocalEvent<TriggerWhitelistComponent, BeforeTriggerEvent>(HandleWhitelist);
}
private void HandleWhitelist(Entity<TriggerWhitelistComponent> ent, ref BeforeTriggerEvent args)
{
args.Cancelled = !_whitelist.CheckBoth(args.User, ent.Comp.Blacklist, ent.Comp.Whitelist);
}
private void OnSoundTrigger(EntityUid uid, SoundOnTriggerComponent component, TriggerEvent args)
@@ -155,16 +169,23 @@ namespace Content.Server.Explosion.EntitySystems
RemCompDeferred<AnchorOnTriggerComponent>(uid);
}
private void OnSpawnTrigger(EntityUid uid, SpawnOnTriggerComponent component, TriggerEvent args)
private void OnSpawnTrigger(Entity<SpawnOnTriggerComponent> ent, ref TriggerEvent args)
{
var xform = Transform(uid);
var xform = Transform(ent);
var coords = xform.Coordinates;
if (ent.Comp.mapCoords)
{
var mapCoords = _transformSystem.GetMapCoordinates(ent, xform);
Spawn(ent.Comp.Proto, mapCoords);
}
else
{
var coords = xform.Coordinates;
if (!coords.IsValid(EntityManager))
return;
Spawn(ent.Comp.Proto, coords);
if (!coords.IsValid(EntityManager))
return;
Spawn(component.Proto, coords);
}
}
private void HandleExplodeTrigger(EntityUid uid, ExplodeOnTriggerComponent component, TriggerEvent args)
@@ -248,6 +269,15 @@ namespace Content.Server.Explosion.EntitySystems
args.Handled = true;
}
private void OnUse(Entity<TriggerOnUseComponent> ent, ref UseInHandEvent args)
{
if (args.Handled)
return;
Trigger(ent.Owner, args.User);
args.Handled = true;
}
private void OnImplantTrigger(EntityUid uid, TriggerImplantActionComponent component, ActivateImplantEvent args)
{
args.Handled = Trigger(uid);
@@ -275,6 +305,11 @@ namespace Content.Server.Explosion.EntitySystems
public bool Trigger(EntityUid trigger, EntityUid? user = null)
{
var beforeTriggerEvent = new BeforeTriggerEvent(trigger, user);
RaiseLocalEvent(trigger, ref beforeTriggerEvent);
if (beforeTriggerEvent.Cancelled)
return false;
var triggerEvent = new TriggerEvent(trigger, user);
EntityManager.EventBus.RaiseLocalEvent(trigger, triggerEvent, true);
return triggerEvent.Handled;

View File

@@ -73,7 +73,6 @@ namespace Content.Server.Forensics
private void OnDNAInit(Entity<DnaComponent> ent, ref MapInitEvent args)
{
Log.Debug($"Init DNA {Name(ent.Owner)} {ent.Comp.DNA}");
if (ent.Comp.DNA == null)
RandomizeDNA((ent.Owner, ent.Comp));
else
@@ -327,7 +326,6 @@ namespace Content.Server.Forensics
ent.Comp.DNA = GenerateDNA();
Dirty(ent);
Log.Debug($"Randomize DNA {Name(ent.Owner)} {ent.Comp.DNA}");
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
RaiseLocalEvent(ent.Owner, ref ev);
}

View File

@@ -0,0 +1,43 @@
using Content.Shared.Cloning;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Gamerule component for spawning a paradox clone antagonist.
/// </summary>
[RegisterComponent]
public sealed partial class ParadoxCloneRuleComponent : Component
{
/// <summary>
/// Cloning settings to be used.
/// </summary>
[DataField]
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
/// <summary>
/// Visual effect spawned when gibbing at round end.
/// </summary>
[DataField]
public EntProtoId GibProto = "MobParadoxTimed";
/// <summary>
/// Mind entity of the original player.
/// Gets assigned when cloning.
/// </summary>
[DataField]
public EntityUid? Original;
/// <summary>
/// Whitelist for Objectives to be copied to the clone.
/// </summary>
[DataField]
public EntityWhitelist? ObjectiveWhitelist;
/// <summary>
/// Blacklist for Objectives to be copied to the clone.
/// </summary>
[DataField]
public EntityWhitelist? ObjectiveBlacklist;
}

View File

@@ -19,6 +19,11 @@ public abstract partial class GameRuleSystem<T> where T: IComponent
return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
}
protected EntityQueryEnumerator<DelayedStartRuleComponent, T, GameRuleComponent> QueryDelayedRules()
{
return EntityQueryEnumerator<DelayedStartRuleComponent, T, GameRuleComponent>();
}
/// <summary>
/// Queries all gamerules, regardless of if they're active or not.
/// </summary>

View File

@@ -0,0 +1,94 @@
using Content.Server.Antag;
using Content.Server.Cloning;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Medical.SuitSensors;
using Content.Server.Objectives.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Gibbing.Components;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.Mind;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
public sealed class ParadoxCloneRuleSystem : GameRuleSystem<ParadoxCloneRuleComponent>
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly CloningSystem _cloning = default!;
[Dependency] private readonly SuitSensorSystem _sensor = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ParadoxCloneRuleComponent, AntagSelectEntityEvent>(OnAntagSelectEntity);
SubscribeLocalEvent<ParadoxCloneRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagEntitySelected);
}
protected override void Started(EntityUid uid, ParadoxCloneRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
// check if we got enough potential cloning targets, otherwise cancel the gamerule so that the ghost role does not show up
var allHumans = _mind.GetAliveHumans();
if (allHumans.Count == 0)
{
Log.Info("Could not find any alive players to create a paradox clone from! Ending gamerule.");
ForceEndSelf(uid, gameRule);
}
}
// we have to do the spawning here so we can transfer the mind to the correct entity and can assign the objectives correctly
private void OnAntagSelectEntity(Entity<ParadoxCloneRuleComponent> ent, ref AntagSelectEntityEvent args)
{
if (args.Session?.AttachedEntity is not { } spawner)
return;
// get possible targets
var allHumans = _mind.GetAliveHumans();
// we already checked when starting the gamerule, but someone might have died since then.
if (allHumans.Count == 0)
{
Log.Warning("Could not find any alive players to create a paradox clone from!");
return;
}
// pick a random player
var playerToClone = _random.Pick(allHumans);
var bodyToClone = playerToClone.Comp.OwnedEntity;
if (bodyToClone == null || !_cloning.TryCloning(bodyToClone.Value, _transform.GetMapCoordinates(spawner), ent.Comp.Settings, out var clone))
{
Log.Error($"Unable to make a paradox clone of entity {ToPrettyString(bodyToClone)}");
return;
}
var targetComp = EnsureComp<TargetOverrideComponent>(clone.Value);
targetComp.Target = playerToClone.Owner; // set the kill target
var gibComp = EnsureComp<GibOnRoundEndComponent>(clone.Value);
gibComp.SpawnProto = ent.Comp.GibProto;
gibComp.PreventGibbingObjectives = new() { "ParadoxCloneKillObjective" }; // don't gib them if they killed the original.
// turn their suit sensors off so they don't immediately get noticed
_sensor.SetAllSensors(clone.Value, SuitSensorMode.SensorOff);
args.Entity = clone;
ent.Comp.Original = playerToClone.Owner;
}
private void AfterAntagEntitySelected(Entity<ParadoxCloneRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (ent.Comp.Original == null)
return;
if (!_mind.TryGetMind(args.EntityUid, out var cloneMindId, out var cloneMindComp))
return;
_mind.CopyObjectives(ent.Comp.Original.Value, (cloneMindId, cloneMindComp), ent.Comp.ObjectiveWhitelist, ent.Comp.ObjectiveBlacklist);
}
}

View File

@@ -189,7 +189,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
commandList.Add(id);
}
return IsGroupDetainedOrDead(commandList, true, true);
return IsGroupDetainedOrDead(commandList, true, true, true);
}
private void OnHeadRevMobStateChanged(EntityUid uid, HeadRevolutionaryComponent comp, MobStateChangedEvent ev)
@@ -214,7 +214,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
// If no Head Revs are alive all normal Revs will lose their Rev status and rejoin Nanotrasen
// Cuffing Head Revs is not enough - they must be killed.
if (IsGroupDetainedOrDead(headRevList, false, false))
if (IsGroupDetainedOrDead(headRevList, false, false, false))
{
var rev = AllEntityQuery<RevolutionaryComponent, MindContainerComponent>();
while (rev.MoveNext(out var uid, out _, out var mc))
@@ -251,34 +251,45 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
/// <param name="list">The list of the entities</param>
/// <param name="checkOffStation">Bool for if you want to check if someone is in space and consider them missing in action. (Won't check when emergency shuttle arrives just in case)</param>
/// <param name="countCuffed">Bool for if you don't want to count cuffed entities.</param>
/// <param name="countRevolutionaries">Bool for if you want to count revolutionaries.</param>
/// <returns></returns>
private bool IsGroupDetainedOrDead(List<EntityUid> list, bool checkOffStation, bool countCuffed)
private bool IsGroupDetainedOrDead(List<EntityUid> list, bool checkOffStation, bool countCuffed, bool countRevolutionaries)
{
var gone = 0;
foreach (var entity in list)
{
if (TryComp<CuffableComponent>(entity, out var cuffed) && cuffed.CuffedHandCount > 0 && countCuffed)
{
gone++;
continue;
}
else
if (TryComp<MobStateComponent>(entity, out var state))
{
if (TryComp<MobStateComponent>(entity, out var state))
{
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
{
gone++;
}
else if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
{
gone++;
}
}
//If they don't have the MobStateComponent they might as well be dead.
else
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
{
gone++;
continue;
}
if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
{
gone++;
continue;
}
}
//If they don't have the MobStateComponent they might as well be dead.
else
{
gone++;
continue;
}
if ((HasComp<RevolutionaryComponent>(entity) || HasComp<HeadRevolutionaryComponent>(entity)) && countRevolutionaries)
{
gone++;
continue;
}
}

View File

@@ -0,0 +1,55 @@
using Content.Shared.GameTicking;
using Content.Shared.Gibbing.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Systems;
using Content.Server.Body.Systems;
namespace Content.Server.Gibbing.Systems;
public sealed class GibOnRoundEndSystem : EntitySystem
{
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
public override void Initialize()
{
base.Initialize();
// this is raised after RoundEndTextAppendEvent, so they can successfully greentext before we gib them
SubscribeLocalEvent<RoundEndMessageEvent>(OnRoundEnd);
}
private void OnRoundEnd(RoundEndMessageEvent args)
{
var gibQuery = EntityQueryEnumerator<GibOnRoundEndComponent>();
// gib everyone with the component
while (gibQuery.MoveNext(out var uid, out var gibComp))
{
var gib = false;
// if they fulfill all objectives given in the component they are not gibbed
if (_mind.TryGetMind(uid, out var mindId, out var mindComp))
{
foreach (var objectiveId in gibComp.PreventGibbingObjectives)
{
if (!_mind.TryFindObjective((mindId, mindComp), objectiveId, out var objective)
|| !_objectives.IsCompleted(objective.Value, (mindId, mindComp)))
{
gib = true;
break;
}
}
}
else
gib = true;
if (!gib)
continue;
if (gibComp.SpawnProto != null)
SpawnAtPosition(gibComp.SpawnProto, Transform(uid).Coordinates);
_body.GibBody(uid, splatModifier: 5f);
}
}
}

View File

@@ -15,6 +15,7 @@ using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
@@ -44,6 +45,7 @@ public sealed class SuitSensorSystem : EntitySystem
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
public override void Initialize()
{
@@ -347,6 +349,20 @@ public sealed class SuitSensorSystem : EntitySystem
}
}
/// <summary>
/// Set all suit sensors on the equipment someone is wearing to the specified mode.
/// </summary>
public void SetAllSensors(EntityUid target, SuitSensorMode mode, SlotFlags slots = SlotFlags.All )
{
// iterate over all inventory slots
var slotEnumerator = _inventory.GetSlotEnumerator(target, slots);
while (slotEnumerator.NextItem(out var item, out _))
{
if (TryComp<SuitSensorComponent>(item, out var sensorComp))
SetSensor((item, sensorComp), mode);
}
}
public SuitSensorStatus? GetSensorState(EntityUid uid, SuitSensorComponent? sensor = null, TransformComponent? transform = null)
{
if (!Resolve(uid, ref sensor, ref transform))

View File

@@ -8,6 +8,7 @@ using Content.Shared.Implants.Components;
using Content.Shared.Mindshield.Components;
using Content.Shared.Revolutionary.Components;
using Content.Shared.Tag;
using Robust.Shared.Containers;
namespace Content.Server.Mindshield;
@@ -29,6 +30,7 @@ public sealed class MindShieldSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<SubdermalImplantComponent, ImplantImplantedEvent>(ImplantCheck);
SubscribeLocalEvent<MindShieldImplantComponent, EntGotRemovedFromContainerMessage>(OnImplantDraw);
}
/// <summary>
@@ -61,4 +63,10 @@ public sealed class MindShieldSystem : EntitySystem
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(implanted)} was deconverted due to being implanted with a Mindshield.");
}
}
private void OnImplantDraw(Entity<MindShieldImplantComponent> ent, ref EntGotRemovedFromContainerMessage args)
{
RemComp<MindShieldComponent>(args.Container.Owner);
}
}

View File

@@ -1,12 +1,8 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random head.
/// If there are no heads it will fallback to any person.
/// </summary>
[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
public sealed partial class PickRandomHeadComponent : Component
{
}
[RegisterComponent]
public sealed partial class PickRandomHeadComponent : Component;

View File

@@ -1,11 +1,7 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random person.
/// </summary>
[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
public sealed partial class PickRandomPersonComponent : Component
{
}
[RegisterComponent]
public sealed partial class PickRandomPersonComponent : Component;

View File

@@ -0,0 +1,8 @@
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets this objective's target to the one given in <see cref="TargetOverrideComponent"/>, if the entity has it.
/// This component needs to be added to objective entity itself.
/// </summary>
[RegisterComponent]
public sealed partial class PickSpecificPersonComponent : Component;

View File

@@ -1,11 +1,7 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="KeepAliveConditionComponent"/> to a random traitor.
/// </summary>
[RegisterComponent, Access(typeof(KeepAliveConditionSystem))]
public sealed partial class RandomTraitorAliveComponent : Component
{
}
[RegisterComponent]
public sealed partial class RandomTraitorAliveComponent : Component;

View File

@@ -1,11 +1,7 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="HelpProgressConditionComponent"/> to a random traitor.
/// </summary>
[RegisterComponent, Access(typeof(HelpProgressConditionSystem))]
public sealed partial class RandomTraitorProgressComponent : Component
{
}
[RegisterComponent]
public sealed partial class RandomTraitorProgressComponent : Component;

View File

@@ -0,0 +1,16 @@
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets a target objective to a specific target when receiving it.
/// The objective entity needs to have <see cref="PickSpecificPersonComponent"/>.
/// This component needs to be added to entity receiving the objective.
/// </summary>
[RegisterComponent]
public sealed partial class TargetOverrideComponent : Component
{
/// <summary>
/// The entity that should be targeted.
/// </summary>
[DataField]
public EntityUid? Target;
}

View File

@@ -1,31 +1,23 @@
using Content.Server.GameTicking.Rules;
using Content.Server.Objectives.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles help progress condition logic and picking random help targets.
/// Handles help progress condition logic.
/// </summary>
public sealed class HelpProgressConditionSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HelpProgressConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
SubscribeLocalEvent<RandomTraitorProgressComponent, ObjectiveAssignedEvent>(OnTraitorAssigned);
}
private void OnGetProgress(EntityUid uid, HelpProgressConditionComponent comp, ref ObjectiveGetProgressEvent args)
@@ -36,55 +28,6 @@ public sealed class HelpProgressConditionSystem : EntitySystem
args.Progress = GetProgress(target.Value);
}
private void OnTraitorAssigned(EntityUid uid, RandomTraitorProgressComponent comp, ref ObjectiveAssignedEvent args)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
{
args.Cancelled = true;
return;
}
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
// cant help anyone who is tasked with helping:
// 1. thats boring
// 2. no cyclic progress dependencies!!!
foreach (var traitor in traitors)
{
// TODO: replace this with TryComp<ObjectivesComponent>(traitor) or something when objectives are moved out of mind
if (!TryComp<MindComponent>(traitor.Id, out var mind))
continue;
foreach (var objective in mind.Objectives)
{
if (HasComp<HelpProgressConditionComponent>(objective))
traitors.RemoveWhere(x => x.Mind == mind);
}
}
// Can't have multiple objectives to help/save the same person
foreach (var objective in args.Mind.Objectives)
{
if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
{
if (TryComp<TargetObjectiveComponent>(objective, out var help))
{
traitors.RemoveWhere(x => x.Id == help.Target);
}
}
}
// no more helpable traitors
if (traitors.Count == 0)
{
args.Cancelled = true;
return;
}
_target.SetTarget(uid, _random.Pick(traitors).Id, target);
}
private float GetProgress(EntityUid target)
{
var total = 0f; // how much progress they have

View File

@@ -1,30 +1,22 @@
using Content.Server.Objectives.Components;
using Content.Server.GameTicking.Rules;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles keep alive condition logic and picking random traitors to keep alive.
/// Handles keep alive condition logic.
/// </summary>
public sealed class KeepAliveConditionSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<KeepAliveConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
SubscribeLocalEvent<RandomTraitorAliveComponent, ObjectiveAssignedEvent>(OnAssigned);
}
private void OnGetProgress(EntityUid uid, KeepAliveConditionComponent comp, ref ObjectiveGetProgressEvent args)
@@ -35,39 +27,6 @@ public sealed class KeepAliveConditionSystem : EntitySystem
args.Progress = GetProgress(target.Value);
}
private void OnAssigned(EntityUid uid, RandomTraitorAliveComponent comp, ref ObjectiveAssignedEvent args)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
{
args.Cancelled = true;
return;
}
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
// Can't have multiple objectives to help/save the same person
foreach (var objective in args.Mind.Objectives)
{
if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
{
if (TryComp<TargetObjectiveComponent>(objective, out var help))
{
traitors.RemoveWhere(x => x.Id == help.Target);
}
}
}
// You are the first/only traitor.
if (traitors.Count == 0)
{
args.Cancelled = true;
return;
}
_target.SetTarget(uid, _random.Pick(traitors).Id, target);
}
private float GetProgress(EntityUid target)
{
if (!TryComp<MindComponent>(target, out var mind))

View File

@@ -1,12 +1,9 @@
using Content.Server.Objectives.Components;
using Content.Server.Revolutionary.Components;
using Content.Server.Shuttles.Systems;
using Content.Shared.CCVar;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Robust.Shared.Configuration;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Objectives.Systems;
@@ -17,7 +14,6 @@ public sealed class KillPersonConditionSystem : EntitySystem
{
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
@@ -26,10 +22,6 @@ public sealed class KillPersonConditionSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<KillPersonConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
SubscribeLocalEvent<PickRandomPersonComponent, ObjectiveAssignedEvent>(OnPersonAssigned);
SubscribeLocalEvent<PickRandomHeadComponent, ObjectiveAssignedEvent>(OnHeadAssigned);
}
private void OnGetProgress(EntityUid uid, KillPersonConditionComponent comp, ref ObjectiveGetProgressEvent args)
@@ -40,74 +32,6 @@ public sealed class KillPersonConditionSystem : EntitySystem
args.Progress = GetProgress(target.Value, comp.RequireDead);
}
private void OnPersonAssigned(EntityUid uid, PickRandomPersonComponent comp, ref ObjectiveAssignedEvent args)
{
// invalid objective prototype
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
{
args.Cancelled = true;
return;
}
// target already assigned
if (target.Target != null)
return;
var allHumans = _mind.GetAliveHumans(args.MindId);
// Can't have multiple objectives to kill the same person
foreach (var objective in args.Mind.Objectives)
{
if (HasComp<KillPersonConditionComponent>(objective) && TryComp<TargetObjectiveComponent>(objective, out var kill))
{
allHumans.RemoveWhere(x => x.Owner == kill.Target);
}
}
// no other humans to kill
if (allHumans.Count == 0)
{
args.Cancelled = true;
return;
}
_target.SetTarget(uid, _random.Pick(allHumans), target);
}
private void OnHeadAssigned(EntityUid uid, PickRandomHeadComponent comp, ref ObjectiveAssignedEvent args)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
{
args.Cancelled = true;
return;
}
// target already assigned
if (target.Target != null)
return;
// no other humans to kill
var allHumans = _mind.GetAliveHumans(args.MindId);
if (allHumans.Count == 0)
{
args.Cancelled = true;
return;
}
var allHeads = new HashSet<Entity<MindComponent>>();
foreach (var person in allHumans)
{
if (TryComp<MindComponent>(person, out var mind) && mind.OwnedEntity is { } ent && HasComp<CommandStaffComponent>(ent))
allHeads.Add(person);
}
if (allHeads.Count == 0)
allHeads = allHumans; // fallback to non-head target
_target.SetTarget(uid, _random.Pick(allHeads), target);
}
private float GetProgress(EntityUid target, bool requireDead)
{
// deleted or gibbed or something, counts as dead

View File

@@ -0,0 +1,212 @@
using Content.Server.Objectives.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.Revolutionary.Components;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles assinging a target to an objective entity with <see cref="TargetObjectiveComponent"/> using different components.
/// These can be combined with condition components for objective completions in order to create a variety of objectives.
/// </summary>
public sealed class PickObjectiveTargetSystem : EntitySystem
{
[Dependency] private readonly TargetObjectiveSystem _target = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PickSpecificPersonComponent, ObjectiveAssignedEvent>(OnSpecificPersonAssigned);
SubscribeLocalEvent<PickRandomPersonComponent, ObjectiveAssignedEvent>(OnRandomPersonAssigned);
SubscribeLocalEvent<PickRandomHeadComponent, ObjectiveAssignedEvent>(OnRandomHeadAssigned);
SubscribeLocalEvent<RandomTraitorProgressComponent, ObjectiveAssignedEvent>(OnRandomTraitorProgressAssigned);
SubscribeLocalEvent<RandomTraitorAliveComponent, ObjectiveAssignedEvent>(OnRandomTraitorAliveAssigned);
}
private void OnSpecificPersonAssigned(Entity<PickSpecificPersonComponent> ent, ref ObjectiveAssignedEvent args)
{
// invalid objective prototype
if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
{
args.Cancelled = true;
return;
}
// target already assigned
if (target.Target != null)
return;
if (args.Mind.OwnedEntity == null)
{
args.Cancelled = true;
return;
}
var user = args.Mind.OwnedEntity.Value;
if (!TryComp<TargetOverrideComponent>(user, out var targetComp) || targetComp.Target == null)
{
args.Cancelled = true;
return;
}
_target.SetTarget(ent.Owner, targetComp.Target.Value);
}
private void OnRandomPersonAssigned(Entity<PickRandomPersonComponent> ent, ref ObjectiveAssignedEvent args)
{
// invalid objective prototype
if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
{
args.Cancelled = true;
return;
}
// target already assigned
if (target.Target != null)
return;
var allHumans = _mind.GetAliveHumans(args.MindId);
// Can't have multiple objectives to kill the same person
foreach (var objective in args.Mind.Objectives)
{
if (HasComp<KillPersonConditionComponent>(objective) && TryComp<TargetObjectiveComponent>(objective, out var kill))
{
allHumans.RemoveWhere(x => x.Owner == kill.Target);
}
}
// no other humans to kill
if (allHumans.Count == 0)
{
args.Cancelled = true;
return;
}
_target.SetTarget(ent.Owner, _random.Pick(allHumans), target);
}
private void OnRandomHeadAssigned(Entity<PickRandomHeadComponent> ent, ref ObjectiveAssignedEvent args)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
{
args.Cancelled = true;
return;
}
// target already assigned
if (target.Target != null)
return;
// no other humans to kill
var allHumans = _mind.GetAliveHumans(args.MindId);
if (allHumans.Count == 0)
{
args.Cancelled = true;
return;
}
var allHeads = new HashSet<Entity<MindComponent>>();
foreach (var person in allHumans)
{
if (TryComp<MindComponent>(person, out var mind) && mind.OwnedEntity is { } owned && HasComp<CommandStaffComponent>(owned))
allHeads.Add(person);
}
if (allHeads.Count == 0)
allHeads = allHumans; // fallback to non-head target
_target.SetTarget(ent.Owner, _random.Pick(allHeads), target);
}
private void OnRandomTraitorProgressAssigned(Entity<RandomTraitorProgressComponent> ent, ref ObjectiveAssignedEvent args)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
{
args.Cancelled = true;
return;
}
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
// cant help anyone who is tasked with helping:
// 1. thats boring
// 2. no cyclic progress dependencies!!!
foreach (var traitor in traitors)
{
// TODO: replace this with TryComp<ObjectivesComponent>(traitor) or something when objectives are moved out of mind
if (!TryComp<MindComponent>(traitor.Id, out var mind))
continue;
foreach (var objective in mind.Objectives)
{
if (HasComp<HelpProgressConditionComponent>(objective))
traitors.RemoveWhere(x => x.Mind == mind);
}
}
// Can't have multiple objectives to help/save the same person
foreach (var objective in args.Mind.Objectives)
{
if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
{
if (TryComp<TargetObjectiveComponent>(objective, out var help))
{
traitors.RemoveWhere(x => x.Id == help.Target);
}
}
}
// no more helpable traitors
if (traitors.Count == 0)
{
args.Cancelled = true;
return;
}
_target.SetTarget(ent.Owner, _random.Pick(traitors).Id, target);
}
private void OnRandomTraitorAliveAssigned(Entity<RandomTraitorAliveComponent> ent, ref ObjectiveAssignedEvent args)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
{
args.Cancelled = true;
return;
}
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
// Can't have multiple objectives to help/save the same person
foreach (var objective in args.Mind.Objectives)
{
if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
{
if (TryComp<TargetObjectiveComponent>(objective, out var help))
{
traitors.RemoveWhere(x => x.Id == help.Target);
}
}
}
// You are the first/only traitor.
if (traitors.Count == 0)
{
args.Cancelled = true;
return;
}
_target.SetTarget(ent.Owner, _random.Pick(traitors).Id, target);
}
}

View File

@@ -0,0 +1,18 @@
using Content.Shared.Polymorph;
using Robust.Shared.Prototypes;
namespace Content.Server.Polymorph.Components;
/// <summary>
/// Intended for use with the trigger system.
/// Polymorphs the user of the trigger.
/// </summary>
[RegisterComponent]
public sealed partial class PolymorphOnTriggerComponent : Component
{
/// <summary>
/// Polymorph settings.
/// </summary>
[DataField(required: true)]
public ProtoId<PolymorphPrototype> Polymorph;
}

View File

@@ -1,65 +0,0 @@
using Content.Server.Polymorph.Components;
using Content.Shared.Polymorph;
using Content.Shared.Projectiles;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
namespace Content.Server.Polymorph.Systems;
public partial class PolymorphSystem
{
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
/// <summary>
/// Need to do this so we don't get a collection enumeration error in physics by polymorphing
/// an entity we're colliding with
/// </summary>
private Queue<PolymorphQueuedData> _queuedPolymorphUpdates = new();
private void InitializeCollide()
{
SubscribeLocalEvent<PolymorphOnCollideComponent, StartCollideEvent>(OnPolymorphCollide);
}
public void UpdateCollide()
{
while (_queuedPolymorphUpdates.TryDequeue(out var data))
{
if (Deleted(data.Ent))
continue;
var ent = PolymorphEntity(data.Ent, data.Polymorph);
if (ent != null)
_audio.PlayPvs(data.Sound, ent.Value);
}
}
private void OnPolymorphCollide(EntityUid uid, PolymorphOnCollideComponent component, ref StartCollideEvent args)
{
if (args.OurFixtureId != SharedProjectileSystem.ProjectileFixture)
return;
var other = args.OtherEntity;
if (_whitelistSystem.IsWhitelistFail(component.Whitelist, other) ||
_whitelistSystem.IsBlacklistPass(component.Blacklist, other))
return;
_queuedPolymorphUpdates.Enqueue(new (other, component.Sound, component.Polymorph));
}
}
public struct PolymorphQueuedData
{
public EntityUid Ent;
public SoundSpecifier Sound;
public ProtoId<PolymorphPrototype> Polymorph;
public PolymorphQueuedData(EntityUid ent, SoundSpecifier sound, ProtoId<PolymorphPrototype> polymorph)
{
Ent = ent;
Sound = sound;
Polymorph = polymorph;
}
}

View File

@@ -0,0 +1,41 @@
using Content.Shared.Polymorph;
using Content.Server.Polymorph.Components;
using Content.Server.Explosion.EntitySystems;
using Robust.Shared.Prototypes;
namespace Content.Server.Polymorph.Systems;
public sealed partial class PolymorphSystem
{
/// <summary>
/// Need to do this so we don't get a collection enumeration error in physics by polymorphing
/// an entity we're colliding with in case of TriggerOnCollide.
/// Also makes sure other trigger effects don't activate in nullspace after we have polymorphed.
/// </summary>
private Queue<(EntityUid Ent, ProtoId<PolymorphPrototype> Polymorph)> _queuedPolymorphUpdates = new();
private void InitializeTrigger()
{
SubscribeLocalEvent<PolymorphOnTriggerComponent, TriggerEvent>(OnTrigger);
}
private void OnTrigger(Entity<PolymorphOnTriggerComponent> ent, ref TriggerEvent args)
{
if (args.User == null)
return;
_queuedPolymorphUpdates.Enqueue((args.User.Value, ent.Comp.Polymorph));
args.Handled = true;
}
public void UpdateTrigger()
{
while (_queuedPolymorphUpdates.TryDequeue(out var data))
{
if (TerminatingOrDeleted(data.Item1))
continue;
PolymorphEntity(data.Item1, data.Item2);
}
}
}

View File

@@ -60,8 +60,8 @@ public sealed partial class PolymorphSystem : EntitySystem
SubscribeLocalEvent<PolymorphedEntityComponent, BeforeFullySlicedEvent>(OnBeforeFullySliced);
SubscribeLocalEvent<PolymorphedEntityComponent, DestructionEventArgs>(OnDestruction);
InitializeCollide();
InitializeMap();
InitializeTrigger();
}
public override void Update(float frameTime)
@@ -89,7 +89,7 @@ public sealed partial class PolymorphSystem : EntitySystem
}
}
UpdateCollide();
UpdateTrigger();
}
private void OnComponentStartup(Entity<PolymorphableComponent> ent, ref ComponentStartup args)
@@ -204,6 +204,12 @@ public sealed partial class PolymorphSystem : EntitySystem
var child = Spawn(configuration.Entity, _transform.GetMapCoordinates(uid, targetTransformComp), rotation: _transform.GetWorldRotation(uid));
if (configuration.PolymorphPopup != null)
_popup.PopupEntity(Loc.GetString(configuration.PolymorphPopup,
("parent", Identity.Entity(uid, EntityManager)),
("child", Identity.Entity(child, EntityManager))),
child);
MakeSentientCommand.MakeSentient(child, EntityManager);
var polymorphedComp = _compFact.GetComponent<PolymorphedEntityComponent>();
@@ -347,10 +353,11 @@ public sealed partial class PolymorphSystem : EntitySystem
var ev = new PolymorphedEvent(uid, parent, true);
RaiseLocalEvent(uid, ref ev);
_popup.PopupEntity(Loc.GetString("polymorph-revert-popup-generic",
if (component.Configuration.ExitPolymorphPopup != null)
_popup.PopupEntity(Loc.GetString(component.Configuration.ExitPolymorphPopup,
("parent", Identity.Entity(uid, EntityManager)),
("child", Identity.Entity(parent, EntityManager))),
parent);
parent);
QueueDel(uid);
return parent;

View File

@@ -10,6 +10,7 @@ using Content.Shared.Dataset;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Pointing;
using Content.Shared.Random.Helpers;
using Content.Shared.RatKing;
using Robust.Shared.Map;
using Robust.Shared.Random;
@@ -120,10 +121,10 @@ namespace Content.Server.RatKing
base.DoCommandCallout(uid, component);
if (!component.OrderCallouts.TryGetValue(component.CurrentOrder, out var datasetId) ||
!PrototypeManager.TryIndex<DatasetPrototype>(datasetId, out var datasetPrototype))
!PrototypeManager.TryIndex<LocalizedDatasetPrototype>(datasetId, out var datasetPrototype))
return;
var msg = Random.Pick(datasetPrototype.Values);
var msg = Random.Pick(datasetPrototype);
_chat.TrySendInGameICMessage(uid, msg, InGameICChatType.Speak, true);
}
}

View File

@@ -0,0 +1,9 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a paradox clone.
/// </summary>
[RegisterComponent]
public sealed partial class ParadoxCloneRoleComponent : BaseMindRoleComponent;

View File

@@ -19,12 +19,14 @@ public sealed partial class SalvageSystem
private const string MagnetChannel = "Supply";
private EntityQuery<SalvageMobRestrictionsComponent> _salvMobQuery;
private EntityQuery<MobStateComponent> _mobStateQuery;
private List<(Entity<TransformComponent> Entity, EntityUid MapUid, Vector2 LocalPosition)> _detachEnts = new();
private void InitializeMagnet()
{
_salvMobQuery = GetEntityQuery<SalvageMobRestrictionsComponent>();
_mobStateQuery = GetEntityQuery<MobStateComponent>();
SubscribeLocalEvent<SalvageMagnetDataComponent, MapInitEvent>(OnMagnetDataMapInit);
@@ -155,6 +157,21 @@ public sealed partial class SalvageSystem
if (_salvMobQuery.HasComp(mobUid))
continue;
bool CheckParents(EntityUid uid)
{
do
{
uid = _transform.GetParentUid(uid);
if (_mobStateQuery.HasComp(uid))
return true;
}
while (uid != xform.GridUid && uid != EntityUid.Invalid);
return false;
}
if (CheckParents(mobUid))
continue;
// Can't parent directly to map as it runs grid traversal.
_detachEnts.Add(((mobUid, xform), xform.MapUid.Value, _transform.GetWorldPosition(xform)));
_transform.DetachEntity(mobUid, xform);

View File

@@ -231,7 +231,7 @@ public sealed class IonStormSystem : EntitySystem
return _robustRandom.Next(0, 35) switch
{
0 => Loc.GetString("ion-storm-law-on-station", ("joined", joined), ("subjects", triple)),
1 => Loc.GetString("ion-storm-law-no-shuttle", ("joined", joined), ("subjects", triple)),
1 => Loc.GetString("ion-storm-law-call-shuttle", ("joined", joined), ("subjects", triple)),
2 => Loc.GetString("ion-storm-law-crew-are", ("who", crewAll), ("joined", joined), ("subjects", objectsThreats)),
3 => Loc.GetString("ion-storm-law-subjects-harmful", ("adjective", adjective), ("subjects", triple)),
4 => Loc.GetString("ion-storm-law-must-harmful", ("must", must)),

View File

@@ -0,0 +1,174 @@
using System.Text;
using Content.Server.PowerCell;
using Content.Shared.Speech.Components;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Robust.Shared.Random;
namespace Content.Server.Speech.EntitySystems;
public sealed class DamagedSiliconAccentSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DamagedSiliconAccentComponent, AccentGetEvent>(OnAccent, after: [typeof(ReplacementAccentSystem)]);
}
private void OnAccent(Entity<DamagedSiliconAccentComponent> ent, ref AccentGetEvent args)
{
var uid = ent.Owner;
if (ent.Comp.EnableChargeCorruption)
{
var currentChargeLevel = 0.0f;
if (ent.Comp.OverrideChargeLevel.HasValue)
{
currentChargeLevel = ent.Comp.OverrideChargeLevel.Value;
}
else if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
{
currentChargeLevel = battery.CurrentCharge / battery.MaxCharge;
}
currentChargeLevel = Math.Clamp(currentChargeLevel, 0.0f, 1.0f);
// Corrupt due to low power (drops characters on longer messages)
args.Message = CorruptPower(args.Message, currentChargeLevel, ref ent.Comp);
}
if (ent.Comp.EnableDamageCorruption)
{
var damage = FixedPoint2.Zero;
if (ent.Comp.OverrideTotalDamage.HasValue)
{
damage = ent.Comp.OverrideTotalDamage.Value;
}
else if (TryComp<DamageableComponent>(uid, out var damageable))
{
damage = damageable.TotalDamage;
}
// Corrupt due to damage (drop, repeat, replace with symbols)
args.Message = CorruptDamage(args.Message, damage, ref ent.Comp);
}
}
public string CorruptPower(string message, float chargeLevel, ref DamagedSiliconAccentComponent comp)
{
// The first idxMin characters are SAFE
var idxMin = comp.StartPowerCorruptionAtCharIdx;
// Probability will max at idxMax
var idxMax = comp.MaxPowerCorruptionAtCharIdx;
// Fast bails, would not have an effect
if (chargeLevel > comp.ChargeThresholdForPowerCorruption || message.Length < idxMin)
{
return message;
}
var outMsg = new StringBuilder();
var maxDropProb = comp.MaxDropProbFromPower * (1.0f - chargeLevel / comp.ChargeThresholdForPowerCorruption);
var idx = -1;
foreach (var letter in message)
{
idx++;
if (idx < idxMin) // Fast character, no effect
{
outMsg.Append(letter);
continue;
}
// use an x^2 interpolation to increase the drop probability until we hit idxMax
var probToDrop = idx >= idxMax
? maxDropProb
: (float)Math.Pow(((double)idx - idxMin) / (idxMax - idxMin), 2.0) * maxDropProb;
// Ensure we're in the range for Prob()
probToDrop = Math.Clamp(probToDrop, 0.0f, 1.0f);
if (_random.Prob(probToDrop)) // Lose a character
{
// Additional chance to change to dot for flavor instead of full drop
if (_random.Prob(comp.ProbToCorruptDotFromPower))
{
outMsg.Append('.');
}
}
else // Character is safe
{
outMsg.Append(letter);
}
}
return outMsg.ToString();
}
private string CorruptDamage(string message, FixedPoint2 totalDamage, ref DamagedSiliconAccentComponent comp)
{
var outMsg = new StringBuilder();
// Linear interpolation of character damage probability
var damagePercent = Math.Clamp((float)totalDamage / (float)comp.DamageAtMaxCorruption, 0, 1);
var chanceToCorruptLetter = damagePercent * comp.MaxDamageCorruption;
foreach (var letter in message)
{
if (_random.Prob(chanceToCorruptLetter)) // Corrupt!
{
outMsg.Append(CorruptLetterDamage(letter));
}
else // Safe!
{
outMsg.Append(letter);
}
}
return outMsg.ToString();
}
private string CorruptLetterDamage(char letter)
{
var res = _random.NextDouble();
return res switch
{
< 0.0 => letter.ToString(), // shouldn't be less than 0!
< 0.5 => CorruptPunctuize(), // 50% chance to replace with random punctuation
< 0.75 => "", // 25% chance to remove character
< 1.00 => CorruptRepeat(letter), // 25% to repeat the character
_ => letter.ToString(), // shouldn't be greater than 1!
};
}
private string CorruptPunctuize()
{
const string punctuation = "\"\\`~!@#$%^&*()_+-={}[]|\\;:<>,.?/";
return punctuation[_random.NextByte((byte)punctuation.Length)].ToString();
}
private string CorruptRepeat(char letter)
{
// 25% chance to add another character in the streak
// (kind of like "exploding dice")
// Solved numerically in closed form for streaks of bernoulli variables with p = 0.25
// Can calculate for different p using python function:
/*
* def prob(streak, p):
* if streak == 0:
* return scipy.stats.binom(streak+1, p).pmf(streak)
* return prob(streak-1) * p
* def prob_cum(streak, p=.25):
* return np.sum([prob(i, p) for i in range(streak+1)])
*/
var numRepeats = _random.NextDouble() switch
{
< 0.75000000 => 2,
< 0.93750000 => 3,
< 0.98437500 => 4,
< 0.99609375 => 5,
< 0.99902344 => 6,
< 0.99975586 => 7,
< 0.99993896 => 8,
< 0.99998474 => 9,
_ => 10,
};
return new string(letter, numRepeats);
}
}

View File

@@ -1,10 +1,12 @@
using System.Linq;
using Content.Server.Administration.Managers;
using Content.Server.Antag;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Station.Components;
using Content.Server.Station.Events;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -17,6 +19,8 @@ public sealed partial class StationJobsSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IBanManager _banManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
private Dictionary<int, HashSet<string>> _jobsByWeight = default!;
private List<int> _orderedWeights = default!;
@@ -345,6 +349,7 @@ public sealed partial class StationJobsSystem
foreach (var (player, profile) in profiles)
{
var roleBans = _banManager.GetJobBans(player);
var antagBlocked = _antag.GetPreSelectedAntagSessions();
var profileJobs = profile.JobPriorities.Keys.Select(k => new ProtoId<JobPrototype>(k)).ToList();
var ev = new StationJobsGetCandidatesEvent(player, profileJobs);
RaiseLocalEvent(ref ev);
@@ -361,6 +366,9 @@ public sealed partial class StationJobsSystem
if (!_prototypeManager.TryIndex(jobId, out var job))
continue;
if (!job.CanBeAntag && (!_playerManager.TryGetSessionById(player, out var session) || antagBlocked.Contains(session)))
continue;
if (weight is not null && job.Weight != weight.Value)
continue;

View File

@@ -64,7 +64,8 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
// Unfortunately this means that an event is called for it as well, and since TryFindIdCard will succeed if the
// given entity is a card and the card itself is the key the record will be mistakenly renamed to the card's name
// if we don't return early.
if (HasComp<IdCardComponent>(ev.Uid))
// We also do not include the PDA itself being renamed, as that triggers the same event (e.g. for chameleon PDAs).
if (HasComp<IdCardComponent>(ev.Uid) || HasComp<PdaComponent>(ev.Uid))
return;
if (_idCard.TryFindIdCard(ev.Uid, out var idCard))

View File

@@ -449,9 +449,14 @@ public enum LogType
/// An atmos networked device (such as a vent or pump) has had its settings changed, usually through an air alarm
/// </summary>
AtmosDeviceSetting = 97,
/// <summary>
/// Commands related to admemes. Stuff like config changes, etc.
/// </summary>
AdminCommands = 98,
/// <summary>
/// A player was selected or assigned antag status
/// </summary>
AntagSelection = 99,
}

View File

@@ -163,7 +163,7 @@ public abstract class SharedAnomalySystem : EntitySystem
var ev = new AnomalySupercriticalEvent(uid, powerMod);
RaiseLocalEvent(uid, ref ev, true);
EndAnomaly(uid, component, true);
EndAnomaly(uid, component, true, logged: true);
}
/// <summary>
@@ -173,13 +173,17 @@ public abstract class SharedAnomalySystem : EntitySystem
/// <param name="component"></param>
/// <param name="supercritical">Whether or not the anomaly ended via supercritical event</param>
/// <param name="spawnCore">Create anomaly cores based on the result of completing an anomaly?</param>
public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false, bool spawnCore = true)
/// <param name="logged">Whether or not the anomaly decaying/going supercritical is logged</param>
public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false, bool spawnCore = true, bool logged = false)
{
// Logging before resolve, in case the anomaly has deleted itself.
if (_net.IsServer)
Log.Info($"Ending anomaly. Entity: {ToPrettyString(uid)}");
AdminLog.Add(LogType.Anomaly, supercritical ? LogImpact.High : LogImpact.Low,
$"Anomaly {ToPrettyString(uid)} {(supercritical ? "went supercritical" : "decayed")}.");
if (logged)
{
// Logging before resolve, in case the anomaly has deleted itself.
if (_net.IsServer)
Log.Info($"Ending anomaly. Entity: {ToPrettyString(uid)}");
AdminLog.Add(LogType.Anomaly, supercritical ? LogImpact.High : LogImpact.Low,
$"Anomaly {ToPrettyString(uid)} {(supercritical ? "went supercritical" : "decayed")}.");
}
if (!Resolve(uid, ref component))
return;
@@ -260,7 +264,7 @@ public abstract class SharedAnomalySystem : EntitySystem
if (newVal < 0)
{
EndAnomaly(uid, component);
EndAnomaly(uid, component, logged: true);
return;
}

View File

@@ -17,7 +17,7 @@ public enum AntagAcceptability
/// <summary>
/// Choose anyone
/// </summary>
All
All,
}
public enum AntagSelectionTime : byte
@@ -28,8 +28,14 @@ public enum AntagSelectionTime : byte
/// </summary>
PrePlayerSpawn,
/// <summary>
/// Antag roles are selected to the player session before job assignment and spawning.
/// Unlike PrePlayerSpawn, this does not remove you from the job spawn pool.
/// </summary>
IntraPlayerSpawn,
/// <summary>
/// Antag roles get assigned after players have been assigned jobs and have spawned in.
/// </summary>
PostPlayerSpawn
PostPlayerSpawn,
}

View File

@@ -23,6 +23,12 @@ public sealed partial class ArmorComponent : Component
/// </summary>
[DataField]
public float PriceMultiplier = 1;
/// <summary>
/// If true, you can examine the armor to see the protection. If false, the verb won't appear.
/// </summary>
[DataField]
public bool ShowArmorOnExamine = true;
}
/// <summary>

View File

@@ -51,7 +51,7 @@ public abstract class SharedArmorSystem : EntitySystem
private void OnArmorVerbExamine(EntityUid uid, ArmorComponent component, GetVerbsEvent<ExamineVerb> args)
{
if (!args.CanInteract || !args.CanAccess)
if (!args.CanInteract || !args.CanAccess || !component.ShowArmorOnExamine)
return;
var examineMarkup = GetArmorExamine(component.Modifiers);

View File

@@ -416,7 +416,7 @@ public abstract partial class SharedBuckleSystem
public bool TryUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup)
{
if (!Resolve(buckle.Owner, ref buckle.Comp))
if (!Resolve(buckle.Owner, ref buckle.Comp, false))
return false;
if (!CanUnbuckle(buckle, user, popup, out var strap))

View File

@@ -0,0 +1,42 @@
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Shared.CartridgeLoader.Cartridges;
/// <summary>
/// Component that indicates a PDA cartridge as containing the NanoTask program
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class NanoTaskCartridgeComponent : Component
{
/// <summary>
/// The list of tasks
/// </summary>
[DataField]
public List<NanoTaskItemAndId> Tasks = new();
/// <summary>
/// counter for generating task IDs
/// </summary>
[DataField]
public int Counter = 1;
/// <summary>
/// When the user can print again
/// </summary>
[DataField, AutoPausedField]
public TimeSpan NextPrintAllowedAfter = TimeSpan.Zero;
/// <summary>
/// How long in between each time the user can print out a task
/// </summary>
[DataField]
public TimeSpan PrintDelay = TimeSpan.FromSeconds(5);
}
/// <summary>
/// Component attached to the PDA a NanoTask cartridge is inserted into for interaction handling
/// </summary>
[RegisterComponent]
public sealed partial class NanoTaskInteractionComponent : Component
{
}

View File

@@ -0,0 +1,16 @@
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Shared.CartridgeLoader.Cartridges;
/// <summary>
/// Component attached to a piece of paper to indicate that it was printed from NanoTask and can be inserted back into it
/// </summary>
[RegisterComponent]
public sealed partial class NanoTaskPrintedComponent : Component
{
/// <summary>
/// The task that this item holds
/// </summary>
[DataField]
public NanoTaskItem? Task;
}

View File

@@ -0,0 +1,91 @@
using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader.Cartridges;
/// <summary>
/// Base UI message for NanoTask interactions
/// </summary>
public interface INanoTaskUiMessagePayload
{
}
/// <summary>
/// Dispatched when a new task is created
/// </summary>
[Serializable, NetSerializable, DataRecord]
public sealed class NanoTaskAddTask : INanoTaskUiMessagePayload
{
/// <summary>
/// The newly created task
/// </summary>
public readonly NanoTaskItem Item;
public NanoTaskAddTask(NanoTaskItem item)
{
Item = item;
}
}
/// <summary>
/// Dispatched when an existing task is modified
/// </summary>
[Serializable, NetSerializable, DataRecord]
public sealed class NanoTaskUpdateTask : INanoTaskUiMessagePayload
{
/// <summary>
/// The task that was updated and its ID
/// </summary>
public readonly NanoTaskItemAndId Item;
public NanoTaskUpdateTask(NanoTaskItemAndId item)
{
Item = item;
}
}
/// <summary>
/// Dispatched when an existing task is deleted
/// </summary>
[Serializable, NetSerializable, DataRecord]
public sealed class NanoTaskDeleteTask : INanoTaskUiMessagePayload
{
/// <summary>
/// The ID of the task to delete
/// </summary>
public readonly int Id;
public NanoTaskDeleteTask(int id)
{
Id = id;
}
}
/// <summary>
/// Dispatched when a task is requested to be printed
/// </summary>
[Serializable, NetSerializable, DataRecord]
public sealed class NanoTaskPrintTask : INanoTaskUiMessagePayload
{
/// <summary>
/// The NanoTask to print
/// </summary>
public readonly NanoTaskItem Item;
public NanoTaskPrintTask(NanoTaskItem item)
{
Item = item;
}
}
/// <summary>
/// Cartridge message event carrying the NanoTask UI messages
/// </summary>
[Serializable, NetSerializable]
public sealed class NanoTaskUiMessageEvent : CartridgeMessageEvent
{
public readonly INanoTaskUiMessagePayload Payload;
public NanoTaskUiMessageEvent(INanoTaskUiMessagePayload payload)
{
Payload = payload;
}
}

View File

@@ -0,0 +1,88 @@
using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader.Cartridges;
/// <summary>
/// The priority assigned to a NanoTask item
/// </summary>
[Serializable, NetSerializable]
public enum NanoTaskPriority : byte
{
High,
Medium,
Low,
};
/// <summary>
/// The data relating to a single NanoTask item, but not its identifier
/// </summary>
[Serializable, NetSerializable, DataRecord]
public sealed class NanoTaskItem
{
/// <summary>
/// The maximum length of the Description and TaskIsFor fields
/// </summary>
public static int MaximumStringLength = 30;
/// <summary>
/// The task description, i.e. "Bake a cake"
/// </summary>
public readonly string Description;
/// <summary>
/// Who the task is for, i.e. "Cargo"
/// </summary>
public readonly string TaskIsFor;
/// <summary>
/// If the task is marked as done or not
/// </summary>
public readonly bool IsTaskDone;
/// <summary>
/// The task's marked priority
/// </summary>
public readonly NanoTaskPriority Priority;
public NanoTaskItem(string description, string taskIsFor, bool isTaskDone, NanoTaskPriority priority)
{
Description = description;
TaskIsFor = taskIsFor;
IsTaskDone = isTaskDone;
Priority = priority;
}
public bool Validate()
{
return Description.Length <= MaximumStringLength && TaskIsFor.Length <= MaximumStringLength;
}
};
/// <summary>
/// Pairs a NanoTask item and its identifier
/// </summary>
[Serializable, NetSerializable, DataRecord]
public sealed class NanoTaskItemAndId
{
public readonly int Id;
public readonly NanoTaskItem Data;
public NanoTaskItemAndId(int id, NanoTaskItem data)
{
Id = id;
Data = data;
}
};
/// <summary>
/// The UI state of the NanoTask
/// </summary>
[Serializable, NetSerializable]
public sealed class NanoTaskUiState : BoundUserInterfaceState
{
public List<NanoTaskItemAndId> Tasks;
public NanoTaskUiState(List<NanoTaskItemAndId> tasks)
{
Tasks = tasks;
}
}

View File

@@ -0,0 +1,19 @@
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Shared.CartridgeLoader.Cartridges;
public abstract class SharedNanoTaskCartridgeSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeAddedEvent>(OnCartridgeAdded);
}
private void OnCartridgeAdded(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeAddedEvent args)
{
EnsureComp<NanoTaskInteractionComponent>(args.Loader);
}
}

View File

@@ -34,7 +34,19 @@ public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPr
/// Disabled when null.
/// </summary>
[DataField]
public SlotFlags? CopyEquipment = SlotFlags.WITHOUT_POCKET;
public SlotFlags? CopyEquipment = SlotFlags.All;
/// <summary>
/// Whether or not to copy slime storage and storage implant contents.
/// </summary>
[DataField]
public bool CopyInternalStorage = true;
/// <summary>
/// Whether or not to copy implants.
/// </summary>
[DataField]
public bool CopyImplants = true;
/// <summary>
/// Whitelist for the equipment allowed to be copied.

View File

@@ -21,7 +21,7 @@ public sealed partial class CriminalRecordsHackerComponent : Component
/// Dataset of random reasons to use.
/// </summary>
[DataField]
public ProtoId<DatasetPrototype> Reasons = "CriminalRecordsWantedReasonPlaceholders";
public ProtoId<LocalizedDatasetPrototype> Reasons = "CriminalRecordsWantedReasonPlaceholders";
/// <summary>
/// Announcement made after the console is hacked.

View File

@@ -82,7 +82,7 @@ public sealed partial class ElectrifiedComponent : Component
/// Shock time multiplier for HV electrocution
/// </summary>
[DataField, AutoNetworkedField]
public float HighVoltageTimeMultiplier = 1.5f;
public float HighVoltageTimeMultiplier = 2f;
/// <summary>
/// Damage multiplier for MV electrocution
@@ -94,7 +94,7 @@ public sealed partial class ElectrifiedComponent : Component
/// Shock time multiplier for MV electrocution
/// </summary>
[DataField, AutoNetworkedField]
public float MediumVoltageTimeMultiplier = 1.25f;
public float MediumVoltageTimeMultiplier = 1.5f;
[DataField, AutoNetworkedField]
public float ShockDamage = 7.5f;
@@ -103,7 +103,7 @@ public sealed partial class ElectrifiedComponent : Component
/// Shock time, in seconds.
/// </summary>
[DataField, AutoNetworkedField]
public float ShockTime = 8f;
public float ShockTime = 5f;
[DataField, AutoNetworkedField]
public float SiemensCoefficient = 1f;

View File

@@ -0,0 +1,22 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Gibbing.Components;
/// <summary>
/// Gibs an entity on round end.
/// </summary>
[RegisterComponent]
public sealed partial class GibOnRoundEndComponent : Component
{
/// <summary>
/// If the entity has all these objectives fulfilled they won't be gibbed.
/// </summary>
[DataField]
public HashSet<EntProtoId> PreventGibbingObjectives = new();
/// <summary>
/// Entity to spawn when gibbed. Can be used for effects.
/// </summary>
[DataField]
public EntProtoId? SpawnProto;
}

View File

@@ -49,11 +49,17 @@ public static class Identity
/// This is an extension method because of its simplicity, and if it was any harder to call it might not
/// be used enough for loc.
/// </summary>
public static EntityUid Entity(EntityUid uid, IEntityManager ent)
/// <param name="viewer">
/// If this entity can see through identities, this method will always return the actual target entity.
/// </param>
public static EntityUid Entity(EntityUid uid, IEntityManager ent, EntityUid? viewer = null)
{
if (!ent.TryGetComponent<IdentityComponent>(uid, out var identity))
return uid;
if (viewer != null && CanSeeThroughIdentity(uid, viewer.Value, ent))
return uid;
return identity.IdentityEntitySlot.ContainedEntity ?? uid;
}

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Containers;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
@@ -16,7 +16,7 @@ public sealed partial class CryoPodComponent : Component
public string PortName { get; set; } = "port";
/// <summary>
/// Specifies the name of the atmospherics port to draw gas from.
/// Specifies the name of the slot that holds beaker with medicine.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("solutionContainerName")]

View File

@@ -11,6 +11,7 @@ using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Objectives.Systems;
using Content.Shared.Players;
using Content.Shared.Whitelist;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
@@ -26,6 +27,7 @@ public abstract class SharedMindSystem : EntitySystem
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
[Dependency] private readonly SharedPlayerSystem _player = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[ViewVariables]
protected readonly Dictionary<NetUserId, EntityUid> UserMinds = new();
@@ -364,6 +366,16 @@ public abstract class SharedMindSystem : EntitySystem
var title = Name(objective);
_adminLogger.Add(LogType.Mind, LogImpact.Low, $"Objective {objective} ({title}) removed from the mind of {MindOwnerLoggingString(mind)}");
mind.Objectives.Remove(objective);
// garbage collection - only delete the objective entity if no mind uses it anymore
// This comes up for stuff like paradox clones where the objectives share the same entity
var mindQuery = new AllEntityQueryEnumerator<MindComponent>();
while (mindQuery.MoveNext(out _, out var queryComp))
{
if (queryComp.Objectives.Contains(objective))
return true;
}
Del(objective);
return true;
}
@@ -395,6 +407,33 @@ public abstract class SharedMindSystem : EntitySystem
return false;
}
/// <summary>
/// Copies objectives from one mind to another, so that they are shared between two players.
/// </summary>
/// <remarks>
/// Only copies the reference to the objective entity, not the entity itself.
/// This relies on the fact that objectives are never changed after spawning them.
/// If someone ever changes that, they will have to address this.
/// </remarks>
/// <param name="source"> mind entity of the player to copy from </param>
/// <param name="target"> mind entity of the player to copy to </param>
/// <param name="except"> whitelist for objectives that should be copied </param>
/// <param name="except"> blacklist for objectives that should not be copied </param>
public void CopyObjectives(Entity<MindComponent?> source, Entity<MindComponent?> target, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
if (!Resolve(source, ref source.Comp) || !Resolve(target, ref target.Comp))
return;
foreach (var objective in source.Comp.Objectives)
{
if (target.Comp.Objectives.Contains(objective))
continue; // target already has this objective
if (_whitelist.CheckBoth(objective, blacklist, whitelist))
AddObjective(target, target.Comp, objective);
}
}
/// <summary>
/// Tries to find an objective that has the same prototype as the argument.
/// </summary>

View File

@@ -0,0 +1,10 @@
using Content.Shared.Revolutionary;
using Robust.Shared.GameStates;
namespace Content.Shared.Mindshield.Components;
/// <summary>
/// Component given to an entity to mark it is a mindshield implant.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))]
public sealed partial class MindShieldImplantComponent : Component;

View File

@@ -128,6 +128,18 @@ public sealed partial record PolymorphConfiguration
/// </summary>
[DataField]
public SoundSpecifier? ExitPolymorphSound;
/// <summary>
/// If not null, this popup will be displayed when being polymorphed into something.
/// </summary>
[DataField]
public LocId? PolymorphPopup = "polymorph-popup-generic";
/// <summary>
/// If not null, this popup will be displayed when when being reverted from a polymorph.
/// </summary>
[DataField]
public LocId? ExitPolymorphPopup = "polymorph-revert-popup-generic";
}
public enum PolymorphInventoryChange : byte

View File

@@ -0,0 +1,80 @@
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;
namespace Content.Shared.Speech.Components;
[RegisterComponent]
[NetworkedComponent]
public sealed partial class DamagedSiliconAccentComponent : Component
{
/// <summary>
/// Enable damage corruption effects
/// </summary>
[DataField]
public bool EnableDamageCorruption = true;
/// <summary>
/// Override total damage for damage corruption effects
/// </summary>
[DataField]
public FixedPoint2? OverrideTotalDamage;
/// <summary>
/// The probability that a character will be corrupted when total damage at or above <see cref="MaxDamageCorruption" />.
/// </summary>
[DataField]
public float MaxDamageCorruption = 0.5f;
/// <summary>
/// Probability of character corruption will increase linearly to <see cref="MaxDamageCorruption" /> once until
/// total damage is at or above this value.
/// </summary>
[DataField]
public FixedPoint2 DamageAtMaxCorruption = 300;
/// <summary>
/// Enable charge level corruption effects
/// </summary>
[DataField]
public bool EnableChargeCorruption = true;
/// <summary>
/// Override charge level for charge level corruption effects
/// </summary>
[DataField]
public float? OverrideChargeLevel;
/// <summary>
/// If the power cell charge is below this value (as a fraction of maximum charge),
/// power corruption will begin to be applied.
/// </summary>
[DataField]
public float ChargeThresholdForPowerCorruption = 0.15f;
/// <summary>
/// Regardless of charge level, this is how many characters at the start of a message will be 100% safe
/// from being dropped.
/// </summary>
[DataField]
public int StartPowerCorruptionAtCharIdx = 8;
/// <summary>
/// The probability that a character will be dropped due to charge level will be maximum for characters past
/// this index. This has the effect of longer messages dropping more characters later in the message.
/// </summary>
[DataField]
public int MaxPowerCorruptionAtCharIdx = 40;
/// <summary>
/// The maximum probability that a character will be dropped due to charge level.
/// </summary>
[DataField]
public float MaxDropProbFromPower = 0.5f;
/// <summary>
/// If a character is "dropped", this is the probability that the character will be turned into a period instead
/// of completely deleting the character.
/// </summary>
[DataField]
public float ProbToCorruptDotFromPower = 0.6f;
}

View File

@@ -145,6 +145,12 @@ namespace Content.Shared.Throwing
{
base.Update(frameTime);
// TODO predicted throwing - remove this check
// We don't want to predict landing or stopping, since throwing isn't actually predicted.
// If we do, the landing/stop will occur prematurely on the client.
if (_gameTiming.InPrediction)
return;
var query = EntityQueryEnumerator<ThrownItemComponent, PhysicsComponent>();
while (query.MoveNext(out var uid, out var thrown, out var physics))
{

View File

@@ -350,10 +350,7 @@ public abstract partial class SharedGunSystem : EntitySystem
// If they're firing an existing clip then don't play anything.
if (shots > 0)
{
if (ev.Reason != null && Timing.IsFirstTimePredicted)
{
PopupSystem.PopupCursor(ev.Reason);
}
PopupSystem.PopupCursor(ev.Reason ?? Loc.GetString("gun-magazine-fired-empty"));
// Don't spam safety sounds at gun fire rate, play it at a reduced rate.
// May cause prediction issues? Needs more tweaking

View File

@@ -12,7 +12,7 @@
license: "Custom"
copyright: "Space Asshole by Chris Remo is used with special permission from the author, under the condition that the project remains non-commercial and open source. The author also requested that a link to his bandcamp be included: https://chrisremo.bandcamp.com/"
source: "https://idlethumbs.bandcamp.com/track/space-asshole"
# The source is a direct link the the track, but not the "main" bandcamp of the author. Hence the link is also included separately in the copyright.
# The source is a direct link to the track, but not the "main" bandcamp of the author. Hence the link is also included separately in the copyright.
- files: ["the_wizard.ogg"]
license: "Custom"
@@ -49,7 +49,7 @@
- files: ["comet_haley.ogg"]
license: "CC-BY-NC-SA-3.0"
copyright: "Comet Haley by Stellardrone. Converted from MP3 to OGG."
source: "https://freemusicarchive.org/music/Stellardrone/Light_Years_1227/07_Comet_Halley"
source: "https://stellardrone.bandcamp.com/track/comet-halley"
- files: ["mod.flip-flap.ogg"]
license: "Custom"
@@ -63,15 +63,15 @@
- files: ["pwmur.ogg"]
license: "CC-BY-NC-SA-3.0"
copyright: "phoron will make us rich by Alexander Divine."
source: "https://soundcloud.com/alexanderdivine/phoron-will-make-us-rich"
copyright: "phoron will make us rich by Sunbeamstress/Lauren Loveless."
source: "https://soundcloud.com/sunbeamstress/phoron-will-make-us-rich"
- files: ["lasers_rip_apart_the_bulkhead.ogg"]
license: "CC-BY-NC-SA-3.0"
copyright: "lasers rip apart the bulkhead by Alexander Divine."
source: "https://soundcloud.com/alexanderdivine/lasers-rip-apart-the-bulkhead"
copyright: "lasers rip apart the bulkhead by Sunbeamstress/Lauren Loveless."
source: "https://soundcloud.com/sunbeamstress/lasers-rip-apart-the-bulkhead"
- files: ["every_light_is_blinking_at_once.ogg"]
license: "CC-BY-NC-SA-3.0"
copyright: "every light is blinking at once by Alexander Divine."
source: "https://soundcloud.com/alexanderdivine/every-light-is-blinking-at-once"
copyright: "every light is blinking at once by Sunbeamstress/Lauren Loveless."
source: "https://soundcloud.com/sunbeamstress/every-light-is-blinking-at-once"

View File

@@ -8,6 +8,11 @@
copyright: "Created by Chillyconmor"
source: "https://github.com/space-wizards/space-station-14/blob/master/Resources/Audio/Misc/ninja_greeting.ogg"
- files: ["nparadox_clone_greeting.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Created by ps3moira"
source: "https://github.com/space-wizards/space-station-14/blob/master/Resources/Audio/Misc/ninja_greeting.ogg"
- files: ["thief_greeting.ogg"]
license: "CC-BY-NC-4.0"
copyright: "Taken from SergeQuadrado via freesound.org, edit and mono by TheShuEd"

Binary file not shown.

View File

@@ -872,5 +872,22 @@ Entries:
id: 108
time: '2025-03-10T09:40:36.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35747
- author: Errant
changes:
- message: The admin overlay once again shows the character name in the top line.
type: Fix
id: 109
time: '2025-03-11T16:47:03.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35783
- author: Errant
changes:
- message: All Solo Antagonist, Team Antagonist, Altered Silicon and (non-harmless)
Free Agent ghostroles are now marked as Antagonist on all interfaces where this
distinction exists. (Classic admin overlay, admin playerlist, round end player
list)
type: Fix
id: 110
time: '2025-03-16T22:11:31.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35832
Name: Admin
Order: 1

View File

@@ -1,258 +1,4 @@
Entries:
- author: BramvanZijp
changes:
- message: The Space Ninja Suit will now give an error popup if you are trying to
install a cell that is not better compared to the current cell.
type: Add
- message: When comparing which power cell is better when trying to swap them, the
Space Ninja's Suit will now also consider if the power cells have self-recharge
capability.
type: Tweak
- message: You can no longer fit weapons-grade power cages into the space ninja
suit.
type: Fix
id: 7548
time: '2024-10-22T23:36:51.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32902
- author: UBlueberry
changes:
- message: The appraisal tool now has in-hand sprites!
type: Fix
id: 7549
time: '2024-10-22T23:51:49.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32849
- author: chromiumboy
changes:
- message: The atmospheric alerts computer has been upgraded to visually indicate
the rooms of the station that are being monitored by its air and fire alarm
systems
type: Tweak
id: 7550
time: '2024-10-23T12:49:58.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/31910
- author: hyphenationc
changes:
- message: Snake meat is now properly considered Meat, so Lizards can eat it.
type: Fix
id: 7551
time: '2024-10-24T03:41:03.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32965
- author: slarticodefast
changes:
- message: Mix 1u aluminium, 1u potassium and 1u sulfur for a flash reaction effect.
The radius scales with the reagent amount.
type: Add
id: 7552
time: '2024-10-25T22:47:12.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32377
- author: BramvanZijp
changes:
- message: Fixed the Lone Nuclear Operative mid-round antagonist being extremely
rare.
type: Fix
id: 7553
time: '2024-10-26T02:16:45.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32942
- author: Moomoobeef
changes:
- message: Bowls no longer make an eating sound when drinking from them.
type: Fix
id: 7554
time: '2024-10-26T04:00:49.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32819
- author: SaphireLattice
changes:
- message: Added a warning about unrevivability in the health analyzer UI.
type: Add
id: 7555
time: '2024-10-26T17:22:09.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32636
- author: slarticodefast
changes:
- message: Fixed pie throwing sound not playing.
type: Fix
id: 7556
time: '2024-10-27T04:25:55.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33017
- author: stalengd
changes:
- message: Fixed playtime labels not being able to correctly display time greater
than 24 hours
type: Fix
id: 7557
time: '2024-10-28T18:00:00.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32974
- author: august-sun
changes:
- message: Extended the minimum round time for meteor swarm events.
type: Tweak
id: 7558
time: '2024-10-28T21:25:34.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32876
- author: deltanedas
changes:
- message: Fixed lava planet expeditions not working.
type: Fix
id: 7559
time: '2024-10-29T05:00:29.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33042
- author: metalgearsloth
changes:
- message: Fix separated game screen bumping slightly.
type: Fix
id: 7560
time: '2024-10-29T05:07:57.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33046
- author: Blackern5000
changes:
- message: Proto-kitentic crushers, glaives, and daggers now have more accurate
inhand sprites.
type: Tweak
id: 7561
time: '2024-10-30T07:38:19.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32212
- author: Blackern5000
changes:
- message: Security belts now contain a holobarrier projector and a handheld security
radio by default rather than tear gas and a flashbang.
type: Tweak
id: 7562
time: '2024-10-30T07:40:33.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32291
- author: Blackern5000
changes:
- message: Added three bottle boxes to the nanomed plus inventory for doctors to
carry small amounts of chemicals on their person
type: Add
id: 7563
time: '2024-10-30T07:41:51.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33018
- author: Blackern5000
changes:
- message: Added the interdyne defibrillator, a black-and-red defibrillator that
can be used as a melee weapon.
type: Add
- message: The syndicate medical bundle now contains an interdyne defibrillator,
a collection of various instant injectors, tourniquets, and several combat kits.
The price has been raised to 24 tc.
type: Tweak
id: 7564
time: '2024-10-30T09:15:30.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32720
- author: Boaz1111
changes:
- message: Pill bottles can now only store pills.
type: Tweak
id: 7565
time: '2024-10-31T10:56:07.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33074
- author: Jarmer123
changes:
- message: You can now find a spare bible in the PietyVend
type: Add
id: 7566
time: '2024-10-31T13:26:46.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32363
- author: justinbrick
changes:
- message: Added a pop-up notification when extra items are dropped while unequipping
something.
type: Tweak
id: 7567
time: '2024-10-31T14:12:26.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33078
- author: BramvanZijp
changes:
- message: The maximum amount of programs that can be installed on a PDA has been
increased from 5 to 8
type: Tweak
- message: The Detective and Head of Security now get the logprobe program pre-installed
on their PDA.
type: Tweak
id: 7568
time: '2024-10-31T14:53:38.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32601
- author: Psychpsyo
changes:
- message: Carp plushies can now be placed in mop buckets, along with other rehydratable
things like monkey cubes.
type: Add
id: 7569
time: '2024-10-31T18:46:19.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33079
- author: Bhijn and Myr
changes:
- message: Tail thumping has been downmixed to mono to fix the sound lacking any
sort of positioning. They're now capable of having a presence in the actual
soundspace, in turn meaning lizards are no longer occupying your headset at
all times of day.
type: Fix
id: 7570
time: '2024-10-31T21:30:58.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33092
- author: reesque
changes:
- message: pie not dropping tin on thrown
type: Fix
id: 7571
time: '2024-11-01T01:43:11.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33013
- author: SlamBamActionman
changes:
- message: Votekicks can now be initiated during the pregame lobby.
type: Fix
id: 7572
time: '2024-11-01T01:52:55.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32528
- author: PopGamer46
changes:
- message: Fixed bolt lights of recently unpowered bolted doors
type: Fix
id: 7573
time: '2024-11-01T02:04:09.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33063
- author: RumiTiger
changes:
- message: A chocolate and banana muffin has been added to the game. The berry and
cherry muffins have also been resprited!
type: Add
- message: Now you can make a regular, chocolate, banana, and berry muffin!
type: Tweak
- message: Muffin tins have been added to the game. They are needed for making muffins!
type: Add
id: 7574
time: '2024-11-01T02:06:46.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29318
- author: ScarKy0
changes:
- message: AI can no longer toggle seeing jobs off.
type: Tweak
- message: Borgs can no longer see mindshield status.
type: Fix
id: 7575
time: '2024-11-01T02:32:28.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33069
- author: Minemoder5000
changes:
- message: The cargo shuttle's cargo pallets can no longer sell or buy.
type: Fix
id: 7576
time: '2024-11-01T06:22:39.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33022
- author: aspiringLich
changes:
- message: Fixed the logic triggering popups when inserting items into machines.
type: Fix
id: 7577
time: '2024-11-02T01:33:26.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28856
- author: K-Dynamic
changes:
- message: Pills are explosion resistant.
type: Tweak
id: 7578
time: '2024-11-02T09:51:45.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/32458
- author: K-Dynamic
changes:
- message: Handcrafted gauze now takes 3 seconds instead of 10 seconds of crafting
@@ -3897,3 +3643,248 @@
id: 8047
time: '2025-03-10T04:38:33.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35758
- author: RedBookcase
changes:
- message: Added about a dozen new cocktails for the Bartender to mix up, and polished
some of the sprites for older drinks.
type: Add
id: 8048
time: '2025-03-10T23:08:24.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33570
- author: Velken
changes:
- message: Performers can now find that their clothing set is more complete!
type: Add
id: 8049
time: '2025-03-10T23:17:24.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35764
- author: ScarKy0
changes:
- message: Mindshield implants can now be removed using an empty implanter.
type: Tweak
- message: Revolutionaries can now win by converting all command members.
type: Add
id: 8050
time: '2025-03-11T09:41:13.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35769
- author: SlamBamActionman
changes:
- message: Chameleon PDAs no longer change your name in the station records.
type: Fix
id: 8051
time: '2025-03-11T13:52:38.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35782
- author: Reisama
changes:
- message: Borgs will no longer get laws to prevent the shuttle from being called
when Ion Stormed, and may get laws to call the shuttle instead.
type: Fix
id: 8052
time: '2025-03-12T01:21:59.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35751
- author: Winkarst-cpu
changes:
- message: The warden job now rolls before security officer/cadet/detective.
type: Tweak
id: 8053
time: '2025-03-12T22:36:41.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35313
- author: Firewars763
changes:
- message: Added gold and coal rock anomalies and ore crab enemies.
type: Add
- message: Added yellow and black crystals, crystal shards, and light tubes.
type: Add
id: 8054
time: '2025-03-12T22:45:22.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/34809
- author: onesch
changes:
- message: Added handheld sprites to some items
type: Add
id: 8055
time: '2025-03-12T22:46:58.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/33689
- author: K-Dynamic
changes:
- message: Nitrous oxide canisters now begin locked like other dangerous gas canisters.
type: Fix
id: 8056
time: '2025-03-13T00:30:17.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35785
- author: Plykiya
changes:
- message: A message now pops up when attempting to fire a gun with no ammo.
type: Add
id: 8057
time: '2025-03-13T07:21:24.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/34816
- author: Crude Oil
changes:
- message: You can now pet the mail teleporter
type: Add
id: 8058
time: '2025-03-13T12:49:25.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35819
- author: ScarKy0
changes:
- message: Singularity and Whitehole grenades are now in the Disruption category
of uplinks.
type: Tweak
- message: Singularity and Whitehole grenades have had their prices reduced to 2TC
each.
type: Tweak
- message: Whitehole grenade no longer has extremely overtuned strength.
type: Fix
id: 8059
time: '2025-03-13T13:12:27.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35821
- author: slarticodefast, ps3moira
changes:
- message: Added the paradox clone as a ghostrole antagonist.
type: Add
id: 8060
time: '2025-03-13T17:09:07.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35794
- author: SlamBamActionman
changes:
- message: All roundstart antags now roll before job selection. You are now able
to ready up with antag-immune job preferences (Sec/Command/interns) set to High,
and if selected for antag you will get an non-antag-immune job instead.
type: Tweak
id: 8061
time: '2025-03-13T20:21:43.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35823
- author: Coolsurf6
changes:
- message: Added a new and powerful drink called Bacchus' Blessing for all bartenders
to struggle with.
type: Add
id: 8062
time: '2025-03-14T06:49:26.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35306
- author: Quantum-cross
changes:
- message: Corrupt borg speech if they are damaged or low power
type: Add
id: 8063
time: '2025-03-14T15:31:10.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35318
- author: Centronias
changes:
- message: Moths can once again eat lizard plushies
type: Fix
- message: Lizard plushes can once again be inserted as payloads into modular grenades
and mines
type: Fix
id: 8064
time: '2025-03-14T20:22:31.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35835
- author: K-Dynamic
changes:
- message: Electrocution stun duration decreased overall; LV or hacking from 8 seconds
to 5, MV from 10 seconds to 7.5, HV from 12 seconds to 10.
type: Tweak
id: 8065
time: '2025-03-15T15:57:36.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/34578
- author: sowelipililimute
changes:
- message: Added NanoTask
type: Add
id: 8066
time: '2025-03-15T16:24:24.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/34095
- author: K-Dynamic
changes:
- message: Renamed "shell box (X)" to "ammunition box (.50 X)" to match existing
naming scheme, and are thus easier to find in certain vendors and fabricators.
type: Tweak
id: 8067
time: '2025-03-15T16:43:49.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35326
- author: VerinSenpai
changes:
- message: Holopads now collide with walls instead of phasing through them.
type: Fix
id: 8068
time: '2025-03-16T00:41:50.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/34300
- author: Tayrtahn
changes:
- message: Cyborgs no longer have their brains removed if the salvage site they
are on despawns.
type: Fix
id: 8069
time: '2025-03-16T18:15:39.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35855
- author: slarticodefast
changes:
- message: Paradox clones now recieve a copy of (almost) all the inventory items
their original has.
type: Tweak
id: 8070
time: '2025-03-16T22:02:19.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35838
- author: Errant
changes:
- message: Some ghostrole antagonists were incorrectly not marked as such in the
round end playerlist. Now all antagonists are sorted to the top and have red
names.
type: Fix
id: 8071
time: '2025-03-16T22:11:31.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35832
- author: slarticodefast
changes:
- message: Paradox clones now receive any objective the original had.
type: Add
id: 8072
time: '2025-03-16T22:24:48.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35829
- author: Errant-4
changes:
- message: The Syndicate Instigator shuttle's crew now has the proper antagonist
role type.
type: Fix
id: 8073
time: '2025-03-17T16:27:08.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35892
- author: SlamBamActionman
changes:
- message: The Syndicate Backpack can now fit more items (35 item slots, from 28).
type: Tweak
id: 8074
time: '2025-03-17T17:58:57.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35888
- author: beck-thompson
changes:
- message: Chameleon vests now have the same stats as winter coats (Cold protection,
minor heat increase, 5% slash resistance, 10% heat resistance)
type: Tweak
id: 8075
time: '2025-03-17T18:39:04.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/34929
- author: slarticodefast
changes:
- message: Paradox clones now receive any implants the original has.
type: Add
- message: Cloning pods no longer copy items in slime storage.
type: Fix
id: 8076
time: '2025-03-18T05:21:56.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35906
- author: slarticodefast
changes:
- message: Tomato Killers now take damage from weedkiller and plant b gone.
type: Tweak
id: 8077
time: '2025-03-18T08:00:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35898
- author: slarticodefast
changes:
- message: Paradox Clones now start with their suit sensors turned off.
type: Tweak
id: 8078
time: '2025-03-18T20:07:06.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/35909

View File

@@ -1,6 +1 @@
[events]
# Annoying
enabled = false
[shuttle]
auto_call_time = 0

View File

@@ -5,6 +5,9 @@ lobbyenabled = false
map = "Dev"
role_timers = false
[events]
enabled = false
[ghost]
role_time = 0.5
quick_lottery = true

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,7 @@ device-pda-slot-component-slot-name-cartridge = Cartridge
default-program-name = Program
notekeeper-program-name = Notekeeper
nano-task-program-name = NanoTask
news-read-program-name = Station news
crew-manifest-program-name = Crew manifest
@@ -28,6 +29,47 @@ astro-nav-program-name = AstroNav
med-tek-program-name = MedTek
# NanoTask cartridge
nano-task-ui-heading-high-priority-tasks =
{ $amount ->
[zero] No High Priority Tasks
[one] 1 High Priority Task
*[other] {$amount} High Priority Tasks
}
nano-task-ui-heading-medium-priority-tasks =
{ $amount ->
[zero] No Medium Priority Tasks
[one] 1 Medium Priority Task
*[other] {$amount} Medium Priority Tasks
}
nano-task-ui-heading-low-priority-tasks =
{ $amount ->
[zero] No Low Priority Tasks
[one] 1 Low Priority Task
*[other] {$amount} Low Priority Tasks
}
nano-task-ui-done = Done
nano-task-ui-revert-done = Undo
nano-task-ui-priority-low = Low
nano-task-ui-priority-medium = Medium
nano-task-ui-priority-high = High
nano-task-ui-cancel = Cancel
nano-task-ui-print = Print
nano-task-ui-delete = Delete
nano-task-ui-save = Save
nano-task-ui-new-task = New Task
nano-task-ui-description-label = Description:
nano-task-ui-description-placeholder = Get something important
nano-task-ui-requester-label = Requester:
nano-task-ui-requester-placeholder = John Nanotrasen
nano-task-ui-item-title = Edit Task
nano-task-printed-description = Description: {$description}
nano-task-printed-requester = Requester: {$requester}
nano-task-printed-high-priority = Priority: High
nano-task-printed-medium-priority = Priority: Medium
nano-task-printed-low-priority = Priority: Low
# Wanted list cartridge
wanted-list-program-name = Wanted list
wanted-list-label-no-records = It's all right, cowboy

View File

@@ -4,8 +4,5 @@ cmd-showmarkers-help = Usage: {$command}
cmd-showsubfloor-desc = Makes entities below the floor always visible.
cmd-showsubfloor-help = Usage: {$command}
cmd-showsubfloorforever-desc = Makes entities below the floor always visible until the client is restarted.
cmd-showsubfloorforever-help = Usage: {$command}
cmd-notify-desc = Send a notify client side.
cmd-notify-help = Usage: {$command} <message>

View File

@@ -0,0 +1,20 @@
placeholders-criminal-records-wanted-reason-1 = Ate a delicious valid salad
placeholders-criminal-records-wanted-reason-2 = Ate their own shoes
placeholders-criminal-records-wanted-reason-3 = Being a clown
placeholders-criminal-records-wanted-reason-4 = Being a mime
placeholders-criminal-records-wanted-reason-5 = Breathed the wrong way
placeholders-criminal-records-wanted-reason-6 = Broke into evac
placeholders-criminal-records-wanted-reason-7 = Did literally nothing
placeholders-criminal-records-wanted-reason-8 = Did their job
placeholders-criminal-records-wanted-reason-9 = Didn't say hello to me
placeholders-criminal-records-wanted-reason-10 = Drank one too many
placeholders-criminal-records-wanted-reason-11 = Had two toolboxes, that's too many
placeholders-criminal-records-wanted-reason-12 = Lied on common radio
placeholders-criminal-records-wanted-reason-13 = Looked at me funny
placeholders-criminal-records-wanted-reason-14 = Lubed up the entire way to evac
placeholders-criminal-records-wanted-reason-15 = Set AME up on time
placeholders-criminal-records-wanted-reason-16 = Slipped the HoS
placeholders-criminal-records-wanted-reason-17 = Stole the clown's mask
placeholders-criminal-records-wanted-reason-18 = Told an unfunny joke
placeholders-criminal-records-wanted-reason-19 = Wore a gasmask
placeholders-criminal-records-wanted-reason-20 = Wore boxing gloves

View File

@@ -0,0 +1,84 @@
news-dataset-1 = Tree stuck in tajaran; firefighters baffled.
news-dataset-2 = Armadillos want aardvarks removed from dictionary claims 'here first'.
news-dataset-3 = Angel found dancing on pinhead ordered to stop; cited for public nuisance.
news-dataset-4 = Letters claim they are better than number; 'Always have been'.
news-dataset-5 = Pens proclaim pencils obsolete, 'lead is dead'.
news-dataset-6 = Rock and paper sues scissors for discrimination.
news-dataset-7 = Steak tell-all book reveals he never liked sitting by potato.
news-dataset-8 = Woodchuck stops counting how many times hes chucked 'Never again'.
news-dataset-9 = 'Here kitty kitty' no longer preferred tajaran retrieval technique.
news-dataset-10 = Man travels 7000 light years to retrieve lost hankie, 'It was my favourite'.
news-dataset-11 = New bowling lane that shoots mini-meteors at bowlers very popular.
news-dataset-12 = Skrell marries computer; wedding attended by 100 modems.
news-dataset-13 = Chef reports successfully using harmonica as cheese grater.
news-dataset-14 = Nanotrasen invents handkerchief that says 'Bless you' after sneeze.
news-dataset-15 = Clone accused of posing for other cloness school photo.
news-dataset-16 = Clone accused of stealing other cloness employee of the month award.
news-dataset-17 = Woman robs station with hair dryer; crewmen love new style.
news-dataset-18 = This space for rent.
news-dataset-19 = Skrell Scientist Discovers Abacus Can Be Used To Dry Towels
news-dataset-20 = Survey: 'Cheese Louise' Voted Best Pizza Restaurant In Tau Ceti
news-dataset-21 = Swamp Gas Verified To Be Exhalations Of Stars--Movie Stars--Long Passed
news-dataset-22 = Tainted Broccoli Weapon Of Choice For Syndicate Assassins
news-dataset-23 = Chefs Find Broccoli Effective Tool For Cutting Cheese
news-dataset-24 = Broccoli Found To Cause Grumpiness In Monkeys
news-dataset-25 = Giant Hairball Has Perfect Grammar But Rolls rr's Too Much, Linguists Say
news-dataset-26 = Gibson Gazette Updates Frequently Absurd, Poll Indicates
news-dataset-27 = Taj Demand Longer Breaks, Cleaner Litter, Slower Mice
news-dataset-28 = Survey: 3 Out Of 5 Skrell Loathe Modern Art
news-dataset-29 = Skrell Scientist Discovers Gravity While Falling Down Stairs
news-dataset-30 = Humans Everywhere Agree: Purring Tajarans Are Happy Tajarans
news-dataset-31 = From The Desk Of Wise Guy Sammy: One Word In This Gazette Is Sdrawkcab
news-dataset-32 = From The Desk Of Wise Guy Sammy: It's Hard To Have Too Much Shelf Space
news-dataset-33 = From The Desk Of Wise Guy Sammy: Wine And Friendships Get Better With Age
news-dataset-34 = From The Desk Of Wise Guy Sammy: The Insides Of Golf Balls Are Mostly Rubber Bands
news-dataset-35 = From The Desk Of Wise Guy Sammy: You Don't Have To Fool All The People, Just The Right Ones
news-dataset-36 = From The Desk Of Wise Guy Sammy: If You Made The Mess, You Clean It Up
news-dataset-37 = From The Desk Of Wise Guy Sammy: It Is Easier To Get Forgiveness Than Permission
news-dataset-38 = From The Desk Of Wise Guy Sammy: Check Your Facts Before Making A Fool Of Yourself
news-dataset-39 = From The Desk Of Wise Guy Sammy: You Can't Outwait A Bureaucracy
news-dataset-40 = From The Desk Of Wise Guy Sammy: It's Better To Yield Right Of Way Than To Demand It
news-dataset-41 = From The Desk Of Wise Guy Sammy: A Person Who Likes Cats Can't Be All Bad
news-dataset-42 = From The Desk Of Wise Guy Sammy: Help Is The Sunny Side Of Control
news-dataset-43 = From The Desk Of Wise Guy Sammy: Two Points Determine A Straight Line
news-dataset-44 = From The Desk Of Wise Guy Sammy: Reading Improves The Mind And Lifts The Spirit
news-dataset-45 = From The Desk Of Wise Guy Sammy: Better To Aim High And Miss Then To Aim Low And Hit
news-dataset-46 = From The Desk Of Wise Guy Sammy: Meteors Often Strike The Same Place More Than Once
news-dataset-47 = Tommy B. Saif Sez: Look Both Ways Before Boarding The Shuttle
news-dataset-48 = Tommy B. Saif Sez: Hold On; Sudden Stops Sometimes Necessary
news-dataset-49 = Tommy B. Saif Sez: Keep Fingers Away From Moving Panels
news-dataset-50 = Tommy B. Saif Sez: No Left Turn, Except Shuttles
news-dataset-51 = Tommy B. Saif Sez: Return Seats And Trays To Their Proper Upright Position
news-dataset-52 = Tommy B. Saif Sez: Eating And Drinking In Docking Bays Is Prohibited
news-dataset-53 = Tommy B. Saif Sez: Accept No Substitutes, And Don't Be Fooled By Imitations
news-dataset-54 = Tommy B. Saif Sez: Do Not Remove This Tag Under Penalty Of Law
news-dataset-55 = Tommy B. Saif Sez: Always Mix Thoroughly When So Instructed
news-dataset-56 = Tommy B. Saif Sez: Try To Keep Six Month's Expenses In Reserve
news-dataset-57 = Tommy B. Saif Sez: Change Not Given Without Purchase
news-dataset-58 = Tommy B. Saif Sez: If You Break It, You Buy It
news-dataset-59 = Tommy B. Saif Sez: Reservations Must Be Cancelled 48 Hours Prior To Event To Obtain Refund
news-dataset-60 = Doughnuts: Is There Anything They Can't Do
news-dataset-61 = If Tin Whistles Are Made Of Tin, What Do They Make Foghorns Out Of?
news-dataset-62 = Broccoli discovered to be colonies of tiny aliens with murder on their minds
## Commented
# {{AFFECTED}} clerk first person able to pronounce '@*$%!'.
# {{AFFECTED}} delis serving boiled paperback dictionaries, 'Adjectives chewy' customers declare.
# {{AFFECTED}} weather deemed 'boring'; meteors and rad storms to be imported.
# Most {{AFFECTED}} security officers prefer cream over sugar.
# Palindrome speakers conference in {{AFFECTED}}; 'Wow!' says Otto.
# Question mark worshipped as deity by ancient {{AFFECTED}} dwellers.
# Spilled milk causes whole {{AFFECTED}} populace to cry.
# World largest carp patty at display on {{AFFECTED}}.
# Guy gets tattoo of Tau Ceti on chest '[pick(CentCom,star,starship,asteroid)] tickles most'.
# {{AFFECTED}} Baker Wins Pickled Crumpet Toss Three Years Running
# I Was Framed, jokes {{AFFECTED}} artist
# Mysterious Loud Rumbling Noises In {{AFFECTED}} Found To Be Mysterious Loud Rumblings
# Alien ambassador becomes lost on {{AFFECTED}}, refuses to ask for directions
# Survey: 80% Of People on {{AFFECTED}} Love Clog-Dancing
# {{AFFECTED}} Phonebooks Print All Wrong Numbers; Results In 15 New Marriages
# Tajaran Burglar Spotted on {{AFFECTED}}, Mistaken For Dalmatian
# Esoteric Verbosity Culminates In Communicative Ennui, {{AFFECTED}} Academics Note
# Boy Saves Tajaran From Tree on {{AFFECTED}}, Thousands Cheer
# Shipment Of Apples Overturns, {{AFFECTED}} Diner Offers Applesauce Special
# Spotted Owl Spotted on {{AFFECTED}}

View File

@@ -0,0 +1,14 @@
rat-king-command-stay-1 = Sit!
rat-king-command-stay-2 = Stay!
rat-king-command-stay-3 = Stop!
rat-king-command-follow-1 = Heel!
rat-king-command-follow-2 = Follow!
rat-king-command-cheese-1 = Attack!
rat-king-command-cheese-2 = Sic!
rat-king-command-cheese-3 = Kill!
rat-king-command-cheese-4 = Cheese 'Em!
rat-king-command-loose-1 = Free!
rat-king-command-loose-2 = Loose!

View File

@@ -264,6 +264,17 @@ flavor-complex-themartinez = like violets and lemon vodka
flavor-complex-cogchamp = like brass
flavor-complex-white-gilgamesh = like lightly carbonated cream
flavor-complex-antifreeze = warm
flavor-complex-caipirinha = like Brazil
flavor-complex-daiquiri = like rum, lime and sugar
flavor-complex-deathintheafternoon = like anise and champagne
flavor-complex-empress75 = like tyrian purple
flavor-complex-espressomartini = like vodka and coffee
flavor-complex-mayojito = like stomach turmoil
flavor-complex-mimeosa = like silence and oranges
flavor-complex-mimosa = like an early brunch
flavor-complex-moscowmule = like vodka and ginger ale
flavor-complex-thesunalsorises = like an absinthe daiquiri
flavor-complex-whiskeysour = like whiskey and egg
flavor-complex-zombiecocktail = like eating brains
flavor-complex-absinthe = like anise
flavor-complex-blue-curacao = like orange flowers
@@ -301,6 +312,7 @@ flavor-complex-toxins-special = like space exploration
flavor-complex-vodka-martini = like a spy movie from Russia
flavor-complex-vodka-tonic = refreshingly bitter
flavor-complex-coconut-rum = like nutty fermented sugar
flavor-complex-bacchus-blessing = like a wall of bricks
### This is exactly what pilk tastes like. I'm not even joking. I might've been a little drunk though
flavor-complex-pilk = like sweet milk

View File

@@ -5,28 +5,28 @@ roles-antag-rev-head-objective = Your objective is to take over the station by c
head-rev-role-greeting =
You are a Head Revolutionary.
You are tasked with removing all of Command from station via death, exilement or imprisonment.
You are tasked with removing all of Command from station via conversion, death or imprisonment.
The Syndicate has sponsored you with a flash that converts the crew to your side.
Beware, this won't work on Security, Command, or those wearing sunglasses.
Beware, this won't work on those with a mindshield or wearing eye protection.
Viva la revolución!
head-rev-briefing =
Use flashes to convert people to your cause.
Get rid of all heads to take over the station.
Get rid of or convert all heads to take over the station.
head-rev-break-mindshield = The Mindshield was destroyed!
## Rev
roles-antag-rev-name = Revolutionary
roles-antag-rev-objective = Your objective is to ensure the safety and follow the orders of the Head Revolutionaries as well as getting rid of all Command staff on station.
roles-antag-rev-objective = Your objective is to ensure the safety and follow the orders of the Head Revolutionaries as well as getting rid or converting of all Command staff on station.
rev-break-control = {$name} has remembered their true allegiance!
rev-role-greeting =
You are a Revolutionary.
You are tasked with taking over the station and protecting the Head Revolutionaries.
Get rid of all of the Command staff.
Get rid of all of or convert the Command staff.
Viva la revolución!
rev-briefing = Help your head revolutionaries get rid of every head to take over the station.

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