Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shipyard rewrite/port #1171

Merged
merged 19 commits into from
May 15, 2024
5 changes: 5 additions & 0 deletions Content.Client/DeltaV/Shipyard/ShipyardConsoleSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Content.Shared.Shipyard;

namespace Content.Client.Shipyard;

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

namespace Content.Client.DeltaV.Shipyard.UI;

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

private readonly AccessReaderSystem _access;

[ViewVariables]
private ShipyardConsoleMenu? _menu;

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

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

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

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)
_menu?.Dispose();
}

private void Purchase(string id)
{
SendMessage(new ShipyardConsolePurchaseMessage(id));
}
}
29 changes: 29 additions & 0 deletions Content.Client/DeltaV/Shipyard/UI/ShipyardConsoleMenu.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
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>
110 changes: 110 additions & 0 deletions Content.Client/DeltaV/Shipyard/UI/ShipyardConsoleMenu.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Access.Systems;
using Content.Shared.Shipyard;
using Content.Shared.Shipyard.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;

namespace Content.Client.DeltaV.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 = new();
private readonly List<string> _categories = new();

public Entity<ShipyardConsoleComponent> Console;
private string? _category;

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

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

// don't include ships that aren't allowed by whitelist, server won't accept them anyway
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
{
if (vessel.Whitelist?.IsValid(console, entMan) != false)
_vessels.Add(vessel);
}
_vessels.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase));

// only list categories in said ships
foreach (var vessel in _vessels)
{
foreach (var category in vessel.Categories)
{
if (!_categories.Contains(category))
_categories.Add(category);
}
}

_categories.Sort();
// inserting here and not adding at the start so it doesn't get affected by sort
_categories.Insert(0, Loc.GetString("cargo-console-menu-populate-categories-all-text"));
PopulateCategories();

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

/// <summary>
/// Populates the list of products that will actually be shown, using the current filters.
/// </summary>
private void PopulateProducts()
{
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.ToLowerInvariant().Contains(search))
continue;
if (_category != null && !vessel.Categories.Contains(_category))
continue;

var vesselEntry = new VesselRow(vessel, access);
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()
{
Categories.Clear();
foreach (var category in _categories)
{
Categories.AddItem(category);
}
}

public void UpdateState(ShipyardConsoleState state)
{
BankAccountLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", state.Balance.ToString()));
PopulateProducts();
}
}
16 changes: 16 additions & 0 deletions Content.Client/DeltaV/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 '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>
29 changes: 29 additions & 0 deletions Content.Client/DeltaV/Shipyard/UI/VesselRow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Content.Shared.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.DeltaV.Shipyard.UI;

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

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

VesselName.Text = vessel.Name;

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

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

namespace Content.IntegrationTests.Tests.DeltaV;

[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 shuttle = shipyard.TryCreateShuttle(vessel.Path.ToString());
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 shuttle = shipyard.TryCreateShuttle(vessel.Path.ToString());
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/DeltaV/Shipyard/MapDeleterShuttleComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Content.Server.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;
}
25 changes: 25 additions & 0 deletions Content.Server/DeltaV/Shipyard/MapDeleterShuttleSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Content.Server.Shuttles.Events;

namespace Content.Server.Shipyard;

public sealed class MapDeleterShuttleSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<MapDeleterShuttleComponent, FTLStartedEvent>(OnFTLStarted);
}

private void OnFTLStarted(Entity<MapDeleterShuttleComponent> ent, ref FTLStartedEvent args)
{
if (ent.Comp.Enabled)
Del(args.FromMapUid);
RemComp<MapDeleterShuttleComponent>(ent); // prevent the shuttle becoming a WMD
}

public void Enable(EntityUid shuttle)
{
EnsureComp<MapDeleterShuttleComponent>(shuttle).Enabled = true;
}
}
Loading
Loading