Merge remote-tracking branch 'refs/remotes/upstream/master' into ed-08-05-2024-upstream

# Conflicts:
#	Content.Server/GameTicking/GameTicker.Spawning.cs
#	Content.Shared/Preferences/HumanoidCharacterProfile.cs
#	Resources/Prototypes/lobbyscreens.yml
This commit is contained in:
Ed
2024-05-08 12:26:05 +03:00
585 changed files with 5719 additions and 3832 deletions

View File

@@ -12,6 +12,7 @@ tab_width = 4
#end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 120
#### .NET Coding Conventions ####
@@ -336,7 +337,11 @@ dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter
# ReSharper properties
resharper_braces_for_ifelse = required_for_multiline
resharper_csharp_wrap_arguments_style = chop_if_long
resharper_csharp_wrap_parameters_style = chop_if_long
resharper_keep_existing_attribute_arrangement = true
resharper_wrap_chained_binary_patterns = chop_if_long
resharper_wrap_chained_method_calls = chop_if_long
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}]
indent_size = 2

View File

@@ -87,14 +87,12 @@ namespace Content.Client.Changelog
if (!tab.AdminOnly || isAdmin)
{
Tabs.SetTabVisible(i, true);
tab.Visible = true;
visibleTabs++;
firstVisible ??= i;
}
else
{
Tabs.SetTabVisible(i, false);
tab.Visible = false;
}
}

View File

@@ -12,7 +12,7 @@
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Actions/scream.png"/>
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Emotes/vocal.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"/>

View File

@@ -119,6 +119,7 @@ namespace Content.Client.Entry
_prototypeManager.RegisterIgnore("wireLayout");
_prototypeManager.RegisterIgnore("alertLevels");
_prototypeManager.RegisterIgnore("nukeopsRole");
_prototypeManager.RegisterIgnore("ghostRoleRaffleDecider");
_componentFactory.GenerateNetIds();
_adminManager.Initialize();

View File

@@ -100,7 +100,7 @@ public partial class BaseShuttleControl : MapGridControl
var textDimensions = handle.GetDimensions(Font, text, UIScale);
handle.DrawCircle(origin, scaledRadius, color, false);
handle.DrawString(Font, ScalePosition(new Vector2(0f, -radius)) - new Vector2(0f, textDimensions.Y), text, color);
handle.DrawString(Font, ScalePosition(new Vector2(0f, -radius)) - new Vector2(0f, textDimensions.Y), text, UIScale, color);
}
const int gridLinesRadial = 8;

View File

@@ -3,7 +3,6 @@ using Content.Client.Actions.UI;
using Content.Client.Cooldown;
using Content.Shared.Alert;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
@@ -117,7 +116,9 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_entityManager.QueueDeleteEntity(_spriteViewEntity);
if (!_entityManager.Deleted(_spriteViewEntity))
_entityManager.QueueDeleteEntity(_spriteViewEntity);
}
}

View File

@@ -5,7 +5,7 @@
Text="{Loc 'ghost-roles-window-request-role-button'}"
StyleClasses="OpenRight"
HorizontalAlignment="Left"
SetWidth="150"/>
SetWidth="300"/>
<Button Name="FollowButton"
Access="Public"
Text="{Loc 'ghost-roles-window-follow-role-button'}"

View File

@@ -1,9 +1,72 @@
using Robust.Client.AutoGenerated;
using Content.Shared.Ghost.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles;
[GenerateTypedNameReferences]
public sealed partial class GhostRoleEntryButtons : BoxContainer
{
[Dependency] private readonly IGameTiming _timing = default!;
private readonly GhostRoleKind _ghostRoleKind;
private readonly uint _playerCount;
private readonly TimeSpan _raffleEndTime = TimeSpan.MinValue;
public GhostRoleEntryButtons(GhostRoleInfo ghostRoleInfo)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_ghostRoleKind = ghostRoleInfo.Kind;
if (IsActiveRaffle(_ghostRoleKind))
{
_playerCount = ghostRoleInfo.RafflePlayerCount;
_raffleEndTime = ghostRoleInfo.RaffleEndTime;
}
UpdateRequestButton();
}
private void UpdateRequestButton()
{
var messageId = _ghostRoleKind switch
{
GhostRoleKind.FirstComeFirstServe => "ghost-roles-window-request-role-button",
GhostRoleKind.RaffleReady => "ghost-roles-window-join-raffle-button",
GhostRoleKind.RaffleInProgress => "ghost-roles-window-raffle-in-progress-button",
GhostRoleKind.RaffleJoined => "ghost-roles-window-leave-raffle-button",
_ => throw new ArgumentOutOfRangeException(nameof(_ghostRoleKind),
$"Unknown {nameof(GhostRoleKind)} '{_ghostRoleKind}'")
};
if (IsActiveRaffle(_ghostRoleKind))
{
var timeLeft = _timing.CurTime <= _raffleEndTime
? _raffleEndTime - _timing.CurTime
: TimeSpan.Zero;
var timeString = $"{timeLeft.Minutes:0}:{timeLeft.Seconds:00}";
RequestButton.Text = Loc.GetString(messageId, ("time", timeString), ("players", _playerCount));
}
else
{
RequestButton.Text = Loc.GetString(messageId);
}
}
private static bool IsActiveRaffle(GhostRoleKind kind)
{
return kind is GhostRoleKind.RaffleInProgress or GhostRoleKind.RaffleJoined;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (IsActiveRaffle(_ghostRoleKind))
{
UpdateRequestButton();
}
}
}

View File

@@ -26,7 +26,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
foreach (var role in roles)
{
var button = new GhostRoleEntryButtons();
var button = new GhostRoleEntryButtons(role);
button.RequestButton.OnPressed += _ => OnRoleSelected?.Invoke(role);
button.FollowButton.OnPressed += _ => OnRoleFollow?.Invoke(role);

View File

@@ -20,13 +20,24 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
{
_window = new GhostRolesWindow();
_window.OnRoleRequested += info =>
_window.OnRoleRequestButtonClicked += info =>
{
if (_windowRules != null)
_windowRules.Close();
_windowRules?.Close();
if (info.Kind == GhostRoleKind.RaffleJoined)
{
SendMessage(new LeaveGhostRoleRaffleMessage(info.Identifier));
return;
}
_windowRules = new GhostRoleRulesWindow(info.Rules, _ =>
{
SendMessage(new GhostRoleTakeoverRequestMessage(info.Identifier));
SendMessage(new RequestGhostRoleMessage(info.Identifier));
// if raffle role, close rules window on request, otherwise do
// old behavior of waiting for the server to close it
if (info.Kind != GhostRoleKind.FirstComeFirstServe)
_windowRules?.Close();
});
_windowRulesId = info.Identifier;
_windowRules.OnClose += () =>
@@ -38,7 +49,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
_window.OnRoleFollow += info =>
{
SendMessage(new GhostRoleFollowRequestMessage(info.Identifier));
SendMessage(new FollowGhostRoleMessage(info.Identifier));
};
_window.OnClose += () =>
@@ -64,7 +75,8 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
{
base.HandleState(state);
if (state is not GhostRolesEuiState ghostState) return;
if (state is not GhostRolesEuiState ghostState)
return;
_window.ClearEntries();
var entityManager = IoCManager.Resolve<IEntityManager>();

View File

@@ -1,7 +1,7 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'ghost-roles-window-title'}"
MinSize="450 400"
SetSize="400 500">
MinSize="490 400"
SetSize="490 500">
<Label Name="NoRolesMessage"
Text="{Loc 'ghost-roles-window-no-roles-available-label'}"
VerticalAlignment="Top" />

View File

@@ -9,7 +9,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
[GenerateTypedNameReferences]
public sealed partial class GhostRolesWindow : DefaultWindow
{
public event Action<GhostRoleInfo>? OnRoleRequested;
public event Action<GhostRoleInfo>? OnRoleRequestButtonClicked;
public event Action<GhostRoleInfo>? OnRoleFollow;
public void ClearEntries()
@@ -23,7 +23,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
NoRolesMessage.Visible = false;
var entry = new GhostRolesEntry(name, description, hasAccess, reason, roles, spriteSystem);
entry.OnRoleSelected += OnRoleRequested;
entry.OnRoleSelected += OnRoleRequestButtonClicked;
entry.OnRoleFollow += OnRoleFollow;
EntryContainer.AddChild(entry);
}

View File

@@ -1,4 +1,5 @@
using Content.Client.Eui;
using Content.Server.Ghost.Roles.Raffles;
using Content.Shared.Eui;
using Content.Shared.Ghost.Roles;
using JetBrains.Annotations;
@@ -41,7 +42,7 @@ public sealed class MakeGhostRoleEui : BaseEui
_window.OpenCentered();
}
private void OnMake(NetEntity entity, string name, string description, string rules, bool makeSentient)
private void OnMake(NetEntity entity, string name, string description, string rules, bool makeSentient, GhostRoleRaffleSettings? raffleSettings)
{
var session = _playerManager.LocalSession;
if (session == null)
@@ -49,12 +50,22 @@ public sealed class MakeGhostRoleEui : BaseEui
return;
}
var command = raffleSettings is not null ? "makeghostroleraffled" : "makeghostrole";
var makeGhostRoleCommand =
$"makeghostrole " +
$"{command} " +
$"\"{CommandParsing.Escape(entity.ToString())}\" " +
$"\"{CommandParsing.Escape(name)}\" " +
$"\"{CommandParsing.Escape(description)}\" " +
$"\"{CommandParsing.Escape(rules)}\"";
$"\"{CommandParsing.Escape(description)}\" ";
if (raffleSettings is not null)
{
makeGhostRoleCommand += $"{raffleSettings.InitialDuration} " +
$"{raffleSettings.JoinExtendsDurationBy} " +
$"{raffleSettings.MaxDuration} ";
}
makeGhostRoleCommand += $"\"{CommandParsing.Escape(rules)}\"";
_consoleHost.ExecuteCommand(session, makeGhostRoleCommand);

View File

@@ -22,6 +22,24 @@
<Label Name="MakeSentientLabel" Text="Make Sentient" />
<CheckBox Name="MakeSentientCheckbox" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Name="RaffleLabel" Text="Raffle Role?" />
<OptionButton Name="RaffleButton" />
</BoxContainer>
<BoxContainer Name="RaffleCustomSettingsContainer" Orientation="Vertical" Visible="False">
<BoxContainer Orientation="Horizontal">
<Label Name="RaffleInitialDurationLabel" Text="Initial Duration (s)" />
<SpinBox Name="RaffleInitialDuration" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Name="RaffleJoinExtendsDurationByLabel" Text="Joins Extend By (s)" />
<SpinBox Name="RaffleJoinExtendsDurationBy" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Name="RaffleMaxDurationLabel" Text="Max Duration (s)" />
<SpinBox Name="RaffleMaxDuration" HorizontalExpand="True" />
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Button Name="MakeButton" Text="Make" />
</BoxContainer>

View File

@@ -1,7 +1,12 @@
using System.Numerics;
using System.Linq;
using System.Numerics;
using Content.Server.Ghost.Roles.Raffles;
using Content.Shared.Ghost.Roles.Raffles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
@@ -9,10 +14,20 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
[GenerateTypedNameReferences]
public sealed partial class MakeGhostRoleWindow : DefaultWindow
{
public delegate void MakeRole(NetEntity uid, string name, string description, string rules, bool makeSentient);
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly List<GhostRoleRaffleSettingsPrototype> _rafflePrototypes = [];
private const int RaffleDontRaffleId = -1;
private const int RaffleCustomRaffleId = -2;
private int _raffleSettingId = RaffleDontRaffleId;
private NetEntity? Entity { get; set; }
public event MakeRole? OnMake;
public MakeGhostRoleWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
MakeSentientLabel.MinSize = new Vector2(150, 0);
@@ -23,13 +38,87 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
RoleDescription.MinSize = new Vector2(300, 0);
RoleRulesLabel.MinSize = new Vector2(150, 0);
RoleRules.MinSize = new Vector2(300, 0);
RaffleLabel.MinSize = new Vector2(150, 0);
RaffleButton.MinSize = new Vector2(300, 0);
RaffleInitialDurationLabel.MinSize = new Vector2(150, 0);
RaffleInitialDuration.MinSize = new Vector2(300, 0);
RaffleJoinExtendsDurationByLabel.MinSize = new Vector2(150, 0);
RaffleJoinExtendsDurationBy.MinSize = new Vector2(270, 0);
RaffleMaxDurationLabel.MinSize = new Vector2(150, 0);
RaffleMaxDuration.MinSize = new Vector2(270, 0);
MakeButton.OnPressed += OnPressed;
RaffleInitialDuration.OverrideValue(30);
RaffleJoinExtendsDurationBy.OverrideValue(5);
RaffleMaxDuration.OverrideValue(60);
RaffleInitialDuration.SetButtons(new List<int> { -30, -10 }, new List<int> { 10, 30 });
RaffleJoinExtendsDurationBy.SetButtons(new List<int> { -10, -5 }, new List<int> { 5, 10 });
RaffleMaxDuration.SetButtons(new List<int> { -30, -10 }, new List<int> { 10, 30 });
RaffleInitialDuration.IsValid = duration => duration > 0;
RaffleJoinExtendsDurationBy.IsValid = duration => duration >= 0;
RaffleMaxDuration.IsValid = duration => duration > 0;
RaffleInitialDuration.ValueChanged += OnRaffleDurationChanged;
RaffleJoinExtendsDurationBy.ValueChanged += OnRaffleDurationChanged;
RaffleMaxDuration.ValueChanged += OnRaffleDurationChanged;
RaffleButton.AddItem("Don't raffle", RaffleDontRaffleId);
RaffleButton.AddItem("Custom settings", RaffleCustomRaffleId);
var raffleProtos =
_prototypeManager.EnumeratePrototypes<GhostRoleRaffleSettingsPrototype>();
var idx = 0;
foreach (var raffleProto in raffleProtos)
{
_rafflePrototypes.Add(raffleProto);
var s = raffleProto.Settings;
var label =
$"{raffleProto.ID} (initial {s.InitialDuration}s, max {s.MaxDuration}s, join adds {s.JoinExtendsDurationBy}s)";
RaffleButton.AddItem(label, idx++);
}
MakeButton.OnPressed += OnMakeButtonPressed;
RaffleButton.OnItemSelected += OnRaffleButtonItemSelected;
}
private NetEntity? Entity { get; set; }
private void OnRaffleDurationChanged(ValueChangedEventArgs args)
{
ValidateRaffleDurations();
}
public event MakeRole? OnMake;
private void ValidateRaffleDurations()
{
if (RaffleInitialDuration.Value > RaffleMaxDuration.Value)
{
MakeButton.Disabled = true;
MakeButton.ToolTip = "The initial duration must not exceed the maximum duration.";
}
else
{
MakeButton.Disabled = false;
MakeButton.ToolTip = null;
}
}
private void OnRaffleButtonItemSelected(OptionButton.ItemSelectedEventArgs args)
{
_raffleSettingId = args.Id;
args.Button.SelectId(args.Id);
if (args.Id != RaffleCustomRaffleId)
{
RaffleCustomSettingsContainer.Visible = false;
MakeButton.ToolTip = null;
MakeButton.Disabled = false;
}
else
{
RaffleCustomSettingsContainer.Visible = true;
ValidateRaffleDurations();
}
}
public void SetEntity(IEntityManager entManager, NetEntity entity)
{
@@ -38,14 +127,32 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
RoleEntity.Text = $"{entity}";
}
private void OnPressed(ButtonEventArgs args)
private void OnMakeButtonPressed(ButtonEventArgs args)
{
if (Entity == null)
{
return;
}
OnMake?.Invoke(Entity.Value, RoleName.Text, RoleDescription.Text, RoleRules.Text, MakeSentientCheckbox.Pressed);
GhostRoleRaffleSettings? raffleSettings = null;
if (_raffleSettingId == RaffleCustomRaffleId)
{
raffleSettings = new GhostRoleRaffleSettings()
{
InitialDuration = (uint) RaffleInitialDuration.Value,
JoinExtendsDurationBy = (uint) RaffleJoinExtendsDurationBy.Value,
MaxDuration = (uint) RaffleMaxDuration.Value
};
}
else if (_raffleSettingId != RaffleDontRaffleId)
{
raffleSettings = _rafflePrototypes[_raffleSettingId].Settings;
}
OnMake?.Invoke(Entity.Value, RoleName.Text, RoleDescription.Text, RoleRules.Text, MakeSentientCheckbox.Pressed, raffleSettings);
}
public delegate void MakeRole(NetEntity uid, string name, string description, string rules, bool makeSentient, GhostRoleRaffleSettings? settings);
}
}

View File

@@ -3,6 +3,7 @@ using Content.Shared.Stacks;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using static Robust.UnitTesting.RobustIntegrationTest;
namespace Content.IntegrationTests.Tests.Interaction;
@@ -54,7 +55,7 @@ public abstract partial class InteractionTest
/// <summary>
/// Convert applicable entity prototypes into stack prototypes.
/// </summary>
public void ConvertToStack(IPrototypeManager protoMan, IComponentFactory factory)
public async Task ConvertToStack(IPrototypeManager protoMan, IComponentFactory factory, ServerIntegrationInstance server)
{
if (Converted)
return;
@@ -73,11 +74,14 @@ public abstract partial class InteractionTest
return;
}
if (entProto.TryGetComponent<StackComponent>(factory.GetComponentName(typeof(StackComponent)),
out var stackComp))
StackComponent? stack = null;
await server.WaitPost(() =>
{
Prototype = stackComp.StackTypeId;
}
entProto.TryGetComponent(factory.GetComponentName(typeof(StackComponent)), out stack);
});
if (stack != null)
Prototype = stack.StackTypeId;
}
}
@@ -100,11 +104,14 @@ public abstract partial class InteractionTest
return default;
}
if (entProto.TryGetComponent<StackComponent>(Factory.GetComponentName(typeof(StackComponent)),
out var stackComp))
StackComponent? stack = null;
await Server.WaitPost(() =>
{
return await SpawnEntity((stackComp.StackTypeId, spec.Quantity), coords);
}
entProto.TryGetComponent(Factory.GetComponentName(typeof(StackComponent)), out stack);
});
if (stack != null)
return await SpawnEntity((stack.StackTypeId, spec.Quantity), coords);
Assert.That(spec.Quantity, Is.EqualTo(1), "SpawnEntity only supports returning a singular entity");
await Server.WaitPost(() => uid = SEntMan.SpawnEntity(spec.Prototype, coords));

View File

@@ -5,6 +5,7 @@ using Content.Shared.Stacks;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using static Robust.UnitTesting.RobustIntegrationTest;
namespace Content.IntegrationTests.Tests.Interaction;
@@ -111,7 +112,7 @@ public abstract partial class InteractionTest
/// <summary>
/// Convert applicable entity prototypes into stack prototypes.
/// </summary>
public void ConvertToStacks(IPrototypeManager protoMan, IComponentFactory factory)
public async Task ConvertToStacks(IPrototypeManager protoMan, IComponentFactory factory, ServerIntegrationInstance server)
{
if (Converted)
return;
@@ -130,14 +131,17 @@ public abstract partial class InteractionTest
continue;
}
if (!entProto.TryGetComponent<StackComponent>(factory.GetComponentName(typeof(StackComponent)),
out var stackComp))
StackComponent? stack = null;
await server.WaitPost(() =>
{
entProto.TryGetComponent(factory.GetComponentName(typeof(StackComponent)), out stack);
});
if (stack == null)
continue;
}
toRemove.Add(id);
toAdd.Add((stackComp.StackTypeId, quantity));
toAdd.Add((stack.StackTypeId, quantity));
}
foreach (var id in toRemove)

View File

