Revert "Revert "Upstream sync""

This commit is contained in:
Morb
2022-08-15 14:19:44 -07:00
committed by GitHub
parent fb72dd694a
commit fac3c2e4c5
628 changed files with 13379 additions and 6616 deletions

View File

@@ -1,6 +1,7 @@
using Content.Client.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.CrewManifest;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.SharedIdCardConsoleComponent;
@@ -36,6 +37,7 @@ namespace Content.Client.Access.UI
_window = new IdCardConsoleWindow(this, _prototypeManager, accessLevels) {Title = _entityManager.GetComponent<MetaDataComponent>(Owner.Owner).EntityName};
_window.CrewManifestButton.OnPressed += _ => SendMessage(new CrewManifestOpenUiMessage());
_window.PrivilegedIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(PrivilegedIdCardSlotId));
_window.TargetIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(TargetIdCardSlotId));

View File

@@ -1,14 +1,19 @@
<DefaultWindow xmlns="https://spacestation14.io"
MinSize="650 290">
<BoxContainer Orientation="Vertical">
<GridContainer Columns="3">
<Label Text="{Loc 'id-card-console-window-privileged-id'}" />
<Button Name="PrivilegedIdButton" Access="Public"/>
<Label Name="PrivilegedIdLabel" />
<GridContainer Columns="2">
<GridContainer Columns="3" HorizontalExpand="True">
<Label Text="{Loc 'id-card-console-window-privileged-id'}" />
<Button Name="PrivilegedIdButton" Access="Public"/>
<Label Name="PrivilegedIdLabel" />
<Label Text="{Loc 'id-card-console-window-target-id'}" />
<Button Name="TargetIdButton" Access="Public"/>
<Label Name="TargetIdLabel" />
<Label Text="{Loc 'id-card-console-window-target-id'}" />
<Button Name="TargetIdButton" Access="Public"/>
<Label Name="TargetIdLabel" />
</GridContainer>
<BoxContainer Orientation="Vertical">
<Button Name="CrewManifestButton" Access="Public" Text="{Loc 'crew-manifest-button-label'}" />
</BoxContainer>
</GridContainer>
<Control MinSize="0 8" />
<GridContainer Columns="3" HSeparationOverride="4">
@@ -21,6 +26,10 @@
<Button Name="JobTitleSaveButton" Text="{Loc 'id-card-console-window-save-button'}" Disabled="True" />
</GridContainer>
<Control MinSize="0 8" />
<GridContainer Columns="2">
<Label Text="{Loc 'id-card-console-window-job-selection-label'}" />
<OptionButton Name="JobPresetOptionButton" />
</GridContainer>
<GridContainer Name="AccessLevelGrid" Columns="5" HorizontalAlignment="Center">
<!-- Access level buttons are added here by the C# code -->

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using Content.Shared.Access;
using Content.Shared.Access.Systems;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
@@ -16,9 +17,12 @@ namespace Content.Client.Access.UI
[GenerateTypedNameReferences]
public sealed partial class IdCardConsoleWindow : DefaultWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly IdCardConsoleBoundUserInterface _owner;
private readonly Dictionary<string, Button> _accessButtons = new();
private readonly List<string> _jobPrototypeIds = new();
private string? _lastFullName;
private string? _lastJobTitle;
@@ -26,6 +30,7 @@ namespace Content.Client.Access.UI
public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeManager prototypeManager, List<string> accessLevels)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_owner = owner;
@@ -43,6 +48,21 @@ namespace Content.Client.Access.UI
};
JobTitleSaveButton.OnPressed += _ => SubmitData();
var jobs = _prototypeManager.EnumeratePrototypes<JobPrototype>();
foreach (var job in jobs)
{
if (!job.SetPreference)
{
continue;
}
_jobPrototypeIds.Add(job.ID);
JobPresetOptionButton.AddItem(Loc.GetString(job.Name), _jobPrototypeIds.Count - 1);
}
JobPresetOptionButton.OnItemSelected += SelectJobPreset;
foreach (var access in accessLevels)
{
if (!prototypeManager.TryIndex<AccessLevelPrototype>(access, out var accessLevel))
@@ -62,6 +82,56 @@ namespace Content.Client.Access.UI
}
}
private void ClearAllAccess()
{
foreach (var button in _accessButtons.Values)
{
if (button.Pressed)
{
button.Pressed = false;
}
}
}
private void SelectJobPreset(OptionButton.ItemSelectedEventArgs args)
{
if (!_prototypeManager.TryIndex(_jobPrototypeIds[args.Id], out JobPrototype? job))
{
return;
}
JobTitleLineEdit.Text = Loc.GetString(job.Name);
ClearAllAccess();
// this is a sussy way to do this
foreach (var access in job.Access)
{
if (_accessButtons.TryGetValue(access, out var button))
{
button.Pressed = true;
}
}
foreach (var group in job.AccessGroups)
{
if (!_prototypeManager.TryIndex(group, out AccessGroupPrototype? groupPrototype))
{
continue;
}
foreach (var access in groupPrototype.Tags)
{
if (_accessButtons.TryGetValue(access, out var button))
{
button.Pressed = true;
}
}
}
SubmitData();
}
public void UpdateState(IdCardConsoleBoundUserInterfaceState state)
{
PrivilegedIdButton.Text = state.IsPrivilegedIdPresent
@@ -100,6 +170,8 @@ namespace Content.Client.Access.UI
JobTitleSaveButton.Disabled = !interfaceEnabled || !jobTitleDirty;
JobPresetOptionButton.Disabled = !interfaceEnabled;
foreach (var (accessName, button) in _accessButtons)
{
button.Disabled = !interfaceEnabled;

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Power;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
@@ -40,7 +41,7 @@ namespace Content.Client.Atmos.Monitor
if (!sprite.LayerMapTryGet(_layerMap, out int layer))
return;
if (component.TryGetData<bool>("powered", out var powered))
if (component.TryGetData<bool>(PowerDeviceVisuals.Powered, out var powered))
{
if (_hideOnDepowered != null)
foreach (var visLayer in _hideOnDepowered)
@@ -53,12 +54,12 @@ namespace Content.Client.Atmos.Monitor
sprite.LayerSetState(setStateLayer, new RSI.StateId(state));
}
if (component.TryGetData<Vector2>("offset", out Vector2 offset))
if (component.TryGetData<Vector2>(AtmosMonitorVisuals.Offset, out Vector2 offset))
{
sprite.Offset = offset;
}
if (component.TryGetData<AtmosMonitorAlarmType>("alarmType", out var alarmType)
if (component.TryGetData<AtmosMonitorAlarmType>(AtmosMonitorVisuals.AlarmType, out var alarmType)
&& powered)
if (_alarmStates.TryGetValue(alarmType, out var state))
sprite.LayerSetState(layer, new RSI.StateId(state));

View File

@@ -0,0 +1,8 @@
using Content.Shared.Audio;
namespace Content.Client.Audio;
public sealed class ContentAudioSystem : SharedContentAudioSystem
{
}

View File

@@ -0,0 +1,51 @@
using Content.Client.Eui;
using Content.Client.GameTicking.Managers;
using Content.Shared.CrewManifest;
using Content.Shared.Eui;
using JetBrains.Annotations;
namespace Content.Client.CrewManifest;
[UsedImplicitly]
public sealed class CrewManifestEui : BaseEui
{
private readonly ClientGameTicker _gameTicker;
private readonly CrewManifestUi _window;
public CrewManifestEui()
{
_gameTicker = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ClientGameTicker>();
_window = new();
_window.OnClose += () =>
{
SendMessage(new CrewManifestEuiClosed());
};
}
public override void Opened()
{
base.Opened();
_window.OpenCentered();
}
public override void Closed()
{
base.Closed();
_window.Close();
}
public override void HandleState(EuiStateBase state)
{
base.HandleState(state);
if (state is not CrewManifestEuiState cast)
{
return;
}
_window.Populate(cast.StationName, cast.Entries);
}
}

View File

@@ -0,0 +1,82 @@
using Content.Client.GameTicking.Managers;
using Content.Shared.CrewManifest;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Client.CrewManifest;
public sealed class CrewManifestSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private Dictionary<string, Dictionary<string, int>> _jobDepartmentLookup = new();
private HashSet<string> _departments = new();
public IReadOnlySet<string> Departments => _departments;
public override void Initialize()
{
base.Initialize();
BuildDepartmentLookup();
_prototypeManager.PrototypesReloaded += OnPrototypesReload;
}
public override void Shutdown()
{
_prototypeManager.PrototypesReloaded -= OnPrototypesReload;
}
/// <summary>
/// Requests a crew manifest from the server.
/// </summary>
/// <param name="uid">EntityUid of the entity we're requesting the crew manifest from.</param>
public void RequestCrewManifest(EntityUid uid)
{
RaiseNetworkEvent(new RequestCrewManifestMessage(uid));
}
private void OnPrototypesReload(PrototypesReloadedEventArgs _)
{
_jobDepartmentLookup.Clear();
_departments.Clear();
BuildDepartmentLookup();
}
private void BuildDepartmentLookup()
{
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
{
_departments.Add(department.ID);
for (var i = 1; i <= department.Roles.Count; i++)
{
if (!_jobDepartmentLookup.TryGetValue(department.Roles[i - 1], out var departments))
{
departments = new();
_jobDepartmentLookup.Add(department.Roles[i - 1], departments);
}
departments.Add(department.ID, i);
}
}
}
public int GetDepartmentOrder(string department, string jobPrototype)
{
if (!Departments.Contains(department))
{
return -1;
}
if (!_jobDepartmentLookup.TryGetValue(jobPrototype, out var departments))
{
return -1;
}
return departments.TryGetValue(department, out var order)
? order
: -1;
}
}

View File

@@ -0,0 +1,21 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.HUD.UI"
Title="{Loc 'crew-manifest-window-title'}"
MinSize="450 750">
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
<ui:StripeBack Name="StationNameContainer">
<PanelContainer>
<Label Name="StationName" Align="Center" />
</PanelContainer>
</ui:StripeBack>
<BoxContainer HorizontalExpand="True" VerticalExpand="True">
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<!-- this MIGHT have race conditions -->
<BoxContainer Name="CrewManifestListing" Orientation="Vertical" HorizontalExpand="True">
<Label Text="{Loc 'crew-manifest-no-valid-station'}" HorizontalExpand="True" />
</BoxContainer>
<!-- Crew manifest goes here. -->
</ScrollContainer>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,186 @@
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.CrewManifest;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.CrewManifest;
[GenerateTypedNameReferences]
public sealed partial class CrewManifestUi : DefaultWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
private readonly CrewManifestSystem _crewManifestSystem;
private EntityUid? _station;
public CrewManifestUi()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_crewManifestSystem = _entitySystemManager.GetEntitySystem<CrewManifestSystem>();
StationName.AddStyleClass("LabelBig");
}
public void Populate(string name, CrewManifestEntries? entries)
{
CrewManifestListing.DisposeAllChildren();
CrewManifestListing.RemoveAllChildren();
StationNameContainer.Visible = entries != null;
StationName.Text = name;
if (entries == null) return;
var entryList = SortEntries(entries);
foreach (var item in entryList)
{
CrewManifestListing.AddChild(new CrewManifestSection(item.section, item.entries, _resourceCache, _crewManifestSystem));
}
}
private List<(string section, List<CrewManifestEntry> entries)> SortEntries(CrewManifestEntries entries)
{
var entryDict = new Dictionary<string, List<CrewManifestEntry>>();
foreach (var entry in entries.Entries)
{
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
{
// this is a little expensive, and could be better
if (department.Roles.Contains(entry.JobPrototype))
{
entryDict.GetOrNew(department.ID).Add(entry);
}
}
}
var entryList = new List<(string section, List<CrewManifestEntry> entries)>();
foreach (var (section, listing) in entryDict)
{
entryList.Add((section, listing));
}
var sortOrder = _configManager.GetCVar(CCVars.CrewManifestOrdering).Split(",").ToList();
entryList.Sort((a, b) =>
{
var ai = sortOrder.IndexOf(a.section);
var bi = sortOrder.IndexOf(b.section);
// this is up here so -1 == -1 occurs first
if (ai == bi)
return 0;
if (ai == -1)
return -1;
if (bi == -1)
return 1;
return ai.CompareTo(bi);
});
return entryList;
}
private sealed class CrewManifestSection : BoxContainer
{
public CrewManifestSection(string sectionTitle, List<CrewManifestEntry> entries, IResourceCache cache, CrewManifestSystem crewManifestSystem)
{
Orientation = LayoutOrientation.Vertical;
HorizontalExpand = true;
AddChild(new Label()
{
StyleClasses = { "LabelBig" },
Text = Loc.GetString(sectionTitle)
});
entries.Sort((a, b) =>
{
var posA = crewManifestSystem.GetDepartmentOrder(sectionTitle, a.JobPrototype);
var posB = crewManifestSystem.GetDepartmentOrder(sectionTitle, b.JobPrototype);
return posA.CompareTo(posB);
});
var gridContainer = new GridContainer()
{
HorizontalExpand = true,
Columns = 2
};
AddChild(gridContainer);
var path = new ResourcePath("/Textures/Interface/Misc/job_icons.rsi");
cache.TryGetResource(path, out RSIResource? rsi);
foreach (var entry in entries)
{
var name = new Label()
{
HorizontalExpand = true,
Text = entry.Name
};
var titleContainer = new BoxContainer()
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true
};
var title = new Label()
{
Text = Loc.GetString(entry.JobTitle)
};
if (rsi != null)
{
var icon = new TextureRect()
{
TextureScale = (2, 2),
Stretch = TextureRect.StretchMode.KeepCentered
};
if (rsi.RSI.TryGetState(entry.JobIcon, out _))
{
var specifier = new SpriteSpecifier.Rsi(path, entry.JobIcon);
icon.Texture = specifier.Frame0();
}
else if (rsi.RSI.TryGetState("Unknown", out _))
{
var specifier = new SpriteSpecifier.Rsi(path, "Unknown");
icon.Texture = specifier.Frame0();
}
titleContainer.AddChild(icon);
titleContainer.AddChild(title);
}
else
{
titleContainer.AddChild(title);
}
gridContainer.AddChild(name);
gridContainer.AddChild(titleContainer);
}
}
}
}

View File

