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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Content.Shared._Forge.Silicons.StationAi;
using Robust.Client.UserInterface;

namespace Content.Client._Forge.Silicons.StationAi;

public sealed class StationAiCustomizationBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
private StationAiCustomizationWindow? _window;

protected override void Open()
{
base.Open();
_window = this.CreateWindow<StationAiCustomizationWindow>();
_window.OnApply += (name, screen, color) => SendMessage(new StationAiCustomizationApplyMessage(name, screen, color));
}

protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is StationAiCustomizationState customization)
_window?.UpdateState(customization);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'station-ai-customization-title'}"
MinSize="650 600"
SetSize="760 700">
<BoxContainer Orientation="Vertical" Margin="12" HorizontalExpand="True">
<Label Text="{Loc 'station-ai-customization-name-label'}" />
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 4 0 10">
<Label Name="NamePrefixLabel" VerticalAlignment="Center" Margin="0 0 6 0" Visible="False" />
<LineEdit Name="NameEdit" HorizontalExpand="True" />
</BoxContainer>
<Label Name="ValidationLabel" FontColorOverride="Red" Visible="False" Margin="0 0 0 8" />
<Label Text="{Loc 'station-ai-customization-screen-label'}" />
<AnimatedTextureRect Name="SelectedPreview" MinSize="0 128" HorizontalExpand="True" Margin="0 4 0 8" />
<ScrollContainer VerticalExpand="True" HorizontalExpand="True" MinSize="0 300" Margin="0 4 0 10">
<GridContainer Name="ScreenGrid" Columns="5" HorizontalExpand="True" />
</ScrollContainer>
<Label Text="{Loc 'station-ai-customization-color-label'}" />
<ColorSelectorSliders Name="ColorSelector" HorizontalExpand="True" Margin="0 4 0 10" />
<Label Name="CooldownLabel" HorizontalAlignment="Center" Margin="0 2 0 8" />
<Button Name="ApplyButton" Text="{Loc 'station-ai-customization-apply'}" HorizontalExpand="True" />
</BoxContainer>
</controls:FancyWindow>
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
using System.Linq;
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared._Forge.Silicons.StationAi;
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;

namespace Content.Client._Forge.Silicons.StationAi;

[GenerateTypedNameReferences]
public sealed partial class StationAiCustomizationWindow : FancyWindow
{
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;

public event Action<string, ProtoId<StationAiScreenPrototype>, Color>? OnApply;

private readonly Dictionary<ProtoId<StationAiScreenPrototype>, Button> _screens = new();
private readonly ButtonGroup _screenGroup = new(false);
private ProtoId<StationAiScreenPrototype> _selectedScreen = "StationAiScreenDefault";
private string _forceNamePrefix = string.Empty;
private TimeSpan _cooldownEnd;
private bool _screensInitialized;

public StationAiCustomizationWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
NameEdit.IsValid = IsNameValid;
ColorSelector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv;
SelectedPreview.DisplayRect.Stretch = TextureRect.StretchMode.KeepCentered;
SelectedPreview.DisplayRect.TextureScale = new Vector2(4f, 4f);

NameEdit.OnTextChanged += _ => UpdateApplyState();
ColorSelector.OnColorChanged += UpdatePreviewColor;
ApplyButton.OnPressed += _ => Apply();
}

private void EnsureScreens()
{
if (_screensInitialized)
return;

_screensInitialized = true;
foreach (var screen in _prototypes.EnumeratePrototypes<StationAiScreenPrototype>().OrderBy(screen => screen.ID))
AddScreen(screen);
}

private void AddScreen(StationAiScreenPrototype screen)
{
var name = Loc.GetString(screen.Name);
var button = new Button
{
MinSize = new Vector2(135, 104),
HorizontalExpand = true,
ToggleMode = true,
Group = _screenGroup,
ToolTip = name,
};

var preview = new TextureRect
{
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
Texture = _entityManager.System<SpriteSystem>().Frame0(new SpriteSpecifier.Rsi(screen.Sprite, screen.State)),
Stretch = TextureRect.StretchMode.KeepCentered,
TextureScale = new Vector2(2f, 2f),
};

var label = new Label
{
Text = name,
ClipText = true,
HorizontalAlignment = HAlignment.Center,
};

var content = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
};
content.AddChild(preview);
content.AddChild(label);
button.AddChild(content);

