Merge pull request #121 from crystallpunk-14/29-04-2024-upstream

29 04 2024 upstream
This commit is contained in:
Ed
2024-04-29 16:39:56 +03:00
committed by GitHub
565 changed files with 6918 additions and 3437 deletions

16
.github/labeler.yml vendored
View File

@@ -1,12 +1,18 @@
"Changes: Sprites":
- '**/*.rsi/*.png'
- changed-files:
- any-glob-to-any-file: '**/*.rsi/*.png'
"Changes: Map":
- 'Resources/Maps/*.yml'
- 'Resources/Prototypes/Maps/*.yml'
- changed-files:
- any-glob-to-any-file:
- 'Resources/Maps/*.yml'
- 'Resources/Prototypes/Maps/*.yml'
"Changes: UI":
- '**/*.xaml*'
- changed-files:
- any-glob-to-any-file: '**/*.xaml*'
"No C#":
- all: ["!**/*.cs"]
- changed-files:
# Equiv to any-glob-to-all as long as this has one matcher. If ALL changed files are not C# files, then apply label.
- all-globs-to-all-files: "!**/*.cs"

View File

@@ -1,18 +1,20 @@
name: Check Merge Conflicts
on:
push:
branches:
- master
pull_request_target:
types:
- opened
- synchronize
- reopened
- ready_for_review
jobs:
Label:
if: github.actor != 'PJBot'
if: ( github.event.pull_request.draft == false ) && ( github.actor != 'PJBot' )
runs-on: ubuntu-latest
steps:
- name: Check for Merge Conflicts
uses: ike709/actions-label-merge-conflict@9eefdd17e10566023c46d2dc6dc04fcb8ec76142
uses: eps1lon/actions-label-merge-conflict@v3.0.0
with:
dirtyLabel: "Merge Conflict"
repoToken: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -6,8 +6,9 @@ on:
jobs:
labeler:
if: github.actor != 'PJBot'
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v3
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
- uses: actions/labeler@v5

View File

@@ -1,3 +1,6 @@
using Content.Shared.Administration;
using Content.Shared.Administration.Managers;
using Content.Shared.Mind.Components;
using Content.Shared.Verbs;
using Robust.Client.Console;
using Robust.Shared.Utility;
@@ -11,10 +14,12 @@ namespace Content.Client.Administration.Systems
{
[Dependency] private readonly IClientConGroupController _clientConGroupController = default!;
[Dependency] private readonly IClientConsoleHost _clientConsoleHost = default!;
[Dependency] private readonly ISharedAdminManager _admin = default!;
public override void Initialize()
{
SubscribeLocalEvent<GetVerbsEvent<Verb>>(AddAdminVerbs);
}
private void AddAdminVerbs(GetVerbsEvent<Verb> args)
@@ -33,6 +38,24 @@ namespace Content.Client.Administration.Systems
};
args.Verbs.Add(verb);
}
if (!_admin.IsAdmin(args.User))
return;
if (_admin.HasAdminFlag(args.User, AdminFlags.Admin))
args.ExtraCategories.Add(VerbCategory.Admin);
if (_admin.HasAdminFlag(args.User, AdminFlags.Fun) && HasComp<MindContainerComponent>(args.Target))
args.ExtraCategories.Add(VerbCategory.Antag);
if (_admin.HasAdminFlag(args.User, AdminFlags.Debug))
args.ExtraCategories.Add(VerbCategory.Debug);
if (_admin.HasAdminFlag(args.User, AdminFlags.Fun))
args.ExtraCategories.Add(VerbCategory.Smite);
if (_admin.HasAdminFlag(args.User, AdminFlags.Admin))
args.ExtraCategories.Add(VerbCategory.Tricks);
}
}
}

View File

@@ -0,0 +1,31 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
BackButtonStyleClass="RadialMenuBackButton"
CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="450 450">
<!-- Main -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Actions/scream.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"/>
</ui:RadialMenuTextureButton>
</ui:RadialContainer>
<!-- General -->
<ui:RadialContainer Name="General" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<!-- Vocal -->
<ui:RadialContainer Name="Vocal" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<!-- Hands -->
<ui:RadialContainer Name="Hands" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
</ui:RadialMenu>

View File

@@ -0,0 +1,112 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Speech;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Chat.UI;
[GenerateTypedNameReferences]
public sealed partial class EmotesMenu : RadialMenu
{
[Dependency] private readonly EntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
private readonly SpriteSystem _spriteSystem;
public event Action<ProtoId<EmotePrototype>>? OnPlayEmote;
public EmotesMenu()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
_spriteSystem = _entManager.System<SpriteSystem>();
var main = FindControl<RadialContainer>("Main");
var emotes = _prototypeManager.EnumeratePrototypes<EmotePrototype>();
foreach (var emote in emotes)
{
var player = _playerManager.LocalSession?.AttachedEntity;
if (emote.Category == EmoteCategory.Invalid ||
emote.ChatTriggers.Count == 0 ||
!(player.HasValue && (emote.Whitelist?.IsValid(player.Value, _entManager) ?? true)) ||
(emote.Blacklist?.IsValid(player.Value, _entManager) ?? false))
continue;
if (!emote.Available &&
_entManager.TryGetComponent<SpeechComponent>(player.Value, out var speech) &&
!speech.AllowedEmotes.Contains(emote.ID))
continue;
var parent = FindControl<RadialContainer>(emote.Category.ToString());
var button = new EmoteMenuButton
{
StyleClasses = { "RadialMenuButton" },
SetSize = new Vector2(64f, 64f),
ToolTip = Loc.GetString(emote.Name),
ProtoId = emote.ID,
};
var tex = new TextureRect
{
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Texture = _spriteSystem.Frame0(emote.Icon),
TextureScale = new Vector2(2f, 2f),
};
button.AddChild(tex);
parent.AddChild(button);
foreach (var child in main.Children)
{
if (child is not RadialMenuTextureButton castChild)
continue;
if (castChild.TargetLayer == emote.Category.ToString())
{
castChild.Visible = true;
break;
}
}
}
// Set up menu actions
foreach (var child in Children)
{
if (child is not RadialContainer container)
continue;
AddEmoteClickAction(container);
}
}
private void AddEmoteClickAction(RadialContainer container)
{
foreach (var child in container.Children)
{
if (child is not EmoteMenuButton castChild)
continue;
castChild.OnButtonUp += _ =>
{
OnPlayEmote?.Invoke(castChild.ProtoId);
Close();
};
}
}
}
public sealed class EmoteMenuButton : RadialMenuTextureButton
{
public ProtoId<EmotePrototype> ProtoId { get; set; }
}

View File

@@ -86,7 +86,7 @@ public sealed class EyeLerpingSystem : EntitySystem
private void HandleMapChange(EntityUid uid, LerpingEyeComponent component, ref EntParentChangedMessage args)
{
// Is this actually a map change? If yes, stop any lerps
if (args.OldMapId != args.Transform.MapID)
if (args.OldMapId != args.Transform.MapUid)
component.LastRotation = GetRotation(uid, args.Transform);
}

View File

@@ -0,0 +1,48 @@
using Robust.Client.GameObjects;
using Content.Shared.Fax.Components;
using Content.Shared.Fax;
using Robust.Client.Animations;
namespace Content.Client.Fax.System;
/// <summary>
/// Visualizer for the fax machine which displays the correct sprite based on the inserted entity.
/// </summary>
public sealed class FaxVisualsSystem : EntitySystem
{
[Dependency] private readonly AnimationPlayerSystem _player = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FaxMachineComponent, AppearanceChangeEvent>(OnAppearanceChanged);
}
private void OnAppearanceChanged(EntityUid uid, FaxMachineComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (_appearance.TryGetData(uid, FaxMachineVisuals.VisualState, out FaxMachineVisualState visuals) && visuals == FaxMachineVisualState.Inserting)
{
_player.Play(uid, new Animation()
{
Length = TimeSpan.FromSeconds(2.4),
AnimationTracks =
{
new AnimationTrackSpriteFlick()
{
LayerKey = FaxMachineVisuals.VisualState,
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(component.InsertingState, 0f),
new AnimationTrackSpriteFlick.KeyFrame("icon", 2.4f),
}
}
}
}, "faxecute");
}
}
}

View File

@@ -40,7 +40,7 @@ public sealed class FaxBoundUi : BoundUserInterface
{
if (_dialogIsOpen)
return;
_dialogIsOpen = true;
var filters = new FileDialogFilters(new FileDialogFilters.Group("txt"));
await using var file = await _fileDialogManager.OpenFile(filters);
@@ -52,8 +52,27 @@ public sealed class FaxBoundUi : BoundUserInterface
}
using var reader = new StreamReader(file);
var firstLine = await reader.ReadLineAsync();
string? label = null;
var content = await reader.ReadToEndAsync();
SendMessage(new FaxFileMessage(content[..Math.Min(content.Length, FaxFileMessageValidation.MaxContentSize)], _window.OfficePaper));
if (firstLine is { })
{
if (firstLine.StartsWith('#'))
{
label = firstLine[1..].Trim();
}
else
{
content = firstLine + "\n" + content;
}
}
SendMessage(new FaxFileMessage(
label?[..Math.Min(label.Length, FaxFileMessageValidation.MaxLabelSize)],
content[..Math.Min(content.Length, FaxFileMessageValidation.MaxContentSize)],
_window.OfficePaper));
}
private void OnSendButtonPressed()

View File

@@ -4,6 +4,7 @@ using Content.Client.Chemistry.EntitySystems;
using Content.Client.Guidebook.Richtext;
using Content.Client.Message;
using Content.Client.UserInterface.ControlExtensions;
using Content.Shared.Body.Prototypes;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using JetBrains.Annotations;
@@ -128,7 +129,7 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
var groupLabel = new RichTextLabel();
groupLabel.SetMarkup(Loc.GetString("guidebook-reagent-effects-metabolism-group-rate",
("group", group), ("rate", effect.MetabolismRate)));
("group", _prototype.Index<MetabolismGroupPrototype>(group).LocalizedName), ("rate", effect.MetabolismRate)));
var descriptionLabel = new RichTextLabel
{
Margin = new Thickness(25, 0, 10, 0)

View File

@@ -139,7 +139,7 @@ namespace Content.Client.HealthAnalyzer.UI
var groupTitleText = $"{Loc.GetString(
"health-analyzer-window-damage-group-text",
("damageGroup", Loc.GetString("health-analyzer-window-damage-group-" + damageGroupId)),
("damageGroup", _prototypes.Index<DamageGroupPrototype>(damageGroupId).LocalizedName),
("amount", damageAmount)
)}";
@@ -170,7 +170,7 @@ namespace Content.Client.HealthAnalyzer.UI
var damageString = Loc.GetString(
"health-analyzer-window-damage-type-text",
("damageType", Loc.GetString("health-analyzer-window-damage-type-" + type)),
("damageType", _prototypes.Index<DamageTypePrototype>(type).LocalizedName),
("amount", typeAmount)
);

View File

@@ -60,6 +60,7 @@ namespace Content.Client.Input
human.AddFunction(ContentKeyFunctions.UseItemInHand);
human.AddFunction(ContentKeyFunctions.AltUseItemInHand);
human.AddFunction(ContentKeyFunctions.OpenCharacterMenu);
human.AddFunction(ContentKeyFunctions.OpenEmotesMenu);
human.AddFunction(ContentKeyFunctions.ActivateItemInWorld);
human.AddFunction(ContentKeyFunctions.ThrowItemInHand);
human.AddFunction(ContentKeyFunctions.AltActivateItemInWorld);

View File

@@ -0,0 +1,18 @@
using Content.Client.Labels.UI;
using Content.Shared.Labels;
using Content.Shared.Labels.Components;
using Content.Shared.Labels.EntitySystems;
namespace Content.Client.Labels.EntitySystems;
public sealed class HandLabelerSystem : SharedHandLabelerSystem
{
protected override void UpdateUI(Entity<HandLabelerComponent> ent)
{
if (UserInterfaceSystem.TryGetOpenUi(ent.Owner, HandLabelerUiKey.Key, out var bui)
&& bui is HandLabelerBoundUserInterface cBui)
{
cBui.Reload();
}
}
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Labels;
using Content.Shared.Labels.Components;
using Robust.Client.GameObjects;
namespace Content.Client.Labels.UI
@@ -8,11 +9,14 @@ namespace Content.Client.Labels.UI
/// </summary>
public sealed class HandLabelerBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IEntityManager _entManager = default!;
[ViewVariables]
private HandLabelerWindow? _window;
public HandLabelerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
@@ -27,24 +31,25 @@ namespace Content.Client.Labels.UI
_window.OnClose += Close;
_window.OnLabelChanged += OnLabelChanged;
Reload();
}
private void OnLabelChanged(string newLabel)
{
SendMessage(new HandLabelerLabelChangedMessage(newLabel));
}
/// <summary>
/// Update the UI state based on server-sent info
/// </summary>
/// <param name="state"></param>
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null || state is not HandLabelerBoundUserInterfaceState cast)
// Focus moment
if (_entManager.TryGetComponent(Owner, out HandLabelerComponent? labeler) &&
labeler.AssignedLabel.Equals(newLabel))
return;
_window.SetCurrentLabel(cast.CurrentLabel);
SendPredictedMessage(new HandLabelerLabelChangedMessage(newLabel));
}
public void Reload()
{
if (_window == null || !_entManager.TryGetComponent(Owner, out HandLabelerComponent? component))
return;
_window.SetCurrentLabel(component.AssignedLabel);
}
protected override void Dispose(bool disposing)

View File

@@ -9,17 +9,40 @@ namespace Content.Client.Labels.UI
{
public event Action<string>? OnLabelChanged;
/// <summary>
/// Is the user currently entering text into the control?
/// </summary>
private bool _focused;
// TODO LineEdit Make this a bool on the LineEdit control
private string _label = string.Empty;
public HandLabelerWindow()
{
RobustXamlLoader.Load(this);
LabelLineEdit.OnTextEntered += e => OnLabelChanged?.Invoke(e.Text);
LabelLineEdit.OnFocusExit += e => OnLabelChanged?.Invoke(e.Text);
LabelLineEdit.OnTextEntered += e =>
{
_label = e.Text;
OnLabelChanged?.Invoke(_label);
};
LabelLineEdit.OnFocusEnter += _ => _focused = true;
LabelLineEdit.OnFocusExit += _ =>
{
_focused = false;
LabelLineEdit.Text = _label;
};
}
public void SetCurrentLabel(string label)
{
LabelLineEdit.Text = label;
if (label == _label)
return;
_label = label;
if (!_focused)
LabelLineEdit.Text = label;
}
}
}

View File

