Merge remote-tracking branch 'upstream/master' into upstream-sync

# Conflicts:
#	Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml
This commit is contained in:
Arthur 2022-03-16 21:50:36 +03:00
commit 336be19a71
237 changed files with 119229 additions and 95575 deletions

View file

@ -5,16 +5,5 @@ namespace Content.Client.Clothing
{
public sealed class MagbootsSystem : SharedMagbootsSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MagbootsComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
}
private void OnRefreshMovespeed(EntityUid uid, MagbootsComponent component, RefreshMovementSpeedModifiersEvent args)
{
args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier);
}
}
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
@ -14,7 +14,7 @@
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="Nett" Version="0.15.0" />
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />

View file

@ -0,0 +1,29 @@
using Robust.Client.GameObjects;
using Content.Shared.Disease;
namespace Content.Client.Disease
{
/// <summary>
/// Controls client-side visuals for the
/// disease machines.
/// </summary>
public sealed class DiseaseMachineSystem : VisualizerSystem<DiseaseMachineVisualsComponent>
{
protected override void OnAppearanceChange(EntityUid uid, DiseaseMachineVisualsComponent component, ref AppearanceChangeEvent args)
{
if (TryComp(uid, out SpriteComponent? sprite)
&& args.Component.TryGetData(DiseaseMachineVisuals.IsOn, out bool isOn)
&& args.Component.TryGetData(DiseaseMachineVisuals.IsRunning, out bool isRunning))
{
var state = isRunning ? component.RunningState : component.IdleState;
sprite.LayerSetVisible(DiseaseMachineVisualLayers.IsOn, isOn);
sprite.LayerSetState(DiseaseMachineVisualLayers.IsRunning, state);
}
}
}
}
public enum DiseaseMachineVisualLayers : byte
{
IsOn,
IsRunning
}

View file

@ -0,0 +1,15 @@
namespace Content.Client.Disease;
/// <summary>
/// Holds the idle and running state for machines to control
/// playing animtions on the client.
/// </summary>
[RegisterComponent]
public sealed class DiseaseMachineVisualsComponent : Component
{
[DataField("idleState", required: true)]
public string IdleState = default!;
[DataField("runningState", required: true)]
public string RunningState = default!;
}

View file

@ -37,6 +37,10 @@ namespace Content.Client.Entry
"Healing",
"Material",
"RandomAppearance",
"DiseaseProtection",
"DiseaseDiagnoser",
"DiseaseVaccine",
"DiseaseVaccineCreator",
"Mineable",
"RangedMagazine",
"Ammo",
@ -47,12 +51,15 @@ namespace Content.Client.Entry
"ResearchClient",
"IdCardConsole",
"ThermalRegulator",
"DiseaseMachineRunning",
"DiseaseMachine",
"AtmosFixMarker",
"CablePlacer",
"Drink",
"Food",
"DeployableBarrier",
"MagicMirror",
"DiseaseSwab",
"FloorTile",
"RandomInsulation",
"Electrified",
@ -62,9 +69,9 @@ namespace Content.Client.Entry
"Bloodstream",
"TransformableContainer",
"Mind",
"DiseaseCarrier",
"StorageFill",
"Mop",
"Bucket",
"Absorbent",
"CableVis",
"BatterySelfRecharger",
"Puddle",
@ -122,6 +129,7 @@ namespace Content.Client.Entry
"RCD",
"RCDAmmo",
"CursedEntityStorage",
"DiseaseArtifact",
"Radio",
"GasArtifact",
"SentienceTarget",

View file

@ -9,6 +9,7 @@ using Content.Shared.Station;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.State;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
@ -21,7 +22,6 @@ namespace Content.Client.GameTicking.Managers
public sealed class ClientGameTicker : SharedGameTicker
{
[Dependency] private readonly IStateManager _stateManager = default!;
[ViewVariables] private bool _initialized;
private Dictionary<StationId, Dictionary<string, int>> _jobsAvailable = new();
private Dictionary<StationId, string> _stationNames = new();
@ -29,6 +29,7 @@ namespace Content.Client.GameTicking.Managers
[ViewVariables] public bool AreWeReady { get; private set; }
[ViewVariables] public bool IsGameStarted { get; private set; }
[ViewVariables] public string? LobbySong { get; private set; }
[ViewVariables] public string? LobbyBackground { get; private set; }
[ViewVariables] public bool DisallowedLateJoin { get; private set; }
[ViewVariables] public string? ServerInfoBlob { get; private set; }
[ViewVariables] public TimeSpan StartTime { get; private set; }
@ -89,6 +90,7 @@ namespace Content.Client.GameTicking.Managers
IsGameStarted = message.IsRoundStarted;
AreWeReady = message.YouAreReady;
LobbySong = message.LobbySong;
LobbyBackground = message.LobbyBackground;
Paused = message.Paused;
if (IsGameStarted)
Status.Clear();

View file

@ -3,6 +3,7 @@ using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Disease.Components;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
using Content.Shared.Damage;
@ -35,7 +36,17 @@ namespace Content.Client.HealthAnalyzer.UI
text.Append($"{Loc.GetString("health-analyzer-window-entity-health-text", ("entityName", entityName))}\n");
text.Append($"{Loc.GetString("health-analyzer-window-entity-damage-total-text", ("amount", damageable.TotalDamage))}\n");
/// Status Effects / Components
if (entities.HasComponent<DiseasedComponent>(msg.TargetEntity))
{
text.Append($"{Loc.GetString("disease-scanner-diseased")}\n");
}else
{
text.Append($"{Loc.GetString("disease-scanner-not-diseased")}\n");
}
/// Damage
text.Append($"\n{Loc.GetString("health-analyzer-window-entity-damage-total-text", ("amount", damageable.TotalDamage))}\n");
HashSet<string> shownTypes = new();

View file

@ -0,0 +1,32 @@
using Content.Client.Changelog;
using Content.Client.Credits;
using Content.Client.Links;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Utility;
namespace Content.Client.Info
{
public sealed class DevInfoBanner : BoxContainer
{
public DevInfoBanner() {
var buttons = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal
};
AddChild(buttons);
var uriOpener = IoCManager.Resolve<IUriOpener>();
var reportButton = new Button {Text = Loc.GetString("server-info-report-button")};
reportButton.OnPressed += args => uriOpener.OpenUri(UILinks.BugReport);
var creditsButton = new Button {Text = Loc.GetString("server-info-credits-button")};
creditsButton.OnPressed += args => new CreditsWindow().Open();
buttons.AddChild(reportButton);
buttons.AddChild(creditsButton);
}
}
}

View file

@ -0,0 +1,43 @@
using Content.Client.Changelog;
using Content.Client.Credits;
using Content.Client.Links;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Utility;
namespace Content.Client.Info
{
public sealed class LinkBanner : BoxContainer
{
public LinkBanner()
{
var buttons = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal
};
AddChild(buttons);
var uriOpener = IoCManager.Resolve<IUriOpener>();
var rulesButton = new Button() {Text = Loc.GetString("server-info-rules-button")};
rulesButton.OnPressed += args => new RulesAndInfoWindow().Open();
var discordButton = new Button {Text = Loc.GetString("server-info-discord-button")};
discordButton.OnPressed += args => uriOpener.OpenUri(UILinks.Discord);
var websiteButton = new Button {Text = Loc.GetString("server-info-website-button")};
websiteButton.OnPressed += args => uriOpener.OpenUri(UILinks.Website);
var wikiButton = new Button {Text = Loc.GetString("server-info-wiki-button")};
wikiButton.OnPressed += args => uriOpener.OpenUri(UILinks.Wiki);
var changelogButton = new ChangelogButton();
buttons.AddChild(changelogButton);
buttons.AddChild(rulesButton);
buttons.AddChild(discordButton);
buttons.AddChild(websiteButton);
buttons.AddChild(wikiButton);
}
}
}

View file

@ -22,48 +22,7 @@ namespace Content.Client.Info
VerticalExpand = true
};
AddChild(_richTextLabel);
var buttons = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal
};
AddChild(buttons);
var uriOpener = IoCManager.Resolve<IUriOpener>();
var rulesButton = new Button() { Text = Loc.GetString("server-info-rules-button") };
rulesButton.OnPressed += args => new RulesAndInfoWindow().Open();
var discordButton = new Button {Text = Loc.GetString("server-info-discord-button") };
discordButton.OnPressed += args => uriOpener.OpenUri(UILinks.Discord);
var websiteButton = new Button {Text = Loc.GetString("server-info-website-button") };
websiteButton.OnPressed += args => uriOpener.OpenUri(UILinks.Website);
var wikiButton = new Button {Text = Loc.GetString("server-info-wiki-button") };
wikiButton.OnPressed += args => uriOpener.OpenUri(UILinks.Wiki);
var reportButton = new Button { Text = Loc.GetString("server-info-report-button") };
reportButton.OnPressed += args => uriOpener.OpenUri(UILinks.BugReport);
var creditsButton = new Button { Text = Loc.GetString("server-info-credits-button") };
creditsButton.OnPressed += args => new CreditsWindow().Open();
var changelogButton = new ChangelogButton
{
HorizontalExpand = true,
HorizontalAlignment = HAlignment.Right
};
buttons.AddChild(rulesButton);
buttons.AddChild(discordButton);
buttons.AddChild(websiteButton);
buttons.AddChild(wikiButton);
buttons.AddChild(reportButton);
buttons.AddChild(creditsButton);
buttons.AddChild(changelogButton);
}
public void SetInfoBlob(string markup)
{
_richTextLabel.SetMessage(FormattedMessage.FromMarkup(markup));

View file

@ -8,6 +8,7 @@ using Content.Client.LateJoin;
using Content.Client.Lobby.UI;
using Content.Client.Preferences;
using Content.Client.Preferences.UI;
using Content.Client.Resources;
using Content.Client.Voting;
using Content.Shared.GameTicking;
using Robust.Client;
@ -22,6 +23,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Client.Lobby
@ -104,12 +106,10 @@ namespace Content.Client.Lobby
_lobby.LeaveButton.OnPressed += _ => _consoleHost.ExecuteCommand("disconnect");
_lobby.OptionsButton.OnPressed += _ => new OptionsMenu().Open();
UpdatePlayerList();
_playerManager.PlayerListUpdated += PlayerManagerOnPlayerListUpdated;
_gameTicker.InfoBlobUpdated += UpdateLobbyUi;
_gameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
_gameTicker.LobbyReadyUpdated += LobbyReadyUpdated;
_gameTicker.LobbyLateJoinStatusUpdated += LobbyLateJoinStatusUpdated;
}
@ -118,7 +118,6 @@ namespace Content.Client.Lobby
_playerManager.PlayerListUpdated -= PlayerManagerOnPlayerListUpdated;
_gameTicker.InfoBlobUpdated -= UpdateLobbyUi;
_gameTicker.LobbyStatusUpdated -= LobbyStatusUpdated;
_gameTicker.LobbyReadyUpdated -= LobbyReadyUpdated;
_gameTicker.LobbyLateJoinStatusUpdated -= LobbyLateJoinStatusUpdated;
_lobby?.Dispose();
@ -176,14 +175,12 @@ namespace Content.Client.Lobby
}
}
UpdatePlayerList();
}
private void LobbyReadyUpdated() => UpdatePlayerList();
private void LobbyStatusUpdated()
{
UpdatePlayerList();
UpdateLobbyBackground();
UpdateLobbyUi();
}
@ -222,35 +219,18 @@ namespace Content.Client.Lobby
}
}
private void UpdatePlayerList()
private void UpdateLobbyBackground()
{
if (_lobby == null) return;
_lobby.OnlinePlayerList.Clear();
var gameTicker = EntitySystem.Get<ClientGameTicker>();
foreach (var session in _playerManager.Sessions.OrderBy(s => s.Name))
if (_gameTicker.LobbyBackground != null)
{
var readyState = string.Empty;
// Don't show ready state if we're ingame
if (!gameTicker.IsGameStarted)
{
LobbyPlayerStatus status;
if (session.UserId == _playerManager.LocalPlayer?.UserId)
status = gameTicker.AreWeReady ? LobbyPlayerStatus.Ready : LobbyPlayerStatus.NotReady;
else
gameTicker.Status.TryGetValue(session.UserId, out status);
readyState = status switch
{
LobbyPlayerStatus.NotReady => Loc.GetString("lobby-state-player-status-not-ready"),
LobbyPlayerStatus.Ready => Loc.GetString("lobby-state-player-status-ready"),
LobbyPlayerStatus.Observer => Loc.GetString("lobby-state-player-status-observer"),
_ => string.Empty,
};
}
_lobby.OnlinePlayerList.AddItem(session.Name, readyState);
_lobby.Background.Texture = _resourceCache.GetResource<TextureResource>(_gameTicker.LobbyBackground );
}
else
{
_lobby.Background.Texture = null;
}
}
private void SetReady(bool newReady)
@ -261,7 +241,6 @@ namespace Content.Client.Lobby
}
_consoleHost.ExecuteCommand($"toggleready {newReady}");
UpdatePlayerList();
}
}
}

View file

@ -2,6 +2,7 @@ using System.Linq;
using Content.Client.HUD.UI;
using Content.Client.Inventory;
using Content.Client.Preferences;
using Content.Client.UserInterface.Controls;
using Content.Shared.CharacterAppearance.Systems;
using Content.Shared.GameTicking;
using Content.Shared.Inventory;
@ -23,23 +24,20 @@ namespace Content.Client.Lobby.UI
{
public sealed class LobbyCharacterPreviewPanel : Control
{
private readonly IEntityManager _entMan;
private readonly IClientPreferencesManager _preferencesManager;
private readonly IPrototypeManager _prototypeManager;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private EntityUid? _previewDummy;
private readonly Label _summaryLabel;
private readonly BoxContainer _loaded;
private readonly BoxContainer _viewBox;
private readonly Label _unloaded;
public LobbyCharacterPreviewPanel(IEntityManager entityManager,
IClientPreferencesManager preferencesManager,
IPrototypeManager prototypeManager)
public LobbyCharacterPreviewPanel()
{
_entMan = entityManager;
_preferencesManager = preferencesManager;
_prototypeManager = prototypeManager;
IoCManager.InjectDependencies(this);
var header = new NanoHeading
{
Text = Loc.GetString("lobby-character-preview-panel-header")
@ -57,9 +55,6 @@ namespace Content.Client.Lobby.UI
{
Orientation = LayoutOrientation.Vertical
};
vBox.AddChild(header);
_unloaded = new Label { Text = Loc.GetString("lobby-character-preview-panel-unloaded-preferences-label") };
_loaded = new BoxContainer
@ -67,17 +62,18 @@ namespace Content.Client.Lobby.UI
Orientation = LayoutOrientation.Vertical,
Visible = false
};
_loaded.AddChild(CharacterSetupButton);
_loaded.AddChild(_summaryLabel);
_viewBox = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal
};
var _vSpacer = new VSpacer();
_loaded.AddChild(_summaryLabel);
_loaded.AddChild(_viewBox);
_loaded.AddChild(_vSpacer);
_loaded.AddChild(CharacterSetupButton);
vBox.AddChild(header);
vBox.AddChild(_loaded);
vBox.AddChild(_unloaded);
AddChild(vBox);
@ -95,7 +91,7 @@ namespace Content.Client.Lobby.UI
_preferencesManager.OnServerDataLoaded -= UpdateUI;
if (!disposing) return;
if (_previewDummy != null) _entMan.DeleteEntity(_previewDummy.Value);
if (_previewDummy != null) _entityManager.DeleteEntity(_previewDummy.Value);
_previewDummy = default;
}
@ -103,7 +99,7 @@ namespace Content.Client.Lobby.UI
{
return new()
{
Sprite = _entMan.GetComponent<ISpriteComponent>(entity),
Sprite = _entityManager.GetComponent<ISpriteComponent>(entity),
OverrideDirection = direction,
Scale = (2, 2)
};
@ -126,7 +122,7 @@ namespace Content.Client.Lobby.UI
}
else
{
_previewDummy = _entMan.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace);
_previewDummy = _entityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace);
var viewSouth = MakeSpriteView(_previewDummy.Value, Direction.South);
var viewNorth = MakeSpriteView(_previewDummy.Value, Direction.North);
var viewWest = MakeSpriteView(_previewDummy.Value, Direction.West);

