Merge remote-tracking branch 'upstream/master' into ed-23-02-2025-upstream

# Conflicts:
#	Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs
#	Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs
#	Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml
This commit is contained in:
Ed
2025-02-23 23:07:55 +03:00
406 changed files with 70688 additions and 45859 deletions

View File

@@ -19,11 +19,11 @@ namespace Content.Client.Administration.Systems
OnBwoinkTextMessageRecieved?.Invoke(this, message);
}
public void Send(NetUserId channelId, string text, bool playSound)
public void Send(NetUserId channelId, string text, bool playSound, bool adminOnly)
{
// Reuse the channel ID as the 'true sender'.
// Server will ignore this and if someone makes it not ignore this (which is bad, allows impersonation!!!), that will help.
RaiseNetworkEvent(new BwoinkTextMessage(channelId, channelId, text, playSound: playSound));
RaiseNetworkEvent(new BwoinkTextMessage(channelId, channelId, text, playSound: playSound, adminOnly: adminOnly));
SendInputTextUpdated(channelId, false);
}

View File

@@ -7,7 +7,9 @@
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2">
<BoxContainer Access="Public" Name="BwoinkArea" VerticalExpand="True" />
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<CheckBox Visible="True" Name="PlaySound" Access="Public" Text="{Loc 'admin-bwoink-play-sound'}" Pressed="True" />
<CheckBox Name="AdminOnly" Access="Public" Text="{Loc 'admin-ahelp-admin-only'}" ToolTip="{Loc 'admin-ahelp-admin-only-tooltip'}" />
<Control HorizontalExpand="True" MinWidth="5" />
<CheckBox Name="PlaySound" Access="Public" Text="{Loc 'admin-bwoink-play-sound'}" Pressed="True" />
<Control HorizontalExpand="True" MinWidth="5" />
<Button Visible="True" Name="PopOut" Access="Public" Text="{Loc 'admin-logs-pop-out'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
<Control HorizontalExpand="True" />

View File

@@ -45,6 +45,8 @@ namespace Content.Client.Administration.UI.Bwoink
_adminManager.AdminStatusUpdated += UpdateButtons;
UpdateButtons();
AdminOnly.OnToggled += args => PlaySound.Disabled = args.Pressed;
ChannelSelector.OnSelectionChanged += sel =>
{
_currentPlayer = sel;

View File

@@ -512,39 +512,15 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
if (scroll == null)
return;
if (!TryGetVerticalScrollbar(scroll, out var vScrollbar))
return;
if (!TryGetNextScrollPosition(out float? nextScrollPosition))
return;
vScrollbar.ValueTarget = nextScrollPosition.Value;
scroll.VScrollTarget = nextScrollPosition.Value;
if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
if (MathHelper.CloseToPercent(scroll.VScroll, scroll.VScrollTarget))
_autoScrollActive = false;
}
private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
{
vScrollBar = null;
foreach (var child in scroll.Children)
{
if (child is not VScrollBar)
continue;
var castChild = child as VScrollBar;
if (castChild != null)
{
vScrollBar = castChild;
return true;
}
}
return false;
}
private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
{
nextScrollPosition = null;

View File

@@ -350,35 +350,15 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
if (scroll == null)
return;
if (!TryGetVerticalScrollbar(scroll, out var vScrollbar))
return;
if (!TryGetNextScrollPosition(out float? nextScrollPosition))
return;
vScrollbar.ValueTarget = nextScrollPosition.Value;
scroll.VScrollTarget = nextScrollPosition.Value;
if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
if (MathHelper.CloseToPercent(scroll.VScroll, scroll.VScrollTarget))
_autoScrollActive = false;
}
private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
{
vScrollBar = null;
foreach (var control in scroll.Children)
{
if (control is not VScrollBar)
continue;
vScrollBar = (VScrollBar)control;
return true;
}
return false;
}
private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
{
nextScrollPosition = null;

View File

@@ -1,4 +1,5 @@
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
namespace Content.Client.Atmos.UI
@@ -16,9 +17,7 @@ namespace Content.Client.Atmos.UI
{
base.Open();
_window = new GasAnalyzerWindow();
_window.OnClose += OnClose;
_window.OpenCenteredLeft();
_window = this.CreateWindowCenteredLeft<GasAnalyzerWindow>();
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)

View File

@@ -66,7 +66,7 @@ public sealed class ClientGlobalSoundSystem : SharedGlobalSoundSystem
{
if(!_adminAudioEnabled) return;
var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
var stream = _audio.PlayGlobal(soundEvent.Specifier, Filter.Local(), false, soundEvent.AudioParams);
_adminAudio.Add(stream?.Entity);
}
@@ -75,13 +75,13 @@ public sealed class ClientGlobalSoundSystem : SharedGlobalSoundSystem
// Either the cvar is disabled or it's already playing
if(!_eventAudioEnabled || _eventAudio.ContainsKey(soundEvent.Type)) return;
var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
var stream = _audio.PlayGlobal(soundEvent.Specifier, Filter.Local(), false, soundEvent.AudioParams);
_eventAudio.Add(soundEvent.Type, stream?.Entity);
}
private void PlayGameSound(GameGlobalSoundEvent soundEvent)
{
_audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
_audio.PlayGlobal(soundEvent.Specifier, Filter.Local(), false, soundEvent.AudioParams);
}
private void StopStationEventMusic(StopStationEventMusic soundEvent)

View File

@@ -218,7 +218,7 @@ public sealed partial class ContentAudioSystem
return;
var file = _gameTicker.RestartSound;
if (string.IsNullOrEmpty(file))
if (ResolvedSoundSpecifier.IsNullOrEmpty(file))
{
return;
}

View File

@@ -1,6 +1,7 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Shared.CCVar;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Serialization.Manager;
@@ -125,6 +126,27 @@ namespace Content.Client.Changelog
_sawmill = _logManager.GetSawmill(SawmillName);
}
/// <summary>
/// Tries to return a human-readable version number from the build.json file
/// </summary>
public string GetClientVersion()
{
var fork = _configManager.GetCVar(CVars.BuildForkId);
var version = _configManager.GetCVar(CVars.BuildVersion);
// This trimming might become annoying if down the line some codebases want to switch to a real
// version format like "104.11.3" while others are still using the git hashes
if (version.Length > 7)
version = version[..7];
if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(fork))
return Loc.GetString("changelog-version-unknown");
return Loc.GetString("changelog-version-tag",
("fork", fork),
("version", version));
}
[DataDefinition]
public sealed partial class Changelog
{

View File

@@ -70,22 +70,7 @@ namespace Content.Client.Changelog
Tabs.SetTabTitle(i++, Loc.GetString($"changelog-tab-title-{changelog.Name}"));
}
// Try to get the current version from the build.json file
var version = _cfg.GetCVar(CVars.BuildVersion);
var forkId = _cfg.GetCVar(CVars.BuildForkId);
var versionText = Loc.GetString("changelog-version-unknown");
// Make sure these aren't empty, like in a dev env
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(forkId))
{
versionText = Loc.GetString("changelog-version-tag",
("fork", forkId),
("version", version[..7])); // Only show the first 7 characters
}
// if else statements are ugly, shut up
VersionLabel.Text = versionText;
VersionLabel.Text = _changelog.GetClientVersion();
TabsUpdated();
}

View File

@@ -141,6 +141,11 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
{
return _reagentSources.GetValueOrDefault(id) ?? new List<ReagentSourceData>();
}
// Is handled on server and updated on client via ReagentGuideRegistryChangedEvent
public override void ReloadAllReagentPrototypes()
{
}
}
/// <summary>

View File

@@ -7,6 +7,7 @@ namespace Content.Client.Construction
[RegisterComponent]
public sealed partial class ConstructionGhostComponent : Component
{
public int GhostId { get; set; }
[ViewVariables] public ConstructionPrototype? Prototype { get; set; }
}
}

View File

@@ -55,6 +55,12 @@ namespace Content.Client.Construction
.Register<ConstructionSystem>();
SubscribeLocalEvent<ConstructionGhostComponent, ExaminedEvent>(HandleConstructionGhostExamined);
SubscribeLocalEvent<ConstructionGhostComponent, ComponentShutdown>(HandleGhostComponentShutdown);
}
private void HandleGhostComponentShutdown(EntityUid uid, ConstructionGhostComponent component, ComponentShutdown args)
{
ClearGhost(component.GhostId);
}
private void OnConstructionGuideReceived(ResponseConstructionGuide ev)
@@ -205,8 +211,9 @@ namespace Content.Client.Construction
ghost = EntityManager.SpawnEntity("constructionghost", loc);
var comp = EntityManager.GetComponent<ConstructionGhostComponent>(ghost.Value);
comp.Prototype = prototype;
comp.GhostId = ghost.GetHashCode();
EntityManager.GetComponent<TransformComponent>(ghost.Value).LocalRotation = dir.ToAngle();
_ghosts.Add(ghost.GetHashCode(), ghost.Value);
_ghosts.Add(comp.GhostId, ghost.Value);
var sprite = EntityManager.GetComponent<SpriteComponent>(ghost.Value);
sprite.Color = new Color(48, 255, 48, 128);

View File

@@ -21,11 +21,10 @@ namespace Content.Client.Crayon.UI
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<CrayonWindow>();
_menu = this.CreateWindowCenteredLeft<CrayonWindow>();
_menu.OnColorSelected += SelectColor;
_menu.OnSelected += Select;
PopulateCrayons();
_menu.OpenCenteredLeft();
}
private void PopulateCrayons()

View File

@@ -144,7 +144,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
{
KeyFrames =
{
new AnimationTrackPlaySound.KeyFrame(_audioSystem.GetSound(unit.FlushSound), 0)
new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(unit.FlushSound), 0)
}
});
}

View File

@@ -10,6 +10,7 @@ using Robust.Client.Graphics;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
using Robust.Shared.Audio;
namespace Content.Client.GameTicking.Managers
{
@@ -26,7 +27,7 @@ namespace Content.Client.GameTicking.Managers
[ViewVariables] public bool AreWeReady { get; private set; }
[ViewVariables] public bool IsGameStarted { get; private set; }
[ViewVariables] public string? RestartSound { get; private set; }
[ViewVariables] public ResolvedSoundSpecifier? RestartSound { get; private set; }
[ViewVariables] public string? LobbyBackground { get; private set; }
[ViewVariables] public bool DisallowedLateJoin { get; private set; }
[ViewVariables] public string? ServerInfoBlob { get; private set; }

View File

@@ -1,3 +1,5 @@
using System.Numerics;
using Content.Client.Changelog;
using Content.Client.Hands;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Screens;
@@ -7,6 +9,7 @@ using Content.Shared.CCVar;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
@@ -20,9 +23,11 @@ namespace Content.Client.Gameplay
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly ChangelogManager _changelog = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
private FpsCounter _fpsCounter = default!;
private Label _version = default!;
public MainViewport Viewport => _uiManager.ActiveScreen!.GetWidget<MainViewport>()!;
@@ -40,6 +45,7 @@ namespace Content.Client.Gameplay
base.Startup();
LoadMainScreen();
_configurationManager.OnValueChanged(CCVars.UILayout, ReloadMainScreenValueChange);
// Add the hand-item overlay.
_overlayManager.AddOverlay(new ShowHandItemOverlay());
@@ -50,7 +56,24 @@ namespace Content.Client.Gameplay
UserInterfaceManager.PopupRoot.AddChild(_fpsCounter);
_fpsCounter.Visible = _configurationManager.GetCVar(CCVars.HudFpsCounterVisible);
_configurationManager.OnValueChanged(CCVars.HudFpsCounterVisible, (show) => { _fpsCounter.Visible = show; });
_configurationManager.OnValueChanged(CCVars.UILayout, ReloadMainScreenValueChange);
// Version number watermark.
_version = new Label();
_version.Text = _changelog.GetClientVersion();
_version.Visible = VersionVisible();
UserInterfaceManager.PopupRoot.AddChild(_version);
_configurationManager.OnValueChanged(CCVars.HudVersionWatermark, (show) => { _version.Visible = VersionVisible(); });
_configurationManager.OnValueChanged(CCVars.ForceClientHudVersionWatermark, (show) => { _version.Visible = VersionVisible(); });
// TODO make this centered or something
LayoutContainer.SetPosition(_version, new Vector2(800, 0));
}
// This allows servers to force the watermark on clients
private bool VersionVisible()
{
var client = _configurationManager.GetCVar(CCVars.HudVersionWatermark);
var server = _configurationManager.GetCVar(CCVars.ForceClientHudVersionWatermark);
return client || server;
}
protected override void Shutdown()

View File