@@ -82,7 +82,7 @@ namespace Content.Client.Damage
/// completely invisible.
/// </remarks>
[DataField("targetLayers")]
private List<string>? _targetLayers;
private List<Enum>? _targetLayers;
/// <summary>
/// The actual sprites for every damage group
@@ -383,18 +383,8 @@ namespace Content.Client.Damage
//
// If the layer doesn't have a base state, or
// the layer key just doesn't exist, we skip it.
foreach (var keyString in _targetLayers)
foreach (var key in _targetLayers)
{
object key;
if (IoCManager.Resolve<IReflectionManager>().TryParseEnumReference(keyString, out var @enum))
{
key = @enum;
}
else
{
key = keyString;
}
if (!spriteComponent.LayerMapTryGet(key, out int index)
|| spriteComponent.LayerGetState(index).ToString() == null)
{
@@ -562,20 +552,11 @@ namespace Content.Client.Damage
/// </summary>
private void UpdateDisabledLayers(SpriteComponent spriteComponent, AppearanceComponent component, DamageVisualizerDataComponent damageData)
{
foreach (object layer in damageData.TargetLayerMapKeys)
foreach (var layer in damageData.TargetLayerMapKeys)
{
bool? layerStatus = null;
switch (layer)
{
case Enum layerEnum:
if (component.TryGetData<bool>(layerEnum, out var layerStateEnum))
layerStatus = layerStateEnum;
break;
case string layerString:
if (component.TryGetData<bool>(layerString, out var layerStateString))
layerStatus = layerStateString;
break;
}
if (component.TryGetData<bool>(layer, out var layerStateEnum))
layerStatus = layerStateEnum;
if (layerStatus == null)
continue;

View File

@@ -13,7 +13,7 @@ namespace Content.Client.Damage
[RegisterComponent]
public sealed class DamageVisualizerDataComponent : Component
{
public List<object> TargetLayerMapKeys = new();
public List<Enum> TargetLayerMapKeys = new();
public bool Disabled = false;
public bool Valid = true;
public FixedPoint2 LastDamageThreshold = FixedPoint2.Zero;

View File

@@ -0,0 +1,9 @@
using Content.Shared.Dragon;
namespace Content.Client.Dragon;
[RegisterComponent]
public sealed class DragonRiftComponent : SharedDragonRiftComponent
{
}

View File

@@ -0,0 +1,51 @@
using Content.Shared.Dragon;
using Robust.Client.GameObjects;
using Robust.Shared.GameStates;
namespace Content.Client.Dragon;
public sealed class DragonSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DragonRiftComponent, ComponentHandleState>(OnRiftHandleState);
}
private void OnRiftHandleState(EntityUid uid, DragonRiftComponent component, ref ComponentHandleState args)
{
if (args.Current is not DragonRiftComponentState state)
return;
if (component.State == state.State) return;
component.State = state.State;
TryComp<SpriteComponent>(uid, out var sprite);
TryComp<PointLightComponent>(uid, out var light);
if (sprite == null && light == null)
return;
switch (state.State)
{
case DragonRiftState.Charging:
sprite?.LayerSetColor(0, Color.FromHex("#569fff"));
if (light != null)
light.Color = Color.FromHex("#366db5");
break;
case DragonRiftState.AlmostFinished:
sprite?.LayerSetColor(0, Color.FromHex("#cf4cff"));
if (light != null)
light.Color = Color.FromHex("#9e2fc1");
break;
case DragonRiftState.Finished:
sprite?.LayerSetColor(0, Color.FromHex("#edbc36"));
if (light != null)
light.Color = Color.FromHex("#cbaf20");
break;
}
}
}

View File

@@ -77,7 +77,7 @@ namespace Content.Client.Info
public static Control MakeRules(IConfigurationManager cfg, IResourceManager res)
{
return MakeSection(Loc.GetString("ui-rules-header"), cfg.GetCVar(CCVars.RulesFile), true, res);
return MakeSection(Loc.GetString(cfg.GetCVar(CCVars.RulesHeader)), cfg.GetCVar(CCVars.RulesFile), true, res);
}
}
}

View File

@@ -1,12 +1,18 @@
using System.Linq;
using Content.Client.CrewManifest;
using Content.Client.Eui;
using Content.Client.GameTicking.Managers;
using Content.Client.HUD.UI;
using Content.Client.Players.PlayTimeTracking;
using Content.Shared.CCVar;
using Content.Shared.CrewManifest;
using Content.Shared.Roles;
using Robust.Client.Console;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;
@@ -17,6 +23,7 @@ namespace Content.Client.LateJoin
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
public event Action<(EntityUid, string)> SelectedId;
@@ -64,6 +71,7 @@ namespace Content.Client.LateJoin
_jobCategories.Clear();
var gameTicker = EntitySystem.Get<ClientGameTicker>();
var tracker = IoCManager.Resolve<PlayTimeTrackingManager>();
if (!gameTicker.DisallowedLateJoin && gameTicker.StationNames.Count == 0)
Logger.Warning("No stations exist, nothing to display in late-join GUI");
@@ -109,6 +117,21 @@ namespace Content.Client.LateJoin
}
}
});
if (_configManager.GetCVar<bool>(CCVars.CrewManifestWithoutEntity))
{
var crewManifestButton = new Button()
{
Text = Loc.GetString("crew-manifest-button-label")
};
crewManifestButton.OnPressed += args =>
{
EntitySystem.Get<CrewManifestSystem>().RequestCrewManifest(id);
};
_base.AddChild(crewManifestButton);
}
var jobListScroll = new ScrollContainer()
{
VerticalExpand = true,
@@ -202,12 +225,8 @@ namespace Content.Client.LateJoin
Stretch = TextureRect.StretchMode.KeepCentered
};
if (prototype.Icon != null)
{
var specifier = new SpriteSpecifier.Rsi(new ResourcePath("/Textures/Interface/Misc/job_icons.rsi"), prototype.Icon);
icon.Texture = specifier.Frame0();
}
var specifier = new SpriteSpecifier.Rsi(new ResourcePath("/Textures/Interface/Misc/job_icons.rsi"), prototype.Icon);
icon.Texture = specifier.Frame0();
jobSelector.AddChild(icon);
var jobLabel = new Label
@@ -226,9 +245,16 @@ namespace Content.Client.LateJoin
SelectedId?.Invoke((id, jobButton.JobId));
};
if (value == 0)
string? reason = null;
if (value == 0 || !tracker.IsAllowed(prototype, out reason))
{
jobButton.Disabled = true;
if (!string.IsNullOrEmpty(reason))
{
jobButton.ToolTip = reason;
}
}
_jobButtons[id][prototype.ID] = jobButton;

View File

@@ -80,7 +80,7 @@ public sealed partial class MappingSystem : EntitySystem
var tileIcon = contentTileDef.IsSpace
? _spaceIcon
: new SpriteSpecifier.Texture(new ResourcePath(tileDef.Path) / $"{tileDef.SpriteName}.png");
: new SpriteSpecifier.Texture(contentTileDef.Sprite!);
ev.Action = new InstantAction()
{

View File

@@ -1,18 +1,24 @@
using Content.Client.Message;
using Content.Shared.CCVar;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.CrewManifest;
using Content.Shared.PDA;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.Configuration;
namespace Content.Client.PDA
{
[UsedImplicitly]
public sealed class PDABoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
private PDAMenu? _menu;
public PDABoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
@@ -27,6 +33,15 @@ namespace Content.Client.PDA
SendMessage(new PDAToggleFlashlightMessage());
};
if (_configManager.GetCVar(CCVars.CrewManifestUnsecure))
{
_menu.CrewManifestButton.Visible = true;
_menu.CrewManifestButton.OnPressed += _ =>
{
SendMessage(new CrewManifestOpenUiMessage());
};
}
_menu.EjectIdButton.OnPressed += _ =>
{
SendMessage(new ItemSlotButtonPressedEvent(PDAComponent.PDAIdSlotId));

View File

@@ -37,6 +37,10 @@
Access="Public"
Text="{Loc 'comp-pda-ui-toggle-flashlight-button'}"
ToggleMode="True" />
<Button Name="CrewManifestButton"
Access="Public"
Text="{Loc 'crew-manifest-button-label'}"
Visible="False" />
<Button Name="ActivateUplinkButton"
Access="Public"
Text="{Loc 'pda-bound-user-interface-uplink-tab-title'}" />

View File

@@ -45,7 +45,8 @@ namespace Content.Client.ParticleAccelerator.UI
{
base.Dispose(disposing);
_menu?.Close();
_menu?.Dispose();
_menu = null;
}
}
}

View File

@@ -334,11 +334,13 @@ namespace Content.Client.ParticleAccelerator.UI
{
return new(this, resourceCache, name, state);
}
UpdateUI(false, false, false, false);
}
private bool StrengthSpinBoxValid(int n)
{
return (n >= 0 && n <= 4 && !_blockSpinBox);
return (n >= 0 && n <= 3 && !_blockSpinBox);
}
private void PowerStateChanged(object? sender, ValueChangedEventArgs e)
@@ -358,13 +360,15 @@ namespace Content.Client.ParticleAccelerator.UI
case 3:
newState = ParticleAcceleratorPowerState.Level2;
break;
case 4:
newState = ParticleAcceleratorPowerState.Level3;
break;
// They can't reach this level anyway and I just want to fix the bugginess for now.
//case 4:
// newState = ParticleAcceleratorPowerState.Level3;
// break;
default:
return;
}
_stateSpinBox.SetButtonDisabled(true);
Owner.SendPowerStateMessage(newState);
}

View File

@@ -0,0 +1,64 @@
using Content.Client.Shuttles.UI;
using Content.Shared.Shuttles.BUIStates;
using Content.Shared.Shuttles.Events;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.Shuttles.BUI;
[UsedImplicitly]
public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
{
private IFFConsoleWindow? _window;
public IFFConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new IFFConsoleWindow();
_window.OnClose += Close;
_window.ShowIFF += SendIFFMessage;
_window.ShowVessel += SendVesselMessage;
_window.OpenCenteredLeft();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not IFFConsoleBoundUserInterfaceState bState)
return;
_window?.UpdateState(bState);
}
private void SendIFFMessage(bool obj)
{
SendMessage(new IFFShowIFFMessage()
{
Show = obj,
});
}
private void SendVesselMessage(bool obj)
{
SendMessage(new IFFShowVesselMessage()
{
Show = obj,
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_window?.Close();
_window = null;
}
}
}

View File

@@ -1,6 +1,6 @@
<userInterface:FancyWindow xmlns="https://spacestation14.io"
xmlns:userInterface="clr-namespace:Content.Client.UserInterface"
Title="Emergency Shuttle Console"
Title="{Loc 'emergency-shuttle-console-window-title'}"
MinSize="400 400">
<BoxContainer Orientation="Vertical"
Margin="5">

View File

@@ -0,0 +1,20 @@
<userInterface:FancyWindow xmlns="https://spacestation14.io"
xmlns:userInterface="clr-namespace:Content.Client.UserInterface"
Title="{Loc 'iff-console-window-title'}"
MinSize="200 200">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<GridContainer Margin="4 0 0 0" Columns="2">
<Label Name="ShowIFFLabel" Text="{Loc 'iff-console-show-iff-label'}" HorizontalExpand="True" StyleClasses="StatusFieldTitle" />
<BoxContainer Orientation="Horizontal" MinWidth="120">
<Button Name="ShowIFFOnButton" Text="{Loc 'iff-console-on'}" StyleClasses="OpenRight" />
<Button Name="ShowIFFOffButton" Text="{Loc 'iff-console-off'}" StyleClasses="OpenLeft" />
</BoxContainer>
<Label Name="ShowVesselLabel" Text="{Loc 'iff-console-show-vessel-label'}" HorizontalExpand="True" StyleClasses="StatusFieldTitle" />
<BoxContainer Orientation="Horizontal" MinWidth="120">
<Button Name="ShowVesselOnButton" Text="{Loc 'iff-console-on'}" StyleClasses="OpenRight" />
<Button Name="ShowVesselOffButton" Text="{Loc 'iff-console-off'}" StyleClasses="OpenLeft" />
</BoxContainer>
</GridContainer>
</BoxContainer>
</userInterface:FancyWindow>

View File

@@ -0,0 +1,86 @@
using Content.Client.Computer;
using Content.Client.UserInterface;
using Content.Shared.Shuttles.BUIStates;
using Content.Shared.Shuttles.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Shuttles.UI;
[GenerateTypedNameReferences]
public sealed partial class IFFConsoleWindow : FancyWindow,
IComputerWindow<IFFConsoleBoundUserInterfaceState>
{
private readonly ButtonGroup _showIFFButtonGroup = new();
private readonly ButtonGroup _showVesselButtonGroup = new();
public event Action<bool>? ShowIFF;
public event Action<bool>? ShowVessel;
public IFFConsoleWindow()
{
RobustXamlLoader.Load(this);
ShowIFFOffButton.Group = _showIFFButtonGroup;
ShowIFFOnButton.Group = _showIFFButtonGroup;
ShowIFFOnButton.OnPressed += args => ShowIFFPressed(true);
ShowIFFOffButton.OnPressed += args => ShowIFFPressed(false);
ShowVesselOffButton.Group = _showVesselButtonGroup;
ShowVesselOnButton.Group = _showVesselButtonGroup;
ShowVesselOnButton.OnPressed += args => ShowVesselPressed(true);
ShowVesselOffButton.OnPressed += args => ShowVesselPressed(false);
}
private void ShowIFFPressed(bool pressed)
{
ShowIFF?.Invoke(pressed);
}
private void ShowVesselPressed(bool pressed)
{
ShowVessel?.Invoke(pressed);
}
public void UpdateState(IFFConsoleBoundUserInterfaceState state)
{
if ((state.AllowedFlags & IFFFlags.HideLabel) != 0x0)
{
ShowIFFOffButton.Disabled = false;
ShowIFFOnButton.Disabled = false;
if ((state.Flags & IFFFlags.HideLabel) != 0x0)
{
ShowIFFOffButton.Pressed = true;
}
else
{
ShowIFFOnButton.Pressed = true;
}
}
else
{
ShowIFFOffButton.Disabled = true;
ShowIFFOnButton.Disabled = true;
}
if ((state.AllowedFlags & IFFFlags.Hide) != 0x0)
{
ShowVesselOffButton.Disabled = false;
ShowVesselOnButton.Disabled = false;
if ((state.Flags & IFFFlags.Hide) != 0x0)
{
ShowVesselOffButton.Pressed = true;
}
else
{
ShowVesselOnButton.Pressed = true;
}
}
else
{
ShowVesselOffButton.Disabled = true;
ShowVesselOnButton.Disabled = true;
}
}
}

View File

@@ -1,5 +1,6 @@
using Content.Client.Stylesheets;
using Content.Shared.Shuttles.BUIStates;
using Content.Shared.Shuttles.Components;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -200,7 +201,8 @@ public sealed class RadarControl : Control
foreach (var grid in _mapManager.FindGridsIntersecting(mapPosition.MapId,
new Box2(mapPosition.Position - MaxRadarRange, mapPosition.Position + MaxRadarRange)))
{
if (grid.GridEntityId == ourGridId) continue;
if (grid.GridEntityId == ourGridId)
continue;
var gridBody = bodyQuery.GetComponent(grid.GridEntityId);
if (gridBody.Mass < 10f)
@@ -209,6 +211,15 @@ public sealed class RadarControl : Control
continue;
}
_entManager.TryGetComponent<IFFComponent>(grid.GridEntityId, out var iff);
// Hide it entirely.
if (iff != null &&
(iff.Flags & IFFFlags.Hide) != 0x0)
{
continue;
}
shown.Add(grid.GridEntityId);
var name = metaQuery.GetComponent(grid.GridEntityId).EntityName;
@@ -219,9 +230,13 @@ public sealed class RadarControl : Control
var gridFixtures = fixturesQuery.GetComponent(grid.GridEntityId);
var gridMatrix = gridXform.WorldMatrix;
Matrix3.Multiply(in gridMatrix, in offsetMatrix, out var matty);
var color = iff?.Color ?? IFFComponent.IFFColor;
if (ShowIFF)
if (ShowIFF &&
(iff == null && IFFComponent.ShowIFFDefault ||
(iff.Flags & IFFFlags.HideLabel) == 0x0))
{
var gridBounds = grid.LocalAABB;
Label label;
if (!_iffControls.TryGetValue(grid.GridEntityId, out var control))
@@ -229,37 +244,35 @@ public sealed class RadarControl : Control
label = new Label()
{
HorizontalAlignment = HAlignment.Left,
FontColorOverride = Color.Aquamarine,
};
control = new PanelContainer()
{
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
Children =
{
label
},
StyleClasses = { StyleNano.StyleClassBorderedWindowPanel },
};
_iffControls[grid.GridEntityId] = control;
AddChild(control);
_iffControls[grid.GridEntityId] = label;
AddChild(label);
}
else
{
label = (Label) control;
}
label.FontColorOverride = color;
var gridCentre = matty.Transform(gridBody.LocalCenter);
gridCentre.Y = -gridCentre.Y;
var distance = gridCentre.Length;
if (gridCentre.Length > RadarRange)
{
gridCentre = gridCentre.Normalized * RadarRange;
}
// y-offset the control to always render below the grid (vertically)
var yOffset = Math.Max(gridBounds.Height, gridBounds.Width) * MinimapScale / 1.8f / UIScale;
control.Visible = true;
label = (Label) control.GetChild(0);
// The actual position in the UI. We offset the matrix position to render it off by half its width
// plus by the offset.
var uiPosition = ScalePosition(gridCentre) / UIScale - new Vector2(label.Width / 2f, -yOffset);
// Look this is uggo so feel free to cleanup. We just need to clamp the UI position to within the viewport.
uiPosition = new Vector2(Math.Clamp(uiPosition.X, 0f, Width - label.Width),
Math.Clamp(uiPosition.Y, 10f, Height - label.Height));
label.Visible = true;
label.Text = Loc.GetString("shuttle-console-iff-label", ("name", name), ("distance", $"{distance:0.0}"));
LayoutContainer.SetPosition(control, ScalePosition(gridCentre) / UIScale);
LayoutContainer.SetPosition(label, uiPosition);
}
else
{
@@ -267,7 +280,7 @@ public sealed class RadarControl : Control
}
// Detailed view
DrawGrid(handle, matty, gridFixtures, Color.Aquamarine);
DrawGrid(handle, matty, gridFixtures, color);
DrawDocks(handle, grid.GridEntityId, matty);
}

View File

@@ -0,0 +1,58 @@
using Content.Shared.Sprite;
using Robust.Client.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Reflection;
namespace Content.Client.Sprite;
public sealed class RandomSpriteSystem : SharedRandomSpriteSystem
{
[Dependency] private readonly IReflectionManager _reflection = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RandomSpriteComponent, ComponentHandleState>(OnHandleState);
}
private void OnHandleState(EntityUid uid, RandomSpriteComponent component, ref ComponentHandleState args)
{
if (args.Current is not RandomSpriteColorComponentState state)
return;
if (state.Selected.Equals(component.Selected))
return;
component.Selected.Clear();
component.Selected.EnsureCapacity(state.Selected.Count);
foreach (var layer in state.Selected)
{
component.Selected.Add(layer.Key, layer.Value);
}
UpdateAppearance(uid, component);
}
private void UpdateAppearance(EntityUid uid, RandomSpriteComponent component, SpriteComponent? sprite = null)
{
if (!Resolve(uid, ref sprite, false))
return;
foreach (var layer in component.Selected)
{
object key;
if (_reflection.TryParseEnumReference(layer.Key, out var @enum))
{
key = @enum;
}
else
{
key = layer.Key;
}
sprite.LayerSetState(key, layer.Value.State);
sprite.LayerSetColor(key, layer.Value.Color ?? Color.White);
}
}
}

View File

@@ -0,0 +1,47 @@
using Content.Shared.StationRecords;
using Robust.Client.GameObjects;
namespace Content.Client.StationRecords;
public sealed class GeneralStationRecordConsoleBoundUserInterface : BoundUserInterface
{
private GeneralStationRecordConsoleWindow? _window = default!;
public GeneralStationRecordConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{}
protected override void Open()
{
base.Open();
_window = new();
_window.OnKeySelected += OnKeySelected;
_window.OnClose += Close;
_window.OpenCentered();
}
private void OnKeySelected(StationRecordKey? key)
{
SendMessage(new SelectGeneralStationRecord(key));
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not GeneralStationRecordConsoleState cast)
{
return;
}
_window?.UpdateState(cast);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_window?.Close();
}
}

