Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Content.Client/_DV/Shipyard/ShipyardConsoleSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Content.Shared._DV.Shipyard;

namespace Content.Client._DV.Shipyard;

public sealed class ShipyardConsoleSystem : SharedShipyardConsoleSystem;
70 changes: 70 additions & 0 deletions Content.Client/_DV/Shipyard/UI/ShipyardBoundUserInterface.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using Content.Shared.Access.Systems;
using Content.Shared._DV.Shipyard;
using Content.Shared.Whitelist;
using Robust.Client.Player;
using Robust.Shared.Prototypes;

namespace Content.Client._DV.Shipyard.UI;

public sealed class ShipyardConsoleBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IPlayerManager _player = default!;

private readonly AccessReaderSystem _access;
private readonly EntityWhitelistSystem _whitelist;

[ViewVariables]
private ShipyardConsoleMenu? _menu;

public ShipyardConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_access = EntMan.System<AccessReaderSystem>();
_whitelist = EntMan.System<EntityWhitelistSystem>();
}

protected override void Open()
{
base.Open();

if (_menu == null)
{
_menu = new ShipyardConsoleMenu(Owner, _proto, EntMan, _player, _access, _whitelist);
_menu.OnClose += Close;
_menu.OnPurchased += Purchase;
}

_menu.OpenCentered();
}

protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);

if (state is not ShipyardConsoleState cast)
return;

_menu?.UpdateState(cast);
}

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

if (!disposing)
return;

if (_menu == null)
return;

_menu.OnClose -= Close;
_menu.OnPurchased -= Purchase;
_menu.Close();
_menu = null;
}

private void Purchase(string id)
{
SendMessage(new ShipyardConsolePurchaseMessage(id));
}
}
28 changes: 28 additions & 0 deletions Content.Client/_DV/Shipyard/UI/ShipyardConsoleMenu.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="500 360"
MinSize="460 280"
Title="{Loc 'shipyard-console-menu-title'}">
<BoxContainer Orientation="Vertical" Margin="5 0 5 0">
<Label Name="BankAccountLabel" />
<BoxContainer Orientation="Horizontal">
<OptionButton Name="Categories"
Prefix="{Loc 'cargo-console-menu-categories-label'}"
HorizontalExpand="True" />
<LineEdit Name="SearchBar"
PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
HorizontalExpand="True" />
</BoxContainer>
<ScrollContainer HorizontalExpand="True"
VerticalExpand="True"
SizeFlagsStretchRatio="6">
<BoxContainer Name="Vessels"
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<!-- Vessels get added here by code -->
</BoxContainer>
</ScrollContainer>
<TextureButton VerticalExpand="True" />
</BoxContainer>
</controls:FancyWindow>
121 changes: 121 additions & 0 deletions Content.Client/_DV/Shipyard/UI/ShipyardConsoleMenu.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Access.Systems;
using Content.Shared._DV.Shipyard;
using Content.Shared._DV.Shipyard.Prototypes;
using Content.Shared.Whitelist;
using Robust.Client.AutoGenerated;
using Robust.Client.Player;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;

namespace Content.Client._DV.Shipyard.UI;

[GenerateTypedNameReferences]
public sealed partial class ShipyardConsoleMenu : FancyWindow
{
private readonly AccessReaderSystem _access;
private readonly IPlayerManager _player;

public event Action<string>? OnPurchased;

private readonly List<VesselPrototype> _vessels = [];
private readonly List<VesselCategoryPrototype> _categories = [];

public Entity<ShipyardConsoleComponent> Console;

// The currently selected category
private ProtoId<VesselCategoryPrototype>? _category;

public ShipyardConsoleMenu(EntityUid console, IPrototypeManager proto, IEntityManager entMan, IPlayerManager player, AccessReaderSystem access, EntityWhitelistSystem whitelist)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);

Console = (console, entMan.GetComponent<ShipyardConsoleComponent>(console));
_access = access;
_player = player;

_categories.Clear();
foreach (var vesselCategoryProto in Console.Comp.Categories)
{
if (proto.Resolve(vesselCategoryProto, out var vesselCategory))
_categories.Add(vesselCategory);
}

// don't include ships that aren't allowed by whitelist, server won't accept them anyway
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
{
if (whitelist.IsWhitelistPassOrNull(vessel.Whitelist, console))
_vessels.Add(vessel);
}

_vessels.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase));

// inserting here and not adding at the start so it doesn't get affected by sort
PopulateCategories(Console);

SearchBar.OnTextChanged += _ => PopulateProducts(Console);
Categories.OnItemSelected += args =>
{
_category = _categories[args.Id];
Categories.SelectId(args.Id);
PopulateProducts(Console);
};

PopulateProducts(Console);
}

/// <summary>
/// Populates the list of products that will actually be shown, using the current filters.
/// </summary>
private void PopulateProducts(Entity<ShipyardConsoleComponent> entity)
{
Vessels.RemoveAllChildren();

var access = _player.LocalSession?.AttachedEntity is { } player
&& _access.IsAllowed(player, Console);

var search = SearchBar.Text.Trim().ToLowerInvariant();
foreach (var vessel in _vessels)
{
if (search.Length != 0 && !vessel.Name.Contains(search, StringComparison.InvariantCultureIgnoreCase))
continue;

if (_category != null && !vessel.Categories.Contains(_category.Value.Id))
continue;

var vesselEntry = new VesselRow(vessel, access, isFree: !entity.Comp.UseStationFunds);
vesselEntry.OnPurchasePressed += () => OnPurchased?.Invoke(vessel.ID);
Vessels.AddChild(vesselEntry);
}
}