@@ -219,10 +219,12 @@ namespace Content.Client.Gameplay
{
entityToClick = GetClickedEntity(mousePosWorld);
}
var transformSystem = _entitySystemManager.GetEntitySystem<SharedTransformSystem>();
var mapSystem = _entitySystemManager.GetEntitySystem<MapSystem>();
coordinates = _mapManager.TryFindGridAt(mousePosWorld, out _, out var grid) ?
grid.MapToGrid(mousePosWorld) :
EntityCoordinates.FromMap(_mapManager, mousePosWorld);
coordinates = _mapManager.TryFindGridAt(mousePosWorld, out var uid, out _) ?
mapSystem.MapToGrid(uid, mousePosWorld) :
transformSystem.ToCoordinates(mousePosWorld);
}
else
{

View File

@@ -0,0 +1,35 @@
<PanelContainer xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Margin="5 5 5 5">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BorderThickness="1" BorderColor="#777777"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical">
<PanelContainer HorizontalExpand="True">
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center">
<BoxContainer Name="IconContainer"/>
<RichTextLabel Name="ResultName"/>
</BoxContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#393c3f"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<GridContainer Columns="2" Margin="10">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<Label Text="{Loc 'guidebook-microwave-ingredients-header'}"/>
<GridContainer Columns="3" Name="IngredientsGrid"/>
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<Label Text="{Loc 'guidebook-microwave-cook-time-header'}"/>
<RichTextLabel Name="CookTimeLabel"/>
</BoxContainer>
</GridContainer>
<BoxContainer Margin="10">
<RichTextLabel Name="ResultDescription" HorizontalAlignment="Left"/>
</BoxContainer>
</BoxContainer>
</PanelContainer>

View File

@@ -0,0 +1,183 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Client.Guidebook.Richtext;
using Content.Client.Message;
using Content.Client.UserInterface.ControlExtensions;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Kitchen;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Guidebook.Controls;
/// <summary>
/// Control for embedding a microwave recipe into a guidebook.
/// </summary>
[UsedImplicitly, GenerateTypedNameReferences]
public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag, ISearchableControl
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
public GuideMicrowaveEmbed()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
MouseFilter = MouseFilterMode.Stop;
_sawmill = _logManager.GetSawmill("guidemicrowaveembed");
}
public GuideMicrowaveEmbed(string recipe) : this()
{
GenerateControl(_prototype.Index<FoodRecipePrototype>(recipe));
}
public GuideMicrowaveEmbed(FoodRecipePrototype recipe) : this()
{
GenerateControl(recipe);
}
public bool CheckMatchesSearch(string query)
{
return this.ChildrenContainText(query);
}
public void SetHiddenState(bool state, string query)
{
Visible = CheckMatchesSearch(query) ? state : !state;
}
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
{
control = null;
if (!args.TryGetValue("Recipe", out var id))
{
_sawmill.Error("Recipe embed tag is missing recipe prototype argument");
return false;
}
if (!_prototype.TryIndex<FoodRecipePrototype>(id, out var recipe))
{
_sawmill.Error($"Specified recipe prototype \"{id}\" is not a valid recipe prototype");
return false;
}
GenerateControl(recipe);
control = this;
return true;
}
private void GenerateHeader(FoodRecipePrototype recipe)
{
var entity = _prototype.Index<EntityPrototype>(recipe.Result);
IconContainer.AddChild(new GuideEntityEmbed(recipe.Result, false, false));
ResultName.SetMarkup(entity.Name);
ResultDescription.SetMarkup(entity.Description);
}
private void GenerateSolidIngredients(FoodRecipePrototype recipe)
{
foreach (var (product, amount) in recipe.IngredientsSolids.OrderByDescending(p => p.Value))
{
var ingredient = _prototype.Index<EntityPrototype>(product);
IngredientsGrid.AddChild(new GuideEntityEmbed(product, false, false));
// solid name
var solidNameMsg = new FormattedMessage();
solidNameMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-solid-name-display", ("ingredient", ingredient.Name)));
solidNameMsg.Pop();
var solidNameLabel = new RichTextLabel();
solidNameLabel.SetMessage(solidNameMsg);
IngredientsGrid.AddChild(solidNameLabel);
// solid quantity
var solidQuantityMsg = new FormattedMessage();
solidQuantityMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-solid-quantity-display", ("amount", amount)));
solidQuantityMsg.Pop();
var solidQuantityLabel = new RichTextLabel();
solidQuantityLabel.SetMessage(solidQuantityMsg);
IngredientsGrid.AddChild(solidQuantityLabel);
}
}
private void GenerateLiquidIngredients(FoodRecipePrototype recipe)
{
foreach (var (product, amount) in recipe.IngredientsReagents.OrderByDescending(p => p.Value))
{
var reagent = _prototype.Index<ReagentPrototype>(product);
// liquid color
var liquidColorMsg = new FormattedMessage();
liquidColorMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-reagent-color-display", ("color", reagent.SubstanceColor)));
liquidColorMsg.Pop();
var liquidColorLabel = new RichTextLabel();
liquidColorLabel.SetMessage(liquidColorMsg);
liquidColorLabel.HorizontalAlignment = Control.HAlignment.Center;
IngredientsGrid.AddChild(liquidColorLabel);
// liquid name
var liquidNameMsg = new FormattedMessage();
liquidNameMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-reagent-name-display", ("reagent", reagent.LocalizedName)));
liquidNameMsg.Pop();
var liquidNameLabel = new RichTextLabel();
liquidNameLabel.SetMessage(liquidNameMsg);
IngredientsGrid.AddChild(liquidNameLabel);
// liquid quantity
var liquidQuantityMsg = new FormattedMessage();
liquidQuantityMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-reagent-quantity-display", ("amount", amount)));
liquidQuantityMsg.Pop();
var liquidQuantityLabel = new RichTextLabel();
liquidQuantityLabel.SetMessage(liquidQuantityMsg);
IngredientsGrid.AddChild(liquidQuantityLabel);
}
}
private void GenerateIngredients(FoodRecipePrototype recipe)
{
GenerateLiquidIngredients(recipe);
GenerateSolidIngredients(recipe);
}
private void GenerateCookTime(FoodRecipePrototype recipe)
{
var msg = new FormattedMessage();
msg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-cook-time", ("time", recipe.CookTime)));
msg.Pop();
CookTimeLabel.SetMessage(msg);
}
private void GenerateControl(FoodRecipePrototype recipe)
{
GenerateHeader(recipe);
GenerateIngredients(recipe);
GenerateCookTime(recipe);
}
}

View File

@@ -0,0 +1,59 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Client.Guidebook.Richtext;
using Content.Shared.Kitchen;
using JetBrains.Annotations;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
namespace Content.Client.Guidebook.Controls;
/// <summary>
/// Control for listing microwave recipes in a guidebook
/// </summary>
[UsedImplicitly]
public sealed partial class GuideMicrowaveGroupEmbed : BoxContainer, IDocumentTag
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
public GuideMicrowaveGroupEmbed()
{
Orientation = LayoutOrientation.Vertical;
IoCManager.InjectDependencies(this);
MouseFilter = MouseFilterMode.Stop;
}
public GuideMicrowaveGroupEmbed(string group) : this()
{
CreateEntries(group);
}
public bool TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
{
control = null;
if (!args.TryGetValue("Group", out var group))
{
Logger.Error("Microwave group embed tag is missing group argument");
return false;
}
CreateEntries(group);
control = this;
return true;
}
private void CreateEntries(string group)
{
var prototypes = _prototype.EnumeratePrototypes<FoodRecipePrototype>()
.Where(p => p.Group.Equals(group))
.OrderBy(p => p.Name);
foreach (var recipe in prototypes)
{
var embed = new GuideMicrowaveEmbed(recipe);
AddChild(embed);
}
}
}

View File

@@ -134,24 +134,14 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
HashSet<ProtoId<GuideEntryPrototype>> entries = new(_entries.Keys);
foreach (var entry in _entries.Values)
{
if (entry.Children.Count > 0)
{
var sortedChildren = entry.Children
.Select(childId => _entries[childId])
.OrderBy(childEntry => childEntry.Priority)
.ThenBy(childEntry => Loc.GetString(childEntry.Name))
.Select(childEntry => new ProtoId<GuideEntryPrototype>(childEntry.Id))
.ToList();
entry.Children = sortedChildren;
}
entries.ExceptWith(entry.Children);
}
rootEntries = entries.ToList();
}
// Only roots need to be sorted.
// As defined in the SS14 Dev Wiki, children are already sorted based on their child field order within their parent's prototype definition.
// Roots are sorted by priority. If there is no defined priority for a root then it is by definition sorted undefined.
return rootEntries
.Select(rootEntryId => _entries[rootEntryId])
.OrderBy(rootEntry => rootEntry.Priority)

View File

@@ -109,7 +109,17 @@ public sealed partial class DocumentParsingManager
.Cast<Control>()))
.Labelled("subheader");
private static readonly Parser<char, Control> TryHeaderControl = OneOf(SubHeaderControlParser, HeaderControlParser);
private static readonly Parser<char, Control> TertiaryHeaderControlParser = Try(String("###"))
.Then(SkipWhitespaces.Then(Map(text => new Label
{
Text = text,
StyleClasses = { "LabelKeyText" }
},
AnyCharExcept('\n').AtLeastOnceString())
.Cast<Control>()))
.Labelled("tertiaryheader");
private static readonly Parser<char, Control> TryHeaderControl = OneOf(TertiaryHeaderControlParser, SubHeaderControlParser, HeaderControlParser);
private static readonly Parser<char, Control> ListControlParser = Try(Char('-'))
.Then(SkipWhitespaces)

View File

@@ -167,7 +167,9 @@ public sealed class GuidebookSystem : EntitySystem
if (!TryComp<SpeechComponent>(uid, out var speech) || speech.SpeechSounds is null)
return;
_audioSystem.PlayGlobal(speech.SpeechSounds, Filter.Local(), false, speech.AudioParams);
// This code is broken because SpeechSounds isn't a file name or sound specifier directly.
// Commenting out to avoid compile failure with https://github.com/space-wizards/RobustToolbox/pull/5540
// _audioSystem.PlayGlobal(speech.SpeechSounds, Filter.Local(), false, speech.AudioParams);
}
public void FakeClientActivateInWorld(EntityUid activated)

View File

@@ -21,14 +21,12 @@ public sealed class HumanoidMarkingModifierBoundUserInterface : BoundUserInterfa
{
base.Open();
_window = this.CreateWindow<HumanoidMarkingModifierWindow>();
_window = this.CreateWindowCenteredLeft<HumanoidMarkingModifierWindow>();
_window.OnMarkingAdded += SendMarkingSet;
_window.OnMarkingRemoved += SendMarkingSet;
_window.OnMarkingColorChange += SendMarkingSetNoResend;
_window.OnMarkingRankChange += SendMarkingSet;
_window.OnLayerInfoModified += SendBaseLayer;
_window.OpenCenteredLeft();
}
protected override void UpdateState(BoundUserInterfaceState state)

View File

@@ -64,11 +64,9 @@ namespace Content.Client.Inventory
{
base.Open();
_strippingMenu = this.CreateWindow<StrippingMenu>();
_strippingMenu = this.CreateWindowCenteredLeft<StrippingMenu>();
_strippingMenu.OnDirty += UpdateMenu;
_strippingMenu.Title = Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner, EntMan)));
_strippingMenu?.OpenCenteredLeft();
}
protected override void Dispose(bool disposing)

View File

@@ -18,9 +18,8 @@ namespace Content.Client.Lathe.UI
{
base.Open();
_menu = this.CreateWindow<LatheMenu>();
_menu = this.CreateWindowCenteredRight<LatheMenu>();
_menu.SetEntity(Owner);
_menu.OpenCenteredRight();
_menu.OnServerListButtonPressed += _ =>
{

View File

@@ -19,7 +19,7 @@ public sealed class BeforeLightTargetOverlay : Overlay
/// <summary>
/// In metres
/// </summary>
private float _skirting = 1.5f;
private float _skirting = 2f;
public const int ContentZIndex = -10;

View File

@@ -39,6 +39,6 @@ public sealed class LightBlurOverlay : Overlay
var target = beforeOverlay.EnlargedLightTarget;
// Yeah that's all this does keep walkin.
//_clyde.BlurRenderTarget(args.Viewport, target, _blurTarget, args.Viewport.Eye, 14f * 2f);
_clyde.BlurRenderTarget(args.Viewport, target, _blurTarget, args.Viewport.Eye, 14f * 5f);
}
}

View File

@@ -1,10 +1,12 @@
using System.Numerics;
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Content.Shared.Maps;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Map.Enumerators;
using Robust.Shared.Physics;
namespace Content.Client.Light;
@@ -17,9 +19,9 @@ public sealed class RoofOverlay : Overlay
private readonly EntityLookupSystem _lookup;
private readonly SharedMapSystem _mapSystem;
private readonly SharedRoofSystem _roof = default!;
private readonly SharedTransformSystem _xformSystem;
private readonly HashSet<Entity<OccluderComponent>> _occluders = new();
private List<Entity<MapGridComponent>> _grids = new();
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
@@ -33,6 +35,7 @@ public sealed class RoofOverlay : Overlay
_lookup = _entManager.System<EntityLookupSystem>();
_mapSystem = _entManager.System<SharedMapSystem>();
_roof = _entManager.System<SharedRoofSystem>();
_xformSystem = _entManager.System<SharedTransformSystem>();
ZIndex = ContentZIndex;
@@ -86,29 +89,14 @@ public sealed class RoofOverlay : Overlay
worldHandle.SetTransform(matty);
var tileEnumerator = _mapSystem.GetTilesEnumerator(grid.Owner, grid, bounds);
var roofEnt = (grid.Owner, grid.Comp, roof);
// Due to stencilling we essentially draw on unrooved tiles
while (tileEnumerator.MoveNext(out var tileRef))
{
if ((tileRef.Tile.Flags & (byte) TileFlag.Roof) == 0x0)
if (!_roof.IsRooved(roofEnt, tileRef.GridIndices))
{
// Check if the tile is occluded in which case hide it anyway.
// This is to avoid lit walls bleeding over to unlit tiles.
_occluders.Clear();
_lookup.GetLocalEntitiesIntersecting(grid.Owner, tileRef.GridIndices, _occluders);
var found = false;
foreach (var occluder in _occluders)
{
if (!occluder.Comp.Enabled)
continue;
found = true;
break;
}
if (!found)
continue;
continue;
}
var local = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize);

View File