View File

@@ -0,0 +1,17 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'general-station-record-console-window-title'}"
MinSize="750 500">
<BoxContainer>
<!-- Record listing -->
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" MinWidth="250" VerticalExpand="True">
<Label Name="RecordListingStatus" Visible="False" />
<ScrollContainer VerticalExpand="True">
<ItemList Name="RecordListing" />
</ScrollContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" Margin="5 5 5 5">
<Label Name="RecordContainerStatus" Visible="False" Text="{Loc 'general-station-record-console-select-record-info'}"/>
<BoxContainer Name="RecordContainer" Orientation="Vertical" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,126 @@
using System.Linq;
using Content.Shared.StationRecords;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.StationRecords;
[GenerateTypedNameReferences]
public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
{
public Action<StationRecordKey?>? OnKeySelected;
private bool _isPopulating;
public GeneralStationRecordConsoleWindow()
{
RobustXamlLoader.Load(this);
RecordListing.OnItemSelected += args =>
{
if (_isPopulating || RecordListing[args.ItemIndex].Metadata is not StationRecordKey cast)
{
return;
}
OnKeySelected?.Invoke(cast);
};
RecordListing.OnItemDeselected += _ =>
{
if (!_isPopulating)
OnKeySelected?.Invoke(null);
};
}
public void UpdateState(GeneralStationRecordConsoleState state)
{
if (state.RecordListing == null)
{
RecordListingStatus.Visible = true;
RecordListing.Visible = false;
RecordListingStatus.Text = Loc.GetString("general-station-record-console-empty-state");
return;
}
RecordListingStatus.Visible = false;
RecordListing.Visible = true;
PopulateRecordListing(state.RecordListing!, state.SelectedKey);
RecordContainerStatus.Visible = state.Record == null;
if (state.Record != null)
{
RecordContainerStatus.Visible = state.SelectedKey == null;
RecordContainerStatus.Text = state.SelectedKey == null
? Loc.GetString("general-station-record-console-no-record-found")
: Loc.GetString("general-station-record-console-select-record-info");
PopulateRecordContainer(state.Record);
}
else
{
RecordContainer.DisposeAllChildren();
RecordContainer.RemoveAllChildren();
}
}
private void PopulateRecordListing(Dictionary<StationRecordKey, string> listing, StationRecordKey? selected)
{
RecordListing.Clear();
RecordListing.ClearSelected();
_isPopulating = true;
foreach (var (key, name) in listing)
{
var item = RecordListing.AddItem(name);
item.Metadata = key;
if (selected != null && key.ID == selected.Value.ID)
{
item.Selected = true;
}
}
_isPopulating = false;
RecordListing.SortItemsByText();
}
private void PopulateRecordContainer(GeneralStationRecord record)
{
RecordContainer.DisposeAllChildren();
RecordContainer.RemoveAllChildren();
// sure
var recordControls = new Control[]
{
new Label()
{
Text = record.Name,
StyleClasses = { "LabelBig" }
},
new Label()
{
Text = Loc.GetString("general-station-record-console-record-age", ("age", record.Age.ToString()))
},
new Label()
{
Text = Loc.GetString("general-station-record-console-record-title", ("job", Loc.GetString(record.JobTitle)))
},
new Label()
{
Text = Loc.GetString("general-station-record-console-record-species", ("species", record.Species))
},
new Label()
{
Text = Loc.GetString("general-station-record-console-record-gender", ("gender", record.Gender.ToString()))
}
};
foreach (var control in recordControls)
{
RecordContainer.AddChild(control);
}
}
}

View File

@@ -0,0 +1,65 @@
using System.Linq;
using Content.Shared.Storage.Components;
using Content.Shared.Storage.EntitySystems;
using Robust.Client.GameObjects;
using Robust.Shared.Utility;
namespace Content.Client.Storage.Systems;
public sealed class ItemMapperSystem : SharedItemMapperSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ItemMapperComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<ItemMapperComponent, AppearanceChangeEvent>(OnAppearance);
}
private void OnStartup(EntityUid uid, ItemMapperComponent component, ComponentStartup args)
{
if (TryComp<SpriteComponent>(uid, out var sprite))
{
component.RSIPath ??= sprite.BaseRSI!.Path!;
}
}
private void OnAppearance(EntityUid uid, ItemMapperComponent component, ref AppearanceChangeEvent args)
{
if (TryComp<SpriteComponent>(component.Owner, out var spriteComponent))
{
if (component.SpriteLayers.Count == 0)
{
InitLayers(component, spriteComponent, args.Component);
}
EnableLayers(component, spriteComponent, args.Component);
}
}
private void InitLayers(ItemMapperComponent component, SpriteComponent spriteComponent, AppearanceComponent appearance)
{
if (!appearance.TryGetData<ShowLayerData>(StorageMapVisuals.InitLayers, out var wrapper))
return;
component.SpriteLayers.AddRange(wrapper.QueuedEntities);
foreach (var sprite in component.SpriteLayers)
{
spriteComponent.LayerMapReserveBlank(sprite);
spriteComponent.LayerSetSprite(sprite, new SpriteSpecifier.Rsi(component.RSIPath!, sprite));
spriteComponent.LayerSetVisible(sprite, false);
}
}
private void EnableLayers(ItemMapperComponent component, SpriteComponent spriteComponent, AppearanceComponent appearance)
{
if (!appearance.TryGetData<ShowLayerData>(StorageMapVisuals.LayerChanged, out var wrapper))
return;
foreach (var layerName in component.SpriteLayers)
{
var show = wrapper.QueuedEntities.Contains(layerName);
spriteComponent.LayerSetVisible(layerName, show);
}
}
}

View File

@@ -1,7 +1,7 @@
using Content.Shared.Storage;
using Content.Client.Animations;
using Content.Client.Animations;
using Content.Shared.Storage;
namespace Content.Client.Storage;
namespace Content.Client.Storage.Systems;
// TODO kill this is all horrid.
public sealed class StorageSystem : EntitySystem

View File

@@ -1,74 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Storage.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Content.Client.Storage.Visualizers
{
[UsedImplicitly]
public sealed class MappedItemVisualizer : AppearanceVisualizer
{
[DataField("sprite")] private ResourcePath? _rsiPath;
private List<string> _spriteLayers = new();
public override void InitializeEntity(EntityUid entity)
{
base.InitializeEntity(entity);
if (IoCManager.Resolve<IEntityManager>().TryGetComponent<ISpriteComponent?>(entity, out var spriteComponent))
{
_rsiPath ??= spriteComponent.BaseRSI!.Path!;
}
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var entities = IoCManager.Resolve<IEntityManager>();
if (entities.TryGetComponent(component.Owner, out ISpriteComponent? spriteComponent))
{
if (_spriteLayers.Count == 0)
{
InitLayers(spriteComponent, component);
}
EnableLayers(spriteComponent, component);
}
}
private void InitLayers(ISpriteComponent spriteComponent, AppearanceComponent component)
{
if (!component.TryGetData<ShowLayerData>(StorageMapVisuals.InitLayers, out var wrapper))
return;
_spriteLayers.AddRange(wrapper.QueuedEntities);
foreach (var sprite in _spriteLayers)
{
spriteComponent.LayerMapReserveBlank(sprite);
spriteComponent.LayerSetSprite(sprite, new SpriteSpecifier.Rsi(_rsiPath!, sprite));
spriteComponent.LayerSetVisible(sprite, false);
}
}
private void EnableLayers(ISpriteComponent spriteComponent, AppearanceComponent component)
{
if (!component.TryGetData<ShowLayerData>(StorageMapVisuals.LayerChanged, out var wrapper))
return;
foreach (var layerName in _spriteLayers)
{
var show = wrapper.QueuedEntities.Contains(layerName);
spriteComponent.LayerSetVisible(layerName, show);
}
}
}
}

View File

@@ -2,6 +2,7 @@ using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager.Attributes;
using static Content.Shared.Foldable.SharedFoldableSystem;
namespace Content.Client.Visualizer;
@@ -19,7 +20,7 @@ public sealed class FoldableVisualizer : AppearanceVisualizer
if (!entManager.TryGetComponent(appearance.Owner, out SpriteComponent? sprite)) return;
if (appearance.TryGetData("FoldedState", out bool folded) && folded)
if (appearance.TryGetData(FoldedVisuals.State, out bool folded) && folded)
{
sprite.LayerSetState(FoldableVisualLayers.Base, $"{_key}_folded");
}

View File

@@ -1,4 +1,5 @@
using JetBrains.Annotations;
using Content.Shared.Buckle.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -20,7 +21,7 @@ namespace Content.Client.Visualizer
if (!entManager.TryGetComponent(appearance.Owner, out SpriteComponent? sprite)) return;
if (appearance.TryGetData("StrapState", out bool strapped) && strapped)
if (appearance.TryGetData(StrapVisuals.State, out bool strapped) && strapped)
{
sprite.LayerSetState(0, $"{_key}_buckled");
}

View File

@@ -63,7 +63,7 @@ public sealed partial class GunSystem : SharedGunSystem
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
SubscribeLocalEvent<GunComponent, MuzzleFlashEvent>(OnMuzzleFlash);
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
// Plays animated effects on the client.
SubscribeNetworkEvent<HitscanEvent>(OnHitscan);
@@ -72,9 +72,9 @@ public sealed partial class GunSystem : SharedGunSystem
InitializeSpentAmmo();
}
private void OnMuzzleFlash(EntityUid uid, GunComponent component, MuzzleFlashEvent args)
private void OnMuzzleFlash(MuzzleFlashEvent args)
{
CreateEffect(uid, args);
CreateEffect(args.Uid, args);
}
private void OnHitscan(HitscanEvent ev)

View File

@@ -436,7 +436,7 @@ public static class PoolManager
mapData.MapGrid = mapManager.CreateGrid(mapData.MapId);
mapData.GridCoords = new EntityCoordinates(mapData.MapGrid.GridEntityId, 0, 0);
var tileDefinitionManager = IoCManager.Resolve<ITileDefinitionManager>();
var plating = tileDefinitionManager["plating"];
var plating = tileDefinitionManager["Plating"];
var platingTile = new Tile(plating.TileId);
mapData.MapGrid.SetTile(mapData.GridCoords, platingTile);
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);

View File

@@ -43,7 +43,7 @@ namespace Content.IntegrationTests.Tests
grid = mapManager.CreateGrid(mapId);
var tileDefinition = tileDefinitionManager["underplating"];
var tileDefinition = tileDefinitionManager["UnderPlating"];
var tile = new Tile(tileDefinition.TileId);
var coordinates = grid.ToCoordinates();
@@ -128,7 +128,7 @@ namespace Content.IntegrationTests.Tests
grid = mapManager.CreateGrid(mapId);
var tileDefinition = tileDefinitionManager["underplating"];
var tileDefinition = tileDefinitionManager["UnderPlating"];
var tile = new Tile(tileDefinition.TileId);
var coordinates = grid.ToCoordinates();
@@ -223,7 +223,7 @@ namespace Content.IntegrationTests.Tests
grid = mapManager.CreateGrid(mapId);
var tileDefinition = tileDefinitionManager["underplating"];
var tileDefinition = tileDefinitionManager["UnderPlating"];
var tile = new Tile(tileDefinition.TileId);
grid.SetTile(Vector2i.Zero, tile);

View File

@@ -106,7 +106,7 @@ namespace Content.IntegrationTests.Tests.Fluids
sGridId = sGrid.GridEntityId;
metaSystem.SetEntityPaused(sGridId, true); // See https://github.com/space-wizards/RobustToolbox/issues/1444
var tileDefinition = sTileDefinitionManager["underplating"];
var tileDefinition = sTileDefinitionManager["UnderPlating"];
var tile = new Tile(tileDefinition.TileId);
sCoordinates = sGrid.ToCoordinates();

View File

@@ -15,7 +15,7 @@
<ItemGroup>
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Maps;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Map;
@@ -41,10 +42,15 @@ namespace Content.MapRenderer.Painters
grid.GetAllTiles().AsParallel().ForAll(tile =>
{
var sprite = _sTileDefinitionManager[tile.Tile.TypeId].Sprite;
if (sprite == null)
return;
var x = (int) (tile.X + xOffset);
var y = (int) (tile.Y + yOffset);
var sprite = _sTileDefinitionManager[tile.Tile.TypeId].SpriteName;
var image = images[sprite][tile.Tile.Variant];
var path = sprite.ToString();
var image = images[path][tile.Tile.Variant];
gridCanvas.Mutate(o => o.DrawImage(image, new Point(x * tileSize, y * tileSize), 1));
@@ -66,15 +72,15 @@ namespace Content.MapRenderer.Painters
foreach (var definition in tileDefinitionManager)
{
var sprite = definition.SpriteName;
images[sprite] = new List<Image>(definition.Variants);
var sprite = definition.Sprite;
if (string.IsNullOrEmpty(sprite))
{
if (sprite == null)
continue;
}
using var stream = resourceCache.ContentFileRead($"{TilesPath}{sprite}.png");
var path = sprite.ToString();
images[path] = new List<Image>(definition.Variants);
using var stream = resourceCache.ContentFileRead(path);
Image tileSheet = Image.Load<Rgba32>(stream);
if (tileSheet.Width != tileSize * definition.Variants || tileSheet.Height != tileSize)
@@ -85,7 +91,7 @@ namespace Content.MapRenderer.Painters
for (var i = 0; i < definition.Variants; i++)
{
var tileImage = tileSheet.Clone(o => o.Crop(new Rectangle(tileSize * i, 0, 32, 32)));
images[sprite].Add(tileImage);
images[path].Add(tileImage);
}
}

View File

@@ -1,10 +1,12 @@
using System.Linq;
using Content.Server.Access.Systems;
using Content.Server.Administration.Logs;
using Content.Server.Station.Systems;
using Content.Server.StationRecords;
using Content.Server.UserInterface;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.StationRecords;
using Content.Server.Administration.Logs;
using Content.Shared.Database;
using Robust.Server.GameObjects;
@@ -91,6 +93,25 @@ namespace Content.Server.Access.Components
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{_entities.ToPrettyString(player):player} has modified {_entities.ToPrettyString(targetIdEntity):entity} with the following accesses: [{string.Join(", ", newAccessList)}]");
UpdateStationRecord(targetIdEntity, newFullName, newJobTitle);
}
private void UpdateStationRecord(EntityUid idCard, string newFullName, string newJobTitle)
{
var station = EntitySystem.Get<StationSystem>().GetOwningStation(Owner);
var recordSystem = EntitySystem.Get<StationRecordsSystem>();
if (station == null
|| !_entities.TryGetComponent(idCard, out StationRecordKeyStorageComponent? keyStorage)
|| keyStorage.Key == null
|| !recordSystem.TryGetRecord(station.Value, keyStorage.Key.Value, out GeneralStationRecord? record))
{
return;
}
record.Name = newFullName;
record.JobTitle = newJobTitle;
recordSystem.Synchronize(station.Value);
}
public void UpdateUserInterface()

View File

@@ -188,8 +188,14 @@ namespace Content.Server.Atmos.Miasma
private void OnExamined(EntityUid uid, PerishableComponent component, ExaminedEvent args)
{
if (component.Rotting)
args.PushMarkup(Loc.GetString("miasma-rotting"));
if (!component.Rotting)
return;
var stage = component.DeathAccumulator / component.RotAfter.TotalSeconds;
var description = stage switch {
>= 3 => "miasma-extremely-bloated",
>= 2 => "miasma-bloated",
_ => "miasma-rotting"};
args.PushMarkup(Loc.GetString(description));
}
/// Containers
@@ -204,7 +210,7 @@ namespace Content.Server.Atmos.Miasma
}
private void OnEntRemoved(EntityUid uid, AntiRottingContainerComponent component, EntRemovedFromContainerMessage args)
{
if (TryComp<PerishableComponent>(args.Entity, out var perishable))
if (TryComp<PerishableComponent>(args.Entity, out var perishable) && !Terminating(uid))
{
ModifyPreservationSource(args.Entity, false);
ToggleDecomposition(args.Entity, true, perishable);

View File

@@ -139,7 +139,7 @@ namespace Content.Server.Atmos.Monitor.Systems
coords = coords.Offset(rotPos);
transform.Coordinates = coords;
appearance.SetData("offset", - new Vector2i(0, -1));
appearance.SetData(AtmosMonitorVisuals.Offset, - new Vector2i(0, -1));
transform.Anchored = true;
}
@@ -192,7 +192,7 @@ namespace Content.Server.Atmos.Monitor.Systems
if (component.DisplayMaxAlarmInNet)
{
if (EntityManager.TryGetComponent(component.Owner, out AppearanceComponent? appearanceComponent))
appearanceComponent.SetData("alarmType", component.HighestAlarmInNetwork);
appearanceComponent.SetData(AtmosMonitorVisuals.AlarmType, component.HighestAlarmInNetwork);
if (component.HighestAlarmInNetwork == AtmosMonitorAlarmType.Danger) PlayAlertSound(uid, component);
}
@@ -227,10 +227,7 @@ namespace Content.Server.Atmos.Monitor.Systems
}
if (EntityManager.TryGetComponent(component.Owner, out AppearanceComponent? appearanceComponent))
{
appearanceComponent.SetData("powered", args.Powered);
appearanceComponent.SetData("alarmType", component.LastAlarmState);
}
appearanceComponent.SetData(AtmosMonitorVisuals.AlarmType, component.LastAlarmState);
}
private void OnFireEvent(EntityUid uid, AtmosMonitorComponent component, ref TileFireEvent args)
@@ -341,7 +338,7 @@ namespace Content.Server.Atmos.Monitor.Systems
if (!Resolve(uid, ref monitor)) return;
monitor.LastAlarmState = state;
if (EntityManager.TryGetComponent(monitor.Owner, out AppearanceComponent? appearanceComponent))
appearanceComponent.SetData("alarmType", monitor.LastAlarmState);
appearanceComponent.SetData(AtmosMonitorVisuals.AlarmType, monitor.LastAlarmState);
BroadcastAlertPacket(monitor, alarms);