/// <summary>
/// Populates the list categories that will actually be shown, using the current filters.
/// </summary>
private void PopulateCategories(Entity<ShipyardConsoleComponent> entity)
{
Categories.Clear();
// Guh, there's gotta be an easier way to select a category by default...
var selectedId = 0; // Default to the first category. May get overridden later.
for (var i = 0; i < _categories.Count; i++)
{
Categories.AddItem(_categories[i].LocalizedName, i);

if (entity.Comp.DefaultCategory.HasValue && entity.Comp.DefaultCategory.Value.Id.Equals(_categories[i].ID, StringComparison.CurrentCultureIgnoreCase))
selectedId = i;
}

if (selectedId >= 0 && selectedId < _categories.Count)
{
_category = _categories[selectedId];
Categories.SelectId(selectedId);
}
}

public void UpdateState(ShipyardConsoleState state)
{
BankAccountLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", state.Balance.ToString()));
PopulateProducts(Console);
}
}
16 changes: 16 additions & 0 deletions Content.Client/_DV/Shipyard/UI/VesselRow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<PanelContainer xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
HorizontalExpand="True">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True">
<Button Name="Purchase" Text="{Loc 'shipyard-console-purchase'}" StyleClasses="LabelSubText" />
<Label Name="VesselName" HorizontalExpand="True" />
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#25252A" />
</PanelContainer.PanelOverride>

<Label Name="Price" MinSize="52 32" Align="Right" />
</PanelContainer>
</BoxContainer>
</PanelContainer>
32 changes: 32 additions & 0 deletions Content.Client/_DV/Shipyard/UI/VesselRow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Content.Shared._DV.Shipyard.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;

namespace Content.Client._DV.Shipyard.UI;

[GenerateTypedNameReferences]
public sealed partial class VesselRow : PanelContainer
{
public event Action? OnPurchasePressed;

public VesselRow(VesselPrototype vessel, bool access = false, bool isFree = false)
{
RobustXamlLoader.Load(this);

VesselName.Text = vessel.Name;

var tooltip = new Tooltip();
tooltip.SetMessage(FormattedMessage.FromMarkupOrThrow(vessel.Description));
Purchase.TooltipSupplier = _ => tooltip;
Purchase.Disabled = !access;
Purchase.OnPressed += _ => OnPurchasePressed?.Invoke();

if (isFree)
Purchase.Text = Loc.GetString("shipyard-console-price-free");
else
Purchase.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", vessel.Price));
}
}
94 changes: 94 additions & 0 deletions Content.IntegrationTests/Tests/DeltaV/ShipyardTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using Content.Server.Cargo.Systems;
using Content.Server._DV.Shipyard;
using Content.Server.Shuttles.Components;
using Content.Shared._DV.Shipyard.Prototypes;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;

namespace Content.IntegrationTests.Tests._DV;

[TestFixture]
[TestOf(typeof(ShipyardSystem))]
public sealed class ShipyardTest
{
[Test]
public async Task NoShipyardArbitrage()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;

var entities = server.ResolveDependency<IEntityManager>();
var proto = server.ResolveDependency<IPrototypeManager>();
var shipyard = entities.System<ShipyardSystem>();
var pricing = entities.System<PricingSystem>();

await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
{
var shuttleCreated = shipyard.TryCreateShuttle(vessel.Path, out var shuttle);

Assert.That(shuttleCreated, Is.True, $"TryCreateShuttle returned false for {vessel.ID}!");
Assert.That(shuttle, Is.Not.Null, $"Failed to spawn shuttle {vessel.ID}!");

var value = pricing.AppraiseGrid(shuttle.Value);
Assert.That(value, Is.AtMost(vessel.Price), $"Found arbitrage on shuttle {vessel.ID}! Price is {vessel.Price} but value is {value}!");
entities.DeleteEntity(shuttle);
}
});
});

await pair.CleanReturnAsync();
}

[Test]
public async Task AllShuttlesValid()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;

var entities = server.ResolveDependency<IEntityManager>();
var proto = server.ResolveDependency<IPrototypeManager>();
var shipyard = entities.System<ShipyardSystem>();

await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
{
var shuttleCreated = shipyard.TryCreateShuttle(vessel.Path, out var shuttle);

Assert.That(shuttleCreated, Is.True, $"TryCreateShuttle returned false for {vessel.ID}!");
Assert.That(shuttle, Is.Not.Null, $"Failed to spawn shuttle {vessel.ID}!");

var console = FindComponent<ShuttleConsoleComponent>(entities, shuttle.Value);
Assert.That(console, Is.True, $"Shuttle {vessel.ID} had no shuttle console!");

var dock = FindComponent<DockingComponent>(entities, shuttle.Value);
Assert.That(dock, Is.True, $"Shuttle {vessel.ID} had no shuttle dock!");

entities.DeleteEntity(shuttle);
}
});
});

await pair.CleanReturnAsync();
}

private bool FindComponent<T>(IEntityManager entities, EntityUid shuttle) where T: Component
{
var query = entities.EntityQueryEnumerator<T, TransformComponent>();
while (query.MoveNext(out _, out var xform))
{
if (xform.ParentUid != shuttle)
continue;

return true;
}

return false;
}
}
17 changes: 17 additions & 0 deletions Content.Server/_DV/Shipyard/MapDeleterShuttleComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Content.Server._DV.Shipyard;

/// <summary>
/// When added to a shuttle, once it FTLs the previous map is deleted.
/// After that the component is removed to prevent insane abuse.
/// </summary>
/// <remarks>
/// Could be upstreamed at some point, loneop shuttle could use it.
/// </remarks>
[RegisterComponent, Access(typeof(MapDeleterShuttleSystem))]
public sealed partial class MapDeleterShuttleComponent : Component
{
/// <summary>
/// Only set by the system to prevent someone in VV deleting maps by mistake or otherwise.
/// </summary>
public bool Enabled;
}
Loading
Loading