@@ -122,7 +122,7 @@ public sealed class PoweredLightVisualizerSystem : VisualizerSystem<PoweredLight
if (comp.BlinkingSound != null)
{
var sound = _audio.GetSound(comp.BlinkingSound);
var sound = _audio.ResolveSound(comp.BlinkingSound);
blinkingAnim.AnimationTracks.Add(new AnimationTrackPlaySound()
{
KeyFrames =

View File

@@ -8,7 +8,8 @@
<SpriteView Scale="2 2"
Margin="0 4 4 4"
OverrideDirection="South"
Name="View"/>
Name="View"
SetSize="64 64"/>
<Label Name="DescriptionLabel"
ClipText="True"
HorizontalExpand="True"/>

View File

@@ -6,7 +6,7 @@
MinSize="800 128">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<BoxContainer Name="RoleNameBox" Orientation="Vertical" Margin="10">
<Label Name="LoadoutNameLabel" Text="{Loc 'loadout-name-edit-label'}"/>
<Label Name="LoadoutNameLabel"/>
<PanelContainer HorizontalExpand="True" SetHeight="24">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />

View File

@@ -39,6 +39,10 @@ public sealed partial class LoadoutWindow : FancyWindow
{
var name = loadout.EntityName;
LoadoutNameLabel.Text = proto.NameDataset == null ?
Loc.GetString("loadout-name-edit-label") :
Loc.GetString("loadout-name-edit-label-dataset");
RoleNameEdit.ToolTip = Loc.GetString(
"loadout-name-edit-tooltip",
("max", HumanoidCharacterProfile.MaxLoadoutNameLength));

View File

@@ -21,9 +21,8 @@ public sealed class MechBoundUserInterface : BoundUserInterface
{
base.Open();
_menu = this.CreateWindow<MechMenu>();
_menu = this.CreateWindowCenteredLeft<MechMenu>();
_menu.SetEntity(Owner);
_menu.OpenCenteredLeft();
_menu.OnRemoveButtonPressed += uid =>
{

View File

@@ -372,14 +372,11 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
if (!_tryToScrollToListFocus)
return;
if (!TryGetVerticalScrollbar(SensorScroller, out var vScrollbar))
return;
if (TryGetNextScrollPosition(out float? nextScrollPosition))
{
vScrollbar.ValueTarget = nextScrollPosition.Value;
SensorScroller.VScrollTarget = nextScrollPosition.Value;
if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
if (MathHelper.CloseToPercent(SensorScroller.VScroll, SensorScroller.VScrollTarget))
{
_tryToScrollToListFocus = false;
return;
@@ -387,22 +384,6 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
}
}
private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
{
vScrollBar = null;
foreach (var child in scroll.Children)
{
if (child is not VScrollBar)
continue;
vScrollBar = (VScrollBar) child;
return true;
}
return false;
}
private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
{
nextScrollPosition = 0;

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Shared.Light.Components;
using Content.Shared.Weather;
using Robust.Client.Graphics;
using Robust.Shared.Map.Components;
@@ -34,11 +35,12 @@ public sealed partial class StencilOverlay
var matrix = _transform.GetWorldMatrix(grid, xformQuery);
var matty = Matrix3x2.Multiply(matrix, invMatrix);
worldHandle.SetTransform(matty);
_entManager.TryGetComponent(grid.Owner, out RoofComponent? roofComp);
foreach (var tile in _map.GetTilesIntersecting(grid.Owner, grid, worldAABB))
{
// Ignored tiles for stencil
if (_weather.CanWeatherAffect(grid.Owner, grid, tile))
if (_weather.CanWeatherAffect(grid.Owner, grid, tile, roofComp))
{
continue;
}

View File

@@ -30,8 +30,7 @@ namespace Content.Client.PDA
private void CreateMenu()
{
_menu = this.CreateWindow<PdaMenu>();
_menu.OpenCenteredLeft();
_menu = this.CreateWindowCenteredLeft<PdaMenu>();
_menu.FlashLightToggleButton.OnToggled += _ =>
{

View File

@@ -123,6 +123,12 @@ namespace Content.Client.Popups
PopupMessage(message, type, coordinates, null, true);
}
public override void PopupPredictedCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small)
{
if (recipient != null && _timing.IsFirstTimePredicted)
PopupCoordinates(message, coordinates, recipient.Value, type);
}
private void PopupCursorInternal(string? message, PopupType type, bool recordReplay)
{
if (message == null)

View File

@@ -0,0 +1,8 @@
using Content.Shared.Power.EntitySystems;
namespace Content.Client.Power.EntitySystems;
public sealed class PowerNetSystem : SharedPowerNetSystem
{
}

View File

@@ -17,7 +17,7 @@ public sealed class PortableGeneratorBoundUserInterface : BoundUserInterface
protected override void Open()
{
base.Open();
_window = this.CreateWindow<GeneratorWindow>();
_window = this.CreateWindowCenteredLeft<GeneratorWindow>();
_window.SetEntity(Owner);
_window.OnState += args =>
{
@@ -34,8 +34,6 @@ public sealed class PortableGeneratorBoundUserInterface : BoundUserInterface
_window.OnPower += SetTargetPower;
_window.OnEjectFuel += EjectFuel;
_window.OnSwitchOutput += SwitchOutput;
_window.OpenCenteredLeft();
}
protected override void UpdateState(BoundUserInterfaceState state)

View File

@@ -269,27 +269,6 @@ public sealed partial class PowerMonitoringWindow
return false;
}
private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
{
vScrollBar = null;
foreach (var child in scroll.Children)
{
if (child is not VScrollBar)
continue;
var castChild = child as VScrollBar;
if (castChild != null)
{
vScrollBar = castChild;
return true;
}
}
return false;
}
private void AutoScrollToFocus()
{
if (!_autoScrollActive)
@@ -299,15 +278,12 @@ public sealed partial class PowerMonitoringWindow
if (scroll == null)
return;
if (!TryGetVerticalScrollbar(scroll, out var vScrollbar))
return;
if (!TryGetNextScrollPosition(out float? nextScrollPosition))
return;
vScrollbar.ValueTarget = nextScrollPosition.Value;
scroll.VScrollTarget = nextScrollPosition.Value;
if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
if (MathHelper.CloseToPercent(scroll.VScroll, scroll.VScrollTarget))
_autoScrollActive = false;
}

View File

@@ -30,9 +30,8 @@ public sealed class SalvageExpeditionConsoleBoundUserInterface : BoundUserInterf
protected override void Open()
{
base.Open();
_window = this.CreateWindow<OfferingWindow>();
_window = this.CreateWindowCenteredLeft<OfferingWindow>();
_window.Title = Loc.GetString("salvage-expedition-window-title");
_window.OpenCenteredLeft();
}
protected override void UpdateState(BoundUserInterfaceState state)

View File

@@ -22,9 +22,8 @@ public sealed class SalvageMagnetBoundUserInterface : BoundUserInterface
{
base.Open();
_window = this.CreateWindow<OfferingWindow>();
_window = this.CreateWindowCenteredLeft<OfferingWindow>();
_window.Title = Loc.GetString("salvage-magnet-window-title");
_window.OpenCenteredLeft();
}
protected override void UpdateState(BoundUserInterfaceState state)

View File

@@ -21,10 +21,9 @@ public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
{
base.Open();
_window = this.CreateWindow<IFFConsoleWindow>();
_window = this.CreateWindowCenteredLeft<IFFConsoleWindow>();
_window.ShowIFF += SendIFFMessage;
_window.ShowVessel += SendVesselMessage;
_window.OpenCenteredLeft();
}
protected override void UpdateState(BoundUserInterfaceState state)

View File

@@ -20,12 +20,21 @@ public sealed class SpriteFadeSystem : EntitySystem
private readonly HashSet<FadingSpriteComponent> _comps = new();
private EntityQuery<SpriteComponent> _spriteQuery;
private EntityQuery<SpriteFadeComponent> _fadeQuery;
private EntityQuery<FadingSpriteComponent> _fadingQuery;
private const float TargetAlpha = 0.4f;
private const float ChangeRate = 1f;
public override void Initialize()
{
base.Initialize();
_spriteQuery = GetEntityQuery<SpriteComponent>();
_fadeQuery = GetEntityQuery<SpriteFadeComponent>();
_fadingQuery = GetEntityQuery<FadingSpriteComponent>();
SubscribeLocalEvent<FadingSpriteComponent, ComponentShutdown>(OnFadingShutdown);
}
@@ -42,28 +51,26 @@ public sealed class SpriteFadeSystem : EntitySystem
base.FrameUpdate(frameTime);
var player = _playerManager.LocalEntity;
var spriteQuery = GetEntityQuery<SpriteComponent>();
var change = ChangeRate * frameTime;
if (TryComp(player, out TransformComponent? playerXform) &&
_stateManager.CurrentState is GameplayState state &&
spriteQuery.TryGetComponent(player, out var playerSprite))
_spriteQuery.TryGetComponent(player, out var playerSprite))
{
var fadeQuery = GetEntityQuery<SpriteFadeComponent>();
var mapPos = _transform.GetMapCoordinates(_playerManager.LocalEntity!.Value, xform: playerXform);
// Also want to handle large entities even if they may not be clickable.
foreach (var ent in state.GetClickableEntities(mapPos))
{
if (ent == player ||
!fadeQuery.HasComponent(ent) ||
!spriteQuery.TryGetComponent(ent, out var sprite) ||
!_fadeQuery.HasComponent(ent) ||
!_spriteQuery.TryGetComponent(ent, out var sprite) ||
sprite.DrawDepth < playerSprite.DrawDepth)
{
continue;
}
if (!TryComp<FadingSpriteComponent>(ent, out var fading))
if (!_fadingQuery.TryComp(ent, out var fading))
{
fading = AddComp<FadingSpriteComponent>(ent);
fading.OriginalAlpha = sprite.Color.A;
@@ -85,7 +92,7 @@ public sealed class SpriteFadeSystem : EntitySystem
if (_comps.Contains(comp))
continue;
if (!spriteQuery.TryGetComponent(uid, out var sprite))
if (!_spriteQuery.TryGetComponent(uid, out var sprite))
continue;
var newColor = Math.Min(sprite.Color.A + change, comp.OriginalAlpha);

View File

@@ -1,8 +1,10 @@
using System.Numerics;
using Content.Client.UserInterface.Systems.Storage;
using Content.Client.UserInterface.Systems.Storage.Controls;
using Content.Shared.Storage;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Storage;
@@ -11,6 +13,8 @@ public sealed class StorageBoundUserInterface : BoundUserInterface
{
private StorageWindow? _window;
public Vector2? Position => _window?.Position;
public StorageBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
@@ -21,7 +25,7 @@ public sealed class StorageBoundUserInterface : BoundUserInterface
_window = IoCManager.Resolve<IUserInterfaceManager>()
.GetUIController<StorageUIController>()
.CreateStorageWindow(Owner);
.CreateStorageWindow(this);
if (EntMan.TryGetComponent(Owner, out StorageComponent? storage))
{
@@ -50,10 +54,20 @@ public sealed class StorageBoundUserInterface : BoundUserInterface
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Reclaim();
}
public void CloseWindow(Vector2 position)
{
if (_window == null)
return;
// Update its position before potentially saving.
// Listen it makes sense okay.
LayoutContainer.SetPosition(_window, position);
_window?.Close();
}
public void Hide()
{
if (_window == null)
@@ -70,11 +84,12 @@ public sealed class StorageBoundUserInterface : BoundUserInterface
_window.Visible = true;
}
public void ReOpen()
public void Show(Vector2 position)
{
_window?.Orphan();
_window = null;
Open();
if (_window == null)
return;
Show();
LayoutContainer.SetPosition(_window, position);
}
}

View File

@@ -19,6 +19,8 @@ public sealed class StorageSystem : SharedStorageSystem
private Dictionary<EntityUid, ItemStorageLocation> _oldStoredItems = new();
private List<(StorageBoundUserInterface Bui, bool Value)> _queuedBuis = new();
public override void Initialize()
{
base.Initialize();
@@ -72,7 +74,7 @@ public sealed class StorageSystem : SharedStorageSystem
if (NestedStorage && player != null && ContainerSystem.TryGetContainingContainer((uid, null, null), out var container) &&
UI.TryGetOpenUi<StorageBoundUserInterface>(container.Owner, StorageComponent.StorageUiKey.Key, out var containerBui))
{
containerBui.Hide();
_queuedBuis.Add((containerBui, false));
}
}
}
@@ -89,7 +91,7 @@ public sealed class StorageSystem : SharedStorageSystem
{
if (UI.TryGetOpenUi<StorageBoundUserInterface>(uid, StorageComponent.StorageUiKey.Key, out var storageBui))
{
storageBui.Hide();
_queuedBuis.Add((storageBui, false));
}
}
@@ -97,7 +99,7 @@ public sealed class StorageSystem : SharedStorageSystem
{
if (UI.TryGetOpenUi<StorageBoundUserInterface>(uid, StorageComponent.StorageUiKey.Key, out var storageBui))
{
storageBui.Show();
_queuedBuis.Add((storageBui, true));
}
}
@@ -152,4 +154,30 @@ public sealed class StorageSystem : SharedStorageSystem
}
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!_timing.IsFirstTimePredicted)
{
return;
}
// This update loop exists just to synchronize with UISystem and avoid 1-tick delays.
// If deferred opens / closes ever get removed you can dump this.
foreach (var (bui, open) in _queuedBuis)
{
if (open)
{
bui.Show();
}
else
{
bui.Hide();
}
}
_queuedBuis.Clear();
}
}

View File

@@ -33,7 +33,7 @@ public sealed class TimerTriggerVisualizerSystem : VisualizerSystem<TimerTrigger
{
comp.PrimingAnimation.AnimationTracks.Add(
new AnimationTrackPlaySound() {
KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.GetSound(comp.PrimingSound), 0) }
KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(comp.PrimingSound), 0) }
}
);
}

View File

@@ -175,7 +175,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
UIHelper = isAdmin ? new AdminAHelpUIHandler(ownerUserId) : new UserAHelpUIHandler(ownerUserId);
UIHelper.DiscordRelayChanged(_discordRelayActive);
UIHelper.SendMessageAction = (userId, textMessage, playSound) => _bwoinkSystem?.Send(userId, textMessage, playSound);
UIHelper.SendMessageAction = (userId, textMessage, playSound, adminOnly) => _bwoinkSystem?.Send(userId, textMessage, playSound, adminOnly);
UIHelper.InputTextChanged += (channel, text) => _bwoinkSystem?.SendInputTextUpdated(channel, text.Length > 0);
UIHelper.OnClose += () => { SetAHelpPressed(false); };
UIHelper.OnOpen += () => { SetAHelpPressed(true); };
@@ -324,7 +324,7 @@ public interface IAHelpUIHandler : IDisposable
public void PeopleTypingUpdated(BwoinkPlayerTypingUpdated args);
public event Action OnClose;
public event Action OnOpen;
public Action<NetUserId, string, bool>? SendMessageAction { get; set; }
public Action<NetUserId, string, bool, bool>? SendMessageAction { get; set; }
public event Action<NetUserId, string>? InputTextChanged;
}
public sealed class AdminAHelpUIHandler : IAHelpUIHandler
@@ -408,7 +408,7 @@ public sealed class AdminAHelpUIHandler : IAHelpUIHandler
public event Action? OnClose;
public event Action? OnOpen;
public Action<NetUserId, string, bool>? SendMessageAction { get; set; }
public Action<NetUserId, string, bool, bool>? SendMessageAction { get; set; }
public event Action<NetUserId, string>? InputTextChanged;
public void Open(NetUserId channelId, bool relayActive)
@@ -462,7 +462,7 @@ public sealed class AdminAHelpUIHandler : IAHelpUIHandler
if (_activePanelMap.TryGetValue(channelId, out var existingPanel))
return existingPanel;
_activePanelMap[channelId] = existingPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(channelId, text, Window?.Bwoink.PlaySound.Pressed ?? true));
_activePanelMap[channelId] = existingPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(channelId, text, Window?.Bwoink.PlaySound.Pressed ?? true, Window?.Bwoink.AdminOnly.Pressed ?? false));
existingPanel.InputTextChanged += text => InputTextChanged?.Invoke(channelId, text);
existingPanel.Visible = false;
if (!Control!.BwoinkArea.Children.Contains(existingPanel))
@@ -548,7 +548,7 @@ public sealed class UserAHelpUIHandler : IAHelpUIHandler
public event Action? OnClose;
public event Action? OnOpen;
public Action<NetUserId, string, bool>? SendMessageAction { get; set; }
public Action<NetUserId, string, bool, bool>? SendMessageAction { get; set; }
public event Action<NetUserId, string>? InputTextChanged;
public void Open(NetUserId channelId, bool relayActive)
@@ -561,7 +561,7 @@ public sealed class UserAHelpUIHandler : IAHelpUIHandler
{
if (_window is { Disposed: false })
return;
_chatPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(_ownerId, text, true));
_chatPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(_ownerId, text, true, false));
_chatPanel.InputTextChanged += text => InputTextChanged?.Invoke(_ownerId, text);
_chatPanel.RelayedToDiscordLabel.Visible = relayActive;
_window = new DefaultWindow()

View File