@@ -5,12 +5,9 @@ using System.Linq;
using System.Numerics;
using System.Reflection;
using Content.Client.Construction;
using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Construction.Components;
using Content.Server.Gravity;
using Content.Server.Item;
using Content.Server.Power.Components;
using Content.Shared.Atmos;
using Content.Shared.Construction.Prototypes;
@@ -634,7 +631,7 @@ public abstract partial class InteractionTest
var entities = await DoEntityLookup(flags);
var found = ToEntityCollection(entities);
expected.Remove(found);
expected.ConvertToStacks(ProtoMan, Factory);
await expected.ConvertToStacks(ProtoMan, Factory, Server);
if (expected.Entities.Count == 0)
return;
@@ -670,7 +667,7 @@ public abstract partial class InteractionTest
LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Contained,
bool shouldSucceed = true)
{
spec.ConvertToStack(ProtoMan, Factory);
await spec.ConvertToStack(ProtoMan, Factory, Server);
var entities = await DoEntityLookup(flags);
foreach (var uid in entities)

View File

@@ -92,23 +92,32 @@ namespace Content.IntegrationTests.Tests
var allSizes = protoMan.EnumeratePrototypes<ItemSizePrototype>().ToList();
allSizes.Sort();
Assert.Multiple(() =>
await Assert.MultipleAsync(async () =>
{
foreach (var proto in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
if (proto.HasComponent<EntityStorageComponent>(compFact))
continue;
if (!proto.TryGetComponent<StorageComponent>("Storage", out var storage))
StorageComponent? storage = null;
ItemComponent? item = null;
StorageFillComponent fill = default!;
var size = 0;
await server.WaitAssertion(() =>
{
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
if (!proto.TryGetComponent("Storage", out storage))
{
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
return;
}
proto.TryGetComponent("Item", out item);
fill = (StorageFillComponent) proto.Components[id].Component;
size = GetFillSize(fill, false, protoMan, itemSys);
});
if (storage == null)
continue;
}
proto.TryGetComponent<ItemComponent>("Item", out var item);
var fill = (StorageFillComponent) proto.Components[id].Component;
var size = GetFillSize(fill, false, protoMan, itemSys);
var maxSize = storage.MaxItemSize;
if (storage.MaxItemSize == null)
@@ -138,7 +147,13 @@ namespace Content.IntegrationTests.Tests
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var fillItem))
continue;
if (!fillItem.TryGetComponent<ItemComponent>("Item", out var entryItem))
ItemComponent? entryItem = null;
await server.WaitPost(() =>
{
fillItem.TryGetComponent("Item", out entryItem);
});
if (entryItem == null)
continue;
Assert.That(protoMan.Index(entryItem.Size).Weight,
@@ -164,25 +179,25 @@ namespace Content.IntegrationTests.Tests
var itemSys = entMan.System<SharedItemSystem>();
Assert.Multiple(() =>
foreach (var proto in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
foreach (var proto in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
if (proto.HasComponent<StorageComponent>(compFact))
continue;
if (proto.HasComponent<StorageComponent>(compFact))
continue;
if (!proto.TryGetComponent<EntityStorageComponent>("EntityStorage", out var entStorage))
{
await server.WaitAssertion(() =>
{
if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
continue;
}
if (entStorage == null)
return;
var fill = (StorageFillComponent) proto.Components[id].Component;
var size = GetFillSize(fill, true, protoMan, itemSys);
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
$"{proto.ID} storage fill is too large.");
}
});
});
}
await pair.CleanReturnAsync();
}

View File

@@ -33,8 +33,8 @@ public sealed class PresetIdCardSystem : EntitySystem
var station = _stationSystem.GetOwningStation(uid);
// If we're not on an extended access station, the ID is already configured correctly from MapInit.
if (station == null || !Comp<StationJobsComponent>(station.Value).ExtendedAccess)
return;
if (station == null || !TryComp<StationJobsComponent>(station.Value, out var jobsComp) || !jobsComp.ExtendedAccess)
continue;
SetupIdAccess(uid, card, true);
SetupIdName(uid, card);

View File

@@ -7,6 +7,7 @@ using Content.Shared.Chat;
using Content.Shared.Mind;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Enums;
using Robust.Shared.Player;
namespace Content.Server.Antag;
@@ -63,15 +64,17 @@ public sealed partial class AntagSelectionSystem
/// </summary>
public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, AntagSelectionPlayerPool? pool, AntagSelectionDefinition def)
{
var poolSize = pool?.Count ?? _playerManager.Sessions.Length;
var poolSize = pool?.Count ?? _playerManager.Sessions
.Count(s => s.State.Status is not SessionStatus.Disconnected and not SessionStatus.Zombie);
// factor in other definitions' affect on the count.
var countOffset = 0;
foreach (var otherDef in ent.Comp.Definitions)
{
countOffset += Math.Clamp(poolSize / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio;
countOffset += Math.Clamp((poolSize - countOffset) / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio;
}
// make sure we don't double-count the current selection
countOffset -= Math.Clamp((poolSize + countOffset) / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
countOffset -= Math.Clamp(poolSize / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
return Math.Clamp((poolSize - countOffset) / def.PlayerRatio, def.Min, def.Max);
}

View File

@@ -280,11 +280,13 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
_transform.SetMapCoordinates((player, playerXform), pos);
}
// If we want to just do a ghost role spawner, set up data here and then return early.
// This could probably be an event in the future if we want to be more refined about it.
if (isSpawner)
{
if (!TryComp<GhostRoleAntagSpawnerComponent>(player, out var spawnerComp))
{
Log.Error("Antag spawner with GhostRoleAntagSpawnerComponent.");
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
return;
}
@@ -293,6 +295,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
return;
}
// The following is where we apply components, equipment, and other changes to our antagonist entity.
EntityManager.AddComponents(player, def.Components);
_stationSpawning.EquipStartingGear(player, def.StartingGear);
@@ -308,11 +311,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
_role.MindAddRoles(curMind.Value, def.MindComponents);
ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
}
if (def.Briefing is { } briefing)
{
SendBriefing(session, briefing);
SendBriefing(session, def.Briefing);
}
var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
@@ -325,7 +324,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
public AntagSelectionPlayerPool GetPlayerPool(Entity<AntagSelectionComponent> ent, List<ICommonSession> sessions, AntagSelectionDefinition def)
{
var preferredList = new List<ICommonSession>();
var secondBestList = new List<ICommonSession>();
var fallbackList = new List<ICommonSession>();
var unwantedList = new List<ICommonSession>();
var invalidList = new List<ICommonSession>();
foreach (var session in sessions)
@@ -344,7 +343,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
}
else if (def.FallbackRoles.Count != 0 && pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p)))
{
secondBestList.Add(session);
fallbackList.Add(session);
}
else
{
@@ -352,7 +351,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
}
}
return new AntagSelectionPlayerPool(new() { preferredList, secondBestList, unwantedList, invalidList });
return new AntagSelectionPlayerPool(new() { preferredList, fallbackList, unwantedList, invalidList });
}
/// <summary>

View File

@@ -17,5 +17,5 @@ public sealed partial class MobReplacementRuleComponent : Component
/// Chance per-entity.
/// </summary>
[DataField]
public float Chance = 0.001f;
public float Chance = 0.004f;
}

View File

@@ -9,180 +9,37 @@ using System.Numerics;
namespace Content.Server.Chemistry.Containers.EntitySystems;
[Obsolete("This is being depreciated. Use SharedSolutionContainerSystem instead!")]
public sealed partial class SolutionContainerSystem : SharedSolutionContainerSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SolutionContainerManagerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SolutionContainerManagerComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<ContainedSolutionComponent, ComponentShutdown>(OnComponentShutdown);
}
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name)
=> EnsureSolution(entity, name, out _);
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, out bool existed)
=> EnsureSolution(entity, name, FixedPoint2.Zero, out existed);
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, FixedPoint2 maxVol, out bool existed)
=> EnsureSolution(entity, name, maxVol, null, out existed);
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed)
{
var (uid, meta) = entity;
if (!Resolve(uid, ref meta))
throw new InvalidOperationException("Attempted to ensure solution on invalid entity.");
var manager = EnsureComp<SolutionContainerManagerComponent>(uid);
if (meta.EntityLifeStage >= EntityLifeStage.MapInitialized)
return EnsureSolutionEntity((uid, manager), name, maxVol, prototype, out existed).Comp.Solution;
else
return EnsureSolutionPrototype((uid, manager), name, maxVol, prototype, out existed);
EnsureSolution(entity, name, maxVol, prototype, out existed, out var solution);
return solution!;//solution is only ever null on the client, so we can suppress this
}
public void EnsureAllSolutions(Entity<SolutionContainerManagerComponent> entity)
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
public Entity<SolutionComponent> EnsureSolutionEntity(
Entity<SolutionContainerManagerComponent?> entity,
string name,
FixedPoint2 maxVol,
Solution? prototype,
out bool existed)
{
if (entity.Comp.Solutions is not { } prototypes)
return;
foreach (var (name, prototype) in prototypes)
{
EnsureSolutionEntity((entity.Owner, entity.Comp), name, prototype.MaxVolume, prototype, out _);
}
entity.Comp.Solutions = null;
Dirty(entity);
EnsureSolutionEntity(entity, name, out existed, out var solEnt, maxVol, prototype);
return solEnt!.Value;//solEnt is only ever null on the client, so we can suppress this
}
public Entity<SolutionComponent> EnsureSolutionEntity(Entity<SolutionContainerManagerComponent?> entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed)
{
existed = true;
var (uid, container) = entity;
var solutionSlot = ContainerSystem.EnsureContainer<ContainerSlot>(uid, $"solution@{name}", out existed);
if (!Resolve(uid, ref container, logMissing: false))
{
existed = false;
container = AddComp<SolutionContainerManagerComponent>(uid);
container.Containers.Add(name);
}
else if (!existed)
{
container.Containers.Add(name);
Dirty(uid, container);
}
var needsInit = false;
SolutionComponent solutionComp;
if (solutionSlot.ContainedEntity is not { } solutionId)
{
prototype ??= new() { MaxVolume = maxVol };
prototype.Name = name;
(solutionId, solutionComp, _) = SpawnSolutionUninitialized(solutionSlot, name, maxVol, prototype);
existed = false;
needsInit = true;
Dirty(uid, container);
}
else
{
solutionComp = Comp<SolutionComponent>(solutionId);
DebugTools.Assert(TryComp(solutionId, out ContainedSolutionComponent? relation) && relation.Container == uid && relation.ContainerName == name);
DebugTools.Assert(solutionComp.Solution.Name == name);
var solution = solutionComp.Solution;
solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, maxVol);
// Depending on MapInitEvent order some systems can ensure solution empty solutions and conflict with the prototype solutions.
// We want the reagents from the prototype to exist even if something else already created the solution.
if (prototype is { Volume.Value: > 0 })
solution.AddSolution(prototype, PrototypeManager);
Dirty(solutionId, solutionComp);
}
if (needsInit)
EntityManager.InitializeAndStartEntity(solutionId, Transform(solutionId).MapID);
return (solutionId, solutionComp);
}
private Solution EnsureSolutionPrototype(Entity<SolutionContainerManagerComponent?> entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed)
{
existed = true;
var (uid, container) = entity;
if (!Resolve(uid, ref container, logMissing: false))
{
container = AddComp<SolutionContainerManagerComponent>(uid);
existed = false;
}
if (container.Solutions is null)
container.Solutions = new(SolutionContainerManagerComponent.DefaultCapacity);
if (!container.Solutions.TryGetValue(name, out var solution))
{
solution = prototype ?? new() { Name = name, MaxVolume = maxVol };
container.Solutions.Add(name, solution);
existed = false;
}
else
solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, maxVol);
Dirty(uid, container);
return solution;
}
private Entity<SolutionComponent, ContainedSolutionComponent> SpawnSolutionUninitialized(ContainerSlot container, string name, FixedPoint2 maxVol, Solution prototype)
{
var coords = new EntityCoordinates(container.Owner, Vector2.Zero);
var uid = EntityManager.CreateEntityUninitialized(null, coords, null);
var solution = new SolutionComponent() { Solution = prototype };
AddComp(uid, solution);
var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name };
AddComp(uid, relation);
MetaData.SetEntityName(uid, $"solution - {name}");
ContainerSystem.Insert(uid, container, force: true);
return (uid, solution, relation);
}
#region Event Handlers
private void OnMapInit(Entity<SolutionContainerManagerComponent> entity, ref MapInitEvent args)
{
EnsureAllSolutions(entity);
}
private void OnComponentShutdown(Entity<SolutionContainerManagerComponent> entity, ref ComponentShutdown args)
{
foreach (var name in entity.Comp.Containers)
{
if (ContainerSystem.TryGetContainer(entity, $"solution@{name}", out var solutionContainer))
ContainerSystem.ShutdownContainer(solutionContainer);
}
entity.Comp.Containers.Clear();
}
private void OnComponentShutdown(Entity<ContainedSolutionComponent> entity, ref ComponentShutdown args)
{
if (TryComp(entity.Comp.Container, out SolutionContainerManagerComponent? container))
{
container.Containers.Remove(entity.Comp.ContainerName);
Dirty(entity.Comp.Container, container);
}
if (ContainerSystem.TryGetContainer(entity, $"solution@{entity.Comp.ContainerName}", out var solutionContainer))
ContainerSystem.ShutdownContainer(solutionContainer);
}
#endregion Event Handlers
}

View File

@@ -33,9 +33,11 @@ namespace Content.Server.Database
}
#region Preferences
public async Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId)
public async Task<PlayerPreferences?> GetPlayerPreferencesAsync(
NetUserId userId,
CancellationToken cancel = default)
{
await using var db = await GetDb();
await using var db = await GetDb(cancel);
var prefs = await db.DbContext
.Preference
@@ -47,7 +49,7 @@ namespace Content.Server.Database
.ThenInclude(l => l.Groups)
.ThenInclude(group => group.Loadouts)
.AsSingleQuery()
.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
.SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
if (prefs is null)
return null;
@@ -515,13 +517,13 @@ namespace Content.Server.Database
#endregion
#region Playtime
public async Task<List<PlayTime>> GetPlayTimes(Guid player)
public async Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel)
{
await using var db = await GetDb();
await using var db = await GetDb(cancel);
return await db.DbContext.PlayTime
.Where(p => p.PlayerId == player)
.ToListAsync();
.ToListAsync(cancel);
}
public async Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates)
@@ -673,7 +675,7 @@ namespace Content.Server.Database
*/
public async Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel)
{
await using var db = await GetDb();
await using var db = await GetDb(cancel);
return await db.DbContext.Admin
.Include(p => p.Flags)
@@ -688,7 +690,7 @@ namespace Content.Server.Database
public async Task<AdminRank?> GetAdminRankDataForAsync(int id, CancellationToken cancel = default)
{
await using var db = await GetDb();
await using var db = await GetDb(cancel);
return await db.DbContext.AdminRank
.Include(r => r.Flags)
@@ -697,7 +699,7 @@ namespace Content.Server.Database
public async Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel)
{
await using var db = await GetDb();
await using var db = await GetDb(cancel);
var admin = await db.DbContext.Admin.SingleAsync(a => a.UserId == userId.UserId, cancel);
db.DbContext.Admin.Remove(admin);
@@ -707,7 +709,7 @@ namespace Content.Server.Database
public async Task AddAdminAsync(Admin admin, CancellationToken cancel)
{
await using var db = await GetDb();
await using var db = await GetDb(cancel);
db.DbContext.Admin.Add(admin);
@@ -716,7 +718,7 @@ namespace Content.Server.Database
public async Task UpdateAdminAsync(Admin admin, CancellationToken cancel)
{
await using var db = await GetDb();
await using var db = await GetDb(cancel);
var existing = await db.DbContext.Admin.Include(a => a.Flags).SingleAsync(a => a.UserId == admin.UserId, cancel);
existing.Flags = admin.Flags;
@@ -728,7 +730,7 @@ namespace Content.Server.Database
public async Task RemoveAdminRankAsync(int rankId, CancellationToken cancel)
{
await using var db = await GetDb();
await using var db = await GetDb(cancel);
var admin = await db.DbContext.AdminRank.SingleAsync(a => a.Id == rankId, cancel);
db.DbContext.AdminRank.Remove(admin);
@@ -738,7 +740,7 @@ namespace Content.Server.Database
public async Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel)
{
await using var db = await GetDb();
await using var db = await GetDb(cancel);
db.DbContext.AdminRank.Add(rank);
@@ -811,7 +813,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel)
{
await using var db = await GetDb();
await using var db = await GetDb(cancel);
var existing = await db.DbContext.AdminRank
.Include(r => r.Flags)
@@ -1594,7 +1596,9 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
return db.DbContext.Database.HasPendingModelChanges();
}
protected abstract Task<DbGuard> GetDb([CallerMemberName] string? name = null);
protected abstract Task<DbGuard> GetDb(
CancellationToken cancel = default,
[CallerMemberName] string? name = null);
protected void LogDbOp(string? name)
{

View File

@@ -29,7 +29,11 @@ namespace Content.Server.Database
void Shutdown();
#region Preferences
Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile);
Task<PlayerPreferences> InitPrefsAsync(
NetUserId userId,
ICharacterProfile defaultProfile,
CancellationToken cancel);
Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index);
Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot);
@@ -38,7 +42,7 @@ namespace Content.Server.Database
// Single method for two operations for transaction.
Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot);
Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId);
Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel);
#endregion
#region User Ids
@@ -157,8 +161,9 @@ namespace Content.Server.Database
/// Look up a player's role timers.
/// </summary>
/// <param name="player">The player to get the role timer information from.</param>
/// <param name="cancel"></param>
/// <returns>All role timers belonging to the player.</returns>
Task<List<PlayTime>> GetPlayTimes(Guid player);
Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel = default);
/// <summary>
/// Update play time information in bulk.
@@ -346,7 +351,10 @@ namespace Content.Server.Database
_sqliteInMemoryConnection?.Dispose();
}
public Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile)
public Task<PlayerPreferences> InitPrefsAsync(
NetUserId userId,
ICharacterProfile defaultProfile,
CancellationToken cancel)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.InitPrefsAsync(userId, defaultProfile));
@@ -376,10 +384,10 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.SaveAdminOOCColorAsync(userId, color));
}
public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId)
public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetPlayerPreferencesAsync(userId));
return RunDbCommand(() => _db.GetPlayerPreferencesAsync(userId, cancel));
}
public Task AssignUserIdAsync(string name, NetUserId userId)
@@ -487,10 +495,10 @@ namespace Content.Server.Database
#region Playtime
public Task<List<PlayTime>> GetPlayTimes(Guid player)
public Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetPlayTimes(player));
return RunDbCommand(() => _db.GetPlayTimes(player, cancel));
}
public Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates)

View File

@@ -527,22 +527,26 @@ WHERE to_tsvector('english'::regconfig, a.message) @@ websearch_to_tsquery('engl
return time;
}
private async Task<DbGuardImpl> GetDbImpl([CallerMemberName] string? name = null)
private async Task<DbGuardImpl> GetDbImpl(
CancellationToken cancel = default,
[CallerMemberName] string? name = null)
{
LogDbOp(name);
await _dbReadyTask;
await _prefsSemaphore.WaitAsync();
await _prefsSemaphore.WaitAsync(cancel);
if (_msLag > 0)
await Task.Delay(_msLag);
await Task.Delay(_msLag, cancel);
return new DbGuardImpl(this, new PostgresServerDbContext(_options));
}
protected override async Task<DbGuard> GetDb([CallerMemberName] string? name = null)
protected override async Task<DbGuard> GetDb(
CancellationToken cancel = default,
[CallerMemberName] string? name = null)
{
return await GetDbImpl(name);
return await GetDbImpl(cancel, name);
}
private sealed class DbGuardImpl : DbGuard

View File