View file

@ -1,10 +1,10 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:maths="clr-namespace:Robust.Shared.Maths;assembly=Robust.Shared.Maths"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:parallax="clr-namespace:Content.Client.Parallax"
xmlns:ui="clr-namespace:Content.Client.Voting.UI"
xmlns:vote="clr-namespace:Content.Client.Voting.UI"
xmlns:style="clr-namespace:Content.Client.Stylesheets"
xmlns:hudUi="clr-namespace:Content.Client.HUD.UI"
xmlns:chatUi="clr-namespace:Content.Client.Chat.UI"
@ -13,29 +13,12 @@
<Control>
<!-- Parallax background -->
<parallax:ParallaxControl />
<Control Margin="20 20 20 20">
<PanelContainer StyleClasses="AngleRect" />
<BoxContainer Orientation="Vertical">
<!-- Top row -->
<BoxContainer Orientation="Horizontal" MinSize="0 40">
<Label Margin="8 0 0 0" StyleClasses="LabelHeadingBigger" VAlign="Center" Text="{Loc 'ui-lobby-title'}" />
<Label Name="ServerName" Access="Public" StyleClasses="LabelHeadingBigger" VAlign="Center" />
<ui:VoteCallMenuButton Name="CallVoteButton" StyleClasses="ButtonBig" />
<Button Name="OptionsButton" Access="Public" StyleClasses="ButtonBig" Text="{Loc 'ui-lobby-options-button'}" />
<Button Name="LeaveButton" Access="Public" StyleClasses="ButtonBig" Text="{Loc 'ui-lobby-leave-button'}" />
</BoxContainer>
<!-- Gold line -->
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}"
ContentMarginTopOverride="2" />
</PanelContainer.PanelOverride>
</PanelContainer>
<!-- Middle section with the two vertical panels -->
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
<!-- Left panel -->
<BoxContainer Orientation="Vertical" Name="LeftPanelContainer" HorizontalExpand="True">
<TextureRect Access="Public" Name = "Background" Stretch="Scale"/>
<Control Margin="10 10 10 10" >
<!-- Left Top Panel -->
<PanelContainer StyleClasses="AngleRect" HorizontalAlignment="Left" Name = "LeftSideTop" VerticalExpand="True" VerticalAlignment="Top" >
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center" MaxWidth="620">
<info:LinkBanner Name="LinkBanner" VerticalExpand="false" HorizontalAlignment="Center" Margin="3 3 3 3"/>
<hudUi:StripeBack>
<BoxContainer Orientation="Horizontal" SeparationOverride="6" Margin="3 3 3 3">
<cc:UICommandButton Command="observe" Name="ObserveButton" Access="Public" Text="{Loc 'ui-lobby-observe-button'}" StyleClasses="ButtonBig" WindowType="{x:Type lobbyUi:ObserveWarningWindow}"/>
@ -46,41 +29,45 @@
StyleClasses="LabelBig" HorizontalExpand="True" />
<Button Name="ReadyButton" Access="Public" ToggleMode="True" Text="{Loc 'ui-lobby-ready-up-button'}"
StyleClasses="ButtonBig" />
</BoxContainer>
</hudUi:StripeBack>
<chatUi:ChatBox Name="Chat" Access="Public" VerticalExpand="True" Margin="3 3 3 3"/>
</BoxContainer></hudUi:StripeBack>
</BoxContainer>
<!-- Gold line -->
<PanelContainer MinSize="2 0">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}" />
</PanelContainer.PanelOverride>
</PanelContainer>
<!-- Right panel -->
<Control HorizontalExpand="True">
<BoxContainer Orientation="Vertical">
<!-- Player list -->
<hudUi:NanoHeading Text="{Loc 'ui-lobby-online-players-block'}" />
<lobbyUi:LobbyPlayerList Name="OnlinePlayerList"
Access="Public"
HorizontalExpand="True"
VerticalExpand="True"
Margin="3 3 3 3" />
<!-- Server info -->
<hudUi:NanoHeading Text="{Loc 'ui-lobby-server-info-block'}" />
<info:ServerInfo Name="ServerInfo"
Access="Public"
VerticalExpand="True"
Margin="3 3 3 3"/>
</BoxContainer>
<BoxContainer Orientation="Vertical"
Name="VoteContainer"
Access="Public"
HorizontalAlignment="Right"
Margin="0 8 8 0" />
</Control>
</BoxContainer>
</PanelContainer>
<!-- Left Bot Panel -->
<BoxContainer Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Bottom" MaxWidth="620">
<info:DevInfoBanner Name="DevInfoBanner" VerticalExpand="false" Margin="3 3 3 3"/>
</BoxContainer>
<!-- RightPanel Panel -->
<PanelContainer Name="RightSide" StyleClasses="AngleRect" HorizontalAlignment= "Right" VerticalExpand="True" VerticalAlignment="Stretch">
<BoxContainer Orientation="Vertical">
<!-- Top row -->
<BoxContainer Orientation="Horizontal" MinSize="0 40" Name="HeaderContainer" Access="Public">
<Label Margin="8 0 0 0" StyleClasses="LabelHeadingBigger" VAlign="Center" Text="{Loc 'ui-lobby-title'}" />
<Label Name="ServerName" Access="Public" StyleClasses="LabelHeadingBigger" VAlign="Center" />
</BoxContainer>
<!-- Gold line -->
<ui:HLine Color="{x:Static style:StyleNano.NanoGold}" Thickness="2"/>
<ui:HSpacer Spacing="10"/>
<!-- Voting bar -->
<BoxContainer Orientation="Horizontal" MinSize="0 40" Name="VoteContainer" Access="Public" HorizontalAlignment="Right">
<cc:CommandButton Name = "AHelpButton" Command="openahelp" Access="Public" Text = "{Loc 'ui-lobby-ahelp-button'}" StyleClasses="ButtonBig"/>
<vote:VoteCallMenuButton Name="CallVoteButton" StyleClasses="ButtonBig" />
<Button Name="OptionsButton" Access="Public" StyleClasses="ButtonBig" Text="{Loc 'ui-lobby-options-button'}" />
<Button Name="LeaveButton" Access="Public" StyleClasses="ButtonBig" Text="{Loc 'ui-lobby-leave-button'}" />
</BoxContainer>
<ui:HSpacer Spacing="10"/>
<!-- Server info -->
<hudUi:NanoHeading Text="{Loc 'ui-lobby-server-info-block'}" />
<info:ServerInfo Name="ServerInfo" Access="Public" MinSize="0 30" VerticalExpand="false" Margin="3 3 3 3" MaxWidth="400"/>
<ui:HSpacer Spacing="5"/>
<lobbyUi:LobbyCharacterPreviewPanel Name="CharacterPreview" Access="Public"></lobbyUi:LobbyCharacterPreviewPanel>
<ui:HSpacer Spacing="5"/>
<BoxContainer MinHeight="10"/>
<!-- Gold line -->
<ui:HLine Color="{x:Static style:StyleNano.NanoGold}" Thickness="2" Access="Public"/>
<ui:HSpacer Spacing="10"/>
<chatUi:ChatBox Name="Chat" Access="Public" VerticalExpand="True" Margin="3 3 3 3" MinHeight="50"/>
</BoxContainer>
</PanelContainer>
</Control>
</Control>
</Control>

View file

@ -17,108 +17,12 @@ namespace Content.Client.Lobby.UI
[GenerateTypedNameReferences]
internal sealed partial class LobbyGui : Control
{
public LobbyCharacterPreviewPanel CharacterPreview { get; }
public LobbyGui(IEntityManager entityManager,
IClientPreferencesManager preferencesManager)
{
RobustXamlLoader.Load(this);
ServerName.HorizontalExpand = true;
ServerName.HorizontalAlignment = HAlignment.Center;
CharacterPreview = new LobbyCharacterPreviewPanel(
entityManager,
preferencesManager,
IoCManager.Resolve<IPrototypeManager>())
{
HorizontalAlignment = HAlignment.Left
};
LeftPanelContainer.AddChild(CharacterPreview);
CharacterPreview.SetPositionFirst();
}
}
public sealed class LobbyPlayerList : Control
{
private readonly ScrollContainer _scroll;
private readonly BoxContainer _vBox;
public LobbyPlayerList()
{
var panel = new PanelContainer()
{
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#202028") },
};
_vBox = new BoxContainer
{
Orientation = LayoutOrientation.Vertical
};
_scroll = new ScrollContainer();
_scroll.AddChild(_vBox);
panel.AddChild(_scroll);
AddChild(panel);
}
// Adds a row
public void AddItem(string name, string status)
{
var hbox = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
};
// Player Name
hbox.AddChild(new PanelContainer()
{
PanelOverride = new StyleBoxFlat
{
BackgroundColor = Color.FromHex("#373744"),
ContentMarginBottomOverride = 2,
ContentMarginLeftOverride = 4,
ContentMarginRightOverride = 4,
ContentMarginTopOverride = 2
},
Children =
{
new Label
{
Text = name
}
},
HorizontalExpand = true
});
// Status
hbox.AddChild(new PanelContainer()
{
PanelOverride = new StyleBoxFlat
{
BackgroundColor = Color.FromHex("#373744"),
ContentMarginBottomOverride = 2,
ContentMarginLeftOverride = 4,
ContentMarginRightOverride = 4,
ContentMarginTopOverride = 2
},
Children =
{
new Label
{
Text = status
}
},
HorizontalExpand = true,
SizeFlagsStretchRatio = 0.2f,
});
_vBox.AddChild(hbox);
}
// Deletes all rows
public void Clear()
{
_vBox.RemoveAllChildren();
}
}
}

View file

@ -0,0 +1,46 @@
using Content.Client.Stylesheets;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
namespace Content.Client.UserInterface.Controls;
public sealed class HLine : Container
{
public Color? Color
{
get
{
if (_line.PanelOverride is StyleBoxFlat styleBox) return styleBox.BackgroundColor;
return null;
}
set
{
if (_line.PanelOverride is StyleBoxFlat styleBox) styleBox.BackgroundColor = value!.Value;
}
}
public float? Thickness {
get
{
if (_line.PanelOverride is StyleBoxFlat styleBox) return styleBox.ContentMarginTopOverride;
return null;
}
set
{
if (_line.PanelOverride is StyleBoxFlat styleBox) styleBox.ContentMarginTopOverride = value!.Value;
}
}
private readonly PanelContainer _line;
public HLine()
{
_line = new PanelContainer();
_line.PanelOverride = new StyleBoxFlat();
_line.PanelOverride.ContentMarginTopOverride = Thickness;
AddChild(_line);
}
}

View file

@ -0,0 +1,21 @@
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
using TerraFX.Interop.Windows;
namespace Content.Client.UserInterface.Controls;
public sealed class HSpacer : Control
{
public float Spacing { get => MinHeight; set => MinHeight = value; }
public HSpacer()
{
MinHeight = Spacing;
}
public HSpacer(float height = 5)
{
Spacing = height;
MinHeight = height;
}
}

View file

@ -0,0 +1,20 @@
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
namespace Content.Client.UserInterface.Controls;
public sealed class VSpacer : Control
{
public float Spacing{ get => MinWidth; set => MinWidth = value; }
public VSpacer()
{
MinWidth = Spacing;
}
public VSpacer(float width = 5)
{
Spacing = width;
MinWidth = width;
}
}

View file

@ -10,9 +10,9 @@
</PropertyGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />

View file