@@ -10,6 +10,8 @@ using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.ResourceManagement;
using Robust.Client.Graphics;
using Robust.Shared.Prototypes;
namespace Content.Client.Lathe.UI;
@@ -19,6 +21,8 @@ public sealed partial class LatheMenu : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceCache _resources = default!;
private EntityUid _owner;
private readonly SpriteSystem _spriteSystem;
private readonly LatheSystem _lathe;
@@ -104,12 +108,21 @@ public sealed partial class LatheMenu : DefaultWindow
RecipeList.Children.Clear();
foreach (var prototype in sortedRecipesToShow)
{
var icon = prototype.Icon == null
? _spriteSystem.GetPrototypeIcon(prototype.Result).Default
: _spriteSystem.Frame0(prototype.Icon);
List<Texture> textures;
if (_prototypeManager.TryIndex(prototype.Result, out EntityPrototype? entityProto) && entityProto != null)
{
textures = SpriteComponent.GetPrototypeTextures(entityProto, _resources).Select(o => o.Default).ToList();
}
else
{
textures = prototype.Icon == null
? new List<Texture> { _spriteSystem.GetPrototypeIcon(prototype.Result).Default }
: new List<Texture> { _spriteSystem.Frame0(prototype.Icon) };
}
var canProduce = _lathe.CanProduce(_owner, prototype, quantity);
var control = new RecipeControl(prototype, () => GenerateTooltipText(prototype), canProduce, icon);
var control = new RecipeControl(prototype, () => GenerateTooltipText(prototype), canProduce, textures);
control.OnButtonPressed += s =>
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)

View File

@@ -5,11 +5,15 @@
Margin="0"
StyleClasses="ButtonSquare">
<BoxContainer Orientation="Horizontal">
<TextureRect
Name="RecipeTexture"
<LayeredTextureRect
Name="RecipeTextures"
Margin="0 0 4 0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="KeepAspectCentered"
MinSize="32 32"
Stretch="KeepAspectCentered" />
CanShrink="true"
/>
<Label Name="RecipeName" HorizontalExpand="True" />
</BoxContainer>
</Button>

View File

@@ -2,8 +2,8 @@ using Content.Shared.Research.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Graphics;
namespace Content.Client.Lathe.UI;
@@ -13,12 +13,12 @@ public sealed partial class RecipeControl : Control
public Action<string>? OnButtonPressed;
public Func<string> TooltipTextSupplier;
public RecipeControl(LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, Texture? texture = null)
public RecipeControl(LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, List<Texture> textures)
{
RobustXamlLoader.Load(this);
RecipeName.Text = recipe.Name;
RecipeTexture.Texture = texture;
RecipeTextures.Textures = textures;
Button.Disabled = !canProduce;
TooltipTextSupplier = tooltipTextSupplier;
Button.TooltipSupplier = SupplyTooltip;

View File

@@ -195,7 +195,7 @@ public sealed partial class ReplaySpectatorSystem
if (uid != _player.LocalEntity)
return;
if (args.Transform.MapUid != null || args.OldMapId == MapId.Nullspace)
if (args.Transform.MapUid != null || args.OldMapId == null)
return;
if (_spectatorData != null)

View File

@@ -0,0 +1,11 @@
<tips:TippyUI xmlns="https://spacestation14.io"
xmlns:tips="clr-namespace:Content.Client.Tips"
MinSize="64 64"
Visible="False">
<PanelContainer Name="LabelPanel" Access="Public" Visible="False" MaxWidth="300" MaxHeight="200">
<ScrollContainer Name="ScrollingContents" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="False" ReturnMeasure="True">
<RichTextLabel Name="Label" Access="Public"/>
</ScrollContainer>
</PanelContainer>
<SpriteView Name="Entity" Access="Public" MinSize="128 128"/>
</tips:TippyUI>

View File

@@ -0,0 +1,54 @@
using Content.Client.Paper;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Tips;
[GenerateTypedNameReferences]
public sealed partial class TippyUI : UIWidget
{
public TippyState State = TippyState.Hidden;
public bool ModifyLayers = true;
public TippyUI()
{
RobustXamlLoader.Load(this);
}
public void InitLabel(PaperVisualsComponent? visuals, IResourceCache resCache)
{
if (visuals == null)
return;
Label.ModulateSelfOverride = visuals.FontAccentColor;
if (visuals.BackgroundImagePath == null)
return;
LabelPanel.ModulateSelfOverride = visuals.BackgroundModulate;
var backgroundImage = resCache.GetResource<TextureResource>(visuals.BackgroundImagePath);
var backgroundImageMode = visuals.BackgroundImageTile ? StyleBoxTexture.StretchMode.Tile : StyleBoxTexture.StretchMode.Stretch;
var backgroundPatchMargin = visuals.BackgroundPatchMargin;
LabelPanel.PanelOverride = new StyleBoxTexture
{
Texture = backgroundImage,
TextureScale = visuals.BackgroundScale,
Mode = backgroundImageMode,
PatchMarginLeft = backgroundPatchMargin.Left,
PatchMarginBottom = backgroundPatchMargin.Bottom,
PatchMarginRight = backgroundPatchMargin.Right,
PatchMarginTop = backgroundPatchMargin.Top
};
}
public enum TippyState : byte
{
Hidden,
Revealing,
Speaking,
Hiding,
}
}

View File

@@ -0,0 +1,241 @@
using Content.Client.Gameplay;
using System.Numerics;
using Content.Client.Message;
using Content.Client.Paper;
using Content.Shared.CCVar;
using Content.Shared.Movement.Components;
using Content.Shared.Tips;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using static Content.Client.Tips.TippyUI;
namespace Content.Client.Tips;
public sealed class TippyUIController : UIController
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IResourceCache _resCache = default!;
[UISystemDependency] private readonly AudioSystem _audio = default!;
public const float Padding = 50;
public static Angle WaddleRotation = Angle.FromDegrees(10);
private EntityUid _entity;
private float _secondsUntilNextState;
private int _previousStep = 0;
private TippyEvent? _currentMessage;
private readonly Queue<TippyEvent> _queuedMessages = new();
public override void Initialize()
{
base.Initialize();
UIManager.OnScreenChanged += OnScreenChanged;
SubscribeNetworkEvent<TippyEvent>(OnTippyEvent);
}
private void OnTippyEvent(TippyEvent msg, EntitySessionEventArgs args)
{
_queuedMessages.Enqueue(msg);
}
public override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
var screen = UIManager.ActiveScreen;
if (screen == null)
{
_queuedMessages.Clear();
return;
}
var tippy = screen.GetOrAddWidget<TippyUI>();
_secondsUntilNextState -= args.DeltaSeconds;
if (_secondsUntilNextState <= 0)
NextState(tippy);
else
{
var pos = UpdatePosition(tippy, screen.Size, args); ;
LayoutContainer.SetPosition(tippy, pos);
}
}
private Vector2 UpdatePosition(TippyUI tippy, Vector2 screenSize, FrameEventArgs args)
{
if (_currentMessage == null)
return default;
var slideTime = _currentMessage.SlideTime;
var offset = tippy.State switch
{
TippyState.Hidden => 0,
TippyState.Revealing => Math.Clamp(1 - _secondsUntilNextState / slideTime, 0, 1),
TippyState.Hiding => Math.Clamp(_secondsUntilNextState / slideTime, 0, 1),
_ => 1,
};
var waddle = _currentMessage.WaddleInterval;
if (_currentMessage == null
|| waddle <= 0
|| tippy.State == TippyState.Hidden
|| tippy.State == TippyState.Speaking
|| !EntityManager.TryGetComponent(_entity, out SpriteComponent? sprite))
{
return new Vector2(screenSize.X - offset * (tippy.DesiredSize.X + Padding), (screenSize.Y - tippy.DesiredSize.Y) / 2);
}
var numSteps = (int) Math.Ceiling(slideTime / waddle);
var curStep = (int) Math.Floor(numSteps * offset);
var stepSize = (tippy.DesiredSize.X + Padding) / numSteps;
if (curStep != _previousStep)
{
_previousStep = curStep;
sprite.Rotation = sprite.Rotation > 0
? -WaddleRotation
: WaddleRotation;
if (EntityManager.TryGetComponent(_entity, out FootstepModifierComponent? step))
{
var audioParams = step.FootstepSoundCollection.Params
.AddVolume(-7f)
.WithVariation(0.1f);
_audio.PlayGlobal(step.FootstepSoundCollection, EntityUid.Invalid, audioParams);
}
}
return new Vector2(screenSize.X - stepSize * curStep, (screenSize.Y - tippy.DesiredSize.Y) / 2);
}
private void NextState(TippyUI tippy)
{
SpriteComponent? sprite;
switch (tippy.State)
{
case TippyState.Hidden:
if (!_queuedMessages.TryDequeue(out var next))
return;
if (next.Proto != null)
{
_entity = EntityManager.SpawnEntity(next.Proto, MapCoordinates.Nullspace);
tippy.ModifyLayers = false;
}
else
{
_entity = EntityManager.SpawnEntity(_cfg.GetCVar(CCVars.TippyEntity), MapCoordinates.Nullspace);
tippy.ModifyLayers = true;
}
if (!EntityManager.TryGetComponent(_entity, out sprite))
return;
if (!EntityManager.HasComponent<PaperVisualsComponent>(_entity))
{
var paper = EntityManager.AddComponent<PaperVisualsComponent>(_entity);
paper.BackgroundImagePath = "/Textures/Interface/Paper/paper_background_default.svg.96dpi.png";
paper.BackgroundPatchMargin = new(16f, 16f, 16f, 16f);
paper.BackgroundModulate = new(255, 255, 204);
paper.FontAccentColor = new(0, 0, 0);
}
tippy.InitLabel(EntityManager.GetComponentOrNull<PaperVisualsComponent>(_entity), _resCache);
var scale = sprite.Scale;
if (tippy.ModifyLayers)
{
sprite.Scale = Vector2.One;
}
else
{
sprite.Scale = new Vector2(3, 3);
}
tippy.Entity.SetEntity(_entity);
tippy.Entity.Scale = scale;
_currentMessage = next;
_secondsUntilNextState = next.SlideTime;
tippy.State = TippyState.Revealing;
_previousStep = 0;
if (tippy.ModifyLayers)
{
sprite.LayerSetAnimationTime("revealing", 0);
sprite.LayerSetVisible("revealing", true);
sprite.LayerSetVisible("speaking", false);
sprite.LayerSetVisible("hiding", false);
}
sprite.Rotation = 0;
tippy.Label.SetMarkup(_currentMessage.Msg);
tippy.Label.Visible = false;
tippy.LabelPanel.Visible = false;
tippy.Visible = true;
sprite.Visible = true;
break;
case TippyState.Revealing:
tippy.State = TippyState.Speaking;
if (!EntityManager.TryGetComponent(_entity, out sprite))
return;
sprite.Rotation = 0;
_previousStep = 0;
if (tippy.ModifyLayers)
{
sprite.LayerSetAnimationTime("speaking", 0);
sprite.LayerSetVisible("revealing", false);
sprite.LayerSetVisible("speaking", true);
sprite.LayerSetVisible("hiding", false);
}
tippy.Label.Visible = true;
tippy.LabelPanel.Visible = true;
tippy.InvalidateArrange();
tippy.InvalidateMeasure();
if (_currentMessage != null)
_secondsUntilNextState = _currentMessage.SpeakTime;
break;
case TippyState.Speaking:
tippy.State = TippyState.Hiding;
if (!EntityManager.TryGetComponent(_entity, out sprite))
return;
if (tippy.ModifyLayers)
{
sprite.LayerSetAnimationTime("hiding", 0);
sprite.LayerSetVisible("revealing", false);
sprite.LayerSetVisible("speaking", false);
sprite.LayerSetVisible("hiding", true);
}
tippy.LabelPanel.Visible = false;
if (_currentMessage != null)
_secondsUntilNextState = _currentMessage.SlideTime;
break;
default: // finished hiding
EntityManager.DeleteEntity(_entity);
_entity = default;
tippy.Visible = false;
_currentMessage = null;
_secondsUntilNextState = 0;
tippy.State = TippyState.Hidden;
break;
}
}
private void OnScreenChanged((UIScreen? Old, UIScreen? New) ev)
{
ev.Old?.RemoveWidget<TippyUI>();
_currentMessage = null;
EntityManager.DeleteEntity(_entity);
}
}

View File

@@ -15,6 +15,7 @@ using Content.Shared.Actions;
using Content.Shared.Input;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
@@ -42,6 +43,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IInputManager _input = default!;
[UISystemDependency] private readonly ActionsSystem? _actionsSystem = default;
[UISystemDependency] private readonly InteractionOutlineSystem? _interactionOutline = default;
@@ -356,6 +358,10 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
{
QueueWindowUpdate();
// TODO ACTIONS allow buttons to persist across state applications
// Then we don't have to interrupt drags any time the buttons get rebuilt.
_menuDragHelper.EndDrag();
if (_actionsSystem != null)
_container?.SetActionData(_actionsSystem, _actions.ToArray());
}
@@ -516,7 +522,8 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
button.ClearData();
if (_container?.TryGetButtonIndex(button, out position) ?? false)
{
_actions.RemoveAt(position);
if (_actions.Count > position && position >= 0)
_actions.RemoveAt(position);
}
}
else if (button.TryReplaceWith(actionId.Value, _actionsSystem) &&
@@ -539,23 +546,22 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void DragAction()
{
if (_menuDragHelper.Dragged is not {ActionId: {} action} dragged)
{
_menuDragHelper.EndDrag();
return;
}
EntityUid? swapAction = null;
if (UIManager.CurrentlyHovered is ActionButton button)
var currentlyHovered = UIManager.MouseGetControl(_input.MouseScreenPosition);
if (currentlyHovered is ActionButton button)
{
if (!_menuDragHelper.IsDragging || _menuDragHelper.Dragged?.ActionId is not { } type)
{
_menuDragHelper.EndDrag();
return;
}
swapAction = button.ActionId;
SetAction(button, type, false);
SetAction(button, action, false);
}
if (_menuDragHelper.Dragged is {Parent: ActionButtonContainer} old)
{
SetAction(old, swapAction, false);
}
if (dragged.Parent is ActionButtonContainer)
SetAction(dragged, swapAction, false);
if (_actionsSystem != null)
_container?.SetActionData(_actionsSystem, _actions.ToArray());
@@ -610,27 +616,27 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void OnActionPressed(GUIBoundKeyEventArgs args, ActionButton button)
{
if (args.Function == EngineKeyFunctions.UIClick)
{
if (button.ActionId == null)
{
var ev = new FillActionSlotEvent();
EntityManager.EventBus.RaiseEvent(EventSource.Local, ev);
if (ev.Action != null)
SetAction(button, ev.Action);
}
else
{
_menuDragHelper.MouseDown(button);
}
args.Handle();
}
else if (args.Function == EngineKeyFunctions.UIRightClick)
if (args.Function == EngineKeyFunctions.UIRightClick)
{
SetAction(button, null);
args.Handle();
return;
}
if (args.Function != EngineKeyFunctions.UIClick)
return;
args.Handle();
if (button.ActionId != null)
{
_menuDragHelper.MouseDown(button);
return;
}
var ev = new FillActionSlotEvent();
EntityManager.EventBus.RaiseEvent(EventSource.Local, ev);
if (ev.Action != null)
SetAction(button, ev.Action);
}
private void OnActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
@@ -638,33 +644,31 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
if (args.Function != EngineKeyFunctions.UIClick || _actionsSystem == null)
return;
//todo: make dragging onto the same spot NOT trigger again
if (UIManager.CurrentlyHovered == button)
{
_menuDragHelper.EndDrag();
args.Handle();
if (_actionsSystem.TryGetActionData(button.ActionId, out var baseAction))
{
if (baseAction is BaseTargetActionComponent action)
{
// for target actions, we go into "select target" mode, we don't
// message the server until we actually pick our target.
// if we're clicking the same thing we're already targeting for, then we simply cancel
// targeting
ToggleTargeting(button.ActionId.Value, action);
return;
}
_actionsSystem?.TriggerAction(button.ActionId.Value, baseAction);
}
}
else
if (_menuDragHelper.IsDragging)
{
DragAction();
return;
}
args.Handle();
_menuDragHelper.EndDrag();
if (!_actionsSystem.TryGetActionData(button.ActionId, out var baseAction))
return;
if (baseAction is not BaseTargetActionComponent action)
{
_actionsSystem?.TriggerAction(button.ActionId.Value, baseAction);
return;
}
// for target actions, we go into "select target" mode, we don't
// message the server until we actually pick our target.
// if we're clicking the same thing we're already targeting for, then we simply cancel
// targeting
ToggleTargeting(button.ActionId.Value, action);
}
private bool OnMenuBeginDrag()

View File

@@ -152,16 +152,8 @@ public sealed class ActionButton : Control, IEntityControl
OnThemeUpdated();
OnKeyBindDown += args =>
{
Depress(args, true);
OnPressed(args);
};
OnKeyBindUp += args =>
{
Depress(args, false);
OnUnpressed(args);
};
OnKeyBindDown += OnPressed;
OnKeyBindUp += OnUnpressed;
TooltipSupplier = SupplyTooltip;
}
@@ -175,11 +167,23 @@ public sealed class ActionButton : Control, IEntityControl
private void OnPressed(GUIBoundKeyEventArgs args)
{
if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.UIRightClick)
return;
if (args.Function == EngineKeyFunctions.UIRightClick)
Depress(args, true);
ActionPressed?.Invoke(args, this);
}
private void OnUnpressed(GUIBoundKeyEventArgs args)
{
if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.UIRightClick)
return;
if (args.Function == EngineKeyFunctions.UIRightClick)
Depress(args, false);
ActionUnpressed?.Invoke(args, this);
}
@@ -378,12 +382,6 @@ public sealed class ActionButton : Control, IEntityControl
if (_action is not {Enabled: true})
return;
if (_depressed && !depress)
{
// fire the action
OnUnpressed(args);
}
_depressed = depress;
DrawModeChanged();
}

