Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ab138a8
massive ai features rework
theg33ka Jul 15, 2026
0463475
/ghost теперь создаёт MindRemovedMessage для ядра
theg33ka Jul 15, 2026
1edad09
screens+fixes
theg33ka Jul 15, 2026
8f21eb3
fix: harden station AI lifecycle and camera features
theg33ka Jul 15, 2026
58085ec
fix: keep AI validator sandbox-compatible
theg33ka Jul 16, 2026
3c4069b
fix: reopen station AI role after ghosting
theg33ka Jul 16, 2026
48b5ae6
fix: reset AI ghost role raffle lifecycle
theg33ka Jul 16, 2026
a20bdef
fix: initialize repeated AI role raffles
theg33ka Jul 16, 2026
be3008c
fix: reference server AI personality component
theg33ka Jul 16, 2026
15209b1
fix: restore station AI camera speech registration
theg33ka Jul 16, 2026
3b37f40
fix: route station AI speech through cameras
theg33ka Jul 16, 2026
975bf30
feat: refine station AI speech and name prefixes
theg33ka Jul 16, 2026
fcca06c
fix: keep station AI core identity prototype-safe
theg33ka Jul 16, 2026
d035fc2
Merge branch 'main' into AI-features
theg33ka Jul 24, 2026
70271a1
Ребаланс цен и содержимого вендоматов (#337)
Rukilator Jul 27, 2026
b1d9722
Revert "Tel-Adi: контент-пак корпорации (компания, снаряжение, флот, …
Rukilator Jul 27, 2026
0380eb0
Минорные фиксы и правки (#333)
gordod3 Jul 27, 2026
11f6573
Merge branch 'main' into AI-features
mersen-tyn Jul 27, 2026
1667b3e
Фикс болтов 2 - не будут выпадать из карманов или оружия (#331)
gordod3 Jul 27, 2026
cd3003a
Fix fire from puddles (#330)
DRON5011 Jul 27, 2026
e952ae5
Fix Helmets for Vulps (#322)
DRON5011 Jul 27, 2026
047c640
Merge branch 'main' into AI-features
theg33ka Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
11 changes: 1 addition & 10 deletions Content.Client/Silicons/StationAi/StationAiOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,7 @@ protected override void Draw(in OverlayDrawArgs args)
var worldBounds = args.WorldBounds;

var playerEnt = _player.LocalEntity;

// Forge-change: reveal vision on the grid the AI's eye (view target) is on, not the player
// entity's grid. This lets the vision follow the eye when it's projected onto another grid
// (e.g. a remote shuttle via a Soulkiller server). For a normal AI the eye shares the
// player's grid, so this is a no-op.
var viewEnt = playerEnt;
if (_entManager.TryGetComponent(playerEnt, out EyeComponent? eyeComp) && eyeComp.Target is { } eyeTarget)
viewEnt = eyeTarget;

_entManager.TryGetComponent(viewEnt, out TransformComponent? playerXform);
_entManager.TryGetComponent(playerEnt, out TransformComponent? playerXform);
var gridUid = playerXform?.GridUid ?? EntityUid.Invalid;
_entManager.TryGetComponent(gridUid, out MapGridComponent? grid);
_entManager.TryGetComponent(gridUid, out BroadphaseComponent? broadphase);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ public void SetGauntlets(Entity<ModsuitGauntletToolsComponent> gauntlets)
if (comp.EnabledSlots.HasFlag(ModsuitGauntletEnabledSlots.Auxiliary) && !comp.AuxiliaryInHand)
AddToolButton(main, ModsuitGauntletToolSlot.Auxiliary, comp.AuxiliaryProto, comp.UsePrototypeMenuIcons);

if (comp.EnabledSlots.HasFlag(ModsuitGauntletEnabledSlots.Rcd) && !comp.RcdInHand)
AddToolButton(main, ModsuitGauntletToolSlot.Rcd, comp.RcdProto, comp.UsePrototypeMenuIcons);
if (comp.EnabledSlots.HasFlag(ModsuitGauntletEnabledSlots.Piping) && !comp.PipingInHand)
AddToolButton(main, ModsuitGauntletToolSlot.Piping, comp.PipingProto, comp.UsePrototypeMenuIcons);

Expand Down Expand Up @@ -133,11 +131,6 @@ private static bool TryGetActiveSlot(ModsuitGauntletToolsComponent comp, out Mod
return true;
}

if (comp.RcdInHand && SharedModsuitGauntletToolsSystem.IsSlotEnabled(comp, ModsuitGauntletToolSlot.Rcd))
{
slot = ModsuitGauntletToolSlot.Rcd;
return true;
}
if (comp.PipingInHand && SharedModsuitGauntletToolsSystem.IsSlotEnabled(comp, ModsuitGauntletToolSlot.Piping))
{
slot = ModsuitGauntletToolSlot.Piping;
Expand Down
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);
}
}
Loading
Loading