@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Content.Server.Administration.Logs;
@ -10,6 +8,9 @@ using Content.Server.MoMMI;
using Content.Server.Players;
using Content.Server.Preferences.Managers;
using Content.Server.Radio.EntitySystems;
using Content.Server.Disease;
using Content.Server.Disease.Components;
using Content.Shared.Disease.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration;
using Content.Shared.CCVar;
@ -22,10 +23,6 @@ using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
@ -208,6 +205,11 @@ namespace Content.Server.Chat.Managers
return;
}
if (_entManager.HasComponent<DiseasedComponent>(source) && _entManager.TryGetComponent<DiseaseCarrierComponent>(source,out var carrier))
{
EntitySystem.Get<DiseaseSystem>().SneezeCough(source, _random.Pick(carrier.Diseases), string.Empty);
}
if (MessageCharacterLimit(source, message))
{
return;

View file

@ -0,0 +1,33 @@
using Content.Shared.Chemistry.Reagent;
using Content.Server.Disease;
using Content.Shared.Disease;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using JetBrains.Annotations;
namespace Content.Server.Chemistry.ReagentEffects
{
/// <summary>
/// Default metabolism for medicine reagents.
/// </summary>
[UsedImplicitly]
public sealed class ChemCauseDisease : ReagentEffect
{
/// <summary>
/// Chance it has each tick to cause disease, between 0 and 1
/// </summary>
[DataField("causeChance")]
public float CauseChance = 0.15f;
/// <summary>
/// The disease to add.
/// </summary>
[DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>), required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public string Disease = default!;
public override void Effect(ReagentEffectArgs args)
{
EntitySystem.Get<DiseaseSystem>().TryAddDisease(args.SolutionEntity, Disease);
}
}
}

View file

@ -0,0 +1,25 @@
using Content.Shared.Chemistry.Reagent;
using Content.Server.Disease;
using JetBrains.Annotations;
namespace Content.Server.Chemistry.ReagentEffects
{
/// <summary>
/// Default metabolism for medicine reagents.
/// </summary>
[UsedImplicitly]
public sealed class ChemCureDisease : ReagentEffect
{
/// <summary>
/// Chance it has each tick to cure a disease, between 0 and 1
/// </summary>
[DataField("cureChance")]
public float CureChance = 0.15f;
public override void Effect(ReagentEffectArgs args)
{
var ev = new CureDiseaseAttemptEvent(CureChance);
args.EntityManager.EventBus.RaiseLocalEvent(args.SolutionEntity, ev, false);
}
}
}

View file

@ -1,7 +1,5 @@
using System.Text.Json.Serialization;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;

View file

@ -21,7 +21,6 @@ namespace Content.Server.Clothing
SubscribeLocalEvent<MagbootsComponent, GetVerbsEvent<ActivationVerb>>(AddToggleVerb);
SubscribeLocalEvent<MagbootsComponent, SlipAttemptEvent>(OnSlipAttempt);
SubscribeLocalEvent<MagbootsComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
SubscribeLocalEvent<MagbootsComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<MagbootsComponent, GotUnequippedEvent>(OnGotUnequipped);
}
@ -63,11 +62,6 @@ namespace Content.Server.Clothing
}
}
private void OnRefreshMovespeed(EntityUid uid, MagbootsComponent component, RefreshMovementSpeedModifiersEvent args)
{
args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier);
}
private void AddToggleVerb(EntityUid uid, MagbootsComponent component, GetVerbsEvent<ActivationVerb> args)
{
if (!args.CanAccess || !args.CanInteract)

View file

@ -15,7 +15,7 @@
</PropertyGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Content.Server.Database\Content.Server.Database.csproj" />

View file

@ -0,0 +1,36 @@
using System.Linq;
using Content.Shared.Disease;
namespace Content.Server.Disease.Components
{
[RegisterComponent]
/// <summary>
/// Allows the enity to be infected with diseases.
/// Please use only on mobs.
/// </summary>
public sealed class DiseaseCarrierComponent : Component
{
/// <summary>
/// Shows the CURRENT diseases on the carrier
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public List<DiseasePrototype> Diseases = new();
/// <summary>
/// The carrier's resistance to disease
/// </summary>
[DataField("diseaseResist")]
[ViewVariables(VVAccess.ReadWrite)]
public float DiseaseResist = 0f;
/// <summary>
/// Diseases the carrier has had, used for immunity.
/// <summary>
[ViewVariables(VVAccess.ReadWrite)]
public List<DiseasePrototype> PastDiseases = new();
/// <summary>
/// All the diseases the carrier has or has had.
/// Checked against when trying to add a disease
/// <summary>
[ViewVariables(VVAccess.ReadWrite)]
public List<DiseasePrototype> AllDiseases => PastDiseases.Concat(Diseases).ToList();
}
}

View file

@ -0,0 +1,9 @@
namespace Content.Server.Disease.Components
{
/// <summary>
/// To give the disease diagnosing machine specific behavior
/// </summary>
[RegisterComponent]
public sealed class DiseaseDiagnoserComponent : Component
{}
}

View file

@ -0,0 +1,31 @@
using Content.Shared.Disease;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Disease.Components
{
[RegisterComponent]
/// <summary>
/// For shared behavior between both disease machines
/// </summary>
public sealed class DiseaseMachineComponent : Component
{
[DataField("delay")]
public float Delay = 5f;
/// <summary>
/// How much time we've accumulated processing
/// </summary>
[ViewVariables]
public float Accumulator = 0f;
/// <summary>
/// The disease prototype currently being diagnosed
/// </summary>
[ViewVariables]
public DiseasePrototype? Disease;
/// <summary>
/// What the machine will spawn
/// </summary>
[DataField("machineOutput", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>), required: true)]
public string MachineOutput = string.Empty;
}
}

View file

@ -0,0 +1,9 @@
namespace Content.Server.Disease.Components
{
/// <summary>
/// For EntityQuery to keep track of which machines are running
/// <summary>
[RegisterComponent]
public sealed class DiseaseMachineRunningComponent : Component
{}
}

View file

@ -0,0 +1,24 @@
namespace Content.Server.Disease.Components
{
/// <summary>
/// Value added to clothing to give its wearer
/// protection against infection from diseases
/// </summary>
[RegisterComponent]
public sealed class DiseaseProtectionComponent : Component
{
/// <summary>
/// Float value between 0 and 1, will be subtracted
/// from the infection chance (which is base 0.7)
/// Reference guide is a full biosuit w/gloves & mask
/// should add up to exactly 0.7
/// </summary>
[DataField("protection")]
public float Protection = 0.1f;
/// <summary>
/// Is the component currently being worn and affecting someone's disease
/// resistance? Making the unequip check not totally CBT
/// </summary>
public bool IsActive = false;
}
}

View file

@ -0,0 +1,33 @@
using System.Threading;
using Content.Shared.Disease;
namespace Content.Server.Disease.Components
{
[RegisterComponent]
/// <summary>
/// For mouth swabs used to collect and process
/// disease samples.
/// </summary>
public sealed class DiseaseSwabComponent : Component
{
/// <summary>
/// How long it takes to swab someone.
/// </summary>
[DataField("swabDelay")]
[ViewVariables]
public float SwabDelay = 2f;
/// <summary>
/// If this swab has been used
/// </summary>
public bool Used = false;
/// <summary>
/// Token for interrupting swabbing do after.
/// </summary>
public CancellationTokenSource? CancelToken;
/// <summary>
/// The disease prototype currently on the swab
/// </summary>
[ViewVariables]
public DiseasePrototype? Disease;
}
}

View file

@ -0,0 +1,33 @@
using System.Threading;
using Content.Shared.Disease;
namespace Content.Server.Disease.Components
{
/// <summary>
/// For disease vaccines
/// </summary>
[RegisterComponent]
public sealed class DiseaseVaccineComponent : Component
{
/// <summary>
/// How long it takes to inject someone
/// </summary>
[DataField("injectDelay")]
[ViewVariables]
public float InjectDelay = 2f;
/// <summary>
/// If this vaccine has been used
/// </summary>
public bool Used = false;
/// <summary>
/// Token for interrupting injection do after.
/// </summary>
public CancellationTokenSource? CancelToken;
/// <summary>
/// The disease prototype currently on the vaccine
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public DiseasePrototype? Disease;
}
}

View file

@ -0,0 +1,10 @@
namespace Content.Server.Disease.Components
{
/// <summary>
/// Controls disease machine behavior specific to the
/// vaccine creating machine
/// </summary>
[RegisterComponent]
public sealed class DiseaseVaccineCreatorComponent : Component
{}
}

View file