View File

@@ -546,7 +546,7 @@ public sealed class ChatUIController : UIController
}
// only admins can see / filter asay
if (_admin.HasFlag(AdminFlags.Admin))
if (_admin.HasFlag(AdminFlags.Adminchat))
{
FilterableChannels |= ChatChannel.Admin;
FilterableChannels |= ChatChannel.AdminAlert;

View File

@@ -0,0 +1,125 @@
using Content.Client.Chat.UI;
using Content.Client.Gameplay;
using Content.Client.UserInterface.Controls;
using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Input;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input.Binding;
using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Systems.Emotes;
[UsedImplicitly]
public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayState>
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClyde _displayManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
private MenuButton? EmotesButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.EmotesButton;
private EmotesMenu? _menu;
public void OnStateEntered(GameplayState state)
{
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenEmotesMenu,
InputCmdHandler.FromDelegate(_ => ToggleEmotesMenu(false)))
.Register<EmotesUIController>();
}
public void OnStateExited(GameplayState state)
{
CommandBinds.Unregister<EmotesUIController>();
}
private void ToggleEmotesMenu(bool centered)
{
if (_menu == null)
{
// setup window
_menu = UIManager.CreateWindow<EmotesMenu>();
_menu.OnClose += OnWindowClosed;
_menu.OnOpen += OnWindowOpen;
_menu.OnPlayEmote += OnPlayEmote;
if (EmotesButton != null)
EmotesButton.SetClickPressed(true);
if (centered)
{
_menu.OpenCentered();
}
else
{
// Open the menu, centered on the mouse
var vpSize = _displayManager.ScreenSize;
_menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
}
}
else
{
_menu.OnClose -= OnWindowClosed;
_menu.OnOpen -= OnWindowOpen;
_menu.OnPlayEmote -= OnPlayEmote;
if (EmotesButton != null)
EmotesButton.SetClickPressed(false);
CloseMenu();
}
}
public void UnloadButton()
{
if (EmotesButton == null)
return;
EmotesButton.OnPressed -= ActionButtonPressed;
}
public void LoadButton()
{
if (EmotesButton == null)
return;
EmotesButton.OnPressed += ActionButtonPressed;
}
private void ActionButtonPressed(BaseButton.ButtonEventArgs args)
{
ToggleEmotesMenu(true);
}
private void OnWindowClosed()
{
if (EmotesButton != null)
EmotesButton.Pressed = false;
CloseMenu();
}
private void OnWindowOpen()
{
if (EmotesButton != null)
EmotesButton.Pressed = true;
}
private void CloseMenu()
{
if (_menu == null)
return;
_menu.Dispose();
_menu = null;
}
private void OnPlayEmote(ProtoId<EmotePrototype> protoId)
{
_entityManager.RaisePredictiveEvent(new PlayEmoteMessage(protoId));
}
}

View File

@@ -3,6 +3,7 @@ using Content.Client.UserInterface.Systems.Admin;
using Content.Client.UserInterface.Systems.Bwoink;
using Content.Client.UserInterface.Systems.Character;
using Content.Client.UserInterface.Systems.Crafting;
using Content.Client.UserInterface.Systems.Emotes;
using Content.Client.UserInterface.Systems.EscapeMenu;
using Content.Client.UserInterface.Systems.Gameplay;
using Content.Client.UserInterface.Systems.Guidebook;
@@ -22,6 +23,7 @@ public sealed class GameTopMenuBarUIController : UIController
[Dependency] private readonly ActionUIController _action = default!;
[Dependency] private readonly SandboxUIController _sandbox = default!;
[Dependency] private readonly GuidebookUIController _guidebook = default!;
[Dependency] private readonly EmotesUIController _emotes = default!;
private GameTopMenuBar? GameTopMenuBar => UIManager.GetActiveUIWidgetOrNull<GameTopMenuBar>();
@@ -44,6 +46,7 @@ public sealed class GameTopMenuBarUIController : UIController
_ahelp.UnloadButton();
_action.UnloadButton();
_sandbox.UnloadButton();
_emotes.UnloadButton();
}
public void LoadButtons()
@@ -56,5 +59,6 @@ public sealed class GameTopMenuBarUIController : UIController
_ahelp.LoadButton();
_action.LoadButton();
_sandbox.LoadButton();
_emotes.LoadButton();
}
}

View File

@@ -43,6 +43,16 @@
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
<ui:MenuButton
Name="EmotesButton"
Access="Internal"
Icon="{xe:Tex '/Textures/Interface/emotes.svg.192dpi.png'}"
ToolTip="{Loc 'game-hud-open-emotes-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenEmotesMenu}"
MinSize="42 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
<ui:MenuButton
Name="CraftingButton"
Access="Internal"

View File

@@ -31,6 +31,7 @@ namespace Content.Client.Verbs.UI
public NetEntity CurrentTarget;
public SortedSet<Verb> CurrentVerbs = new();
public List<VerbCategory> ExtraCategories = new();
/// <summary>
/// Separate from <see cref="ContextMenuUIController.RootMenu"/>, since we can open a verb menu as a submenu
@@ -91,19 +92,12 @@ namespace Content.Client.Verbs.UI
menu.MenuBody.DisposeAllChildren();
CurrentTarget = target;
CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, force);
CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, out ExtraCategories, force);
OpenMenu = menu;
// Fill in client-side verbs.
FillVerbPopup(menu);
// Add indicator that some verbs may be missing.
// I long for the day when verbs will all be predicted and this becomes unnecessary.
if (!target.IsClientSide())
{
_context.AddElement(menu, new ContextMenuElement(Loc.GetString("verb-system-waiting-on-server-text")));
}
// if popup isn't null (ie we are opening out of an entity menu element),
// assume that that is going to handle opening the submenu properly
if (popup != null)
@@ -128,11 +122,19 @@ namespace Content.Client.Verbs.UI
var element = new VerbMenuElement(verb);
_context.AddElement(popup, element);
}
else if (listedCategories.Add(verb.Category.Text))
AddVerbCategory(verb.Category, popup);
}
if (ExtraCategories != null)
{
foreach (var category in ExtraCategories)
{
if (listedCategories.Add(category.Text))
AddVerbCategory(category, popup);
}
}
popup.InvalidateMeasure();
}
@@ -153,10 +155,11 @@ namespace Content.Client.Verbs.UI
}
}
if (verbsInCategory.Count == 0)
if (verbsInCategory.Count == 0 && !ExtraCategories.Contains(category))
return;
var element = new VerbMenuElement(category, verbsInCategory[0].TextStyleClass);
var style = verbsInCategory.FirstOrDefault()?.TextStyleClass ?? Verb.DefaultTextStyleClass;
var element = new VerbMenuElement(category, style);
_context.AddElement(popup, element);
// Create the pop-up that appears when hovering over this element

View File

@@ -165,29 +165,23 @@ namespace Content.Client.Verbs
return true;
}
/// <summary>
/// Asks the server to send back a list of server-side verbs, for the given verb type.
/// </summary>
public SortedSet<Verb> GetVerbs(EntityUid target, EntityUid user, Type type, bool force = false)
{
return GetVerbs(GetNetEntity(target), user, new List<Type>() { type }, force);
}
/// <summary>
/// Ask the server to send back a list of server-side verbs, and for now return an incomplete list of verbs
/// (only those defined locally).
/// </summary>
public SortedSet<Verb> GetVerbs(NetEntity target, EntityUid user, List<Type> verbTypes,
bool force = false)
public SortedSet<Verb> GetVerbs(NetEntity target, EntityUid user, List<Type> verbTypes, out List<VerbCategory> extraCategories, bool force = false)
{
if (!target.IsClientSide())
RaiseNetworkEvent(new RequestServerVerbsEvent(target, verbTypes, adminRequest: force));
// Some admin menu interactions will try get verbs for entities that have not yet been sent to the player.
if (!TryGetEntity(target, out var local))
{
extraCategories = new();
return new();
}
return GetLocalVerbs(local.Value, user, verbTypes, force);
return GetLocalVerbs(local.Value, user, verbTypes, out extraCategories, force);
}

View File

@@ -78,17 +78,44 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
else
UpBiasButton.Pressed = true;
var disabled = !state.ServerConnected || !state.CanScan || state.PointAmount <= 0;
ExtractButton.Disabled = false;
if (!state.ServerConnected)
{
ExtractButton.Disabled = true;
ExtractButton.ToolTip = Loc.GetString("analysis-console-no-server-connected");
}
else if (!state.CanScan)
{
ExtractButton.Disabled = true;
ExtractButton.Disabled = disabled;
// CanScan can be false if either there's no analyzer connected or if there's
// no entity on the scanner. The `Information` text will always tell the user
// of the former case, but in the latter, it'll only show a message if a scan
// has never been performed, so add a tooltip to indicate that the artifact
// is gone.
if (state.AnalyzerConnected)
{
ExtractButton.ToolTip = Loc.GetString("analysis-console-no-artifact-placed");
}
else
{
ExtractButton.ToolTip = null;
}
}
else if (state.PointAmount <= 0)
{
ExtractButton.Disabled = true;
ExtractButton.ToolTip = Loc.GetString("analysis-console-no-points-to-extract");
}
if (disabled)
if (ExtractButton.Disabled)
{
ExtractButton.RemoveStyleClass("ButtonColorGreen");
}
else
{
ExtractButton.AddStyleClass("ButtonColorGreen");
ExtractButton.ToolTip = null;
}
}
private void UpdateArtifactIcon(EntityUid? uid)

View File

@@ -19,36 +19,45 @@ namespace Content.IntegrationTests.Tests.Damageable
# Define some damage groups
- type: damageType
id: TestDamage1
name: damage-type-blunt
- type: damageType
id: TestDamage2a
name: damage-type-blunt
- type: damageType
id: TestDamage2b
name: damage-type-blunt
- type: damageType
id: TestDamage3a
name: damage-type-blunt
- type: damageType
id: TestDamage3b
name: damage-type-blunt
- type: damageType
id: TestDamage3c
name: damage-type-blunt
# Define damage Groups with 1,2,3 damage types
- type: damageGroup
id: TestGroup1
name: damage-group-brute
damageTypes:
- TestDamage1
- type: damageGroup
id: TestGroup2
name: damage-group-brute
damageTypes:
- TestDamage2a
- TestDamage2b
- type: damageGroup
id: TestGroup3
name: damage-group-brute
damageTypes:
- TestDamage3a
- TestDamage3b

View File

@@ -12,24 +12,31 @@ namespace Content.IntegrationTests.Tests.Destructible
public const string DamagePrototypes = $@"
- type: damageType
id: TestBlunt
name: damage-type-blunt
- type: damageType
id: TestSlash
name: damage-type-slash
- type: damageType
id: TestPiercing
name: damage-type-piercing
- type: damageType
id: TestHeat
name: damage-type-heat
- type: damageType
id: TestShock
name: damage-type-shock
- type: damageType
id: TestCold
name: damage-type-cold
- type: damageGroup
id: TestBrute
name: damage-group-brute
damageTypes:
- TestBlunt
- TestSlash
@@ -37,6 +44,7 @@ namespace Content.IntegrationTests.Tests.Destructible
- type: damageGroup
id: TestBurn
name: damage-group-burn
damageTypes:
- TestHeat
- TestShock

View File

@@ -18,7 +18,6 @@ using Content.Server.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Server.Tabletop;
using Content.Server.Tabletop.Components;
using Content.Server.Terminator.Systems;
using Content.Shared.Administration;
using Content.Shared.Administration.Components;
using Content.Shared.Body.Components;
@@ -31,7 +30,6 @@ using Content.Shared.Database;
using Content.Shared.Electrocution;
using Content.Shared.Interaction.Components;
using Content.Shared.Inventory;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
@@ -74,7 +72,6 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly TabletopSystem _tabletopSystem = default!;
[Dependency] private readonly TerminatorSystem _terminator = default!;
[Dependency] private readonly VomitSystem _vomitSystem = default!;
[Dependency] private readonly WeldableSystem _weldableSystem = default!;
[Dependency] private readonly SharedContentEyeSystem _eyeSystem = default!;
@@ -824,27 +821,5 @@ public sealed partial class AdminVerbSystem
Impact = LogImpact.Extreme,
};
args.Verbs.Add(superBonk);
Verb terminate = new()
{
Text = "Terminate",
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("Mobs/Species/Terminator/parts.rsi"), "skull_icon"),
Act = () =>
{
if (!TryComp<MindContainerComponent>(args.Target, out var mindContainer) || mindContainer.Mind == null)
return;
var coords = Transform(args.Target).Coordinates;
var mindId = mindContainer.Mind.Value;
_terminator.CreateSpawner(coords, mindId);
_popupSystem.PopupEntity(Loc.GetString("admin-smite-terminate-prompt"), args.Target,
args.Target, PopupType.LargeCaution);
},
Impact = LogImpact.Extreme,
Message = Loc.GetString("admin-smite-terminate-description")
};
args.Verbs.Add(terminate);
}
}