@@ -439,7 +439,7 @@ namespace Content.Server.Database
public override async Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
CancellationToken cancel)
{
await using var db = await GetDbImpl();
await using var db = await GetDbImpl(cancel);
var admins = await db.SqliteDbContext.Admin
.Include(a => a.Flags)
@@ -514,23 +514,27 @@ namespace Content.Server.Database
return DateTime.SpecifyKind(time, DateTimeKind.Utc);
}
private async Task<DbGuardImpl> GetDbImpl([CallerMemberName] string? name = null)
private async Task<DbGuardImpl> GetDbImpl(
CancellationToken cancel = default,
[CallerMemberName] string? name = null)
{
LogDbOp(name);
await _dbReadyTask;
if (_msDelay > 0)
await Task.Delay(_msDelay);
await Task.Delay(_msDelay, cancel);
await _prefsSemaphore.WaitAsync();
await _prefsSemaphore.WaitAsync(cancel);
var dbContext = new SqliteServerDbContext(_options());
return new DbGuardImpl(this, dbContext);
}
protected override async Task<DbGuard> GetDb([CallerMemberName] string? name = null)
protected override async Task<DbGuard> GetDb(
CancellationToken cancel = default,
[CallerMemberName] string? name = null)
{
return await GetDbImpl(name).ConfigureAwait(false);
return await GetDbImpl(cancel, name).ConfigureAwait(false);
}
private sealed class DbGuardImpl : DbGuard
@@ -569,9 +573,9 @@ namespace Content.Server.Database
_semaphore = new SemaphoreSlim(maxCount, maxCount);
}
public Task WaitAsync()
public Task WaitAsync(CancellationToken cancel = default)
{
var task = _semaphore.WaitAsync();
var task = _semaphore.WaitAsync(cancel);
if (_synchronous)
{

View File

@@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Preferences.Managers;
using Robust.Server.Player;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Utility;
@@ -16,17 +17,22 @@ namespace Content.Server.Database;
/// Actual loading code is handled by separate managers such as <see cref="IServerPreferencesManager"/>.
/// This manager is simply a centralized "is loading done" controller for other code to rely on.
/// </remarks>
public sealed class UserDbDataManager
public sealed class UserDbDataManager : IPostInjectInit
{
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
private readonly Dictionary<NetUserId, UserData> _users = new();
private ISawmill _sawmill = default!;
// TODO: Ideally connected/disconnected would be subscribed to IPlayerManager directly,
// but this runs into ordering issues with game ticker.
public void ClientConnected(ICommonSession session)
{
_sawmill.Verbose($"Initiating load for user {session}");
DebugTools.Assert(!_users.ContainsKey(session.UserId), "We should not have any cached data on client connect.");
var cts = new CancellationTokenSource();
@@ -51,11 +57,52 @@ public sealed class UserDbDataManager
private async Task Load(ICommonSession session, CancellationToken cancel)
{
await Task.WhenAll(
_prefs.LoadData(session, cancel),
_playTimeTracking.LoadData(session, cancel));
// The task returned by this function is only ever observed by callers of WaitLoadComplete,
// which doesn't even happen currently if the lobby is enabled.
// As such, this task must NOT throw a non-cancellation error!
try
{
await Task.WhenAll(
_prefs.LoadData(session, cancel),
_playTimeTracking.LoadData(session, cancel));
cancel.ThrowIfCancellationRequested();
_prefs.FinishLoad(session);
_sawmill.Verbose($"Load complete for user {session}");
}
catch (OperationCanceledException)
{
_sawmill.Debug($"Load cancelled for user {session}");
// We can rethrow the cancellation.
// This will make the task returned by WaitLoadComplete() also return a cancellation.
throw;
}
catch (Exception e)
{
// Must catch all exceptions here, otherwise task may go unobserved.
_sawmill.Error($"Load of user data failed: {e}");
// Kick them from server, since something is hosed. Let them try again I guess.
session.Channel.Disconnect("Loading of server user data failed, this is a bug.");
// We throw a OperationCanceledException so users of WaitLoadComplete() always see cancellation here.
throw new OperationCanceledException("Load of user data cancelled due to unknown error");
}
}
/// <summary>
/// Wait for all on-database data for a user to be loaded.
/// </summary>
/// <remarks>
/// The task returned by this function may end up in a cancelled state
/// (throwing <see cref="OperationCanceledException"/>) if the user disconnects while loading or an error occurs.
/// </remarks>
/// <param name="session"></param>
/// <returns>
/// A task that completes when all on-database data for a user has finished loading.
/// </returns>
public Task WaitLoadComplete(ICommonSession session)
{
return _users[session.UserId].Task;
@@ -63,7 +110,7 @@ public sealed class UserDbDataManager
public bool IsLoadComplete(ICommonSession session)
{
return GetLoadTask(session).IsCompleted;
return GetLoadTask(session).IsCompletedSuccessfully;
}
public Task GetLoadTask(ICommonSession session)
@@ -71,5 +118,10 @@ public sealed class UserDbDataManager
return _users[session.UserId].Task;
}
void IPostInjectInit.PostInject()
{
_sawmill = _logManager.GetSawmill("userdb");
}
private sealed record UserData(CancellationTokenSource Cancel, Task Task);
}

View File

@@ -81,6 +81,13 @@ public sealed partial class ExplosiveComponent : Component
[DataField("deleteAfterExplosion")]
public bool? DeleteAfterExplosion;
/// <summary>
/// Whether to not set <see cref="Exploded"/> to true, allowing it to explode multiple times.
/// This should never be used if it is damageable.
/// </summary>
[DataField]
public bool Repeatable;
/// <summary>
/// Avoid somehow double-triggering this explosion (e.g. by damaging this entity from its own explosion.
/// </summary>

View File

@@ -0,0 +1,25 @@
using Content.Server.Explosion.EntitySystems;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Constantly triggers after being added to an entity.
/// </summary>
[RegisterComponent, Access(typeof(TriggerSystem))]
[AutoGenerateComponentPause]
public sealed partial class RepeatingTriggerComponent : Component
{
/// <summary>
/// How long to wait between triggers.
/// The first trigger starts this long after the component is added.
/// </summary>
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(1);
/// <summary>
/// When the next trigger will be.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextTrigger;
}

View File

@@ -160,7 +160,7 @@ public sealed partial class ExplosionSystem : EntitySystem
if (explosive.Exploded)
return;
explosive.Exploded = true;
explosive.Exploded = !explosive.Repeatable;
// Override the explosion intensity if optional arguments were provided.
if (radius != null)

View File

@@ -94,6 +94,7 @@ namespace Content.Server.Explosion.EntitySystems
SubscribeLocalEvent<TriggerOnStepTriggerComponent, StepTriggeredOffEvent>(OnStepTriggered);
SubscribeLocalEvent<TriggerOnSlipComponent, SlipEvent>(OnSlipTriggered);
SubscribeLocalEvent<TriggerWhenEmptyComponent, OnEmptyGunShotEvent>(OnEmptyTriggered);
SubscribeLocalEvent<RepeatingTriggerComponent, MapInitEvent>(OnRepeatInit);
SubscribeLocalEvent<SpawnOnTriggerComponent, TriggerEvent>(OnSpawnTrigger);
SubscribeLocalEvent<DeleteOnTriggerComponent, TriggerEvent>(HandleDeleteTrigger);
@@ -241,6 +242,11 @@ namespace Content.Server.Explosion.EntitySystems
Trigger(uid, args.EmptyGun);
}
private void OnRepeatInit(Entity<RepeatingTriggerComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextTrigger = _timing.CurTime + ent.Comp.Delay;
}
public bool Trigger(EntityUid trigger, EntityUid? user = null)
{
var triggerEvent = new TriggerEvent(trigger, user);
@@ -323,6 +329,7 @@ namespace Content.Server.Explosion.EntitySystems
UpdateProximity();
UpdateTimer(frameTime);
UpdateTimedCollide(frameTime);
UpdateRepeat();
}
private void UpdateTimer(float frameTime)
@@ -357,5 +364,19 @@ namespace Content.Server.Explosion.EntitySystems
_appearance.SetData(uid, TriggerVisuals.VisualState, TriggerVisualState.Unprimed, appearance);
}
}
private void UpdateRepeat()
{
var now = _timing.CurTime;
var query = EntityQueryEnumerator<RepeatingTriggerComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.NextTrigger > now)
continue;
comp.NextTrigger = now + comp.Delay;
Trigger(uid);
}
}
}
}

View File

@@ -144,14 +144,33 @@ namespace Content.Server.GameTicking
async void SpawnWaitDb()
{
await _userDb.WaitLoadComplete(session);
try
{
await _userDb.WaitLoadComplete(session);
}
catch (OperationCanceledException)
{
// Bail, user must've disconnected or something.
Log.Debug($"Database load cancelled while waiting to spawn {session}");
return;
}
SpawnPlayer(session, EntityUid.Invalid);
}
async void SpawnObserverWaitDb()
{
await _userDb.WaitLoadComplete(session);
try
{
await _userDb.WaitLoadComplete(session);
}
catch (OperationCanceledException)
{
// Bail, user must've disconnected or something.
Log.Debug($"Database load cancelled while waiting to spawn {session}");
return;
}
JoinAsObserver(session);
}

View File