View File

@@ -0,0 +1,35 @@
using Content.Shared.Atmos;
namespace Content.Server.Atmos.Piping.Trinary.Components
{
[RegisterComponent]
public sealed class PressureControlledValveComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("inlet")]
public string InletName { get; set; } = "inlet";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("control")]
public string ControlName { get; set; } = "control";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("outlet")]
public string OutletName { get; set; } = "outlet";
[ViewVariables(VVAccess.ReadOnly)]
[DataField("enabled")]
public bool Enabled { get; set; } = false;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("gain")]
public float Gain { get; set; } = 10;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("threshold")]
public float Threshold { get; set; } = Atmospherics.OneAtmosphere;
[DataField("maxTransferRate")]
public float MaxTransferRate { get; set; } = Atmospherics.MaxTransferRate;
}
}

View File

@@ -0,0 +1,96 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Trinary.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos.Piping;
using Content.Shared.Audio;
using JetBrains.Annotations;
using Robust.Shared.Timing;
namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
{
[UsedImplicitly]
public sealed class PressureControlledValveSystem : EntitySystem
{
[Dependency] private IGameTiming _gameTiming = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PressureControlledValveComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<PressureControlledValveComponent, AtmosDeviceUpdateEvent>(OnUpdate);
SubscribeLocalEvent<PressureControlledValveComponent, AtmosDeviceDisabledEvent>(OnFilterLeaveAtmosphere);
}
private void OnInit(EntityUid uid, PressureControlledValveComponent comp, ComponentInit args)
{
UpdateAppearance(uid, comp);
}
private void OnUpdate(EntityUid uid, PressureControlledValveComponent comp, AtmosDeviceUpdateEvent args)
{
if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer)
|| !EntityManager.TryGetComponent(uid, out AtmosDeviceComponent? device)
|| !nodeContainer.TryGetNode(comp.InletName, out PipeNode? inletNode)
|| !nodeContainer.TryGetNode(comp.ControlName, out PipeNode? controlNode)
|| !nodeContainer.TryGetNode(comp.OutletName, out PipeNode? outletNode))
{
_ambientSoundSystem.SetAmbience(comp.Owner, false);
comp.Enabled = false;
return;
}
// If output is higher than input, flip input/output to enable bidirectional flow.
if (outletNode.Air.Pressure > inletNode.Air.Pressure)
{
PipeNode temp = outletNode;
outletNode = inletNode;
inletNode = temp;
}
float control = (controlNode.Air.Pressure - outletNode.Air.Pressure) - comp.Threshold;
float transferRate;
if (control < 0)
{
comp.Enabled = false;
transferRate = 0;
}
else
{
comp.Enabled = true;
transferRate = Math.Min(control * comp.Gain, comp.MaxTransferRate);
}
UpdateAppearance(uid, comp);
// We multiply the transfer rate in L/s by the seconds passed since the last process to get the liters.
var transferRatio = (float)(transferRate * (_gameTiming.CurTime - device.LastProcess).TotalSeconds) / inletNode.Air.Volume;
if (transferRatio <= 0)
{
_ambientSoundSystem.SetAmbience(comp.Owner, false);
return;
}
_ambientSoundSystem.SetAmbience(comp.Owner, true);
var removed = inletNode.Air.RemoveRatio(transferRatio);
_atmosphereSystem.Merge(outletNode.Air, removed);
}
private void OnFilterLeaveAtmosphere(EntityUid uid, PressureControlledValveComponent comp, AtmosDeviceDisabledEvent args)
{
comp.Enabled = false;
UpdateAppearance(uid, comp);
_ambientSoundSystem.SetAmbience(comp.Owner, false);
}
private void UpdateAppearance(EntityUid uid, PressureControlledValveComponent? comp = null, AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref comp, ref appearance, false))
return;
appearance.SetData(FilterVisuals.Enabled, comp.Enabled);
}
}
}

View File

@@ -3,6 +3,7 @@ using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.Cargo.Systems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
@@ -23,6 +24,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
public override void Initialize()
@@ -36,6 +38,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
SubscribeLocalEvent<GasCanisterComponent, InteractUsingEvent>(OnCanisterInteractUsing);
SubscribeLocalEvent<GasCanisterComponent, EntInsertedIntoContainerMessage>(OnCanisterContainerInserted);
SubscribeLocalEvent<GasCanisterComponent, EntRemovedFromContainerMessage>(OnCanisterContainerRemoved);
SubscribeLocalEvent<GasCanisterComponent, PriceCalculationEvent>(CalculateCanisterPrice);
// Bound UI subscriptions
SubscribeLocalEvent<GasCanisterComponent, GasCanisterHoldingTankEjectMessage>(OnHoldingTankEjectMessage);
SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleasePressureMessage>(OnCanisterChangeReleasePressure);
@@ -292,5 +295,25 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
_atmosphereSystem.Merge(containerAir, buffer);
containerAir.Multiply(containerAir.Volume / buffer.Volume);
}
private void CalculateCanisterPrice(EntityUid uid, GasCanisterComponent component, ref PriceCalculationEvent args)
{
float basePrice = 0; // moles of gas * price/mole
float totalMoles = 0; // total number of moles in can
float maxComponent = 0; // moles of the dominant gas
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
basePrice += component.Air.Moles[i] * _atmosphereSystem.GetGas(i).PricePerMole;
totalMoles += component.Air.Moles[i];
maxComponent = Math.Max(maxComponent, component.Air.Moles[i]);
}
// Pay more for gas canisters that are more pure
float purity = 1;
if (totalMoles > 0) {
purity = maxComponent / totalMoles;
}
args.Price += basePrice * purity;
}
}
}

View File

@@ -0,0 +1,8 @@
using Content.Shared.Audio;
namespace Content.Server.Audio;
public sealed class ContentAudioSystem : SharedContentAudioSystem
{
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Shared.Atmos;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
@@ -63,7 +64,7 @@ public struct SeedChemQuantity
// TODO reduce the number of friends to a reasonable level. Requires ECS-ing things like plant holder component.
[Virtual, DataDefinition]
[Access(typeof(BotanySystem), typeof(PlantHolderSystem), typeof(SeedExtractorSystem), typeof(PlantHolderComponent))]
[Access(typeof(BotanySystem), typeof(PlantHolderSystem), typeof(SeedExtractorSystem), typeof(PlantHolderComponent), typeof(ReagentEffect))]
public class SeedData
{
#region Tracking

View File

@@ -115,7 +115,7 @@ namespace Content.Server.Buckle.Components
// Update the visuals of the strap object
if (IoCManager.Resolve<IEntityManager>().TryGetComponent<AppearanceComponent>(Owner, out var appearance))
{
appearance.SetData("StrapState", true);
appearance.SetData(StrapVisuals.State, true);
}
Dirty();
@@ -133,7 +133,7 @@ namespace Content.Server.Buckle.Components
{
if (IoCManager.Resolve<IEntityManager>().TryGetComponent<AppearanceComponent>(Owner, out var appearance))
{
appearance.SetData("StrapState", false);
appearance.SetData(StrapVisuals.State, false);
}
_occupiedSize -= buckle.Size;

View File

@@ -266,6 +266,7 @@ public sealed partial class ChatSystem : SharedChatSystem
("entityName", Name(source)));
SendInVoiceRange(ChatChannel.Local, message, messageWrap, source, hideChat);
_listener.PingListeners(source, message, null);
var ev = new EntitySpokeEvent(message);
RaiseLocalEvent(source, ev);

View File

@@ -18,14 +18,14 @@ namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism
var random = IoCManager.Resolve<IRobustRandom>();
if (plantHolderComp.Seed.Potency < 100 && random.Prob(0.1f))
if (plantHolderComp.Seed.Potency < 100)
{
plantHolderComp.EnsureUniqueSeed();
plantHolderComp.Seed.Potency++;
plantHolderComp.Seed.Potency = Math.Min(plantHolderComp.Seed.Potency + 3, 100);
}
if (plantHolderComp.Seed.Yield > 1 && random.Prob(0.1f))
else if (plantHolderComp.Seed.Yield > 1 && random.Prob(0.1f))
{
// Too much of a good thing reduces yield
plantHolderComp.EnsureUniqueSeed();
plantHolderComp.Seed.Yield--;
}

View File

@@ -131,7 +131,7 @@ The ban reason is: ""{ban.Reason}""
&& await _db.GetWhitelistStatusAsync(userId) == false
&& adminData is null)
{
return (ConnectionDenyReason.Whitelist, Loc.GetString("whitelist-not-whitelisted"), null);
return (ConnectionDenyReason.Whitelist, Loc.GetString(_cfg.GetCVar(CCVars.WhitelistReason)), null);
}
return null;

View File

@@ -1,5 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.Coordinates.Helpers;
using Content.Server.Popups;
@@ -11,6 +10,7 @@ using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Pulling.Components;
using Content.Shared.Tools.Components;
using Robust.Shared.Map;
using Robust.Shared.Player;
namespace Content.Server.Construction
@@ -18,6 +18,7 @@ namespace Content.Server.Construction
public sealed class AnchorableSystem : SharedAnchorableSystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly ToolSystem _toolSystem = default!;
[Dependency] private readonly PullingSystem _pullingSystem = default!;
@@ -71,6 +72,12 @@ namespace Content.Server.Construction
{
component.CancelToken = null;
var xform = Transform(uid);
if (TryComp<PhysicsComponent>(uid, out var anchorBody) &&
!TileFree(xform.Coordinates, anchorBody))
{
_popup.PopupEntity(Loc.GetString("anchorable-occupied"), uid, Filter.Entities(args.User));
return;
}
// Snap rotation to cardinal (multiple of 90)
var rot = xform.LocalRotation;
@@ -81,8 +88,9 @@ namespace Content.Server.Construction
_pullingSystem.TryStopPull(pullable);
}
// TODO: Anchoring snaps rn anyway!
if (component.Snap)
xform.Coordinates = xform.Coordinates.SnapToGrid();
xform.Coordinates = xform.Coordinates.SnapToGrid(EntityManager, _mapManager);
RaiseLocalEvent(uid, new BeforeAnchoredEvent(args.User, args.Using), false);
xform.Anchored = true;
@@ -97,6 +105,34 @@ namespace Content.Server.Construction
);
}
private bool TileFree(EntityCoordinates coordinates, PhysicsComponent anchorBody)
{
// Probably ignore CanCollide on the anchoring body?
var gridUid = coordinates.GetGridUid(EntityManager);
if (!_mapManager.TryGetGrid(gridUid, out var grid))
return false;
var tileIndices = grid.TileIndicesFor(coordinates);
var enumerator = grid.GetAnchoredEntitiesEnumerator(tileIndices);
var bodyQuery = GetEntityQuery<PhysicsComponent>();
while (enumerator.MoveNext(out var ent))
{
if (!bodyQuery.TryGetComponent(ent, out var body) ||
!body.CanCollide)
continue;
if ((body.CollisionMask & anchorBody.CollisionLayer) != 0x0 ||
(body.CollisionLayer & anchorBody.CollisionMask) != 0x0)
{
return false;
}
}
return true;
}
/// <summary>
/// Checks if a tool can change the anchored status.
/// </summary>
@@ -135,14 +171,24 @@ namespace Content.Server.Construction
SharedPullableComponent? pullable = null,
ToolComponent? usingTool = null)
{
if (!Resolve(uid, ref anchorable, ref transform)) return;
if (!Resolve(uid, ref anchorable, ref transform))
return;
// Optional resolves.
Resolve(uid, ref pullable, false);
if (!Resolve(usingUid, ref usingTool)) return;
if (!Resolve(usingUid, ref usingTool))
return;
if (!Valid(uid, userUid, usingUid, true, anchorable, usingTool)) return;
if (!Valid(uid, userUid, usingUid, true, anchorable, usingTool))
return;
if (TryComp<PhysicsComponent>(uid, out var anchorBody) &&
!TileFree(transform.Coordinates, anchorBody))
{
_popup.PopupEntity(Loc.GetString("anchorable-occupied"), uid, Filter.Entities(userUid));
return;
}
anchorable.CancelToken = new CancellationTokenSource();

View File

@@ -17,7 +17,7 @@ namespace Content.Server.Construction.Commands
public string Description => "Puts an underplating tile below every wall on a grid.";
public string Help => $"Usage: {Command} <gridId> | {Command}";
public const string TilePrototypeID = "plating";
public const string TilePrototypeID = "Plating";
public const string WallTag = "Wall";
public void Execute(IConsoleShell shell, string argStr, string[] args)

View File

@@ -1,4 +1,4 @@
using Content.Shared.Construction;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization;
@@ -22,10 +22,6 @@ namespace Content.Server.Construction.Completions
{
appearance.SetData(@enum, Data);
}
else
{
appearance.SetData(Key, Data);
}
}
}
}

View File