View File

@@ -25,16 +25,19 @@ namespace Content.Server.Atmos.EntitySystems
private void OnAirtightInit(Entity<AirtightComponent> airtight, ref ComponentInit args)
{
var xform = EntityManager.GetComponent<TransformComponent>(airtight);
if (airtight.Comp.FixAirBlockedDirectionInitialize)
// TODO AIRTIGHT what FixAirBlockedDirectionInitialize even for?
if (!airtight.Comp.FixAirBlockedDirectionInitialize)
{
var moveEvent = new MoveEvent(airtight, default, default, Angle.Zero, xform.LocalRotation, xform, false);
if (AirtightMove(airtight, ref moveEvent))
return;
UpdatePosition(airtight);
return;
}
UpdatePosition(airtight);
var xform = Transform(airtight);
airtight.Comp.CurrentAirBlockedDirection =
(int) Rotate((AtmosDirection) airtight.Comp.InitialAirBlockedDirection, xform.LocalRotation);
UpdatePosition(airtight, xform);
var airtightEv = new AirtightChanged(airtight, airtight, default);
RaiseLocalEvent(airtight, ref airtightEv, true);
}
private void OnAirtightShutdown(Entity<AirtightComponent> airtight, ref ComponentShutdown args)
@@ -42,13 +45,8 @@ namespace Content.Server.Atmos.EntitySystems
var xform = Transform(airtight);
// If the grid is deleting no point updating atmos.
if (HasComp<MapGridComponent>(xform.GridUid) &&
MetaData(xform.GridUid.Value).EntityLifeStage > EntityLifeStage.MapInitialized)
{
return;
}
SetAirblocked(airtight, false, xform);
if (xform.GridUid != null && LifeStage(xform.GridUid.Value) <= EntityLifeStage.MapInitialized)
SetAirblocked(airtight, false, xform);
}
private void OnAirtightPositionChanged(EntityUid uid, AirtightComponent airtight, ref AnchorStateChangedEvent args)
@@ -83,21 +81,14 @@ namespace Content.Server.Atmos.EntitySystems
}
}
private void OnAirtightMoved(Entity<AirtightComponent> airtight, ref MoveEvent ev)
{
AirtightMove(airtight, ref ev);
}
private bool AirtightMove(Entity<AirtightComponent> ent, ref MoveEvent ev)
private void OnAirtightMoved(Entity<AirtightComponent> ent, ref MoveEvent ev)
{
var (owner, airtight) = ent;
airtight.CurrentAirBlockedDirection = (int) Rotate((AtmosDirection)airtight.InitialAirBlockedDirection, ev.NewRotation);
var pos = airtight.LastPosition;
UpdatePosition(ent, ev.Component);
var airtightEv = new AirtightChanged(owner, airtight, pos);
RaiseLocalEvent(owner, ref airtightEv, true);
return true;
}
public void SetAirblocked(Entity<AirtightComponent> airtight, bool airblocked, TransformComponent? xform = null)

View File

@@ -149,6 +149,29 @@ public sealed class RottingSystem : SharedRottingSystem
args.Handled = component.CurrentTemperature < Atmospherics.T0C + 0.85f;
}
public void ReduceAccumulator(EntityUid uid, TimeSpan time)
{
if (!TryComp<PerishableComponent>(uid, out var perishable))
return;
if (!TryComp<RottingComponent>(uid, out var rotting))
{
perishable.RotAccumulator -= time;
return;
}
var total = (rotting.TotalRotTime + perishable.RotAccumulator) - time;
if (total < perishable.RotAfter)
{
RemCompDeferred(uid, rotting);
perishable.RotAccumulator = total;
}
else
rotting.TotalRotTime = total - perishable.RotAfter;
}
/// <summary>
/// Is anything speeding up the decay?
/// e.g. buried in a grave

View File

@@ -5,7 +5,7 @@ using Robust.Shared.Console;
namespace Content.Server.Chat.Commands
{
[AdminCommand(AdminFlags.Admin)]
[AdminCommand(AdminFlags.Adminchat)]
internal sealed class AdminChatCommand : IConsoleCommand
{
public string Command => "asay";

View File

@@ -1,5 +1,6 @@
using System.Collections.Frozen;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Speech;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -80,6 +81,16 @@ public partial class ChatSystem
bool ignoreActionBlocker = false
)
{
if (!(emote.Whitelist?.IsValid(source, EntityManager) ?? true))
return;
if (emote.Blacklist?.IsValid(source, EntityManager) ?? false)
return;
if (!emote.Available &&
TryComp<SpeechComponent>(source, out var speech) &&
!speech.AllowedEmotes.Contains(emote.ID))
return;
// check if proto has valid message for chat
if (emote.ChatMessages.Count != 0)
{

View File

@@ -20,7 +20,7 @@ public sealed class TransformableContainerSystem : EntitySystem
SubscribeLocalEvent<TransformableContainerComponent, SolutionContainerChangedEvent>(OnSolutionChange);
}
private void OnMapInit(Entity<TransformableContainerComponent> entity, ref MapInitEvent args)
private void OnMapInit(Entity<TransformableContainerComponent> entity, ref MapInitEvent args)
{
var meta = MetaData(entity.Owner);
if (string.IsNullOrEmpty(entity.Comp.InitialName))

View File

@@ -1,4 +1,4 @@
using Content.Server.Body.Components;
using Content.Server.Body.Components;
using Content.Shared.Body.Prototypes;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
@@ -35,7 +35,7 @@ namespace Content.Server.Chemistry.ReagentEffectConditions
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-organ-type",
("name", prototype.Index<MetabolizerTypePrototype>(Type).Name),
("name", prototype.Index<MetabolizerTypePrototype>(Type).LocalizedName),
("shouldhave", ShouldHave));
}
}

View File

@@ -1,4 +1,4 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
@@ -43,7 +43,7 @@ namespace Content.Server.Chemistry.ReagentEffectConditions
prototype.TryIndex(Reagent, out reagentProto);
return Loc.GetString("reagent-effect-condition-guidebook-reagent-threshold",
("reagent", reagentProto?.LocalizedName ?? "this reagent"),
("reagent", reagentProto?.LocalizedName ?? Loc.GetString("reagent-effect-condition-guidebook-this-reagent")),
("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()),
("min", Min.Float()));
}

View File

@@ -1,4 +1,4 @@
using Content.Shared.Body.Prototypes;
using Content.Shared.Body.Prototypes;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
@@ -74,7 +74,7 @@ namespace Content.Server.Chemistry.ReagentEffects
return Loc.GetString("reagent-effect-guidebook-adjust-reagent-group",
("chance", Probability),
("deltasign", MathF.Sign(Amount.Float())),
("group", groupProto.ID),
("group", groupProto.LocalizedName),
("amount", MathF.Abs(Amount.Float())));
}

View File

@@ -74,7 +74,7 @@ namespace Content.Server.Chemistry.ReagentEffects
damages.Add(
Loc.GetString("health-change-display",
("kind", group.ID),
("kind", group.LocalizedName),
("amount", MathF.Abs(amount.Float())),
("deltasign", sign)
));
@@ -96,7 +96,7 @@ namespace Content.Server.Chemistry.ReagentEffects
damages.Add(
Loc.GetString("health-change-display",
("kind", kind),
("kind", prototype.Index<DamageTypePrototype>(kind).LocalizedName),
("amount", MathF.Abs(amount.Float())),
("deltasign", sign)
));

View File