button.OnPressed += _ => SelectScreen(screen.ID);
ScreenGrid.AddChild(button);
_screens.Add(screen.ID, button);
}

private void SelectScreen(ProtoId<StationAiScreenPrototype> screen)
{
if (!_screens.ContainsKey(screen) || !_prototypes.TryIndex(screen, out var prototype))
return;

_selectedScreen = screen;
_screens[screen].Pressed = true;
SelectedPreview.SetFromSpriteSpecifier(new SpriteSpecifier.Rsi(prototype.Sprite, prototype.State));
UpdatePreviewColor(ColorSelector.Color);
}

public void UpdateState(StationAiCustomizationState state)
{
EnsureScreens();
_forceNamePrefix = state.ForceNamePrefix;
NamePrefixLabel.Text = _forceNamePrefix;
NamePrefixLabel.Visible = _forceNamePrefix.Length > 0;
NameEdit.Text = state.Name;
ColorSelector.Color = state.Color;
SelectScreen(_screens.ContainsKey(state.Screen) ? state.Screen : (ProtoId<StationAiScreenPrototype>) "StationAiScreenDefault");
_cooldownEnd = _timing.CurTime + TimeSpan.FromSeconds(state.CooldownSeconds);
UpdateCooldown();
}

private void Apply()
{
if (!TryGetNormalizedName(out var name) ||
!_screens.ContainsKey(_selectedScreen) ||
!StationAiCustomizationValidator.TryNormalizeColor(ColorSelector.Color, out var color))
{
UpdateApplyState();
return;
}

OnApply?.Invoke(name, _selectedScreen, color);
}

private void UpdatePreviewColor(Color color)
{
if (StationAiCustomizationValidator.TryNormalizeColor(color, out var normalized))
SelectedPreview.DisplayRect.Modulate = normalized;

UpdateApplyState();
}

private bool IsNameValid(string text)
{
return StationAiCustomizationValidator.TryNormalizeNamePart(
text,
_forceNamePrefix,
_configuration.GetCVar(CCVars.RestrictedNames),
_configuration.GetCVar(CCVars.ICNameCase),
out _,
out _);
}

private bool TryGetNormalizedName(out string name)
{
return StationAiCustomizationValidator.TryNormalizeNamePart(
NameEdit.Text,
_forceNamePrefix,
_configuration.GetCVar(CCVars.RestrictedNames),
_configuration.GetCVar(CCVars.ICNameCase),
out name,
out _);
}

protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
UpdateCooldown();
}

private void UpdateCooldown()
{
var remaining = Math.Max(0, (int) Math.Ceiling((_cooldownEnd - _timing.CurTime).TotalSeconds));
CooldownLabel.Visible = remaining > 0;
CooldownLabel.Text = remaining > 0
? Loc.GetString("station-ai-customization-cooldown", ("seconds", remaining))
: string.Empty;
UpdateApplyState(remaining);
}

private void UpdateApplyState(int? cooldown = null)
{
var invalidName = !TryGetNormalizedName(out _);
var invalidColor = !StationAiCustomizationValidator.TryNormalizeColor(ColorSelector.Color, out _);
ValidationLabel.Visible = invalidName || invalidColor;
ValidationLabel.Text = invalidColor
? Loc.GetString("station-ai-customization-invalid-color")
: invalidName
? Loc.GetString("station-ai-customization-invalid-name")
: string.Empty;

var remaining = cooldown ?? Math.Max(0, (int) Math.Ceiling((_cooldownEnd - _timing.CurTime).TotalSeconds));
ApplyButton.Disabled = remaining > 0 || invalidName || invalidColor || !_screens.ContainsKey(_selectedScreen);
}
}
57 changes: 57 additions & 0 deletions Content.Client/_Forge/Silicons/StationAi/StationAiScreenSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Content.Shared._Forge.Silicons.StationAi;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Shared.Prototypes;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.TypeSerializers.Implementations;

namespace Content.Client._Forge.Silicons.StationAi;

public sealed class StationAiScreenSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly IResourceCache _resources = default!;
[Dependency] private readonly SpriteSystem _sprites = default!;