@@ -9,6 +9,7 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Input;
using Content.Shared.Item;
using Content.Shared.Storage;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -190,6 +191,26 @@ public sealed class StorageWindow : BaseWindow
BuildGridRepresentation();
}
private void CloseParent()
{
if (StorageEntity == null)
return;
var containerSystem = _entity.System<SharedContainerSystem>();
var uiSystem = _entity.System<UserInterfaceSystem>();
if (containerSystem.TryGetContainingContainer(StorageEntity.Value, out var container) &&
_entity.TryGetComponent(container.Owner, out StorageComponent? storage) &&
storage.Container.Contains(StorageEntity.Value) &&
uiSystem
.TryGetOpenUi<StorageBoundUserInterface>(container.Owner,
StorageComponent.StorageUiKey.Key,
out var parentBui))
{
parentBui.CloseWindow(Position);
}
}
private void BuildGridRepresentation()
{
if (!_entity.TryGetComponent<StorageComponent>(StorageEntity, out var comp) || comp.Grid.Count == 0)
@@ -212,7 +233,9 @@ public sealed class StorageWindow : BaseWindow
};
exitButton.OnPressed += _ =>
{
// Close ourselves and all parent BUIs.
Close();
CloseParent();
};
exitButton.OnKeyBindDown += args =>
{
@@ -220,6 +243,7 @@ public sealed class StorageWindow : BaseWindow
if (!args.Handled && args.Function == ContentKeyFunctions.ActivateItemInWorld)
{
Close();
CloseParent();
args.Handle();
}
};
@@ -258,7 +282,8 @@ public sealed class StorageWindow : BaseWindow
var containerSystem = _entity.System<SharedContainerSystem>();
if (containerSystem.TryGetContainingContainer(StorageEntity.Value, out var container) &&
_entity.TryGetComponent(container.Owner, out StorageComponent? storage))
_entity.TryGetComponent(container.Owner, out StorageComponent? storage) &&
storage.Container.Contains(StorageEntity.Value))
{
Close();
@@ -267,7 +292,7 @@ public sealed class StorageWindow : BaseWindow
StorageComponent.StorageUiKey.Key,
out var parentBui))
{
parentBui.Show();
parentBui.Show(Position);
}
}
};
@@ -412,6 +437,8 @@ public sealed class StorageWindow : BaseWindow
{
if (storageComp.StoredItems.TryGetValue(ent, out var updated))
{
data.Control.Marked = IsMarked(ent);
if (data.Loc.Equals(updated))
{
DebugTools.Assert(data.Control.Location == updated);
@@ -450,12 +477,7 @@ public sealed class StorageWindow : BaseWindow
var gridPiece = new ItemGridPiece((ent, itemEntComponent), loc, _entity)
{
MinSize = size,
Marked = _contained.IndexOf(ent) switch
{
0 => ItemGridPieceMarks.First,
1 => ItemGridPieceMarks.Second,
_ => null,
}
Marked = IsMarked(ent),
};
gridPiece.OnPiecePressed += OnPiecePressed;
gridPiece.OnPieceUnpressed += OnPieceUnpressed;
@@ -467,6 +489,16 @@ public sealed class StorageWindow : BaseWindow
}
}
private ItemGridPieceMarks? IsMarked(EntityUid uid)
{
return _contained.IndexOf(uid) switch
{
0 => ItemGridPieceMarks.First,
1 => ItemGridPieceMarks.Second,
_ => null,
};
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
@@ -486,8 +518,9 @@ public sealed class StorageWindow : BaseWindow
{
if (StorageEntity != null && _entity.System<StorageSystem>().NestedStorage)
{
// If parent container nests us then show back button
if (containerSystem.TryGetContainingContainer(StorageEntity.Value, out var container) &&
_entity.HasComponent<StorageComponent>(container.Owner))
_entity.TryGetComponent(container.Owner, out StorageComponent? storageComp) && storageComp.Container.Contains(StorageEntity.Value))
{
_backButton.Visible = true;
}

View File

@@ -11,6 +11,7 @@ using Content.Shared.CCVar;
using Content.Shared.Input;
using Content.Shared.Interaction;
using Content.Shared.Storage;
using Robust.Client.GameObjects;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
@@ -37,6 +38,7 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
[Dependency] private readonly IInputManager _input = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[UISystemDependency] private readonly StorageSystem _storage = default!;
[UISystemDependency] private readonly UserInterfaceSystem _ui = default!;
private readonly DragDropHelper<ItemGridPiece> _menuDragHelper;
@@ -59,39 +61,11 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
{
base.Initialize();
UIManager.OnScreenChanged += OnScreenChange;
_configuration.OnValueChanged(CCVars.StaticStorageUI, OnStaticStorageChanged, true);
_configuration.OnValueChanged(CCVars.OpaqueStorageWindow, OnOpaqueWindowChanged, true);
_configuration.OnValueChanged(CCVars.StorageWindowTitle, OnStorageWindowTitle, true);
}
private void OnScreenChange((UIScreen? Old, UIScreen? New) obj)
{
// Handle reconnects with hotbargui.
// Essentially HotbarGui / the screen gets loaded AFTER gamestates at the moment (because clientgameticker manually changes it via event)
// and changing this may be a massive change.
// So instead we'll just manually reload it for now.
if (!StaticStorageUIEnabled ||
obj.New == null ||
!EntityManager.TryGetComponent(_player.LocalEntity, out UserInterfaceUserComponent? userComp))
{
return;
}
// UISystemDependency not injected at this point so do it the old fashion way, I love ordering issues.
var uiSystem = EntityManager.System<SharedUserInterfaceSystem>();
foreach (var bui in uiSystem.GetActorUis((_player.LocalEntity.Value, userComp)))
{
if (!uiSystem.TryGetOpenUi<StorageBoundUserInterface>(bui.Entity, StorageComponent.StorageUiKey.Key, out var storageBui))
continue;
storageBui.ReOpen();
}
}
private void OnStorageWindowTitle(bool obj)
{
WindowTitle = obj;
@@ -107,7 +81,7 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
StaticStorageUIEnabled = obj;
}
public StorageWindow CreateStorageWindow(EntityUid uid)
public StorageWindow CreateStorageWindow(StorageBoundUserInterface sBui)
{
var window = new StorageWindow();
window.MouseFilter = Control.MouseFilterMode.Pass;
@@ -127,9 +101,25 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
}
else
{
window.OpenCenteredLeft();
// Open at parent position if it's open.
if (_ui.TryGetOpenUi<StorageBoundUserInterface>(EntityManager.GetComponent<TransformComponent>(sBui.Owner).ParentUid,
StorageComponent.StorageUiKey.Key, out var bui) && bui.Position != null)
{
window.Open(bui.Position.Value);
}
// Open at the saved position if it exists.
else if (_ui.TryGetPosition(sBui.Owner, StorageComponent.StorageUiKey.Key, out var pos))
{
window.Open(pos);
}
// Open at the default position.
else
{
window.OpenCenteredLeft();
}
}
_ui.RegisterControl(sBui, window);
return window;
}

View File

@@ -23,8 +23,7 @@ namespace Content.Client.VendingMachines
{
base.Open();
_menu = this.CreateWindow<VendingMachineMenu>();
_menu.OpenCenteredLeft();
_menu = this.CreateWindowCenteredLeft<VendingMachineMenu>();
_menu.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
_menu.OnItemSelected += OnItemSelected;
Refresh();

View File

@@ -6,6 +6,7 @@ using Content.Shared.Hands.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.StatusEffect;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Components;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Client.GameObjects;
@@ -89,16 +90,6 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
// TODO using targeted actions while combat mode is enabled should NOT trigger attacks.
// TODO: Need to make alt-fire melee its own component I guess?
// Melee and guns share a lot in the middle but share virtually nothing at the start and end so
// it's kinda tricky.
// I think as long as we make secondaries their own component it's probably fine
// as long as guncomp has an alt-use key then it shouldn't be too much of a PITA to deal with.
if (TryComp<GunComponent>(weaponUid, out var gun) && gun.UseKey)
{
return;
}
var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
if (mousePos.MapId == MapId.Nullspace)
@@ -116,6 +107,30 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
{
coordinates = TransformSystem.ToCoordinates(_map.GetMap(mousePos.MapId), mousePos);
}
// If the gun has AltFireComponent, it can be used to attack.
if (TryComp<GunComponent>(weaponUid, out var gun) && gun.UseKey)
{
if (!TryComp<AltFireMeleeComponent>(weaponUid, out var altFireComponent) || altDown != BoundKeyState.Down)
return;
switch(altFireComponent.AttackType)
{
case AltFireAttackType.Light:
ClientLightAttack(entity, mousePos, coordinates, weaponUid, weapon);
break;
case AltFireAttackType.Heavy:
ClientHeavyAttack(entity, coordinates, weaponUid, weapon);
break;
case AltFireAttackType.Disarm:
ClientDisarm(entity, mousePos, coordinates);
break;
}
return;
}
// Heavy attack.
if (altDown == BoundKeyState.Down)
@@ -123,14 +138,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
// If it's an unarmed attack then do a disarm
if (weapon.AltDisarm && weaponUid == entity)
{
EntityUid? target = null;
if (_stateManager.CurrentState is GameplayStateBase screen)
{
target = screen.GetClickedEntity(mousePos);
}
EntityManager.RaisePredictiveEvent(new DisarmAttackEvent(GetNetEntity(target), GetNetCoordinates(coordinates)));
ClientDisarm(entity, mousePos, coordinates);
return;
}
@@ -140,28 +148,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
// Light attack
if (useDown == BoundKeyState.Down)
{
var attackerPos = TransformSystem.GetMapCoordinates(entity);
if (mousePos.MapId != attackerPos.MapId ||
(attackerPos.Position - mousePos.Position).Length() > weapon.Range)
{
return;
}
EntityUid? target = null;
if (_stateManager.CurrentState is GameplayStateBase screen)
{
target = screen.GetClickedEntity(mousePos);
}
// Don't light-attack if interaction will be handling this instead
if (Interaction.CombatModeCanHandInteract(entity, target))
return;
RaisePredictiveEvent(new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(coordinates)));
}
ClientLightAttack(entity, mousePos, coordinates, weaponUid, weapon);
}
protected override bool InRange(EntityUid user, EntityUid target, float range, ICommonSession? session)
@@ -235,6 +222,35 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
var entities = GetNetEntityList(ArcRayCast(userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user).ToList());
RaisePredictiveEvent(new HeavyAttackEvent(GetNetEntity(meleeUid), entities.GetRange(0, Math.Min(MaxTargets, entities.Count)), GetNetCoordinates(coordinates)));
}
private void ClientDisarm(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates)
{
EntityUid? target = null;
if (_stateManager.CurrentState is GameplayStateBase screen)
target = screen.GetClickedEntity(mousePos);
RaisePredictiveEvent(new DisarmAttackEvent(GetNetEntity(target), GetNetCoordinates(coordinates)));
}
private void ClientLightAttack(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates, EntityUid weaponUid, MeleeWeaponComponent meleeComponent)
{
var attackerPos = TransformSystem.GetMapCoordinates(attacker);
if (mousePos.MapId != attackerPos.MapId || (attackerPos.Position - mousePos.Position).Length() > meleeComponent.Range)
return;
EntityUid? target = null;
if (_stateManager.CurrentState is GameplayStateBase screen)
target = screen.GetClickedEntity(mousePos);
// Don't light-attack if interaction will be handling this instead
if (Interaction.CombatModeCanHandInteract(attacker, target))
return;
RaisePredictiveEvent(new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(coordinates)));
}
private void OnMeleeLunge(MeleeLungeEvent ev)
{

View File

@@ -16,6 +16,7 @@ public sealed class TetherGunSystem : SharedTetherGunSystem
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
public override void Initialize()
{
@@ -73,11 +74,11 @@ public sealed class TetherGunSystem : SharedTetherGunSystem
if (_mapManager.TryFindGridAt(mouseWorldPos, out var gridUid, out _))
{
coords = EntityCoordinates.FromMap(gridUid, mouseWorldPos, TransformSystem);
coords = TransformSystem.ToCoordinates(gridUid, mouseWorldPos);
}
else
{
coords = EntityCoordinates.FromMap(_mapManager.GetMapEntityId(mouseWorldPos.MapId), mouseWorldPos, TransformSystem);
coords = TransformSystem.ToCoordinates(_mapSystem.GetMap(mouseWorldPos.MapId), mouseWorldPos);
}
const float BufferDistance = 0.1f;

View File

@@ -37,6 +37,7 @@ public sealed partial class GunSystem : SharedGunSystem
[Dependency] private readonly InputSystem _inputSystem = default!;
[Dependency] private readonly SharedCameraRecoilSystem _recoil = default!;
[Dependency] private readonly SharedMapSystem _maps = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
[ValidatePrototypeId<EntityPrototype>]
public const string HitscanProto = "HitscanEffect";
@@ -102,6 +103,13 @@ public sealed partial class GunSystem : SharedGunSystem
private void OnHitscan(HitscanEvent ev)
{
// ALL I WANT IS AN ANIMATED EFFECT
// TODO EFFECTS
// This is very jank
// because the effect consists of three unrelatd entities, the hitscan beam can be split appart.
// E.g., if a grid rotates while part of the beam is parented to the grid, and part of it is parented to the map.
// Ideally, there should only be one entity, with one sprite that has multiple layers
// Or at the very least, have the other entities parented to the same entity to make sure they stick together.
foreach (var a in ev.Sprites)
{
if (a.Sprite is not SpriteSpecifier.Rsi rsi)
@@ -109,13 +117,17 @@ public sealed partial class GunSystem : SharedGunSystem
var coords = GetCoordinates(a.coordinates);
if (Deleted(coords.EntityId))
if (!TryComp(coords.EntityId, out TransformComponent? relativeXform))
continue;
var ent = Spawn(HitscanProto, coords);
var sprite = Comp<SpriteComponent>(ent);
var xform = Transform(ent);
xform.LocalRotation = a.angle;
var targetWorldRot = a.angle + _xform.GetWorldRotation(relativeXform);
var delta = targetWorldRot - _xform.GetWorldRotation(xform);
_xform.SetLocalRotationNoLerp(ent, xform.LocalRotation + delta, xform);
sprite[EffectLayers.Unshaded].AutoAnimated = false;
sprite.LayerSetSprite(EffectLayers.Unshaded, rsi);
sprite.LayerSetState(EffectLayers.Unshaded, rsi.RsiState);

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Shared.Light.Components;
using Content.Shared.Weather;
using Robust.Client.Audio;
using Robust.Client.GameObjects;
@@ -57,6 +58,7 @@ public sealed class WeatherSystem : SharedWeatherSystem
// Work out tiles nearby to determine volume.
if (TryComp<MapGridComponent>(entXform.GridUid, out var grid))
{
TryComp(entXform.GridUid, out RoofComponent? roofComp);
var gridId = entXform.GridUid.Value;
// FloodFill to the nearest tile and use that for audio.
var seed = _mapSystem.GetTileRef(gridId, grid, entXform.Coordinates);
@@ -71,7 +73,7 @@ public sealed class WeatherSystem : SharedWeatherSystem
if (!visited.Add(node.GridIndices))
continue;
if (!CanWeatherAffect(entXform.GridUid.Value, grid, node))
if (!CanWeatherAffect(entXform.GridUid.Value, grid, node, roofComp))
{
// Add neighbors
// TODO: Ideally we pick some deterministically random direction and use that

View File

@@ -26,11 +26,7 @@ public sealed partial class TestPair
instance.ProtoMan.LoadString(file, changed: changed);
}
await instance.WaitPost(() =>
{
instance.ProtoMan.ResolveResults();
instance.ProtoMan.ReloadPrototypes(changed);
});
await instance.WaitPost(() => instance.ProtoMan.ReloadPrototypes(changed));
foreach (var (kind, ids) in changed)
{
@@ -65,4 +61,4 @@ public sealed partial class TestPair
{
return _loadedPrototypes.TryGetValue(kind, out var ids) && ids.Contains(id);
}
}
}

View File

@@ -0,0 +1,84 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Server.Doors.Systems;
using Content.Shared.Doors.Components;
namespace Content.IntegrationTests.Tests.Doors;
public sealed class AirlockPryingTest : InteractionTest
{
[Test]
public async Task PoweredClosedAirlock_Pry_DoesNotOpen()
{
await SpawnTarget(Airlock);
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
await RunTicks(1);
Assert.That(TryComp<AirlockComponent>(out var airlockComp), "Airlock does not have AirlockComponent?");
Assert.That(airlockComp.Powered, "Airlock should be powered for this test.");
Assert.That(TryComp<DoorComponent>(out var doorComp), "Airlock does not have DoorComponent?");
Assert.That(doorComp.State, Is.EqualTo(DoorState.Closed), "Airlock did not start closed.");
await InteractUsing(Pry);
Assert.That(doorComp.State, Is.EqualTo(DoorState.Closed), "Powered airlock was pried open.");
}
[Test]
public async Task PoweredOpenAirlock_Pry_DoesNotClose()
{
await SpawnTarget(Airlock);
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
await RunTicks(1);
Assert.That(TryComp<AirlockComponent>(out var airlockComp), "Airlock does not have AirlockComponent?");
Assert.That(airlockComp.Powered, "Airlock should be powered for this test.");
var doorSys = SEntMan.System<DoorSystem>();
await Server.WaitPost(() => doorSys.SetState(SEntMan.GetEntity(Target.Value), DoorState.Open));
Assert.That(TryComp<DoorComponent>(out var doorComp), "Airlock does not have DoorComponent?");
Assert.That(doorComp.State, Is.EqualTo(DoorState.Open), "Airlock did not start open.");
await InteractUsing(Pry);
Assert.That(doorComp.State, Is.EqualTo(DoorState.Open), "Powered airlock was pried closed.");
}
[Test]
public async Task UnpoweredClosedAirlock_Pry_Opens()
{
await SpawnTarget(Airlock);
Assert.That(TryComp<AirlockComponent>(out var airlockComp), "Airlock does not have AirlockComponent?");
Assert.That(airlockComp.Powered, Is.False, "Airlock should not be powered for this test.");
Assert.That(TryComp<DoorComponent>(out var doorComp), "Airlock does not have DoorComponent?");
Assert.That(doorComp.State, Is.EqualTo(DoorState.Closed), "Airlock did not start closed.");
await InteractUsing(Pry);
Assert.That(doorComp.State, Is.EqualTo(DoorState.Opening), "Unpowered airlock failed to pry open.");
}
[Test]
public async Task UnpoweredOpenAirlock_Pry_Closes()
{
await SpawnTarget(Airlock);
Assert.That(TryComp<AirlockComponent>(out var airlockComp), "Airlock does not have AirlockComponent?");
Assert.That(airlockComp.Powered, Is.False, "Airlock should not be powered for this test.");
var doorSys = SEntMan.System<DoorSystem>();
await Server.WaitPost(() => doorSys.SetState(SEntMan.GetEntity(Target.Value), DoorState.Open));
Assert.That(TryComp<DoorComponent>(out var doorComp), "Airlock does not have DoorComponent?");
Assert.That(doorComp.State, Is.EqualTo(DoorState.Open), "Airlock did not start open.");
await InteractUsing(Pry);
Assert.That(doorComp.State, Is.EqualTo(DoorState.Closing), "Unpowered airlock failed to pry closed.");
}
}

View File

@@ -10,6 +10,9 @@ public abstract partial class InteractionTest
protected const string Plating = "Plating";
protected const string Lattice = "Lattice";
// Structures
protected const string Airlock = "Airlock";
// Tools/steps
protected const string Wrench = "Wrench";
protected const string Screw = "Screwdriver";

View File

@@ -0,0 +1,70 @@
using System.Collections.Generic;
using Content.Client.Weapons.Ranged.Components;
using Content.Shared.Prototypes;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests;
/// <summary>
/// Tests all entity prototypes with the MagazineVisualsComponent.
/// </summary>
[TestFixture]
public sealed class MagazineVisualsSpriteTest
{
[Test]
public async Task MagazineVisualsSpritesExist()
{
await using var pair = await PoolManager.GetServerClient();
var client = pair.Client;
var protoMan = client.ResolveDependency<IPrototypeManager>();
var componentFactory = client.ResolveDependency<IComponentFactory>();
await client.WaitAssertion(() =>
{
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
{
if (proto.Abstract || pair.IsTestPrototype(proto))
continue;
if (!proto.TryGetComponent<MagazineVisualsComponent>(out var visuals, componentFactory))
continue;
Assert.That(proto.TryGetComponent<SpriteComponent>(out var sprite, componentFactory),
@$"{proto.ID} has MagazineVisualsComponent but no SpriteComponent.");
Assert.That(proto.HasComponent<AppearanceComponent>(componentFactory),
@$"{proto.ID} has MagazineVisualsComponent but no AppearanceComponent.");
var toTest = new List<(int, string)>();
if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out var magLayerId))
toTest.Add((magLayerId, ""));
if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out var magUnshadedLayerId))
toTest.Add((magUnshadedLayerId, "-unshaded"));
Assert.That(toTest, Is.Not.Empty,
@$"{proto.ID} has MagazineVisualsComponent but no Mag or MagUnshaded layer map.");
var start = visuals.ZeroVisible ? 0 : 1;
foreach (var (id, midfix) in toTest)
{
Assert.That(sprite.TryGetLayer(id, out var layer));
var rsi = layer.ActualRsi;
for (var i = start; i < visuals.MagSteps; i++)
{
var state = $"{visuals.MagState}{midfix}-{i}";
Assert.That(rsi.TryGetState(state, out _),
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but {rsi.Path} doesn't have state {state}!");
}
// MagSteps includes the 0th step, so sometimes people are off by one.
var extraState = $"{visuals.MagState}{midfix}-{visuals.MagSteps}";
Assert.That(rsi.TryGetState(extraState, out _), Is.False,
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but more states exist!");
}
}
});
await pair.CleanReturnAsync();
}
}

View File

@@ -987,6 +987,8 @@ namespace Content.Server.Database
BabyJail = 4,
/// Results from rejected connections with external API checking tools
IPChecks = 5,
/// Results from rejected connections who are authenticated but have no modern hwid associated with them.
NoHwid = 6
}
public class ServerBanHit

View File

@@ -136,7 +136,7 @@ public sealed partial class AdminVerbSystem
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
var board = Spawn("ChessBoard", xform.Coordinates);
var session = _tabletopSystem.EnsureSession(Comp<TabletopGameComponent>(board));
xform.Coordinates = EntityCoordinates.FromMap(_mapManager, session.Position);
xform.Coordinates = _transformSystem.ToCoordinates(session.Position);
_transformSystem.SetWorldRotationNoLerp((args.Target, xform), Angle.Zero);
},
Impact = LogImpact.Extreme,