@@ -0,0 +1,31 @@
using Content.Shared.Chemistry.Reagent;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Content.Server.Atmos.Rotting;
namespace Content.Server.Chemistry.ReagentEffects
{
/// <summary>
/// Reduces the rotting accumulator on the patient, making them revivable.
/// </summary>
[UsedImplicitly]
public sealed partial class ReduceRotting : ReagentEffect
{
[DataField("seconds")]
public double RottingAmount = 10;
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-reduce-rotting",
("chance", Probability),
("time", RottingAmount));
public override void Effect(ReagentEffectArgs args)
{
if (args.Scale != 1f)
return;
var rottingSys = args.EntityManager.EntitySysManager.GetEntitySystem<RottingSystem>();
rottingSys.ReduceAccumulator(args.SolutionEntity, TimeSpan.FromSeconds(RottingAmount));
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Content.Server.Construction.Conditions
public bool Condition(EntityUid uid, IEntityManager entityManager)
{
if (!entityManager.TryGetComponent(uid, out LockComponent? lockcomp))
return false;
return true;
return lockcomp.Locked == IsLocked;
}
@@ -25,7 +25,8 @@ namespace Content.Server.Construction.Conditions
var entMan = IoCManager.Resolve<IEntityManager>();
var entity = args.Examined;
if (!entMan.TryGetComponent(entity, out LockComponent? lockcomp)) return false;
if (!entMan.TryGetComponent(entity, out LockComponent? lockcomp))
return true;
switch (IsLocked)
{

View File

@@ -0,0 +1,35 @@
using Robust.Shared.Audio;
namespace Content.Server.Containers;
/// <summary>
/// Allows objects to fall inside the Container when thrown
/// </summary>
[RegisterComponent]
[Access(typeof(ThrowInsertContainerSystem))]
public sealed partial class ThrowInsertContainerComponent : Component
{
[DataField(required: true)]
public string ContainerId = string.Empty;
/// <summary>
/// Throw chance of hitting into the container
/// </summary>
[DataField]
public float Probability = 0.75f;
/// <summary>
/// Sound played when an object is throw into the container.
/// </summary>
[DataField]
public SoundSpecifier? InsertSound = new SoundPathSpecifier("/Audio/Effects/trashbag1.ogg");
/// <summary>
/// Sound played when an item is thrown and misses the container.
/// </summary>
[DataField]
public SoundSpecifier? MissSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
[DataField]
public LocId MissLocString = "container-thrown-missed";
}

View File

@@ -0,0 +1,50 @@
using Content.Server.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Popups;
using Content.Shared.Throwing;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Random;
namespace Content.Server.Containers;
public sealed class ThrowInsertContainerSystem : EntitySystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ThrowInsertContainerComponent, ThrowHitByEvent>(OnThrowCollide);
}
private void OnThrowCollide(Entity<ThrowInsertContainerComponent> ent, ref ThrowHitByEvent args)
{
var container = _containerSystem.GetContainer(ent, ent.Comp.ContainerId);
if (!_containerSystem.CanInsert(args.Thrown, container))
return;
var rand = _random.NextFloat();
if (_random.Prob(ent.Comp.Probability))
{
_audio.PlayPvs(ent.Comp.MissSound, ent);
_popup.PopupEntity(Loc.GetString(ent.Comp.MissLocString), ent);
return;
}
if (!_containerSystem.Insert(args.Thrown, container))
throw new InvalidOperationException("Container insertion failed but CanInsert returned true");
_audio.PlayPvs(ent.Comp.InsertSound, ent);
if (args.Component.Thrower != null)
_adminLogger.Add(LogType.Landed, LogImpact.Low, $"{ToPrettyString(args.Thrown)} thrown by {ToPrettyString(args.Component.Thrower.Value):player} landed in {ToPrettyString(ent)}");
}
}

View File

@@ -1,6 +1,6 @@
using Content.Shared.Popups;
namespace Content.Server.Destructible.Thresholds.Behaviors;
namespace Content.Server.Destructible.Thresholds.Behaviors;
/// <summary>
/// Shows a popup for everyone.

View File

@@ -73,8 +73,6 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
SubscribeLocalEvent<DisposalUnitComponent, PowerChangedEvent>(OnPowerChange);
SubscribeLocalEvent<DisposalUnitComponent, ComponentInit>(OnDisposalInit);
SubscribeLocalEvent<DisposalUnitComponent, ThrowHitByEvent>(OnThrowCollide);
SubscribeLocalEvent<DisposalUnitComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<DisposalUnitComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
SubscribeLocalEvent<DisposalUnitComponent, DragDropTargetEvent>(OnDragDropOn);
@@ -294,40 +292,6 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
args.Handled = true;
}
/// <summary>
/// Thrown items have a chance of bouncing off the unit and not going in.
/// </summary>
private void OnThrowCollide(EntityUid uid, SharedDisposalUnitComponent component, ThrowHitByEvent args)
{
var canInsert = CanInsert(uid, component, args.Thrown);
var randDouble = _robustRandom.NextDouble();
if (!canInsert)
{
return;
}
if (randDouble > 0.75)
{
_audioSystem.PlayPvs(component.MissSound, uid);
_popupSystem.PopupEntity(Loc.GetString("disposal-unit-thrown-missed"), uid);
return;
}
var inserted = _containerSystem.Insert(args.Thrown, component.Container);
if (!inserted)
{
throw new InvalidOperationException("Container insertion failed but CanInsert returned true");
}
if (args.Component.Thrower != null)
_adminLogger.Add(LogType.Landed, LogImpact.Low, $"{ToPrettyString(args.Thrown)} thrown by {ToPrettyString(args.Component.Thrower.Value):player} landed in {ToPrettyString(uid)}");
AfterInsert(uid, component, args.Thrown);
}
private void OnDisposalInit(EntityUid uid, SharedDisposalUnitComponent component, ComponentInit args)
{
component.Container = _containerSystem.EnsureContainer<Container>(uid, SharedDisposalUnitComponent.ContainerId);

View File

@@ -1,6 +1,7 @@
using Content.Server.DeviceNetwork.Components;
using Content.Server.EUI;
using Content.Shared.Eui;
using Content.Shared.Fax.Components;
using Content.Shared.Fax;
using Content.Shared.Follower;
using Content.Shared.Ghost;
@@ -54,7 +55,7 @@ public sealed class AdminFaxEui : BaseEui
}
case AdminFaxEuiMsg.Send sendData:
{
var printout = new FaxPrintout(sendData.Content, sendData.Title, null, sendData.StampState,
var printout = new FaxPrintout(sendData.Content, sendData.Title, null, null, sendData.StampState,
new() { new StampDisplayInfo { StampedName = sendData.From, StampedColor = sendData.StampColor } });
_faxSystem.Receive(_entityManager.GetEntity(sendData.Target), printout);
break;

View File

@@ -23,6 +23,7 @@ public static class FaxConstants
public const string FaxNameData = "fax_data_name";
public const string FaxPaperNameData = "fax_data_title";
public const string FaxPaperLabelData = "fax_data_label";
public const string FaxPaperPrototypeData = "fax_data_prototype";
public const string FaxPaperContentData = "fax_data_content";
public const string FaxPaperStampStateData = "fax_data_stamp_state";

View File

@@ -4,6 +4,7 @@ using Content.Server.Chat.Managers;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Labels;
using Content.Server.Paper;
using Content.Server.Popups;
using Content.Server.Power.Components;
@@ -16,7 +17,11 @@ using Content.Shared.DeviceNetwork;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Fax;
using Content.Shared.Fax.Systems;
using Content.Shared.Fax.Components;
using Content.Shared.Interaction;
using Content.Shared.Labels.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Paper;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
@@ -36,12 +41,14 @@ public sealed class FaxSystem : EntitySystem
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly PaperSystem _paperSystem = default!;
[Dependency] private readonly LabelSystem _labelSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly ToolSystem _toolSystem = default!;
[Dependency] private readonly QuickDialogSystem _quickDialog = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly FaxecuteSystem _faxecute = default!;
private const string PaperSlotId = "Paper";
@@ -236,7 +243,7 @@ public sealed class FaxSystem : EntitySystem
}
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.User):user} renamed {ToPrettyString(uid)} from \"{component.FaxName}\" to \"{newName}\"");
$"{ToPrettyString(args.User):user} renamed {ToPrettyString(uid):tool} from \"{component.FaxName}\" to \"{newName}\"");
component.FaxName = newName;
_popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-name-set"), uid);
UpdateUserInterface(uid, component);
@@ -288,11 +295,12 @@ public sealed class FaxSystem : EntitySystem
!args.Data.TryGetValue(FaxConstants.FaxPaperContentData, out string? content))
return;
args.Data.TryGetValue(FaxConstants.FaxPaperLabelData, out string? label);
args.Data.TryGetValue(FaxConstants.FaxPaperStampStateData, out string? stampState);
args.Data.TryGetValue(FaxConstants.FaxPaperStampedByData, out List<StampDisplayInfo>? stampedBy);
args.Data.TryGetValue(FaxConstants.FaxPaperPrototypeData, out string? prototypeId);
var printout = new FaxPrintout(content, name, prototypeId, stampState, stampedBy);
var printout = new FaxPrintout(content, name, label, prototypeId, stampState, stampedBy);
Receive(uid, printout, args.SenderAddress);
break;
@@ -307,18 +315,25 @@ public sealed class FaxSystem : EntitySystem
private void OnFileButtonPressed(EntityUid uid, FaxMachineComponent component, FaxFileMessage args)
{
args.Label = args.Label?[..Math.Min(args.Label.Length, FaxFileMessageValidation.MaxLabelSize)];
args.Content = args.Content[..Math.Min(args.Content.Length, FaxFileMessageValidation.MaxContentSize)];
PrintFile(uid, component, args);
}
private void OnCopyButtonPressed(EntityUid uid, FaxMachineComponent component, FaxCopyMessage args)
{
Copy(uid, component, args);
if (HasComp<MobStateComponent>(component.PaperSlot.Item))
_faxecute.Faxecute(uid, component); /// when button pressed it will hurt the mob.
else
Copy(uid, component, args);
}
private void OnSendButtonPressed(EntityUid uid, FaxMachineComponent component, FaxSendMessage args)
{
Send(uid, component, args.Actor);
if (HasComp<MobStateComponent>(component.PaperSlot.Item))
_faxecute.Faxecute(uid, component); /// when button pressed it will hurt the mob.
else
Send(uid, component, args);
}
private void OnRefreshButtonPressed(EntityUid uid, FaxMachineComponent component, FaxRefreshMessage args)
@@ -336,14 +351,20 @@ public sealed class FaxSystem : EntitySystem
if (!Resolve(uid, ref component))
return;
if (TryComp<FaxableObjectComponent>(component.PaperSlot.Item, out var faxable))
component.InsertingState = faxable.InsertingState;
if (component.InsertingTimeRemaining > 0)
{
_appearanceSystem.SetData(uid, FaxMachineVisuals.VisualState, FaxMachineVisualState.Inserting);
Dirty(uid, component);
}
else if (component.PrintingTimeRemaining > 0)
_appearanceSystem.SetData(uid, FaxMachineVisuals.VisualState, FaxMachineVisualState.Printing);
else
_appearanceSystem.SetData(uid, FaxMachineVisuals.VisualState, FaxMachineVisualState.Normal);
}
private void UpdateUserInterface(EntityUid uid, FaxMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
@@ -409,16 +430,20 @@ public sealed class FaxSystem : EntitySystem
else
prototype = DefaultPaperPrototypeId;
var name = Loc.GetString("fax-machine-printed-paper-name");
var name = Loc.GetString("fax-machine-printed-paper-name");
var printout = new FaxPrintout(args.Content, name, prototype);
var printout = new FaxPrintout(args.Content, name, args.Label, prototype);
component.PrintingQueue.Enqueue(printout);
component.SendTimeoutRemaining += component.SendTimeout;
UpdateUserInterface(uid, component);
// Unfortunately, since a paper entity does not yet exist, we have to emulate what LabelSystem will do.
var nameWithLabel = (args.Label is { } label) ? $"{name} ({label})" : name;
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.Actor):actor} added print job to {ToPrettyString(uid):tool} with text: {args.Content}");
$"{ToPrettyString(args.Actor):actor} " +
$"added print job to \"{component.FaxName}\" {ToPrettyString(uid):tool} " +
$"of {nameWithLabel}: {args.Content}");
}
/// <summary>
@@ -438,9 +463,12 @@ public sealed class FaxSystem : EntitySystem
!TryComp<PaperComponent>(sendEntity, out var paper))
return;
TryComp<LabelComponent>(sendEntity, out var labelComponent);
// TODO: See comment in 'Send()' about not being able to copy whole entities
var printout = new FaxPrintout(paper.Content,
metadata.EntityName,
labelComponent?.OriginalName ?? metadata.EntityName,
labelComponent?.CurrentLabel,
metadata.EntityPrototype?.ID ?? DefaultPaperPrototypeId,
paper.StampState,
paper.StampedBy);
@@ -454,14 +482,16 @@ public sealed class FaxSystem : EntitySystem
UpdateUserInterface(uid, component);
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.Actor):actor} added copy job to {ToPrettyString(uid):tool} with text: {ToPrettyString(component.PaperSlot.Item):subject}");
$"{ToPrettyString(args.Actor):actor} " +
$"added copy job to \"{component.FaxName}\" {ToPrettyString(uid):tool} " +
$"of {ToPrettyString(sendEntity):subject}: {printout.Content}");
}
/// <summary>
/// Sends message to addressee if paper is set and a known fax is selected
/// A timeout is set after sending, which is shared by the copy button.
/// </summary>
public void Send(EntityUid uid, FaxMachineComponent? component = null, EntityUid? sender = null)
public void Send(EntityUid uid, FaxMachineComponent? component, FaxSendMessage args)
{
if (!Resolve(uid, ref component))
return;
@@ -477,13 +507,16 @@ public sealed class FaxSystem : EntitySystem
return;
if (!TryComp<MetaDataComponent>(sendEntity, out var metadata) ||
!TryComp<PaperComponent>(sendEntity, out var paper))
!TryComp<PaperComponent>(sendEntity, out var paper))
return;
TryComp<LabelComponent>(sendEntity, out var labelComponent);
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, FaxConstants.FaxPrintCommand },
{ FaxConstants.FaxPaperNameData, metadata.EntityName },
{ FaxConstants.FaxPaperNameData, labelComponent?.OriginalName ?? metadata.EntityName },
{ FaxConstants.FaxPaperLabelData, labelComponent?.CurrentLabel },
{ FaxConstants.FaxPaperContentData, paper.Content },
};
@@ -504,7 +537,11 @@ public sealed class FaxSystem : EntitySystem
_deviceNetworkSystem.QueuePacket(uid, component.DestinationFaxAddress, payload);
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{(sender != null ? ToPrettyString(sender.Value) : "Unknown"):user} sent fax from \"{component.FaxName}\" {ToPrettyString(uid)} to {faxName} ({component.DestinationFaxAddress}): {paper.Content}");
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.Actor):actor} " +
$"sent fax from \"{component.FaxName}\" {ToPrettyString(uid):tool} " +
$"to \"{faxName}\" ({component.DestinationFaxAddress}) " +
$"of {ToPrettyString(sendEntity):subject}: {paper.Content}");
component.SendTimeoutRemaining += component.SendTimeout;
@@ -560,7 +597,13 @@ public sealed class FaxSystem : EntitySystem
}
_metaData.SetEntityName(printed, printout.Name);
_adminLogger.Add(LogType.Action, LogImpact.Low, $"\"{component.FaxName}\" {ToPrettyString(uid)} printed {ToPrettyString(printed)}: {printout.Content}");
if (printout.Label is { } label)
{
_labelSystem.Label(printed, label);
}
_adminLogger.Add(LogType.Action, LogImpact.Low, $"\"{component.FaxName}\" {ToPrettyString(uid):tool} printed {ToPrettyString(printed):subject}: {printout.Content}");
}
private void NotifyAdmins(string faxName)

View File

@@ -87,19 +87,25 @@ public sealed class DrainSystem : SharedDrainSystem
// Try to transfer as much solution as possible to the drain
var transferSolution = _solutionContainerSystem.SplitSolution(containerSoln.Value,
FixedPoint2.Min(containerSolution.Volume, drainSolution.AvailableVolume));
var amountToPutInDrain = drainSolution.AvailableVolume;
var amountToSpillOnGround = containerSolution.Volume - drainSolution.AvailableVolume;
_solutionContainerSystem.TryAddSolution(drain.Solution.Value, transferSolution);
_audioSystem.PlayPvs(drain.ManualDrainSound, target);
_ambientSoundSystem.SetAmbience(target, true);
// If drain is full, spill
if (drainSolution.MaxVolume == drainSolution.Volume)
if (amountToPutInDrain > 0)
{
_puddleSystem.TrySpillAt(Transform(target).Coordinates, containerSolution, out _);
var solutionToPutInDrain = _solutionContainerSystem.SplitSolution(containerSoln.Value, amountToPutInDrain);
_solutionContainerSystem.TryAddSolution(drain.Solution.Value, solutionToPutInDrain);
_audioSystem.PlayPvs(drain.ManualDrainSound, target);
_ambientSoundSystem.SetAmbience(target, true);
}
// Spill the remainder.
if (amountToSpillOnGround > 0)
{
var solutionToSpill = _solutionContainerSystem.SplitSolution(containerSoln.Value, amountToSpillOnGround);
_puddleSystem.TrySpillAt(Transform(target).Coordinates, solutionToSpill, out _);
_popupSystem.PopupEntity(
Loc.GetString("drain-component-empty-verb-target-is-full-message", ("object", target)),
container);

View File

@@ -173,6 +173,26 @@ namespace Content.Server.GameTicking
return gridUids;
}
public int ReadyPlayerCount()
{
var total = 0;
foreach (var (userId, status) in _playerGameStatuses)
{
if (LobbyEnabled && status == PlayerGameStatus.NotReadyToPlay)
continue;
if (!_playerManager.TryGetSessionById(userId, out _))
continue;
if (_banManager.GetRoleBans(userId) == null)
continue;
total++;
}
return total;
}
public void StartRound(bool force = false)
{
#if EXCEPTION_TOLERANCE
@@ -228,6 +248,8 @@ namespace Content.Server.GameTicking
readyPlayerProfiles.Add(userId, profile);
}
DebugTools.AssertEqual(readyPlayers.Count, ReadyPlayerCount());
// Just in case it hasn't been loaded previously we'll try loading it.
LoadMaps();

View File

@@ -1,15 +1,17 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.GameTicking.Components;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Presets;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
namespace Content.Server.GameTicking.Rules;
@@ -20,11 +22,46 @@ public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IComponentFactory _compFact = default!;
private string _ruleCompName = default!;
public override void Initialize()
{
base.Initialize();
_ruleCompName = _compFact.GetComponentName(typeof(GameRuleComponent));
}
protected override void Added(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, component, gameRule, args);
PickRule(component);
var weights = _configurationManager.GetCVar(CCVars.SecretWeightPrototype);
if (!TryPickPreset(weights, out var preset))
{
Log.Error($"{ToPrettyString(uid)} failed to pick any preset. Removing rule.");
Del(uid);
return;
}
Log.Info($"Selected {preset.ID} as the secret preset.");
_adminLogger.Add(LogType.EventStarted, $"Selected {preset.ID} as the secret preset.");
_chatManager.SendAdminAnnouncement(Loc.GetString("rule-secret-selected-preset", ("preset", preset.ID)));
foreach (var rule in preset.Rules)
{
EntityUid ruleEnt;
// if we're pre-round (i.e. will only be added)
// then just add rules. if we're added in the middle of the round (or at any other point really)
// then we want to start them as well
if (GameTicker.RunLevel <= GameRunLevel.InRound)
ruleEnt = GameTicker.AddGameRule(rule);
else
GameTicker.StartGameRule(rule, out ruleEnt);
component.AdditionalGameRules.Add(ruleEnt);
}
}
protected override void Ended(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
@@ -37,32 +74,101 @@ public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
}
}
private void PickRule(SecretRuleComponent component)
private bool TryPickPreset(ProtoId<WeightedRandomPrototype> weights, [NotNullWhen(true)] out GamePresetPrototype? preset)
{
// TODO: This doesn't consider what can't start due to minimum player count,
// but currently there's no way to know anyway as they use cvars.
var presetString = _configurationManager.GetCVar(CCVars.SecretWeightPrototype);
var preset = _prototypeManager.Index<WeightedRandomPrototype>(presetString).Pick(_random);
Log.Info($"Selected {preset} for secret.");
_adminLogger.Add(LogType.EventStarted, $"Selected {preset} for secret.");
_chatManager.SendAdminAnnouncement(Loc.GetString("rule-secret-selected-preset", ("preset", preset)));
var options = _prototypeManager.Index(weights).Weights.ShallowClone();
var players = GameTicker.ReadyPlayerCount();
var rules = _prototypeManager.Index<GamePresetPrototype>(preset).Rules;
foreach (var rule in rules)
GamePresetPrototype? selectedPreset = null;
var sum = options.Values.Sum();
while (options.Count > 0)
{
EntityUid ruleEnt;
// if we're pre-round (i.e. will only be added)
// then just add rules. if we're added in the middle of the round (or at any other point really)
// then we want to start them as well
if (GameTicker.RunLevel <= GameRunLevel.InRound)
ruleEnt = GameTicker.AddGameRule(rule);
else
var accumulated = 0f;
var rand = _random.NextFloat(sum);
foreach (var (key, weight) in options)
{
GameTicker.StartGameRule(rule, out ruleEnt);
accumulated += weight;
if (accumulated < rand)
continue;
if (!_prototypeManager.TryIndex(key, out selectedPreset))
Log.Error($"Invalid preset {selectedPreset} in secret rule weights: {weights}");
options.Remove(key);
sum -= weight;
break;
}
component.AdditionalGameRules.Add(ruleEnt);
if (CanPick(selectedPreset, players))
{
preset = selectedPreset;
return true;
}
if (selectedPreset != null)
Log.Info($"Excluding {selectedPreset.ID} from secret preset selection.");
}
preset = null;
return false;
}
public bool CanPickAny()
{
var secretPresetId = _configurationManager.GetCVar(CCVars.SecretWeightPrototype);
return CanPickAny(secretPresetId);
}
/// <summary>
/// Can any of the given presets be picked, taking into account the currently available player count?
/// </summary>
public bool CanPickAny(ProtoId<WeightedRandomPrototype> weightedPresets)
{
var ids = _prototypeManager.Index(weightedPresets).Weights.Keys
.Select(x => new ProtoId<GamePresetPrototype>(x));
return CanPickAny(ids);
}
/// <summary>
/// Can any of the given presets be picked, taking into account the currently available player count?
/// </summary>
public bool CanPickAny(IEnumerable<ProtoId<GamePresetPrototype>> protos)
{
var players = GameTicker.ReadyPlayerCount();
foreach (var id in protos)
{
if (!_prototypeManager.TryIndex(id, out var selectedPreset))
Log.Error($"Invalid preset {selectedPreset} in secret rule weights: {id}");
if (CanPick(selectedPreset, players))
return true;
}
return false;
}
/// <summary>
/// Can the given preset be picked, taking into account the currently available player count?
/// </summary>
private bool CanPick([NotNullWhen(true)] GamePresetPrototype? selected, int players)
{
if (selected == null)
return false;
foreach (var ruleId in selected.Rules)
{
if (!_prototypeManager.TryIndex(ruleId, out EntityPrototype? rule)
|| !rule.TryGetComponent(_ruleCompName, out GameRuleComponent? ruleComp))
{
Log.Error($"Encountered invalid rule {ruleId} in preset {selected.ID}");
return false;
}
if (ruleComp.MinPlayers > players)
return false;
}
return true;
}
}