@ -0,0 +1,33 @@
using Content.Shared.Disease;
using Content.Server.Buckle.Components;
namespace Content.Server.Disease.Cures
{
/// <summary>
/// Cures the disease after a certain amount of time
/// strapped.
/// </summary>
/// TODO: Revisit after bed pr merged
public sealed class DiseaseBedrestCure : DiseaseCure
{
[ViewVariables(VVAccess.ReadWrite)]
public int Ticker = 0;
[DataField("maxLength", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public int MaxLength = 60;
public override bool Cure(DiseaseEffectArgs args)
{
if (!args.EntityManager.TryGetComponent<BuckleComponent>(args.DiseasedEntity, out var buckle))
return false;
if (buckle.Buckled)
Ticker++;
return Ticker >= MaxLength;
}
public override string CureText()
{
return (Loc.GetString("diagnoser-cure-bedrest", ("time", MaxLength)));
}
}
}

View file

@ -0,0 +1,34 @@
using Content.Server.Temperature.Components;
using Content.Shared.Disease;
namespace Content.Server.Disease.Cures
{
/// <summary>
/// Cures the disease if temperature is within certain bounds.
/// </summary>
public sealed class DiseaseBodyTemperatureCure : DiseaseCure
{
[DataField("min")]
public float Min = 0;
[DataField("max")]
public float Max = float.MaxValue;
public override bool Cure(DiseaseEffectArgs args)
{
if (!args.EntityManager.TryGetComponent(args.DiseasedEntity, out TemperatureComponent temp))
return false;
return temp.CurrentTemperature > Min && temp.CurrentTemperature < float.MaxValue;
}
public override string CureText()
{
if (Min == 0)
return Loc.GetString("diagnoser-cure-temp-max", ("max", Math.Round(Max)));
if (Max == float.MaxValue)
return Loc.GetString("diagnoser-cure-temp-min", ("min", Math.Round(Min)));
return Loc.GetString("diagnoser-cure-temp-both", ("max", Math.Round(Max)), ("min", Math.Round(Min)));
}
}
}

View file

@ -0,0 +1,31 @@
using Content.Shared.Disease;
namespace Content.Server.Disease.Cures
{
/// <summary>
/// Automatically removes the disease after a
/// certain amount of time.
/// </summary>
public sealed class DiseaseJustWaitCure : DiseaseCure
{
/// <summary>
/// All of these are in seconds
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int Ticker = 0;
[DataField("maxLength", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public int MaxLength = 150;
public override bool Cure(DiseaseEffectArgs args)
{
Ticker++;
return Ticker >= MaxLength;
}
public override string CureText()
{
return Loc.GetString("diagnoser-cure-wait", ("time", MaxLength));
}
}
}

View file

@ -0,0 +1,38 @@
using Content.Shared.Disease;
using Content.Shared.FixedPoint;
using Content.Server.Body.Components;
namespace Content.Server.Disease.Cures
{
/// <summary>
/// Cures the disease if a certain amount of reagent
/// is in the host's chemstream.
/// </summary>
public sealed class DiseaseReagentCure : DiseaseCure
{
[DataField("min")]
public FixedPoint2 Min = 5;
[DataField("reagent")]
public string? Reagent;
public override bool Cure(DiseaseEffectArgs args)
{
if (!args.EntityManager.TryGetComponent<BloodstreamComponent>(args.DiseasedEntity, out var bloodstream))
return false;
var quant = FixedPoint2.Zero;
if (Reagent != null && bloodstream.ChemicalSolution.ContainsReagent(Reagent))
{
quant = bloodstream.ChemicalSolution.GetReagentQuantity(Reagent);
}
return quant >= Min;
}
public override string CureText()
{
if (Reagent == null)
return string.Empty;
return (Loc.GetString("diagnoser-cure-reagent", ("units", Min), ("reagent", Reagent)));
}
}
}

View file

@ -0,0 +1,415 @@
using System.Threading;
using Content.Server.Disease.Components;
using Content.Shared.Disease;
using Content.Shared.Disease.Components;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Examine;
using Content.Server.DoAfter;
using Content.Server.Popups;
using Content.Server.Hands.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Server.Paper;
using Content.Server.Tools.Components;
using Content.Server.Power.Components;
using Robust.Shared.Random;
using Robust.Shared.Player;
using Robust.Shared.Audio;
using Robust.Shared.Utility;
namespace Content.Server.Disease
{
/// Everything that's about disease diangosis and machines is in here
public sealed class DiseaseDiagnosisSystem : EntitySystem
{
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DiseaseSwabComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<DiseaseSwabComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<DiseaseDiagnoserComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
SubscribeLocalEvent<DiseaseVaccineCreatorComponent, AfterInteractUsingEvent>(OnAfterInteractUsingVaccine);
/// Visuals
SubscribeLocalEvent<DiseaseMachineComponent, PowerChangedEvent>(OnPowerChanged);
/// Private Events
SubscribeLocalEvent<DiseaseDiagnoserComponent, DiseaseMachineFinishedEvent>(OnDiagnoserFinished);
SubscribeLocalEvent<DiseaseVaccineCreatorComponent, DiseaseMachineFinishedEvent>(OnVaccinatorFinished);
SubscribeLocalEvent<TargetSwabSuccessfulEvent>(OnTargetSwabSuccessful);
SubscribeLocalEvent<SwabCancelledEvent>(OnSwabCancelled);
}
private Queue<EntityUid> AddQueue = new();
private Queue<EntityUid> RemoveQueue = new();
/// <summary>
/// This handles running disease machines
/// to handle their delay and visuals.
/// </summary>
public override void Update(float frameTime)
{
foreach (var uid in AddQueue)
EnsureComp<DiseaseMachineRunningComponent>(uid);
AddQueue.Clear();
foreach (var uid in RemoveQueue)
RemComp<DiseaseMachineRunningComponent>(uid);
RemoveQueue.Clear();
foreach (var (runningComp, diseaseMachine) in EntityQuery<DiseaseMachineRunningComponent, DiseaseMachineComponent>(false))
{
if (diseaseMachine.Accumulator < diseaseMachine.Delay)
{
diseaseMachine.Accumulator += frameTime;
return;
}
diseaseMachine.Accumulator = 0;
var ev = new DiseaseMachineFinishedEvent(diseaseMachine);
RaiseLocalEvent(diseaseMachine.Owner, ev, false);
RemoveQueue.Enqueue(diseaseMachine.Owner);
}
}
///
/// Event Handlers
///
/// <summary>
/// This handles using swabs on other people
/// and checks that the swab isn't already used
/// and the other person's mouth is accessible
/// and then adds a random disease from that person
/// to the swab if they have any
/// </summary>
private void OnAfterInteract(EntityUid uid, DiseaseSwabComponent swab, AfterInteractEvent args)
{
if (swab.CancelToken != null)
{
swab.CancelToken.Cancel();
swab.CancelToken = null;
return;
}
if (args.Target == null || !args.CanReach)
return;
if (!TryComp<DiseaseCarrierComponent>(args.Target, out var carrier))
return;
if (swab.Used)
{
_popupSystem.PopupEntity(Loc.GetString("swab-already-used"), args.User, Filter.Entities(args.User));
return;
}
if (_inventorySystem.TryGetSlotEntity(args.Target.Value, "mask", out var maskUid) &&
EntityManager.TryGetComponent<IngestionBlockerComponent>(maskUid, out var blocker) &&
blocker.Enabled)
{
_popupSystem.PopupEntity(Loc.GetString("swab-mask-blocked", ("target", args.Target), ("mask", maskUid)), args.User, Filter.Entities(args.User));
return;
}
swab.CancelToken = new CancellationTokenSource();
_doAfterSystem.DoAfter(new DoAfterEventArgs(args.User, swab.SwabDelay, swab.CancelToken.Token, target: args.Target)
{
BroadcastFinishedEvent = new TargetSwabSuccessfulEvent(args.User, args.Target, swab, carrier),
BroadcastCancelledEvent = new SwabCancelledEvent(swab),
BreakOnTargetMove = true,
BreakOnUserMove = true,
BreakOnStun = true,
NeedHand = true
});
}
/// <summary>
/// This handles the disease diagnoser machine up
/// until it's turned on. It has some slight
/// differences in checks from the vaccinator.
/// </summary>
private void OnAfterInteractUsing(EntityUid uid, DiseaseDiagnoserComponent component, AfterInteractUsingEvent args)
{
var machine = Comp<DiseaseMachineComponent>(uid);
if (args.Handled || !args.CanReach)
return;
if (HasComp<DiseaseMachineRunningComponent>(uid) || TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
return;
if (!HasComp<HandsComponent>(args.User) || HasComp<ToolComponent>(args.Used)) // Don't want to accidentally breach wrenching or whatever
return;
if (!TryComp<DiseaseSwabComponent>(args.Used, out var swab))
{
_popupSystem.PopupEntity(Loc.GetString("diagnoser-cant-use-swab", ("machine", uid), ("swab", args.Used)), uid, Filter.Entities(args.User));
return;
}
_popupSystem.PopupEntity(Loc.GetString("diagnoser-insert-swab", ("machine", uid), ("swab", args.Used)), uid, Filter.Entities(args.User));
machine.Disease = swab.Disease;
EntityManager.DeleteEntity(args.Used);
AddQueue.Enqueue(uid);
UpdateAppearance(uid, true, true);
SoundSystem.Play(Filter.Pvs(uid), "/Audio/Machines/diagnoser_printing.ogg", uid);
}
/// <summary>
/// This handles the vaccinator machine up
/// until it's turned on. It has some slight
/// differences in checks from the diagnoser.
/// </summary>
private void OnAfterInteractUsingVaccine(EntityUid uid, DiseaseVaccineCreatorComponent component, AfterInteractUsingEvent args)
{
if (args.Handled || !args.CanReach)
return;
if (HasComp<DiseaseMachineRunningComponent>(uid) || TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
return;
if (!HasComp<HandsComponent>(args.User) || HasComp<ToolComponent>(args.Used)) //This check ensures tools don't break without yaml ordering jank
return;
if (!TryComp<DiseaseSwabComponent>(args.Used, out var swab) || swab.Disease == null || !swab.Disease.Infectious)
{
_popupSystem.PopupEntity(Loc.GetString("diagnoser-cant-use-swab", ("machine", uid), ("swab", args.Used)), uid, Filter.Entities(args.User));
return;
}
_popupSystem.PopupEntity(Loc.GetString("diagnoser-insert-swab", ("machine", uid), ("swab", args.Used)), uid, Filter.Entities(args.User));
var machine = Comp<DiseaseMachineComponent>(uid);
machine.Disease = swab.Disease;
EntityManager.DeleteEntity(args.Used);
AddQueue.Enqueue(uid);
UpdateAppearance(uid, true, true);
SoundSystem.Play(Filter.Pvs(uid), "/Audio/Machines/vaccinator_running.ogg", uid);
}
/// <summary>
/// This handles swab examination text
/// so you can tell if they are used or not.
/// </summary>
private void OnExamined(EntityUid uid, DiseaseSwabComponent swab, ExaminedEvent args)
{
if (args.IsInDetailsRange)
{
if (swab.Used)
args.PushMarkup(Loc.GetString("swab-used"));
else
args.PushMarkup(Loc.GetString("swab-unused"));
}
}
///
/// Helper functions
///
/// <summary>
/// This assembles a disease report
/// With its basic details and
/// specific cures (i.e. not spaceacillin).
/// The cure resist field tells you how
/// effective spaceacillin etc will be.
/// </summary>
private FormattedMessage AssembleDiseaseReport(DiseasePrototype disease)
{
FormattedMessage report = new();
report.AddMarkup(Loc.GetString("diagnoser-disease-report-name", ("disease", disease.Name)));
report.PushNewline();
if (disease.Infectious)
{
report.AddMarkup(Loc.GetString("diagnoser-disease-report-infectious"));
report.PushNewline();
} else
{
report.AddMarkup(Loc.GetString("diagnoser-disease-report-not-infectious"));
report.PushNewline();
}
string cureResistLine = string.Empty;
cureResistLine += disease.CureResist switch
{
< 0f => Loc.GetString("diagnoser-disease-report-cureresist-none"),
<= 0.05f => Loc.GetString("diagnoser-disease-report-cureresist-low"),
<= 0.14f => Loc.GetString("diagnoser-disease-report-cureresist-medium"),
_ => Loc.GetString("diagnoser-disease-report-cureresist-high")
};
report.AddMarkup(cureResistLine);
report.PushNewline();
/// Add Cures
if (disease.Cures.Count == 0)
{
report.AddMarkup(Loc.GetString("diagnoser-no-cures"));
}
else
{
report.PushNewline();
report.AddMarkup(Loc.GetString("diagnoser-cure-has"));
report.PushNewline();
foreach (var cure in disease.Cures)
{
report.AddMarkup(cure.CureText());
report.PushNewline();
}
}
return report;
}
///
/// Appearance stuff
///
/// <summary>
/// Appearance helper function to
/// set the component's power and running states.
/// </summary>
private void UpdateAppearance(EntityUid uid, bool isOn, bool isRunning)
{
if (!TryComp<AppearanceComponent>(uid, out var appearance))
return;
appearance.SetData(DiseaseMachineVisuals.IsOn, isOn);
appearance.SetData(DiseaseMachineVisuals.IsRunning, isRunning);
}
/// <summary>
/// Makes sure the machine is visually off/on.
/// </summary>
private void OnPowerChanged(EntityUid uid, DiseaseMachineComponent component, PowerChangedEvent args)
{
UpdateAppearance(uid, args.Powered, false);
}
///
/// Private events
///
/// <summary>
/// Copies a disease prototype to the swab
/// after the doafter completes.
/// </summary>
private void OnTargetSwabSuccessful(TargetSwabSuccessfulEvent args)
{
if (args.Target == null)
return;
args.Swab.Used = true;
_popupSystem.PopupEntity(Loc.GetString("swab-swabbed", ("target", args.Target)), args.Target.Value, Filter.Entities(args.User));
if (args.Swab.Disease != null || args.Carrier.Diseases.Count == 0)
return;
args.Swab.Disease = _random.Pick(args.Carrier.Diseases);
}
/// <summary>
/// Cancels the swab doafter if needed.
/// </summary>
private static void OnSwabCancelled(SwabCancelledEvent args)
{
args.Swab.CancelToken = null;
}
/// <summary>
/// Prints a diagnostic report with its findings.
/// Also cancels the animation.
/// </summary>
private void OnDiagnoserFinished(EntityUid uid, DiseaseDiagnoserComponent component, DiseaseMachineFinishedEvent args)
{
var power = Comp<ApcPowerReceiverComponent>(uid);
UpdateAppearance(uid, power.Powered, false);
// spawn a piece of paper.
var printed = EntityManager.SpawnEntity(args.Machine.MachineOutput, Transform(uid).Coordinates);
if (!TryComp<PaperComponent>(printed, out var paper))
return;
var reportTitle = string.Empty;
FormattedMessage contents = new();
if (args.Machine.Disease != null)
{
reportTitle = Loc.GetString("diagnoser-disease-report", ("disease", args.Machine.Disease.Name));
contents = AssembleDiseaseReport(args.Machine.Disease);
} else
{
reportTitle = Loc.GetString("diagnoser-disease-report-none");
contents.AddMarkup(Loc.GetString("diagnoser-disease-report-none-contents"));
}
MetaData(printed).EntityName = reportTitle;
paper.SetContent(contents.ToMarkup());
}
/// <summary>
/// Prints a vaccine that will vaccinate
/// against the disease on the inserted swab.
/// <summary>
private void OnVaccinatorFinished(EntityUid uid, DiseaseVaccineCreatorComponent component, DiseaseMachineFinishedEvent args)
{
var power = Comp<ApcPowerReceiverComponent>(uid);
UpdateAppearance(uid, power.Powered, false);
// spawn a vaccine
var vaxx = EntityManager.SpawnEntity(args.Machine.MachineOutput, Transform(uid).Coordinates);
if (!TryComp<DiseaseVaccineComponent>(vaxx, out var vaxxComp))
return;
vaxxComp.Disease = args.Machine.Disease;
}
/// <summary>
/// Cancels the mouth-swabbing doafter
/// </summary>
private sealed class SwabCancelledEvent : EntityEventArgs
{
public readonly DiseaseSwabComponent Swab;
public SwabCancelledEvent(DiseaseSwabComponent swab)
{
Swab = swab;
}
}
/// <summary>
/// Fires if the doafter for swabbing someone's mouth succeeds
/// </summary>
private sealed class TargetSwabSuccessfulEvent : EntityEventArgs
{
public EntityUid User { get; }
public EntityUid? Target { get; }
public DiseaseSwabComponent Swab { get; }
public DiseaseCarrierComponent Carrier { get; }
public TargetSwabSuccessfulEvent(EntityUid user, EntityUid? target, DiseaseSwabComponent swab, DiseaseCarrierComponent carrier)
{
User = user;
Target = target;
Swab = swab;
Carrier = carrier;
}
}
/// <summary>
/// Fires when a disease machine is done
/// with its production delay and ready to
/// create a report or vaccine
/// </summary>
private sealed class DiseaseMachineFinishedEvent : EntityEventArgs
{
public DiseaseMachineComponent Machine {get;}
public DiseaseMachineFinishedEvent(DiseaseMachineComponent machine)
{
Machine = machine;
}
}
}
}

View file

@ -0,0 +1,456 @@
using System.Threading;
using Content.Shared.Disease;
using Content.Shared.Disease.Components;
using Content.Server.Disease.Components;
using Content.Server.Clothing.Components;
using Content.Shared.MobState.Components;
using Content.Shared.Examine;
using Content.Shared.Inventory;
using Content.Shared.Interaction;
using Content.Server.Popups;
using Content.Server.DoAfter;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
using Content.Shared.Inventory.Events;
using Content.Server.Nutrition.EntitySystems;
using Robust.Shared.Utility;
namespace Content.Server.Disease
{
/// <summary>
/// Handles disease propagation & curing
/// </summary>
public sealed class DiseaseSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ISerializationManager _serializationManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DiseaseCarrierComponent, CureDiseaseAttemptEvent>(OnTryCureDisease);
SubscribeLocalEvent<DiseasedComponent, InteractHandEvent>(OnInteractDiseasedHand);
SubscribeLocalEvent<DiseasedComponent, InteractUsingEvent>(OnInteractDiseasedUsing);
SubscribeLocalEvent<DiseaseProtectionComponent, GotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<DiseaseProtectionComponent, GotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<DiseaseVaccineComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<DiseaseVaccineComponent, ExaminedEvent>(OnExamined);
// Private events stuff
SubscribeLocalEvent<TargetVaxxSuccessfulEvent>(OnTargetVaxxSuccessful);
SubscribeLocalEvent<VaxxCancelledEvent>(OnVaxxCancelled);
}
private Queue<EntityUid> AddQueue = new();
private Queue<(DiseaseCarrierComponent carrier, DiseasePrototype disease)> CureQueue = new();
/// <summary>
/// First, adds or removes diseased component from the queues and clears them.
/// Then, iterates over every diseased component to check for their effects
/// and cures
/// </summary>
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var entity in AddQueue)
{
EnsureComp<DiseasedComponent>(entity);
}
AddQueue.Clear();
foreach (var tuple in CureQueue)
{
if (tuple.carrier.Diseases.Count == 1) //This is reliable unlike testing Count == 0 right after removal for reasons I don't quite get
RemComp<DiseasedComponent>(tuple.carrier.Owner);
tuple.carrier.PastDiseases.Add(tuple.disease);
tuple.carrier.Diseases.Remove(tuple.disease);
}
CureQueue.Clear();
foreach (var (_, carrierComp, mobState) in EntityQuery<DiseasedComponent, DiseaseCarrierComponent, MobStateComponent>())
{
DebugTools.Assert(carrierComp.Diseases.Count > 0);
if (mobState.IsDead())
{
if (_random.Prob(0.005f * frameTime)) //Mean time to remove is 200 seconds per disease
CureDisease(carrierComp, _random.Pick(carrierComp.Diseases));
continue;
}
foreach (var disease in carrierComp.Diseases)
{
var args = new DiseaseEffectArgs(carrierComp.Owner, disease, EntityManager);
disease.Accumulator += frameTime;
if (disease.Accumulator >= disease.TickTime)
{
disease.Accumulator -= disease.TickTime;
foreach (var cure in disease.Cures)
{
if (cure.Cure(args))
CureDisease(carrierComp, disease);
}
foreach (var effect in disease.Effects)
{
if (_random.Prob(effect.Probability))
effect.Effect(args);
}
}
}
}
}
///
/// Event Handlers
///
/// <summary>
/// Used when something is trying to cure ANY disease on the target,
/// not for special disease interactions. Randomly
/// tries to cure every disease on the target.
/// </summary>
private void OnTryCureDisease(EntityUid uid, DiseaseCarrierComponent component, CureDiseaseAttemptEvent args)
{
foreach (var disease in component.Diseases)
{
var cureProb = ((args.CureChance / component.Diseases.Count) - disease.CureResist);
if (cureProb < 0)
return;
if (cureProb > 1)
{
CureDisease(component, disease);
return;
}
if (_random.Prob(cureProb))
{
CureDisease(component, disease);
return;
}
}
}
/// <summary>
/// Called when a component with disease protection
/// is equipped so it can be added to the person's
/// total disease resistance
/// </summary>
private void OnEquipped(EntityUid uid, DiseaseProtectionComponent component, GotEquippedEvent args)
{
// This only works on clothing
if (!TryComp<ClothingComponent>(uid, out var clothing))
return;
// Is the clothing in its actual slot?
if (!clothing.SlotFlags.HasFlag(args.SlotFlags))
return;
// Give the user the component's disease resist
if(TryComp<DiseaseCarrierComponent>(args.Equipee, out var carrier))
carrier.DiseaseResist += component.Protection;
// Set the component to active to the unequip check isn't CBT
component.IsActive = true;
}
/// <summary>
/// Called when a component with disease protection
/// is unequipped so it can be removed from the person's
/// total disease resistance
/// </summary>
private void OnUnequipped(EntityUid uid, DiseaseProtectionComponent component, GotUnequippedEvent args)
{
// Only undo the resistance if it was affecting the user
if (!component.IsActive)
return;
if(TryComp<DiseaseCarrierComponent>(args.Equipee, out var carrier))
carrier.DiseaseResist -= component.Protection;
component.IsActive = false;
}
/// <summary>
/// Called when it's already decided a disease will be cured
/// so it can be safely queued up to be removed from the target
/// and added to past disease history (for immunity)
/// </summary>
private void CureDisease(DiseaseCarrierComponent carrier, DiseasePrototype disease)
{
var CureTuple = (carrier, disease);
CureQueue.Enqueue(CureTuple);
_popupSystem.PopupEntity(Loc.GetString("disease-cured"), carrier.Owner, Filter.Entities(carrier.Owner));
}
/// <summary>
/// Called when someone interacts with a diseased person with an empty hand
/// to check if they get infected
/// </summary>
private void OnInteractDiseasedHand(EntityUid uid, DiseasedComponent component, InteractHandEvent args)
{
InteractWithDiseased(args.Target, args.User);
}
/// <summary>
/// Called when someone interacts with a diseased person with any object
/// to check if they get infected
/// </summary>
private void OnInteractDiseasedUsing(EntityUid uid, DiseasedComponent component, InteractUsingEvent args)
{
InteractWithDiseased(args.Target, args.User);
}
/// <summary>
/// Called when a vaccine is used on someone
/// to handle the vaccination doafter
/// </summary>
private void OnAfterInteract(EntityUid uid, DiseaseVaccineComponent vaxx, AfterInteractEvent args)
{
if (vaxx.CancelToken != null)
{
vaxx.CancelToken.Cancel();
vaxx.CancelToken = null;
return;
}
if (args.Target == null)
return;
if (!args.CanReach)
return;
if (vaxx.CancelToken != null)
return;
if (!TryComp<DiseaseCarrierComponent>(args.Target, out var carrier))
return;
if (vaxx.Used)
{
_popupSystem.PopupEntity(Loc.GetString("vaxx-already-used"), args.User, Filter.Entities(args.User));
return;
}
vaxx.CancelToken = new CancellationTokenSource();
_doAfterSystem.DoAfter(new DoAfterEventArgs(args.User, vaxx.InjectDelay, vaxx.CancelToken.Token, target: args.Target)
{
BroadcastFinishedEvent = new TargetVaxxSuccessfulEvent(args.User, args.Target, vaxx, carrier),
BroadcastCancelledEvent = new VaxxCancelledEvent(vaxx),
BreakOnTargetMove = true,
BreakOnUserMove = true,
BreakOnStun = true,
NeedHand = true
});
}
/// <summary>
/// Called when a vaccine is examined.
/// Currently doesn't do much because
/// vaccines don't have unique art with a seperate
/// state visualizer.
/// </summary>
private void OnExamined(EntityUid uid, DiseaseVaccineComponent vaxx, ExaminedEvent args)
{
if (args.IsInDetailsRange)
{
if (vaxx.Used)
args.PushMarkup(Loc.GetString("vaxx-used"));
else
args.PushMarkup(Loc.GetString("vaxx-unused"));
}
}
///
/// Helper functions
///
/// <summary>
/// Tries to infect anyone that
/// interacts with a diseased person or body
/// </summary>
private void InteractWithDiseased(EntityUid diseased, EntityUid target, DiseaseCarrierComponent? diseasedCarrier = null)
{
if (!Resolve(diseased, ref diseasedCarrier, false) ||
diseasedCarrier.Diseases.Count == 0 ||
!TryComp<DiseaseCarrierComponent>(target, out var carrier))
return;
var disease = _random.Pick(diseasedCarrier.Diseases);
TryInfect(carrier, disease, 0.4f);
}
/// <summary>
/// Adds a disease to a target
/// if it's not already in their current
/// or past diseases. If you want this
/// to not be guaranteed you are looking
/// for TryInfect.
/// </summary>
public void TryAddDisease(EntityUid host, DiseasePrototype addedDisease, DiseaseCarrierComponent? target = null)
{
if (!Resolve(host, ref target, false))
return;
foreach (var disease in target.AllDiseases)
{
if (disease.ID == addedDisease?.ID) //ID because of the way protoypes work
return;
}
var freshDisease = _serializationManager.CreateCopy(addedDisease);
if (freshDisease == null) return;
target.Diseases.Add(freshDisease);
AddQueue.Enqueue(target.Owner);
}
public void TryAddDisease(EntityUid host, string? addedDisease, DiseaseCarrierComponent? target = null)
{
if (addedDisease == null || !_prototypeManager.TryIndex<DiseasePrototype>(addedDisease, out var added))
return;
TryAddDisease(host, added, target);
}
/// <summary>
/// Pits the infection chance against the
/// person's disease resistance and
/// rolls the dice to see if they get
/// the disease.
/// </summary>
public void TryInfect(DiseaseCarrierComponent carrier, DiseasePrototype? disease, float chance = 0.7f)
{
if(disease is not { Infectious: true })
return;
var infectionChance = chance - carrier.DiseaseResist;
if (infectionChance <= 0)
return;
if (_random.Prob(infectionChance))
TryAddDisease(carrier.Owner, disease, carrier);
}
/// <summary>
/// Plays a sneeze/cough popup if applicable
/// and then tries to infect anyone in range
/// if the snougher is not wearing a mask.
/// </summary>
public void SneezeCough(EntityUid uid, DiseasePrototype? disease, string snoughMessage, bool airTransmit = true, TransformComponent? xform = null)
{
if (!Resolve(uid, ref xform)) return;
if (!string.IsNullOrEmpty(snoughMessage))
_popupSystem.PopupEntity(Loc.GetString(snoughMessage, ("person", uid)), uid, Filter.Pvs(uid));
if (disease is not { Infectious: true } || !airTransmit)
return;
if (_inventorySystem.TryGetSlotEntity(uid, "mask", out var maskUid) &&
EntityManager.TryGetComponent<IngestionBlockerComponent>(maskUid, out var blocker) &&
blocker.Enabled)
return;
var carrierQuery = GetEntityQuery<DiseaseCarrierComponent>();
foreach (var entity in _lookup.GetEntitiesInRange(xform.MapID, xform.WorldPosition, 2f))
{
if (!carrierQuery.TryGetComponent(entity, out var carrier) ||
!_interactionSystem.InRangeUnobstructed(uid, entity)) continue;
TryInfect(carrier, disease, 0.3f);
}
}
/// <summary>
/// Adds a disease to the carrier's
/// past diseases to give them immunity
/// IF they don't already have the disease.
/// </summary>
public void Vaccinate(DiseaseCarrierComponent carrier, DiseasePrototype disease)
{
foreach (var currentDisease in carrier.Diseases)
{
if (currentDisease.ID == disease.ID) //ID because of the way protoypes work
return;
}
carrier.PastDiseases.Add(disease);
}
///
/// Private Events Stuff
///
/// <summary>
/// Injects the vaccine into the target
/// if the doafter is completed
/// </summary>
private void OnTargetVaxxSuccessful(TargetVaxxSuccessfulEvent args)
{
if (args.Vaxx.Disease == null)
return;
Vaccinate(args.Carrier, args.Vaxx.Disease);
EntityManager.DeleteEntity(args.Vaxx.Owner);
}
/// <summary>
/// Cancels the vaccine doafter
/// </summary>
private static void OnVaxxCancelled(VaxxCancelledEvent args)
{
args.Vaxx.CancelToken = null;
}
/// These two are standard doafter stuff you can ignore
private sealed class VaxxCancelledEvent : EntityEventArgs
{
public readonly DiseaseVaccineComponent Vaxx;
public VaxxCancelledEvent(DiseaseVaccineComponent vaxx)
{
Vaxx = vaxx;
}
}
private sealed class TargetVaxxSuccessfulEvent : EntityEventArgs
{
public EntityUid User { get; }
public EntityUid? Target { get; }
public DiseaseVaccineComponent Vaxx { get; }
public DiseaseCarrierComponent Carrier { get; }
public TargetVaxxSuccessfulEvent(EntityUid user, EntityUid? target, DiseaseVaccineComponent vaxx, DiseaseCarrierComponent carrier)
{
User = user;
Target = target;
Vaxx = vaxx;
Carrier = carrier;
}
}
}
/// <summary>
/// This event is fired by chems
/// and other brute-force rather than
/// specific cures. It will roll the dice to attempt
/// to cure each disease on the target
/// </summary>
public sealed class CureDiseaseAttemptEvent : EntityEventArgs
{
public float CureChance { get; }
public CureDiseaseAttemptEvent(float cureChance)
{
CureChance = cureChance;
}
}
/// <summary>
/// Controls whether the snough is a sneeze, cough
/// or neither. If none, will not create
/// a popup. Mostly used for talking
/// </summary>
public enum SneezeCoughType
{
Sneeze,
Cough,
None
}
}

View file

@ -0,0 +1,46 @@
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
using Content.Server.Body.Components;
using Content.Shared.Disease;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Disease.Effects
{
/// <summary>
/// Adds or removes reagents from the
/// host's chemstream.
/// </summary>
[UsedImplicitly]
public sealed class DiseaseAdjustReagent : DiseaseEffect
{
/// <summary>
/// The reagent ID to add or remove.
/// </summary>
[DataField("reagent", customTypeSerializer:typeof(PrototypeIdSerializer<ReagentPrototype>))]
public string? Reagent = null;
[DataField("amount", required: true)]
public FixedPoint2 Amount = default!;
public override void Effect(DiseaseEffectArgs args)
{
if (!args.EntityManager.TryGetComponent<BloodstreamComponent>(args.DiseasedEntity, out var bloodstream))
return;
var stream = bloodstream.ChemicalSolution;
if (stream != null)
{
var solutionSys = args.EntityManager.EntitySysManager.GetEntitySystem<SolutionContainerSystem>();
if (Reagent != null)
{
if (Amount < 0 && stream.ContainsReagent(Reagent))
solutionSys.TryRemoveReagent(args.DiseasedEntity, stream, Reagent, -Amount);
if (Amount > 0)
solutionSys.TryAddReagent(args.DiseasedEntity, stream, Reagent, Amount, out _);
}
}
}
}
}

View file

@ -0,0 +1,67 @@
using Content.Shared.Disease;
using Content.Shared.StatusEffect;
using JetBrains.Annotations;
namespace Content.Server.Disease.Effects
{
/// <summary>
/// Adds a generic status effect to the entity.
/// Differs from the chem version in its defaults
/// to better facilitate adding components that
/// last the length of the disease.
/// </summary>
[UsedImplicitly]
public sealed class DiseaseGenericStatusEffect : DiseaseEffect
{
/// <summary>
/// The status effect key
/// Prevents other components from being with the same key
/// </summary>
[DataField("key", required: true)]
public string Key = default!;
/// <summary>
/// The component to add
/// </summary>
[DataField("component")]
public string Component = String.Empty;
[DataField("time")]
public float Time = 1.01f; /// I'm afraid if this was exact the key could get stolen by another thing
/// <remarks>
/// true - refresh status effect time, false - accumulate status effect time
/// </remarks>
[DataField("refresh")]
public bool Refresh = false;
/// <summary>
/// Should this effect add the status effect, remove time from it, or set its cooldown?
/// </summary>
[DataField("type")]
public StatusEffectDiseaseType Type = StatusEffectDiseaseType.Add;
public override void Effect(DiseaseEffectArgs args)
{
var statusSys = EntitySystem.Get<StatusEffectsSystem>();
if (Type == StatusEffectDiseaseType.Add && Component != String.Empty)
{
statusSys.TryAddStatusEffect(args.DiseasedEntity, Key, TimeSpan.FromSeconds(Time), Refresh, Component);
}
else if (Type == StatusEffectDiseaseType.Remove)
{
statusSys.TryRemoveTime(args.DiseasedEntity, Key, TimeSpan.FromSeconds(Time));
}
else if (Type == StatusEffectDiseaseType.Set)
{
statusSys.TrySetTime(args.DiseasedEntity, Key, TimeSpan.FromSeconds(Time));
}
}
}
/// See status effects for how these work
public enum StatusEffectDiseaseType
{
Add,
Remove,
Set
}
}

View file

@ -0,0 +1,21 @@
using Content.Shared.Disease;
using Content.Shared.Damage;
using JetBrains.Annotations;
namespace Content.Server.Disease.Effects
{
/// <summary>
/// Deals or heals damage to the host
/// </summary>
[UsedImplicitly]
public sealed class DiseaseHealthChange : DiseaseEffect
{
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = default!;
public override void Effect(DiseaseEffectArgs args)
{
EntitySystem.Get<DamageableSystem>().TryChangeDamage(args.DiseasedEntity, Damage, true, false);
}
}
}

View file

@ -0,0 +1,38 @@
using Content.Shared.Disease;
using Content.Shared.Popups;
using Robust.Shared.Player;
using JetBrains.Annotations;
namespace Content.Server.Disease.Effects
{
[UsedImplicitly]
/// <summary>
/// Plays a popup on the host's transform.
/// Supports passing the host's entity metadata
/// in PVS ones with {$person}
/// </summary>
public sealed class DiseasePopUp : DiseaseEffect
{
[DataField("message")]
public string Message = "disease-sick-generic";
[DataField("type")]
public PopupType Type = PopupType.Local;
public override void Effect(DiseaseEffectArgs args)
{
var popupSys = EntitySystem.Get<SharedPopupSystem>();
if (Type == PopupType.Local)
popupSys.PopupEntity(Loc.GetString(Message), args.DiseasedEntity, Filter.Entities(args.DiseasedEntity));
else if (Type == PopupType.Pvs)
popupSys.PopupEntity(Loc.GetString(Message, ("person", args.DiseasedEntity)), args.DiseasedEntity, Filter.Pvs(args.DiseasedEntity));
}
}
public enum PopupType
{
Pvs,
Local
}
}

View file

@ -0,0 +1,29 @@
using Content.Shared.Disease;
using JetBrains.Annotations;
namespace Content.Server.Disease
{
/// <summary>
/// Makes the diseased sneeze or cough
/// or neither.
/// </summary>
[UsedImplicitly]
public sealed class DiseaseSnough : DiseaseEffect
{
/// <summary>
/// Message to play when snoughing
/// </summary>
[DataField("snoughMessage")]
public string SnoughMessage = "disease-sneeze";
/// <summary>
/// Whether to spread the disease throught he air
/// </summary>
[DataField("airTransmit")]
public bool AirTransmit = true;
public override void Effect(DiseaseEffectArgs args)
{
EntitySystem.Get<DiseaseSystem>().SneezeCough(args.DiseasedEntity, args.Disease, SnoughMessage, AirTransmit);
}
}
}

View file

@ -1,3 +1,4 @@
using System.Linq;
using Content.Shared.Drone;
using Content.Server.Drone.Components;
using Content.Shared.Actions;
@ -88,9 +89,16 @@ namespace Content.Server.Drone
{
var body = Comp<SharedBodyComponent>(uid); //There's no way something can have a mobstate but not a body...
foreach (var item in drone.ToolUids)
foreach (var item in drone.ToolUids.Select((value, i) => ( value, i )))
{
EntityManager.DeleteEntity(item);
if (_tagSystem.HasTag(item.value, "Drone"))
{
RemComp<UnremoveableComponent>(item.value);
}
else
{
EntityManager.DeleteEntity(item.value);
}
}
body.Gib();
EntityManager.DeleteEntity(uid);
@ -111,12 +119,12 @@ namespace Content.Server.Drone
if (TryComp<HandsComponent>(uid, out var hands) && hands.Count >= drone.Tools.Count)
{
foreach (var entry in drone.Tools)
{
var item = EntityManager.SpawnEntity(entry.PrototypeId, spawnCoord);
AddComp<UnremoveableComponent>(item);
hands.PutInHand(item);
drone.ToolUids.Add(item);
}
{
var item = EntityManager.SpawnEntity(entry.PrototypeId, spawnCoord);
AddComp<UnremoveableComponent>(item);
hands.PutInHand(item);
drone.ToolUids.Add(item);
}
}
if (TryComp<ActionsComponent>(uid, out var actions) && TryComp<UnpoweredFlashlightComponent>(uid, out var flashlight))

View file

@ -17,6 +17,7 @@ namespace Content.Server.Entry
"ClientEntitySpawner",
"CharacterInfo",
"ItemCabinetVisuals",
"DiseaseMachineVisuals",
"HandheldGPS",
"PotencyVisuals"
};

View file

@ -0,0 +1,54 @@
using Content.Server.Fluids.EntitySystems;
using Content.Shared.FixedPoint;
using Content.Shared.Sound;
namespace Content.Server.Fluids.Components;
/// <summary>
/// For entities that can clean up puddles
/// </summary>
[RegisterComponent, Friend(typeof(MoppingSystem))]
public sealed class AbsorbentComponent : Component
{
public const string SolutionName = "absorbed";
[DataField("pickupAmount")]
public FixedPoint2 PickupAmount = FixedPoint2.New(10);
/// <summary>
/// When using this tool on an empty floor tile, leave this much reagent as a new puddle.
/// </summary>
[DataField("residueAmount")]
public FixedPoint2 ResidueAmount = FixedPoint2.New(10); // Should be higher than MopLowerLimit
/// <summary>
/// To leave behind a wet floor, this tool will be unable to take from puddles with a volume less than this amount.
/// </summary>
[DataField("mopLowerLimit")]
public FixedPoint2 MopLowerLimit = FixedPoint2.New(5);
[DataField("pickupSound")]
public SoundSpecifier PickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg");
[DataField("transferSound")]
public SoundSpecifier TransferSound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg");
/// <summary>
/// Multiplier for the do_after delay for how quickly the mopping happens.
/// </summary>
[ViewVariables]
[DataField("mopSpeed")] public float MopSpeed = 1;
/// <summary>
/// How many entities can this tool interact with at once?
/// </summary>
[DataField("maxEntities")]
public int MaxInteractingEntities = 1;
/// <summary>
/// What entities is this tool interacting with right now?
/// </summary>
[ViewVariables]
public HashSet<EntityUid> InteractingEntities = new();
}

View file

@ -1,141 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.DoAfter;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Popups;
using Content.Shared.Sound;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Fluids.Components
{
/// <summary>
/// Can a mop click on this entity and dump its fluids
/// </summary>
[RegisterComponent]
public sealed class BucketComponent : Component, IInteractUsing
{
[Dependency] private readonly IEntityManager _entMan = default!;
public const string SolutionName = "bucket";
private List<EntityUid> _currentlyUsing = new();
public FixedPoint2 MaxVolume
{
get =>
EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution)
? solution.MaxVolume
: FixedPoint2.Zero;
set
{
if (EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution))
{
solution.MaxVolume = value;
}
}
}
public FixedPoint2 CurrentVolume => EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution)
? solution.CurrentVolume
: FixedPoint2.Zero;
[DataField("sound")]
private SoundSpecifier _sound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg");
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
var solutionsSys = EntitySystem.Get<SolutionContainerSystem>();
if (!solutionsSys.TryGetSolution(Owner, SolutionName, out var contents) ||
_currentlyUsing.Contains(eventArgs.Using) ||
!_entMan.TryGetComponent(eventArgs.Using, out MopComponent? mopComponent) ||
mopComponent.Mopping)
{
return false;
}
if (CurrentVolume <= 0)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("bucket-component-bucket-is-empty-message"));
return false;
}
_currentlyUsing.Add(eventArgs.Using);
// IMO let em move while doing it.
var doAfterArgs = new DoAfterEventArgs(eventArgs.User, 1.0f, target: eventArgs.Target)
{
BreakOnStun = true,
BreakOnDamage = true,
};
var result = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(doAfterArgs);
_currentlyUsing.Remove(eventArgs.Using);
if (result == DoAfterStatus.Cancelled || _entMan.Deleted(Owner) || mopComponent.Deleted ||
CurrentVolume <= 0 || !EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(Owner, mopComponent.Owner))
return false;
//Checks if the mop is empty
if(mopComponent.CurrentVolume == 0)
{
// Transfers up to half the mop's available capacity to the mop
// Takes the lower of the mop's available volume and the bucket's current volume.
var transferAmount = FixedPoint2.Min(0.5*mopComponent.AvailableVolume, CurrentVolume);
if (transferAmount == 0)
{
return false;
}
var mopContents = mopComponent.MopSolution;
if (mopContents == null)
{
return false;
}
// Transfer solution from the bucket to the mop
// Owner is the bucket being interacted with. contents is the Solution contained by said bucket.
var solution = solutionsSys.SplitSolution(Owner, contents, transferAmount);
if (!solutionsSys.TryAddSolution(mopComponent.Owner, mopComponent.MopSolution, solution))
{
return false; //if the attempt fails
}
Owner.PopupMessage(eventArgs.User, Loc.GetString("bucket-component-mop-is-now-wet-message"));
}
else //if mop is not empty
{
//Transfer the mop solution to the bucket
if (mopComponent.MopSolution == null)
return false;
var solutionFromMop = solutionsSys.SplitSolution(mopComponent.Owner, mopComponent.MopSolution, mopComponent.CurrentVolume);
EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution);
if (!solutionsSys.TryAddSolution(Owner, solution, solutionFromMop))
{
return false; //if the attempt fails
}
Owner.PopupMessage(eventArgs.User, Loc.GetString("bucket-component-mop-is-now-dry-message"));
}
SoundSystem.Play(Filter.Pvs(Owner), _sound.GetSound(), Owner);
return true;
}
}
}