View File

@@ -640,7 +640,7 @@ namespace Content.Server.Administration.Systems
var personalChannel = senderSession.UserId == message.UserId;
var senderAdmin = _adminManager.GetAdminData(senderSession);
var senderAHelpAdmin = senderAdmin?.HasFlag(AdminFlags.Adminhelp) ?? false;
var authorized = personalChannel || senderAHelpAdmin;
var authorized = personalChannel && !message.AdminOnly || senderAHelpAdmin;
if (!authorized)
{
// Unauthorized bwoink (log?)
@@ -676,11 +676,11 @@ namespace Content.Server.Administration.Systems
bwoinkText = $"{senderSession.Name}";
}
bwoinkText = $"{(message.PlaySound ? "" : "(S) ")}{bwoinkText}: {escapedText}";
bwoinkText = $"{(message.AdminOnly ? Loc.GetString("bwoink-message-admin-only") : !message.PlaySound ? Loc.GetString("bwoink-message-silent") : "")} {bwoinkText}: {escapedText}";
// If it's not an admin / admin chooses to keep the sound then play it.
var playSound = !senderAHelpAdmin || message.PlaySound;
var msg = new BwoinkTextMessage(message.UserId, senderSession.UserId, bwoinkText, playSound: playSound);
// If it's not an admin / admin chooses to keep the sound and message is not an admin only message, then play it.
var playSound = (!senderAHelpAdmin || message.PlaySound) && !message.AdminOnly;
var msg = new BwoinkTextMessage(message.UserId, senderSession.UserId, bwoinkText, playSound: playSound, adminOnly: message.AdminOnly);
LogBwoink(msg);
@@ -700,7 +700,7 @@ namespace Content.Server.Administration.Systems
}
// Notify player
if (_playerManager.TryGetSessionById(message.UserId, out var session))
if (_playerManager.TryGetSessionById(message.UserId, out var session) && !message.AdminOnly)
{
if (!admins.Contains(session.Channel))
{

View File

@@ -19,11 +19,12 @@ public sealed partial class AntagSelectionSystem
/// Tries to get the next non-filled definition based on the current amount of selected minds and other factors.
/// </summary>
public bool TryGetNextAvailableDefinition(Entity<AntagSelectionComponent> ent,
[NotNullWhen(true)] out AntagSelectionDefinition? definition)
[NotNullWhen(true)] out AntagSelectionDefinition? definition,
int? players = null)
{
definition = null;
var totalTargetCount = GetTargetAntagCount(ent);
var totalTargetCount = GetTargetAntagCount(ent, players);
var mindCount = ent.Comp.SelectedMinds.Count;
if (mindCount >= totalTargetCount)
return false;

View File

@@ -144,7 +144,10 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
DebugTools.AssertEqual(antag.SelectionTime, AntagSelectionTime.PostPlayerSpawn);
if (!TryGetNextAvailableDefinition((uid, antag), out var def))
// do not count players in the lobby for the antag ratio
var players = _playerManager.NetworkedSessions.Count(x => x.AttachedEntity != null);
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
continue;
if (TryMakeAntag((uid, antag), args.Player, def.Value))

View File

@@ -76,14 +76,13 @@ public sealed class GasAnalyzerSystem : EntitySystem
/// </summary>
private void OnUseInHand(Entity<GasAnalyzerComponent> entity, ref UseInHandEvent args)
{
// Not checking for Handled because ActivatableUISystem already marks it as such.
if (!entity.Comp.Enabled)
{
ActivateAnalyzer(entity, args.User);
}
else
{
DisableAnalyzer(entity, args.User);
}
args.Handled = true;
}

View File

@@ -19,9 +19,9 @@ public sealed class ServerGlobalSoundSystem : SharedGlobalSoundSystem
_conHost.UnregisterCommand("playglobalsound");
}
public void PlayAdminGlobal(Filter playerFilter, string filename, AudioParams? audioParams = null, bool replay = true)
public void PlayAdminGlobal(Filter playerFilter, ResolvedSoundSpecifier specifier, AudioParams? audioParams = null, bool replay = true)
{
var msg = new AdminSoundEvent(filename, audioParams);
var msg = new AdminSoundEvent(specifier, audioParams);
RaiseNetworkEvent(msg, playerFilter, recordReplay: replay);
}
@@ -32,9 +32,9 @@ public sealed class ServerGlobalSoundSystem : SharedGlobalSoundSystem
return stationFilter;
}
public void PlayGlobalOnStation(EntityUid source, string filename, AudioParams? audioParams = null)
public void PlayGlobalOnStation(EntityUid source, ResolvedSoundSpecifier specifier, AudioParams? audioParams = null)
{
var msg = new GameGlobalSoundEvent(filename, audioParams);
var msg = new GameGlobalSoundEvent(specifier, audioParams);
var filter = GetStationAndPvs(source);
RaiseNetworkEvent(msg, filter);
}
@@ -52,13 +52,13 @@ public sealed class ServerGlobalSoundSystem : SharedGlobalSoundSystem
public void DispatchStationEventMusic(EntityUid source, SoundSpecifier sound, StationEventMusicType type)
{
DispatchStationEventMusic(source, _audio.GetSound(sound), type);
DispatchStationEventMusic(source, _audio.ResolveSound(sound), type);
}
public void DispatchStationEventMusic(EntityUid source, string sound, StationEventMusicType type)
public void DispatchStationEventMusic(EntityUid source, ResolvedSoundSpecifier specifier, StationEventMusicType type)
{
var audio = AudioParams.Default.WithVolume(-8);
var msg = new StationEventMusicEvent(sound, type, audio);
var msg = new StationEventMusicEvent(specifier, type, audio);
var filter = GetStationAndPvs(source);
RaiseNetworkEvent(msg, filter);

View File

@@ -369,7 +369,7 @@ namespace Content.Server.Cargo.Systems
private void PlayDenySound(EntityUid uid, CargoOrderConsoleComponent component)
{
_audio.PlayPvs(_audio.GetSound(component.ErrorSound), uid);
_audio.PlayPvs(_audio.ResolveSound(component.ErrorSound), uid);
}
private static CargoOrderData GetOrderData(CargoConsoleAddOrderMessage args, CargoProductPrototype cargoProduct, int id)

View File

@@ -92,7 +92,7 @@ public sealed partial class CargoSystem
var currentOrder = comp.CurrentOrders.First();
if (FulfillOrder(currentOrder, xform.Coordinates, comp.PrinterOutput))
{
_audio.PlayPvs(_audio.GetSound(comp.TeleportSound), uid, AudioParams.Default.WithVolume(-8f));
_audio.PlayPvs(_audio.ResolveSound(comp.TeleportSound), uid, AudioParams.Default.WithVolume(-8f));
if (_station.GetOwningStation(uid) is { } station)
UpdateOrders(station);

View File

@@ -196,17 +196,30 @@ public partial class ChatSystem
/// <returns></returns>
private bool AllowedToUseEmote(EntityUid source, EmotePrototype emote)
{
if ((_whitelistSystem.IsWhitelistFail(emote.Whitelist, source) || _whitelistSystem.IsBlacklistPass(emote.Blacklist, source)))
return false;
// If emote is in AllowedEmotes, it will bypass whitelist and blacklist
if (TryComp<SpeechComponent>(source, out var speech) &&
speech.AllowedEmotes.Contains(emote.ID))
{
return true;
}
if (!emote.Available &&
TryComp<SpeechComponent>(source, out var speech) &&
!speech.AllowedEmotes.Contains(emote.ID))
// Check the whitelist and blacklist
if (_whitelistSystem.IsWhitelistFail(emote.Whitelist, source) ||
_whitelistSystem.IsBlacklistPass(emote.Blacklist, source))
{
return false;
}
// Check if the emote is available for all
if (!emote.Available)
{
return false;
}
return true;
}
private void InvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
{
var ev = new EmoteEvent(proto);

View File

@@ -328,7 +328,7 @@ public sealed partial class ChatSystem : SharedChatSystem
_chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
if (playSound)
{
_audio.PlayGlobal(announcementSound == null ? DefaultAnnouncementSound : _audio.GetSound(announcementSound), Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
_audio.PlayGlobal(announcementSound == null ? DefaultAnnouncementSound : _audio.ResolveSound(announcementSound), Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
}
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Global station announcement from {sender}: {message}");
}

View File

@@ -64,4 +64,9 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
var ev = new ReagentGuideRegistryChangedEvent(changeset);
RaiseNetworkEvent(ev);
}
public override void ReloadAllReagentPrototypes()
{
InitializeServerRegistry();
}
}

View File

@@ -17,14 +17,14 @@ namespace Content.Server.Chemistry.TileReactions
[DataDefinition]
public sealed partial class SpillTileReaction : ITileReaction
{
[DataField("launchForwardsMultiplier")] private float _launchForwardsMultiplier = 1;
[DataField("requiredSlipSpeed")] private float _requiredSlipSpeed = 6;
[DataField("paralyzeTime")] private float _paralyzeTime = 1;
[DataField("launchForwardsMultiplier")] public float LaunchForwardsMultiplier = 1;
[DataField("requiredSlipSpeed")] public float RequiredSlipSpeed = 6;
[DataField("paralyzeTime")] public float ParalyzeTime = 1;
/// <summary>
/// <see cref="SlipperyComponent.SuperSlippery"/>
/// </summary>
[DataField("superSlippery")] private bool _superSlippery;
[DataField("superSlippery")] public bool SuperSlippery;
public FixedPoint2 TileReact(TileRef tile,
ReagentPrototype reagent,
@@ -39,13 +39,13 @@ namespace Content.Server.Chemistry.TileReactions
.TrySpillAt(tile, new Solution(reagent.ID, reactVolume, data), out var puddleUid, false, false))
{
var slippery = entityManager.EnsureComponent<SlipperyComponent>(puddleUid);
slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier;
slippery.ParalyzeTime = _paralyzeTime;
slippery.SuperSlippery = _superSlippery;
slippery.LaunchForwardsMultiplier = LaunchForwardsMultiplier;
slippery.ParalyzeTime = ParalyzeTime;
slippery.SuperSlippery = SuperSlippery;
entityManager.Dirty(puddleUid, slippery);
var step = entityManager.EnsureComponent<StepTriggerComponent>(puddleUid);
entityManager.EntitySysManager.GetEntitySystem<StepTriggerSystem>().SetRequiredTriggerSpeed(puddleUid, _requiredSlipSpeed, step);
entityManager.EntitySysManager.GetEntitySystem<StepTriggerSystem>().SetRequiredTriggerSpeed(puddleUid, RequiredSlipSpeed, step);
var slow = entityManager.EnsureComponent<SpeedModifierContactsComponent>(puddleUid);
var speedModifier = 1 - reagent.Viscosity;

View File

@@ -222,6 +222,11 @@ namespace Content.Server.Connection
var modernHwid = e.UserData.ModernHWIds;
if (modernHwid.Length == 0 && e.AuthType == LoginType.LoggedIn && _cfg.GetCVar(CCVars.RequireModernHardwareId))
{
return (ConnectionDenyReason.NoHwid, Loc.GetString("hwid-required"), null);
}
var bans = await _db.GetServerBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false);
if (bans.Count > 0)
{

View File

@@ -43,7 +43,7 @@ namespace Content.Server.Damage.Systems
if (TryComp<CP14SharpenedComponent>(uid, out var sharp))
damage *= sharp.Sharpness;
var dmg = _damageable.TryChangeDamage(args.Target, damage, component.IgnoreResistances, origin: args.Component.Thrower);
var dmg = _damageable.TryChangeDamage(args.Target, damage * _damageable.UniversalThrownDamageModifier, component.IgnoreResistances, origin: args.Component.Thrower);
//CrystallEdge Melee upgrade end
// Log damage only for mobs. Useful for when people throw spears at each other, but also avoids log-spam when explosions send glass shards flying.
@@ -66,12 +66,14 @@ namespace Content.Server.Damage.Systems
private void OnDamageExamine(EntityUid uid, DamageOtherOnHitComponent component, ref DamageExamineEvent args)
{
var damage = component.Damage;
//CP14 Sharpening damage apply
var damage = component.Damage * _damageable.UniversalThrownDamageModifier;
if (TryComp<CP14SharpenedComponent>(uid, out var sharp))
damage *= sharp.Sharpness;
_damageExamine.AddDamageExamine(args.Message, damage, Loc.GetString("damage-throw"));
//CP14 Sharpening damage apply end
}
/// <summary>

View File

@@ -1,9 +0,0 @@
namespace Content.Server.DetailExaminable
{
[RegisterComponent]
public sealed partial class DetailExaminableComponent : Component
{
[DataField("content", required: true)] [ViewVariables(VVAccess.ReadWrite)]
public string Content = "";
}
}

View File

@@ -1,44 +0,0 @@
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Server.DetailExaminable
{
public sealed class DetailExaminableSystem : EntitySystem
{
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DetailExaminableComponent, GetVerbsEvent<ExamineVerb>>(OnGetExamineVerbs);
}
private void OnGetExamineVerbs(EntityUid uid, DetailExaminableComponent component, GetVerbsEvent<ExamineVerb> args)
{
if (Identity.Name(args.Target, EntityManager) != MetaData(args.Target).EntityName)
return;
var detailsRange = _examineSystem.IsInDetailsRange(args.User, uid);
var verb = new ExamineVerb()
{
Act = () =>
{
var markup = new FormattedMessage();
markup.AddMarkupOrThrow(component.Content);
_examineSystem.SendExamineTooltip(args.User, uid, markup, false, false);
},
Text = Loc.GetString("detail-examinable-verb-text"),
Category = VerbCategory.Examine,
Disabled = !detailsRange,
Message = detailsRange ? null : Loc.GetString("detail-examinable-verb-disabled"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/examine.svg.192dpi.png"))
};
args.Verbs.Add(verb);
}
}
}

View File

@@ -44,6 +44,26 @@ namespace Content.Server.EntityEffects.Effects
var damageSpec = new DamageSpecifier(Damage);
var universalReagentDamageModifier = entSys.GetEntitySystem<DamageableSystem>().UniversalReagentDamageModifier;
var universalReagentHealModifier = entSys.GetEntitySystem<DamageableSystem>().UniversalReagentHealModifier;
if (universalReagentDamageModifier != 1 || universalReagentHealModifier != 1)
{
foreach (var (type, val) in damageSpec.DamageDict)
{
if (val < 0f)
{
damageSpec.DamageDict[type] = val * universalReagentHealModifier;
}
if (val > 0f)
{
damageSpec.DamageDict[type] = val * universalReagentDamageModifier;
}
}
}
damageSpec = entSys.GetEntitySystem<DamageableSystem>().ApplyUniversalAllModifiers(damageSpec);
foreach (var group in prototype.EnumeratePrototypes<DamageGroupPrototype>())
{
if (!damageSpec.TryGetDamageInGroup(group, out var amount))
@@ -114,17 +134,37 @@ namespace Content.Server.EntityEffects.Effects
public override void Effect(EntityEffectBaseArgs args)
{
var scale = FixedPoint2.New(1);
var damageSpec = new DamageSpecifier(Damage);
if (args is EntityEffectReagentArgs reagentArgs)
{
scale = ScaleByQuantity ? reagentArgs.Quantity * reagentArgs.Scale : reagentArgs.Scale;
}
args.EntityManager.System<DamageableSystem>().TryChangeDamage(
args.TargetEntity,
Damage * scale,
IgnoreResistances,
interruptsDoAfters: false);
var universalReagentDamageModifier = args.EntityManager.System<DamageableSystem>().UniversalReagentDamageModifier;
var universalReagentHealModifier = args.EntityManager.System<DamageableSystem>().UniversalReagentHealModifier;
if (universalReagentDamageModifier != 1 || universalReagentHealModifier != 1)
{
foreach (var (type, val) in damageSpec.DamageDict)
{
if (val < 0f)
{
damageSpec.DamageDict[type] = val * universalReagentHealModifier;
}
if (val > 0f)
{
damageSpec.DamageDict[type] = val * universalReagentDamageModifier;
}
}
}
args.EntityManager.System<DamageableSystem>()
.TryChangeDamage(
args.TargetEntity,
damageSpec * scale,
IgnoreResistances,
interruptsDoAfters: false);
}
}
}

View File

@@ -464,7 +464,7 @@ public sealed partial class ExplosionSystem
}
// TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin.
_damageableSystem.TryChangeDamage(entity, damage, ignoreResistances: true);
_damageableSystem.TryChangeDamage(entity, damage * _damageableSystem.UniversalExplosionDamageModifier, ignoreResistances: true);
}
}