View File

@@ -1,19 +0,0 @@
using Content.Shared.Whitelist;
namespace Content.Server.Labels.Components
{
[RegisterComponent]
public sealed partial class HandLabelerComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("assignedLabel")]
public string AssignedLabel { get; set; } = string.Empty;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("maxLabelChars")]
public int MaxLabelChars { get; set; } = 50;
[DataField("whitelist")]
public EntityWhitelist Whitelist = new();
}
}

View File

@@ -1,116 +1,8 @@
using Content.Server.Labels.Components;
using Content.Server.UserInterface;
using Content.Server.Popups;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Labels;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Content.Shared.Labels.EntitySystems;
namespace Content.Server.Labels
namespace Content.Server.Labels.Label;
public sealed class HandLabelerSystem : SharedHandLabelerSystem
{
/// <summary>
/// A hand labeler system that lets an object apply labels to objects with the <see cref="LabelComponent"/> .
/// </summary>
[UsedImplicitly]
public sealed class HandLabelerSystem : EntitySystem
{
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly LabelSystem _labelSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HandLabelerComponent, AfterInteractEvent>(AfterInteractOn);
SubscribeLocalEvent<HandLabelerComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
// Bound UI subscriptions
SubscribeLocalEvent<HandLabelerComponent, HandLabelerLabelChangedMessage>(OnHandLabelerLabelChanged);
}
private void OnUtilityVerb(EntityUid uid, HandLabelerComponent handLabeler, GetVerbsEvent<UtilityVerb> args)
{
if (args.Target is not { Valid: true } target || !handLabeler.Whitelist.IsValid(target) || !args.CanAccess)
return;
string labelerText = handLabeler.AssignedLabel == string.Empty ? Loc.GetString("hand-labeler-remove-label-text") : Loc.GetString("hand-labeler-add-label-text");
var verb = new UtilityVerb()
{
Act = () =>
{
AddLabelTo(uid, handLabeler, target, out var result);
if (result != null)
_popupSystem.PopupEntity(result, args.User, args.User);
},
Text = labelerText
};
args.Verbs.Add(verb);
}
private void AfterInteractOn(EntityUid uid, HandLabelerComponent handLabeler, AfterInteractEvent args)
{
if (args.Target is not {Valid: true} target || !handLabeler.Whitelist.IsValid(target) || !args.CanReach)
return;
AddLabelTo(uid, handLabeler, target, out string? result);
if (result == null)
return;
_popupSystem.PopupEntity(result, args.User, args.User);
// Log labeling
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.User):user} labeled {ToPrettyString(target):target} with {ToPrettyString(uid):labeler}");
}
private void AddLabelTo(EntityUid uid, HandLabelerComponent? handLabeler, EntityUid target, out string? result)
{
if (!Resolve(uid, ref handLabeler))
{
result = null;
return;
}
if (handLabeler.AssignedLabel == string.Empty)
{
_labelSystem.Label(target, null);
result = Loc.GetString("hand-labeler-successfully-removed");
return;
}
_labelSystem.Label(target, handLabeler.AssignedLabel);
result = Loc.GetString("hand-labeler-successfully-applied");
}
private void OnHandLabelerLabelChanged(EntityUid uid, HandLabelerComponent handLabeler, HandLabelerLabelChangedMessage args)
{
if (args.Actor is not {Valid: true} player)
return;
var label = args.Label.Trim();
handLabeler.AssignedLabel = label.Substring(0, Math.Min(handLabeler.MaxLabelChars, label.Length));
DirtyUI(uid, handLabeler);
// Log label change
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(player):user} set {ToPrettyString(uid):labeler} to apply label \"{handLabeler.AssignedLabel}\"");
}
private void DirtyUI(EntityUid uid,
HandLabelerComponent? handLabeler = null)
{
if (!Resolve(uid, ref handLabeler))
return;
_userInterfaceSystem.SetUiState(uid, HandLabelerUiKey.Key,
new HandLabelerBoundUserInterfaceState(handLabeler.AssignedLabel));
}
}
}

View File

@@ -50,7 +50,7 @@ namespace Content.Server.Labels
/// <param name="text">intended label text (null to remove)</param>
/// <param name="label">label component for resolve</param>
/// <param name="metadata">metadata component for resolve</param>
public void Label(EntityUid uid, string? text, MetaDataComponent? metadata = null, LabelComponent? label = null)
public override void Label(EntityUid uid, string? text, MetaDataComponent? metadata = null, LabelComponent? label = null)
{
if (!Resolve(uid, ref metadata))
return;

View File

@@ -1,113 +0,0 @@
using Content.Server.Light.Events;
using Content.Shared.Actions;
using Content.Shared.Decals;
using Content.Shared.Emag.Systems;
using Content.Shared.Light;
using Content.Shared.Light.Components;
using Content.Shared.Mind.Components;
using Content.Shared.Toggleable;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Light.EntitySystems
{
public sealed class UnpoweredFlashlightSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UnpoweredFlashlightComponent, GetVerbsEvent<ActivationVerb>>(AddToggleLightVerbs);
SubscribeLocalEvent<UnpoweredFlashlightComponent, GetItemActionsEvent>(OnGetActions);
SubscribeLocalEvent<UnpoweredFlashlightComponent, ToggleActionEvent>(OnToggleAction);
SubscribeLocalEvent<UnpoweredFlashlightComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<UnpoweredFlashlightComponent, GotEmaggedEvent>(OnGotEmagged);
SubscribeLocalEvent<UnpoweredFlashlightComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(EntityUid uid, UnpoweredFlashlightComponent component, MapInitEvent args)
{
_actionContainer.EnsureAction(uid, ref component.ToggleActionEntity, component.ToggleAction);
Dirty(uid, component);
}
private void OnToggleAction(EntityUid uid, UnpoweredFlashlightComponent component, ToggleActionEvent args)
{
if (args.Handled)
return;
ToggleLight(uid, component);
args.Handled = true;
}
private void OnGetActions(EntityUid uid, UnpoweredFlashlightComponent component, GetItemActionsEvent args)
{
args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
}
private void AddToggleLightVerbs(EntityUid uid, UnpoweredFlashlightComponent component, GetVerbsEvent<ActivationVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
ActivationVerb verb = new()
{
Text = Loc.GetString("toggle-flashlight-verb-get-data-text"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/light.svg.192dpi.png")),
Act = () => ToggleLight(uid, component),
Priority = -1 // For things like PDA's, Open-UI and other verbs that should be higher priority.
};
args.Verbs.Add(verb);
}
private void OnMindAdded(EntityUid uid, UnpoweredFlashlightComponent component, MindAddedMessage args)
{
_actionsSystem.AddAction(uid, ref component.ToggleActionEntity, component.ToggleAction);
}
private void OnGotEmagged(EntityUid uid, UnpoweredFlashlightComponent component, ref GotEmaggedEvent args)
{
if (!_light.TryGetLight(uid, out var light))
return;
if (_prototypeManager.TryIndex<ColorPalettePrototype>(component.EmaggedColorsPrototype, out var possibleColors))
{
var pick = _random.Pick(possibleColors.Colors.Values);
_light.SetColor(uid, pick, light);
}
args.Repeatable = true;
args.Handled = true;
}
public void ToggleLight(EntityUid uid, UnpoweredFlashlightComponent flashlight)
{
if (!_light.TryGetLight(uid, out var light))
return;
flashlight.LightOn = !flashlight.LightOn;
_light.SetEnabled(uid, flashlight.LightOn, light);
_appearance.SetData(uid, UnpoweredFlashlightVisuals.LightOn, flashlight.LightOn);
_audioSystem.PlayPvs(flashlight.ToggleSound, uid);
RaiseLocalEvent(uid, new LightToggleEvent(flashlight.LightOn), true);
_actionsSystem.SetToggled(flashlight.ToggleActionEntity, flashlight.LightOn);
}
}
}

View File

@@ -1,12 +0,0 @@
namespace Content.Server.Light.Events
{
public sealed class LightToggleEvent : EntityEventArgs
{
public bool IsOn;
public LightToggleEvent(bool isOn)
{
IsOn = isOn;
}
}
}

View File

@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Chat.Systems;
using Content.Server.Fax;
using Content.Shared.Fax.Components;
using Content.Server.Paper;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
@@ -65,6 +66,7 @@ namespace Content.Server.Nuke
paperContent,
Loc.GetString("nuke-codes-fax-paper-name"),
null,
null,
"paper_stamp-centcom",
new List<StampDisplayInfo>
{

View File

@@ -1,12 +0,0 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets this objective's target to the exterminator's target override, if it has one.
/// If not it will be random.
/// </summary>
[RegisterComponent, Access(typeof(TerminatorTargetOverrideSystem))]
public sealed partial class TerminatorTargetOverrideComponent : Component
{
}

View File

@@ -1,41 +0,0 @@
using Content.Server.Objectives.Components;
using Content.Server.Terminator.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles copying the exterminator's target override to this objective.
/// </summary>
public sealed class TerminatorTargetOverrideSystem : EntitySystem
{
[Dependency] private readonly TargetObjectiveSystem _target = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TerminatorTargetOverrideComponent, ObjectiveAssignedEvent>(OnAssigned);
}
private void OnAssigned(EntityUid uid, TerminatorTargetOverrideComponent comp, ref ObjectiveAssignedEvent args)
{
if (args.Mind.OwnedEntity == null)
{
args.Cancelled = true;
return;
}
var user = args.Mind.OwnedEntity.Value;
if (!TryComp<TerminatorComponent>(user, out var terminator))
{
args.Cancelled = true;
return;
}
// this exterminator has a target override so set its objective target accordingly
if (terminator.Target != null)
_target.SetTarget(uid, terminator.Target.Value);
}
}

View File

@@ -4,7 +4,6 @@ using Content.Server.Chat.Managers;
using Content.Server.DeviceNetwork.Components;
using Content.Server.Instruments;
using Content.Server.Light.EntitySystems;
using Content.Server.Light.Events;
using Content.Server.PDA.Ringer;
using Content.Server.Station.Systems;
using Content.Server.Store.Components;
@@ -12,7 +11,9 @@ using Content.Server.Store.Systems;
using Content.Shared.Access.Components;
using Content.Shared.CartridgeLoader;
using Content.Shared.Chat;
using Content.Shared.Light;
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Content.Shared.PDA;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
@@ -207,8 +208,9 @@ namespace Content.Server.PDA
if (!PdaUiKey.Key.Equals(msg.UiKey))
return;
if (TryComp<UnpoweredFlashlightComponent>(uid, out var flashlight))
_unpoweredFlashlight.ToggleLight(uid, flashlight);
// TODO PREDICTION
// When moving this to shared, fill in the user field
_unpoweredFlashlight.TryToggleLight(uid, user: null);
}
private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaShowRingtoneMessage msg)

View File

@@ -1,5 +1,10 @@
using Content.Shared.Power;
using Content.Shared.Whitelist;
using Content.Shared.Power;
using Content.Shared.Whitelist;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Power.Components
{
@@ -26,5 +31,12 @@ namespace Content.Server.Power.Components
/// </summary>
[DataField("whitelist")]
public EntityWhitelist? Whitelist;
/// <summary>
/// Indicates whether the charger is portable and thus subject to EMP effects
/// and bypasses checks for transform, anchored, and ApcPowerReceiverComponent.
/// </summary>
[DataField]
public bool Portable = false;
}
}

View File