View file

@ -1,182 +0,0 @@
using System.Threading.Tasks;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.DoAfter;
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Popups;
using Content.Shared.Sound;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Fluids.Components
{
/// <summary>
/// For cleaning up puddles
/// </summary>
[RegisterComponent]
public sealed class MopComponent : Component, IAfterInteract
{
[Dependency] private readonly IEntityManager _entities = default!;
public const string SolutionName = "mop";
/// <summary>
/// Used to prevent do_after spam if we're currently mopping.
/// </summary>
public bool Mopping { get; private set; }
// MopSolution Object stores whatever solution the mop has absorbed.
public Solution? MopSolution
{
get
{
EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution);
return solution;
}
}
// MaxVolume is the Maximum volume the mop can absorb (however, this is defined in janitor.yml)
public FixedPoint2 MaxVolume
{
get => MopSolution?.MaxVolume ?? FixedPoint2.Zero;
set
{
var solution = MopSolution;
if (solution != null)
{
solution.MaxVolume = value;
}
}
}
// CurrentVolume is the volume the mop has absorbed.
public FixedPoint2 CurrentVolume => MopSolution?.CurrentVolume ?? FixedPoint2.Zero;
// AvailableVolume is the remaining volume capacity of the mop.
public FixedPoint2 AvailableVolume => MopSolution?.AvailableVolume ?? FixedPoint2.Zero;
// Currently there's a separate amount for pickup and dropoff so
// Picking up a puddle requires multiple clicks
// Dumping in a bucket requires 1 click
// Long-term you'd probably use a cooldown and start the pickup once we have some form of global cooldown
[DataField("pickup_amount")]
public FixedPoint2 PickupAmount { get; } = FixedPoint2.New(10);
/// <summary>
/// When using the mop on an empty floor tile, leave this much reagent as a new puddle.
/// </summary>
[DataField("residueAmount")]
public FixedPoint2 ResidueAmount { get; } = FixedPoint2.New(10); // Should be higher than MopLowerLimit
/// <summary>
/// To leave behind a wet floor, the mop will be unable to take from puddles with a volume less than this amount.
/// </summary>
[DataField("mopLowerLimit")]
public FixedPoint2 MopLowerLimit { get; } = FixedPoint2.New(5);
[DataField("pickup_sound")]
private SoundSpecifier _pickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg");
/// <summary>
/// Multiplier for the do_after delay for how fast the mop works.
/// </summary>
[ViewVariables]
[DataField("speed")] private float _mopSpeed = 1;
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
/*
* Functionality:
* Essentially if we click on an empty tile spill our contents there
* Otherwise, try to mop up the puddle (if it is a puddle).
* It will try to destroy solution on the mop to do so, and if it is successful
* will spill some of the mop's solution onto the puddle which will evaporate eventually.
*/
var solutionSystem = EntitySystem.Get<SolutionContainerSystem>();
var spillableSystem = EntitySystem.Get<SpillableSystem>();
if (!eventArgs.CanReach ||
!solutionSystem.TryGetSolution(Owner, SolutionName, out var contents ) ||
Mopping)
{
return false;
}
if (eventArgs.Target is not {Valid: true} target)
{
// Drop the liquid on the mop on to the ground
var solution = solutionSystem.SplitSolution(Owner, contents, FixedPoint2.Min(ResidueAmount, CurrentVolume));
spillableSystem.SpillAt(solution, eventArgs.ClickLocation, "PuddleSmear");
return true;
}
if (!_entities.TryGetComponent(target, out PuddleComponent? puddleComponent) ||
!solutionSystem.TryGetSolution((puddleComponent).Owner, puddleComponent.SolutionName, out var puddleSolution))
return false;
// if the puddle is too small for the mop to effectively take any more solution
if (puddleSolution.TotalVolume <= MopLowerLimit)
{
// Transfers solution from the mop to the puddle
solutionSystem.TryAddSolution(target, puddleSolution, solutionSystem.SplitSolution(Owner, contents, FixedPoint2.Min(ResidueAmount,CurrentVolume)));
return true;
}
// if the mop is full
if(AvailableVolume <= 0)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("mop-component-mop-is-full-message"));
return false;
}
// Mopping duration (aka delay) should scale with PickupAmount and not puddle volume, because we are picking up a constant volume of solution with each click.
var doAfterArgs = new DoAfterEventArgs(eventArgs.User, _mopSpeed * PickupAmount.Float() / 10.0f,
target: target)
{
BreakOnUserMove = true,
BreakOnStun = true,
BreakOnDamage = true,
};
Mopping = true;
var result = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(doAfterArgs);
Mopping = false;
if (result == DoAfterStatus.Cancelled ||
_entities.Deleted(Owner) ||
puddleComponent.Deleted)
return false;
// The volume the mop will take from the puddle
FixedPoint2 transferAmount;
// does the puddle actually have reagents? it might not if its a weird cosmetic entity.
if (puddleSolution.TotalVolume == 0)
transferAmount = FixedPoint2.Min(PickupAmount, AvailableVolume);
else
{
transferAmount = FixedPoint2.Min(PickupAmount, puddleSolution.TotalVolume, AvailableVolume);
if ((puddleSolution.TotalVolume - transferAmount) < MopLowerLimit) // If the transferAmount would bring the puddle below the MopLowerLimit
transferAmount = puddleSolution.TotalVolume - MopLowerLimit; // Then the transferAmount should bring the puddle down to the MopLowerLimit exactly
}
// Transfers solution from the puddle to the mop
solutionSystem.TryAddSolution(Owner, contents, solutionSystem.SplitSolution(target, puddleSolution, transferAmount));
SoundSystem.Play(Filter.Pvs(Owner), _pickupSound.GetSound(), Owner);
// if the mop became full after that puddle, let the player know.
if(AvailableVolume <= 0)
Owner.PopupMessage(eventArgs.User, Loc.GetString("mop-component-mop-is-now-full-message"));
return true;
}
}
}