View File

@@ -16,6 +16,7 @@ using Content.Shared.GameTicking;
using Content.Shared.Inventory;
using Content.Shared.Projectiles;
using Content.Shared.Throwing;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Server.Player;
using Robust.Shared.Audio.Systems;
@@ -38,6 +39,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly NodeGroupSystem _nodeGroupSystem = default!;
@@ -345,7 +347,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
CameraShake(iterationIntensity.Count * 4f, pos, queued.TotalIntensity);
//For whatever bloody reason, sound system requires ENTITY coordinates.
var mapEntityCoords = EntityCoordinates.FromMap(_mapManager.GetMapEntityId(pos.MapId), pos, _transformSystem, EntityManager);
var mapEntityCoords = _transformSystem.ToCoordinates(_mapSystem.GetMap(pos.MapId), pos);
// play sound.
// for the normal audio, we want everyone in pvs range

View File

@@ -141,7 +141,7 @@ public sealed class DrainSystem : SharedDrainSystem
if (!_solutionContainerSystem.ResolveSolution((uid, manager), DrainComponent.SolutionName, ref drain.Solution, out var drainSolution))
continue;
if (drainSolution.AvailableVolume <= 0)
if (drainSolution.Volume <= 0 && !drain.AutoDrain)
{
_ambientSoundSystem.SetAmbience(uid, false);
continue;
@@ -158,7 +158,7 @@ public sealed class DrainSystem : SharedDrainSystem
_puddles.Clear();
_lookup.GetEntitiesInRange(Transform(uid).Coordinates, drain.Range, _puddles);
if (_puddles.Count == 0)
if (_puddles.Count == 0 && drainSolution.Volume <= 0)
{
_ambientSoundSystem.SetAmbience(uid, false);
continue;

View File

@@ -1,4 +1,6 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Chemistry.TileReactions;
using Content.Server.DoAfter;
using Content.Server.Fluids.Components;
using Content.Server.Spreader;
@@ -387,23 +389,36 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
private void UpdateSlip(EntityUid entityUid, PuddleComponent component, Solution solution)
{
var isSlippery = false;
var isSuperSlippery = false;
// The base sprite is currently at 0.3 so we require at least 2nd tier to be slippery or else it's too hard to see.
var amountRequired = FixedPoint2.New(component.OverflowVolume.Float() * LowThreshold);
var slipperyAmount = FixedPoint2.Zero;
// Utilize the defaults from their relevant systems... this sucks, and is a bandaid
var launchForwardsMultiplier = SlipperyComponent.DefaultLaunchForwardsMultiplier;
var paralyzeTime = SlipperyComponent.DefaultParalyzeTime;
var requiredSlipSpeed = StepTriggerComponent.DefaultRequiredTriggeredSpeed;
foreach (var (reagent, quantity) in solution.Contents)
{
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
if (reagentProto.Slippery)
{
slipperyAmount += quantity;
if (!reagentProto.Slippery)
continue;
slipperyAmount += quantity;
if (slipperyAmount > amountRequired)
{
isSlippery = true;
break;
}
if (slipperyAmount <= amountRequired)
continue;
isSlippery = true;
foreach (var tileReaction in reagentProto.TileReactions)
{
if (tileReaction is not SpillTileReaction spillTileReaction)
continue;
isSuperSlippery = spillTileReaction.SuperSlippery;
launchForwardsMultiplier = launchForwardsMultiplier < spillTileReaction.LaunchForwardsMultiplier ? spillTileReaction.LaunchForwardsMultiplier : launchForwardsMultiplier;
requiredSlipSpeed = requiredSlipSpeed > spillTileReaction.RequiredSlipSpeed ? spillTileReaction.RequiredSlipSpeed : requiredSlipSpeed;
paralyzeTime = paralyzeTime < spillTileReaction.ParalyzeTime ? spillTileReaction.ParalyzeTime : paralyzeTime;
}
}
@@ -413,6 +428,14 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
_stepTrigger.SetActive(entityUid, true, comp);
var friction = EnsureComp<TileFrictionModifierComponent>(entityUid);
_tile.SetModifier(entityUid, TileFrictionController.DefaultFriction * 0.5f, friction);
if (!TryComp<SlipperyComponent>(entityUid, out var slipperyComponent))
return;
slipperyComponent.SuperSlippery = isSuperSlippery;
_stepTrigger.SetRequiredTriggerSpeed(entityUid, requiredSlipSpeed);
slipperyComponent.LaunchForwardsMultiplier = launchForwardsMultiplier;
slipperyComponent.ParalyzeTime = paralyzeTime;
}
else if (TryComp<StepTriggerComponent>(entityUid, out var comp))
{

View File

@@ -585,7 +585,7 @@ namespace Content.Server.GameTicking
// This ordering mechanism isn't great (no ordering of minds) but functions
var listOfPlayerInfoFinal = listOfPlayerInfo.OrderBy(pi => pi.PlayerOOCName).ToArray();
var sound = RoundEndSoundCollection == null ? null : _audio.GetSound(new SoundCollectionSpecifier(RoundEndSoundCollection));
var sound = RoundEndSoundCollection == null ? null : _audio.ResolveSound(new SoundCollectionSpecifier(RoundEndSoundCollection));
var roundEndMessageEvent = new RoundEndMessageEvent(
gamemodeTitle,

View File

@@ -361,6 +361,7 @@ namespace Content.Server.GameTicking
if (DummyTicker)
return;
var makeObserver = false;
Entity<MindComponent?>? mind = player.GetMind();
if (mind == null)
{
@@ -368,10 +369,13 @@ namespace Content.Server.GameTicking
var (mindId, mindComp) = _mind.CreateMind(player.UserId, name);
mind = (mindId, mindComp);
_mind.SetUserId(mind.Value, player.UserId);
_roles.MindAddRole(mind.Value, "MindRoleObserver");
makeObserver = true;
}
var ghost = _ghost.SpawnGhost(mind.Value);
if (makeObserver)
_roles.MindAddRole(mind.Value, "MindRoleObserver");
_adminLogger.Add(LogType.LateJoin,
LogImpact.Low,
$"{player.Name} late joined the round as an Observer with {ToPrettyString(ghost):entity}.");

View File

@@ -0,0 +1,8 @@
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Component for the SurvivorRuleSystem. Game rule that turns everyone into a survivor and gives them the objective to escape centcom alive.
/// Started by Wizard Summon Guns/Magic spells.
/// </summary>
[RegisterComponent, Access(typeof(SurvivorRuleSystem))]
public sealed partial class SurvivorRuleComponent : Component;

View File

@@ -0,0 +1,108 @@
using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Roles;
using Content.Server.Shuttles.Systems;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Survivor.Components;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
namespace Content.Server.GameTicking.Rules;
public sealed class SurvivorRuleSystem : GameRuleSystem<SurvivorRuleComponent>
{
[Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly TransformSystem _xform = default!;
[Dependency] private readonly EmergencyShuttleSystem _eShuttle = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SurvivorRoleComponent, GetBriefingEvent>(OnGetBriefing);
}
// TODO: Planned rework post wizard release when RandomGlobalSpawnSpell becomes a gamerule
protected override void Started(EntityUid uid, SurvivorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
var allAliveHumanMinds = _mind.GetAliveHumans();
foreach (var humanMind in allAliveHumanMinds)
{
if (!humanMind.Comp.OwnedEntity.HasValue)
continue;
var mind = humanMind.Owner;
var ent = humanMind.Comp.OwnedEntity.Value;
if (HasComp<SurvivorComponent>(mind) || _tag.HasTag(mind, "InvalidForSurvivorAntag"))
continue;
EnsureComp<SurvivorComponent>(mind);
_role.MindAddRole(mind, "MindRoleSurvivor");
_antag.SendBriefing(ent, Loc.GetString("survivor-role-greeting"), Color.Olive, null);
}
}
private void OnGetBriefing(Entity<SurvivorRoleComponent> ent, ref GetBriefingEvent args)
{
args.Append(Loc.GetString("survivor-role-greeting"));
}
protected override void AppendRoundEndText(EntityUid uid,
SurvivorRuleComponent component,
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
base.AppendRoundEndText(uid, component, gameRule, ref args);
// Using this instead of alive antagonists to make checking for shuttle & if the ent is alive easier
var existingSurvivors = AllEntityQuery<SurvivorComponent, MindComponent>();
var deadSurvivors = 0;
var aliveMarooned = 0;
var aliveOnShuttle = 0;
var eShuttle = _eShuttle.GetShuttle();
while (existingSurvivors.MoveNext(out _, out _, out var mindComp))
{
// If their brain is gone or they respawned/became a ghost role
if (mindComp.CurrentEntity is null)
{
deadSurvivors++;
continue;
}
var survivor = mindComp.CurrentEntity.Value;
if (!_mobState.IsAlive(survivor))
{
deadSurvivors++;
continue;
}
if (eShuttle != null && eShuttle.Value.IsValid() && (Transform(eShuttle.Value).MapID == _xform.GetMapCoordinates(survivor).MapId))
{
aliveOnShuttle++;
continue;
}
aliveMarooned++;
}
args.AddLine(Loc.GetString("survivor-round-end-dead-count", ("deadCount", deadSurvivors)));
args.AddLine(Loc.GetString("survivor-round-end-alive-count", ("aliveCount", aliveMarooned)));
args.AddLine(Loc.GetString("survivor-round-end-alive-on-shuttle-count", ("aliveCount", aliveOnShuttle)));
// Player manifest at EOR shows who's a survivor so no need for extra info here.
}
}

View File

@@ -26,6 +26,7 @@ using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
using Content.Shared.Storage.Components;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
@@ -64,6 +65,7 @@ namespace Content.Server.Ghost
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly TagSystem _tag = default!;
private EntityQuery<GhostComponent> _ghostQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
@@ -403,8 +405,11 @@ namespace Content.Server.Ghost
public void MakeVisible(bool visible)
{
var entityQuery = EntityQueryEnumerator<GhostComponent, VisibilityComponent>();
while (entityQuery.MoveNext(out var uid, out _, out var vis))
while (entityQuery.MoveNext(out var uid, out var _, out var vis))
{
if (!_tag.HasTag(uid, "AllowGhostShownByEvent"))
continue;
if (visible)
{
_visibilitySystem.AddLayer((uid, vis), (int) VisibilityFlags.Normal, false);

View File

@@ -6,7 +6,6 @@ using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Whitelist;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
@@ -26,6 +25,7 @@ public sealed class RandomGiftSystem : EntitySystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private readonly List<string> _possibleGiftsSafe = new();
private readonly List<string> _possibleGiftsUnsafe = new();
@@ -63,11 +63,16 @@ public sealed class RandomGiftSystem : EntitySystem
if (component.Wrapper is not null)
Spawn(component.Wrapper, coords);
args.Handled = true;
_audio.PlayPvs(component.Sound, args.User);
Del(uid);
// Don't delete the entity in the event bus, so we queue it for deletion.
// We need the free hand for the new item, so we send it to nullspace.
_transform.DetachEntity(uid, Transform(uid));
QueueDel(uid);
_hands.PickupOrDrop(args.User, handsEnt);
args.Handled = true;
}
private void OnGiftMapInit(EntityUid uid, RandomGiftComponent component, MapInitEvent args)

View File

@@ -115,13 +115,13 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (source != null)
{
// Close any AI request windows
if (_stationAiSystem.TryGetStationAiCore(args.Actor, out var stationAiCore) && stationAiCore != null)
if (_stationAiSystem.TryGetCore(args.Actor, out var stationAiCore))
_userInterfaceSystem.CloseUi(receiver.Owner, HolopadUiKey.AiRequestWindow, args.Actor);
// Try to warn the AI if the source of the call is out of its range
if (TryComp<TelephoneComponent>(stationAiCore, out var stationAiTelephone) &&
TryComp<TelephoneComponent>(source, out var sourceTelephone) &&
!_telephoneSystem.IsSourceInRangeOfReceiver((stationAiCore.Value.Owner, stationAiTelephone), (source.Value.Owner, sourceTelephone)))
!_telephoneSystem.IsSourceInRangeOfReceiver((stationAiCore.Owner, stationAiTelephone), (source.Value.Owner, sourceTelephone)))
{
_popupSystem.PopupEntity(Loc.GetString("holopad-ai-is-unable-to-reach-holopad"), receiver, args.Actor);
return;
@@ -150,11 +150,11 @@ public sealed class HolopadSystem : SharedHolopadSystem
// If the user is an AI, end all calls originating from its
// associated core to ensure that any broadcasts will end
if (!TryComp<StationAiHeldComponent>(args.Actor, out var stationAiHeld) ||
!_stationAiSystem.TryGetStationAiCore((args.Actor, stationAiHeld), out var stationAiCore))
!_stationAiSystem.TryGetCore(args.Actor, out var stationAiCore))
return;
if (TryComp<TelephoneComponent>(stationAiCore, out var telephone))
_telephoneSystem.EndTelephoneCalls((stationAiCore.Value, telephone));
_telephoneSystem.EndTelephoneCalls((stationAiCore, telephone));
}
private void OnHolopadActivateProjector(Entity<HolopadComponent> entity, ref HolopadActivateProjectorMessage args)
@@ -176,17 +176,17 @@ public sealed class HolopadSystem : SharedHolopadSystem
// Link the AI to the holopad they are broadcasting from
LinkHolopadToUser(source, args.Actor);
if (!_stationAiSystem.TryGetStationAiCore((args.Actor, stationAiHeld), out var stationAiCore) ||
stationAiCore.Value.Comp.RemoteEntity == null ||
if (!_stationAiSystem.TryGetCore(args.Actor, out var stationAiCore) ||
stationAiCore.Comp?.RemoteEntity == null ||
!TryComp<HolopadComponent>(stationAiCore, out var stationAiCoreHolopad))
return;
// Execute the broadcast, but have it originate from the AI core
ExecuteBroadcast((stationAiCore.Value, stationAiCoreHolopad), args.Actor);
ExecuteBroadcast((stationAiCore, stationAiCoreHolopad), args.Actor);
// Switch the AI's perspective from free roaming to the target holopad
_xformSystem.SetCoordinates(stationAiCore.Value.Comp.RemoteEntity.Value, Transform(source).Coordinates);
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore.Value, false);
_xformSystem.SetCoordinates(stationAiCore.Comp.RemoteEntity.Value, Transform(source).Coordinates);
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore, false);
return;
}
@@ -220,10 +220,10 @@ public sealed class HolopadSystem : SharedHolopadSystem
reachableAiCores.Add((receiverUid, receiverTelephone));
if (!_stationAiSystem.TryGetInsertedAI((receiver, receiverStationAiCore), out var insertedAi))
if (!_stationAiSystem.TryGetHeld((receiver, receiverStationAiCore), out var insertedAi))
continue;
if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi.Value.Owner))
if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi))
LinkHolopadToUser(entity, args.Actor);
}
@@ -274,8 +274,8 @@ public sealed class HolopadSystem : SharedHolopadSystem
return;
// Auto-close the AI request window
if (_stationAiSystem.TryGetInsertedAI((entity, stationAiCore), out var insertedAi))
_userInterfaceSystem.CloseUi(entity.Owner, HolopadUiKey.AiRequestWindow, insertedAi.Value.Owner);
if (_stationAiSystem.TryGetHeld((entity, stationAiCore), out var insertedAi))
_userInterfaceSystem.CloseUi(entity.Owner, HolopadUiKey.AiRequestWindow, insertedAi);
}
private void OnTelephoneMessageSent(Entity<HolopadComponent> holopad, ref TelephoneMessageSentEvent args)
@@ -381,13 +381,13 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (!TryComp<StationAiHeldComponent>(entity, out var entityStationAiHeld))
return;
if (!_stationAiSystem.TryGetStationAiCore((entity, entityStationAiHeld), out var stationAiCore))
if (!_stationAiSystem.TryGetCore(entity, out var stationAiCore))
return;
if (!TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone))
return;
_telephoneSystem.EndTelephoneCalls((stationAiCore.Value, stationAiCoreTelephone));
_telephoneSystem.EndTelephoneCalls((stationAiCore, stationAiCoreTelephone));
}
private void AddToggleProjectorVerb(Entity<HolopadComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
@@ -407,8 +407,8 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (!TryComp<StationAiHeldComponent>(user, out var userAiHeld))
return;
if (!_stationAiSystem.TryGetStationAiCore((user, userAiHeld), out var stationAiCore) ||
stationAiCore.Value.Comp.RemoteEntity == null)
if (!_stationAiSystem.TryGetCore(user, out var stationAiCore) ||
stationAiCore.Comp?.RemoteEntity == null)
return;
AlternativeVerb verb = new()
@@ -595,17 +595,17 @@ public sealed class HolopadSystem : SharedHolopadSystem
{
// Check if the associated holopad user is an AI
if (TryComp<StationAiHeldComponent>(entity.Comp.User, out var stationAiHeld) &&
_stationAiSystem.TryGetStationAiCore((entity.Comp.User.Value, stationAiHeld), out var stationAiCore))
_stationAiSystem.TryGetCore(entity.Comp.User.Value, out var stationAiCore))
{
// Return the AI eye to free roaming
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore.Value, true);
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true);
// If the AI core is still broadcasting, end its calls
if (entity.Owner != stationAiCore.Value.Owner &&
if (entity.Owner != stationAiCore.Owner &&
TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone) &&
_telephoneSystem.IsTelephoneEngaged((stationAiCore.Value.Owner, stationAiCoreTelephone)))
_telephoneSystem.IsTelephoneEngaged((stationAiCore.Owner, stationAiCoreTelephone)))
{
_telephoneSystem.EndTelephoneCalls((stationAiCore.Value.Owner, stationAiCoreTelephone));
_telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone));
}
}
@@ -625,8 +625,8 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (!TryComp<StationAiHeldComponent>(user, out var userAiHeld))
return;
if (!_stationAiSystem.TryGetStationAiCore((user, userAiHeld), out var stationAiCore) ||
stationAiCore.Value.Comp.RemoteEntity == null)
if (!_stationAiSystem.TryGetCore(user, out var stationAiCore) ||
stationAiCore.Comp?.RemoteEntity == null)
return;
if (!TryComp<TelephoneComponent>(stationAiCore, out var stationAiTelephone))
@@ -635,7 +635,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (!TryComp<HolopadComponent>(stationAiCore, out var stationAiHolopad))
return;
var source = new Entity<TelephoneComponent>(stationAiCore.Value, stationAiTelephone);
var source = new Entity<TelephoneComponent>(stationAiCore, stationAiTelephone);
// Check if the AI is unable to activate the projector (unlikely this will ever pass; its just a safeguard)
if (!_telephoneSystem.IsSourceInRangeOfReceiver(source, receiver))
@@ -658,11 +658,11 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (!_telephoneSystem.IsSourceConnectedToReceiver(source, receiver))
return;
LinkHolopadToUser((stationAiCore.Value, stationAiHolopad), user);
LinkHolopadToUser((stationAiCore, stationAiHolopad), user);
// Switch the AI's perspective from free roaming to the target holopad
_xformSystem.SetCoordinates(stationAiCore.Value.Comp.RemoteEntity.Value, Transform(entity).Coordinates);
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore.Value, false);
_xformSystem.SetCoordinates(stationAiCore.Comp.RemoteEntity.Value, Transform(entity).Coordinates);
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore, false);
// Open the holopad UI if it hasn't been opened yet
if (TryComp<UserInterfaceComponent>(entity, out var entityUserInterfaceComponent))