@@ -55,7 +55,9 @@ namespace Content.Server.GameTicking
return spawnableStations;
}
private void SpawnPlayers(List<ICommonSession> readyPlayers, Dictionary<NetUserId, HumanoidCharacterProfile> profiles, bool force)
private void SpawnPlayers(List<ICommonSession> readyPlayers,
Dictionary<NetUserId, HumanoidCharacterProfile> profiles,
bool force)
{
// Allow game rules to spawn players by themselves if needed. (For example, nuke ops or wizard)
RaiseLocalEvent(new RulePlayerSpawningEvent(readyPlayers, profiles, force));
@@ -94,7 +96,8 @@ namespace Content.Server.GameTicking
if (job == null)
{
var playerSession = _playerManager.GetSessionById(netUser);
_chatManager.DispatchServerMessage(playerSession, Loc.GetString("job-not-available-wait-in-lobby"));
_chatManager.DispatchServerMessage(playerSession,
Loc.GetString("job-not-available-wait-in-lobby"));
}
else
{
@@ -116,10 +119,17 @@ namespace Content.Server.GameTicking
RefreshLateJoinAllowed();
// Allow rules to add roles to players who have been spawned in. (For example, on-station traitors)
RaiseLocalEvent(new RulePlayerJobsAssignedEvent(assignedJobs.Keys.Select(x => _playerManager.GetSessionById(x)).ToArray(), profiles, force));
RaiseLocalEvent(new RulePlayerJobsAssignedEvent(
assignedJobs.Keys.Select(x => _playerManager.GetSessionById(x)).ToArray(),
profiles,
force));
}
private void SpawnPlayer(ICommonSession player, EntityUid station, string? jobId = null, bool lateJoin = true, bool silent = false)
private void SpawnPlayer(ICommonSession player,
EntityUid station,
string? jobId = null,
bool lateJoin = true,
bool silent = false)
{
var character = GetPlayerProfile(player);
@@ -132,7 +142,12 @@ namespace Content.Server.GameTicking
SpawnPlayer(player, character, station, jobId, lateJoin, silent);
}
private void SpawnPlayer(ICommonSession player, HumanoidCharacterProfile character, EntityUid station, string? jobId = null, bool lateJoin = true, bool silent = false)
private void SpawnPlayer(ICommonSession player,
HumanoidCharacterProfile character,
EntityUid station,
string? jobId = null,
bool lateJoin = true,
bool silent = false)
{
// Can't spawn players with a dummy ticker!
if (DummyTicker)
@@ -176,7 +191,9 @@ namespace Content.Server.GameTicking
restrictedRoles.UnionWith(jobBans);
// Pick best job best on prefs.
jobId ??= _stationJobs.PickBestAvailableJobWithPriority(station, character.JobPriorities, true,
jobId ??= _stationJobs.PickBestAvailableJobWithPriority(station,
character.JobPriorities,
true,
restrictedRoles);
// If no job available, stay in lobby, or if no lobby spawn as observer
if (jobId is null)
@@ -185,7 +202,9 @@ namespace Content.Server.GameTicking
{
JoinAsObserver(player);
}
_chatManager.DispatchServerMessage(player, Loc.GetString("game-ticker-player-no-jobs-available-when-joining"));
_chatManager.DispatchServerMessage(player,
Loc.GetString("game-ticker-player-no-jobs-available-when-joining"));
return;
}
@@ -199,7 +218,7 @@ namespace Content.Server.GameTicking
_mind.SetUserId(newMind, data.UserId);
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
var job = new JobComponent { Prototype = jobId };
var job = new JobComponent {Prototype = jobId};
_roles.MindAddRole(newMind, job, silent: silent);
var jobName = _jobs.MindTryGetJobName(newMind);
@@ -214,12 +233,11 @@ namespace Content.Server.GameTicking
if (lateJoin && !silent)
{
_chatSystem.DispatchStationAnnouncement(station,
Loc.GetString(
"latejoin-arrival-announcement",
("character", MetaData(mob).EntityName),
("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName)),
("gender", character.Gender) // CrystallPunk-LastnameGender
), Loc.GetString("latejoin-arrival-sender"),
Loc.GetString("latejoin-arrival-announcement",
("character", MetaData(mob).EntityName),
("gender", character.Gender), // CrystallPunk-LastnameGender
("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))),
Loc.GetString("latejoin-arrival-sender"),
playDefaultSound: false);
}
@@ -231,14 +249,17 @@ namespace Content.Server.GameTicking
_stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
if (lateJoin)
_adminLogger.Add(LogType.LateJoin, LogImpact.Medium, $"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
_adminLogger.Add(LogType.LateJoin,
LogImpact.Medium,
$"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
else
_adminLogger.Add(LogType.RoundStartJoin, LogImpact.Medium, $"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
_adminLogger.Add(LogType.RoundStartJoin,
LogImpact.Medium,
$"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
// Make sure they're aware of extended access.
if (Comp<StationJobsComponent>(station).ExtendedAccess
&& (jobPrototype.ExtendedAccess.Count > 0
|| jobPrototype.ExtendedAccessGroups.Count > 0))
&& (jobPrototype.ExtendedAccess.Count > 0 || jobPrototype.ExtendedAccessGroups.Count > 0))
{
_chatManager.DispatchServerMessage(player, Loc.GetString("job-greet-crew-shortages"));
}
@@ -260,14 +281,20 @@ namespace Content.Server.GameTicking
}
else
{
_chatManager.DispatchServerMessage(player, Loc.GetString("latejoin-arrivals-direction-time",
("time", $"{arrival:mm\\:ss}")));
_chatManager.DispatchServerMessage(player,
Loc.GetString("latejoin-arrivals-direction-time", ("time", $"{arrival:mm\\:ss}")));
}
}
// We raise this event directed to the mob, but also broadcast it so game rules can do something now.
PlayersJoinedRoundNormally++;
var aev = new PlayerSpawnCompleteEvent(mob, player, jobId, lateJoin, PlayersJoinedRoundNormally, station, character);
var aev = new PlayerSpawnCompleteEvent(mob,
player,
jobId,
lateJoin,
PlayersJoinedRoundNormally,
station,
character);
RaiseLocalEvent(mob, aev, true);
}
@@ -289,7 +316,10 @@ namespace Content.Server.GameTicking
/// <param name="station">The station they're spawning on</param>
/// <param name="jobId">An optional job for them to spawn as</param>
/// <param name="silent">Whether or not the player should be greeted upon joining</param>
public void MakeJoinGame(ICommonSession player, EntityUid station, string? jobId = null, bool silent = false)
public void MakeJoinGame(ICommonSession player,
EntityUid station,
string? jobId = null,
bool silent = false)
{
if (!_playerGameStatuses.ContainsKey(player.UserId))
return;
@@ -335,23 +365,29 @@ namespace Content.Server.GameTicking
_metaData.SetEntityName(ghost, name);
_ghost.SetCanReturnToBody(ghost, false);
_mind.TransferTo(mind.Value, ghost);
_adminLogger.Add(LogType.LateJoin, LogImpact.Low, $"{player.Name} late joined the round as an Observer with {ToPrettyString(ghost):entity}.");
_adminLogger.Add(LogType.LateJoin,
LogImpact.Low,
$"{player.Name} late joined the round as an Observer with {ToPrettyString(ghost):entity}.");
}
#region Mob Spawning Helpers
private EntityUid SpawnObserverMob()
{
var coordinates = GetObserverSpawnPoint();
return EntityManager.SpawnEntity(ObserverPrototypeName, coordinates);
}
#endregion
#region Spawn Points
public EntityCoordinates GetObserverSpawnPoint()
{
_possiblePositions.Clear();
foreach (var (point, transform) in EntityManager.EntityQuery<SpawnPointComponent, TransformComponent>(true))
foreach (var (point, transform) in EntityManager
.EntityQuery<SpawnPointComponent, TransformComponent>(true))
{
if (point.SpawnType != SpawnPointType.Observer)
continue;
@@ -367,8 +403,7 @@ namespace Content.Server.GameTicking
var query = AllEntityQuery<MapGridComponent>();
while (query.MoveNext(out var uid, out var grid))
{
if (!metaQuery.TryGetComponent(uid, out var meta) ||
meta.EntityPaused)
if (!metaQuery.TryGetComponent(uid, out var meta) || meta.EntityPaused)
{
continue;
}
@@ -389,8 +424,7 @@ namespace Content.Server.GameTicking
{
var gridXform = Transform(gridUid);
return new EntityCoordinates(gridUid,
gridXform.InvWorldMatrix.Transform(toMap.Position));
return new EntityCoordinates(gridUid, gridXform.InvWorldMatrix.Transform(toMap.Position));
}
return spawn;
@@ -406,8 +440,7 @@ namespace Content.Server.GameTicking
{
var mapUid = _mapManager.GetMapEntityId(map);
if (!metaQuery.TryGetComponent(mapUid, out var meta) ||
meta.EntityPaused)
if (!metaQuery.TryGetComponent(mapUid, out var meta) || meta.EntityPaused)
{
continue;
}
@@ -420,6 +453,7 @@ namespace Content.Server.GameTicking
_sawmill.Warning("Found no observer spawn points!");
return EntityCoordinates.Invalid;
}
#endregion
}
@@ -437,7 +471,11 @@ namespace Content.Server.GameTicking
public bool LateJoin { get; }
public EntityUid Station { get; }
public PlayerBeforeSpawnEvent(ICommonSession player, HumanoidCharacterProfile profile, string? jobId, bool lateJoin, EntityUid station)
public PlayerBeforeSpawnEvent(ICommonSession player,
HumanoidCharacterProfile profile,
string? jobId,
bool lateJoin,
EntityUid station)
{
Player = player;
Profile = profile;
@@ -465,7 +503,13 @@ namespace Content.Server.GameTicking
// Ex. If this is the 27th person to join, this will be 27.
public int JoinOrder { get; }
public PlayerSpawnCompleteEvent(EntityUid mob, ICommonSession player, string? jobId, bool lateJoin, int joinOrder, EntityUid station, HumanoidCharacterProfile profile)
public PlayerSpawnCompleteEvent(EntityUid mob,
ICommonSession player,
string? jobId,
bool lateJoin,
int joinOrder,
EntityUid station,
HumanoidCharacterProfile profile)
{
Mob = mob;
Player = player;

View File

@@ -1,7 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Content.Shared.Random.Helpers;
using Robust.Server.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@@ -82,17 +85,23 @@ public abstract partial class GameRuleSystem<T> where T: IComponent
targetCoords = EntityCoordinates.Invalid;
targetGrid = EntityUid.Invalid;
var possibleTargets = station.Comp.Grids;
if (possibleTargets.Count == 0)
// Weight grid choice by tilecount
var weights = new Dictionary<Entity<MapGridComponent>, float>();
foreach (var possibleTarget in station.Comp.Grids)
{
if (!TryComp<MapGridComponent>(possibleTarget, out var comp))
continue;
weights.Add((possibleTarget, comp), _map.GetAllTiles(possibleTarget, comp).Count());
}
if (weights.Count == 0)
{
targetGrid = EntityUid.Invalid;
return false;
}
targetGrid = RobustRandom.Pick(possibleTargets);
if (!TryComp<MapGridComponent>(targetGrid, out var gridComp))
return false;
(targetGrid, var gridComp) = RobustRandom.Pick(weights);
var found = false;
var aabb = gridComp.LocalAABB;

View File

@@ -1,4 +1,5 @@
using Content.Server.Mind.Commands;
using Content.Server.Ghost.Roles.Raffles;
using Content.Server.Mind.Commands;
using Content.Shared.Roles;
namespace Content.Server.Ghost.Roles.Components
@@ -87,5 +88,12 @@ namespace Content.Server.Ghost.Roles.Components
[ViewVariables(VVAccess.ReadWrite)]
[DataField("reregister")]
public bool ReregisterOnGhost { get; set; } = true;
/// <summary>
/// If set, ghost role is raffled, otherwise it is first-come-first-serve.
/// </summary>
[DataField("raffle")]
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
public GhostRoleRaffleConfig? RaffleConfig { get; set; }
}
}

View File

@@ -0,0 +1,58 @@
using Content.Server.Ghost.Roles.Raffles;
using Robust.Shared.Player;
namespace Content.Server.Ghost.Roles.Components;
/// <summary>
/// Indicates that a ghost role is currently being raffled, and stores data about the raffle in progress.
/// Raffles start when the first player joins a raffle.
/// </summary>
[RegisterComponent]
[Access(typeof(GhostRoleSystem))]
public sealed partial class GhostRoleRaffleComponent : Component
{
/// <summary>
/// Identifier of the <see cref="GhostRoleComponent">Ghost Role</see> this raffle is for.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[DataField]
public uint Identifier { get; set; }
/// <summary>
/// List of sessions that are currently in the raffle.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public HashSet<ICommonSession> CurrentMembers = [];
/// <summary>
/// List of sessions that are currently or were previously in the raffle.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public HashSet<ICommonSession> AllMembers = [];
/// <summary>
/// Time left in the raffle in seconds. This must be initialized to a positive value.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[DataField]
public TimeSpan Countdown = TimeSpan.MaxValue;
/// <summary>
/// The cumulative time, i.e. how much time the raffle will take in total. Added to when the time is extended
/// by someone joining the raffle.
/// Must be set to the same value as <see cref="Countdown"/> on initialization.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[DataField("cumulativeTime")]
public TimeSpan CumulativeTime = TimeSpan.MaxValue;
/// <inheritdoc cref="GhostRoleRaffleSettings.JoinExtendsDurationBy"/>
[ViewVariables(VVAccess.ReadOnly)]
[DataField("joinExtendsDurationBy")]
public TimeSpan JoinExtendsDurationBy { get; set; }
/// <inheritdoc cref="GhostRoleRaffleSettings.MaxDuration"/>
[ViewVariables(VVAccess.ReadOnly)]
[DataField("maxDuration")]
public TimeSpan MaxDuration { get; set; }
}

View File

@@ -1,7 +1,10 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.EUI;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Ghost.Roles.Events;
using Content.Server.Ghost.Roles.Raffles;
using Content.Shared.Ghost.Roles.Raffles;
using Content.Server.Ghost.Roles.UI;
using Content.Server.Mind.Commands;
using Content.Shared.Administration;
@@ -21,7 +24,9 @@ using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Server.Popups;
using Content.Shared.Verbs;
@@ -41,12 +46,16 @@ namespace Content.Server.Ghost.Roles
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
private uint _nextRoleIdentifier;
private bool _needsUpdateGhostRoleCount = true;
private readonly Dictionary<uint, Entity<GhostRoleComponent>> _ghostRoles = new();
private readonly Dictionary<uint, Entity<GhostRoleRaffleComponent>> _ghostRoleRaffles = new();
private readonly Dictionary<ICommonSession, GhostRolesEui> _openUis = new();
private readonly Dictionary<ICommonSession, MakeGhostRoleEui> _openMakeGhostRoleUis = new();
@@ -63,10 +72,12 @@ namespace Content.Server.Ghost.Roles
SubscribeLocalEvent<GhostTakeoverAvailableComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<GhostTakeoverAvailableComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<GhostRoleComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<GhostRoleComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<GhostRoleComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<GhostRoleComponent, ComponentStartup>(OnRoleStartup);
SubscribeLocalEvent<GhostRoleComponent, ComponentShutdown>(OnRoleShutdown);
SubscribeLocalEvent<GhostRoleComponent, EntityPausedEvent>(OnPaused);
SubscribeLocalEvent<GhostRoleComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentInit>(OnRaffleInit);
SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentShutdown>(OnRaffleShutdown);
SubscribeLocalEvent<GhostRoleMobSpawnerComponent, TakeGhostRoleEvent>(OnSpawnerTakeRole);
SubscribeLocalEvent<GhostTakeoverAvailableComponent, TakeGhostRoleEvent>(OnTakeoverTakeRole);
SubscribeLocalEvent<GhostRoleMobSpawnerComponent, GetVerbsEvent<Verb>>(OnVerb);
@@ -165,17 +176,118 @@ namespace Content.Server.Ghost.Roles
public override void Update(float frameTime)
{
base.Update(frameTime);
if (_needsUpdateGhostRoleCount)
UpdateGhostRoleCount();
UpdateRaffles(frameTime);
}
/// <summary>
/// Handles sending count update for the ghost role button in ghost UI, if ghost role count changed.
/// </summary>
private void UpdateGhostRoleCount()
{
if (!_needsUpdateGhostRoleCount)
return;
_needsUpdateGhostRoleCount = false;
var response = new GhostUpdateGhostRoleCountEvent(GetGhostRoleCount());
foreach (var player in _playerManager.Sessions)
{
_needsUpdateGhostRoleCount = false;
var response = new GhostUpdateGhostRoleCountEvent(GetGhostRolesInfo().Length);
foreach (var player in _playerManager.Sessions)
{
RaiseNetworkEvent(response, player.Channel);
}
RaiseNetworkEvent(response, player.Channel);
}
}
/// <summary>
/// Handles ghost role raffle logic.
/// </summary>
private void UpdateRaffles(float frameTime)
{
var query = EntityQueryEnumerator<GhostRoleRaffleComponent, MetaDataComponent>();
while (query.MoveNext(out var entityUid, out var raffle, out var meta))
{
if (meta.EntityPaused)
continue;
// if all participants leave/were removed from the raffle, the raffle is canceled.
if (raffle.CurrentMembers.Count == 0)
{
RemoveRaffleAndUpdateEui(entityUid, raffle);
continue;
}
raffle.Countdown = raffle.Countdown.Subtract(TimeSpan.FromSeconds(frameTime));
if (raffle.Countdown.Ticks > 0)
continue;
// the raffle is over! find someone to take over the ghost role
if (!TryComp(entityUid, out GhostRoleComponent? ghostRole))
{
Log.Warning($"Ghost role raffle finished on {entityUid} but {nameof(GhostRoleComponent)} is missing");
RemoveRaffleAndUpdateEui(entityUid, raffle);
continue;
}
if (ghostRole.RaffleConfig is null)
{
Log.Warning($"Ghost role raffle finished on {entityUid} but RaffleConfig became null");
RemoveRaffleAndUpdateEui(entityUid, raffle);
continue;
}
var foundWinner = false;
var deciderPrototype = _prototype.Index(ghostRole.RaffleConfig.Decider);
// use the ghost role's chosen winner picker to find a winner
deciderPrototype.Decider.PickWinner(
raffle.CurrentMembers.AsEnumerable(),
session =>
{
var success = TryTakeover(session, raffle.Identifier);
foundWinner |= success;
return success;
}
);
if (!foundWinner)
{
Log.Warning($"Ghost role raffle for {entityUid} ({ghostRole.RoleName}) finished without " +
$"{ghostRole.RaffleConfig?.Decider} finding a winner");
}
// raffle over
RemoveRaffleAndUpdateEui(entityUid, raffle);
}
}
private bool TryTakeover(ICommonSession player, uint identifier)
{
// TODO: the following two checks are kind of redundant since they should already be removed
// from the raffle
// can't win if you are disconnected (although you shouldn't be a candidate anyway)
if (player.Status != SessionStatus.InGame)
return false;
// can't win if you are no longer a ghost (e.g. if you returned to your body)
if (player.AttachedEntity == null || !HasComp<GhostComponent>(player.AttachedEntity))
return false;
if (Takeover(player, identifier))
{
// takeover successful, we have a winner! remove the winner from other raffles they might be in
LeaveAllRaffles(player);
return true;
}
return false;
}
private void RemoveRaffleAndUpdateEui(EntityUid entityUid, GhostRoleRaffleComponent raffle)
{
_ghostRoleRaffles.Remove(raffle.Identifier);
RemComp(entityUid, raffle);
UpdateAllEui();
}
private void PlayerStatusChanged(object? blah, SessionStatusEventArgs args)
{
if (args.NewStatus == SessionStatus.InGame)
@@ -183,6 +295,11 @@ namespace Content.Server.Ghost.Roles
var response = new GhostUpdateGhostRoleCountEvent(_ghostRoles.Count);
RaiseNetworkEvent(response, args.Session.Channel);
}
else
{
// people who disconnect are removed from ghost role raffles
LeaveAllRaffles(args.Session);
}
}
public void RegisterGhostRole(Entity<GhostRoleComponent> role)
@@ -201,24 +318,170 @@ namespace Content.Server.Ghost.Roles
return;
_ghostRoles.Remove(comp.Identifier);
if (TryComp(role.Owner, out GhostRoleRaffleComponent? raffle))
{
// if a raffle is still running, get rid of it
RemoveRaffleAndUpdateEui(role.Owner, raffle);
}
else
{
UpdateAllEui();
}
}
// probably fine to be init because it's never added during entity initialization, but much later
private void OnRaffleInit(Entity<GhostRoleRaffleComponent> ent, ref ComponentInit args)
{
if (!TryComp(ent, out GhostRoleComponent? ghostRole))
{
// can't have a raffle for a ghost role that doesn't exist
RemComp<GhostRoleRaffleComponent>(ent);
return;
}
var config = ghostRole.RaffleConfig;
if (config is null)
return; // should, realistically, never be reached but you never know
var settings = config.SettingsOverride
?? _prototype.Index<GhostRoleRaffleSettingsPrototype>(config.Settings).Settings;
if (settings.MaxDuration < settings.InitialDuration)
{
Log.Error($"Ghost role on {ent} has invalid raffle settings (max duration shorter than initial)");
ghostRole.RaffleConfig = null; // make it a non-raffle role so stuff isn't entirely broken
RemComp<GhostRoleRaffleComponent>(ent);
return;
}
var raffle = ent.Comp;
raffle.Identifier = ghostRole.Identifier;
raffle.Countdown = TimeSpan.FromSeconds(settings.InitialDuration);
raffle.CumulativeTime = TimeSpan.FromSeconds(settings.InitialDuration);
// we copy these settings into the component because they would be cumbersome to access otherwise
raffle.JoinExtendsDurationBy = TimeSpan.FromSeconds(settings.JoinExtendsDurationBy);
raffle.MaxDuration = TimeSpan.FromSeconds(settings.MaxDuration);
}
private void OnRaffleShutdown(Entity<GhostRoleRaffleComponent> ent, ref ComponentShutdown args)
{
_ghostRoleRaffles.Remove(ent.Comp.Identifier);
}
/// <summary>
/// Joins the given player onto a ghost role raffle, or creates it if it doesn't exist.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="identifier">The ID that represents the ghost role or ghost role raffle.
/// (A raffle will have the same ID as the ghost role it's for.)</param>
private void JoinRaffle(ICommonSession player, uint identifier)
{
if (!_ghostRoles.TryGetValue(identifier, out var roleEnt))
return;
// get raffle or create a new one if it doesn't exist
var raffle = _ghostRoleRaffles.TryGetValue(identifier, out var raffleEnt)
? raffleEnt.Comp
: EnsureComp<GhostRoleRaffleComponent>(roleEnt.Owner);
_ghostRoleRaffles.TryAdd(identifier, (roleEnt.Owner, raffle));
if (!raffle.CurrentMembers.Add(player))
{
Log.Warning($"{player.Name} tried to join raffle for ghost role {identifier} but they are already in the raffle");
return;
}
// if this is the first time the player joins this raffle, and the player wasn't the starter of the raffle:
// extend the countdown, but only if doing so will not make the raffle take longer than the maximum
// duration
if (raffle.AllMembers.Add(player) && raffle.AllMembers.Count > 1
&& raffle.CumulativeTime.Add(raffle.JoinExtendsDurationBy) <= raffle.MaxDuration)
{
raffle.Countdown += raffle.JoinExtendsDurationBy;
raffle.CumulativeTime += raffle.JoinExtendsDurationBy;
}
UpdateAllEui();
}
public void Takeover(ICommonSession player, uint identifier)
/// <summary>
/// Makes the given player leave the raffle corresponding to the given ID.
/// </summary>
public void LeaveRaffle(ICommonSession player, uint identifier)
{
if (!_ghostRoleRaffles.TryGetValue(identifier, out var raffleEnt))
return;
if (raffleEnt.Comp.CurrentMembers.Remove(player))
{
UpdateAllEui();
}
else
{
Log.Warning($"{player.Name} tried to leave raffle for ghost role {identifier} but they are not in the raffle");
}
// (raffle ending because all players left is handled in update())
}
/// <summary>
/// Makes the given player leave all ghost role raffles.
/// </summary>
public void LeaveAllRaffles(ICommonSession player)
{
var shouldUpdateEui = false;
foreach (var raffleEnt in _ghostRoleRaffles.Values)
{
shouldUpdateEui |= raffleEnt.Comp.CurrentMembers.Remove(player);
}
if (shouldUpdateEui)
UpdateAllEui();
}
/// <summary>
/// Request a ghost role. If it's a raffled role starts or joins a raffle, otherwise the player immediately
/// takes over the ghost role if possible.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="identifier">ID of the ghost role.</param>
public void Request(ICommonSession player, uint identifier)
{
if (!_ghostRoles.TryGetValue(identifier, out var roleEnt))
return;
if (roleEnt.Comp.RaffleConfig is not null)
{
JoinRaffle(player, identifier);
}
else
{
Takeover(player, identifier);
}
}
/// <summary>
/// Attempts having the player take over the ghost role with the corresponding ID. Does not start a raffle.
/// </summary>
/// <returns>True if takeover was successful, otherwise false.</returns>
public bool Takeover(ICommonSession player, uint identifier)
{
if (!_ghostRoles.TryGetValue(identifier, out var role))
return;
return false;
var ev = new TakeGhostRoleEvent(player);
RaiseLocalEvent(role, ref ev);
if (!ev.TookRole)
return;
return false;
if (player.AttachedEntity != null)
_adminLogger.Add(LogType.GhostRoleTaken, LogImpact.Low, $"{player:player} took the {role.Comp.RoleName:roleName} ghost role {ToPrettyString(player.AttachedEntity.Value):entity}");
CloseEui(player);
return true;
}
public void Follow(ICommonSession player, uint identifier)
@@ -247,7 +510,22 @@ namespace Content.Server.Ghost.Roles
_mindSystem.TransferTo(newMind, mob);
}
public GhostRoleInfo[] GetGhostRolesInfo()
/// <summary>
/// Returns the number of available ghost roles.
/// </summary>
public int GetGhostRoleCount()
{
var metaQuery = GetEntityQuery<MetaDataComponent>();
return _ghostRoles.Count(pair => metaQuery.GetComponent(pair.Value.Owner).EntityPaused == false);
}
/// <summary>
/// Returns information about all available ghost roles.
/// </summary>
/// <param name="player">
/// If not null, the <see cref="GhostRoleInfo"/>s will show if the given player is in a raffle.
/// </param>
public GhostRoleInfo[] GetGhostRolesInfo(ICommonSession? player)
{
var roles = new List<GhostRoleInfo>();
var metaQuery = GetEntityQuery<MetaDataComponent>();
@@ -257,7 +535,40 @@ namespace Content.Server.Ghost.Roles
if (metaQuery.GetComponent(uid).EntityPaused)
continue;
roles.Add(new GhostRoleInfo { Identifier = id, Name = role.RoleName, Description = role.RoleDescription, Rules = role.RoleRules, Requirements = role.Requirements });
var kind = GhostRoleKind.FirstComeFirstServe;
GhostRoleRaffleComponent? raffle = null;
if (role.RaffleConfig is not null)
{
kind = GhostRoleKind.RaffleReady;
if (_ghostRoleRaffles.TryGetValue(id, out var raffleEnt))
{
kind = GhostRoleKind.RaffleInProgress;
raffle = raffleEnt.Comp;
if (player is not null && raffle.CurrentMembers.Contains(player))
kind = GhostRoleKind.RaffleJoined;
}
}
var rafflePlayerCount = (uint?) raffle?.CurrentMembers.Count ?? 0;
var raffleEndTime = raffle is not null
? _timing.CurTime.Add(raffle.Countdown)
: TimeSpan.MinValue;
roles.Add(new GhostRoleInfo
{
Identifier = id,
Name = role.RoleName,
Description = role.RoleDescription,
Rules = role.RoleRules,
Requirements = role.Requirements,
Kind = kind,
RafflePlayerCount = rafflePlayerCount,
RaffleEndTime = raffleEndTime
});
}
return roles.ToArray();
@@ -272,6 +583,10 @@ namespace Content.Server.Ghost.Roles
if (HasComp<GhostComponent>(message.Entity))
return;
// The player is not a ghost (anymore), so they should not be in any raffles. Remove them.
// This ensures player doesn't win a raffle after returning to their (revived) body and ends up being
// forced into a ghost role.
LeaveAllRaffles(message.Player);
CloseEui(message.Player);
}
@@ -306,6 +621,7 @@ namespace Content.Server.Ghost.Roles
_openUis.Clear();
_ghostRoles.Clear();
_ghostRoleRaffles.Clear();
_nextRoleIdentifier = 0;
}
@@ -331,12 +647,12 @@ namespace Content.Server.Ghost.Roles
RemCompDeferred<GhostRoleComponent>(ent);
}
private void OnStartup(Entity<GhostRoleComponent> ent, ref ComponentStartup args)
private void OnRoleStartup(Entity<GhostRoleComponent> ent, ref ComponentStartup args)
{
RegisterGhostRole(ent);
}
private void OnShutdown(Entity<GhostRoleComponent> role, ref ComponentShutdown args)
private void OnRoleShutdown(Entity<GhostRoleComponent> role, ref ComponentShutdown args)
{
UnregisterGhostRole(role);
}

View File

@@ -0,0 +1,127 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Ghost.Roles.Raffles;
using Content.Shared.Administration;
using Content.Shared.Ghost.Roles.Raffles;
using Content.Shared.Mind.Components;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
namespace Content.Server.Ghost.Roles
{
[AdminCommand(AdminFlags.Admin)]
public sealed class MakeRaffledGhostRoleCommand : IConsoleCommand
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
public string Command => "makeghostroleraffled";
public string Description => "Turns an entity into a raffled ghost role.";
public string Help => $"Usage: {Command} <entity uid> <name> <description> (<settings prototype> | <initial duration> <extend by> <max duration>) [<rules>]\n" +
$"Durations are in seconds.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length is < 4 or > 7)
{
shell.WriteLine($"Invalid amount of arguments.\n{Help}");
return;
}
if (!NetEntity.TryParse(args[0], out var uidNet) || !_entManager.TryGetEntity(uidNet, out var uid))
{
shell.WriteLine($"{args[0]} is not a valid entity uid.");
return;
}
if (!_entManager.TryGetComponent(uid, out MetaDataComponent? metaData))
{
shell.WriteLine($"No entity found with uid {uid}");
return;
}
if (_entManager.TryGetComponent(uid, out MindContainerComponent? mind) &&
mind.HasMind)
{
shell.WriteLine($"Entity {metaData.EntityName} with id {uid} already has a mind.");
return;
}
if (_entManager.TryGetComponent(uid, out GhostRoleComponent? ghostRole))
{
shell.WriteLine($"Entity {metaData.EntityName} with id {uid} already has a {nameof(GhostRoleComponent)}");
return;
}
if (_entManager.HasComponent<GhostTakeoverAvailableComponent>(uid))
{
shell.WriteLine($"Entity {metaData.EntityName} with id {uid} already has a {nameof(GhostTakeoverAvailableComponent)}");
return;
}
var name = args[1];
var description = args[2];
// if the rules are specified then use those, otherwise use the default
var rules = args.Length switch
{
5 => args[4],
7 => args[6],
_ => Loc.GetString("ghost-role-component-default-rules"),
};
// is it an invocation with a prototype ID and optional rules?
var isProto = args.Length is 4 or 5;
GhostRoleRaffleSettings settings;
if (isProto)
{
if (!_protoManager.TryIndex<GhostRoleRaffleSettingsPrototype>(args[4], out var proto))
{
var validProtos = string.Join(", ",
_protoManager.EnumeratePrototypes<GhostRoleRaffleSettingsPrototype>().Select(p => p.ID)
);
shell.WriteLine($"{args[4]} is not a valid raffle settings prototype. Valid options: {validProtos}");
return;
}
settings = proto.Settings;
}
else
{
if (!uint.TryParse(args[3], out var initial)
|| !uint.TryParse(args[4], out var extends)
|| !uint.TryParse(args[5], out var max)
|| initial == 0 || max == 0)
{
shell.WriteLine($"The raffle initial/extends/max settings must be positive numbers.");
return;
}
if (initial > max)
{
shell.WriteLine("The initial duration must be smaller than or equal to the maximum duration.");
return;
}
settings = new GhostRoleRaffleSettings()
{
InitialDuration = initial,
JoinExtendsDurationBy = extends,
MaxDuration = max
};
}
ghostRole = _entManager.AddComponent<GhostRoleComponent>(uid.Value);
_entManager.AddComponent<GhostTakeoverAvailableComponent>(uid.Value);
ghostRole.RoleName = name;
ghostRole.RoleDescription = description;
ghostRole.RoleRules = rules;
ghostRole.RaffleConfig = new GhostRoleRaffleConfig(settings);
shell.WriteLine($"Made entity {metaData.EntityName} a raffled ghost role.");
}
}
}

View File

@@ -0,0 +1,35 @@
using Content.Shared.Ghost.Roles.Raffles;
using Robust.Shared.Prototypes;
namespace Content.Server.Ghost.Roles.Raffles;
/// <summary>
/// Raffle configuration.
/// </summary>
[DataDefinition]
public sealed partial class GhostRoleRaffleConfig
{
public GhostRoleRaffleConfig(GhostRoleRaffleSettings settings)
{
SettingsOverride = settings;
}
/// <summary>
/// Specifies the raffle settings to use.
/// </summary>
[DataField("settings", required: true)]
public ProtoId<GhostRoleRaffleSettingsPrototype> Settings { get; set; } = "default";
/// <summary>
/// If not null, the settings from <see cref="Settings"/> are ignored and these settings are used instead.
/// Intended for allowing admins to set custom raffle settings for admeme ghost roles.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public GhostRoleRaffleSettings? SettingsOverride { get; set; }
/// <summary>
/// Sets which <see cref="IGhostRoleRaffleDecider"/> is used.
/// </summary>
[DataField("decider")]
public ProtoId<GhostRoleRaffleDeciderPrototype> Decider { get; set; } = "default";
}

View File

@@ -0,0 +1,20 @@
using Robust.Shared.Prototypes;
namespace Content.Server.Ghost.Roles.Raffles;
/// <summary>
/// Allows getting a <see cref="IGhostRoleRaffleDecider"/> as prototype.
/// </summary>
[Prototype("ghostRoleRaffleDecider")]
public sealed class GhostRoleRaffleDeciderPrototype : IPrototype
{
/// <inheritdoc />
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// The <see cref="IGhostRoleRaffleDecider"/> instance that chooses the winner of a raffle.
/// </summary>
[DataField("decider", required: true)]
public IGhostRoleRaffleDecider Decider { get; private set; } = new RngGhostRoleRaffleDecider();
}

View File

@@ -0,0 +1,28 @@
using Robust.Shared.Player;
namespace Content.Server.Ghost.Roles.Raffles;
/// <summary>
/// Chooses a winner of a ghost role raffle.
/// </summary>
[ImplicitDataDefinitionForInheritors]
public partial interface IGhostRoleRaffleDecider
{
/// <summary>
/// Chooses a winner of a ghost role raffle draw from the given pool of candidates.
/// </summary>
/// <param name="candidates">The players in the session at the time of drawing.</param>
/// <param name="tryTakeover">
/// Call this with the chosen winner as argument.
/// <ul><li>If <c>true</c> is returned, your winner was able to take over the ghost role, and the drawing is complete.
/// <b>Do not call <see cref="tryTakeover"/> again after true is returned.</b></li>
/// <li>If <c>false</c> is returned, your winner was not able to take over the ghost role,
/// and you must choose another winner, and call <see cref="tryTakeover"/> with the new winner as argument.</li>
/// </ul>
///
/// If <see cref="tryTakeover"/> is not called, or only returns false, the raffle will end without a winner.
/// Do not call <see cref="tryTakeover"/> with the same player several times.
/// </param>
void PickWinner(IEnumerable<ICommonSession> candidates, Func<ICommonSession, bool> tryTakeover);
}