View file

@ -0,0 +1,361 @@
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.DoAfter;
using Content.Server.Fluids.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Sound;
using Content.Shared.Tag;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Map;
using JetBrains.Annotations;
namespace Content.Server.Fluids.EntitySystems;
[UsedImplicitly]
public sealed class MoppingSystem : EntitySystem
{
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SpillableSystem _spillableSystem = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
const string puddlePrototypeId = "PuddleSmear"; // The puddle prototype to use when releasing liquid to the floor, making a new puddle
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AbsorbentComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<TransferCancelledEvent>(OnTransferCancelled);
SubscribeLocalEvent<TransferCompleteEvent>(OnTransferComplete);
}
private void OnAfterInteract(EntityUid uid, AbsorbentComponent component, AfterInteractEvent args)
{
if (!args.CanReach) // if user cannot reach the target
{
return;
}
if (args.Handled) // if the event was already handled
{
return;
}
_solutionSystem.TryGetSolution(args.Used, AbsorbentComponent.SolutionName, out var absorbedSolution);
if (absorbedSolution is null)
{
return;
}
var toolAvailableVolume = absorbedSolution.AvailableVolume;
var toolCurrentVolume = absorbedSolution.CurrentVolume;
// For adding liquid to an empty floor tile
if (args.Target is null) // if a tile is clicked
{
ReleaseToFloor(args.ClickLocation, component, absorbedSolution);
args.Handled = true;
args.User.PopupMessage(args.User, Loc.GetString("mopping-system-release-to-floor"));
return;
}
else if (args.Target is not null)
{
// Handle our do_after logic
HandleDoAfter(args.User, args.Used, args.Target.Value, component, toolCurrentVolume, toolAvailableVolume);
}
args.Handled = true;
return;
}
private void ReleaseToFloor(EntityCoordinates clickLocation, AbsorbentComponent absorbent, Solution? absorbedSolution)
{
if ((_mapManager.TryGetGrid(clickLocation.GetGridId(EntityManager), out var mapGrid)) // needs valid grid
&& absorbedSolution is not null) // needs a solution to place on the tile
{
TileRef tile = mapGrid.GetTileRef(clickLocation);
// Drop some of the absorbed liquid onto the ground
var releaseAmount = FixedPoint2.Min(absorbent.ResidueAmount, absorbedSolution.CurrentVolume); // The release amount specified on the absorbent component, or the amount currently absorbed (whichever is less).
var releasedSolution = _solutionSystem.SplitSolution(absorbent.Owner, absorbedSolution, releaseAmount); // Remove releaseAmount of solution from the absorbent component
_spillableSystem.SpillAt(tile, releasedSolution, puddlePrototypeId); // And spill it onto the tile.
}
}
// Handles logic for our different types of valid target.
// Checks for conditions that would prevent a doAfter from starting.
private void HandleDoAfter(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, FixedPoint2 currentVolume, FixedPoint2 availableVolume)
{
// Below variables will be set within this function depending on what kind of target was clicked.
// They will be passed to the OnTransferComplete if the doAfter succeeds.
EntityUid donor;
EntityUid acceptor;
string donorSolutionName;
string acceptorSolutionName;
FixedPoint2 transferAmount;
var delay = 1.0f; //default do_after delay in seconds.
string msg;
SoundSpecifier sfx;
// For our purposes, if our target has a PuddleComponent, treat it as a puddle above all else.
if (TryComp<PuddleComponent>(target, out var puddle))
{
// These return conditions will abort BEFORE the do_after is called:
if(!_solutionSystem.TryGetSolution(target, puddle.SolutionName, out var puddleSolution) // puddle Solution is null
|| (puddleSolution.TotalVolume <= 0)) // puddle is completely empty
{
return;
}
else if (availableVolume < 0) // mop is completely full
{
msg = "mopping-system-tool-full";
user.PopupMessage(user, Loc.GetString(msg, ("used", used))); // play message now because we are aborting.
return;
}
// adding to puddles
else if (puddleSolution.TotalVolume < component.MopLowerLimit // if the puddle is too small for the tool to effectively absorb any more solution from it
&& currentVolume > 0) // tool needs a solution to dilute the puddle with.
{
// Dilutes the puddle with some solution from the tool
transferAmount = FixedPoint2.Max(component.ResidueAmount, currentVolume);
TryTransfer(used, target, "absorbed", puddle.SolutionName, transferAmount); // Complete the transfer right away, with no doAfter.
sfx = component.TransferSound;
SoundSystem.Play(Filter.Pvs(user), sfx.GetSound(), used); // Give instant feedback for diluting puddle, so that it's clear that the player is adding to the puddle (as opposed to other behaviours, which have a doAfter).
msg = "mopping-system-puddle-diluted";
user.PopupMessage(user, Loc.GetString(msg)); // play message now because we are aborting.
return; // Do not begin a doAfter.
}
else
{
// Taking from puddles:
// Determine transferAmount:
transferAmount = FixedPoint2.Min(component.PickupAmount, puddleSolution.TotalVolume, availableVolume);
// TODO: consider onelining this with the above, using additional args on Min()?
if ((puddleSolution.TotalVolume - transferAmount) < component.MopLowerLimit) // If the transferAmount would bring the puddle below the MopLowerLimit
{
transferAmount = puddleSolution.TotalVolume - component.MopLowerLimit; // Then the transferAmount should bring the puddle down to the MopLowerLimit exactly
}
donor = target; // the puddle Uid
donorSolutionName = puddle.SolutionName;
acceptor = used; // the mop/tool Uid
acceptorSolutionName = "absorbed"; // by definition on AbsorbentComponent
// Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter.
delay = (component.PickupAmount.Float() / 10.0f) * component.MopSpeed; // Delay should scale with PickupAmount, which represents the maximum we can pick up per click.
msg = "mopping-system-puddle-success";
sfx = component.PickupSound;
DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx);
}
}
else if ((TryComp<RefillableSolutionComponent>(target, out var refillable)) // We can put solution from the tool into the target
&& (currentVolume > 0)) // And the tool is wet
{
// These return conditions will abort BEFORE the do_after is called:
if (!_solutionSystem.TryGetRefillableSolution(target, out var refillableSolution)) // refillable Solution is null
{
return;
}
else if (refillableSolution.AvailableVolume <= 0) // target container is full (liquid destination)
{
msg = "mopping-system-target-container-full";
user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are aborting.
return;
}
else if (refillableSolution.MaxVolume <= FixedPoint2.New(20)) // target container is too small (e.g. syringe)
{
msg = "mopping-system-target-container-too-small";
user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are aborting.
return;
}
else
{
// Determine transferAmount
if (_tagSystem.HasTag(used, "Mop") // if the tool used is a literal mop (and not a sponge, rag, etc.)
&& !_tagSystem.HasTag(target, "Wringer")) // and if the target does not have a wringer for properly drying the mop
{
delay = 5.0f; // Should take much longer if you don't have a wringer
if ((currentVolume / (currentVolume + availableVolume) ) > 0.25) // mop is more than one-quarter full
{
transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume * 0.6); // squeeze up to 60% of the solution from the mop.
msg = "mopping-system-hand-squeeze-little-wet";
if ((currentVolume / (currentVolume + availableVolume) ) > 0.5) // if the mop is more than half full
msg = "mopping-system-hand-squeeze-still-wet"; // overwrites the above
}
else // mop is less than one-quarter full
{
transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume); // squeeze remainder of solution from the mop.
msg = "mopping-system-hand-squeeze-dry";
}
}
else
{
transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume); //Transfer all liquid from the tool to the container, but only if it will fit.
msg = "mopping-system-refillable-success";
delay = 1.0f;
}
donor = used; // the mop/tool Uid
donorSolutionName = "absorbed"; // by definition on AbsorbentComponent
acceptor = target; // the refillable container's Uid
acceptorSolutionName = refillable.Solution;
// Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter.
sfx = component.TransferSound;
DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx);
}
}
else if (TryComp<DrainableSolutionComponent>(target, out var drainable) // We can take solution from the target
&& currentVolume <= 0 ) // tool is dry
{
// These return conditions will abort BEFORE the do_after is called:
if (!_solutionSystem.TryGetDrainableSolution(target, out var drainableSolution))
{
return;
}
else if (drainableSolution.CurrentVolume <= 0) // target container is empty (liquid source)
{
msg = "mopping-system-target-container-empty";
user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are returning.
return;
}
else
{
// Determine transferAmount
transferAmount = FixedPoint2.Min(availableVolume * 0.5, drainableSolution.CurrentVolume); // Let's transfer up to to half the tool's available capacity to the tool.
donor = target; // the drainable container's Uid
donorSolutionName = drainable.Solution;
acceptor = used; // the mop/tool Uid
acceptorSolutionName = "absorbed"; // by definition on AbsorbentComponent
// Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter.
// default delay is fine for this case.
msg = "mopping-system-drainable-success";
sfx = component.TransferSound;
DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx);
}
}
}
private void DoMopInteraction(EntityUid user, EntityUid used, EntityUid target, EntityUid donor, EntityUid acceptor,
AbsorbentComponent component, string donorSolutionName, string acceptorSolutionName,
FixedPoint2 transferAmount, float delay, string msg, SoundSpecifier sfx)
{
var doAfterArgs = new DoAfterEventArgs(user, delay, target: target)
{
BreakOnUserMove = true,
BreakOnStun = true,
BreakOnDamage = true,
MovementThreshold = 0.2f,
BroadcastCancelledEvent = new TransferCancelledEvent()
{
Target = target,
Component = component // (the AbsorbentComponent)
},
BroadcastFinishedEvent = new TransferCompleteEvent()
{
User = user,
Tool = used,
Target = target,
Donor = donor,
Acceptor = acceptor,
Component = component,
DonorSolutionName = donorSolutionName,
AcceptorSolutionName = acceptorSolutionName,
Message = msg,
Sound = sfx,
TransferAmount = transferAmount
}
};
// Can't interact with too many entities at once.
if (component.MaxInteractingEntities < component.InteractingEntities.Count + 1)
return;
// Can't interact with the same container multiple times at once.
if (!component.InteractingEntities.Add(target))
return;
var result = _doAfterSystem.WaitDoAfter(doAfterArgs);
}
private void OnTransferComplete(TransferCompleteEvent ev)
{
SoundSystem.Play(Filter.Pvs(ev.User), ev.Sound.GetSound(), ev.Tool); // Play the After SFX
ev.User.PopupMessage(ev.User, Loc.GetString(ev.Message, ("target", ev.Target), ("used", ev.Tool))); // Play the After popup message
TryTransfer(ev.Donor, ev.Acceptor, ev.DonorSolutionName, ev.AcceptorSolutionName, ev.TransferAmount);
ev.Component.InteractingEntities.Remove(ev.Target); // Tell the absorbentComponent that we have stopped interacting with the target.
return;
}
private void OnTransferCancelled(TransferCancelledEvent ev)
{
if (!ev.Component.Deleted)
ev.Component.InteractingEntities.Remove(ev.Target); // Tell the absorbentComponent that we have stopped interacting with the target.
return;
}
private void TryTransfer(EntityUid donor, EntityUid acceptor, string donorSolutionName, string acceptorSolutionName, FixedPoint2 transferAmount)
{
if (_solutionSystem.TryGetSolution(donor, donorSolutionName, out var donorSolution) // If the donor solution is valid
&& _solutionSystem.TryGetSolution(acceptor, acceptorSolutionName, out var acceptorSolution)) // And the acceptor solution is valid
{
var solutionToTransfer = _solutionSystem.SplitSolution(donor, donorSolution, transferAmount); // Split a portion of the donor solution
_solutionSystem.TryAddSolution(acceptor, acceptorSolution, solutionToTransfer); // And add it to the acceptor solution
}
}
}
public sealed class TransferCompleteEvent : EntityEventArgs
{
public EntityUid User;
public EntityUid Tool;
public EntityUid Target;
public EntityUid Donor;
public EntityUid Acceptor;
public AbsorbentComponent Component { get; init; } = default!;
public string DonorSolutionName = "";
public string AcceptorSolutionName = "";
public string Message = "";
public SoundSpecifier Sound { get; init; } = default!;
public FixedPoint2 TransferAmount;
}
public sealed class TransferCancelledEvent : EntityEventArgs
{
public EntityUid Target;
public AbsorbentComponent Component { get; init; } = default!;
}