View File

@@ -23,7 +23,7 @@ using System.Numerics;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Systems;
using Content.Server.IdentityManagement;
using Content.Server.DetailExaminable;
using Content.Shared.DetailExaminable;
using Content.Shared.Store.Components;
using Robust.Shared.Collections;
using Robust.Shared.Map.Components;

View File

@@ -202,7 +202,7 @@ namespace Content.Server.Light.EntitySystems
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery) &&
!TryComp(uid, out battery))
{
_audio.PlayPvs(_audio.GetSound(component.TurnOnFailSound), uid);
_audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid);
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-missing-message"), uid, user);
return false;
}
@@ -212,7 +212,7 @@ namespace Content.Server.Light.EntitySystems
// Simple enough.
if (component.Wattage > battery.CurrentCharge)
{
_audio.PlayPvs(_audio.GetSound(component.TurnOnFailSound), uid);
_audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid);
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-dead-message"), uid, user);
return false;
}

View File

@@ -1,12 +1,20 @@
using Content.Server.Chat.Systems;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.Magic;
using Content.Shared.Magic.Events;
using Content.Shared.Mind;
using Content.Shared.Tag;
using Robust.Shared.Prototypes;
namespace Content.Server.Magic;
public sealed class MagicSystem : SharedMagicSystem
{
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
@@ -32,4 +40,20 @@ public sealed class MagicSystem : SharedMagicSystem
Spawn(ev.Effect, perfXForm.Coordinates);
Spawn(ev.Effect, targetXForm.Coordinates);
}
protected override void OnRandomGlobalSpawnSpell(RandomGlobalSpawnSpellEvent ev)
{
base.OnRandomGlobalSpawnSpell(ev);
if (!ev.MakeSurvivorAntagonist)
return;
if (_mind.TryGetMind(ev.Performer, out var mind, out _) && !_tag.HasTag(mind, "InvalidForSurvivorAntag"))
_tag.AddTag(mind, "InvalidForSurvivorAntag");
EntProtoId survivorRule = "Survivor";
if (!_gameTicker.IsGameRuleActive<SurvivorRuleComponent>())
_gameTicker.StartGameRule(survivorRule);
}
}