@@ -1,8 +1,10 @@
using Content.Server.Power.Components;
using Content.Server.Emp;
using Content.Server.PowerCell;
using Content.Shared.Examine;
using Content.Shared.Power;
using Content.Shared.PowerCell.Components;
using Content.Shared.Emp;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using System.Diagnostics.CodeAnalysis;
@@ -28,6 +30,8 @@ internal sealed class ChargerSystem : EntitySystem
SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<ChargerComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
SubscribeLocalEvent<ChargerComponent, ExaminedEvent>(OnChargerExamine);
SubscribeLocalEvent<ChargerComponent, EmpPulseEvent>(OnEmpPulse);
}
private void OnStartup(EntityUid uid, ChargerComponent component, ComponentStartup args)
@@ -158,18 +162,27 @@ internal sealed class ChargerSystem : EntitySystem
}
}
private void OnEmpPulse(EntityUid uid, ChargerComponent component, ref EmpPulseEvent args)
{
args.Affected = true;
args.Disabled = true;
}
private CellChargerStatus GetStatus(EntityUid uid, ChargerComponent component)
{
if (!TryComp(uid, out TransformComponent? transformComponent))
return CellChargerStatus.Off;
if (!transformComponent.Anchored)
return CellChargerStatus.Off;
if (!component.Portable)
{
if (!TryComp(uid, out TransformComponent? transformComponent) || !transformComponent.Anchored)
return CellChargerStatus.Off;
}
if (!TryComp(uid, out ApcPowerReceiverComponent? apcPowerReceiverComponent))
return CellChargerStatus.Off;
if (!apcPowerReceiverComponent.Powered)
if (!component.Portable && !apcPowerReceiverComponent.Powered)
return CellChargerStatus.Off;
if (HasComp<EmpDisabledComponent>(uid))
return CellChargerStatus.Off;
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
@@ -186,7 +199,7 @@ internal sealed class ChargerSystem : EntitySystem
return CellChargerStatus.Charging;
}
private void TransferPower(EntityUid uid, EntityUid targetEntity, ChargerComponent component, float frameTime)
{
if (!TryComp(uid, out ApcPowerReceiverComponent? receiverComponent))

View File

@@ -47,9 +47,13 @@ public sealed class RandomMetadataSystem : EntitySystem
var outputSegments = new List<string>();
foreach (var segment in segments)
{
if (_prototype.TryIndex<DatasetPrototype>(segment, out var proto))
outputSegments.Add(_random.Pick(proto.Values));
else if (Loc.TryGetString(segment, out var localizedSegment))
if (_prototype.TryIndex<DatasetPrototype>(segment, out var proto)) {
var random = _random.Pick(proto.Values);
if (Loc.TryGetString(random, out var localizedSegment))
outputSegments.Add(localizedSegment);
else
outputSegments.Add(random);
} else if (Loc.TryGetString(segment, out var localizedSegment))
outputSegments.Add(localizedSegment);
else
outputSegments.Add(segment);

View File

@@ -15,7 +15,6 @@ public sealed class RoleSystem : SharedRoleSystem
SubscribeAntagEvents<NukeopsRoleComponent>();
SubscribeAntagEvents<RevolutionaryRoleComponent>();
SubscribeAntagEvents<SubvertedSiliconRoleComponent>();
SubscribeAntagEvents<TerminatorRoleComponent>();
SubscribeAntagEvents<TraitorRoleComponent>();
SubscribeAntagEvents<ZombieRoleComponent>();
SubscribeAntagEvents<ThiefRoleComponent>();

View File

@@ -0,0 +1,30 @@
using Content.Server.Chat.Systems;
using Content.Shared.Chat;
using Robust.Shared.Prototypes;
namespace Content.Server.Speech;
public sealed partial class EmotesMenuSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ChatSystem _chat = default!;
public override void Initialize()
{
base.Initialize();
SubscribeAllEvent<PlayEmoteMessage>(OnPlayEmote);
}
private void OnPlayEmote(PlayEmoteMessage msg, EntitySessionEventArgs args)
{
var player = args.SenderSession.AttachedEntity;
if (!player.HasValue)
return;
if (!_prototypeManager.TryIndex(msg.ProtoId, out var proto) || proto.ChatTriggers.Count == 0)
return;
_chat.TryEmoteWithChat(player.Value, msg.ProtoId);
}
}

View File

@@ -4,6 +4,7 @@ using Content.Server.Speech.Components;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Humanoid;
using Content.Shared.Speech;
using Content.Shared.Speech.Components;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;

View File

@@ -58,8 +58,7 @@ namespace Content.Server.StationEvents
/// </summary>
private void ResetTimer(BasicStationEventSchedulerComponent component)
{
// 5 - 25 minutes. TG does 3-10 but that's pretty frequent
component.TimeUntilNextEvent = _random.Next(300, 1500);
component.TimeUntilNextEvent = _random.Next(3 * 60, 10 * 60);
}
}

View File

@@ -1,19 +0,0 @@
using Content.Server.Terminator.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Terminator.Components;
/// <summary>
/// Main terminator component, handles the target, if any, and objectives.
/// </summary>
[RegisterComponent, Access(typeof(TerminatorSystem))]
public sealed partial class TerminatorComponent : Component
{
/// <summary>
/// Used to force the terminate objective's target.
/// If null it will be a random person.
/// </summary>
[DataField("target")]
public EntityUid? Target;
}

View File

@@ -1,16 +0,0 @@
using Content.Server.Terminator.Systems;
namespace Content.Server.Terminator.Components;
/// <summary>
/// Sets <see cref="TerminatorComponent.Target"/> after the ghost role spawns.
/// </summary>
[RegisterComponent, Access(typeof(TerminatorSystem))]
public sealed partial class TerminatorTargetComponent : Component
{
/// <summary>
/// The target to set after the ghost role spawns.
/// </summary>
[DataField("target")]
public EntityUid? Target;
}

View File

@@ -1,66 +0,0 @@
using Content.Server.Body.Components;
using Content.Server.GenericAntag;
using Content.Server.Ghost.Roles.Events;
using Content.Server.Roles;
using Content.Server.Terminator.Components;
using Content.Shared.Roles;
using Robust.Shared.Map;
namespace Content.Server.Terminator.Systems;
public sealed class TerminatorSystem : EntitySystem
{
[Dependency] private readonly SharedRoleSystem _role = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TerminatorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<TerminatorComponent, GhostRoleSpawnerUsedEvent>(OnSpawned);
SubscribeLocalEvent<TerminatorComponent, GenericAntagCreatedEvent>(OnCreated);
}
private void OnMapInit(EntityUid uid, TerminatorComponent comp, MapInitEvent args)
{
// cyborg doesn't need to breathe
RemComp<RespiratorComponent>(uid);
}
private void OnSpawned(EntityUid uid, TerminatorComponent comp, GhostRoleSpawnerUsedEvent args)
{
if (!TryComp<TerminatorTargetComponent>(args.Spawner, out var target))
return;
comp.Target = target.Target;
}
private void OnCreated(EntityUid uid, TerminatorComponent comp, ref GenericAntagCreatedEvent args)
{
var mindId = args.MindId;
var mind = args.Mind;
_role.MindAddRole(mindId, new RoleBriefingComponent
{
Briefing = Loc.GetString("terminator-role-briefing")
}, mind);
_role.MindAddRole(mindId, new TerminatorRoleComponent(), mind);
}
/// <summary>
/// Create a spawner at a position and return it.
/// </summary>
/// <param name="coords">Coordinates to create the spawner at</param>
/// <param name="target">Optional target mind to force the terminator to target</param>
public EntityUid CreateSpawner(EntityCoordinates coords, EntityUid? target)
{
var uid = Spawn("SpawnPointGhostTerminator", coords);
if (target != null)
{
var comp = EnsureComp<TerminatorTargetComponent>(uid);
comp.Target = target;
}
return uid;
}
}

View File

@@ -1,9 +1,13 @@
using Content.Server.Chat.Managers;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Dataset;
using Content.Shared.Tips;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -22,11 +26,14 @@ public sealed class TipsSystem : EntitySystem
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
private bool _tipsEnabled;
private float _tipTimeOutOfRound;
private float _tipTimeInRound;
private string _tipsDataset = "";
private float _tipTippyChance;
[ViewVariables(VVAccess.ReadWrite)]
private TimeSpan _nextTipTime = TimeSpan.Zero;
@@ -40,10 +47,100 @@ public sealed class TipsSystem : EntitySystem
Subs.CVar(_cfg, CCVars.TipFrequencyInRound, SetInRound, true);
Subs.CVar(_cfg, CCVars.TipsEnabled, SetEnabled, true);
Subs.CVar(_cfg, CCVars.TipsDataset, SetDataset, true);
Subs.CVar(_cfg, CCVars.TipsTippyChance, SetTippyChance, true);
RecalculateNextTipTime();
_conHost.RegisterCommand("tippy", Loc.GetString("cmd-tippy-desc"), Loc.GetString("cmd-tippy-help"), SendTippy, SendTippyHelper);
_conHost.RegisterCommand("tip", Loc.GetString("cmd-tip-desc"), "tip", SendTip);
}
private CompletionResult SendTippyHelper(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 => CompletionResult.FromHintOptions(CompletionHelper.SessionNames(), Loc.GetString("cmd-tippy-auto-1")),
2 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-2")),
3 => CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs<EntityPrototype>(), Loc.GetString("cmd-tippy-auto-3")),
4 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-4")),
5 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-5")),
6 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-6")),
_ => CompletionResult.Empty
};
}
private void SendTip(IConsoleShell shell, string argstr, string[] args)
{
AnnounceRandomTip();
RecalculateNextTipTime();
}
private void SendTippy(IConsoleShell shell, string argstr, string[] args)
{
if (args.Length < 2)
{
shell.WriteLine(Loc.GetString("cmd-tippy-help"));
return;
}
ActorComponent? actor = null;
if (args[0] != "all")
{
ICommonSession? session;
if (args.Length > 0)
{
// Get player entity
if (!_playerManager.TryGetSessionByUsername(args[0], out session))
{
shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user"));
return;
}
}
else
{
session = shell.Player;
}
if (session?.AttachedEntity is not { } user)
{
shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user"));
return;
}
if (!TryComp(user, out actor))
{
shell.WriteError(Loc.GetString("cmd-tippy-error-no-user"));
return;
}
}
var ev = new TippyEvent(args[1]);
if (args.Length > 2)
{
ev.Proto = args[2];
if (!_prototype.HasIndex<EntityPrototype>(args[2]))
{
shell.WriteError(Loc.GetString("cmd-tippy-error-no-prototype", ("proto", args[2])));
return;
}
}
if (args.Length > 3)
ev.SpeakTime = float.Parse(args[3]);
if (args.Length > 4)
ev.SlideTime = float.Parse(args[4]);
if (args.Length > 5)
ev.WaddleInterval = float.Parse(args[5]);
if (actor != null)
RaiseNetworkEvent(ev, actor.PlayerSession);
else
RaiseNetworkEvent(ev);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@@ -81,6 +178,11 @@ public sealed class TipsSystem : EntitySystem
_tipsDataset = value;
}
private void SetTippyChance(float value)
{
_tipTippyChance = value;
}
private void AnnounceRandomTip()
{
if (!_prototype.TryIndex<DatasetPrototype>(_tipsDataset, out var tips))
@@ -89,8 +191,16 @@ public sealed class TipsSystem : EntitySystem
var tip = _random.Pick(tips.Values);
var msg = Loc.GetString("tips-system-chat-message-wrap", ("tip", tip));
_chat.ChatMessageToManyFiltered(Filter.Broadcast(), ChatChannel.OOC, tip, msg,
if (_random.Prob(_tipTippyChance))
{
var ev = new TippyEvent(msg);
ev.SpeakTime = 1 + tip.Length * 0.05f;
RaiseNetworkEvent(ev);
} else
{
_chat.ChatMessageToManyFiltered(Filter.Broadcast(), ChatChannel.OOC, tip, msg,
EntityUid.Invalid, false, false, Color.MediumPurple);
}
}
private void RecalculateNextTipTime()

View File

@@ -105,7 +105,7 @@ public sealed partial class GunSystem : SharedGunSystem
// Update shot based on the recoil
toMap = fromMap.Position + angle.ToVec() * mapDirection.Length();
mapDirection = toMap - fromMap.Position;
var gunVelocity = Physics.GetMapLinearVelocity(gunUid);
var gunVelocity = Physics.GetMapLinearVelocity(fromEnt);
// I must be high because this was getting tripped even when true.
// DebugTools.Assert(direction != Vector2.Zero);

View File

@@ -35,8 +35,7 @@ public sealed class PortalArtifactSystem : EntitySystem
var secondPortal = Spawn(artifact.Comp.PortalProto, _transform.GetMapCoordinates(target));
//Manual position swapping, because the portal that opens doesn't trigger a collision, and doesn't teleport targets the first time.
_transform.SetCoordinates(artifact, Transform(secondPortal).Coordinates);
_transform.SetCoordinates(target, Transform(firstPortal).Coordinates);
_transform.SwapPositions(target, secondPortal);
_link.TryLink(firstPortal, secondPortal, true);
}

View File

@@ -22,7 +22,6 @@ public sealed class ShuffleArtifactSystem : EntitySystem
{
var mobState = GetEntityQuery<MobStateComponent>();
List<EntityCoordinates> allCoords = new();
List<Entity<TransformComponent>> toShuffle = new();
foreach (var ent in _lookup.GetEntitiesInRange(uid, component.Radius, LookupFlags.Dynamic | LookupFlags.Sundries))
@@ -33,13 +32,15 @@ public sealed class ShuffleArtifactSystem : EntitySystem
var xform = Transform(ent);
toShuffle.Add((ent, xform));
allCoords.Add(xform.Coordinates);
}
foreach (var xform in toShuffle)
_random.Shuffle(toShuffle);
while (toShuffle.Count > 1)
{
var xformUid = xform.Owner;
_xform.SetCoordinates(xformUid, xform, _random.PickAndTake(allCoords));
var ent1 = _random.PickAndTake(toShuffle);
var ent2 = _random.PickAndTake(toShuffle);
_xform.SwapPositions((ent1, ent1), (ent2, ent2));
}
}
}

View File

@@ -68,9 +68,10 @@ public sealed class GetItemActionsEvent : EntityEventArgs
AddAction(ref actionId, prototypeId, Provider);
}
public void AddAction(EntityUid actionId)
public void AddAction(EntityUid? actionId)
{
Actions.Add(actionId);
if (actionId != null)
Actions.Add(actionId.Value);
}
}

View File

@@ -99,6 +99,11 @@
/// </summary>
Stealth = 1 << 16,
///<summary>
/// Allows you to use Admin chat
///</summary>
Adminchat = 1 << 17,
/// <summary>
/// Dangerous host permissions like scsi.
/// </summary>

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared.Body.Prototypes
{
@@ -7,5 +7,11 @@ namespace Content.Shared.Body.Prototypes
{
[IdDataField]
public string ID { get; private set; } = default!;
[DataField("name", required: true)]
private LocId Name { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
public string LocalizedName => Loc.GetString(Name);
}
}

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared.Body.Prototypes
{
@@ -9,6 +9,9 @@ namespace Content.Shared.Body.Prototypes
public string ID { get; private set; } = default!;
[DataField("name", required: true)]
public string Name { get; private set; } = default!;
private LocId Name { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
public string LocalizedName => Loc.GetString(Name);
}
}

View File

@@ -435,6 +435,12 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<string> LoginTipsDataset =
CVarDef.Create("tips.login_dataset", "Tips");
/// <summary>
/// The chance for Tippy to replace a normal tip message.
/// </summary>
public static readonly CVarDef<float> TipsTippyChance =
CVarDef.Create("tips.tippy_chance", 0.01f);
/*
* Console
*/
@@ -1994,6 +2000,10 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> GatewayGeneratorEnabled =
CVarDef.Create("gateway.generator_enabled", true);
// Clippy!
public static readonly CVarDef<string> TippyEntity =
CVarDef.Create("tippy.entity", "Tippy", CVar.SERVER | CVar.REPLICATED);
/*
* DEBUG
*/

View File

@@ -0,0 +1,11 @@
using Content.Shared.Chat.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Chat;
[Serializable, NetSerializable]
public sealed class PlayEmoteMessage(ProtoId<EmotePrototype> protoId) : EntityEventArgs
{
public readonly ProtoId<EmotePrototype> ProtoId = protoId;
}

View File

@@ -1,11 +1,13 @@
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Chat.Prototypes;
/// <summary>
/// IC emotes (scream, smile, clapping, etc).
/// Entities can activate emotes by chat input or code.
/// Entities can activate emotes by chat input, radial or code.
/// </summary>
[Prototype("emote")]
public sealed partial class EmotePrototype : IPrototype
@@ -13,18 +15,50 @@ public sealed partial class EmotePrototype : IPrototype
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// Localization string for the emote name. Displayed in the radial UI.
/// </summary>
[DataField(required: true)]
public string Name = default!;
/// <summary>
/// Determines if emote available to all by default
/// <see cref="Whitelist"/> check comes after this setting
/// <see cref="Content.Shared.Speech.SpeechComponent.AllowedEmotes"/> can ignore this setting
/// </summary>
[DataField]
public bool Available = true;
/// <summary>
/// Different emote categories may be handled by different systems.
/// Also may be used for filtering.
/// </summary>
[DataField("category")]
[DataField]
public EmoteCategory Category = EmoteCategory.General;
/// <summary>
/// An icon used to visually represent the emote in radial UI.
/// </summary>
[DataField]
public SpriteSpecifier Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Actions/scream.png"));
/// <summary>
/// Determines conditions to this emote be available to use
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Determines conditions to this emote be unavailable to use
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
/// <summary>
/// Collection of words that will be sent to chat if emote activates.
/// Will be picked randomly from list.
/// </summary>
[DataField("chatMessages")]
[DataField]
public List<string> ChatMessages = new();
/// <summary>
@@ -32,7 +66,7 @@ public sealed partial class EmotePrototype : IPrototype
/// When typed into players chat they will activate emote event.
/// All words should be unique across all emote prototypes.
/// </summary>
[DataField("chatTriggers")]
[DataField]
public HashSet<string> ChatTriggers = new();
}