View file

@ -71,7 +71,7 @@ namespace Content.Server.GameTicking
private TickerLobbyStatusEvent GetStatusMsg(IPlayerSession session)
{
_playersInLobby.TryGetValue(session, out var status);
return new TickerLobbyStatusEvent(RunLevel != GameRunLevel.PreRoundLobby, LobbySong, status == LobbyPlayerStatus.Ready, _roundStartTime, Paused);
return new TickerLobbyStatusEvent(RunLevel != GameRunLevel.PreRoundLobby, LobbySong, LobbyBackground,status == LobbyPlayerStatus.Ready, _roundStartTime, Paused);
}
private void SendStatusToAll()

View file

@ -0,0 +1,33 @@
using System.IO;
using System.Linq;
using Content.Shared.Audio;
using Robust.Shared.ContentPack;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameTicking;
public sealed partial class GameTicker
{
[Dependency] private readonly IResourceManager _resourceManager = default!;
[ViewVariables]
public string? LobbyBackground { get; private set; }
[ViewVariables]
private List<ResourcePath>? _lobbyBackgrounds;
private static readonly string[] WhitelistedBackgroundExtensions = new string[] {"png", "jpg", "jpeg"};
private const string LobbyScreenPath = "/Textures/LobbyScreens";
private void InitializeLobbyBackground()
{
_lobbyBackgrounds = _resourceManager.ContentFindFiles(new ResourcePath(LobbyScreenPath)).ToList();
_lobbyBackgrounds = _lobbyBackgrounds.FindAll(path => WhitelistedBackgroundExtensions.Contains((path.Extension)));
LobbyBackground = _lobbyBackgrounds.Any() ? _robustRandom.Pick(_lobbyBackgrounds).ToString() : null;
}
}

View file

@ -50,6 +50,7 @@ namespace Content.Server.GameTicking
InitializeCVars();
InitializePlayer();
InitializeLobbyMusic();
InitializeLobbyBackground();
InitializeGamePreset();
InitializeJobController();
InitializeUpdates();

View file

@ -1,7 +1,9 @@
using System.Threading;
using Content.Server.UserInterface;
using Content.Shared.MedicalScanner;
using Content.Shared.Disease;
using Robust.Server.GameObjects;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Medical.Components
{
@ -23,5 +25,12 @@ namespace Content.Server.Medical.Components
/// </summary>
public CancellationTokenSource? CancelToken;
public BoundUserInterface? UserInterface => Owner.GetUIOrNull(HealthAnalyzerUiKey.Key);
/// <summary>
/// The disease this will give people.
/// </summary>
[DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>))]
[ViewVariables(VVAccess.ReadWrite)]
public string? Disease;
}
}

View file

@ -1,17 +1,22 @@
using System.Threading;
using Content.Server.DoAfter;
using Content.Server.Medical.Components;
using Content.Server.Disease;
using Content.Server.Popups;
using Content.Shared.Damage;
using Content.Shared.Interaction;
using Content.Shared.MobState.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using static Content.Shared.MedicalScanner.SharedHealthAnalyzerComponent;
namespace Content.Server.Medical
{
public sealed class HealthAnalyzerSystem : EntitySystem
{
[Dependency] private readonly DiseaseSystem _disease = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
public override void Initialize()
{
@ -64,6 +69,22 @@ namespace Content.Server.Medical
{
args.Component.CancelToken = null;
UpdateScannedUser(args.Component.Owner, args.User, args.Target, args.Component);
// Below is for the traitor item
// Piggybacking off another component's doafter is complete CBT so I gave up
// and put it on the same component
if (string.IsNullOrEmpty(args.Component.Disease) || args.Target == null)
return;
_disease.TryAddDisease(args.Target.Value, args.Component.Disease);
if (args.User == args.Target)
{
_popupSystem.PopupEntity(Loc.GetString("disease-scanner-gave-self", ("disease", args.Component.Disease)),
args.User, Filter.Entities(args.User));
return;
}
_popupSystem.PopupEntity(Loc.GetString("disease-scanner-gave-other", ("target", args.Target), ("disease", args.Component.Disease)),
args.User, Filter.Entities(args.User));
}
private void OpenUserInterface(EntityUid user, HealthAnalyzerComponent healthAnalyzer)

View file

@ -59,10 +59,9 @@ namespace Content.Server.Medical
private void OnActivated(EntityUid uid, MedicalScannerComponent scannerComponent, ActivateInWorldEvent args)
{
if (!TryComp<ActorComponent>(args.User, out var actor) || !IsPowered(scannerComponent))
if (!IsPowered(scannerComponent))
return;
scannerComponent.UserInterface?.Toggle(actor.PlayerSession);
UpdateUserInterface(uid, scannerComponent);
}

View file

@ -0,0 +1,64 @@
using System.Linq;
using Content.Server.Chat.Managers;
using Content.Server.Disease.Components;
using Content.Server.Disease;
using Content.Shared.Disease;
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
namespace Content.Server.StationEvents.Events;
/// <summary>
/// Infects a couple people
/// with a random disease that isn't super deadly
/// </summary>
public sealed class DiseaseOutbreak : StationEvent
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
/// <summary>
/// Disease prototypes I decided were not too deadly for a random event
/// </summary>
public readonly IReadOnlyList<string> NotTooSeriousDiseases = new[]
{
"SpaceCold",
"VanAusdallsRobovirus",
"VentCough",
"AMIV"
};
public override string Name => "DiseaseOutbreak";
public override float Weight => WeightNormal;
protected override float EndAfter => 1.0f;
/// <summary>
/// Finds 2-5 random entities that can host diseases
/// and gives them a randomly selected disease.
/// They all get the same disease.
/// </summary>
public override void Startup()
{
base.Startup();
var targetList = _entityManager.EntityQuery<DiseaseCarrierComponent>().ToList();
_random.Shuffle(targetList);
var toInfect = _random.Next(2, 5);
var diseaseName = _random.Pick(NotTooSeriousDiseases);
if (!_prototypeManager.TryIndex(diseaseName, out DiseasePrototype? disease))
return;
var diseaseSystem = EntitySystem.Get<DiseaseSystem>();
foreach (var target in targetList)
{
if (toInfect-- == 0)
break;
diseaseSystem.TryAddDisease(target.Owner, disease, target);
}
_chatManager.DispatchStationAnnouncement(Loc.GetString("station-event-disease-outbreak-announcement"));
}
}

View file

@ -42,7 +42,7 @@ public sealed class VentClog : StationEvent
public readonly IReadOnlyList<string> SafeishVentChemicals = new[]
{
"Water", "Iron", "Oxygen", "Tritium", "Plasma", "SulfuricAcid", "Blood", "SpaceDrugs", "SpaceCleaner", "Flour",
"Nutriment", "Sugar", "SpaceLube", "Ethanol", "Mercury", "Ephedrine", "WeldingFuel"
"Nutriment", "Sugar", "SpaceLube", "Ethanol", "Mercury", "Ephedrine", "WeldingFuel", "VentCrud"
};
public override void Startup()

View file

@ -33,6 +33,22 @@ namespace Content.Server.UserInterface
[DataField("verbText")]
public string VerbText = "ui-verb-toggle-open";
/// <summary>
/// Whether you need a hand to operate this UI. The hand does not need to be free, you just need to have one.
/// </summary>
/// <remarks>
/// This should probably be true for most machines & computers, but there will still be UIs that represent a
/// more generic interaction / configuration that might not require hands.
/// </remarks>
[DataField("requireHands")]
public bool RequireHands = true;
/// <summary>
/// Whether spectators (non-admin ghosts) should be allowed to view this UI.
/// </summary>
[DataField("allowSpectator")]
public bool AllowSpectator = true;
/// <summary>
/// The client channel currently using the object, or null if there's none/not single user.
/// NOTE: DO NOT DIRECTLY SET, USE ActivatableUISystem.SetCurrentSingleUser

View file

@ -1,8 +1,7 @@
using Content.Server.Administration.Managers;
using Content.Server.Ghost.Components;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Verbs;
@ -28,12 +27,25 @@ namespace Content.Server.UserInterface
// *THIS IS A BLATANT WORKAROUND!* RATIONALE: Microwaves need it
SubscribeLocalEvent<ActivatableUIComponent, EntParentChangedMessage>(OnParentChanged);
SubscribeLocalEvent<ActivatableUIComponent, BoundUIClosedEvent>(OnUIClose);
SubscribeLocalEvent<BoundUserInterfaceMessageAttempt>(OnBoundInterfaceInteractAttempt);
SubscribeLocalEvent<ActivatableUIComponent, GetVerbsEvent<ActivationVerb>>(AddOpenUiVerb);
SubscribeLocalEvent<ServerUserInterfaceComponent, OpenUiActionEvent>(OnActionPerform);
}
private void OnBoundInterfaceInteractAttempt(BoundUserInterfaceMessageAttempt ev)
{
if (!TryComp(ev.Target, out ActivatableUIComponent? comp))
return;
if (!comp.RequireHands)
return;
if (!TryComp(ev.Sender.AttachedEntity, out SharedHandsComponent? hands) || hands.Hands.Count == 0)
ev.Cancel();
}
private void OnActionPerform(EntityUid uid, ServerUserInterfaceComponent component, OpenUiActionEvent args)
{
if (args.Handled || args.Key == null)
@ -57,7 +69,7 @@ namespace Content.Server.UserInterface
if (component.InHandsOnly && args.Using != uid)
return;
if (!args.CanInteract && !HasComp<GhostComponent>(args.User))
if (!args.CanInteract && (!component.AllowSpectator || !HasComp<GhostComponent>(args.User)))
return;
ActivationVerb verb = new();

View file

@ -0,0 +1,37 @@
using Content.Shared.Disease;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// Spawn a random disease at regular intervals when artifact activated.
/// </summary>
[RegisterComponent]
public sealed class DiseaseArtifactComponent : Component
{
public override string Name => "DiseaseArtifact";
/// <summary>
/// Disease the artifact will spawn
/// If empty, picks a random one from its list
/// </summary>
[DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>))]
[ViewVariables(VVAccess.ReadWrite)]
public string SpawnDisease = string.Empty;
/// <summary>
/// How far away it will check for people
/// If empty, picks a random one from its list
/// </summary>
[DataField("range")]
[ViewVariables(VVAccess.ReadWrite)]
public float Range = 5f;
[ViewVariables(VVAccess.ReadWrite)]
public DiseasePrototype ResolveDisease = default!;
[ViewVariables(VVAccess.ReadWrite)]
public readonly IReadOnlyList<string> ArtifactDiseases = new[]
{
"VanAusdallsRobovirus",
"OwOnavirus",
"BleedersBite",
"Ultragigacancer",
"AMIV"
};
}