View File

@@ -9,6 +9,7 @@ using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.Server.Mapping
@@ -35,6 +36,8 @@ namespace Content.Server.Mapping
var opts = CompletionHelper.UserFilePath(args[1], res.UserData)
.Concat(CompletionHelper.ContentFilePath(args[1], res));
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-mapping-path"));
case 3:
return CompletionResult.FromHintOptions(["false", "true"], Loc.GetString("cmd-mapping-hint-grid"));
}
return CompletionResult.Empty;
}
@@ -47,7 +50,7 @@ namespace Content.Server.Mapping
return;
}
if (args.Length > 2)
if (args.Length > 3)
{
shell.WriteLine(Help);
return;
@@ -57,12 +60,20 @@ namespace Content.Server.Mapping
shell.WriteLine(Loc.GetString("cmd-mapping-warning"));
#endif
// For backwards compatibility, isGrid is optional and we allow mappers to try load grids without explicitly
// specifying that they are loading a grid. Currently content is not allowed to override a map's MapId, so
// without engine changes this needs to be done by brute force by just trying to load it as a map first.
// This can result in errors being logged if the file is actually a grid, but the command should still work.
// yipeeee
bool? isGrid = args.Length < 3 ? null : bool.Parse(args[2]);
MapId mapId;
string? toLoad = null;
var mapSys = _entities.System<SharedMapSystem>();
Entity<MapGridComponent>? grid = null;
// Get the map ID to use
if (args.Length is 1 or 2)
if (args.Length > 0)
{
if (!int.TryParse(args[0], out var intMapId))
{
@@ -79,7 +90,7 @@ namespace Content.Server.Mapping
return;
}
if (_map.MapExists(mapId))
if (mapSys.MapExists(mapId))
{
shell.WriteError(Loc.GetString("cmd-mapping-exists", ("mapId", mapId)));
return;
@@ -93,12 +104,43 @@ namespace Content.Server.Mapping
else
{
var path = new ResPath(args[1]);
toLoad = path.FilenameWithoutExtension;
var opts = new DeserializationOptions {StoreYamlUids = true};
_entities.System<MapLoaderSystem>().TryLoadMapWithId(mapId, path, out _, out _, opts);
var loader = _entities.System<MapLoaderSystem>();
if (isGrid == true)
{
mapSys.CreateMap(mapId, runMapInit: false);
if (!loader.TryLoadGrid(mapId, path, out grid, opts))
{
shell.WriteError(Loc.GetString("cmd-mapping-error"));
mapSys.DeleteMap(mapId);
return;
}
}
else if (!loader.TryLoadMapWithId(mapId, path, out _, out _, opts))
{
if (isGrid == false)
{
shell.WriteError(Loc.GetString("cmd-mapping-error"));
return;
}
// isGrid was not specified and loading it as a map failed, so we fall back to trying to load
// the file as a grid
shell.WriteLine(Loc.GetString("cmd-mapping-try-grid"));
mapSys.CreateMap(mapId, runMapInit: false);
if (!loader.TryLoadGrid(mapId, path, out grid, opts))
{
shell.WriteError(Loc.GetString("cmd-mapping-error"));
mapSys.DeleteMap(mapId);
return;
}
}
}
// was the map actually created or did it fail somehow?
if (!_map.MapExists(mapId))
if (!mapSys.MapExists(mapId))
{
shell.WriteError(Loc.GetString("cmd-mapping-error"));
return;
@@ -117,19 +159,25 @@ namespace Content.Server.Mapping
}
// don't interrupt mapping with events or auto-shuttle
shell.ExecuteCommand("sudo cvar events.enabled false");
shell.ExecuteCommand("sudo cvar shuttle.auto_call_time 0");
shell.ExecuteCommand("changecvar events.enabled false");
shell.ExecuteCommand("changecvar shuttle.auto_call_time 0");
var auto = _entities.System<MappingSystem>();
if (grid != null)
auto.ToggleAutosave(grid.Value.Owner, toLoad ?? "NEWGRID");
else
auto.ToggleAutosave(mapId, toLoad ?? "NEWMAP");
if (_cfg.GetCVar(CCVars.AutosaveEnabled))
shell.ExecuteCommand($"toggleautosave {mapId} {toLoad ?? "NEWMAP"}");
shell.ExecuteCommand($"tp 0 0 {mapId}");
shell.RemoteExecuteCommand("mappingclientsidesetup");
_map.SetMapPaused(mapId, true);
DebugTools.Assert(mapSys.IsPaused(mapId));
if (args.Length == 2)
shell.WriteLine(Loc.GetString("cmd-mapping-success-load",("mapId",mapId),("path", args[1])));
else
if (args.Length != 2)
shell.WriteLine(Loc.GetString("cmd-mapping-success", ("mapId", mapId)));
else if (grid == null)
shell.WriteLine(Loc.GetString("cmd-mapping-success-load", ("mapId", mapId), ("path", args[1])));
else
shell.WriteLine(Loc.GetString("cmd-mapping-success-load-grid", ("mapId", mapId), ("path", args[1])));
}
}
}

View File

@@ -2,12 +2,12 @@ using System.IO;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -21,7 +21,7 @@ public sealed class MappingSystem : EntitySystem
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly IResourceManager _resMan = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
@@ -30,7 +30,7 @@ public sealed class MappingSystem : EntitySystem
/// map id -> next autosave timespan & original filename.
/// </summary>
/// <returns></returns>
private Dictionary<MapId, (TimeSpan next, string fileName)> _currentlyAutosaving = new();
private Dictionary<EntityUid, (TimeSpan next, string fileName)> _currentlyAutosaving = new();
private bool _autosaveEnabled;
@@ -60,25 +60,29 @@ public sealed class MappingSystem : EntitySystem
if (!_autosaveEnabled)
return;
foreach (var (map, (time, name))in _currentlyAutosaving.ToArray())
foreach (var (uid, (time, name))in _currentlyAutosaving)
{
if (_timing.RealTime <= time)
continue;
if (!_mapManager.MapExists(map) || _mapManager.IsMapInitialized(map))
if (LifeStage(uid) >= EntityLifeStage.MapInitialized)
{
Log.Warning($"Can't autosave map {map}; it doesn't exist, or is initialized. Removing from autosave.");
_currentlyAutosaving.Remove(map);
return;
Log.Warning($"Can't autosave entity {uid}; it doesn't exist, or is initialized. Removing from autosave.");
_currentlyAutosaving.Remove(uid);
continue;
}
_currentlyAutosaving[uid] = (CalculateNextTime(), name);
var saveDir = Path.Combine(_cfg.GetCVar(CCVars.AutosaveDirectory), name);
_resMan.UserData.CreateDir(new ResPath(saveDir).ToRootedPath());
var path = Path.Combine(saveDir, $"{DateTime.Now.ToString("yyyy-M-dd_HH.mm.ss")}-AUTO.yml");
_currentlyAutosaving[map] = (CalculateNextTime(), name);
Log.Info($"Autosaving map {name} ({map}) to {path}. Next save in {ReadableTimeLeft(map)} seconds.");
_loader.TrySaveMap(map, new ResPath(path));
var path = new ResPath(Path.Combine(saveDir, $"{DateTime.Now:yyyy-M-dd_HH.mm.ss}-AUTO.yml"));
Log.Info($"Autosaving map {name} ({uid}) to {path}. Next save in {ReadableTimeLeft(uid)} seconds.");
if (HasComp<MapComponent>(uid))
_loader.TrySaveMap(uid, path);
else
_loader.TrySaveGrid(uid, path);
}
}
@@ -87,34 +91,41 @@ public sealed class MappingSystem : EntitySystem
return _timing.RealTime + TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.AutosaveInterval));
}
private double ReadableTimeLeft(MapId map)
private double ReadableTimeLeft(EntityUid uid)
{
return Math.Round(_currentlyAutosaving[map].next.TotalSeconds - _timing.RealTime.TotalSeconds);
return Math.Round(_currentlyAutosaving[uid].next.TotalSeconds - _timing.RealTime.TotalSeconds);
}
#region Public API
public void ToggleAutosave(MapId map, string? path=null)
public void ToggleAutosave(MapId map, string? path = null)
{
if (_map.TryGetMap(map, out var uid))
ToggleAutosave(uid.Value, path);
}
public void ToggleAutosave(EntityUid uid, string? path=null)
{
if (!_autosaveEnabled)
return;
if (path != null && _currentlyAutosaving.TryAdd(map, (CalculateNextTime(), Path.GetFileName(path))))
{
if (!_mapManager.MapExists(map) || _mapManager.IsMapInitialized(map))
{
Log.Warning("Tried to enable autosaving on non-existant or already initialized map!");
_currentlyAutosaving.Remove(map);
return;
}
if (_currentlyAutosaving.Remove(uid) || path == null)
return;
Log.Info($"Started autosaving map {path} ({map}). Next save in {ReadableTimeLeft(map)} seconds.");
}
else
if (LifeStage(uid) >= EntityLifeStage.MapInitialized)
{
_currentlyAutosaving.Remove(map);
Log.Info($"Stopped autosaving on map {map}");
Log.Error("Tried to enable autosaving on a post map-init entity.");
return;
}
if (!HasComp<MapComponent>(uid) && !HasComp<MapGridComponent>(uid))
{
Log.Error($"{ToPrettyString(uid)} is neither a grid or map");
return;
}
_currentlyAutosaving[uid] = (CalculateNextTime(), Path.GetFileName(path));
Log.Info($"Started autosaving map {path} ({uid}). Next save in {ReadableTimeLeft(uid)} seconds.");
}
#endregion

View File

@@ -83,7 +83,7 @@ public sealed class HealingSystem : EntitySystem
if (healing.ModifyBloodLevel != 0)
_bloodstreamSystem.TryModifyBloodLevel(entity.Owner, healing.ModifyBloodLevel);
var healed = _damageable.TryChangeDamage(entity.Owner, healing.Damage, true, origin: args.Args.User);
var healed = _damageable.TryChangeDamage(entity.Owner, healing.Damage * _damageable.UniversalTopicalsHealModifier, true, origin: args.Args.User);
if (healed == null && healing.BloodlossModifier != 0)
return;

View File

@@ -27,6 +27,9 @@ public sealed class MousetrapSystem : EntitySystem
private void OnUseInHand(EntityUid uid, MousetrapComponent component, UseInHandEvent args)
{
if (args.Handled)
return;
component.IsActive = !component.IsActive;
_popupSystem.PopupEntity(component.IsActive
? Loc.GetString("mousetrap-on-activate")
@@ -35,6 +38,8 @@ public sealed class MousetrapSystem : EntitySystem
args.User);
UpdateVisuals(uid);
args.Handled = true;
}
private void OnStepTriggerAttempt(EntityUid uid, MousetrapComponent component, ref StepTriggerAttemptEvent args)

View File

@@ -49,7 +49,7 @@ public sealed class NukeSystem : EntitySystem
/// Used to calculate when the nuke song should start playing for maximum kino with the nuke sfx
/// </summary>
private float _nukeSongLength;
private string _selectedNukeSong = String.Empty;
private ResolvedSoundSpecifier _selectedNukeSong = String.Empty;
/// <summary>
/// Time to leave between the nuke song and the nuke alarm playing.
@@ -302,7 +302,7 @@ public sealed class NukeSystem : EntitySystem
// Start playing the nuke event song so that it ends a couple seconds before the alert sound
// should play
if (nuke.RemainingTime <= _nukeSongLength + nuke.AlertSoundTime + NukeSongBuffer && !nuke.PlayedNukeSong && !string.IsNullOrEmpty(_selectedNukeSong))
if (nuke.RemainingTime <= _nukeSongLength + nuke.AlertSoundTime + NukeSongBuffer && !nuke.PlayedNukeSong && !ResolvedSoundSpecifier.IsNullOrEmpty(_selectedNukeSong))
{
_sound.DispatchStationEventMusic(uid, _selectedNukeSong, StationEventMusicType.Nuke);
nuke.PlayedNukeSong = true;
@@ -311,7 +311,7 @@ public sealed class NukeSystem : EntitySystem
// play alert sound if time is running out
if (nuke.RemainingTime <= nuke.AlertSoundTime && !nuke.PlayedAlertSound)
{
_sound.PlayGlobalOnStation(uid, _audio.GetSound(nuke.AlertSound), new AudioParams{Volume = -5f});
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(nuke.AlertSound), new AudioParams{Volume = -5f});
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
nuke.PlayedAlertSound = true;
UpdateAppearance(uid, nuke);
@@ -469,7 +469,7 @@ public sealed class NukeSystem : EntitySystem
var posText = $"({x}, {y})";
// We are collapsing the randomness here, otherwise we would get separate random song picks for checking duration and when actually playing the song afterwards
_selectedNukeSong = _audio.GetSound(component.ArmMusic);
_selectedNukeSong = _audio.ResolveSound(component.ArmMusic);
// warn a crew
var announcement = Loc.GetString("nuke-component-announcement-armed",
@@ -478,7 +478,7 @@ public sealed class NukeSystem : EntitySystem
var sender = Loc.GetString("nuke-component-announcement-sender");
_chatSystem.DispatchStationAnnouncement(stationUid ?? uid, announcement, sender, false, null, Color.Red);
_sound.PlayGlobalOnStation(uid, _audio.GetSound(component.ArmSound));
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(component.ArmSound));
_nukeSongLength = (float) _audio.GetAudioLength(_selectedNukeSong).TotalSeconds;
// turn on the spinny light
@@ -519,7 +519,7 @@ public sealed class NukeSystem : EntitySystem
_chatSystem.DispatchStationAnnouncement(uid, announcement, sender, false);
component.PlayedNukeSong = false;
_sound.PlayGlobalOnStation(uid, _audio.GetSound(component.DisarmSound));
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(component.DisarmSound));
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
// reset nuke remaining time to either itself or the minimum time, whichever is higher

View File

@@ -41,7 +41,9 @@ namespace Content.Server.Nutrition.EntitySystems
protected override void SplattedCreamPie(EntityUid uid, CreamPieComponent creamPie)
{
_audio.PlayPvs(_audio.GetSound(creamPie.Sound), uid, AudioParams.Default.WithVariation(0.125f));
// The entity is deleted, so play the sound at its position rather than parenting
var coordinates = Transform(uid).Coordinates;
_audio.PlayPvs(_audio.ResolveSound(creamPie.Sound), coordinates, AudioParams.Default.WithVariation(0.125f));
if (EntityManager.TryGetComponent(uid, out FoodComponent? foodComp))
{

View File

@@ -37,6 +37,8 @@ public sealed class PAISystem : SharedPAISystem
private void OnUseInHand(EntityUid uid, PAIComponent component, UseInHandEvent args)
{
// Not checking for Handled because ToggleableGhostRoleSystem already marks it as such.
if (!TryComp<MindContainerComponent>(uid, out var mind) || !mind.HasMind)
component.LastUser = args.User;
}

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