View File

@@ -0,0 +1,27 @@
using System.Linq;
using JetBrains.Annotations;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.Ghost.Roles.Raffles;
/// <summary>
/// Chooses the winner of a ghost role raffle entirely randomly, without any weighting.
/// </summary>
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public sealed partial class RngGhostRoleRaffleDecider : IGhostRoleRaffleDecider
{
public void PickWinner(IEnumerable<ICommonSession> candidates, Func<ICommonSession, bool> tryTakeover)
{
var random = IoCManager.Resolve<IRobustRandom>();
var choices = candidates.ToList();
random.Shuffle(choices); // shuffle the list so we can pick a lucky winner!
foreach (var candidate in choices)
{
if (tryTakeover(candidate))
return;
}
}
}

View File

@@ -6,9 +6,16 @@ namespace Content.Server.Ghost.Roles.UI
{
public sealed class GhostRolesEui : BaseEui
{
[Dependency] private readonly GhostRoleSystem _ghostRoleSystem;
public GhostRolesEui()
{
_ghostRoleSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GhostRoleSystem>();
}
public override GhostRolesEuiState GetNewState()
{
return new(EntitySystem.Get<GhostRoleSystem>().GetGhostRolesInfo());
return new(_ghostRoleSystem.GetGhostRolesInfo(Player));
}
public override void HandleMessage(EuiMessageBase msg)
@@ -17,11 +24,14 @@ namespace Content.Server.Ghost.Roles.UI
switch (msg)
{
case GhostRoleTakeoverRequestMessage req:
EntitySystem.Get<GhostRoleSystem>().Takeover(Player, req.Identifier);
case RequestGhostRoleMessage req:
_ghostRoleSystem.Request(Player, req.Identifier);
break;
case GhostRoleFollowRequestMessage req:
EntitySystem.Get<GhostRoleSystem>().Follow(Player, req.Identifier);
case FollowGhostRoleMessage req:
_ghostRoleSystem.Follow(Player, req.Identifier);
break;
case LeaveGhostRoleRaffleMessage req:
_ghostRoleSystem.LeaveRaffle(Player, req.Identifier);
break;
}
}

View File

@@ -14,6 +14,7 @@ using Content.Shared.Database;
using Content.Shared.Emag.Components;
using Content.Shared.Lathe;
using Content.Shared.Materials;
using Content.Shared.ReagentSpeed;
using Content.Shared.Research.Components;
using Content.Shared.Research.Prototypes;
using JetBrains.Annotations;
@@ -35,6 +36,7 @@ namespace Content.Server.Lathe
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly UserInterfaceSystem _uiSys = default!;
[Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
[Dependency] private readonly ReagentSpeedSystem _reagentSpeed = default!;
[Dependency] private readonly StackSystem _stack = default!;
[Dependency] private readonly TransformSystem _transform = default!;
@@ -186,9 +188,11 @@ namespace Content.Server.Lathe
var recipe = component.Queue.First();
component.Queue.RemoveAt(0);
var time = _reagentSpeed.ApplySpeed(uid, recipe.CompleteTime);
var lathe = EnsureComp<LatheProducingComponent>(uid);
lathe.StartTime = _timing.CurTime;
lathe.ProductionLength = recipe.CompleteTime * component.TimeMultiplier;
lathe.ProductionLength = time * component.TimeMultiplier;
component.CurrentRecipe = recipe;
var ev = new LatheStartPrintingEvent(recipe);

View File

@@ -35,7 +35,7 @@ public sealed partial class GunAmmoPrecondition : HTNPrecondition
else
percent = ammoEv.Count / (float) ammoEv.Capacity;
percent = Math.Clamp(percent, 0f, 1f);
percent = System.Math.Clamp(percent, 0f, 1f);
if (MaxPercent < percent)
return false;

View File

@@ -0,0 +1,12 @@
namespace Content.Server.NPC.HTN.Preconditions;
public sealed partial class KeyNotExistsPrecondition : HTNPrecondition
{
[DataField(required: true)]
public string Key = string.Empty;
public override bool IsMet(NPCBlackboard blackboard)
{
return !blackboard.ContainsKey(Key);
}
}

View File

@@ -0,0 +1,23 @@
namespace Content.Server.NPC.HTN.Preconditions.Math;
/// <summary>
/// Checks for the presence of data in the blackboard and makes a comparison with the specified boolean
/// </summary>
public sealed partial class KeyBoolEqualsPrecondition : HTNPrecondition
{
[Dependency] private readonly IEntityManager _entManager = default!;
[DataField(required: true)]
public string Key = string.Empty;
[DataField(required: true)]
public bool Value;
public override bool IsMet(NPCBlackboard blackboard)
{
if (!blackboard.TryGetValue<bool>(Key, out var value, _entManager))
return false;
return Value == value;
}
}

View File

@@ -0,0 +1,18 @@
namespace Content.Server.NPC.HTN.Preconditions.Math;
public sealed partial class KeyFloatEqualsPrecondition : HTNPrecondition
{
[Dependency] private readonly IEntityManager _entManager = default!;
[DataField(required: true)]
public string Key = string.Empty;
[DataField(required: true)]
public float Value;
public override bool IsMet(NPCBlackboard blackboard)
{
return blackboard.TryGetValue<float>(Key, out var value, _entManager) &&
MathHelper.CloseTo(value, value);
}
}

View File

@@ -0,0 +1,17 @@
namespace Content.Server.NPC.HTN.Preconditions.Math;
public sealed partial class KeyFloatGreaterPrecondition : HTNPrecondition
{
[Dependency] private readonly IEntityManager _entManager = default!;
[DataField(required: true)]
public string Key = string.Empty;
[DataField(required: true)]
public float Value;
public override bool IsMet(NPCBlackboard blackboard)
{
return blackboard.TryGetValue<float>(Key, out var value, _entManager) && value > Value;
}
}

View File

@@ -0,0 +1,17 @@
namespace Content.Server.NPC.HTN.Preconditions.Math;
public sealed partial class KeyFloatLessPrecondition : HTNPrecondition
{
[Dependency] private readonly IEntityManager _entManager = default!;
[DataField(required: true)]
public string Key = string.Empty;
[DataField(required: true)]
public float Value;
public override bool IsMet(NPCBlackboard blackboard)
{
return blackboard.TryGetValue<float>(Key, out var value, _entManager) && value < Value;
}
}

View File

@@ -0,0 +1,33 @@
using System.Threading;
using System.Threading.Tasks;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Math;
/// <summary>
/// Gets the key, and adds the value to that float
/// </summary>
public sealed partial class AddFloatOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
[DataField(required: true)]
public string TargetKey = string.Empty;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float Amount;
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
if (!blackboard.TryGetValue<float>(TargetKey, out var value, _entManager))
return (false, null);
return (
true,
new Dictionary<string, object>
{
{ TargetKey, value + Amount }
}
);
}
}

View File

@@ -0,0 +1,28 @@
using System.Threading;
using System.Threading.Tasks;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Math;
/// <summary>
/// Just sets a blackboard key to a bool
/// </summary>
public sealed partial class SetBoolOperator : HTNOperator
{
[DataField(required: true)]
public string TargetKey = string.Empty;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool Value;
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
return (
true,
new Dictionary<string, object>
{
{ TargetKey, Value }
}
);
}
}

View File

@@ -1,24 +1,28 @@
using System.Threading;
using System.Threading.Tasks;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Math;
/// <summary>
/// Just sets a blackboard key to a float
/// </summary>
public sealed partial class SetFloatOperator : HTNOperator
{
[DataField("targetKey", required: true)] public string TargetKey = string.Empty;
[DataField(required: true)]
public string TargetKey = string.Empty;
[ViewVariables(VVAccess.ReadWrite), DataField("amount")]
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float Amount;
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
return (true, new Dictionary<string, object>()
{
{TargetKey, Amount},
});
return (
true,
new Dictionary<string, object>
{
{ TargetKey, Amount }
}
);
}
}

View File

@@ -0,0 +1,34 @@
using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.Random;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Math;
/// <summary>
/// Sets a random float from MinAmount to MaxAmount to blackboard
/// </summary>
public sealed partial class SetRandomFloatOperator : HTNOperator
{
[Dependency] private readonly IRobustRandom _random = default!;
[DataField(required: true)]
public string TargetKey = string.Empty;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float MaxAmount = 1f;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float MinAmount = 0f;
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
return (
true,
new Dictionary<string, object>
{
{ TargetKey, _random.NextFloat(MinAmount, MaxAmount) }
}
);
}
}

View File

@@ -0,0 +1,28 @@
using Robust.Server.Audio;
using Robust.Shared.Audio;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
public sealed partial class PlaySoundOperator : HTNOperator
{
private AudioSystem _audio = default!;
[DataField(required: true)]
public SoundSpecifier? Sound;
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_audio = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>();
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
var uid = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
_audio.PlayPvs(Sound, uid);
return base.Update(blackboard, frameTime);
}
}

View File

@@ -0,0 +1,36 @@
using Content.Server.Chat.Systems;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
public sealed partial class SayKeyOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
private ChatSystem _chat = default!;
[DataField(required: true)]
public string Key = string.Empty;
/// <summary>
/// Whether to hide message from chat window and logs.
/// </summary>
[DataField]
public bool Hidden;
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_chat = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
if (!blackboard.TryGetValue<object>(Key, out var value, _entManager))
return HTNOperatorStatus.Failed;
var speaker = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
_chat.TrySendInGameICMessage(speaker, value.ToString() ?? "Oh no...", InGameICChatType.Speak, hideChat: Hidden, hideLog: Hidden);
return base.Update(blackboard, frameTime);
}
}

View File

@@ -973,7 +973,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
/// <summary>
/// Creates a simple planet setup for a map.
/// </summary>
public void EnsurePlanet(EntityUid mapUid, BiomeTemplatePrototype biomeTemplate, int? seed = null, MetaDataComponent? metadata = null)
public void EnsurePlanet(EntityUid mapUid, BiomeTemplatePrototype biomeTemplate, int? seed = null, MetaDataComponent? metadata = null, Color? mapLight = null)
{
if (!Resolve(mapUid, ref metadata))
return;
@@ -998,7 +998,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
// Lava: #A34931
var light = EnsureComp<MapLightComponent>(mapUid);
light.AmbientLightColor = Color.FromHex("#D8B059");
light.AmbientLightColor = mapLight ?? Color.FromHex("#D8B059");
Dirty(mapUid, light, metadata);
var moles = new float[Atmospherics.AdjustedNumberOfGases];

View File

@@ -309,7 +309,7 @@ public sealed class PlayTimeTrackingManager : ISharedPlaytimeManager
var data = new PlayTimeData();
_playTimeData.Add(session, data);
var playTimes = await _db.GetPlayTimes(session.UserId);
var playTimes = await _db.GetPlayTimes(session.UserId, cancel);
cancel.ThrowIfCancellationRequested();
foreach (var timer in playTimes)

View File

@@ -12,6 +12,7 @@ namespace Content.Server.Preferences.Managers
void Init();
Task LoadData(ICommonSession session, CancellationToken cancel);
void FinishLoad(ICommonSession session);
void OnClientDisconnected(ICommonSession session);
bool TryGetCachedPreferences(NetUserId userId, [NotNullWhen(true)] out PlayerPreferences? playerPreferences);

View File

@@ -13,6 +13,7 @@ using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Preferences.Managers
@@ -27,6 +28,7 @@ namespace Content.Server.Preferences.Managers
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IDependencyCollection _dependencies = default!;
[Dependency] private readonly IPrototypeManager _protos = default!;
// Cache player prefs on the server so we don't need as much async hell related to them.
@@ -101,9 +103,8 @@ namespace Content.Server.Preferences.Managers
var curPrefs = prefsData.Prefs!;
var session = _playerManager.GetSessionById(userId);
var collection = IoCManager.Instance!;
profile.EnsureValid(session, collection);
profile.EnsureValid(session, _dependencies);
var profiles = new Dictionary<int, ICharacterProfile>(curPrefs.Characters)
{
@@ -196,21 +197,32 @@ namespace Content.Server.Preferences.Managers
async Task LoadPrefs()
{
var prefs = await GetOrCreatePreferencesAsync(session.UserId);
var prefs = await GetOrCreatePreferencesAsync(session.UserId, cancel);
prefsData.Prefs = prefs;
prefsData.PrefsLoaded = true;
var msg = new MsgPreferencesAndSettings();
msg.Preferences = prefs;
msg.Settings = new GameSettings
{
MaxCharacterSlots = MaxCharacterSlots
};
_netManager.ServerSendMessage(msg, session.Channel);
}
}
}
public void FinishLoad(ICommonSession session)
{
// This is a separate step from the actual database load.
// Sanitizing preferences requires play time info due to loadouts.
// And play time info is loaded concurrently from the DB with preferences.
var prefsData = _cachedPlayerPrefs[session.UserId];
DebugTools.Assert(prefsData.Prefs != null);
prefsData.Prefs = SanitizePreferences(session, prefsData.Prefs, _dependencies);
prefsData.PrefsLoaded = true;
var msg = new MsgPreferencesAndSettings();
msg.Preferences = prefsData.Prefs;
msg.Settings = new GameSettings
{
MaxCharacterSlots = MaxCharacterSlots
};
_netManager.ServerSendMessage(msg, session.Channel);
}
public void OnClientDisconnected(ICommonSession session)
{
_cachedPlayerPrefs.Remove(session.UserId);
@@ -270,18 +282,15 @@ namespace Content.Server.Preferences.Managers
return null;
}
private async Task<PlayerPreferences> GetOrCreatePreferencesAsync(NetUserId userId)
private async Task<PlayerPreferences> GetOrCreatePreferencesAsync(NetUserId userId, CancellationToken cancel)
{
var prefs = await _db.GetPlayerPreferencesAsync(userId);
var prefs = await _db.GetPlayerPreferencesAsync(userId, cancel);
if (prefs is null)
{
return await _db.InitPrefsAsync(userId, HumanoidCharacterProfile.Random());
return await _db.InitPrefsAsync(userId, HumanoidCharacterProfile.Random(), cancel);
}
var session = _playerManager.GetSessionById(userId);
var collection = IoCManager.Instance!;
return SanitizePreferences(session, prefs, collection);
return prefs;
}
private PlayerPreferences SanitizePreferences(ICommonSession session, PlayerPreferences prefs, IDependencyCollection collection)

View File

@@ -40,14 +40,21 @@ public sealed partial class SalvageExpeditionComponent : SharedSalvageExpedition
/// <summary>
/// Countdown audio stream.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Stream = null;
/// <summary>
/// Sound that plays when the mission end is imminent.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("sound")]
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Misc/tension_session.ogg")
[ViewVariables(VVAccess.ReadWrite), DataField]
public SoundSpecifier Sound = new SoundCollectionSpecifier("ExpeditionEnd")
{
Params = AudioParams.Default.WithVolume(-5),
};
/// <summary>
/// Song selected on MapInit so we can predict the audio countdown properly.
/// </summary>
[DataField]
public SoundPathSpecifier SelectedSong;
}

View File

@@ -4,7 +4,9 @@ using Content.Server.Salvage.Expeditions;
using Content.Server.Salvage.Expeditions.Structure;
using Content.Shared.CCVar;
using Content.Shared.Examine;
using Content.Shared.Random.Helpers;
using Content.Shared.Salvage.Expeditions;
using Robust.Shared.Audio;
using Robust.Shared.CPUJob.JobQueues;
using Robust.Shared.CPUJob.JobQueues.Queues;
using Robust.Shared.GameStates;
@@ -32,6 +34,7 @@ public sealed partial class SalvageSystem
SubscribeLocalEvent<SalvageExpeditionConsoleComponent, EntParentChangedMessage>(OnSalvageConsoleParent);
SubscribeLocalEvent<SalvageExpeditionConsoleComponent, ClaimSalvageMessage>(OnSalvageClaimMessage);
SubscribeLocalEvent<SalvageExpeditionComponent, MapInitEvent>(OnExpeditionMapInit);
SubscribeLocalEvent<SalvageExpeditionComponent, ComponentShutdown>(OnExpeditionShutdown);
SubscribeLocalEvent<SalvageExpeditionComponent, ComponentGetState>(OnExpeditionGetState);
@@ -64,6 +67,12 @@ public sealed partial class SalvageSystem
_cooldown = obj;
}
private void OnExpeditionMapInit(EntityUid uid, SalvageExpeditionComponent component, MapInitEvent args)
{
var selectedFile = _audio.GetSound(component.Sound);
component.SelectedSong = new SoundPathSpecifier(selectedFile, component.Sound.Params);
}
private void OnExpeditionShutdown(EntityUid uid, SalvageExpeditionComponent component, ComponentShutdown args)
{
component.Stream = _audio.Stop(component.Stream);

View File

@@ -144,6 +144,7 @@ public sealed partial class SalvageSystem
while (query.MoveNext(out var uid, out var comp))
{
var remaining = comp.EndTime - _timing.CurTime;
var audioLength = _audio.GetAudioLength(comp.SelectedSong.Path.ToString());
if (comp.Stage < ExpeditionStage.FinalCountdown && remaining < TimeSpan.FromSeconds(45))
{
@@ -151,13 +152,14 @@ public sealed partial class SalvageSystem
Dirty(uid, comp);
Announce(uid, Loc.GetString("salvage-expedition-announcement-countdown-seconds", ("duration", TimeSpan.FromSeconds(45).Seconds)));
}
else if (comp.Stage < ExpeditionStage.MusicCountdown && remaining < TimeSpan.FromMinutes(2))
else if (comp.Stream == null && remaining < audioLength)
{
// TODO: Some way to play audio attached to a map for players.
comp.Stream = _audio.PlayGlobal(comp.Sound, Filter.BroadcastMap(Comp<MapComponent>(uid).MapId), true).Value.Entity;
var audio = _audio.PlayPvs(comp.Sound, uid).Value;
comp.Stream = audio.Entity;
_audio.SetMapAudio(audio);
comp.Stage = ExpeditionStage.MusicCountdown;
Dirty(uid, comp);
Announce(uid, Loc.GetString("salvage-expedition-announcement-countdown-minutes", ("duration", TimeSpan.FromMinutes(2).Minutes)));
Announce(uid, Loc.GetString("salvage-expedition-announcement-countdown-minutes", ("duration", audioLength.Minutes)));
}
else if (comp.Stage < ExpeditionStage.Countdown && remaining < TimeSpan.FromMinutes(4))
{

View File

@@ -0,0 +1,14 @@
using Content.Shared.DeviceLinking;
using Robust.Shared.Prototypes;
namespace Content.Server.Shuttles.Components;
[RegisterComponent]
public sealed partial class DockingSignalControlComponent : Component
{
/// <summary>
/// Output port that is high while docked.
/// </summary>
[DataField]
public ProtoId<SourcePortPrototype> DockStatusSignalPort = "DockStatus";
}

View File

@@ -0,0 +1,28 @@
using Content.Server.DeviceLinking.Systems;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events;
namespace Content.Server.Shuttles.Systems;
public sealed class DockingSignalControlSystem : EntitySystem
{
[Dependency] private readonly DeviceLinkSystem _deviceLinkSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DockingSignalControlComponent, DockEvent>(OnDocked);
SubscribeLocalEvent<DockingSignalControlComponent, UndockEvent>(OnUndocked);
}
private void OnDocked(Entity<DockingSignalControlComponent> ent, ref DockEvent args)
{
_deviceLinkSystem.SendSignal(ent, ent.Comp.DockStatusSignalPort, signal: true);
}
private void OnUndocked(Entity<DockingSignalControlComponent> ent, ref UndockEvent args)
{
_deviceLinkSystem.SendSignal(ent, ent.Comp.DockStatusSignalPort, signal: false);
}
}

View File

@@ -264,11 +264,6 @@ public sealed class ThrusterSystem : EntitySystem
return;
}
if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPower))
{
apcPower.NeedsPower = true;
}
component.IsOn = true;
if (!EntityManager.TryGetComponent(xform.GridUid, out ShuttleComponent? shuttleComponent))
@@ -371,11 +366,6 @@ public sealed class ThrusterSystem : EntitySystem
if (!EntityManager.TryGetComponent(gridId, out ShuttleComponent? shuttleComponent))
return;
if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPower))
{
apcPower.NeedsPower = false;
}
// Logger.DebugS("thruster", $"Disabled thruster {uid}");
switch (component.Type)

View File