View File

@@ -1,5 +1,6 @@
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Shared.Chat.Prototypes;
@@ -8,8 +9,8 @@ namespace Content.Shared.Chat.Prototypes;
/// Sounds collection for each <see cref="EmotePrototype"/>.
/// Different entities may use different sounds collections.
/// </summary>
[Prototype("emoteSounds")]
public sealed partial class EmoteSoundsPrototype : IPrototype
[Prototype("emoteSounds"), Serializable, NetSerializable]
public sealed class EmoteSoundsPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;

View File

@@ -96,20 +96,23 @@ public abstract class ClothingSystem : EntitySystem
{
if (TryComp(item, out HideLayerClothingComponent? comp))
{
//Checks for mask toggling. TODO: Make a generic system for this
if (comp.HideOnToggle && TryComp(item, out MaskComponent? mask) && TryComp(item, out ClothingComponent? clothing))
if (comp.Slots.Contains(layer))
{
if (clothing.EquippedPrefix != mask.EquippedPrefix)
//Checks for mask toggling. TODO: Make a generic system for this
if (comp.HideOnToggle && TryComp(item, out MaskComponent? mask) && TryComp(item, out ClothingComponent? clothing))
{
if (clothing.EquippedPrefix != mask.EquippedPrefix)
{
shouldLayerShow = false;
break;
}
}
else
{
shouldLayerShow = false;
break;
}
}
else
{
shouldLayerShow = false;
break;
}
}
}
_humanoidSystem.SetLayerVisibility(equipee, layer, shouldLayerShow);

View File

@@ -45,7 +45,7 @@ public sealed class MaskSystem : EntitySystem
var dir = mask.IsToggled ? "down" : "up";
var msg = $"action-mask-pull-{dir}-popup-message";
_popupSystem.PopupEntity(Loc.GetString(msg, ("mask", uid)), args.Performer, args.Performer);
_popupSystem.PopupClient(Loc.GetString(msg, ("mask", uid)), args.Performer, args.Performer);
ToggleMaskComponents(uid, mask, args.Performer, mask.EquippedPrefix);
}

View File

@@ -17,6 +17,12 @@ namespace Content.Shared.Damage.Prototypes
{
[IdDataField] public string ID { get; } = default!;
[DataField(required: true)]
private LocId Name { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
public string LocalizedName => Loc.GetString(Name);
[DataField("damageTypes", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
public List<string> DamageTypes { get; private set; } = default!;
}

View File

@@ -11,6 +11,12 @@ namespace Content.Shared.Damage.Prototypes
[IdDataField]
public string ID { get; private set; } = default!;
[DataField(required: true)]
private LocId Name { get; set; }
[ViewVariables(VVAccess.ReadOnly)]
public string LocalizedName => Loc.GetString(Name);
/// <summary>
/// The price for each 1% damage reduction in armors
/// </summary>

View File

@@ -1,8 +1,10 @@
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Events;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Verbs;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Damage.Systems;
@@ -10,6 +12,7 @@ namespace Content.Shared.Damage.Systems;
public sealed class DamageExamineSystem : EntitySystem
{
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
public override void Initialize()
{
@@ -66,7 +69,7 @@ public sealed class DamageExamineSystem : EntitySystem
if (damage.Value != FixedPoint2.Zero)
{
msg.PushNewline();
msg.AddMarkup(Loc.GetString("damage-value", ("type", damage.Key), ("amount", damage.Value)));
msg.AddMarkup(Loc.GetString("damage-value", ("type", _prototype.Index<DamageTypePrototype>(damage.Key).LocalizedName), ("amount", damage.Value)));
}
}

View File

@@ -36,13 +36,6 @@ public abstract partial class SharedDisposalUnitComponent : Component
[ViewVariables(VVAccess.ReadWrite), DataField("soundInsert")]
public SoundSpecifier? InsertSound = new SoundPathSpecifier("/Audio/Effects/trashbag1.ogg");
/// <summary>
/// Sound played when an item is thrown and misses the disposal unit.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("soundMiss")]
public SoundSpecifier? MissSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
/// <summary>
/// State for this disposals unit.
/// </summary>

View File

@@ -1,12 +1,13 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Paper;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Fax;
namespace Content.Shared.Fax.Components;
[RegisterComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class FaxMachineComponent : Component
{
/// <summary>
@@ -16,6 +17,13 @@ public sealed partial class FaxMachineComponent : Component
[DataField("name")]
public string FaxName { get; set; } = "Unknown";
/// <summary>
/// Sprite to use when inserting an object.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public string InsertingState = "inserting";
/// <summary>
/// Device address of fax in network to which data will be send
/// </summary>
@@ -26,7 +34,7 @@ public sealed partial class FaxMachineComponent : Component
/// <summary>
/// Contains the item to be sent, assumes it's paper...
/// </summary>
[DataField("paperSlot", required: true)]
[DataField(required: true)]
public ItemSlot PaperSlot = new();
/// <summary>
@@ -34,39 +42,39 @@ public sealed partial class FaxMachineComponent : Component
/// This will make it visible to others on the network
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("responsePings")]
[DataField]
public bool ResponsePings { get; set; } = true;
/// <summary>
/// Should admins be notified on message receive
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("notifyAdmins")]
[DataField]
public bool NotifyAdmins { get; set; } = false;
/// <summary>
/// Should that fax receive nuke codes send by admins. Probably should be captain fax only
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("receiveNukeCodes")]
[DataField]
public bool ReceiveNukeCodes { get; set; } = false;
/// <summary>
/// Sound to play when fax has been emagged
/// </summary>
[DataField("emagSound")]
[DataField]
public SoundSpecifier EmagSound = new SoundCollectionSpecifier("sparks");
/// <summary>
/// Sound to play when fax printing new message
/// </summary>
[DataField("printSound")]
[DataField]
public SoundSpecifier PrintSound = new SoundPathSpecifier("/Audio/Machines/printer.ogg");
/// <summary>
/// Sound to play when fax successfully send message
/// </summary>
[DataField("sendSound")]
[DataField]
public SoundSpecifier SendSound = new SoundPathSpecifier("/Audio/Machines/high_tech_confirm.ogg");
/// <summary>
@@ -79,27 +87,27 @@ public sealed partial class FaxMachineComponent : Component
/// Print queue of the incoming message
/// </summary>
[ViewVariables]
[DataField("printingQueue")]
[DataField]
public Queue<FaxPrintout> PrintingQueue { get; private set; } = new();
/// <summary>
/// Message sending timeout
/// </summary>
[ViewVariables]
[DataField("sendTimeoutRemaining")]
[DataField]
public float SendTimeoutRemaining;
/// <summary>
/// Message sending timeout
/// </summary>
[ViewVariables]
[DataField("sendTimeout")]
[DataField]
public float SendTimeout = 5f;
/// <summary>
/// Remaining time of inserting animation
/// </summary>
[DataField("insertingTimeRemaining")]
[DataField]
public float InsertingTimeRemaining;
/// <summary>
@@ -111,7 +119,7 @@ public sealed partial class FaxMachineComponent : Component
/// <summary>
/// Remaining time of printing animation
/// </summary>
[DataField("printingTimeRemaining")]
[DataField]
public float PrintingTimeRemaining;
/// <summary>
@@ -124,13 +132,16 @@ public sealed partial class FaxMachineComponent : Component
[DataDefinition]
public sealed partial class FaxPrintout
{
[DataField("name", required: true)]
[DataField(required: true)]
public string Name { get; private set; } = default!;
[DataField("content", required: true)]
[DataField]
public string? Label { get; private set; }
[DataField(required: true)]
public string Content { get; private set; } = default!;
[DataField("prototypeId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>), required: true)]
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>), required: true)]
public string PrototypeId { get; private set; } = default!;
[DataField("stampState")]
@@ -143,10 +154,11 @@ public sealed partial class FaxPrintout
{
}
public FaxPrintout(string content, string name, string? prototypeId = null, string? stampState = null, List<StampDisplayInfo>? stampedBy = null)
public FaxPrintout(string content, string name, string? label = null, string? prototypeId = null, string? stampState = null, List<StampDisplayInfo>? stampedBy = null)
{
Content = content;
Name = name;
Label = label;
PrototypeId = prototypeId ?? "";
StampState = stampState;
StampedBy = stampedBy ?? new List<StampDisplayInfo>();

View File

@@ -0,0 +1,16 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Fax.Components;
/// <summary>
/// Entity with this component can be faxed.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class FaxableObjectComponent : Component
{
/// <summary>
/// Sprite to use when inserting an object.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public string InsertingState = "inserting";
}

View File

@@ -0,0 +1,19 @@
using Content.Shared.Damage;
using Robust.Shared.GameStates;
namespace Content.Shared.Fax.Components;
/// <summary>
/// A fax component which stores a damage specifier for attempting to fax a mob.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class FaxecuteComponent : Component
{
/// <summary>
/// Type of damage dealt when entity is faxecuted.
/// </summary>
[DataField(required: true), AutoNetworkedField]
public DamageSpecifier Damage = new();
}

View File

@@ -0,0 +1,9 @@
namespace Content.Shared.Fax.Components;
/// <summary>
/// Event for killing any mob within the fax machine.
/// </summary
[ByRefEvent]
public record struct DamageOnFaxecuteEvent(FaxMachineComponent? Action);

View File

@@ -37,11 +37,13 @@ public sealed class FaxUiState : BoundUserInterfaceState
[Serializable, NetSerializable]
public sealed class FaxFileMessage : BoundUserInterfaceMessage
{
public string? Label;
public string Content;
public bool OfficePaper;
public FaxFileMessage(string content, bool officePaper)
public FaxFileMessage(string? label, string content, bool officePaper)
{
Label = label;
Content = content;
OfficePaper = officePaper;
}
@@ -49,6 +51,7 @@ public sealed class FaxFileMessage : BoundUserInterfaceMessage
public static class FaxFileMessageValidation
{
public const int MaxLabelSize = 50; // parity with Content.Server.Labels.Components.HandLabelerComponent.MaxLabelChars
public const int MaxContentSize = 10000;
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.Damage;
using Content.Shared.Popups;
using Content.Shared.Fax.Components;
namespace Content.Shared.Fax.Systems;
/// <summary>
/// System for handling execution of a mob within fax when copy or send attempt is made.
/// </summary>
public sealed class FaxecuteSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
public override void Initialize()
{
base.Initialize();
}
public void Faxecute(EntityUid uid, FaxMachineComponent component, DamageOnFaxecuteEvent? args = null)
{
var sendEntity = component.PaperSlot.Item;
if (sendEntity == null)
return;
if (!TryComp<FaxecuteComponent>(uid, out var faxecute))
return;
var damageSpec = faxecute.Damage;
_damageable.TryChangeDamage(sendEntity, damageSpec);
_popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-error", ("target", uid)), uid, PopupType.LargeCaution);
return;
}
}

View File

@@ -87,7 +87,6 @@ public abstract partial class SharedHandsSystem
/// </summary>
/// <param name="uid"></param>
/// <param name="handsComp"></param>
public void RemoveHands(EntityUid uid, HandsComponent? handsComp = null)
{
if (!Resolve(uid, ref handsComp))
@@ -137,6 +136,43 @@ public abstract partial class SharedHandsSystem
return false;
}
public bool TryGetActiveHand(Entity<HandsComponent?> entity, [NotNullWhen(true)] out Hand? hand)
{
if (!Resolve(entity, ref entity.Comp, false))
{
hand = null;
return false;
}
hand = entity.Comp.ActiveHand;
return hand != null;
}
public bool TryGetActiveItem(Entity<HandsComponent?> entity, [NotNullWhen(true)] out EntityUid? item)
{
if (!TryGetActiveHand(entity, out var hand))
{
item = null;
return false;
}
item = hand.HeldEntity;
return item != null;
}
public Hand? GetActiveHand(Entity<HandsComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return null;
return entity.Comp.ActiveHand;
}
public EntityUid? GetActiveItem(Entity<HandsComponent?> entity)
{
return GetActiveHand(entity)?.HeldEntity;
}
/// <summary>
/// Enumerate over hands, starting with the currently active hand.
/// </summary>
@@ -227,9 +263,17 @@ public abstract partial class SharedHandsSystem
return true;
}
public bool IsHolding(EntityUid uid, EntityUid? entity, [NotNullWhen(true)] out Hand? inHand, HandsComponent? handsComp = null)
public bool IsHolding(Entity<HandsComponent?> entity, [NotNullWhen(true)] EntityUid? item)
{
return IsHolding(entity, item, out _, entity);
}
public bool IsHolding(EntityUid uid, [NotNullWhen(true)] EntityUid? entity, [NotNullWhen(true)] out Hand? inHand, HandsComponent? handsComp = null)
{
inHand = null;
if (entity == null)
return false;
if (!Resolve(uid, ref handsComp, false))
return false;

View File

@@ -25,6 +25,7 @@ namespace Content.Shared.Input
public static readonly BoundKeyFunction CycleChatChannelBackward = "CycleChatChannelBackward";
public static readonly BoundKeyFunction EscapeContext = "EscapeContext";
public static readonly BoundKeyFunction OpenCharacterMenu = "OpenCharacterMenu";
public static readonly BoundKeyFunction OpenEmotesMenu = "OpenEmotesMenu";
public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu";
public static readonly BoundKeyFunction OpenGuidebook = "OpenGuidebook";
public static readonly BoundKeyFunction OpenInventoryMenu = "OpenInventoryMenu";

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