@@ -23,7 +23,7 @@ namespace Content.Server.Contests
/// </summary>
public float MassContest(EntityUid roller, EntityUid target, PhysicsComponent? rollerPhysics = null, PhysicsComponent? targetPhysics = null)
{
if (!Resolve(roller, ref rollerPhysics) || !Resolve(target, ref targetPhysics))
if (!Resolve(roller, ref rollerPhysics, false) || !Resolve(target, ref targetPhysics, false))
return 1f;
if (rollerPhysics == null || targetPhysics == null)
@@ -43,7 +43,7 @@ namespace Content.Server.Contests
/// <summary>
public float DamageContest(EntityUid roller, EntityUid target, DamageableComponent? rollerDamage = null, DamageableComponent? targetDamage = null)
{
if (!Resolve(roller, ref rollerDamage) || !Resolve(target, ref targetDamage))
if (!Resolve(roller, ref rollerDamage, false) || !Resolve(target, ref targetDamage, false))
return 1f;
if (rollerDamage == null || targetDamage == null)

View File

@@ -0,0 +1,47 @@
using Content.Server.EUI;
using Content.Shared.CrewManifest;
using Content.Shared.Eui;
namespace Content.Server.CrewManifest;
public sealed class CrewManifestEui : BaseEui
{
private readonly CrewManifestSystem _crewManifest;
/// <summary>
/// Station this EUI instance is currently tracking.
/// </summary>
private readonly EntityUid _station;
/// <summary>
/// Current owner of this UI, if it has one. This is
/// to ensure that if a BUI is closed, the EUIs related
/// to the BUI are closed as well.
/// </summary>
public readonly EntityUid? Owner;
public CrewManifestEui(EntityUid station, EntityUid? owner, CrewManifestSystem crewManifestSystem)
{
_station = station;
Owner = owner;
_crewManifest = crewManifestSystem;
}
public override CrewManifestEuiState GetNewState()
{
var (name, entries) = _crewManifest.GetCrewManifest(_station);
return new(name, entries);
}
public override void HandleMessage(EuiMessageBase msg)
{
base.HandleMessage(msg);
switch (msg)
{
case CrewManifestEuiClosed:
_crewManifest.CloseEui(_station, Player, Owner);
break;
}
}
}

View File

@@ -0,0 +1,263 @@
using Content.Server.Administration;
using Content.Server.EUI;
using Content.Server.GameTicking;
using Content.Server.Station.Systems;
using Content.Server.StationRecords;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.CrewManifest;
using Content.Shared.GameTicking;
using Content.Shared.Roles;
using Content.Shared.StationRecords;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
namespace Content.Server.CrewManifest;
public sealed class CrewManifestSystem : EntitySystem
{
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly StationRecordsSystem _recordsSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly EuiManager _euiManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
/// <summary>
/// Cached crew manifest entries. The alternative is to outright
/// rebuild the crew manifest every time the state is requested:
/// this is inefficient.
/// </summary>
private readonly Dictionary<EntityUid, CrewManifestEntries> _cachedEntries = new();
private readonly Dictionary<EntityUid, Dictionary<IPlayerSession, CrewManifestEui>> _openEuis = new();
public override void Initialize()
{
SubscribeLocalEvent<AfterGeneralRecordCreatedEvent>(AfterGeneralRecordCreated);
SubscribeLocalEvent<RecordModifiedEvent>(OnRecordModified);
SubscribeLocalEvent<CrewManifestViewerComponent, BoundUIClosedEvent>(OnBoundUiClose);
SubscribeLocalEvent<CrewManifestViewerComponent, CrewManifestOpenUiMessage>(OpenEuiFromBui);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
SubscribeNetworkEvent<RequestCrewManifestMessage>(OnRequestCrewManifest);
}
private void OnRoundRestart(RoundRestartCleanupEvent ev)
{
foreach (var (_, euis) in _openEuis)
{
foreach (var (_, eui) in euis)
{
eui.Close();
}
}
_openEuis.Clear();
_cachedEntries.Clear();
}
private void OnRequestCrewManifest(RequestCrewManifestMessage message, EntitySessionEventArgs args)
{
if (args.SenderSession is not IPlayerSession sessionCast
|| !_configManager.GetCVar(CCVars.CrewManifestWithoutEntity))
{
return;
}
OpenEui(message.Id, sessionCast);
}
// Not a big fan of this one. Rebuilds the crew manifest every time
// somebody spawns in, meaning that at round start, it rebuilds the crew manifest
// wrt the amount of players readied up.
private void AfterGeneralRecordCreated(AfterGeneralRecordCreatedEvent ev)
{
BuildCrewManifest(ev.Key.OriginStation);
UpdateEuis(ev.Key.OriginStation);
}
private void OnRecordModified(RecordModifiedEvent ev)
{
BuildCrewManifest(ev.Key.OriginStation);
UpdateEuis(ev.Key.OriginStation);
}
private void OnBoundUiClose(EntityUid uid, CrewManifestViewerComponent component, BoundUIClosedEvent ev)
{
var owningStation = _stationSystem.GetOwningStation(uid);
if (owningStation == null || ev.Session is not IPlayerSession sessionCast)
{
return;
}
CloseEui(owningStation.Value, sessionCast, uid);
}
/// <summary>
/// Gets the crew manifest for a given station, along with the name of the station.
/// </summary>
/// <param name="station">Entity uid of the station.</param>
/// <returns>The name and crew manifest entries (unordered) of the station.</returns>
public (string name, CrewManifestEntries? entries) GetCrewManifest(EntityUid station)
{
var valid = _cachedEntries.TryGetValue(station, out var manifest);
return (valid ? MetaData(station).EntityName : string.Empty, valid ? manifest : null);
}
private void UpdateEuis(EntityUid station)
{
if (_openEuis.TryGetValue(station, out var euis))
{
foreach (var eui in euis.Values)
{
eui.StateDirty();
}
}
}
private void OpenEuiFromBui(EntityUid uid, CrewManifestViewerComponent component, CrewManifestOpenUiMessage msg)
{
var owningStation = _stationSystem.GetOwningStation(uid);
if (owningStation == null || msg.Session is not IPlayerSession sessionCast)
{
return;
}
if (!_configManager.GetCVar(CCVars.CrewManifestUnsecure) && component.Unsecure)
{
return;
}
OpenEui(owningStation.Value, sessionCast, uid);
}
/// <summary>
/// Opens a crew manifest EUI for a given player.
/// </summary>
/// <param name="station">Station that we're displaying the crew manifest for.</param>
/// <param name="session">The player's session.</param>
/// <param name="owner">If this EUI should be 'owned' by an entity.</param>
public void OpenEui(EntityUid station, IPlayerSession session, EntityUid? owner = null)
{
if (!HasComp<StationRecordsComponent>(station))
{
return;
}
if (!_openEuis.TryGetValue(station, out var euis))
{
euis = new();
_openEuis.Add(station, euis);
}
if (euis.ContainsKey(session))
{
return;
}
var eui = new CrewManifestEui(station, owner, this);
euis.Add(session, eui);
_euiManager.OpenEui(eui, session);
eui.StateDirty();
}
/// <summary>
/// Closes an EUI for a given player.
/// </summary>
/// <param name="station">Station that we're displaying the crew manifest for.</param>
/// <param name="session">The player's session.</param>
/// <param name="owner">The owner of this EUI, if there was one.</param>
public void CloseEui(EntityUid station, IPlayerSession session, EntityUid? owner = null)
{
if (!HasComp<StationRecordsComponent>(station))
{
return;
}
if (!_openEuis.TryGetValue(station, out var euis)
|| !euis.TryGetValue(session, out var eui))
{
return;
}
if (eui.Owner == owner)
{
eui.Close();
euis.Remove(session);
}
if (euis.Count == 0)
{
_openEuis.Remove(station);
}
}
/// <summary>
/// Builds the crew manifest for a station. Stores it in the cache afterwards.
/// </summary>
/// <param name="station"></param>
private void BuildCrewManifest(EntityUid station)
{
var iter = _recordsSystem.GetRecordsOfType<GeneralStationRecord>(station);
var entries = new CrewManifestEntries();
foreach (var recordObject in iter)
{
var record = recordObject.Item2;
var entry = new CrewManifestEntry(record.Name, record.JobTitle, record.JobIcon, record.JobPrototype);
entries.Entries.Add(entry);
}
if (_cachedEntries.ContainsKey(station))
{
_cachedEntries[station] = entries;
}
else
{
_cachedEntries.Add(station, entries);
}
}
}
[AdminCommand(AdminFlags.Admin)]
public sealed class CrewManifestCommand : IConsoleCommand
{
public string Command => "crewmanifest";
public string Description => "Opens the crew manifest for the given station.";
public string Help => $"Usage: {Command} <entity uid>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteLine($"Invalid argument count.\n{Help}");
return;
}
var entMan = IoCManager.Resolve<IEntityManager>();
if (!EntityUid.TryParse(args[0], out var uid))
{
shell.WriteLine($"{args[0]} is not a valid entity UID.");
return;
}
if (shell.Player == null || shell.Player is not IPlayerSession session)
{
shell.WriteLine("You must run this from a client.");
return;
}
var crewManifestSystem = entMan.EntitySysManager.GetEntitySystem<CrewManifestSystem>();
crewManifestSystem.OpenEui(uid, session);
}
}

View File

@@ -0,0 +1,12 @@
namespace Content.Server.CrewManifest;
[RegisterComponent]
public sealed class CrewManifestViewerComponent : Component
{
/// <summary>
/// If this manifest viewer is unsecure or not. If it is,
/// CCVars.CrewManifestUnsecure being false will
/// not allow this entity to be processed by CrewManifestSystem.
/// </summary>
[DataField("unsecure")] public bool Unsecure;
}

View File

@@ -32,11 +32,42 @@ namespace Content.Server.Dragon
[DataField("devourAction")]
public EntityTargetAction? DevourAction;
[DataField("spawnActionId", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string SpawnActionId = "DragonSpawn";
/// <summary>
/// If we have active rifts.
/// </summary>
[ViewVariables, DataField("rifts")]
public List<EntityUid> Rifts = new();
[DataField("spawnAction")]
public InstantAction? SpawnAction;
public bool Weakened => WeakenedAccumulator > 0f;
/// <summary>
/// When any rift is destroyed how long is the dragon weakened for
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("weakenedDuration")]
public float WeakenedDuration = 120f;
/// <summary>
/// Has a rift been destroyed and the dragon in a temporary weakened state?
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("weakenedAccumulator")]
public float WeakenedAccumulator = 0f;
[ViewVariables(VVAccess.ReadWrite), DataField("riftAccumulator")]
public float RiftAccumulator = 0f;
/// <summary>
/// Maximum time the dragon can go without spawning a rift before they die.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("maxAccumulator")] public float RiftMaxAccumulator = 300f;
/// <summary>
/// Spawns a rift which can summon more mobs.
/// </summary>
[ViewVariables, DataField("spawnRiftAction")]
public InstantAction? SpawnRiftAction;
[ViewVariables(VVAccess.ReadWrite), DataField("riftPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string RiftPrototype = "CarpRift";
/// <summary>
/// The amount of time it takes to devour something
@@ -48,14 +79,7 @@ namespace Content.Server.Dragon
public float StructureDevourTime = 10f;
[DataField("devourTime")]
public float DevourTime = 2f;
[DataField("spawnCount")] public int SpawnsLeft = 2;
[DataField("maxSpawnCount")] public int MaxSpawns = 2;
[DataField("spawnProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? SpawnPrototype = "MobCarpDragon";
public float DevourTime = 3f;
[ViewVariables(VVAccess.ReadWrite), DataField("soundDeath")]
public SoundSpecifier? SoundDeath = new SoundPathSpecifier("/Audio/Animals/space_dragon_roar.ogg");
@@ -76,7 +100,7 @@ namespace Content.Server.Dragon
public SoundSpecifier? SoundRoar =
new SoundPathSpecifier("/Audio/Animals/space_dragon_roar.ogg")
{
Params = AudioParams.Default.WithVolume(-3f),
Params = AudioParams.Default.WithVolume(3f),
};
public CancellationTokenSource? CancelToken;
@@ -103,5 +127,5 @@ namespace Content.Server.Dragon
public sealed class DragonDevourActionEvent : EntityTargetActionEvent {}
public sealed class DragonSpawnActionEvent : InstantActionEvent {}
public sealed class DragonSpawnRiftActionEvent : InstantActionEvent {}
}

View File

@@ -0,0 +1,40 @@
using Content.Shared.Dragon;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Dragon;
[RegisterComponent]
public sealed class DragonRiftComponent : SharedDragonRiftComponent
{
/// <summary>
/// Dragon that spawned this rift.
/// </summary>
[ViewVariables, DataField("dragon")] public EntityUid Dragon;
/// <summary>
/// How long the rift has been active.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("accumulator")]
public float Accumulator = 0f;
/// <summary>
/// The maximum amount we can accumulate before becoming impervious.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("maxAccumuluator")] public float MaxAccumulator = 300f;
/// <summary>
/// Accumulation of the spawn timer.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("spawnAccumulator")]
public float SpawnAccumulator = 30f;
/// <summary>
/// How long it takes for a new spawn to be added.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("spawnCooldown")]
public float SpawnCooldown = 30f;
[ViewVariables(VVAccess.ReadWrite), DataField("spawn", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string SpawnPrototype = "MobCarpDragon";
}

View File

@@ -0,0 +1,67 @@
using System.Linq;
using Content.Server.GameTicking;
using Content.Server.StationEvents.Components;
using Content.Shared.Dragon;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
namespace Content.Server.Dragon;
public sealed partial class DragonSystem
{
public override string Prototype => "Dragon";
private int RiftsMet(DragonComponent component)
{
var finished = 0;
foreach (var rift in component.Rifts)
{
if (!TryComp<DragonRiftComponent>(rift, out var drift) ||
drift.State != DragonRiftState.Finished)
continue;
finished++;
}
return finished;
}
public override void Started()
{
var spawnLocations = EntityManager.EntityQuery<IMapGridComponent, TransformComponent>().ToList();
if (spawnLocations.Count == 0)
return;
var location = _random.Pick(spawnLocations);
Spawn("MobDragon", location.Item2.MapPosition);
}
public override void Ended()
{
return;
}
private void OnRiftRoundEnd(RoundEndTextAppendEvent args)
{
if (!RuleAdded)
return;
args.AddLine(Loc.GetString("dragon-round-end-summary"));
foreach (var dragon in EntityQuery<DragonComponent>(true))
{
var met = RiftsMet(dragon);
if (TryComp<ActorComponent>(dragon.Owner, out var actor))
{
args.AddLine(Loc.GetString("dragon-round-end-dragon-player", ("name", dragon.Owner), ("count", met), ("player", actor.PlayerSession)));
}
else
{
args.AddLine(Loc.GetString("dragon-round-end-dragon", ("name", dragon.Owner), ("count", met)));
}
}
}
}

View File

@@ -9,30 +9,263 @@ using Content.Shared.MobState.Components;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using System.Threading;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Shared.Damage;
using Content.Shared.Dragon;
using Content.Shared.Examine;
using Content.Shared.Maps;
using Content.Shared.Movement.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Server.Dragon
{
public sealed class DragonSystem : EntitySystem
public sealed partial class DragonSystem : GameRuleSystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDef = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movement = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
/// <summary>
/// Minimum distance between 2 rifts allowed.
/// </summary>
private const int RiftRange = 15;
/// <summary>
/// Radius of tiles
/// </summary>
private const int RiftTileRadius = 2;
private const int RiftsAllowed = 3;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DragonComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<DragonComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<DragonComponent, DragonDevourComplete>(OnDragonDevourComplete);
SubscribeLocalEvent<DragonComponent, DragonDevourActionEvent>(OnDevourAction);
SubscribeLocalEvent<DragonComponent, DragonSpawnActionEvent>(OnDragonSpawnAction);
SubscribeLocalEvent<DragonComponent, DragonSpawnRiftActionEvent>(OnDragonRift);
SubscribeLocalEvent<DragonComponent, RefreshMovementSpeedModifiersEvent>(OnDragonMove);
SubscribeLocalEvent<DragonComponent, DragonStructureDevourComplete>(OnDragonStructureDevourComplete);
SubscribeLocalEvent<DragonComponent, DragonDevourCancelledEvent>(OnDragonDevourCancelled);
SubscribeLocalEvent<DragonComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<DragonRiftComponent, ComponentShutdown>(OnRiftShutdown);
SubscribeLocalEvent<DragonRiftComponent, ComponentGetState>(OnRiftGetState);
SubscribeLocalEvent<DragonRiftComponent, AnchorStateChangedEvent>(OnAnchorChange);
SubscribeLocalEvent<DragonRiftComponent, ExaminedEvent>(OnRiftExamined);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRiftRoundEnd);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var comp in EntityQuery<DragonComponent>())
{
if (comp.WeakenedAccumulator > 0f)
{
comp.WeakenedAccumulator -= frameTime;
// No longer weakened.
if (comp.WeakenedAccumulator < 0f)
{
comp.WeakenedAccumulator = 0f;
_movement.RefreshMovementSpeedModifiers(comp.Owner);
}
}
// At max rifts
if (comp.Rifts.Count >= RiftsAllowed)
{
continue;
}
// If there's an active rift don't accumulate.
if (comp.Rifts.Count > 0)
{
var lastRift = comp.Rifts[^1];
if (TryComp<DragonRiftComponent>(lastRift, out var rift) && rift.State != DragonRiftState.Finished)
{
comp.RiftAccumulator = 0f;
continue;
}
}
comp.RiftAccumulator += frameTime;
// Delete it, naughty dragon!
if (comp.RiftAccumulator >= comp.RiftMaxAccumulator)
{
Roar(comp);
QueueDel(comp.Owner);
}
}
foreach (var comp in EntityQuery<DragonRiftComponent>())
{
if (comp.State != DragonRiftState.Finished && comp.Accumulator >= comp.MaxAccumulator)
{
// TODO: When we get autocall you can buff if the rift finishes / 3 rifts are up
// for now they just keep 3 rifts up.
comp.Accumulator = comp.MaxAccumulator;
RemComp<DamageableComponent>(comp.Owner);
comp.State = DragonRiftState.Finished;
Dirty(comp);
}
else if (comp.State != DragonRiftState.Finished)
{
comp.Accumulator += frameTime;
}
comp.SpawnAccumulator += frameTime;
if (comp.State < DragonRiftState.AlmostFinished && comp.Accumulator > comp.MaxAccumulator / 2f)
{
comp.State = DragonRiftState.AlmostFinished;
Dirty(comp);
var location = Transform(comp.Owner).LocalPosition;
_chat.DispatchGlobalAnnouncement(Loc.GetString("carp-rift-warning", ("location", location)), playSound: false, colorOverride: Color.Red);
_audioSystem.PlayGlobal("/Audio/Misc/notice1.ogg", Filter.Broadcast());
}
if (comp.SpawnAccumulator > comp.SpawnCooldown)
{
comp.SpawnAccumulator -= comp.SpawnCooldown;
Spawn(comp.SpawnPrototype, Transform(comp.Owner).MapPosition);
// TODO: When NPC refactor make it guard the rift.
}
}
}
#region Rift
private void OnRiftExamined(EntityUid uid, DragonRiftComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("carp-rift-examine", ("percentage", MathF.Round(component.Accumulator / component.MaxAccumulator * 100))));
}
private void OnAnchorChange(EntityUid uid, DragonRiftComponent component, ref AnchorStateChangedEvent args)
{
if (!args.Anchored && component.State == DragonRiftState.Charging)
{
QueueDel(uid);
}
}
private void OnRiftShutdown(EntityUid uid, DragonRiftComponent component, ComponentShutdown args)
{
if (TryComp<DragonComponent>(component.Dragon, out var dragon) && !dragon.Weakened)
{
foreach (var rift in dragon.Rifts)
{
QueueDel(rift);
}
dragon.Rifts.Clear();
// We can't predict the rift being destroyed anyway so no point adding weakened to shared.
dragon.WeakenedAccumulator = dragon.WeakenedDuration;
_movement.RefreshMovementSpeedModifiers(component.Dragon);
_popupSystem.PopupEntity(Loc.GetString("carp-rift-destroyed"), component.Dragon, Filter.Entities(component.Dragon));
}
}
private void OnRiftGetState(EntityUid uid, DragonRiftComponent component, ref ComponentGetState args)
{
args.State = new DragonRiftComponentState()
{
State = component.State
};
}
private void OnDragonMove(EntityUid uid, DragonComponent component, RefreshMovementSpeedModifiersEvent args)
{
if (component.Weakened)
{
args.ModifySpeed(0.5f, 0.5f);
}
}
private void OnDragonRift(EntityUid uid, DragonComponent component, DragonSpawnRiftActionEvent args)
{
if (component.Weakened)
{
_popupSystem.PopupEntity(Loc.GetString("carp-rift-weakened"), uid, Filter.Entities(uid));
return;
}
if (component.Rifts.Count >= RiftsAllowed)
{
_popupSystem.PopupEntity(Loc.GetString("carp-rift-max"), uid, Filter.Entities(uid));
return;
}
if (component.Rifts.Count > 0 && TryComp<DragonRiftComponent>(component.Rifts[^1], out var rift) && rift.State != DragonRiftState.Finished)
{
_popupSystem.PopupEntity(Loc.GetString("carp-rift-duplicate"), uid, Filter.Entities(uid));
return;
}
var xform = Transform(uid);
// Have to be on a grid fam
if (!_mapManager.TryGetGrid(xform.GridUid, out var grid))
{
_popupSystem.PopupEntity(Loc.GetString("carp-rift-anchor"), uid, Filter.Entities(uid));
return;
}
foreach (var (_, riftXform) in EntityQuery<DragonRiftComponent, TransformComponent>(true))
{
if (riftXform.Coordinates.InRange(EntityManager, xform.Coordinates, RiftRange))
{
_popupSystem.PopupEntity(Loc.GetString("carp-rift-proximity", ("proximity", RiftRange)), uid, Filter.Entities(uid));
return;
}
}
foreach (var tile in grid.GetTilesIntersecting(new Circle(xform.WorldPosition, RiftTileRadius), false))
{
if (!tile.IsSpace(_tileDef))
continue;
_popupSystem.PopupEntity(Loc.GetString("carp-rift-space-proximity", ("proximity", RiftTileRadius)), uid, Filter.Entities(uid));
return;
}
var carpUid = Spawn(component.RiftPrototype, xform.MapPosition);
component.Rifts.Add(carpUid);
Comp<DragonRiftComponent>(carpUid).Dragon = uid;
_audioSystem.Play("/Audio/Weapons/Guns/Gunshots/rocket_launcher.ogg", Filter.Pvs(carpUid, entityManager: EntityManager), carpUid);
}
#endregion
private void OnShutdown(EntityUid uid, DragonComponent component, ComponentShutdown args)
{
foreach (var rift in component.Rifts)
{
QueueDel(rift);
}
}
private void OnMobStateChanged(EntityUid uid, DragonComponent component, MobStateChangedEvent args)
@@ -45,6 +278,13 @@ namespace Content.Server.Dragon
_audioSystem.PlayPvs(component.SoundDeath, uid, component.SoundDeath.Params);
component.DragonStomach.EmptyContainer();
foreach (var rift in component.Rifts)
{
QueueDel(rift);
}
component.Rifts.Clear();
}
}
@@ -59,13 +299,7 @@ namespace Content.Server.Dragon
var ichorInjection = new Solution(component.DevourChem, component.DevourHealRate);
//Humanoid devours allow dragon to get eggs, corpses included
if (EntityManager.HasComponent<HumanoidAppearanceComponent>(args.Target))
{
// Add a spawn for a consumed humanoid
component.SpawnsLeft = Math.Min(component.SpawnsLeft + 1, component.MaxSpawns);
}
//Non-humanoid mobs can only heal dragon for half the normal amount, with no additional spawn tickets
else
if (!EntityManager.HasComponent<HumanoidAppearanceComponent>(args.Target))
{
ichorInjection.ScaleSolution(0.5f);
}
@@ -87,10 +321,14 @@ namespace Content.Server.Dragon
_audioSystem.PlayPvs(component.SoundDevour, uid, component.SoundDevour.Params);
}
private void Roar(DragonComponent component)
{
if (component.SoundRoar != null)
_audioSystem.Play(component.SoundRoar, Filter.Pvs(component.Owner, 4f, EntityManager), component.Owner, component.SoundRoar.Params);
}
private void OnStartup(EntityUid uid, DragonComponent component, ComponentStartup args)
{
component.SpawnsLeft = Math.Min(component.SpawnsLeft, component.MaxSpawns);
//Dragon doesn't actually chew, since he sends targets right into his stomach.
//I did it mom, I added ERP content into upstream. Legally!
component.DragonStomach = _containerSystem.EnsureContainer<Container>(uid, "dragon_stomach");
@@ -98,11 +336,10 @@ namespace Content.Server.Dragon
if (component.DevourAction != null)
_actionsSystem.AddAction(uid, component.DevourAction, null);
if (component.SpawnAction != null)
_actionsSystem.AddAction(uid, component.SpawnAction, null);
if (component.SpawnRiftAction != null)
_actionsSystem.AddAction(uid, component.SpawnRiftAction, null);
if (component.SoundRoar != null)
_audioSystem.Play(component.SoundRoar, Filter.Pvs(uid, 4f, EntityManager), uid, component.SoundRoar.Params);
Roar(component);
}
/// <summary>
@@ -117,7 +354,6 @@ namespace Content.Server.Dragon
return;
}
args.Handled = true;
var target = args.Target;
@@ -164,22 +400,6 @@ namespace Content.Server.Dragon
});
}
private void OnDragonSpawnAction(EntityUid dragonuid, DragonComponent component, DragonSpawnActionEvent args)
{
if (component.SpawnPrototype == null)
return;
// If dragon has spawns then add one.
if (component.SpawnsLeft > 0)
{
Spawn(component.SpawnPrototype, Transform(dragonuid).Coordinates);
component.SpawnsLeft--;
return;
}
_popupSystem.PopupEntity(Loc.GetString("dragon-spawn-action-popup-message-fail-no-eggs"), dragonuid, Filter.Entities(dragonuid));
}
private sealed class DragonDevourComplete : EntityEventArgs
{
public EntityUid User { get; }

View File

@@ -53,6 +53,7 @@ namespace Content.Server.Entry
var prototypes = IoCManager.Resolve<IPrototypeManager>();
factory.DoAutoRegistrations();
factory.IgnoreMissingComponents("Visuals");
foreach (var ignoreName in IgnoredComponents.List)
{

View File

@@ -6,40 +6,17 @@ namespace Content.Server.Entry
public static string[] List => new[] {
"ConstructionGhost",
"IconSmooth",
"StasisBedVisuals",
"InteractionOutline",
"MeleeWeaponArcAnimation",
"EffectVisuals",
"DamageStateVisuals",
"PortableScrubberVisuals",
"AnimationsTest",
"ItemStatus",
"VehicleVisuals",
"Marker",
"Clickable",
"Icon",
"ClientEntitySpawner",
"CharacterInfo",
"ItemCabinetVisuals",
"LatheVisuals",
"DiseaseMachineVisuals",
"HandheldGPS",
"SpentAmmoVisuals",
"MagazineVisuals",
"SolutionContainerVisuals",
"PowerCellVisuals",
"ToggleableLightVisuals",
"CableVisualizer",
"PotencyVisuals",
"PaperVisuals",
"SurveillanceCameraVisuals",
"KudzuVisuals",
"AMEControllerVisuals",
"AMEShieldingVisuals",
"PipeColorVisuals",
"FireVisuals",
"CrematoriumVisuals",
"PlantHolderVisuals",
};
}
}