@@ -10,6 +10,10 @@ public sealed class FrenchAccentSystem : EntitySystem
{
[Dependency] private readonly ReplacementAccentSystem _replacement = default!;
private static readonly Regex RegexTh = new(@"th", RegexOptions.IgnoreCase);
private static readonly Regex RegexStartH = new(@"(?<!\w)h", RegexOptions.IgnoreCase);
private static readonly Regex RegexSpacePunctuation = new(@"(?<=\w\w)[!?;:](?!\w)", RegexOptions.IgnoreCase);
public override void Initialize()
{
base.Initialize();
@@ -23,17 +27,14 @@ public sealed class FrenchAccentSystem : EntitySystem
msg = _replacement.ApplyReplacements(msg, "french");
// replaces th with dz
msg = Regex.Replace(msg, @"th", "'z", RegexOptions.IgnoreCase);
// replaces th with dz
msg = RegexTh.Replace(msg, "'z");
// removes the letter h from the start of words.
msg = Regex.Replace(msg, @"(?<!\w)[h]", "'", RegexOptions.IgnoreCase);
msg = RegexStartH.Replace(msg, "'");
// spaces out ! ? : and ;.
msg = Regex.Replace(msg, @"(?<=\w\w)!(?!\w)", " !", RegexOptions.IgnoreCase);
msg = Regex.Replace(msg, @"(?<=\w\w)[?](?!\w)", " ?", RegexOptions.IgnoreCase);
msg = Regex.Replace(msg, @"(?<=\w\w)[;](?!\w)", " ;", RegexOptions.IgnoreCase);
msg = Regex.Replace(msg, @"(?<=\w\w)[:](?!\w)", " :", RegexOptions.IgnoreCase);
msg = RegexSpacePunctuation.Replace(msg, " $&");
return msg;
}

View File

@@ -5,6 +5,13 @@ namespace Content.Server.Speech.EntitySystems;
public sealed class FrontalLispSystem : EntitySystem
{
// @formatter:off
private static readonly Regex RegexUpperTh = new(@"[T]+[Ss]+|[S]+[Cc]+(?=[IiEeYy]+)|[C]+(?=[IiEeYy]+)|[P][Ss]+|([S]+[Tt]+|[T]+)(?=[Ii]+[Oo]+[Uu]*[Nn]*)|[C]+[Hh]+(?=[Ii]*[Ee]*)|[Z]+|[S]+|[X]+(?=[Ee]+)");
private static readonly Regex RegexLowerTh = new(@"[t]+[s]+|[s]+[c]+(?=[iey]+)|[c]+(?=[iey]+)|[p][s]+|([s]+[t]+|[t]+)(?=[i]+[o]+[u]*[n]*)|[c]+[h]+(?=[i]*[e]*)|[z]+|[s]+|[x]+(?=[e]+)");
private static readonly Regex RegexUpperEcks = new(@"[E]+[Xx]+[Cc]*|[X]+");
private static readonly Regex RegexLowerEcks = new(@"[e]+[x]+[c]*|[x]+");
// @formatter:on
public override void Initialize()
{
base.Initialize();
@@ -16,11 +23,11 @@ public sealed class FrontalLispSystem : EntitySystem
var message = args.Message;
// handles ts, sc(i|e|y), c(i|e|y), ps, st(io(u|n)), ch(i|e), z, s
message = Regex.Replace(message, @"[T]+[Ss]+|[S]+[Cc]+(?=[IiEeYy]+)|[C]+(?=[IiEeYy]+)|[P][Ss]+|([S]+[Tt]+|[T]+)(?=[Ii]+[Oo]+[Uu]*[Nn]*)|[C]+[Hh]+(?=[Ii]*[Ee]*)|[Z]+|[S]+|[X]+(?=[Ee]+)", "TH");
message = Regex.Replace(message, @"[t]+[s]+|[s]+[c]+(?=[iey]+)|[c]+(?=[iey]+)|[p][s]+|([s]+[t]+|[t]+)(?=[i]+[o]+[u]*[n]*)|[c]+[h]+(?=[i]*[e]*)|[z]+|[s]+|[x]+(?=[e]+)", "th");
message = RegexUpperTh.Replace(message, "TH");
message = RegexLowerTh.Replace(message, "th");
// handles ex(c), x
message = Regex.Replace(message, @"[E]+[Xx]+[Cc]*|[X]+", "EKTH");
message = Regex.Replace(message, @"[e]+[x]+[c]*|[x]+", "ekth");
message = RegexUpperEcks.Replace(message, "EKTH");
message = RegexLowerEcks.Replace(message, "ekth");
args.Message = message;
}

View File

@@ -5,6 +5,12 @@ namespace Content.Server.Speech.EntitySystems;
public sealed class LizardAccentSystem : EntitySystem
{
private static readonly Regex RegexLowerS = new("s+");
private static readonly Regex RegexUpperS = new("S+");
private static readonly Regex RegexInternalX = new(@"(\w)x");
private static readonly Regex RegexLowerEndX = new(@"\bx([\-|r|R]|\b)");
private static readonly Regex RegexUpperEndX = new(@"\bX([\-|r|R]|\b)");
public override void Initialize()
{
base.Initialize();
@@ -16,15 +22,15 @@ public sealed class LizardAccentSystem : EntitySystem
var message = args.Message;
// hissss
message = Regex.Replace(message, "s+", "sss");
message = RegexLowerS.Replace(message, "sss");
// hiSSS
message = Regex.Replace(message, "S+", "SSS");
message = RegexUpperS.Replace(message, "SSS");
// ekssit
message = Regex.Replace(message, @"(\w)x", "$1kss");
message = RegexInternalX.Replace(message, "$1kss");
// ecks
message = Regex.Replace(message, @"\bx([\-|r|R]|\b)", "ecks$1");
message = RegexLowerEndX.Replace(message, "ecks$1");
// eckS
message = Regex.Replace(message, @"\bX([\-|r|R]|\b)", "ECKS$1");
message = RegexUpperEndX.Replace(message, "ECKS$1");
args.Message = message;
}

View File

@@ -1,4 +1,3 @@
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Content.Server.Speech.Components;
@@ -8,30 +7,17 @@ namespace Content.Server.Speech.EntitySystems;
public sealed class MobsterAccentSystem : EntitySystem
{
private static readonly Regex RegexIng = new(@"(?<=\w\w)(in)g(?!\w)", RegexOptions.IgnoreCase);
private static readonly Regex RegexLowerOr = new(@"(?<=\w)o[Rr](?=\w)");
private static readonly Regex RegexUpperOr = new(@"(?<=\w)O[Rr](?=\w)");
private static readonly Regex RegexLowerAr = new(@"(?<=\w)a[Rr](?=\w)");
private static readonly Regex RegexUpperAr = new(@"(?<=\w)A[Rr](?=\w)");
private static readonly Regex RegexFirstWord = new(@"^(\S+)");
private static readonly Regex RegexLastWord = new(@"(\S+)$");
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ReplacementAccentSystem _replacement = default!;
private static readonly Dictionary<string, string> DirectReplacements = new()
{
{ "let me", "lemme" },
{ "should", "oughta" },
{ "the", "da" },
{ "them", "dem" },
{ "attack", "whack" },
{ "kill", "whack" },
{ "murder", "whack" },
{ "dead", "sleepin' with da fishies"},
{ "hey", "ey'o" },
{ "hi", "ey'o"},
{ "hello", "ey'o"},
{ "rules", "roolz" },
{ "you", "yous" },
{ "have to", "gotta" },
{ "going to", "boutta" },
{ "about to", "boutta" },
{ "here", "'ere" }
};
public override void Initialize()
{
base.Initialize();
@@ -51,20 +37,20 @@ public sealed class MobsterAccentSystem : EntitySystem
// thinking -> thinkin'
// king -> king
//Uses captures groups to make sure the captialization of IN is kept
msg = Regex.Replace(msg, @"(?<=\w\w)(in)g(?!\w)", "$1'", RegexOptions.IgnoreCase);
msg = RegexIng.Replace(msg, "$1'");
// or -> uh and ar -> ah in the middle of words (fuhget, tahget)
msg = Regex.Replace(msg, @"(?<=\w)o[Rr](?=\w)", "uh");
msg = Regex.Replace(msg, @"(?<=\w)O[Rr](?=\w)", "UH");
msg = Regex.Replace(msg, @"(?<=\w)a[Rr](?=\w)", "ah");
msg = Regex.Replace(msg, @"(?<=\w)A[Rr](?=\w)", "AH");
msg = RegexLowerOr.Replace(msg, "uh");
msg = RegexUpperOr.Replace(msg, "UH");
msg = RegexLowerAr.Replace(msg, "ah");
msg = RegexUpperAr.Replace(msg, "AH");
// Prefix
if (_random.Prob(0.15f))
{
//Checks if the first word of the sentence is all caps
//So the prefix can be allcapped and to not resanitize the captial
var firstWordAllCaps = !Regex.Match(msg, @"^(\S+)").Value.Any(char.IsLower);
var firstWordAllCaps = !RegexFirstWord.Match(msg).Value.Any(char.IsLower);
var pick = _random.Next(1, 2);
// Reverse sanitize capital
@@ -84,7 +70,7 @@ public sealed class MobsterAccentSystem : EntitySystem
{
//Checks if the last word of the sentence is all caps
//So the suffix can be allcapped
var lastWordAllCaps = !Regex.Match(msg, @"(\S+)$").Value.Any(char.IsLower);
var lastWordAllCaps = !RegexLastWord.Match(msg).Value.Any(char.IsLower);
var suffix = "";
if (component.IsBoss)
{
@@ -94,7 +80,7 @@ public sealed class MobsterAccentSystem : EntitySystem
else
{
var pick = _random.Next(1, 3);
suffix = Loc.GetString($"accent-mobster-suffix-minion-{pick}");
suffix = Loc.GetString($"accent-mobster-suffix-minion-{pick}");
}
if (lastWordAllCaps)
suffix = suffix.ToUpper();

View File

@@ -5,6 +5,9 @@ namespace Content.Server.Speech.EntitySystems;
public sealed class MothAccentSystem : EntitySystem
{
private static readonly Regex RegexLowerBuzz = new Regex("z{1,3}");
private static readonly Regex RegexUpperBuzz = new Regex("Z{1,3}");
public override void Initialize()
{
base.Initialize();
@@ -16,10 +19,10 @@ public sealed class MothAccentSystem : EntitySystem
var message = args.Message;
// buzzz
message = Regex.Replace(message, "z{1,3}", "zzz");
message = RegexLowerBuzz.Replace(message, "zzz");
// buZZZ
message = Regex.Replace(message, "Z{1,3}", "ZZZ");
message = RegexUpperBuzz.Replace(message, "ZZZ");
args.Message = message;
}
}

View File

@@ -7,6 +7,8 @@ namespace Content.Server.Speech.EntitySystems;
public sealed partial class ParrotAccentSystem : EntitySystem
{
private static readonly Regex WordCleanupRegex = new Regex("[^A-Za-z0-9 -]");
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
@@ -27,7 +29,7 @@ public sealed partial class ParrotAccentSystem : EntitySystem
if (_random.Prob(entity.Comp.LongestWordRepeatChance))
{
// Don't count non-alphanumeric characters as parts of words
var cleaned = Regex.Replace(message, "[^A-Za-z0-9 -]", string.Empty);
var cleaned = WordCleanupRegex.Replace(message, string.Empty);
// Split on whitespace and favor words towards the end of the message
var words = cleaned.Split(null).Reverse();
// Find longest word

View File

@@ -7,6 +7,8 @@ namespace Content.Server.Speech.EntitySystems;
public sealed class PirateAccentSystem : EntitySystem
{
private static readonly Regex FirstWordAllCapsRegex = new(@"^(\S+)");
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ReplacementAccentSystem _replacement = default!;
@@ -26,7 +28,7 @@ public sealed class PirateAccentSystem : EntitySystem
return msg;
//Checks if the first word of the sentence is all caps
//So the prefix can be allcapped and to not resanitize the captial
var firstWordAllCaps = !Regex.Match(msg, @"^(\S+)").Value.Any(char.IsLower);
var firstWordAllCaps = !FirstWordAllCapsRegex.Match(msg).Value.Any(char.IsLower);
var pick = _random.Pick(component.PirateWords);
var pirateWord = Loc.GetString(pick);

View File

@@ -7,6 +7,8 @@ namespace Content.Server.Speech.EntitySystems
{
public sealed class ScrambledAccentSystem : EntitySystem
{
private static readonly Regex RegexLoneI = new(@"(?<=\ )i(?=[\ \.\?]|$)");
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
@@ -34,7 +36,7 @@ namespace Content.Server.Speech.EntitySystems
msg = msg[0].ToString().ToUpper() + msg.Remove(0, 1);
// Capitalize lone i's
msg = Regex.Replace(msg, @"(?<=\ )i(?=[\ \.\?]|$)", "I");
msg = RegexLoneI.Replace(msg, "I");
return msg;
}

View File

@@ -5,8 +5,12 @@ namespace Content.Server.Speech.EntitySystems;
public sealed class SouthernAccentSystem : EntitySystem
{
private static readonly Regex RegexIng = new(@"ing\b");
private static readonly Regex RegexAnd = new(@"\band\b");
private static readonly Regex RegexDve = new("d've");
[Dependency] private readonly ReplacementAccentSystem _replacement = default!;
public override void Initialize()
{
base.Initialize();
@@ -20,9 +24,9 @@ public sealed class SouthernAccentSystem : EntitySystem
message = _replacement.ApplyReplacements(message, "southern");
//They shoulda started runnin' an' hidin' from me!
message = Regex.Replace(message, @"ing\b", "in'");
message = Regex.Replace(message, @"\band\b", "an'");
message = Regex.Replace(message, "d've", "da");
message = RegexIng.Replace(message, "in'");
message = RegexAnd.Replace(message, "an'");
message = RegexDve.Replace(message, "da");
args.Message = message;
}
};

View File

@@ -16,4 +16,7 @@ public sealed partial class StationBiomeComponent : Component
// If null, its random
[DataField]
public int? Seed = null;
[DataField]
public Color MapLightColor = Color.Black;
}

View File

@@ -30,6 +30,6 @@ public sealed partial class StationBiomeSystem : EntitySystem
var mapId = Transform(station.Value).MapID;
var mapUid = _mapManager.GetMapEntityId(mapId);
_biome.EnsurePlanet(mapUid, _proto.Index(map.Comp.Biome), map.Comp.Seed);
_biome.EnsurePlanet(mapUid, _proto.Index(map.Comp.Biome), map.Comp.Seed, mapLight: map.Comp.MapLightColor);
}
}

View File

@@ -1,5 +1,6 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
@@ -22,6 +23,9 @@ namespace Content.Server.StationEvents
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EventManagerSystem _event = default!;
public const float MinEventTime = 60 * 3;
public const float MaxEventTime = 60 * 10;
protected override void Ended(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule,
GameRuleEndedEvent args)
{
@@ -58,7 +62,7 @@ namespace Content.Server.StationEvents
/// </summary>
private void ResetTimer(BasicStationEventSchedulerComponent component)
{
component.TimeUntilNextEvent = _random.Next(3 * 60, 10 * 60);
component.TimeUntilNextEvent = _random.NextFloat(MinEventTime, MaxEventTime);
}
}
@@ -66,6 +70,59 @@ namespace Content.Server.StationEvents
public sealed class StationEventCommand : ToolshedCommand
{
private EventManagerSystem? _stationEvent;
private BasicStationEventSchedulerSystem? _basicScheduler;
private IRobustRandom? _random;
/// <summary>
/// Estimates the expected number of times an event will run over the course of X rounds, taking into account weights and
/// how many events are expected to run over a given timeframe for a given playercount by repeatedly simulating rounds.
/// Effectively /100 (if you put 100 rounds) = probability an event will run per round.
/// </summary>
/// <remarks>
/// This isn't perfect. Code path eventually goes into <see cref="EventManagerSystem.CanRun"/>, which requires
/// state from <see cref="GameTicker"/>. As a result, you should probably just run this locally and not doing
/// a real round (it won't pollute the state, but it will get contaminated by previously ran events in the actual round)
/// and things like `MaxOccurrences` and `ReoccurrenceDelay` won't be respected.
///
/// I consider these to not be that relevant to the analysis here though (and I don't want most uses of them
/// to even exist) so I think it's fine.
/// </remarks>
[CommandImplementation("simulate")]
public IEnumerable<(string, float)> Simulate([CommandArgument] int rounds, [CommandArgument] int playerCount, [CommandArgument] float roundEndMean, [CommandArgument] float roundEndStdDev)
{
_stationEvent ??= GetSys<EventManagerSystem>();
_basicScheduler ??= GetSys<BasicStationEventSchedulerSystem>();
_random ??= IoCManager.Resolve<IRobustRandom>();
var occurrences = new Dictionary<string, int>();
foreach (var ev in _stationEvent.AllEvents())
{
occurrences.Add(ev.Key.ID, 0);
}
for (var i = 0; i < rounds; i++)
{
var curTime = TimeSpan.Zero;
var randomEndTime = _random.NextGaussian(roundEndMean, roundEndStdDev) * 60; // *60 = minutes to seconds
if (randomEndTime <= 0)
continue;
while (curTime.TotalSeconds < randomEndTime)
{
// sim an event
curTime += TimeSpan.FromSeconds(_random.NextFloat(BasicStationEventSchedulerSystem.MinEventTime, BasicStationEventSchedulerSystem.MaxEventTime));
var available = _stationEvent.AvailableEvents(false, playerCount, curTime);
var ev = _stationEvent.FindEvent(available);
if (ev == null)
continue;
occurrences[ev] += 1;
}
}
return occurrences.Select(p => (p.Key, (float) p.Value)).OrderByDescending(p => p.Item2);
}
[CommandImplementation("lsprob")]
public IEnumerable<(string, float)> LsProb()

View File

@@ -1,4 +1,5 @@
using System.Linq;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.StationEvents.Components;
using Content.Shared.CCVar;
@@ -15,6 +16,7 @@ public sealed class EventManagerSystem : EntitySystem
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] public readonly GameTicker GameTicker = default!;
public bool EventsEnabled { get; private set; }
@@ -43,6 +45,7 @@ public sealed class EventManagerSystem : EntitySystem
var ent = GameTicker.AddGameRule(randomEvent);
var str = Loc.GetString("station-event-system-run-event",("eventName", ToPrettyString(ent)));
_chat.SendAdminAlert(str);
Log.Info(str);
return str;
}
@@ -61,7 +64,7 @@ public sealed class EventManagerSystem : EntitySystem
/// Pick a random event from the available events at this time, also considering their weightings.
/// </summary>
/// <returns></returns>
private string? FindEvent(Dictionary<EntityPrototype, StationEventComponent> availableEvents)
public string? FindEvent(Dictionary<EntityPrototype, StationEventComponent> availableEvents)
{
if (availableEvents.Count == 0)
{
@@ -95,16 +98,20 @@ public sealed class EventManagerSystem : EntitySystem
/// <summary>
/// Gets the events that have met their player count, time-until start, etc.
/// </summary>
/// <param name="ignoreEarliestStart"></param>
/// <param name="playerCountOverride">Override for player count, if using this to simulate events rather than in an actual round.</param>
/// <param name="currentTimeOverride">Override for round time, if using this to simulate events rather than in an actual round.</param>
/// <returns></returns>
private Dictionary<EntityPrototype, StationEventComponent> AvailableEvents(bool ignoreEarliestStart = false)
public Dictionary<EntityPrototype, StationEventComponent> AvailableEvents(
bool ignoreEarliestStart = false,
int? playerCountOverride = null,
TimeSpan? currentTimeOverride = null)
{
var playerCount = _playerManager.PlayerCount;
var playerCount = playerCountOverride ?? _playerManager.PlayerCount;
// playerCount does a lock so we'll just keep the variable here
var currentTime = !ignoreEarliestStart
var currentTime = currentTimeOverride ?? (!ignoreEarliestStart
? GameTicker.RoundDuration()
: TimeSpan.Zero;
: TimeSpan.Zero);
var result = new Dictionary<EntityPrototype, StationEventComponent>();
@@ -112,7 +119,6 @@ public sealed class EventManagerSystem : EntitySystem
{
if (CanRun(proto, stationEvent, playerCount, currentTime))
{
Log.Debug($"Adding event {proto.ID} to possibilities");
result.Add(proto, stationEvent);
}
}

View File

@@ -52,7 +52,8 @@ namespace Content.Shared.Alert
SuitPower,
BorgHealth,
BorgCrit,
BorgDead
BorgDead,
Deflecting
}
}

View File

@@ -11,10 +11,13 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Dependency = Robust.Shared.IoC.DependencyAttribute;
namespace Content.Shared.Chemistry.EntitySystems;
@@ -58,6 +61,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
[Dependency] protected readonly SharedHandsSystem Hands = default!;
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
[Dependency] protected readonly MetaDataSystem MetaData = default!;
[Dependency] protected readonly INetManager NetManager = default!;
public override void Initialize()
{
@@ -66,13 +70,18 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
InitializeRelays();
SubscribeLocalEvent<SolutionComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<SolutionComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<SolutionComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<SolutionContainerManagerComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<SolutionComponent, ComponentStartup>(OnSolutionStartup);
SubscribeLocalEvent<SolutionComponent, ComponentShutdown>(OnSolutionShutdown);
SubscribeLocalEvent<SolutionContainerManagerComponent, ComponentInit>(OnContainerManagerInit);
SubscribeLocalEvent<ExaminableSolutionComponent, ExaminedEvent>(OnExamineSolution);
SubscribeLocalEvent<ExaminableSolutionComponent, GetVerbsEvent<ExamineVerb>>(OnSolutionExaminableVerb);
SubscribeLocalEvent<SolutionContainerManagerComponent, MapInitEvent>(OnMapInit);
if (NetManager.IsServer)
{
SubscribeLocalEvent<SolutionContainerManagerComponent, ComponentShutdown>(OnContainerManagerShutdown);
SubscribeLocalEvent<ContainedSolutionComponent, ComponentShutdown>(OnContainedSolutionShutdown);
}
}
@@ -121,8 +130,14 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
/// <param name="name">The name of the solution entity to fetch.</param>
/// <param name="entity">Returns the solution entity that was fetched.</param>
/// <param name="solution">Returns the solution state of the solution entity that was fetched.</param>
/// /// <param name="errorOnMissing">Should we print an error if the solution specified by name is missing</param>
/// <returns></returns>
public bool TryGetSolution(Entity<SolutionContainerManagerComponent?> container, string? name, [NotNullWhen(true)] out Entity<SolutionComponent>? entity, [NotNullWhen(true)] out Solution? solution)
public bool TryGetSolution(
Entity<SolutionContainerManagerComponent?> container,
string? name,
[NotNullWhen(true)] out Entity<SolutionComponent>? entity,
[NotNullWhen(true)] out Solution? solution,
bool errorOnMissing = false)
{
if (!TryGetSolution(container, name, out entity))
{
@@ -135,7 +150,11 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
}
/// <inheritdoc cref="TryGetSolution"/>
public bool TryGetSolution(Entity<SolutionContainerManagerComponent?> container, string? name, [NotNullWhen(true)] out Entity<SolutionComponent>? entity)
public bool TryGetSolution(
Entity<SolutionContainerManagerComponent?> container,
string? name,
[NotNullWhen(true)] out Entity<SolutionComponent>? entity,
bool errorOnMissing = false)
{
if (TryComp(container, out BlockSolutionAccessComponent? blocker))
{
@@ -155,12 +174,18 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
else
{
entity = null;
if (!errorOnMissing)
return false;
Log.Error($"{ToPrettyString(container)} does not have a solution with ID: {name}");
return false;
}
if (!TryComp(uid, out SolutionComponent? comp))
{
entity = null;
if (!errorOnMissing)
return false;
Log.Error($"{ToPrettyString(container)} does not have a solution with ID: {name}");
return false;
}
@@ -171,13 +196,18 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
/// <summary>
/// Version of TryGetSolution that doesn't take or return an entity.
/// Used for prototypes and with old code parity.
public bool TryGetSolution(SolutionContainerManagerComponent container, string name, [NotNullWhen(true)] out Solution? solution)
public bool TryGetSolution(SolutionContainerManagerComponent container,
string name,
[NotNullWhen(true)] out Solution? solution,
bool errorOnMissing = false)
{
solution = null;
if (container.Solutions == null)
if (container.Solutions != null)
return container.Solutions.TryGetValue(name, out solution);
if (!errorOnMissing)
return false;
return container.Solutions.TryGetValue(name, out solution);
Log.Error($"{container} does not have a solution with ID: {name}");
return false;
}
public IEnumerable<(string? Name, Entity<SolutionComponent> Solution)> EnumerateSolutions(Entity<SolutionContainerManagerComponent?> container, bool includeSelf = true)
@@ -703,17 +733,17 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
entity.Comp.Solution.ValidateSolution();
}
private void OnComponentStartup(Entity<SolutionComponent> entity, ref ComponentStartup args)
private void OnSolutionStartup(Entity<SolutionComponent> entity, ref ComponentStartup args)
{
UpdateChemicals(entity);
}
private void OnComponentShutdown(Entity<SolutionComponent> entity, ref ComponentShutdown args)
private void OnSolutionShutdown(Entity<SolutionComponent> entity, ref ComponentShutdown args)
{
RemoveAllSolution(entity);
}
private void OnComponentInit(Entity<SolutionContainerManagerComponent> entity, ref ComponentInit args)
private void OnContainerManagerInit(Entity<SolutionContainerManagerComponent> entity, ref ComponentInit args)
{
if (entity.Comp.Containers is not { Count: > 0 } containers)
return;
@@ -733,7 +763,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
return;
}
if (!CanSeeHiddenSolution(entity,args.Examiner))
if (!CanSeeHiddenSolution(entity, args.Examiner))
return;
var primaryReagent = solution.GetPrimaryReagentId();
@@ -832,7 +862,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
return;
}
if (!CanSeeHiddenSolution(entity,args.User))
if (!CanSeeHiddenSolution(entity, args.User))
return;
var target = args.Target;
@@ -881,6 +911,9 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
, ("amount", quantity)));
}
msg.PushNewline();
msg.AddMarkup(Loc.GetString("scannable-solution-temperature", ("temperature", Math.Round(solution.Temperature))));
return msg;
}
@@ -901,5 +934,273 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
return true;
}
private void OnMapInit(Entity<SolutionContainerManagerComponent> entity, ref MapInitEvent args)
{
EnsureAllSolutions(entity);
}
private void OnContainerManagerShutdown(Entity<SolutionContainerManagerComponent> entity, ref ComponentShutdown args)
{
foreach (var name in entity.Comp.Containers)
{
if (ContainerSystem.TryGetContainer(entity, $"solution@{name}", out var solutionContainer))
ContainerSystem.ShutdownContainer(solutionContainer);
}
entity.Comp.Containers.Clear();
}
private void OnContainedSolutionShutdown(Entity<ContainedSolutionComponent> entity, ref ComponentShutdown args)
{
if (TryComp(entity.Comp.Container, out SolutionContainerManagerComponent? container))
{
container.Containers.Remove(entity.Comp.ContainerName);
Dirty(entity.Comp.Container, container);
}
if (ContainerSystem.TryGetContainer(entity, $"solution@{entity.Comp.ContainerName}", out var solutionContainer))
ContainerSystem.ShutdownContainer(solutionContainer);
}
#endregion Event Handlers
public bool EnsureSolution(
Entity<MetaDataComponent?> entity,
string name,
[NotNullWhen(true)]out Solution? solution,
FixedPoint2 maxVol = default)
{
return EnsureSolution(entity, name, maxVol, null, out _, out solution);
}
public bool EnsureSolution(
Entity<MetaDataComponent?> entity,
string name,
out bool existed,
[NotNullWhen(true)]out Solution? solution,
FixedPoint2 maxVol = default)
{
return EnsureSolution(entity, name, maxVol, null, out existed, out solution);
}
public bool EnsureSolution(
Entity<MetaDataComponent?> entity,
string name,
FixedPoint2 maxVol,
Solution? prototype,
out bool existed,
[NotNullWhen(true)] out Solution? solution)
{
solution = null;
existed = false;
var (uid, meta) = entity;
if (!Resolve(uid, ref meta))
throw new InvalidOperationException("Attempted to ensure solution on invalid entity.");
var manager = EnsureComp<SolutionContainerManagerComponent>(uid);
if (meta.EntityLifeStage >= EntityLifeStage.MapInitialized)
{
EnsureSolutionEntity((uid, manager), name, out existed,
out var solEnt, maxVol, prototype);
solution = solEnt!.Value.Comp.Solution;
return true;
}
solution = EnsureSolutionPrototype((uid, manager), name, maxVol, prototype, out existed);
return true;
}
public void EnsureAllSolutions(Entity<SolutionContainerManagerComponent> entity)
{
if (NetManager.IsClient)
return;
if (entity.Comp.Solutions is not { } prototypes)
return;
foreach (var (name, prototype) in prototypes)
{
EnsureSolutionEntity((entity.Owner, entity.Comp), name, out _, out _, prototype.MaxVolume, prototype);
}
entity.Comp.Solutions = null;
Dirty(entity);
}
public bool EnsureSolutionEntity(
Entity<SolutionContainerManagerComponent?> entity,
string name,
[NotNullWhen(true)] out Entity<SolutionComponent>? solutionEntity,
FixedPoint2 maxVol = default) =>
EnsureSolutionEntity(entity, name, out _, out solutionEntity, maxVol);
public bool EnsureSolutionEntity(
Entity<SolutionContainerManagerComponent?> entity,
string name,
out bool existed,
[NotNullWhen(true)] out Entity<SolutionComponent>? solutionEntity,
FixedPoint2 maxVol = default,
Solution? prototype = null
)
{
existed = true;
solutionEntity = null;
var (uid, container) = entity;
var solutionSlot = ContainerSystem.EnsureContainer<ContainerSlot>(uid, $"solution@{name}", out existed);
if (!Resolve(uid, ref container, logMissing: false))
{
existed = false;
container = AddComp<SolutionContainerManagerComponent>(uid);
container.Containers.Add(name);
if (NetManager.IsClient)
return false;
}
else if (!existed)
{
container.Containers.Add(name);
Dirty(uid, container);
}
var needsInit = false;
SolutionComponent solutionComp;
if (solutionSlot.ContainedEntity is not { } solutionId)
{
if (NetManager.IsClient)
return false;
prototype ??= new() { MaxVolume = maxVol };
prototype.Name = name;
(solutionId, solutionComp, _) = SpawnSolutionUninitialized(solutionSlot, name, maxVol, prototype);
existed = false;
needsInit = true;
Dirty(uid, container);
}
else
{
solutionComp = Comp<SolutionComponent>(solutionId);
DebugTools.Assert(TryComp(solutionId, out ContainedSolutionComponent? relation) && relation.Container == uid && relation.ContainerName == name);
DebugTools.Assert(solutionComp.Solution.Name == name);
var solution = solutionComp.Solution;
solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, maxVol);
// Depending on MapInitEvent order some systems can ensure solution empty solutions and conflict with the prototype solutions.
// We want the reagents from the prototype to exist even if something else already created the solution.
if (prototype is { Volume.Value: > 0 })
solution.AddSolution(prototype, PrototypeManager);
Dirty(solutionId, solutionComp);
}
if (needsInit)
EntityManager.InitializeAndStartEntity(solutionId, Transform(solutionId).MapID);
solutionEntity = (solutionId, solutionComp);
return true;
}
private Solution EnsureSolutionPrototype(Entity<SolutionContainerManagerComponent?> entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed)
{
existed = true;
var (uid, container) = entity;
if (!Resolve(uid, ref container, logMissing: false))
{
container = AddComp<SolutionContainerManagerComponent>(uid);
existed = false;
}
if (container.Solutions is null)
container.Solutions = new(SolutionContainerManagerComponent.DefaultCapacity);
if (!container.Solutions.TryGetValue(name, out var solution))
{
solution = prototype ?? new() { Name = name, MaxVolume = maxVol };
container.Solutions.Add(name, solution);
existed = false;
}
else
solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, maxVol);
Dirty(uid, container);
return solution;
}
private Entity<SolutionComponent, ContainedSolutionComponent> SpawnSolutionUninitialized(ContainerSlot container, string name, FixedPoint2 maxVol, Solution prototype)
{
var coords = new EntityCoordinates(container.Owner, Vector2.Zero);
var uid = EntityManager.CreateEntityUninitialized(null, coords, null);
var solution = new SolutionComponent() { Solution = prototype };
AddComp(uid, solution);
var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name };
AddComp(uid, relation);
MetaData.SetEntityName(uid, $"solution - {name}");
ContainerSystem.Insert(uid, container, force: true);
return (uid, solution, relation);
}
public void AdjustDissolvedReagent(
Entity<SolutionComponent> dissolvedSolution,
FixedPoint2 volume,
ReagentId reagent,
float concentrationChange)
{
if (concentrationChange == 0)
return;
var dissolvedSol = dissolvedSolution.Comp.Solution;
var amtChange =
GetReagentQuantityFromConcentration(dissolvedSolution, volume, MathF.Abs(concentrationChange));
if (concentrationChange > 0)
{
dissolvedSol.AddReagent(reagent, amtChange);
}
else
{
dissolvedSol.RemoveReagent(reagent,amtChange);
}
UpdateChemicals(dissolvedSolution);
}
public FixedPoint2 GetReagentQuantityFromConcentration(Entity<SolutionComponent> dissolvedSolution,
FixedPoint2 volume,float concentration)
{
var dissolvedSol = dissolvedSolution.Comp.Solution;
if (volume == 0
|| dissolvedSol.Volume == 0)
return 0;
return concentration * volume;
}
public float GetReagentConcentration(Entity<SolutionComponent> dissolvedSolution,
FixedPoint2 volume, ReagentId dissolvedReagent)
{
var dissolvedSol = dissolvedSolution.Comp.Solution;
if (volume == 0
|| dissolvedSol.Volume == 0
|| !dissolvedSol.TryGetReagentQuantity(dissolvedReagent, out var dissolvedVol))
return 0;
return (float)dissolvedVol / volume.Float();
}
public FixedPoint2 ClampReagentAmountByConcentration(
Entity<SolutionComponent> dissolvedSolution,
FixedPoint2 volume,
ReagentId dissolvedReagent,
FixedPoint2 dissolvedReagentAmount,
float maxConcentration = 1f)
{
var dissolvedSol = dissolvedSolution.Comp.Solution;
if (volume == 0
|| dissolvedSol.Volume == 0
|| !dissolvedSol.TryGetReagentQuantity(dissolvedReagent, out var dissolvedVol))
return 0;
volume *= maxConcentration;
dissolvedVol += dissolvedReagentAmount;
var overflow = volume - dissolvedVol;
if (overflow < 0)
dissolvedReagentAmount += overflow;
return dissolvedReagentAmount;
}
}

View File

@@ -12,6 +12,21 @@ namespace Content.Shared.Ghost.Roles
public string Description { get; set; }
public string Rules { get; set; }
public HashSet<JobRequirement>? Requirements { get; set; }
/// <inheritdoc cref="GhostRoleKind"/>
public GhostRoleKind Kind { get; set; }
/// <summary>
/// if <see cref="Kind"/> is <see cref="GhostRoleKind.RaffleInProgress"/>, specifies how many players are currently
/// in the raffle for this role.
/// </summary>
public uint RafflePlayerCount { get; set; }
/// <summary>
/// if <see cref="Kind"/> is <see cref="GhostRoleKind.RaffleInProgress"/>, specifies when raffle finishes.
/// </summary>
public TimeSpan RaffleEndTime { get; set; }
}
[NetSerializable, Serializable]
@@ -26,24 +41,62 @@ namespace Content.Shared.Ghost.Roles
}
[NetSerializable, Serializable]
public sealed class GhostRoleTakeoverRequestMessage : EuiMessageBase
public sealed class RequestGhostRoleMessage : EuiMessageBase
{
public uint Identifier { get; }
public GhostRoleTakeoverRequestMessage(uint identifier)
public RequestGhostRoleMessage(uint identifier)
{
Identifier = identifier;
}
}
[NetSerializable, Serializable]
public sealed class GhostRoleFollowRequestMessage : EuiMessageBase
public sealed class FollowGhostRoleMessage : EuiMessageBase
{
public uint Identifier { get; }
public GhostRoleFollowRequestMessage(uint identifier)
public FollowGhostRoleMessage(uint identifier)
{
Identifier = identifier;
}
}
[NetSerializable, Serializable]
public sealed class LeaveGhostRoleRaffleMessage : EuiMessageBase
{
public uint Identifier { get; }
public LeaveGhostRoleRaffleMessage(uint identifier)
{
Identifier = identifier;
}
}
/// <summary>
/// Determines whether a ghost role is a raffle role, and if it is, whether it's running.
/// </summary>
[NetSerializable, Serializable]
public enum GhostRoleKind
{
/// <summary>
/// Role is not a raffle role and can be taken immediately.
/// </summary>
FirstComeFirstServe,
/// <summary>
/// Role is a raffle role, but raffle hasn't started yet.
/// </summary>
RaffleReady,
/// <summary>
/// Role is raffle role and currently being raffled, but player hasn't joined raffle.
/// </summary>
RaffleInProgress,
/// <summary>
/// Role is raffle role and currently being raffled, and player joined raffle.
/// </summary>
RaffleJoined
}
}