View file

@ -0,0 +1,63 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Shared.Disease;
using Content.Server.Disease;
using Content.Server.Disease.Components;
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
using Content.Shared.Interaction;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems
{
/// <summary>
/// Handles disease-producing artifacts
/// </summary>
public sealed class DiseaseArtifactSystem : EntitySystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DiseaseArtifactComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<DiseaseArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
/// <summary>
/// Makes sure this artifact is assigned a disease
/// </summary>
private void OnMapInit(EntityUid uid, DiseaseArtifactComponent component, MapInitEvent args)
{
if (component.SpawnDisease == string.Empty && component.ArtifactDiseases.Count != 0)
{
var diseaseName = _random.Pick(component.ArtifactDiseases);
component.SpawnDisease = diseaseName;
}
if (_prototypeManager.TryIndex(component.SpawnDisease, out DiseasePrototype? disease) && disease != null)
component.ResolveDisease = disease;
}
/// <summary>
/// When activated, blasts everyone in LOS within 3 tiles
/// with a high-probability disease infection attempt
/// </summary>
private void OnActivate(EntityUid uid, DiseaseArtifactComponent component, ArtifactActivatedEvent args)
{
var xform = Transform(uid);
foreach (var entity in _lookup.GetEntitiesInRange(xform.MapID, xform.WorldPosition, 3f))
{
if (!_interactionSystem.InRangeUnobstructed(uid, entity, 3f))
continue;
if (TryComp<DiseaseCarrierComponent>(entity, out var carrier))
EntitySystem.Get<DiseaseSystem>().TryInfect(carrier, component.ResolveDisease);
}
}
}
}

View file

@ -1,7 +1,5 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Chemistry.Reagent
{

View file

@ -0,0 +1,36 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Clothing;
[RegisterComponent, NetworkedComponent, Friend(typeof(ClothingSpeedModifierSystem))]
public sealed class ClothingSpeedModifierComponent : Component
{
[DataField("walkModifier", required: true)] [ViewVariables(VVAccess.ReadWrite)]
public float WalkModifier = 1.0f;
[DataField("sprintModifier", required: true)] [ViewVariables(VVAccess.ReadWrite)]
public float SprintModifier = 1.0f;
/// <summary>
/// Is this clothing item currently 'actively' slowing you down?
/// e.g. magboots can be turned on and off.
/// </summary>
[DataField("enabled")] public bool Enabled = true;
}
[Serializable, NetSerializable]
public sealed class ClothingSpeedModifierComponentState : ComponentState
{
public float WalkModifier;
public float SprintModifier;
public bool Enabled;
public ClothingSpeedModifierComponentState(float walkModifier, float sprintModifier, bool enabled)
{
WalkModifier = walkModifier;
SprintModifier = sprintModifier;
Enabled = enabled;
}
}

View file

@ -0,0 +1,70 @@
using Content.Shared.Movement.EntitySystems;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
namespace Content.Shared.Clothing;
public sealed class ClothingSpeedModifierSystem : EntitySystem
{
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ClothingSpeedModifierComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<ClothingSpeedModifierComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<ClothingSpeedModifierComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMoveSpeed);
}
// Public API
public void SetClothingSpeedModifierEnabled(EntityUid uid, bool enabled, ClothingSpeedModifierComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return;
if (component.Enabled != enabled)
{
component.Enabled = enabled;
Dirty(component);
// inventory system will automatically hook into the event raised by this and update accordingly
if (_container.TryGetContainingContainer(uid, out var container))
{
_movementSpeed.RefreshMovementSpeedModifiers(container.Owner);
}
}
}
// Event handlers
private void OnGetState(EntityUid uid, ClothingSpeedModifierComponent component, ref ComponentGetState args)
{
args.State = new ClothingSpeedModifierComponentState(component.WalkModifier, component.SprintModifier, component.Enabled);
}
private void OnHandleState(EntityUid uid, ClothingSpeedModifierComponent component, ref ComponentHandleState args)
{
if (args.Current is ClothingSpeedModifierComponentState state)
{
component.WalkModifier = state.WalkModifier;
component.SprintModifier = state.SprintModifier;
component.Enabled = state.Enabled;
if (_container.TryGetContainingContainer(uid, out var container))
{
_movementSpeed.RefreshMovementSpeedModifiers(container.Owner);
}
}
}
private void OnRefreshMoveSpeed(EntityUid uid, ClothingSpeedModifierComponent component, RefreshMovementSpeedModifiersEvent args)
{
if (!component.Enabled)
return;
args.ModifySpeed(component.WalkModifier, component.SprintModifier);
}
}

View file

@ -18,22 +18,8 @@ namespace Content.Shared.Clothing
protected void OnChanged()
{
EntitySystem.Get<SharedActionsSystem>().SetToggled(ToggleAction, On);
// inventory system will automatically hook into the event raised by this and update accordingly
if (Owner.TryGetContainer(out var container))
{
EntitySystem.Get<MovementSpeedModifierSystem>().RefreshMovementSpeedModifiers(container.Owner);
}
EntitySystem.Get<ClothingSpeedModifierSystem>().SetClothingSpeedModifierEnabled(Owner, On);
}
[DataField("walkMoveCoeffecient", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public float WalkMoveCoeffecient = 0.85f;
[DataField("sprintMoveCoeffecient", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public float SprintMoveCoeffecient = 0.65f;
public float WalkSpeedModifier => On ? WalkMoveCoeffecient : 1;
public float SprintSpeedModifier => On ? SprintMoveCoeffecient : 1;
[Serializable, NetSerializable]
public sealed class MagbootsComponentState : ComponentState

View file

@ -13,7 +13,7 @@
</PropertyGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Content.Shared.Database\Content.Shared.Database.csproj" />

View file

@ -0,0 +1,24 @@
using JetBrains.Annotations;
namespace Content.Shared.Disease
{
[ImplicitDataDefinitionForInheritors]
[MeansImplicitUse]
public abstract class DiseaseCure
{
/// <summary>
/// This returns true if the disease should be cured
/// and false otherwise
/// </summary>
public abstract bool Cure(DiseaseEffectArgs args);
/// <summary>
/// This is used by the disease diangoser machine
/// to generate reports to tell people all of a disease's
/// special cures using in-game methods.
/// So it should return a localization string describing
/// the cure
/// </summary>
public abstract string CureText();
}
}

View file

@ -0,0 +1,29 @@
using JetBrains.Annotations;
namespace Content.Shared.Disease
{
[ImplicitDataDefinitionForInheritors]
[MeansImplicitUse]
public abstract class DiseaseEffect
{
/// <summary>
/// What's the chance, from 0 to 1, that this effect will occur?
/// </summary>
[DataField("probability")]
public float Probability = 1.0f;
/// <summary>
/// What effect the disease will have.
/// </summary>
public abstract void Effect(DiseaseEffectArgs args);
}
/// <summary>
/// What you have to work with in any disease effect/cure.
/// Includes an entity manager because it is out of scope
/// otherwise.
/// </summary>
public readonly record struct DiseaseEffectArgs(
EntityUid DiseasedEntity,
DiseasePrototype Disease,
IEntityManager EntityManager
);
}

View file

@ -0,0 +1,16 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Disease
{
[Serializable, NetSerializable]
/// <summary>
/// Stores bools for if the machine is on
/// and if it's currently running.
/// Used for the visualizer
/// </summary>
public enum DiseaseMachineVisuals : byte
{
IsOn,
IsRunning
}
}

View file

@ -0,0 +1,69 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Disease
{
/// <summary>
/// Diseases encompass everything from viruses to cancers to heart disease.
/// It's not just a virology thing.
/// </summary>
[Prototype("disease")]
[DataDefinition]
public sealed class DiseasePrototype : IPrototype, IInheritingPrototype
{
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; } = default!;
[DataField("name")]
public string Name { get; } = string.Empty;
[DataField("parent", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>))]
public string? Parent { get; private set; }
[NeverPushInheritance]
[DataField("abstract")]
public bool Abstract { get; private set; }
/// <summary>
/// Controls how often a disease ticks.
/// </summary>
public float TickTime = 1f;
/// <summary>
/// Since disease isn't mapped to metabolism or anything,
/// it needs something to control its tickrate
/// </summary>
public float Accumulator = 0f;
/// <summary>
/// List of effects the disease has that will
/// run every second (by default anyway)
/// </summary>
[DataField("effects", serverOnly: true)]
public readonly List<DiseaseEffect> Effects = new(0);
/// <summary>
/// List of SPECIFIC CURES the disease has that will
/// be checked every second.
/// Stuff like spaceacillin operates outside this.
/// </summary>
[DataField("cures", serverOnly: true)]
public readonly List<DiseaseCure> Cures = new(0);
/// <summary>
/// This flatly reduces the probabilty disease medicine
/// has to cure it every tick. Although, since spaceacillin is
/// used as a reference and it has 0.15 chance, this is
/// a base 33% reduction in cure chance
/// </summary>
[DataField("cureResist", serverOnly: true)]
public float CureResist = 0.05f;
/// <summary>
/// Whether the disease can infect other people.
/// Since this isn't just a virology thing, this
/// primary determines what sort of disease it is.
/// This also affects things like the vaccine machine.
/// You can't print a cancer vaccine
/// </summary>
[DataField("infectious", serverOnly: true)]
public bool Infectious = true;
}
}

View file

@ -0,0 +1,12 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Disease.Components
{
[NetworkedComponent]
[RegisterComponent]
/// This is added to anyone with at least 1 disease
/// and helps cull event subscriptions and entity queries
/// when they are not relevant.
public sealed class DiseasedComponent : Component
{}
}

View file

@ -44,15 +44,17 @@ namespace Content.Shared.GameTicking
{
public bool IsRoundStarted { get; }
public string? LobbySong { get; }
public string? LobbyBackground { get; }
public bool YouAreReady { get; }
// UTC.
public TimeSpan StartTime { get; }
public bool Paused { get; }
public TickerLobbyStatusEvent(bool isRoundStarted, string? lobbySong, bool youAreReady, TimeSpan startTime, bool paused)
public TickerLobbyStatusEvent(bool isRoundStarted, string? lobbySong, string? lobbyBackground, bool youAreReady, TimeSpan startTime, bool paused)
{
IsRoundStarted = isRoundStarted;
LobbySong = lobbySong;
LobbyBackground = lobbyBackground;
YouAreReady = youAreReady;
StartTime = startTime;
Paused = paused;

View file

@ -12,10 +12,10 @@
</PropertyGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
<ItemGroup>
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.15.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -8,7 +8,7 @@ absconditus.ogg (Absconditus) by ZhayTee is licensed under CC-BY-NC-SA-3.0. It w
atomicamnesiammx.ogg (Atomic Amnesia MMX) by Phillip Dyer is licensed under CC-BY-NC-SA-3.0. It was converted from MP3 to OGG. The source can be found here: https://soundcloud.com/3kliksphilip/atomic-amnesia-mmx-1
singuloose.ogg (Singuloose) by Janis Schiedková is licensed under a CC-BY-NC-SA 3.0. It was converted from MP3 to OGG. The source can be found here: https://soundcloud.com/js_m/singuloose
singuloose.ogg (Singuloose) by Janis Schiedková is licensed under a CC-BY-NC-SA 3.0. It was converted from MP3 to OGG. The source can be found here: https://soundcloud.com/js_m/singuloose
vibe_ace.ogg (Vibe Ace) by Kevin MacLeod is licensed under CC-BY-3.0. It was converted from MP3 to OGG. The source can be found here: https://freemusicarchive.org/music/Kevin_MacLeod/Jazz_Sampler/Vibe_Ace
@ -17,3 +17,5 @@ Title3.ogg (Title3) By Cuboos is licensed under a BY-NC-SA 3.0 Creative Commons
Marhaba.ogg (Marhaba) by Ian Alex Mac is licensed under a BY 4.0 Creative Commons License. It was converted from MP3 to OGG. The source can be found here: https://freemusicarchive.org/music/Ian_Alex_Mac/Cues/Marhaba
Comet-Haley.ogg (Comet Haley) by Stellardrone is licensed under a BY 3.0 Creative Commons License. It was converted from MP3 to OGG. The source can be found here: https://freemusicarchive.org/music/Stellardrone/Light_Years_1227/07_Comet_Halley
Flip Flap.ogg (Flip Flap) by X-ceed is licensed by soundimage.org and is free for non-commercial use. It was converted from MP3 to OGG. The source is here http://aminet.net/package/mods/xceed/Flipflap

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,3 @@
diagnoser_printing.ogg taken from https://freesound.org/people/RobSp1derp1g/sounds/615419/ and edited
vaccinator_running.ogg taken from https://freesound.org/people/RutgerMuller/sounds/365413/ and edited

Binary file not shown.

Binary file not shown.

Binary file not shown.

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