View File

@@ -1,15 +1,12 @@
using System.Linq;
using Content.Server.CharacterAppearance.Components;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Configurations;
using Content.Server.Nuke;
using Content.Server.Players;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Spawners.Components;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.CCVar;
using Content.Shared.MobState;
@@ -23,7 +20,8 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Content.Server.Traitor;
using System.Data;
using Robust.Shared.Audio;
using Robust.Shared.Player;
namespace Content.Server.GameTicking.Rules;
@@ -36,7 +34,6 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
[Dependency] private readonly IMapLoader _mapLoader = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly StationSpawningSystem _stationSpawningSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
private Dictionary<Mind.Mind, bool> _aliveNukeops = new();
@@ -44,6 +41,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
public override string Prototype => "Nukeops";
private readonly SoundSpecifier _greetSound = new SoundPathSpecifier("/Audio/Misc/nukeops.ogg");
private const string NukeopsPrototypeId = "Nukeops";
private const string NukeopsCommanderPrototypeId = "NukeopsCommander";
@@ -293,6 +292,12 @@ public sealed class NukeopsRuleSystem : GameRuleSystem
GameTicker.PlayerJoinGame(session);
}
SoundSystem.Play(_greetSound.GetSound(), Filter.Empty().AddWhere(s =>
{
var mind = ((IPlayerSession) s).Data.ContentData()?.Mind;
return mind != null && _aliveNukeops.ContainsKey(mind);
}), AudioParams.Default);
}
//For admins forcing someone to nukeOps.

View File

@@ -45,9 +45,9 @@ namespace Content.Server.Headset
_radioSystem = EntitySystem.Get<RadioSystem>();
}
public bool CanListen(string message, EntityUid source, RadioChannelPrototype prototype)
public bool CanListen(string message, EntityUid source, RadioChannelPrototype? prototype)
{
return Channels.Contains(prototype.ID) && RadioRequested;
return prototype != null && Channels.Contains(prototype.ID) && RadioRequested;
}
public void Receive(string message, RadioChannelPrototype channel, EntityUid source)
@@ -73,8 +73,13 @@ namespace Content.Server.Headset
_netManager.ServerSendMessage(msg, playerChannel);
}
public void Listen(string message, EntityUid speaker, RadioChannelPrototype channel)
public void Listen(string message, EntityUid speaker, RadioChannelPrototype? channel)
{
if (channel == null)
{
return;
}
Broadcast(message, speaker, channel);
}

View File

@@ -66,8 +66,8 @@ namespace Content.Server.Interaction
if (!tileDef.CanCrowbar) continue;
var underplating = tileDefinitionManager["underplating"];
mapGrid.SetTile(coordinates, new Robust.Shared.Map.Tile(underplating.TileId));
var underplating = tileDefinitionManager["UnderPlating"];
mapGrid.SetTile(coordinates, new Tile(underplating.TileId));
}
}
}

View File

@@ -15,6 +15,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Player;
using JetBrains.Annotations;
using System.Linq;
using Content.Server.Power.Components;
using Robust.Server.Player;
namespace Content.Server.Lathe
@@ -27,7 +28,7 @@ namespace Content.Server.Lathe
[Dependency] private readonly SharedAudioSystem _audioSys = default!;
[Dependency] private readonly UserInterfaceSystem _uiSys = default!;
[Dependency] private readonly ResearchSystem _researchSys = default!;
public override void Initialize()
{
base.Initialize();
@@ -36,6 +37,7 @@ namespace Content.Server.Lathe
SubscribeLocalEvent<LatheComponent, LatheQueueRecipeMessage>(OnLatheQueueRecipeMessage);
SubscribeLocalEvent<LatheComponent, LatheSyncRequestMessage>(OnLatheSyncRequestMessage);
SubscribeLocalEvent<LatheComponent, LatheServerSelectionMessage>(OnLatheServerSelectionMessage);
SubscribeLocalEvent<LatheComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<TechnologyDatabaseComponent, LatheServerSyncMessage>(OnLatheServerSyncMessage);
}
@@ -156,7 +158,7 @@ namespace Content.Server.Lathe
EntityManager.QueueDeleteEntity(args.Used);
EnsureComp<LatheInsertingComponent>(uid).TimeRemaining = component.InsertionTime;
_popupSystem.PopupEntity(Loc.GetString("machine-insert-item", ("machine", uid),
("item", args.Used)), uid, Filter.Entities(args.User));
@@ -167,6 +169,14 @@ namespace Content.Server.Lathe
UpdateInsertingAppearance(uid, true);
}
private void OnPowerChanged(EntityUid uid, LatheComponent component, PowerChangedEvent args)
{
//if the power state changes, try to produce.
//aka, if you went from unpowered --> powered, resume lathe queue.
TryStartProducing(uid, component: component);
}
/// <summary>
/// This handles the checks to start producing an item, and
/// starts up the sound and visuals

View File

@@ -21,7 +21,7 @@ namespace Content.Server.Objectives.Conditions
/// instead of "steal advanced magboots. Should be a loc string.
/// </summary>
[ViewVariables]
[DataField("owner", required: true)] private string _owner = string.Empty;
[DataField("owner")] private string? _owner = null;
public IObjectiveCondition GetAssigned(Mind.Mind mind)
{
@@ -38,7 +38,10 @@ namespace Content.Server.Objectives.Conditions
? prototype.Name
: "[CANNOT FIND NAME]";
public string Title => Loc.GetString("objective-condition-steal-title", ("owner", Loc.GetString(_owner)), ("itemName", Loc.GetString(PrototypeName)));
public string Title =>
_owner == null
? Loc.GetString("objective-condition-steal-title-no-owner", ("itemName", Loc.GetString(PrototypeName)))
: Loc.GetString("objective-condition-steal-title", ("owner", Loc.GetString(_owner)), ("itemName", Loc.GetString(PrototypeName)));
public string Description => Loc.GetString("objective-condition-steal-description",("itemName", Loc.GetString(PrototypeName)));

View File

@@ -2,6 +2,7 @@ using Content.Shared.Interaction;
using Content.Shared.Pinpointer;
using System.Linq;
using Robust.Shared.Utility;
using Content.Server.Shuttles.Events;
namespace Content.Server.Pinpointer
{
@@ -13,12 +14,29 @@ namespace Content.Server.Pinpointer
{
base.Initialize();
SubscribeLocalEvent<PinpointerComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<HyperspaceJumpCompletedEvent>(OnLocateTarget);
}
private void OnActivate(EntityUid uid, PinpointerComponent component, ActivateInWorldEvent args)
{
TogglePinpointer(uid, component);
LocateTarget(uid, component);
}
private void OnLocateTarget(HyperspaceJumpCompletedEvent ev)
{
// This feels kind of expensive, but it only happens once per hyperspace jump
foreach (var uid in ActivePinpointers)
{
if (TryComp<PinpointerComponent>(uid, out var component))
{
LocateTarget(uid, component);
}
}
}
private void LocateTarget(EntityUid uid, PinpointerComponent component)
{
// try to find target from whitelist
if (component.IsActive && component.Component != null)
{

View File

@@ -219,6 +219,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
jobs.RemoveSwap(i);
i--;
break;
}
}
}

View File

@@ -1,5 +1,4 @@
using Content.Server.Actions;
using Content.Server.Body.Components;
using Content.Server.Buckle.Components;
using Content.Server.Inventory;
using Content.Server.Mind.Commands;
@@ -13,11 +12,8 @@ using Content.Shared.Damage;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Polymorph;
using Robust.Server.Containers;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -90,6 +86,10 @@ namespace Content.Server.Polymorph.Systems
/// logic and conditions specified in the prototype, and everything else that may be needed.
/// I am clinically insane - emo
// if it's already morphed, don't allow it again with this condition active.
if (!proto.AllowRepeatedMorphs && HasComp<PolymorphedEntityComponent>(target))
return null;
// mostly just for vehicles
if (TryComp<BuckleComponent>(target, out var buckle))
buckle.TryUnbuckle(target, true);

View File

@@ -113,8 +113,8 @@ namespace Content.Server.RCD.Systems
{
//Floor mode just needs the tile to be a space tile (subFloor)
case RcdMode.Floors:
mapGrid.SetTile(snapPos, new Tile(_tileDefinitionManager["floor_steel"].TileId));
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to set grid: {tile.GridIndex} {snapPos} to floor_steel");
mapGrid.SetTile(snapPos, new Tile(_tileDefinitionManager["FloorSteel"].TileId));
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to set grid: {tile.GridIndex} {snapPos} to FloorSteel");
break;
//We don't want to place a space tile on something that's already a space tile. Let's do the inverse of the last check.
case RcdMode.Deconstruct:

View File

@@ -1,3 +1,4 @@
using System.Linq;
using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Radio.EntitySystems;
@@ -20,6 +21,7 @@ namespace Content.Server.Radio.Components
{
private ChatSystem _chatSystem = default!;
private RadioSystem _radioSystem = default!;
private IPrototypeManager _prototypeManager = default!;
private bool _radioOn;
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
@@ -52,6 +54,7 @@ namespace Content.Server.Radio.Components
_radioSystem = EntitySystem.Get<RadioSystem>();
_chatSystem = EntitySystem.Get<ChatSystem>();
IoCManager.Resolve(ref _prototypeManager);
RadioOn = false;
}
@@ -72,12 +75,16 @@ namespace Content.Server.Radio.Components
return true;
}
public bool CanListen(string message, EntityUid source, RadioChannelPrototype prototype)
public bool CanListen(string message, EntityUid source, RadioChannelPrototype? prototype)
{
if (!_channels.Contains(prototype.ID)) return false;
if (prototype != null && !_channels.Contains(prototype.ID)
|| !_prototypeManager.HasIndex<RadioChannelPrototype>(BroadcastChannel))
{
return false;
}
return RadioOn &&
EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(Owner, source, range: ListenRange);
return RadioOn
&& EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(Owner, source, range: ListenRange);
}
public void Receive(string message, RadioChannelPrototype channel, EntityUid speaker)
@@ -88,9 +95,16 @@ namespace Content.Server.Radio.Components
}
}
public void Listen(string message, EntityUid speaker, RadioChannelPrototype channel)
public void Listen(string message, EntityUid speaker, RadioChannelPrototype? prototype)
{
Broadcast(message, speaker, channel);
// if we can't get the channel, we need to just use the broadcast frequency
if (prototype == null
&& !_prototypeManager.TryIndex(BroadcastChannel, out prototype))
{
return;
}
Broadcast(message, speaker, prototype);
}
public void Broadcast(string message, EntityUid speaker, RadioChannelPrototype channel)

View File

@@ -10,8 +10,8 @@ namespace Content.Server.Radio.Components
{
int ListenRange { get; }
bool CanListen(string message, EntityUid source, RadioChannelPrototype channelPrototype);
bool CanListen(string message, EntityUid source, RadioChannelPrototype? channelPrototype);
void Listen(string message, EntityUid speaker, RadioChannelPrototype channel);
void Listen(string message, EntityUid speaker, RadioChannelPrototype? channel);
}
}

View File