View File

@@ -0,0 +1,30 @@
namespace Content.Server.Ghost.Roles.Raffles;
/// <summary>
/// Defines settings for a ghost role raffle.
/// </summary>
[DataDefinition]
public sealed partial class GhostRoleRaffleSettings
{
/// <summary>
/// The initial duration of a raffle in seconds. This is the countdown timer's value when the raffle starts.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField(required: true)]
public uint InitialDuration { get; set; }
/// <summary>
/// When the raffle is joined by a player, the countdown timer is extended by this value in seconds.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField(required: true)]
public uint JoinExtendsDurationBy { get; set; }
/// <summary>
/// The maximum duration in seconds for the ghost role raffle. A raffle cannot run for longer than this
/// duration, even if extended by joiners. Must be greater than or equal to <see cref="InitialDuration"/>.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField(required: true)]
public uint MaxDuration { get; set; }
}

View File

@@ -0,0 +1,22 @@
using Content.Server.Ghost.Roles.Raffles;
using Robust.Shared.Prototypes;
namespace Content.Shared.Ghost.Roles.Raffles;
/// <summary>
/// Allows specifying the settings for a ghost role raffle as a prototype.
/// </summary>
[Prototype("ghostRoleRaffleSettings")]
public sealed class GhostRoleRaffleSettingsPrototype : IPrototype
{
/// <inheritdoc />
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// The settings for a ghost role raffle.
/// </summary>
/// <seealso cref="GhostRoleRaffleSettings"/>
[DataField(required: true)]
public GhostRoleRaffleSettings Settings { get; private set; } = new();
}

View File

@@ -13,6 +13,7 @@ using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.Pulling.Events;
using Content.Shared.Standing;
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
@@ -62,6 +63,7 @@ public sealed class PullingSystem : EntitySystem
SubscribeLocalEvent<PullerComponent, EntityUnpausedEvent>(OnPullerUnpaused);
SubscribeLocalEvent<PullerComponent, VirtualItemDeletedEvent>(OnVirtualItemDeleted);
SubscribeLocalEvent<PullerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
SubscribeLocalEvent<PullerComponent, DropHandItemsEvent>(OnDropHandItems);
CommandBinds.Builder
.Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(OnRequestMovePulledObject))
@@ -69,6 +71,17 @@ public sealed class PullingSystem : EntitySystem
.Register<PullingSystem>();
}
private void OnDropHandItems(EntityUid uid, PullerComponent pullerComp, DropHandItemsEvent args)
{
if (pullerComp.Pulling == null || pullerComp.NeedsHands)
return;
if (!TryComp(pullerComp.Pulling, out PullableComponent? pullableComp))
return;
TryStopPull(pullerComp.Pulling.Value, pullableComp, uid);
}
private void OnPullerContainerInsert(Entity<PullerComponent> ent, ref EntGotInsertedIntoContainerMessage args)
{
if (ent.Comp.Pulling == null) return;

View File

@@ -57,7 +57,8 @@ namespace Content.Shared.Random.Helpers
throw new InvalidOperationException($"Invalid weighted pick for {prototype.ID}!");
}
public static string Pick(this IRobustRandom random, Dictionary<string, float> weights)
public static T Pick<T>(this IRobustRandom random, Dictionary<T, float> weights)
where T: notnull
{
var sum = weights.Values.Sum();
var accumulated = 0f;
@@ -74,7 +75,7 @@ namespace Content.Shared.Random.Helpers
}
}
throw new InvalidOperationException($"Invalid weighted pick");
throw new InvalidOperationException("Invalid weighted pick");
}
public static (string reagent, FixedPoint2 quantity) Pick(this WeightedRandomFillSolutionPrototype prototype, IRobustRandom? random = null)

View File

@@ -0,0 +1,34 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Shared.ReagentSpeed;
/// <summary>
/// Makes a device work faster by consuming reagents on each use.
/// Other systems must use <see cref="ReagentSpeedSystem.ApplySpeed"/> for this to do anything.
/// </summary>
[RegisterComponent, Access(typeof(ReagentSpeedSystem))]
public sealed partial class ReagentSpeedComponent : Component
{
/// <summary>
/// Solution that will be checked.
/// Anything that isn't in <c>Modifiers</c> is left alone.
/// </summary>
[DataField(required: true)]
public string Solution = string.Empty;
/// <summary>
/// How much reagent from the solution to use up for each use.
/// This is per-modifier-reagent and not shared between them.
/// </summary>
[DataField]
public FixedPoint2 Cost = 5;
/// <summary>
/// Reagents and how much they modify speed at full purity.
/// Small number means faster large number means slower.
/// </summary>
[DataField(required: true)]
public Dictionary<ProtoId<ReagentPrototype>, float> Modifiers = new();
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Chemistry.EntitySystems;
namespace Content.Shared.ReagentSpeed;
public sealed class ReagentSpeedSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
/// <summary>
/// Consumes reagents and modifies the duration.
/// This can be production time firing delay etc.
/// </summary>
public TimeSpan ApplySpeed(Entity<ReagentSpeedComponent?> ent, TimeSpan time)
{
if (!Resolve(ent, ref ent.Comp, false))
return time;
if (!_solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var solution))
return time;
foreach (var (reagent, fullModifier) in ent.Comp.Modifiers)
{
var used = solution.RemoveReagent(reagent, ent.Comp.Cost);
var efficiency = (used / ent.Comp.Cost).Float();
// scale the speed modifier so microdosing has less effect
var reduction = (1f - fullModifier) * efficiency;
var modifier = 1f - reduction;
time *= modifier;
}
return time;
}
}

