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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -119,6 +119,7 @@ namespace Content.Client.Entry
|
||||
_prototypeManager.RegisterIgnore("wireLayout");
|
||||
_prototypeManager.RegisterIgnore("alertLevels");
|
||||
_prototypeManager.RegisterIgnore("nukeopsRole");
|
||||
_prototypeManager.RegisterIgnore("ghostRoleRaffleDecider");
|
||||
|
||||
_componentFactory.GenerateNetIds();
|
||||
_adminManager.Initialize();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'}"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
127
Content.Server/Ghost/Roles/MakeRaffledGhostRoleCommand.cs
Normal file
127
Content.Server/Ghost/Roles/MakeRaffledGhostRoleCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Content.Server/Ghost/Roles/Raffles/GhostRoleRaffleConfig.cs
Normal file
35
Content.Server/Ghost/Roles/Raffles/GhostRoleRaffleConfig.cs
Normal 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";
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,8 @@ namespace Content.Shared.Alert
|
||||
SuitPower,
|
||||
BorgHealth,
|
||||
BorgCrit,
|
||||
BorgDead
|
||||
BorgDead,
|
||||
Deflecting
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
34
Content.Shared/ReagentSpeed/ReagentSpeedComponent.cs
Normal file
34
Content.Shared/ReagentSpeed/ReagentSpeedComponent.cs
Normal 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();
|
||||
}
|
||||
33
Content.Shared/ReagentSpeed/ReagentSpeedSystem.cs
Normal file
33
Content.Shared/ReagentSpeed/ReagentSpeedSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
41
Content.Shared/Weapons/Ranged/Systems/ActionGunSystem.cs
Normal file
41
Content.Shared/Weapons/Ranged/Systems/ActionGunSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
9
Resources/Audio/Expedition/attributions.yml
Normal file
9
Resources/Audio/Expedition/attributions.yml
Normal 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"
|
||||
BIN
Resources/Audio/Expedition/deadline.ogg
Normal file
BIN
Resources/Audio/Expedition/deadline.ogg
Normal file
Binary file not shown.
@@ -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
Reference in New Issue
Block a user