public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationAiScreenComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<StationAiScreenComponent, AfterAutoHandleStateEvent>(OnState);
}

private void OnStartup(Entity<StationAiScreenComponent> ent, ref ComponentStartup args)
{
UpdateScreen(ent);
}

private void OnState(Entity<StationAiScreenComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateScreen(ent);
}

private void UpdateScreen(Entity<StationAiScreenComponent> ent)
{
if (!TryComp(ent.Owner, out SpriteComponent? sprite))
return;

if (!sprite.LayerMapTryGet(ent.Comp.ScreenLayer, out var layer))
return;

var path = ent.Comp.EmptySprite;
var state = ent.Comp.EmptyState;
if (ent.Comp.Occupied &&
(_prototypes.TryIndex(ent.Comp.Screen, out var prototype) ||
_prototypes.TryIndex(ent.Comp.DefaultScreen, out prototype)))
{
path = prototype.Sprite;
state = prototype.State;
}

if (!_resources.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / path, out var resource))
return;

_sprites.LayerSetRsi((ent.Owner, sprite), layer, resource.RSI, state);
_sprites.LayerSetColor((ent.Owner, sprite), layer, ent.Comp.Occupied ? ent.Comp.Color : Color.White);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Linq;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Players;
using Content.Shared.CCVar;
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Content.Shared.Players;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;

namespace Content.IntegrationTests.Tests._Forge.StationAi;

[TestFixture]
public sealed class StationAiGhostRoleTests
{
[Test]
public async Task GhostCommandReopensTakenCoreRole()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Dirty = true,
DummyTicker = false,
Connected = true,
});

var server = pair.Server;
var client = pair.Client;
var map = await pair.CreateTestMap();
var entityManager = server.ResolveDependency<IEntityManager>();
var playerManager = server.ResolveDependency<Robust.Server.Player.IPlayerManager>();
var console = client.ResolveDependency<IConsoleHost>();
var mindSystem = entityManager.System<SharedMindSystem>();
var ghostRoleSystem = entityManager.System<GhostRoleSystem>();
var session = playerManager.Sessions.Single();
var originalMind = session.ContentData()!.Mind!.Value;
server.CfgMan.SetCVar(CCVars.GhostQuickLottery, true);

EntityUid originalMob = default;
await server.WaitPost(() =>
{
originalMob = entityManager.SpawnEntity(null, map.GridCoords);
mindSystem.TransferTo(originalMind, originalMob, true);
});
await pair.RunTicksSync(10);

console.ExecuteCommand("ghost");
await pair.RunTicksSync(10);
Assert.That(entityManager.HasComponent<GhostComponent>(session.AttachedEntity), Is.True);

EntityUid brain = default;
await server.WaitPost(() =>
{
brain = entityManager.SpawnEntity("StationAiBrain", map.GridCoords);
var role = entityManager.GetComponent<GhostRoleComponent>(brain);
ghostRoleSystem.Request(session, role.Identifier);
});
await pair.RunTicksSync(60);

var brainRole = entityManager.GetComponent<GhostRoleComponent>(brain);
Assert.Multiple(() =>
{
Assert.That(session.AttachedEntity, Is.EqualTo(brain));
Assert.That(brainRole.Taken, Is.True);
Assert.That(ghostRoleSystem.GhostRoles.Select(role => role.Owner), Does.Not.Contain(brain));
});

console.ExecuteCommand("ghost");
await pair.RunTicksSync(10);

Assert.Multiple(() =>
{
Assert.That(entityManager.HasComponent<GhostComponent>(session.AttachedEntity), Is.True);
Assert.That(brainRole.Taken, Is.False);
Assert.That(ghostRoleSystem.GhostRoles.Select(role => role.Owner), Does.Contain(brain));
});

await server.WaitPost(() =>
{
ghostRoleSystem.Request(session, brainRole.Identifier);
Assert.DoesNotThrow(() => ghostRoleSystem.GetGhostRolesInfo(session));
ghostRoleSystem.LeaveRaffle(session, brainRole.Identifier);
});
await pair.RunTicksSync(2);
server.CfgMan.SetCVar(CCVars.GhostQuickLottery, false);

await pair.CleanReturnAsync();
}
}
Loading
Loading