View File

@@ -0,0 +1,37 @@
using Content.Shared.Actions;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Weapons.Ranged.Components;
/// <summary>
/// Lets you shoot a gun using an action.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(ActionGunSystem))]
public sealed partial class ActionGunComponent : Component
{
/// <summary>
/// Action to create, must use <see cref="ActionGunShootEvent"/>.
/// </summary>
[DataField(required: true)]
public EntProtoId Action = string.Empty;
[DataField]
public EntityUid? ActionEntity;
/// <summary>
/// Prototype of gun entity to spawn.
/// Deleted when this component is removed.
/// </summary>
[DataField(required: true)]
public EntProtoId GunProto = string.Empty;
[DataField]
public EntityUid? Gun;
}
/// <summary>
/// Action event for <see cref="ActionGunComponent"/> to shoot at a position.
/// </summary>
public sealed partial class ActionGunShootEvent : WorldTargetActionEvent;

View File

@@ -0,0 +1,41 @@
using Content.Shared.Actions;
using Content.Shared.Weapons.Ranged.Components;
namespace Content.Shared.Weapons.Ranged.Systems;
public sealed class ActionGunSystem : EntitySystem
{
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedGunSystem _gun = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActionGunComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ActionGunComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ActionGunComponent, ActionGunShootEvent>(OnShoot);
}
private void OnMapInit(Entity<ActionGunComponent> ent, ref MapInitEvent args)
{
if (string.IsNullOrEmpty(ent.Comp.Action))
return;
_actions.AddAction(ent, ref ent.Comp.ActionEntity, ent.Comp.Action);
ent.Comp.Gun = Spawn(ent.Comp.GunProto);
}
private void OnShutdown(Entity<ActionGunComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.Gun is {} gun)
QueueDel(gun);
}
private void OnShoot(Entity<ActionGunComponent> ent, ref ActionGunShootEvent args)
{
if (TryComp<GunComponent>(ent.Comp.Gun, out var gun))
_gun.AttemptShoot(ent, ent.Comp.Gun.Value, gun, args.Target);
}
}

View File

@@ -21,17 +21,42 @@ public sealed partial class ReflectComponent : Component
[ViewVariables(VVAccess.ReadWrite), DataField("reflects")]
public ReflectType Reflects = ReflectType.Energy | ReflectType.NonEnergy;
/// <summary>
/// Probability for a projectile to be reflected.
/// </summary>
[DataField("reflectProb"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public float ReflectProb = 0.25f;
[DataField("spread"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public Angle Spread = Angle.FromDegrees(45);
[DataField("soundOnReflect")]
public SoundSpecifier? SoundOnReflect = new SoundPathSpecifier("/Audio/Weapons/Guns/Hits/laser_sear_wall.ogg");
/// <summary>
/// Is the deflection an innate power or something actively maintained? If true, this component grants a flat
/// deflection chance rather than a chance that degrades when moving/weightless/stunned/etc.
/// </summary>
[DataField]
public bool Innate = false;
/// <summary>
/// Maximum probability for a projectile to be reflected.
/// </summary>
[DataField("reflectProb"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public float ReflectProb = 0.25f;
/// <summary>
/// The maximum velocity a wielder can move at before losing effectiveness.
/// </summary>
[DataField]
public float VelocityBeforeNotMaxProb = 2.5f; // Walking speed for a human. Suitable for a weightless deflector like an e-sword.
/// <summary>
/// The velocity a wielder has to be moving at to use the minimum effectiveness value.
/// </summary>
[DataField]
public float VelocityBeforeMinProb = 4.5f; // Sprinting speed for a human. Suitable for a weightless deflector like an e-sword.
/// <summary>
/// Minimum probability for a projectile to be reflected.
/// </summary>
[DataField]
public float MinReflectProb = 0.1f;
}
[Flags]

View File

@@ -1,17 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.Audio;
using Content.Shared.Damage.Components;
using Content.Shared.Database;
using Content.Shared.Gravity;
using Content.Shared.Hands;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Popups;
using Content.Shared.Projectiles;
using Content.Shared.Standing;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Physics.Components;
@@ -35,6 +38,9 @@ public sealed class ReflectSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedGravitySystem _gravity = default!;
[Dependency] private readonly StandingStateSystem _standing = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
public override void Initialize()
{
@@ -91,15 +97,20 @@ public sealed class ReflectSystem : EntitySystem
private bool TryReflectProjectile(EntityUid user, EntityUid reflector, EntityUid projectile, ProjectileComponent? projectileComp = null, ReflectComponent? reflect = null)
{
if (!Resolve(reflector, ref reflect, false) ||
// Do we have the components needed to try a reflect at all?
if (
!Resolve(reflector, ref reflect, false) ||
!reflect.Enabled ||
!TryComp<ReflectiveComponent>(projectile, out var reflective) ||
(reflect.Reflects & reflective.Reflective) == 0x0 ||
!_random.Prob(reflect.ReflectProb) ||
!TryComp<PhysicsComponent>(projectile, out var physics))
{
!TryComp<PhysicsComponent>(projectile, out var physics) ||
TryComp<StaminaComponent>(reflector, out var staminaComponent) && staminaComponent.Critical ||
_standing.IsDown(reflector)
)
return false;
if (!_random.Prob(CalcReflectChance(reflector, reflect)))
return false;
}
var rotation = _random.NextAngle(-reflect.Spread / 2, reflect.Spread / 2).Opposite();
var existingVelocity = _physics.GetMapLinearVelocity(projectile, component: physics);
@@ -137,6 +148,34 @@ public sealed class ReflectSystem : EntitySystem
return true;
}
private float CalcReflectChance(EntityUid reflector, ReflectComponent reflect)
{
/*
* The rules of deflection are as follows:
* If you innately reflect things via magic, biology etc., you always have a full chance.
* If you are standing up and standing still, you're prepared to deflect and have full chance.
* If you have velocity, your deflection chance depends on your velocity, clamped.
* If you are floating, your chance is the minimum value possible.
* You cannot deflect if you are knocked down or stunned.
*/
if (reflect.Innate)
return reflect.ReflectProb;
if (_gravity.IsWeightless(reflector))
return reflect.MinReflectProb;
if (!TryComp<PhysicsComponent>(reflector, out var reflectorPhysics))
return reflect.ReflectProb;
return MathHelper.Lerp(
reflect.MinReflectProb,
reflect.ReflectProb,
// Inverse progression between velocities fed in as progression between probabilities. We go high -> low so the output here needs to be _inverted_.
1 - Math.Clamp((reflectorPhysics.LinearVelocity.Length() - reflect.VelocityBeforeNotMaxProb) / (reflect.VelocityBeforeMinProb - reflect.VelocityBeforeNotMaxProb), 0, 1)
);
}
private void OnReflectHitscan(EntityUid uid, ReflectComponent component, ref HitScanReflectAttemptEvent args)
{
if (args.Reflected ||
@@ -162,7 +201,14 @@ public sealed class ReflectSystem : EntitySystem
{
if (!TryComp<ReflectComponent>(reflector, out var reflect) ||
!reflect.Enabled ||
!_random.Prob(reflect.ReflectProb))
TryComp<StaminaComponent>(reflector, out var staminaComponent) && staminaComponent.Critical ||
_standing.IsDown(reflector))
{
newDirection = null;
return false;
}
if (!_random.Prob(CalcReflectChance(reflector, reflect)))
{
newDirection = null;
return false;
@@ -191,6 +237,9 @@ public sealed class ReflectSystem : EntitySystem
return;
EnsureComp<ReflectUserComponent>(args.Equipee);
if (component.Enabled)
EnableAlert(args.Equipee);
}
private void OnReflectUnequipped(EntityUid uid, ReflectComponent comp, GotUnequippedEvent args)
@@ -204,6 +253,9 @@ public sealed class ReflectSystem : EntitySystem
return;
EnsureComp<ReflectUserComponent>(args.User);
if (component.Enabled)
EnableAlert(args.User);
}
private void OnReflectHandUnequipped(EntityUid uid, ReflectComponent component, GotUnequippedHandEvent args)
@@ -215,6 +267,11 @@ public sealed class ReflectSystem : EntitySystem
{
comp.Enabled = args.Activated;
Dirty(uid, comp);
if (comp.Enabled)
EnableAlert(uid);
else
DisableAlert(uid);
}
/// <summary>
@@ -228,9 +285,22 @@ public sealed class ReflectSystem : EntitySystem
continue;
EnsureComp<ReflectUserComponent>(user);
EnableAlert(user);
return;
}
RemCompDeferred<ReflectUserComponent>(user);
DisableAlert(user);
}
private void EnableAlert(EntityUid alertee)
{
_alerts.ShowAlert(alertee, AlertType.Deflecting);
}
private void DisableAlert(EntityUid alertee)
{
_alerts.ClearAlert(alertee, AlertType.Deflecting);
}
}

View File

@@ -0,0 +1,9 @@
- files: ["tension_session.ogg"]
license: "CC-BY-3.0"
copyright: "Created by qwertyquerty"
source: "https://www.youtube.com/@qwertyquerty"
- files: ["deadline.ogg"]
license: "CC-BY-4.0"
copyright: "Bolgarich"
source: "https://www.youtube.com/watch?v=q7_NFEeeEac"

Binary file not shown.

View File

@@ -1,237 +1,4 @@
Entries:
- author: MACMAN2003
changes:
- message: You can now make LED light tubes and LED light bulbs at an autolathe.
type: Add
id: 6028
time: '2024-02-26T22:58:01.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25616
- author: botanySupremist
changes:
- message: Fixed a few small typos in ghost role information.
type: Fix
id: 6029
time: '2024-02-26T22:58:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25613
- author: Whisper
changes:
- message: Juice that makes you Weh can be found in; anomalies, Artifacts, random
puddles.
type: Add
id: 6030
time: '2024-02-26T22:59:15.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25611
- author: Hanzdegloker
changes:
- message: ERT medic PDA now has a health analyzer built in.
type: Tweak
id: 6031
time: '2024-02-26T23:00:51.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25606
- author: Ilya246
changes:
- message: Macrobomb implant price reduced 20->13 TC.
type: Tweak
id: 6032
time: '2024-02-26T23:04:26.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25585
- author: QuietlyWhisper
changes:
- message: Handcuff and handcuff replacements are faster to escape. You can allow
your teammates to uncuff themselves!
type: Tweak
id: 6033
time: '2024-02-26T23:05:16.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25601
- author: PoorMansDreams
changes:
- message: New lobby art (of horror)
type: Add
id: 6034
time: '2024-02-26T23:06:09.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25455
- author: TheShuEd
changes:
- message: Carpets can now be printed at the uniform printer.
type: Add
- message: Carpets can be collected from the floor and buthered into fabric.
type: Add
- message: New white and cyan carpets.
type: Add
- message: Carpets can now be placed on wooden tables to get beautiful new tables
with tablecloths.
type: Add
id: 6035
time: '2024-02-26T23:11:20.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25562
- author: SlamBamActionman
changes:
- message: Added new noses for humans and dwarves.
type: Add
id: 6036
time: '2024-02-26T23:15:04.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25557
- author: MACMAN2003
changes:
- message: Added a NanoTrasen balloon toy.
type: Add
id: 6037
time: '2024-02-26T23:15:53.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25543
- author: TheShuEd
changes:
- message: Added a new T2 industrial technology "Space Scanning"! It includes a
mass scanner console board, a Borg GPS module, and a new handheld mass scanner.
type: Add
- message: GPS borg module now have handheld mass scanner.
type: Add
id: 6038
time: '2024-02-26T23:19:51.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25526
- author: Sphiral
changes:
- message: NT has finally perfected methods for infusing Plasma/Uranium into Glass!
Windoors now have such varients! And Uranium Slim Windows exist!
type: Add
id: 6039
time: '2024-02-26T23:25:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25501
- author: Whisper
changes:
- message: Bleed wounds will no longer heal so fast they do not need to be treated.
Please see the medical guidebook.
type: Tweak
- message: Decreased passive bleed wound clotting by 60% (now 0.33u per 3 seconds).
type: Tweak
- message: Decreased all bloodloss damage (as a result of bleeding) by 50%.
type: Tweak
- message: Health analyzers will report if your patient is bleeding.
type: Add
id: 6040
time: '2024-02-26T23:26:46.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25434
- author: Velcroboy
changes:
- message: Tweaked wallmounted objects can not be interacted with through thindows
or windoors.
type: Tweak
- message: Fixed NanoMed vendor not being accessible from all directions.
type: Fix
id: 6041
time: '2024-02-26T23:28:55.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25451
- author: Lank
changes:
- message: Diona can no longer split or reform to cure themselves of a zombie infection.
type: Fix
id: 6042
time: '2024-02-26T23:31:37.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25404
- author: Zadeon
changes:
- message: Added salt as a new type of ore that can be mined from asteroids. Salt
can be ground up for its base components.
type: Add
id: 6043
time: '2024-02-26T23:34:15.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25324
- author: juliangiebel
changes:
- message: The mass media consoles UI got overhauled
type: Tweak
- message: Added PDA notifications in chat for new news articles. They can be turned
off in the news reader program and are only visible to you.
type: Add
id: 6044
time: '2024-02-27T01:38:00.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/19610
- author: rosieposieeee
changes:
- message: Added lockable wall buttons and decorative frames for differentiating
wall buttons.
type: Add
id: 6045
time: '2024-02-27T07:57:17.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25631
- author: metalgearsloth
changes:
- message: Fix chat bubbles.
type: Fix
id: 6046
time: '2024-02-27T13:01:24.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25643
- author: rosieposieeee
changes:
- message: Added grey stalagmites and made all stalagmites weaker and more fun to
break.
type: Add
id: 6047
time: '2024-02-27T19:24:17.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25646
- author: metalgearsloth
changes:
- message: Made NPC movement less janky.
type: Fix
id: 6048
time: '2024-02-28T06:41:15.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25666
- author: musicmanvr
changes:
- message: Added the Syndicate decoy bundle, a creative way to trick your foes.
type: Add
- message: Snap pops have been added to the toy crate.
type: Add
- message: Syndicate Commanders now have a trench whistle, Avanti!
type: Add
id: 6049
time: '2024-02-28T21:53:46.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25333
- author: Whisper
changes:
- message: Iron and copper no longer grant infinite blood!
type: Fix
id: 6050
time: '2024-02-28T21:56:22.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25678
- author: Cojoke-dot
changes:
- message: E-sword now lights plasma fires
type: Tweak
id: 6051
time: '2024-02-28T21:59:35.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25665
- author: lzk228
changes:
- message: Engineering structures orders in cargo now supplied via flatpacks (e.g.
tesla & singulo generators, other stuff for them).
type: Tweak
- message: Thrusters and gyroscopes now supplied in crates via flatpacks.
type: Tweak
id: 6052
time: '2024-02-28T22:02:02.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25647
- author: c4llv07e
changes:
- message: Cryo won't remove you from the new body if you chose a new ghost role
before cryo deleted your old body.
type: Fix
id: 6053
time: '2024-02-28T22:09:02.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/24991
- author: Nairodian
changes:
- message: You can now remove the rust from reinforced walls with a welder.
type: Fix
id: 6054
time: '2024-02-29T00:44:01.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25690
- author: Ubaser
changes:
- message: Eris UI theme has been removed.
type: Remove
id: 6055
time: '2024-02-29T05:53:48.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25673
- author: Rainfey
changes:
- message: Disallow multiple antag roles per player
@@ -3874,3 +3641,213 @@
id: 6527
time: '2024-05-04T11:11:46.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27618
- author: Tyzemol
changes:
- message: Fixed NPCs being able to see and attack you hiding in closed lockers
and crates
type: Fix
id: 6528
time: '2024-05-04T19:57:59.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27677
- author: Blackern5000
changes:
- message: Security belts can now hold more items that security commonly uses.
type: Tweak
id: 6529
time: '2024-05-04T20:00:22.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27674
- author: Blackern5000
changes:
- message: Floodlights now use medium power cells instead of small ones.
type: Tweak
id: 6530
time: '2024-05-04T20:06:16.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27672
- author: lzk228
changes:
- message: Grey whistle added to arcade machine prizes.
type: Add
id: 6531
time: '2024-05-05T14:42:35.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27676
- author: Killerqu00
changes:
- message: Welding masks can now be put on utility belts.
type: Tweak
id: 6532
time: '2024-05-05T23:30:06.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27694
- author: Dutch-VanDerLinde
changes:
- message: Chemical analysis goggles now show the scanned solution's temperature.
type: Tweak
id: 6533
time: '2024-05-05T23:32:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27693
- author: Allen
changes:
- message: Added new icons for the emote wheel
type: Add
id: 6534
time: '2024-05-06T01:12:30.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27541
- author: DogZeroX
changes:
- message: 'Added a new salvage song: Deadline by Bolgarich'
type: Add
id: 6535
time: '2024-05-06T03:51:33.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27707
- author: '0x6273'
changes:
- message: Shuttle docks now have a "dock status" device link port
type: Add
id: 6536
time: '2024-05-06T03:59:01.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27646
- author: DogZeroX
changes:
- message: 'Added new lobby art: Just a week away by Fern'
type: Add
id: 6537
time: '2024-05-06T08:17:35.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27717
- author: mirrorcult
changes:
- message: Event weights have been tweaked across the board, and some events removed
(such as false alarm) to hopefully allow for a more interesting and varied slate
of random occurrences per round.
type: Add
id: 6538
time: '2024-05-06T10:10:12.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27721
- author: mirrorcult
changes:
- message: Tile weighting for many events and variation rules (messes, dragons,
revenants, kudzu, gasleak, rods, etc) has been fixed. They will now no longer
spawn disproportionately often on smaller grids.
type: Add
id: 6539
time: '2024-05-06T11:25:11.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27724
- author: Blackern5000
changes:
- message: Burn damage is now half as effective at cauterizing wounds as it was
previously
type: Tweak
id: 6540
time: '2024-05-06T20:19:57.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27726
- author: Blackern5000
changes:
- message: Active lights now deal significantly less burn damage when touched.
type: Tweak
id: 6541
time: '2024-05-06T20:20:19.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27728
- author: pissdemon
changes:
- message: Special ghost roles are now raffled, instead of being given out to the
first player requesting them.
type: Add
id: 6542
time: '2024-05-07T01:48:16.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26629
- author: mirrorcult
changes:
- message: Less ratkings should spawn during the mouse migration event now
type: Tweak
id: 6543
time: '2024-05-07T03:23:50.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27760
- author: PJB3005
changes:
- message: Fixed preferences not loading sometimes if you had a loadout item with
playtime requirement selected in a character profile.
type: Fix
id: 6544
time: '2024-05-07T04:21:03.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27742
- author: lzk228
changes:
- message: Morgue now can be anchored and unanchored.
type: Tweak
- message: Changed morgue's collision layer, so items now can go through it.
type: Tweak
id: 6545
time: '2024-05-07T07:26:33.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27750
- author: mirrorcult
changes:
- message: Events that run now get logged to admin alert chat
type: Tweak
id: 6546
time: '2024-05-07T15:13:29.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27761
- author: cooldolphin
changes:
- message: Cigarettes in the loadout menu.
type: Add
id: 6547
time: '2024-05-07T15:55:03.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27292
- author: FairlySadPanda
changes:
- message: Weapons that deflect shots, like the e-sword, now are most effective
at doing so when standing still, with moving or sprinting making the deflection
chance worse.
type: Tweak
id: 6548
time: '2024-05-07T18:14:59.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27219
- author: deltanedas
changes:
- message: Lathes can now be sped up by using Space Lube.
type: Tweak
id: 6549
time: '2024-05-07T18:20:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25515
- author: OnsenCapy
changes:
- message: Combat gloves now have their own unique sprite.
type: Tweak
- message: Combat gloves now have heat resistance to match their fireproof description.
type: Tweak
id: 6550
time: '2024-05-08T00:19:55.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27373
- author: deltanedas
changes:
- message: Dragons can now breathe fire.
type: Add
id: 6551
time: '2024-05-08T00:25:41.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26746
- author: Cojoke-dot
changes:
- message: You can now shoot over racks
type: Tweak
id: 6552
time: '2024-05-08T06:46:04.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27797
- author: ShadowCommander
changes:
- message: Lizards now stop tail pulling when they are downed.
type: Fix
id: 6553
time: '2024-05-08T06:49:28.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27796
- author: Plykiya
changes:
- message: Re-added supplybots.
type: Add
id: 6554
time: '2024-05-08T07:30:04.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27769
- author: Hobbitmax
changes:
- message: Replaced Syndicate Jaws Of Life with a Pyjama bundle on the Train map.
type: Tweak
id: 6555
time: '2024-05-08T07:30:53.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27734

File diff suppressed because one or more lines are too long

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