@@ -7,7 +7,7 @@ namespace Content.Server.Radio.EntitySystems
[UsedImplicitly]
public sealed class ListeningSystem : EntitySystem
{
public void PingListeners(EntityUid source, string message, RadioChannelPrototype channel)
public void PingListeners(EntityUid source, string message, RadioChannelPrototype? channel)
{
foreach (var listener in EntityManager.EntityQuery<IListen>(true))
{

View File

@@ -15,8 +15,6 @@ namespace Content.Server.RatKing
{
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly ActionsSystem _action = default!;
[Dependency] private readonly DiseaseSystem _disease = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly AtmosphereSystem _atmos = default!;
[Dependency] private readonly TransformSystem _xform = default!;

View File

@@ -6,17 +6,11 @@ namespace Content.Server.Resist;
public sealed class CanEscapeInventoryComponent : Component
{
/// <summary>
/// How long it takes to break out of storage. Default at 5 seconds.
/// Base doafter length for uncontested breakouts.
/// </summary>
[ViewVariables]
[DataField("resistTime")]
public float ResistTime = 5f;
/// <summary>
/// For quick exit if the player attempts to move while already resisting
/// </summary>
[ViewVariables]
public bool IsResisting = false;
[DataField("baseResistTime")]
public float BaseResistTime = 5f;
/// <summary>
/// Cancellation token used to cancel the DoAfter if the mob is removed before it's complete

View File

@@ -1,13 +1,14 @@
using Content.Shared.Movement;
using Content.Server.DoAfter;
using Content.Server.Contests;
using Robust.Shared.Containers;
using Content.Server.Popups;
using Robust.Shared.Player;
using Content.Shared.Storage;
using Content.Shared.Inventory;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.ActionBlocker;
using Content.Shared.Movement.Events;
using Content.Shared.Interaction.Events;
namespace Content.Server.Resist;
@@ -17,6 +18,13 @@ public sealed class EscapeInventorySystem : EntitySystem
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly ContestsSystem _contests = default!;
/// <summary>
/// You can't escape the hands of an entity this many times more massive than you.
/// </summary>
public const float MaximumMassDisadvantage = 6f;
public override void Initialize()
{
@@ -26,20 +34,38 @@ public sealed class EscapeInventorySystem : EntitySystem
SubscribeLocalEvent<CanEscapeInventoryComponent, UpdateCanMoveEvent>(OnMoveAttempt);
SubscribeLocalEvent<CanEscapeInventoryComponent, EscapeDoAfterComplete>(OnEscapeComplete);
SubscribeLocalEvent<CanEscapeInventoryComponent, EscapeDoAfterCancel>(OnEscapeFail);
SubscribeLocalEvent<CanEscapeInventoryComponent, DroppedEvent>(OnDropped);
}
private void OnRelayMovement(EntityUid uid, CanEscapeInventoryComponent component, ref MoveInputEvent args)
{
//Prevents the user from creating multiple DoAfters if they're already resisting.
if (component.IsResisting == true)
if (component.CancelToken != null)
return;
if (_containerSystem.TryGetContainingContainer(uid, out var container)
&& (HasComp<SharedStorageComponent>(container.Owner) || HasComp<InventoryComponent>(container.Owner) || HasComp<SharedHandsComponent>(container.Owner)))
if (!_containerSystem.TryGetContainingContainer(uid, out var container) || !_actionBlockerSystem.CanInteract(uid, container.Owner))
return;
// Contested
if (_handsSystem.IsHolding(container.Owner, uid, out var inHand))
{
if (_actionBlockerSystem.CanInteract(uid, container.Owner))
AttemptEscape(uid, container.Owner, component);
var contestResults = _contests.MassContest(uid, container.Owner);
// Inverse if we aren't going to divide by 0, otherwise just use a default multiplier of 1.
if (contestResults != 0)
contestResults = 1 / contestResults;
else
contestResults = 1;
if (contestResults >= MaximumMassDisadvantage)
return;
AttemptEscape(uid, container.Owner, component, contestResults);
return;
}
// Uncontested
if (HasComp<SharedStorageComponent>(container.Owner) || HasComp<InventoryComponent>(container.Owner))
AttemptEscape(uid, container.Owner, component);
}
private void OnMoveAttempt(EntityUid uid, CanEscapeInventoryComponent component, UpdateCanMoveEvent args)
@@ -48,10 +74,10 @@ public sealed class EscapeInventorySystem : EntitySystem
args.Cancel();
}
private void AttemptEscape(EntityUid user, EntityUid container, CanEscapeInventoryComponent component)
private void AttemptEscape(EntityUid user, EntityUid container, CanEscapeInventoryComponent component, float multiplier = 1f)
{
component.CancelToken = new();
var doAfterEventArgs = new DoAfterEventArgs(user, component.ResistTime, component.CancelToken.Token, container)
var doAfterEventArgs = new DoAfterEventArgs(user, component.BaseResistTime * multiplier, component.CancelToken.Token, container)
{
BreakOnTargetMove = false,
BreakOnUserMove = false,
@@ -62,7 +88,6 @@ public sealed class EscapeInventorySystem : EntitySystem
UserCancelledEvent = new EscapeDoAfterCancel(),
};
component.IsResisting = true;
_popupSystem.PopupEntity(Loc.GetString("escape-inventory-component-start-resisting"), user, Filter.Entities(user));
_popupSystem.PopupEntity(Loc.GetString("escape-inventory-component-start-resisting-target"), container, Filter.Entities(container));
_doAfterSystem.DoAfter(doAfterEventArgs);
@@ -72,12 +97,17 @@ public sealed class EscapeInventorySystem : EntitySystem
{
//Drops the mob on the tile below the container
Transform(uid).AttachParentToContainerOrGrid(EntityManager);
component.IsResisting = false;
component.CancelToken = null;
}
private void OnEscapeFail(EntityUid uid, CanEscapeInventoryComponent component, EscapeDoAfterCancel ev)
{
component.IsResisting = false;
component.CancelToken = null;
}
private void OnDropped(EntityUid uid, CanEscapeInventoryComponent component, DroppedEvent args)
{
component.CancelToken?.Cancel();
}
private sealed class EscapeDoAfterComplete : EntityEventArgs { }

View File

@@ -0,0 +1,14 @@
using Content.Server.Shuttles.Systems;
using Content.Shared.Shuttles.Components;
namespace Content.Server.Shuttles.Components;
[RegisterComponent, Access(typeof(ShuttleSystem))]
public sealed class IFFConsoleComponent : Component
{
/// <summary>
/// Flags that this console is allowed to set.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("allowedFlags")]
public IFFFlags AllowedFlags = IFFFlags.HideLabel;
}

View File

@@ -0,0 +1,8 @@
using Content.Server.Shuttles.Systems;
namespace Content.Server.Shuttles.Events;
/// <summary>
/// Raised when <see cref="ShuttleSystem.FasterThanLight"/> has completed FTL Travel.
/// </summary>
public sealed class HyperspaceJumpCompletedEvent: EntityEventArgs {}

View File

@@ -13,6 +13,7 @@ using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Shuttles.Events;
namespace Content.Server.Shuttles.Systems;
@@ -307,6 +308,7 @@ public sealed partial class ShuttleSystem
comp.State = FTLState.Cooldown;
comp.Accumulator += FTLCooldown;
_console.RefreshShuttleConsoles(comp.Owner);
RaiseLocalEvent(new HyperspaceJumpCompletedEvent());
break;
case FTLState.Cooldown:
RemComp<FTLComponent>(comp.Owner);

View File

@@ -0,0 +1,91 @@
using Content.Server.Shuttles.Components;
using Content.Shared.Shuttles.BUIStates;
using Content.Shared.Shuttles.Components;
using Content.Shared.Shuttles.Events;
namespace Content.Server.Shuttles.Systems;
public sealed partial class ShuttleSystem
{
private void InitializeIFF()
{
SubscribeLocalEvent<IFFConsoleComponent, AnchorStateChangedEvent>(OnIFFConsoleAnchor);
SubscribeLocalEvent<IFFConsoleComponent, IFFShowIFFMessage>(OnIFFShow);
SubscribeLocalEvent<IFFConsoleComponent, IFFShowVesselMessage>(OnIFFShowVessel);
}
private void OnIFFShow(EntityUid uid, IFFConsoleComponent component, IFFShowIFFMessage args)
{
if (!TryComp<TransformComponent>(uid, out var xform) || xform.GridUid == null ||
(component.AllowedFlags & IFFFlags.HideLabel) == 0x0)
{
return;
}
if (!args.Show)
{
AddIFFFlag(xform.GridUid.Value, IFFFlags.HideLabel);
}
else
{
RemoveIFFFlag(xform.GridUid.Value, IFFFlags.HideLabel);
}
}
private void OnIFFShowVessel(EntityUid uid, IFFConsoleComponent component, IFFShowVesselMessage args)
{
if (!TryComp<TransformComponent>(uid, out var xform) || xform.GridUid == null ||
(component.AllowedFlags & IFFFlags.Hide) == 0x0)
{
return;
}
if (!args.Show)
{
AddIFFFlag(xform.GridUid.Value, IFFFlags.Hide);
}
else
{
RemoveIFFFlag(xform.GridUid.Value, IFFFlags.Hide);
}
}
private void OnIFFConsoleAnchor(EntityUid uid, IFFConsoleComponent component, ref AnchorStateChangedEvent args)
{
// If we anchor / re-anchor then make sure flags up to date.
if (!args.Anchored ||
!TryComp<TransformComponent>(uid, out var xform) ||
!TryComp<IFFComponent>(xform.GridUid, out var iff))
{
_uiSystem.TrySetUiState(uid, IFFConsoleUiKey.Key, new IFFConsoleBoundUserInterfaceState()
{
AllowedFlags = component.AllowedFlags,
Flags = IFFFlags.None,
});
}
else
{
_uiSystem.TrySetUiState(uid, IFFConsoleUiKey.Key, new IFFConsoleBoundUserInterfaceState()
{
AllowedFlags = component.AllowedFlags,
Flags = iff.Flags,
});
}
}
protected override void UpdateIFFInterfaces(EntityUid gridUid, IFFComponent component)
{
base.UpdateIFFInterfaces(gridUid, component);
foreach (var (comp, xform) in EntityQuery<IFFConsoleComponent, TransformComponent>(true))
{
if (xform.GridUid != gridUid)
continue;
_uiSystem.TrySetUiState(comp.Owner, IFFConsoleUiKey.Key, new IFFConsoleBoundUserInterfaceState()
{
AllowedFlags = comp.AllowedFlags,
Flags = component.Flags,
});
}
}
}

View File

@@ -32,6 +32,7 @@ namespace Content.Server.Shuttles.Systems
InitializeEmergencyConsole();
InitializeEscape();
InitializeFTL();
InitializeIFF();
SubscribeLocalEvent<ShuttleComponent, ComponentAdd>(OnShuttleAdd);
SubscribeLocalEvent<ShuttleComponent, ComponentStartup>(OnShuttleStartup);

View File

@@ -16,7 +16,7 @@ namespace Content.Server.Singularity.Components
// For the "emitter fired" sound
public const float Variation = 0.25f;
public const float Volume = 0.5f;
public const float Distance = 3f;
public const float Distance = 6f;
[ViewVariables] public int FireShotCounter;

View File

@@ -0,0 +1,10 @@
namespace Content.Server.Sound.Components
{
/// <summary>
/// Simple sound emitter that emits sound on entity drop
/// </summary>
[RegisterComponent]
public sealed class EmitSoundOnDropComponent : BaseEmitSoundComponent
{
}
}

View File

@@ -0,0 +1,10 @@
namespace Content.Server.Sound.Components
{
/// <summary>
/// Simple sound emitter that emits sound on entity pickup
/// </summary>
[RegisterComponent]
public sealed class EmitSoundOnPickupComponent : BaseEmitSoundComponent
{
}
}

View File

@@ -4,8 +4,10 @@ using Content.Server.Sound.Components;
using Content.Server.Throwing;
using Content.Server.UserInterface;
using Content.Server.Popups;
using Content.Shared.Hands;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Maps;
using Content.Shared.Throwing;
using JetBrains.Annotations;
@@ -58,6 +60,8 @@ namespace Content.Server.Sound
SubscribeLocalEvent<EmitSoundOnActivateComponent, ActivateInWorldEvent>(HandleEmitSoundOnActivateInWorld);
SubscribeLocalEvent<EmitSoundOnTriggerComponent, TriggerEvent>(HandleEmitSoundOnTrigger);
SubscribeLocalEvent<EmitSoundOnUIOpenComponent, AfterActivatableUIOpenEvent>(HandleEmitSoundOnUIOpen);
SubscribeLocalEvent<EmitSoundOnPickupComponent, GotEquippedHandEvent>(HandleEmitSoundOnPickup);
SubscribeLocalEvent<EmitSoundOnDropComponent, DroppedEvent>(HandleEmitSoundOnDrop);
}
private void HandleEmitSoundOnTrigger(EntityUid uid, EmitSoundOnTriggerComponent component, TriggerEvent args)
@@ -106,6 +110,16 @@ namespace Content.Server.Sound
TryEmitSound(component);
}
private void HandleEmitSoundOnPickup(EntityUid uid, EmitSoundOnPickupComponent component, GotEquippedHandEvent args)
{
TryEmitSound(component);
}
private void HandleEmitSoundOnDrop(EntityUid uid, EmitSoundOnDropComponent component, DroppedEvent args)
{
TryEmitSound(component);
}
private void TryEmitSound(BaseEmitSoundComponent component)
{
_audioSystem.PlayPvs(component.Sound, component.Owner, component.Sound.Params.AddVolume(-2f));

View File

@@ -1,13 +0,0 @@
namespace Content.Server.Sprite.Components
{
[RegisterComponent]
public sealed class RandomSpriteColorComponent : Component
{
// This should handle random states + colors for layers.
// Saame with RandomSpriteState
[DataField("selected")] public string? SelectedColor;
[DataField("state")] public string BaseState = "error";
[DataField("colors")] public readonly Dictionary<string, Color> Colors = new();
}
}

View File

@@ -1,10 +0,0 @@
namespace Content.Server.Sprite.Components
{
[RegisterComponent]
public sealed class RandomSpriteStateComponent : Component
{
[DataField("spriteStates")] public List<string>? SpriteStates;
[DataField("spriteLayer")] public int SpriteLayer;
}
}

View File

@@ -1,46 +1,53 @@
using Content.Server.Sprite.Components;
using Content.Shared.Random.Helpers;
using Robust.Server.GameObjects;
using System.Linq;
using Content.Shared.Decals;
using Content.Shared.Sprite;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Sprite;
public sealed class RandomSpriteSystem: EntitySystem
public sealed class RandomSpriteSystem: SharedRandomSpriteSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RandomSpriteColorComponent, ComponentStartup>(OnSpriteColorStartup);
SubscribeLocalEvent<RandomSpriteColorComponent, MapInitEvent>(OnSpriteColorMapInit);
SubscribeLocalEvent<RandomSpriteStateComponent, MapInitEvent>(OnSpriteStateMapInit);
SubscribeLocalEvent<RandomSpriteComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<RandomSpriteComponent, MapInitEvent>(OnMapInit);
}
private void OnSpriteColorStartup(EntityUid uid, RandomSpriteColorComponent component, ComponentStartup args)
private void OnMapInit(EntityUid uid, RandomSpriteComponent component, MapInitEvent args)
{
UpdateColor(component);
if (component.Selected.Count > 0)
return;
if (component.Available.Count == 0)
return;
var group = _random.Pick(component.Available);
component.Selected.EnsureCapacity(group.Count);
foreach (var layer in group)
{
Color? color = null;
if (!string.IsNullOrEmpty(layer.Value.Color))
color = _random.Pick(_prototype.Index<ColorPalettePrototype>(layer.Value.Color).Colors.Values);
component.Selected.Add(layer.Key, (layer.Value.State, color));
}
Dirty(component);
}
private void OnSpriteColorMapInit(EntityUid uid, RandomSpriteColorComponent component, MapInitEvent args)
private void OnGetState(EntityUid uid, RandomSpriteComponent component, ref ComponentGetState args)
{
component.SelectedColor = _random.Pick(component.Colors.Keys);
UpdateColor(component);
}
private void OnSpriteStateMapInit(EntityUid uid, RandomSpriteStateComponent component, MapInitEvent args)
{
if (component.SpriteStates == null) return;
if (!TryComp<SpriteComponent>(uid, out var spriteComponent)) return;
spriteComponent.LayerSetState(component.SpriteLayer, _random.Pick(component.SpriteStates));
}
private void UpdateColor(RandomSpriteColorComponent component)
{
if (!TryComp<SpriteComponent>(component.Owner, out var spriteComponent) || component.SelectedColor == null) return;
spriteComponent.LayerSetState(0, component.BaseState);
spriteComponent.LayerSetColor(0, component.Colors[component.SelectedColor]);
args.State = new RandomSpriteColorComponentState()
{
Selected = component.Selected,
};
}
}

View File

@@ -8,7 +8,6 @@ using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.StationEvents
{
@@ -43,16 +42,21 @@ namespace Content.Server.StationEvents
// Can't just check debug / release for a default given mappers need to use release mode
// As such we'll always pause it by default.
_configurationManager.OnValueChanged(CCVars.EventsEnabled, value => RuleAdded = value, true);
_configurationManager.OnValueChanged(CCVars.EventsEnabled, SetEnabled, true);
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
}
public override void Started()
public override void Shutdown()
{
if (!_configurationManager.GetCVar(CCVars.EventsEnabled))
RuleAdded = false;
base.Shutdown();
_configurationManager.UnsubValueChanged(CCVars.EventsEnabled, SetEnabled);
}
public bool EventsEnabled { get; private set; }
private void SetEnabled(bool value) => EventsEnabled = value;
public override void Started() { }
public override void Ended() { }
/// <summary>
@@ -86,7 +90,7 @@ namespace Content.Server.StationEvents
{
base.Update(frameTime);
if (!RuleStarted)
if (!RuleStarted || !EventsEnabled)
return;
if (_timeUntilNextEvent > 0)

View File

@@ -0,0 +1,9 @@
using Content.Shared.StationRecords;
namespace Content.Server.StationRecords;
[RegisterComponent]
public sealed class GeneralStationRecordConsoleComponent : Component
{
public StationRecordKey? ActiveKey { get; set; }
}

View File

@@ -0,0 +1,13 @@
using Content.Shared.StationRecords;
namespace Content.Server.StationRecords;
[RegisterComponent]
public sealed class StationRecordKeyStorageComponent : Component
{
/// <summary>
/// The key stored in this component.
/// </summary>
[ViewVariables]
public StationRecordKey? Key;
}

View File

@@ -0,0 +1,11 @@
namespace Content.Server.StationRecords;
[RegisterComponent]
public sealed class StationRecordsComponent : Component
{
// Every single record in this station, by key.
// Essentially a columnar database, but I really suck
// at implementing that so
[ViewVariables]
public StationRecordSet Records = new();
}

View File

@@ -0,0 +1,158 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.StationRecords;
namespace Content.Server.StationRecords;
/// <summary>
/// Set of station records. StationRecordsComponent stores these.
/// Keyed by StationRecordKey, which should be obtained from
/// an entity that stores a reference to it.
/// </summary>
public sealed class StationRecordSet
{
private uint _currentRecordId;
private HashSet<StationRecordKey> _keys = new();
private HashSet<StationRecordKey> _recentlyAccessed = new();
[ViewVariables]
private Dictionary<Type, Dictionary<StationRecordKey, object>> _tables = new();
/// <summary>
/// Gets all records of a specific type stored in the record set.
/// </summary>
/// <typeparam name="T">The type of record to fetch.</typeparam>
/// <returns>An enumerable object that contains a pair of both a station key, and the record associated with it.</returns>
public IEnumerable<(StationRecordKey, T)> GetRecordsOfType<T>()
{
if (!_tables.ContainsKey(typeof(T)))
{
yield break;
}
foreach (var (key, entry) in _tables[typeof(T)])
{
if (entry is not T cast)
{
continue;
}
_recentlyAccessed.Add(key);
yield return (key, cast);
}
}
/// <summary>
/// Add a new record into this set of entries.
/// </summary>
/// <param name="station">Station that we're adding the record for.</param>
/// <returns>A key that represents the record in this set.</returns>
public StationRecordKey AddRecord(EntityUid station)
{
var key = new StationRecordKey(_currentRecordId++, station);
_keys.Add(key);
return key;
}
/// <summary>
/// Add an entry into a record.
/// </summary>
/// <param name="key">Key for the record.</param>
/// <param name="entry">Entry to add.</param>
/// <typeparam name="T">Type of the entry that's being added.</typeparam>
public void AddRecordEntry<T>(StationRecordKey key, T entry)
{
if (!_keys.Contains(key) || entry == null)
{
return;
}
if (!_tables.TryGetValue(typeof(T), out var table))
{
table = new();
_tables.Add(typeof(T), table);
}
table.Add(key, entry);
}
/// <summary>
/// Try to get an record entry by type, from this record key.
/// </summary>
/// <param name="key">The StationRecordKey to get the entries from.</param>
/// <param name="entry">The entry that is retrieved from the record set.</param>
/// <typeparam name="T">The type of entry to search for.</typeparam>
/// <returns>True if the record exists and was retrieved, false otherwise.</returns>
public bool TryGetRecordEntry<T>(StationRecordKey key, [NotNullWhen(true)] out T? entry)
{
entry = default;
if (!_keys.Contains(key)
|| !_tables.TryGetValue(typeof(T), out var table)
|| !table.TryGetValue(key, out var entryObject))
{
return false;
}
entry = (T) entryObject;
_recentlyAccessed.Add(key);
return true;
}
/// <summary>
/// Checks if the record associated with this key has an entry of a certain type.
/// </summary>
/// <param name="key">The record key.</param>
/// <typeparam name="T">Type to check.</typeparam>
/// <returns>True if the entry exists, false otherwise.</returns>
public bool HasRecordEntry<T>(StationRecordKey key)
{
return _keys.Contains(key)
&& _tables.TryGetValue(typeof(T), out var table)
&& table.ContainsKey(key);
}
/// <summary>
/// Get the recently accessed keys from this record set.
/// </summary>
/// <returns>All recently accessed keys from this record set.</returns>
public IEnumerable<StationRecordKey> GetRecentlyAccessed()
{
return _recentlyAccessed;
}
/// <summary>
/// Clears the recently accessed keys from the set.
/// </summary>
public void ClearRecentlyAccessed()
{
_recentlyAccessed.Clear();
}
/// <summary>
/// Removes all record entries related to this key from this set.
/// </summary>
/// <param name="key">The key to remove.</param>
/// <returns>True if successful, false otherwise.</returns>
public bool RemoveAllRecords(StationRecordKey key)
{
if (!_keys.Remove(key))
{
return false;
}
foreach (var table in _tables.Values)
{
table.Remove(key);
}
return true;
}
}

View File

@@ -0,0 +1,75 @@
using Content.Server.Station.Systems;
using Content.Shared.StationRecords;
using Robust.Server.GameObjects;
namespace Content.Server.StationRecords.Systems;
public sealed class GeneralStationRecordConsoleSystem : EntitySystem
{
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly StationRecordsSystem _stationRecordsSystem = default!;
public override void Initialize()
{
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, BoundUIOpenedEvent>(UpdateUserInterface);
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, SelectGeneralStationRecord>(OnKeySelected);
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, RecordModifiedEvent>(UpdateUserInterface);
SubscribeLocalEvent<GeneralStationRecordConsoleComponent, AfterGeneralRecordCreatedEvent>(UpdateUserInterface);
}
private void UpdateUserInterface<T>(EntityUid uid, GeneralStationRecordConsoleComponent component, T ev)
{
UpdateUserInterface(uid, component);
}
private void OnKeySelected(EntityUid uid, GeneralStationRecordConsoleComponent component,
SelectGeneralStationRecord msg)
{
component.ActiveKey = msg.SelectedKey;
UpdateUserInterface(uid, component);
}
private void UpdateUserInterface(EntityUid uid, GeneralStationRecordConsoleComponent? console = null)
{
if (!Resolve(uid, ref console))
{
return;
}
var owningStation = _stationSystem.GetOwningStation(uid);
if (!TryComp<StationRecordsComponent>(owningStation, out var stationRecordsComponent))
{
_userInterface.GetUiOrNull(uid, GeneralStationRecordConsoleKey.Key)?.SetState(new GeneralStationRecordConsoleState(null, null, null));
return;
}
var enumerator = _stationRecordsSystem.GetRecordsOfType<GeneralStationRecord>(owningStation.Value, stationRecordsComponent);
var listing = new Dictionary<StationRecordKey, string>();
foreach (var pair in enumerator)
{
listing.Add(pair.Item1, pair.Item2.Name);
}
if (listing.Count == 0)
{
_userInterface.GetUiOrNull(uid, GeneralStationRecordConsoleKey.Key)?.SetState(new GeneralStationRecordConsoleState(null, null, null));
return;
}
GeneralStationRecord? record = null;
if (console.ActiveKey != null)
{
_stationRecordsSystem.TryGetRecord(owningStation.Value, console.ActiveKey.Value, out record,
stationRecordsComponent);
}
_userInterface
.GetUiOrNull(uid, GeneralStationRecordConsoleKey.Key)?
.SetState(new GeneralStationRecordConsoleState(console.ActiveKey, record, listing));
}
}

View File

@@ -0,0 +1,57 @@
using Content.Shared.StationRecords;
namespace Content.Server.StationRecords.Systems;
public sealed class StationRecordKeyStorageSystem : EntitySystem
{
/// <summary>
/// Assigns a station record key to an entity.
/// </summary>
/// <param name="uid"></param>
/// <param name="key"></param>
/// <param name="keyStorage"></param>
public void AssignKey(EntityUid uid, StationRecordKey key, StationRecordKeyStorageComponent? keyStorage = null)
{
if (!Resolve(uid, ref keyStorage))
{
return;
}
keyStorage.Key = key;
}
/// <summary>
/// Removes a station record key from an entity.
/// </summary>
/// <param name="uid"></param>
/// <param name="keyStorage"></param>
/// <returns></returns>
public StationRecordKey? RemoveKey(EntityUid uid, StationRecordKeyStorageComponent? keyStorage = null)
{
if (!Resolve(uid, ref keyStorage) || keyStorage.Key == null)
{
return null;
}
var key = keyStorage.Key;
keyStorage.Key = null;
return key;
}
/// <summary>
/// Checks if an entity currently contains a station record key.
/// </summary>
/// <param name="uid"></param>
/// <param name="keyStorage"></param>
/// <returns></returns>
public bool CheckKey(EntityUid uid, StationRecordKeyStorageComponent? keyStorage = null)
{
if (!Resolve(uid, ref keyStorage))
{
return false;
}
return keyStorage.Key != null;
}
}

View File

@@ -0,0 +1,282 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Access.Systems;
using Content.Server.GameTicking;
using Content.Server.Station.Systems;
using Content.Server.StationRecords;
using Content.Server.StationRecords.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.StationRecords;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
/// <summary>
/// Station records.
///
/// A station record is tied to an ID card, or anything that holds
/// a station record's key. This key will determine access to a
/// station record set's record entries, and it is imperative not
/// to lose the item that holds the key under any circumstance.
///
/// Records are mostly a roleplaying tool, but can have some
/// functionality as well (i.e., security records indicating that
/// a specific person holding an ID card with a linked key is
/// currently under warrant, showing a crew manifest with user
/// settable, custom titles).
///
/// General records are tied into this system, as most crewmembers
/// should have a general record - and most systems should probably
/// depend on this general record being created. This is subject
/// to change.
/// </summary>
public sealed class StationRecordsSystem : EntitySystem
{
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly StationRecordKeyStorageSystem _keyStorageSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationInitializedEvent>(OnStationInitialize);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn);
}
private void OnStationInitialize(StationInitializedEvent args)
{
AddComp<StationRecordsComponent>(args.Station);
}
private void OnPlayerSpawn(PlayerSpawnCompleteEvent args)
{
CreateGeneralRecord(args.Station, args.Mob, args.Profile, args.JobId);
}
private void CreateGeneralRecord(EntityUid station, EntityUid player, HumanoidCharacterProfile profile,
string? jobId, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records)
|| String.IsNullOrEmpty(jobId)
|| !_prototypeManager.HasIndex<JobPrototype>(jobId))
{
return;
}
if (!_inventorySystem.TryGetSlotEntity(player, "id", out var idUid))
{
return;
}
CreateGeneralRecord(station, idUid.Value, profile.Name, profile.Age, profile.Species, profile.Gender, jobId, profile, records);
}
/// <summary>
/// Create a general record to store in a station's record set.
/// </summary>
/// <remarks>
/// This is tied into the record system, as any crew member's
/// records should generally be dependent on some generic
/// record with the bare minimum of information involved.
/// </remarks>
/// <param name="station">The entity uid of the station.</param>
/// <param name="idUid">The entity uid of an entity's ID card. Can be null.</param>
/// <param name="name">Name of the character.</param>
/// <param name="species">Species of the character.</param>
/// <param name="gender">Gender of the character.</param>
/// <param name="jobId">
/// The job to initially tie this record to. This must be a valid job loaded in, otherwise
/// this call will cause an exception. Ensure that a general record starts out with a job
/// that is currently a valid job prototype.
/// </param>
/// <param name="profile">
/// Profile for the related player. This is so that other systems can get further information
/// about the player character.
/// Optional - other systems should anticipate this.
/// </param>
/// <param name="records">Station records component.</param>
public void CreateGeneralRecord(EntityUid station, EntityUid? idUid, string name, int age, string species, Gender gender, string jobId, HumanoidCharacterProfile? profile = null,
StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
{
return;
}
if (!_prototypeManager.TryIndex(jobId, out JobPrototype? jobPrototype))
{
throw new ArgumentException($"Invalid job prototype ID: {jobId}");
}
var record = new GeneralStationRecord()
{
Name = name,
Age = age,
JobTitle = jobPrototype.Name,
JobIcon = jobPrototype.Icon,
JobPrototype = jobId,
Species = species,
Gender = gender,
DisplayPriority = jobPrototype.Weight
};
var key = records.Records.AddRecord(station);
records.Records.AddRecordEntry(key, record);
// entry.Entries.Add(typeof(GeneralStationRecord), record);
if (idUid != null)
{
var keyStorageEntity = idUid;
if (TryComp(idUid, out PDAComponent? pdaComponent) && pdaComponent.ContainedID != null)
{
keyStorageEntity = pdaComponent.IdSlot.Item;
}
if (keyStorageEntity != null)
{
_keyStorageSystem.AssignKey(keyStorageEntity.Value, key);
}
}
RaiseLocalEvent(new AfterGeneralRecordCreatedEvent(key, record, profile));
}
/// <summary>
/// Removes a record from this station.
/// </summary>
/// <param name="station">Station to remove the record from.</param>
/// <param name="key">The key to remove.</param>
/// <param name="records">Station records component.</param>
/// <returns>True if the record was removed, false otherwise.</returns>
public bool RemoveRecord(EntityUid station, StationRecordKey key, StationRecordsComponent? records = null)
{
if (station != key.OriginStation || !Resolve(station, ref records))
{
return false;
}
RaiseLocalEvent(new RecordRemovedEvent(key));
return records.Records.RemoveAllRecords(key);
}
/// <summary>
/// Try to get a record from this station's record entries,
/// from the provided station record key. Will always return
/// null if the key does not match the station.
/// </summary>
/// <param name="station">Station to get the record from.</param>
/// <param name="key">Key to try and index from the record set.</param>
/// <param name="entry">The resulting entry.</param>
/// <param name="records">Station record component.</param>
/// <typeparam name="T">Type to get from the record set.</typeparam>
/// <returns>True if the record was obtained, false otherwise.</returns>
public bool TryGetRecord<T>(EntityUid station, StationRecordKey key, [NotNullWhen(true)] out T? entry, StationRecordsComponent? records = null)
{
entry = default;
if (key.OriginStation != station || !Resolve(station, ref records))
{
return false;
}
return records.Records.TryGetRecordEntry(key, out entry);
}
/// <summary>
/// Gets all records of a specific type from a station.
/// </summary>
/// <param name="station">The station to get the records from.</param>
/// <param name="records">Station records component.</param>
/// <typeparam name="T">Type of record to fetch</typeparam>
/// <returns>Enumerable of pairs with a station record key, and the entry in question of type T.</returns>
public IEnumerable<(StationRecordKey, T)> GetRecordsOfType<T>(EntityUid station, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
{
return new (StationRecordKey, T)[]{};
}
return records.Records.GetRecordsOfType<T>();
}
/// <summary>
/// Synchronizes a station's records with any systems that need it.
/// </summary>
/// <param name="station">The station to synchronize any recently accessed records with..</param>
/// <param name="records">Station records component.</param>
public void Synchronize(EntityUid station, StationRecordsComponent? records = null)
{
if (!Resolve(station, ref records))
{
return;
}
foreach (var key in records.Records.GetRecentlyAccessed())
{
RaiseLocalEvent(new RecordModifiedEvent(key));
}
records.Records.ClearRecentlyAccessed();
}
}
/// <summary>
/// Event raised after the player's general profile is created.
/// Systems that modify records on a station would have more use
/// listening to this event, as it contains the character's record key.
/// Also stores the general record reference, to save some time.
/// </summary>
public sealed class AfterGeneralRecordCreatedEvent : EntityEventArgs
{
public StationRecordKey Key { get; }
public GeneralStationRecord Record { get; }
/// <summary>
/// Profile for the related player. This is so that other systems can get further information
/// about the player character.
/// Optional - other systems should anticipate this.
/// </summary>
public HumanoidCharacterProfile? Profile { get; }
public AfterGeneralRecordCreatedEvent(StationRecordKey key, GeneralStationRecord record, HumanoidCharacterProfile? profile)
{
Key = key;
Record = record;
Profile = profile;
}
}
/// <summary>
/// Event raised after a record is removed. Only the key is given
/// when the record is removed, so that any relevant systems/components
/// that store record keys can then remove the key from their internal
/// fields.
/// </summary>
public sealed class RecordRemovedEvent : EntityEventArgs
{
public StationRecordKey Key { get; }
public RecordRemovedEvent(StationRecordKey key)
{
Key = key;
}
}
/// <summary>
/// Event raised after a record is modified. This is to
/// inform other systems that records stored in this key
/// may have changed.
/// </summary>
public sealed class RecordModifiedEvent : EntityEventArgs
{
public StationRecordKey Key { get; }
public RecordModifiedEvent(StationRecordKey key)
{
Key = key;
}
}

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