Port goobstation factorio (#4035)

* Initial port of goobstation factorio, missing disposals integration and faxing. Also ports impstations modification for robotic arms to have static power draw. Also adds automation slots to silos and advanced microwave.

* Ports goobstation factorio fax automation, adds to the guidebook entry info about gas canisters.

* Ported Goob Disposals. Removed part about taking
mats out of storage silo cuz it ain't implemented
yet. Seems to work.

* Adds constructor circuitboard to research cuz I
forgor
This commit is contained in:
AlgisAlphonse 2025-07-27 14:31:36 +02:00 committed by GitHub
parent e18cda9df8
commit f7db03182a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
131 changed files with 5550 additions and 83 deletions

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory;
namespace Content.Client._Goobstation.Factory;
public sealed class ConstructorSystem : SharedConstructorSystem;

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory;
namespace Content.Client._Goobstation.Factory;
public sealed class InteractorSystem : SharedInteractorSystem;

View File

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;
namespace Content.Client._Goobstation.Factory;
/// <summary>
/// Animations robotic arm's arm layer swinging.
/// Can't be done with engine AnimationPlayer as it can't animate individual layers.
/// </summary>
public sealed class RoboticArmAnimationSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
public override void FrameUpdate(float frameTime)
{
var query = EntityQueryEnumerator<RoboticArmComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.ItemSlot == null)
continue;
if (comp.NextMove is {} nextMove)
Animate((uid, comp), nextMove);
else
Reset((uid, comp));
}
}
private void Animate(Entity<RoboticArmComponent> ent, TimeSpan nextMove)
{
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
var started = nextMove - ent.Comp.MoveDelay;
// 0-1 unless something weird happens
var progress = (_timing.CurTime - started) / ent.Comp.MoveDelay;
if (!ent.Comp.HasItem) // returning to the resting position when emptied
progress = 1f - progress;
var angle = Angle.FromDegrees(progress * 180f);
sprite.LayerSetRotation(RoboticArmLayers.Arm, angle);
}
private void Reset(Entity<RoboticArmComponent> ent)
{
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
var angle = ent.Comp.HasItem ? new Angle(Math.PI) : Angle.Zero;
sprite.LayerSetRotation(RoboticArmLayers.Arm, angle);
}
}

View File

@ -0,0 +1,193 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Client.Construction;
using Content.Client.Construction.UI;
using Content.Shared._Goobstation.Factory;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Whitelist;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
using System.Linq;
namespace Content.Client._Goobstation.Factory.UI;
public sealed class ConstructorBUI : BoundUserInterface
{
[Dependency] private readonly IPrototypeManager _proto = default!;
private readonly ConstructionSystem _construction;
private readonly EntityWhitelistSystem _whitelist;
private readonly SpriteSystem _sprite;
private ConstructionMenu? _menu;
private string? _id;
private List<ConstructionMenu.ConstructionMenuListData> _recipes = new();
private readonly LocId _favoriteCatName = "construction-category-favorites";
private readonly LocId _forAllCategoryName = "construction-category-all";
public ConstructorBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_construction = EntMan.System<ConstructionSystem>();
_whitelist = EntMan.System<EntityWhitelistSystem>();
_sprite = EntMan.System<SpriteSystem>();
_id = EntMan.GetComponentOrNull<ConstructorComponent>(owner)?.Construction;
}
protected override void Open()
{
base.Open();
// god BLESS whoever made construction ui for having it so decoupled <3
_menu = this.CreateWindow<ConstructionMenu>();
PopulateCategories();
PopulateRecipes(string.Empty, string.Empty);
_menu.PopulateRecipes += (_, args) => PopulateRecipes(args.Item1, args.Item2);
_menu.RecipeSelected += (_, item) =>
{
_menu.ClearRecipeInfo();
if (item != null && item.Prototype != null)
{
_id = item.Prototype.ID;
_menu.SetRecipeInfo(item.Prototype.Name ?? "", item.Prototype.Description ?? "", item?.TargetPrototype,
item!.Prototype.Type != ConstructionType.Item, true); // TODO: favourites
GenerateStepList(item.Prototype);
}
else
{
_id = null;
}
};
_menu.BuildButtonToggled += (_, _) =>
{
SendPredictedMessage(new ConstructorSetProtoMessage(_id));
_menu.Close();
};
}
private void PopulateCategories(string? selected = null)
{
if (_menu is not {} menu)
return;
var categories = new HashSet<string>();
foreach (var prototype in _proto.EnumeratePrototypes<ConstructionPrototype>())
{
var category = prototype.Category;
if (!string.IsNullOrEmpty(category))
categories.Add(category);
}
var categoriesArray = new string[categories.Count + 1];
// hard-coded to show all recipes
var idx = 0;
categoriesArray[idx++] = _forAllCategoryName;
foreach (var cat in categories.OrderBy(Loc.GetString))
{
categoriesArray[idx++] = cat;
}
menu.OptionCategories.Clear();
for (var i = 0; i < categoriesArray.Length; i++)
{
menu.OptionCategories.AddItem(Loc.GetString(categoriesArray[i]), i);
if (!string.IsNullOrEmpty(selected) && selected == categoriesArray[i])
menu.OptionCategories.SelectId(i);
}
menu.Categories = categoriesArray;
}
// copypasted and optimised from ConstructionMenuPresenter
private void PopulateRecipes(string search, string category)
{
if (PlayerManager.LocalEntity is not { } user
|| _menu is not { } menu)
return;
search = search.Trim().ToLowerInvariant();
var searching = !string.IsNullOrEmpty(search);
var isEmptyCategory = string.IsNullOrEmpty(category) || category == _forAllCategoryName;
_recipes.Clear();
foreach (var recipe in _proto.EnumeratePrototypes<ConstructionPrototype>())
{
if (recipe.Hide)
continue;
if (_whitelist.IsWhitelistFail(recipe.EntityWhitelist, user))
continue;
if (searching
&& recipe.Name != null
&& !recipe.Name.ToLowerInvariant().Contains(search))
continue;
if (!isEmptyCategory)
{
// TODO: when favourites get sent from server do this
// currently its specific to the G menu
//if (!_favoritedRecipes.Contains(recipe))
if (category == _favoriteCatName)
continue;
else if (recipe.Category != category)
continue;
}
if (!_construction!.TryGetRecipePrototype(recipe.ID, out var targetProtoId))
continue;
if (!_proto.TryIndex(targetProtoId, out EntityPrototype? proto))
continue;
_recipes.Add(new(recipe, proto));
}
_recipes.Sort((a, b) => string.Compare(a.Prototype.Name, b.Prototype.Name, StringComparison.InvariantCulture));
var recipesList = menu.Recipes;
recipesList.PopulateList(_recipes);
menu.RecipesGridScrollContainer.Visible = false;
menu.Recipes.Visible = true;
}
private void GenerateStepList(ConstructionPrototype proto)
{
if (_construction.GetGuide(proto) is not { } guide
|| _menu is not { } menu)
return;
var list = menu.RecipeStepList;
foreach (var entry in guide.Entries)
{
var text = entry.Arguments != null
? Loc.GetString(entry.Localization, entry.Arguments)
: Loc.GetString(entry.Localization);
if (entry.EntryNumber is { } number)
text = Loc.GetString("construction-presenter-step-wrapper",
("step-number", number), ("text", text));
// The padding needs to be applied regardless of text length... (See PadLeft documentation)
text = text.PadLeft(text.Length + entry.Padding);
var icon = entry.Icon != null ? _sprite.Frame0(entry.Icon) : Texture.Transparent;
list.AddItem(text, icon, false);
}
}
}

View File

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory.Filters;
using Robust.Client.UserInterface;
namespace Content.Client._Goobstation.Factory.UI;
public sealed class LabelFilterBUI : BoundUserInterface
{
private LabelFilterWindow? _window;
public LabelFilterBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<LabelFilterWindow>();
_window.SetEntity(Owner);
_window.OnSetLabel += label => SendPredictedMessage(new LabelFilterSetLabelMessage(label));
}
}

View File

@ -0,0 +1,6 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="using:Content.Client.UserInterface.Controls"
Title="{Loc 'label-filter-window-title'}"
MinSize="300 100">
<LineEdit Name="LabelEdit" PlaceHolder="{Loc 'label-filter-placeholder'}"/>
</controls:FancyWindow>

View File

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Client.UserInterface.Controls;
using Content.Shared._Goobstation.Factory.Filters;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Goobstation.Factory.UI;
[GenerateTypedNameReferences]
public sealed partial class LabelFilterWindow : FancyWindow
{
[Dependency] private readonly EntityManager _entMan = default!;
public event Action<string>? OnSetLabel;
public LabelFilterWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
LabelEdit.OnTextChanged += _ => OnSetLabel?.Invoke(LabelEdit.Text);
}
public void SetEntity(EntityUid uid)
{
if (!_entMan.TryGetComponent<LabelFilterComponent>(uid, out var comp))
return;
var max = comp.MaxLength;
LabelEdit.IsValid = label => label.Length < max;
LabelEdit.Text = comp.Label;
}
}

View File

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory.Filters;
using Robust.Client.UserInterface;
namespace Content.Client._Goobstation.Factory.UI;
public sealed class NameFilterBUI : BoundUserInterface
{
private NameFilterWindow? _window;
public NameFilterBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<NameFilterWindow>();
_window.SetEntity(Owner);
_window.OnSetName += name => SendPredictedMessage(new NameFilterSetNameMessage(name));
_window.OnSetMode += mode => SendPredictedMessage(new NameFilterSetModeMessage(mode));
}
}

View File

@ -0,0 +1,9 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="using:Content.Client.UserInterface.Controls"
Title="{Loc 'name-filter-window-title'}"
MinSize="350 100">
<BoxContainer Orientation="Horizontal">
<OptionButton Name="ModeButton" MaxHeight="50"/>
<LineEdit Name="NameEdit" HorizontalExpand="True"/>
</BoxContainer>
</controls:FancyWindow>

View File

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Client.UserInterface.Controls;
using Content.Shared._Goobstation.Factory.Filters;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Goobstation.Factory.UI;
[GenerateTypedNameReferences]
public sealed partial class NameFilterWindow : FancyWindow
{
[Dependency] private readonly EntityManager _entMan = default!;
public event Action<string>? OnSetName;
public event Action<NameFilterMode>? OnSetMode;
public NameFilterWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
foreach (var mode in Enum.GetValues<NameFilterMode>())
{
ModeButton.AddItem(Loc.GetString($"name-filter-mode-{mode}"), (int) mode);
}
ModeButton.OnItemSelected += args =>
{
ModeButton.SelectId(args.Id);
OnSetMode?.Invoke((NameFilterMode) args.Id);
};
NameEdit.OnTextChanged += _ => OnSetName?.Invoke(NameEdit.Text);
}
public void SetEntity(EntityUid uid)
{
if (!_entMan.TryGetComponent<NameFilterComponent>(uid, out var comp))
return;
ModeButton.SelectId((int) comp.Mode);
var max = comp.MaxLength;
NameEdit.IsValid = name => name.Length < max;
NameEdit.Text = comp.Name;
}
}

View File

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory.Filters;
using Robust.Client.UserInterface;
namespace Content.Client._Goobstation.Factory.UI;
public sealed class PressureFilterBUI : BoundUserInterface
{
private PressureFilterWindow? _window;
public PressureFilterBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<PressureFilterWindow>();
_window.SetEntity(Owner);
_window.OnSetMin += min => SendPredictedMessage(new PressureFilterSetMinMessage(min));
_window.OnSetMax += max => SendPredictedMessage(new PressureFilterSetMaxMessage(max));
}
}

View File

@ -0,0 +1,19 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="using:Content.Client.UserInterface.Controls"
Title="{Loc 'pressure-filter-window-title'}"
MinSize="350 150">
<BoxContainer Orientation="Vertical" Align="Center" Margin="10">
<BoxContainer Orientation="Horizontal" Margin="5">
<Label Text="{Loc 'pressure-filter-min-pressure'}" Margin="5 0"/>
<LineEdit Name="MinEdit" PlaceHolder="0" HorizontalExpand="True"/>
<Label Text="{Loc 'units-k-pascal'}" MinWidth="40"/>
<Button Name="MinConfirmButton" Text="{Loc 'generic-confirm'}" MaxSize="100 50"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="5">
<Label Text="{Loc 'pressure-filter-max-pressure'}" Margin="5 0"/>
<LineEdit Name="MaxEdit" PlaceHolder="101.325" HorizontalExpand="True"/>
<Label Text="{Loc 'units-k-pascal'}" MinWidth="40"/>
<Button Name="MaxConfirmButton" Text="{Loc 'generic-confirm'}" MaxSize="100 50"/>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Client.UserInterface.Controls;
using Content.Shared._Goobstation.Factory.Filters;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Goobstation.Factory.UI;
[GenerateTypedNameReferences]
public sealed partial class PressureFilterWindow : FancyWindow
{
[Dependency] private readonly EntityManager _entMan = default!;
public event Action<float>? OnSetMin;
public event Action<float>? OnSetMax;
private float _min, _max;
public PressureFilterWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
MinEdit.OnTextChanged += _ => UpdateButtons();
MinConfirmButton.OnPressed += _ =>
{
if (float.TryParse(MinEdit.Text, out var min))
OnSetMin?.Invoke(min);
};
MaxEdit.OnTextChanged += _ => UpdateButtons();
MaxConfirmButton.OnPressed += _ =>
{
if (float.TryParse(MaxEdit.Text, out var max))
OnSetMax?.Invoke(max);
};
OnSetMin += min => { _min = min; UpdateButtons(); };
OnSetMax += max => { _max = max; UpdateButtons(); };
}
public void SetEntity(EntityUid uid)
{
if (!_entMan.TryGetComponent<PressureFilterComponent>(uid, out var comp))
return;
_min = comp.Min;
_max = comp.Max;
MinEdit.Text = _min.ToString();
MaxEdit.Text = _max.ToString();
UpdateButtons();
}
private void UpdateButtons()
{
MinConfirmButton.Disabled = !float.TryParse(MinEdit.Text, out var min) || min < 0f || min > _max || min == _min;
MaxConfirmButton.Disabled = !float.TryParse(MaxEdit.Text, out var max) || max < _min || max == _max;
}
}

View File

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory.Filters;
using Robust.Client.UserInterface;
namespace Content.Client._Goobstation.Factory.UI;
public sealed class StackFilterBUI : BoundUserInterface
{
private StackFilterWindow? _window;
public StackFilterBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<StackFilterWindow>();
_window.SetEntity(Owner);
_window.OnSetMin += min => SendPredictedMessage(new StackFilterSetMinMessage(min));
_window.OnSetSize += size => SendPredictedMessage(new StackFilterSetSizeMessage(size));
}
}

View File

@ -0,0 +1,17 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="using:Content.Client.UserInterface.Controls"
Title="{Loc 'stack-filter-window-title'}"
MinSize="280 180">
<BoxContainer Orientation="Vertical" Align="Center" Margin="10">
<BoxContainer Orientation="Horizontal" Margin="5">
<Label Text="{Loc 'stack-filter-min-stack-size'}"/>
<LineEdit Name="MinEdit" PlaceHolder="1" HorizontalExpand="True" MaxWidth="40"/>
<Button Name="MinConfirmButton" Text="{Loc 'generic-confirm'}" MaxSize="100 50"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="5">
<Label Text="{Loc 'stack-filter-stack-chunk-size'}"/>
<LineEdit Name="SizeEdit" PlaceHolder="1" HorizontalExpand="True" MaxWidth="40"/>
<Button Name="SizeConfirmButton" Text="{Loc 'generic-confirm'}" MaxSize="100 50"/>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Client.UserInterface.Controls;
using Content.Shared._Goobstation.Factory.Filters;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Goobstation.Factory.UI;
[GenerateTypedNameReferences]
public sealed partial class StackFilterWindow : FancyWindow
{
[Dependency] private readonly EntityManager _entMan = default!;
public event Action<int>? OnSetMin;
public event Action<int>? OnSetSize;
public StackFilterWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
MinEdit.OnTextChanged += _ =>
{
MinConfirmButton.Disabled = !int.TryParse(MinEdit.Text, out var min) || min < 1;
};
MinConfirmButton.OnPressed += _ =>
{
if (int.TryParse(MinEdit.Text, out var min))
OnSetMin?.Invoke(min);
};
SizeEdit.OnTextChanged += _ =>
{
SizeConfirmButton.Disabled = !int.TryParse(SizeEdit.Text, out var size) || size < 0;
};
SizeConfirmButton.OnPressed += _ =>
{
if (int.TryParse(SizeEdit.Text, out var size))
OnSetSize?.Invoke(size);
};
}
public void SetEntity(EntityUid uid)
{
if (!_entMan.TryGetComponent<StackFilterComponent>(uid, out var comp))
return;
var min = comp.Min;
MinEdit.Text = min.ToString();
var size = comp.Size;
SizeEdit.Text = size.ToString();
}
}

View File

@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Client.Guidebook.Controls;
using Content.Client.Guidebook.Richtext;
using Content.Shared._Goobstation.Factory;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using System.Diagnostics.CodeAnalysis;
namespace Content.Client._Goobstation.Guidebook.Controls;
/// <summary>
/// Lists all entities with <see cref="AutomationSlotsComponent"/>.
/// </summary>
public sealed partial class GuideAutomationSlotsEmbed : IDocumentTag
{
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly AutomationSystem _automation;
public GuideAutomationSlotsEmbed()
{
IoCManager.InjectDependencies(this);
_automation = _entMan.System<AutomationSystem>();
}
bool IDocumentTag.TryParseTag(Dictionary<string, string> args, [NotNullWhen(true)] out Control? control)
{
var scroll = new ScrollContainer()
{
MinHeight = 200f,
MaxHeight = 400f
};
var box = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true
};
foreach (var id in _automation.Automatable)
{
box.AddChild(new GuideEntityEmbed(id, false, true));
}
scroll.AddChild(box);
control = scroll;
return true;
}
}

View File

@ -2,6 +2,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Construction.Components;
using Content.Shared._Goobstation.Construction;
using Content.Shared.ActionBlocker;
using Content.Shared.Construction;
using Content.Shared.Construction.Prototypes;
@ -13,6 +14,7 @@ using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Mind.Components; // Goobstation
using Content.Shared.Storage;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
@ -62,12 +64,17 @@ namespace Content.Server.Construction
yield return item;
}
// <Goobstation> - lets slimepeople and constructors use their storageAdd commentMore actions
if (TryComp<StorageComponent>(user, out var userStorage))
foreach (var userItem in userStorage.Container.ContainedEntities!)
yield return userItem;
// </Goobstation>
if (_inventorySystem.TryGetContainerSlotEnumerator(user, out var containerSlotEnumerator))
{
while (containerSlotEnumerator.MoveNext(out var containerSlot))
{
if(!containerSlot.ContainedEntity.HasValue)
if (!containerSlot.ContainedEntity.HasValue)
continue;
if (EntityManager.TryGetComponent(containerSlot.ContainedEntity.Value, out StorageComponent? storage))
@ -360,7 +367,8 @@ namespace Content.Server.Construction
if (!_actionBlocker.CanInteract(user, null))
return false;
if (!HasComp<HandsComponent>(user))
if (HasComp<MindContainerComponent>(user)
&& !HasComp<HandsComponent>(user)) // goobstation - don't require hands for constructor
return false;
foreach (var condition in constructionPrototype.Conditions)
@ -403,6 +411,11 @@ namespace Content.Server.Construction
Transform(user).Coordinates) is not { Valid: true } item)
return false;
// <Goobstation>
var constructedEv = new ConstructedEvent(item);
RaiseLocalEvent(user, ref constructedEv);
// </Goobstation>
// Just in case this is a stack, attempt to merge it. If it isn't a stack, this will just normally pick up
// or drop the item as normal.
_stackSystem.TryMergeToHands(item, user);
@ -412,78 +425,96 @@ namespace Content.Server.Construction
// LEGACY CODE. See warning at the top of the file!
private async void HandleStartStructureConstruction(TryStartStructureConstructionMessage ev, EntitySessionEventArgs args)
{
if (!PrototypeManager.TryIndex(ev.PrototypeName, out ConstructionPrototype? constructionPrototype))
// <Goobstation> - use public API
if (args.SenderSession.AttachedEntity is {} user)
await TryStartStructureConstruction(user,
ev.PrototypeName,
GetCoordinates(ev.Location),
ev.Angle,
ev.Ack,
args.SenderSession);
}
/// <summary>
/// Goobstation - Taken out of HandleStartStructureConstruction
/// Changed to return false and only send the ack event to the user.
/// </summary>
public async Task<bool> TryStartStructureConstruction(EntityUid user,
string prototypeName,
EntityCoordinates location,
Angle angle,
int ack = 0,
ICommonSession? senderSession = null)
{
// </Goobstation>
if (!PrototypeManager.TryIndex(prototypeName, out ConstructionPrototype? constructionPrototype))
{
Log.Error($"Tried to start construction of invalid recipe '{ev.PrototypeName}'!");
RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack));
return;
Log.Error($"Tried to start construction of invalid recipe '{prototypeName}'!");
RaiseNetworkEvent(new AckStructureConstructionMessage(ack), user);
return false;
}
if (!PrototypeManager.TryIndex(constructionPrototype.Graph, out ConstructionGraphPrototype? constructionGraph))
{
Log.Error($"Invalid construction graph '{constructionPrototype.Graph}' in recipe '{ev.PrototypeName}'!");
RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack));
return;
}
if (args.SenderSession.AttachedEntity is not {Valid: true} user)
{
Log.Error($"Client sent {nameof(TryStartStructureConstructionMessage)} with no attached entity!");
return;
Log.Error($"Invalid construction graph '{constructionPrototype.Graph}' in recipe '{prototypeName}'!");
RaiseNetworkEvent(new AckStructureConstructionMessage(ack), user);
return false;
}
if (_whitelistSystem.IsWhitelistFail(constructionPrototype.EntityWhitelist, user))
{
_popup.PopupEntity(Loc.GetString("construction-system-cannot-start"), user, user);
return;
return false;
}
if (_container.IsEntityInContainer(user))
{
_popup.PopupEntity(Loc.GetString("construction-system-inside-container"), user, user);
return;
return false;
}
var startNode = constructionGraph.Nodes[constructionPrototype.StartNode];
var targetNode = constructionGraph.Nodes[constructionPrototype.TargetNode];
var pathFind = constructionGraph.Path(startNode.Name, targetNode.Name);
if (_beingBuilt.TryGetValue(args.SenderSession, out var set))
if (senderSession is {} session) // Goobstation - ignore check for constructor
{
if (!set.Add(ev.Ack))
if (_beingBuilt.TryGetValue(session, out var set))
{
_popup.PopupEntity(Loc.GetString("construction-system-already-building"), user, user);
return;
if (!set.Add(ack))
{
_popup.PopupEntity(Loc.GetString("construction-system-already-building"), user, user);
return false;
}
}
else
{
var newSet = new HashSet<int> {ack};
_beingBuilt[session] = newSet;
}
}
else
{
var newSet = new HashSet<int> {ev.Ack};
_beingBuilt[args.SenderSession] = newSet;
}
var location = GetCoordinates(ev.Location);
foreach (var condition in constructionPrototype.Conditions)
{
if (!condition.Condition(user, location, ev.Angle.GetCardinalDir()))
if (!condition.Condition(user, location, angle.GetCardinalDir()))
{
Cleanup();
return;
return false;
}
}
void Cleanup()
{
_beingBuilt[args.SenderSession].Remove(ev.Ack);
if (senderSession is {} session) // Goobstation - not added for constructor
_beingBuilt[session].Remove(ack);
}
HandsComponent? hands = null; // Goobstation
if (!_actionBlocker.CanInteract(user, null)
|| !EntityManager.TryGetComponent(user, out HandsComponent? hands) || hands.ActiveHandEntity == null)
|| (senderSession != null && EntityManager.TryGetComponent(user, out hands) && hands.ActiveHandEntity == null)) // Goobstation - dont check hands for constructor
{
Cleanup();
return;
return false;
}
var mapPos = _transformSystem.ToMapCoordinates(location);
@ -492,64 +523,73 @@ namespace Content.Server.Construction
if (!_interactionSystem.InRangeUnobstructed(user, mapPos, predicate: predicate))
{
Cleanup();
return;
return false;
}
if (pathFind == null)
throw new InvalidDataException($"Can't find path from starting node to target node in construction! Recipe: {ev.PrototypeName}");
throw new InvalidDataException($"Can't find path from starting node to target node in construction! Recipe: {prototypeName}");
var edge = startNode.GetEdge(pathFind[0].Name);
if(edge == null)
throw new InvalidDataException($"Can't find edge from starting node to the next node in pathfinding! Recipe: {ev.PrototypeName}");
throw new InvalidDataException($"Can't find edge from starting node to the next node in pathfinding! Recipe: {prototypeName}");
var valid = false;
if (hands.ActiveHandEntity is not {Valid: true} holding)
if (senderSession != null) // Goobstation - don't check this for constructor machine
{
Cleanup();
return;
}
var valid = false;
// No support for conditions here!
foreach (var step in edge.Steps)
{
switch (step)
if (hands?.ActiveHandEntity is not { Valid: true } holding) // Goobstation - don't check for constructor machine
{
case EntityInsertConstructionGraphStep entityInsert:
if (entityInsert.EntityValid(holding, EntityManager, _factory))
valid = true;
Cleanup();
return false;
}
// No support for conditions here!
foreach (var step in edge.Steps)
{
switch (step)
{
case EntityInsertConstructionGraphStep entityInsert:
if (entityInsert.EntityValid(holding, EntityManager, _factory))
valid = true;
break;
case ToolConstructionGraphStep _:
throw new InvalidDataException("Invalid first step for item recipe!");
}
if (valid)
break;
case ToolConstructionGraphStep _:
throw new InvalidDataException("Invalid first step for item recipe!");
}
if (valid)
break;
if (!valid)
{
Cleanup();
return false;
}
}
if (!valid)
{
Cleanup();
return;
}
if (await Construct(user,
(ev.Ack + constructionPrototype.GetHashCode()).ToString(),
(ack + constructionPrototype.GetHashCode()).ToString(),
constructionGraph,
edge,
targetNode,
GetCoordinates(ev.Location),
constructionPrototype.CanRotate ? ev.Angle : Angle.Zero) is not {Valid: true} structure)
location,
constructionPrototype.CanRotate ? angle : Angle.Zero) is not {Valid: true} structure)
{
Cleanup();
return;
return false;
}
RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack, GetNetEntity(structure)));
_adminLogger.Add(LogType.Construction, LogImpact.Low, $"{ToPrettyString(user):player} has turned a {ev.PrototypeName} construction ghost into {ToPrettyString(structure)} at {Transform(structure).Coordinates}");
// <Goobstation>
var constructedEv = new ConstructedEvent(structure);
RaiseLocalEvent(user, ref constructedEv);
// </Goobstation>
RaiseNetworkEvent(new AckStructureConstructionMessage(ack, GetNetEntity(structure)), user);
_adminLogger.Add(LogType.Construction, LogImpact.Low, $"{ToPrettyString(user):player} has turned a {prototypeName} construction ghost into {ToPrettyString(structure)} at {Transform(structure).Coordinates}");
Cleanup();
return true;
}
}
}

View File

@ -148,7 +148,12 @@ public sealed class FaxSystem : EntitySystem
private void OnComponentInit(EntityUid uid, FaxMachineComponent component, ComponentInit args)
{
_itemSlotsSystem.AddItemSlot(uid, PaperSlotId, component.PaperSlot);
// <Goobstation> - define the slot in ItemSlots instead of adding it
if (_itemSlotsSystem.TryGetSlot(uid, PaperSlotId, out var slot))
component.PaperSlot = slot;
else
_itemSlotsSystem.AddItemSlot(uid, PaperSlotId, component.PaperSlot);
// </Goobstation>
UpdateAppearance(uid, component);
}
@ -480,6 +485,9 @@ public sealed class FaxSystem : EntitySystem
UpdateUserInterface(uid, component);
if (!args.Actor.IsValid()) // Goobstation - no log for automation
return;
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(args.Actor):actor} " +
@ -543,6 +551,7 @@ public sealed class FaxSystem : EntitySystem
_deviceNetworkSystem.QueuePacket(uid, component.DestinationFaxAddress, payload);
if (!args.Actor.IsValid()) // Goobstation - no log for automation
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(args.Actor):actor} " +

View File

@ -107,6 +107,9 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
("machine", receiver),
("item", toInsert)),
receiver);
if (user != receiver) // Goobstation - for automation to not spam popups
_popup.PopupEntity(Loc.GetString("machine-insert-item", ("user", user), ("machine", receiver),
("item", toInsert)), receiver);
QueueDel(toInsert);
// Logging

View File

@ -161,9 +161,9 @@ namespace Content.Server.Power.EntitySystems
return !_recQuery.Resolve(uid, ref receiver, false) || receiver.Powered;
}
public void SetLoad(ApcPowerReceiverComponent comp, float load)
public override void SetLoad(SharedApcPowerReceiverComponent comp, float load) // Goobstation - override shared method
{
comp.Load = load;
((ApcPowerReceiverComponent) comp).Load = load; // Goobstation
}
public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component)

View File

@ -40,7 +40,7 @@ namespace Content.Server.Stack
/// <summary>
/// Try to split this stack into two. Returns a non-null <see cref="Robust.Shared.GameObjects.EntityUid"/> if successful.
/// </summary>
public EntityUid? Split(EntityUid uid, int amount, EntityCoordinates spawnPosition, StackComponent? stack = null)
public override EntityUid? Split(EntityUid uid, int amount, EntityCoordinates spawnPosition, StackComponent? stack = null) // Goobstation - override virtual method
{
if (!Resolve(uid, ref stack))
return null;

View File

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;
namespace Content.Server._Goobstation.Atmos.EntitySystems;
/// <summary>
/// Handles control signals for automated gas canisters.
/// </summary>
public sealed class GasCanisterSignalSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasCanisterComponent, SignalReceivedEvent>(OnSignalReceived);
}
private void OnSignalReceived(Entity<GasCanisterComponent> ent, ref SignalReceivedEvent args)
{
var valve = args.Port switch
{
"Open" => true,
"Close" => false,
"Toggle" => !ent.Comp.ReleaseValve,
_ => false // fuck you c# cant just return
};
if (ent.Comp.ReleaseValve == valve)
return;
var ev = new GasCanisterChangeReleaseValveMessage(valve);
ev.UiKey = GasCanisterUiKey.Key;
if (args.Trigger is {} actor)
ev.Actor = actor;
RaiseLocalEvent(ent, ev);
}
}

View File

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Construction.Components;
using Content.Shared.DeviceLinking;
using Robust.Shared.Prototypes;
namespace Content.Server._Goobstation.Construction;
public sealed class FlatpackSignalSystem : EntitySystem
{
public static readonly ProtoId<SinkPortPrototype> OnPort = "On";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FlatpackCreatorComponent, SignalReceivedEvent>(OnSignalReceived);
}
private void OnSignalReceived(Entity<FlatpackCreatorComponent> ent, ref SignalReceivedEvent args)
{
if (args.Port != OnPort)
return;
// supercode has no API so we have to do this
var ev = new FlatpackCreatorStartPackBuiMessage();
RaiseLocalEvent(ent, ev);
}
}

View File

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.DeviceLinking.Events;
using Content.Server.Power.EntitySystems;
using Content.Shared.DeviceLinking;
using Content.Shared.Disposal.Unit;
using Robust.Shared.Prototypes;
using Content.Shared.Disposal.Components;
using Content.Server.Disposal.Unit;
namespace Content.Server._Goobstation.Disposals;
public sealed class DisposalSignalSystem : EntitySystem
{
[Dependency] private readonly DisposalUnitSystem _disposal = default!;
[Dependency] private readonly PowerReceiverSystem _power = default!;
public static readonly ProtoId<SinkPortPrototype> FlushPort = "DisposalFlush";
public static readonly ProtoId<SinkPortPrototype> EjectPort = "DisposalEject";
public static readonly ProtoId<SinkPortPrototype> TogglePort = "Toggle";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalUnitComponent, SignalReceivedEvent>(OnSignalReceived);
}
private void OnSignalReceived(Entity<DisposalUnitComponent> ent, ref SignalReceivedEvent args)
{
if (args.Port == FlushPort)
_disposal.ToggleEngage(ent, ent);
else if (args.Port == EjectPort)
_disposal.TryEjectContents(ent, ent);
else if (args.Port == TogglePort)
_power.TogglePower(ent);
}
}

View File

@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory;
using Content.Server.Construction;
using Content.Shared.Construction.Prototypes;
using Content.Shared.DoAfter;
using Robust.Shared.Maths;
namespace Content.Server._Goobstation.Factory;
public sealed class ConstructorSystem : SharedConstructorSystem
{
[Dependency] private readonly ConstructionSystem _construction = default!;
[Dependency] private readonly StartableMachineSystem _machine = default!;
private EntityQuery<ActiveDoAfterComponent> _activeQuery;
public override void Initialize()
{
base.Initialize();
_activeQuery = GetEntityQuery<ActiveDoAfterComponent>();
SubscribeLocalEvent<ConstructorComponent, MachineStartedEvent>(OnStarted);
}
private void OnStarted(Entity<ConstructorComponent> ent, ref MachineStartedEvent args)
{
// can't start if it's already building something
if (_activeQuery.HasComp(ent))
_machine.Failed(ent.Owner);
else
Construct(ent);
}
// async because construction shitcode
private async void Construct(Entity<ConstructorComponent> ent)
{
var uid = ent.Owner;
if (ent.Comp.Construction is not {} id)
{
_machine.Failed(uid);
return;
}
_machine.Started(uid);
var proto = Proto.Index(id);
var completed = proto.Type switch
{
ConstructionType.Structure => await _construction.TryStartStructureConstruction(uid, id, OutputPosition(ent), Angle.Zero),
ConstructionType.Item => await _construction.TryStartItemConstruction(id, uid)
};
if (completed)
_machine.Completed(uid);
else
_machine.Failed(uid);
}
}

View File

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Server.Atmos.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Shared._Goobstation.Factory.Filters;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
namespace Content.Server._Goobstation.Factory.Filters;
public sealed class PressureFilterSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PressureFilterComponent, AutomationFilterEvent>(OnPressureFilter);
}
private void OnPressureFilter(Entity<PressureFilterComponent> ent, ref AutomationFilterEvent args)
{
// TODO: replace this shit with InternalAir if it gets refactored
float pressure = 0f;
if (TryComp<GasTankComponent>(args.Item, out var tank))
pressure = tank.Air.Pressure;
else if (TryComp<GasCanisterComponent>(args.Item, out var can))
pressure = can.Air.Pressure;
else
return; // has to be a gas holder
args.Allowed = pressure >= ent.Comp.Min && pressure <= ent.Comp.Max;
args.CouldAllow = true; // pressure can change with a gas canister or if the tank/can valve is opened
}
}

View File

@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory;
using Content.Server.Construction.Components;
namespace Content.Server._Goobstation.Factory;
public sealed class InteractorSystem : SharedInteractorSystem
{
private EntityQuery<ConstructionComponent> _constructionQuery;
public override void Initialize()
{
base.Initialize();
_constructionQuery = GetEntityQuery<ConstructionComponent>();
SubscribeLocalEvent<InteractorComponent, MachineStartedEvent>(OnStarted);
}
private void OnStarted(Entity<InteractorComponent> ent, ref MachineStartedEvent args)
{
// nothing there or another doafter is already running
var count = ent.Comp.TargetEntities.Count;
if (count == 0 || HasDoAfter(ent))
{
Machine.Failed(ent.Owner);
return;
}
var i = count - 1;
var netEnt = ent.Comp.TargetEntities[i].Item1;
var target = GetEntity(netEnt);
_constructionQuery.TryComp(target, out var construction);
var originalCount = construction?.InteractionQueue?.Count ?? 0;
if (!InteractWith(ent, target))
{
// have to remove it since user's filter was bad due to unhandled interaction
RemoveTarget(ent, target);
Machine.Failed(ent.Owner);
return;
}
// construction supercode queues it instead of starting a doafter now, assume that queuing means it has started
var newCount = construction?.InteractionQueue?.Count ?? 0;
if (newCount > originalCount
|| HasDoAfter(ent))
{
Machine.Started(ent.Owner);
UpdateAppearance(ent, InteractorState.Active);
}
else
{
// no doafter, complete it immediately
TryRemoveTarget(ent, target);
Machine.Completed(ent.Owner);
UpdateAppearance(ent);
}
}
}

View File

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Fax;
using Content.Shared.Fax.Components;
using Robust.Shared.Prototypes;
namespace Content.Server._Goobstation.Fax;
/// <summary>
/// Handles signals for automated fax machines.
/// </summary>
public sealed class FaxSignalSystem : EntitySystem
{
public static readonly ProtoId<SinkPortPrototype> CopyPort = "FaxCopy";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FaxMachineComponent, SignalReceivedEvent>(OnSignalReceived);
}
private void OnSignalReceived(Entity<FaxMachineComponent> ent, ref SignalReceivedEvent args)
{
if (args.Port == CopyPort)
RaiseLocalEvent(ent, new FaxCopyMessage());
}
}

View File

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Server.Kitchen.Components;
using Robust.Shared.Containers;
namespace Content.Server._Goobstation.Kitchen;
/// <summary>
/// Prevents automation taking items out of an active microwave.
/// Only exists because microwave supercode only prevents it in interaction, not attempt events.
/// </summary>
public sealed class MicrowaveEventsSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActiveMicrowaveComponent, ContainerIsRemovingAttemptEvent>(OnRemoveAttempt);
}
private void OnRemoveAttempt(Entity<ActiveMicrowaveComponent> ent, ref ContainerIsRemovingAttemptEvent args)
{
args.Cancel();
}
}

View File

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Shared.Serialization;
namespace Content.Server._Goobstation.Singularity;
/// <summary>
/// Emits signals depending on tank pressure for automated radiation collectors.
/// </summary>
[RegisterComponent, Access(typeof(RadCollectorSignalSystem))]
public sealed partial class RadCollectorSignalComponent : Component
{
[DataField]
public RadCollectorState LastState = RadCollectorState.Empty;
}
[Serializable]
public enum RadCollectorState : byte
{
Empty,
Low,
Full
}

View File

@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory;
using Content.Server.DeviceLinking.Systems;
using Content.Shared.DeviceLinking;
using Content.Shared.Singularity.Components;
using Robust.Shared.Prototypes;
namespace Content.Server._Goobstation.Singularity;
public sealed class RadCollectorSignalSystem : EntitySystem
{
[Dependency] private readonly AutomationSystem _automation = default!;
[Dependency] private readonly DeviceLinkSystem _device = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public static readonly ProtoId<SourcePortPrototype> EmptyPort = "RadEmpty";
public static readonly ProtoId<SourcePortPrototype> LowPort = "RadLow";
public static readonly ProtoId<SourcePortPrototype> FullPort = "RadFull";
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<RadCollectorSignalComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (!_automation.IsAutomated(uid))
continue;
var ent = (uid, comp);
_appearance.TryGetData<int>(uid, RadiationCollectorVisuals.PressureState, out var rawState);
var state = rawState switch
{
3 => RadCollectorState.Full,
2 => RadCollectorState.Low,
_ => RadCollectorState.Empty
};
// nothing changed
if (comp.LastState == state)
continue;
_device.SendSignal(uid, GetPort(comp.LastState), false);
comp.LastState = state;
_device.SendSignal(uid, GetPort(state), true);
}
}
private static string GetPort(RadCollectorState state) => state switch
{
RadCollectorState.Empty => EmptyPort,
RadCollectorState.Low => LowPort,
RadCollectorState.Full => FullPort
};
}

View File

@ -72,7 +72,7 @@ namespace Content.Shared.Containers.ItemSlots
continue;
var item = Spawn(slot.StartingItem, Transform(uid).Coordinates);
if (slot.ContainerSlot != null)
_containers.Insert(item, slot.ContainerSlot);
}
@ -144,7 +144,7 @@ namespace Content.Shared.Containers.ItemSlots
{
itemSlot = null;
if (!Resolve(uid, ref component))
if (!Resolve(uid, ref component, false)) // Goobstation - sane API
return false;
return component.Slots.TryGetValue(slotId, out itemSlot);

View File

@ -188,6 +188,32 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
return Loc.GetString(proto.Name);
}
/// <summary>
/// Goobstation - Removes a port from a source.
/// </summary>
public void RemoveSourcePort(EntityUid uid, ProtoId<SourcePortPrototype> port)
{
if (!TryComp<DeviceLinkSourceComponent>(uid, out var comp))
return;
comp.Ports.Remove(port);
if (comp.Ports.Count == 0)
RemCompDeferred<DeviceLinkSourceComponent>(uid);
}
/// <summary>
/// Goobstation - Removes a port from a sink.
/// </summary>
public void RemoveSinkPort(EntityUid uid, ProtoId<SinkPortPrototype> port)
{
if (!TryComp<DeviceLinkSinkComponent>(uid, out var comp))
return;
comp.Ports.Remove(port);
if (comp.Ports.Count == 0)
RemCompDeferred<DeviceLinkSinkComponent>(uid);
}
#endregion
#region Links

View File

@ -5,6 +5,7 @@ using Content.Shared.Body.Components;
using Content.Shared.Climbing.Systems;
using Content.Shared.Containers;
using Content.Shared.Database;
using Content.Shared.DeviceLinking; // Goobstation
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Unit.Events;
using Content.Shared.DoAfter;
@ -30,6 +31,7 @@ using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Prototypes; // Goobstation
using Robust.Shared.Utility;
namespace Content.Shared.Disposal.Unit;
@ -59,6 +61,8 @@ public abstract class SharedDisposalUnitSystem : EntitySystem
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly SharedDeviceLinkSystem _device = default!; // Goobstation
public static readonly ProtoId<SourcePortPrototype> ReadyPort = "DisposalReady"; // Goobstation
protected static TimeSpan ExitAttemptDelay = TimeSpan.FromSeconds(0.5);
@ -547,6 +551,7 @@ public abstract class SharedDisposalUnitSystem : EntitySystem
if (state == DisposalsPressureState.Ready)
{
component.NextPressurized = TimeSpan.Zero;
_device.InvokePort(uid, ReadyPort); // Goobstation
// Manually engaged
if (component.Engaged)

View File

@ -14,6 +14,12 @@ public sealed partial class DoAfterComponent : Component
[DataField("doAfters")]
public Dictionary<ushort, DoAfter> DoAfters = new();
/// <summary>
/// Goobstation - Whether to raise <c>DoAfterEndedEvent</c> on the user after it ends.
/// </summary>
[DataField]
public bool RaiseEndedEvent;
// Used by obsolete async do afters
public readonly Dictionary<ushort, TaskCompletionSource<DoAfterStatus>> AwaitedDoAfters = new();
}

View File

@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Content.Shared._Goobstation.DoAfter; // Goobstation
using Content.Shared.ActionBlocker;
using Content.Shared.Damage;
using Content.Shared.Hands.Components;
@ -85,6 +86,15 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
else if (doAfter.Args.Broadcast)
RaiseLocalEvent((object)ev);
// <Goobstation>
if (component.RaiseEndedEvent
&& Exists(doAfter.Args.User))
{
var ended = new DoAfterEndedEvent(doAfter.Args.Target, doAfter.Cancelled);
RaiseLocalEvent(doAfter.Args.User, ref ended);
}
// </Goobstation>
if (component.AwaitedDoAfters.Remove(doAfter.Index, out var tcs))
tcs.SetResult(doAfter.Cancelled ? DoAfterStatus.Cancelled : DoAfterStatus.Finished);
}

View File

@ -27,6 +27,7 @@ using Content.Shared.Timing;
using Content.Shared.UserInterface;
using Content.Shared.Verbs;
using Content.Shared.Wall;
using Content.Shared._Goobstation.DoAfter; // Goobstation
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Input;
@ -474,22 +475,21 @@ namespace Content.Shared.Interaction
return uid != null && IsDeleted(uid.Value);
}
public void InteractHand(EntityUid user, EntityUid target)
public bool InteractHand(EntityUid user, EntityUid target) // Goobstation - useful return value
{
if (IsDeleted(user) || IsDeleted(target))
return;
return false; // Goobstation
var complexInteractions = _actionBlockerSystem.CanComplexInteract(user);
if (!complexInteractions)
{
InteractionActivate(user,
return InteractionActivate(user, // Goobstation
target,
checkCanInteract: false,
checkUseDelay: true,
checkAccess: false,
complexInteractions: complexInteractions,
checkDeletion: false);
return;
}
// allow for special logic before main interaction
@ -498,7 +498,7 @@ namespace Content.Shared.Interaction
if (ev.Handled)
{
_adminLogger.Add(LogType.InteractHand, LogImpact.Low, $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target}, but it was handled by another system");
return;
return false; // Goobstation
}
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target));
@ -508,11 +508,11 @@ namespace Content.Shared.Interaction
_adminLogger.Add(LogType.InteractHand, LogImpact.Low, $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target}");
DoContactInteraction(user, target, message);
if (message.Handled)
return;
return true;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target));
// Else we run Activate.
InteractionActivate(user,
return InteractionActivate(user,
target,
checkCanInteract: false,
checkUseDelay: true,

View File

@ -122,7 +122,7 @@ public sealed class MobThresholdSystem : EntitySystem
MobThresholdsComponent? thresholdComponent = null)
{
threshold = null;
if (!Resolve(target, ref thresholdComponent))
if (!Resolve(target, ref thresholdComponent, false)) // Goobstation
return false;
foreach (var pair in thresholdComponent.Thresholds)

View File

@ -17,6 +17,13 @@ public abstract class SharedPowerReceiverSystem : EntitySystem
public abstract bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component);
/// <summary>
/// Goobstation - Lets shared code set power load.
/// </summary>
public virtual void SetLoad(SharedApcPowerReceiverComponent comp, float load)
{
}
public void SetNeedsPower(EntityUid uid, bool value, SharedApcPowerReceiverComponent? receiver = null)
{
if (!ResolveApc(uid, ref receiver) || receiver.NeedsPower == value)
@ -92,8 +99,8 @@ public abstract class SharedPowerReceiverSystem : EntitySystem
// NOOP on server because client has 0 idea of load so we can't raise it properly in shared.
}
/// <summary>
/// Checks if entity is APC-powered device, and if it have power.
/// <summary>
/// Checks if entity is APC-powered device, and if it have power.
/// </summary>
public bool IsPowered(Entity<SharedApcPowerReceiverComponent?> entity)
{

View File

@ -7,6 +7,7 @@ using Content.Shared.Popups;
using Content.Shared.Storage.EntitySystems;
using JetBrains.Annotations;
using Robust.Shared.GameStates;
using Robust.Shared.Map; // Goobstation
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@ -181,6 +182,15 @@ namespace Content.Shared.Stacks
RaiseLocalEvent(uid, new StackCountChangedEvent(old, component.Count));
}
/// <summary>
/// Goobstation - virtual method to allow calling from shared.
/// Does nothing on the client.
/// </summary>
public virtual EntityUid? Split(EntityUid uid, int amount, EntityCoordinates spawnPosition, StackComponent? stack = null)
{
return null;
}
/// <summary>
/// Try to use an amount of items on this stack. Returns whether this succeeded.
/// </summary>

View File

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Construction;
/// <summary>
/// Raised on the user after an entity is created by construction.
/// </summary>
[ByRefEvent]
public readonly record struct ConstructedEvent(EntityUid Entity);

View File

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.DoAfter;
/// <summary>
/// Event raised on the doafter user after a doafter ends.
/// </summary>
[ByRefEvent]
public readonly record struct DoAfterEndedEvent(EntityUid? Target, bool Cancelled);

View File

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Shared.GameStates;
namespace Content.Shared._Goobstation.Factory;
/// <summary>
/// Component added to machines with <see cref="AutomationSlotsComponent"/> to enable their ports for linking.
/// They can then be automated with things like a <see cref="RoboticArmComponent"/>.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class AutomatedComponent : Component;

View File

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory.Slots;
using Robust.Shared.GameStates;
namespace Content.Shared._Goobstation.Factory;
/// <summary>
/// Adds slots to an entity that can be controlled by automation machines if it also has <see cref="AutomationComponent"/>.
/// Slots using <see cref="AutomationSlot"/> can provide or accept items.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(AutomationSystem))]
public sealed partial class AutomationSlotsComponent : Component
{
/// <summary>
/// All input slots that can be automated.
/// </summary>
[DataField(required: true)]
public List<AutomationSlot> Slots = new();
}

View File

@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory.Slots;
using Content.Shared.Prototypes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
namespace Content.Shared._Goobstation.Factory;
public sealed class AutomationSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
private EntityQuery<AutomationSlotsComponent> _slotsQuery;
private EntityQuery<AutomatedComponent> _automatedQuery;
private List<EntProtoId> _automatable = new();
/// <summary>
/// All entities with <see cref="AutomationSlotsComponent"/>, maintained on prototype reload.
/// </summary>
public IReadOnlyList<EntProtoId> Automatable => _automatable;
public override void Initialize()
{
base.Initialize();
_slotsQuery = GetEntityQuery<AutomationSlotsComponent>();
_automatedQuery = GetEntityQuery<AutomatedComponent>();
SubscribeLocalEvent<AutomationSlotsComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<AutomatedComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<AutomatedComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<PhysicsComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
CacheEntities();
}
private void OnInit(Entity<AutomationSlotsComponent> ent, ref ComponentInit args)
{
foreach (var slot in ent.Comp.Slots)
{
slot.Owner = ent;
slot.Initialize();
}
}
private void OnMapInit(Entity<AutomatedComponent> ent, ref MapInitEvent args)
{
if (!TryComp<AutomationSlotsComponent>(ent, out var comp))
return;
foreach (var slot in comp.Slots)
{
slot.AddPorts();
}
}
private void OnShutdown(Entity<AutomatedComponent> ent, ref ComponentShutdown args)
{
if (!TryComp<AutomationSlotsComponent>(ent, out var comp))
return;
foreach (var slot in comp.Slots)
{
slot.RemovePorts();
}
}
private void OnAnchorChanged(Entity<PhysicsComponent> ent, ref AnchorStateChangedEvent args)
{
// force collision events so machines can react to objects getting unanchored
// should get reset after a tick due to collision wake
if (!args.Anchored)
_physics.WakeBody(ent);
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (!args.WasModified<EntityPrototype>())
return;
CacheEntities();
}
private void CacheEntities()
{
_automatable.Clear();
var factory = EntityManager.ComponentFactory;
foreach (var proto in _proto.EnumeratePrototypes<EntityPrototype>())
{
if (proto.HasComponent<AutomationSlotsComponent>(factory))
_automatable.Add(proto.ID);
}
_automatable.Sort();
}
#region Public API
public AutomationSlot? GetSlot(Entity<AutomationSlotsComponent?> ent, string port, bool input)
{
// entity has no automation slots to begin with
if (!_slotsQuery.Resolve(ent, ref ent.Comp, false))
return null;
// automation isn't enabled
if (!IsAutomated(ent))
return null;
foreach (var slot in ent.Comp.Slots)
{
string? id = input ? slot.Input : slot.Output;
if (id == port)
return slot;
}
return null;
}
public bool IsAutomated(EntityUid uid)
{
return _automatedQuery.HasComp(uid);
}
public bool HasSlot(Entity<AutomationSlotsComponent?> ent, string port, bool input)
{
return GetSlot(ent, port, input) != null;
}
#endregion
}

View File

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Construction.Prototypes;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Factory;
/// <summary>
/// Machine that starts constructions.
/// Multi-step objects will need interactors to complete their steps.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedConstructorSystem))]
[AutoGenerateComponentState]
public sealed partial class ConstructorComponent : Component
{
/// <summary>
/// The construction it will try to build when start is invoked.
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<ConstructionPrototype>? Construction;
}
[Serializable, NetSerializable]
public enum ConstructorUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed class ConstructorSetProtoMessage(ProtoId<ConstructionPrototype>? id) : BoundUserInterfaceMessage
{
public ProtoId<ConstructionPrototype>? Id = id;
}

View File

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Shared.GameStates;
namespace Content.Shared._Goobstation.Factory.Filters;
/// <summary>
/// Marker component for filter items.
/// Only used for whitelisting, does nothing on its own.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class AutomationFilterComponent : Component;
/// <summary>
/// Event raised on a filter to determine if it should block an item.
/// If <c>CouldAllow</c> is set to true, IsAlwaysBlocked will return false.
/// </summary>
[ByRefEvent]
public record struct AutomationFilterEvent(EntityUid Item, bool Allowed = false, bool CouldAllow = false);
/// <summary>
/// Event raised on a filter to get its stack split size.
/// </summary>
[ByRefEvent]
public record struct AutomationFilterSplitEvent(int Size = 0);

View File

@ -0,0 +1,389 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Containers.ItemSlots;
using Content.Shared.DeviceLinking;
using Content.Shared.Examine;
using Content.Shared.Interaction.Events;
using Content.Shared.Labels.Components;
using Content.Shared.Popups;
using Content.Shared.Stacks;
using Robust.Shared.Prototypes;
namespace Content.Shared._Goobstation.Factory.Filters;
public sealed class AutomationFilterSystem : EntitySystem
{
[Dependency] private readonly ItemSlotsSystem _slots = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedStackSystem _stack = default!;
private EntityQuery<FilterSlotComponent> _slotQuery;
private EntityQuery<LabelComponent> _labelQuery;
private EntityQuery<StackComponent> _stackQuery;
public static readonly int GateCount = Enum.GetValues(typeof(LogicGate)).Length;
public override void Initialize()
{
base.Initialize();
_slotQuery = GetEntityQuery<FilterSlotComponent>();
_labelQuery = GetEntityQuery<LabelComponent>();
_stackQuery = GetEntityQuery<StackComponent>();
Subs.BuiEvents<LabelFilterComponent>(LabelFilterUiKey.Key, subs =>
{
subs.Event<LabelFilterSetLabelMessage>(OnLabelSet);
});
SubscribeLocalEvent<LabelFilterComponent, ExaminedEvent>(OnLabelExamined);
SubscribeLocalEvent<LabelFilterComponent, AutomationFilterEvent>(OnLabelFilter);
Subs.BuiEvents<NameFilterComponent>(NameFilterUiKey.Key, subs =>
{
subs.Event<NameFilterSetNameMessage>(OnNameSet);
subs.Event<NameFilterSetModeMessage>(OnNameSetMode);
});
SubscribeLocalEvent<NameFilterComponent, ExaminedEvent>(OnNameExamined);
SubscribeLocalEvent<NameFilterComponent, AutomationFilterEvent>(OnNameFilter);
Subs.BuiEvents<StackFilterComponent>(StackFilterUiKey.Key, subs =>
{
subs.Event<StackFilterSetMinMessage>(OnStackSetMin);
subs.Event<StackFilterSetSizeMessage>(OnStackSetSize);
});
SubscribeLocalEvent<StackFilterComponent, ExaminedEvent>(OnStackExamined);
SubscribeLocalEvent<StackFilterComponent, AutomationFilterEvent>(OnStackFilter);
SubscribeLocalEvent<StackFilterComponent, AutomationFilterSplitEvent>(OnStackSplit);
SubscribeLocalEvent<CombinedFilterComponent, ComponentInit>(OnCombinedInit);
SubscribeLocalEvent<CombinedFilterComponent, UseInHandEvent>(OnCombinedUse);
SubscribeLocalEvent<CombinedFilterComponent, ExaminedEvent>(OnCombinedExamined);
SubscribeLocalEvent<CombinedFilterComponent, AutomationFilterEvent>(OnCombinedFilter);
SubscribeLocalEvent<CombinedFilterComponent, AutomationFilterSplitEvent>(OnCombinedSplit);
Subs.BuiEvents<PressureFilterComponent>(PressureFilterUiKey.Key, subs =>
{
subs.Event<PressureFilterSetMinMessage>(OnPressureSetMin);
subs.Event<PressureFilterSetMaxMessage>(OnPressureSetMax);
});
SubscribeLocalEvent<PressureFilterComponent, ExaminedEvent>(OnPressureExamined);
// OnPressureFilter is in server because atmos is serverside
SubscribeLocalEvent<FilterSlotComponent, ComponentInit>(OnSlotInit);
}
/* Label filter */
private void OnLabelSet(Entity<LabelFilterComponent> ent, ref LabelFilterSetLabelMessage args)
{
var label = args.Label.Trim();
if (label.Length > ent.Comp.MaxLength)
return;
ent.Comp.Label = label;
Dirty(ent);
}
private void OnLabelExamined(Entity<LabelFilterComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (string.IsNullOrEmpty(ent.Comp.Label))
{
args.PushMarkup(Loc.GetString("automation-filter-examine-empty"));
return;
}
args.PushText(Loc.GetString("automation-filter-examine-string", ("name", ent.Comp.Label)));
}
private void OnLabelFilter(Entity<LabelFilterComponent> ent, ref AutomationFilterEvent args)
{
args.Allowed = _labelQuery.CompOrNull(args.Item)?.CurrentLabel == ent.Comp.Label;
args.CouldAllow = true; // hand labelers can change the label
}
/* Name filter */
private void OnNameSet(Entity<NameFilterComponent> ent, ref NameFilterSetNameMessage args)
{
var name = args.Name.Trim();
if (name.Length > ent.Comp.MaxLength || ent.Comp.Name == name)
return;
ent.Comp.Name = name;
Dirty(ent);
}
private void OnNameSetMode(Entity<NameFilterComponent> ent, ref NameFilterSetModeMessage args)
{
if (ent.Comp.Mode == args.Mode)
return;
ent.Comp.Mode = args.Mode;
Dirty(ent);
}
private void OnNameExamined(Entity<NameFilterComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (string.IsNullOrEmpty(ent.Comp.Name))
{
args.PushMarkup(Loc.GetString("automation-filter-examine-empty"));
return;
}
args.PushText(Loc.GetString("automation-filter-examine-string", ("name", ent.Comp.Name)));
}
private void OnNameFilter(Entity<NameFilterComponent> ent, ref AutomationFilterEvent args)
{
var name = Name(args.Item);
var check = ent.Comp.Name;
args.Allowed = ent.Comp.Mode switch
{
NameFilterMode.Contain => name.Contains(check),
NameFilterMode.Start => name.StartsWith(check),
NameFilterMode.End => name.EndsWith(check),
NameFilterMode.Match => name == check
};
// entity names usually don't change except for the end including a label
args.CouldAllow = ent.Comp.Mode switch
{
NameFilterMode.End | NameFilterMode.Match => true,
_ => false
};
}
/* Stack filter */
private void OnStackSetMin(Entity<StackFilterComponent> ent, ref StackFilterSetMinMessage args)
{
if (args.Min < 1 || ent.Comp.Min == args.Min)
return;
ent.Comp.Min = args.Min;
Dirty(ent);
}
private void OnStackSetSize(Entity<StackFilterComponent> ent, ref StackFilterSetSizeMessage args)
{
if (args.Size < 0 || ent.Comp.Size == args.Size)
return;
ent.Comp.Size = args.Size;
Dirty(ent);
}
private void OnStackExamined(Entity<StackFilterComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
args.PushMarkup(Loc.GetString("stack-filter-examine", ("size", ent.Comp.Size)));
}
private void OnStackFilter(Entity<StackFilterComponent> ent, ref AutomationFilterEvent args)
{
args.Allowed = _stackQuery.CompOrNull(args.Item)?.Count >= ent.Comp.Min;
args.CouldAllow = true;
}
private void OnStackSplit(Entity<StackFilterComponent> ent, ref AutomationFilterSplitEvent args)
{
args.Size = ent.Comp.Size;
}
/* Combined filter */
private void OnCombinedInit(Entity<CombinedFilterComponent> ent, ref ComponentInit args)
{
if (!TryComp<ItemSlotsComponent>(ent, out var slots))
return;
if (!_slots.TryGetSlot(ent, CombinedFilterComponent.FilterAName, out var filterA, slots) ||
!_slots.TryGetSlot(ent, CombinedFilterComponent.FilterBName, out var filterB, slots))
{
Log.Error($"{ToPrettyString(ent)} was missing filter slots!");
RemCompDeferred<CombinedFilterComponent>(ent);
return;
}
ent.Comp.FilterA = filterA;
ent.Comp.FilterB = filterB;
}
private void OnCombinedUse(Entity<CombinedFilterComponent> ent, ref UseInHandEvent args)
{
if (args.Handled)
return;
args.Handled = true;
var gate = (int) ent.Comp.Gate;
gate = ++gate % GateCount;
ent.Comp.Gate = (LogicGate) gate;
Dirty(ent);
var msg = Loc.GetString("logic-gate-cycle", ("gate", ent.Comp.Gate.ToString().ToUpper()));
_popup.PopupClient(msg, ent, args.User);
}
private void OnCombinedExamined(Entity<CombinedFilterComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
args.PushMarkup(Loc.GetString("combined-filter-examine", ("gate", ent.Comp.Gate.ToString().ToUpper())));
}
private void OnCombinedFilter(Entity<CombinedFilterComponent> ent, ref AutomationFilterEvent args)
{
var a = IsAllowed(ent.Comp.FilterA.Item, args.Item, out var couldAllowA);
var b = IsAllowed(ent.Comp.FilterB.Item, args.Item, out var couldAllowB);
args.Allowed = ent.Comp.Gate switch
{
LogicGate.Or => a || b,
LogicGate.And => a && b,
LogicGate.Xor => a != b,
LogicGate.Nor => !(a || b),
LogicGate.Nand => !(a && b),
LogicGate.Xnor => a == b
};
args.CouldAllow = couldAllowA || couldAllowB; // if any subfilter could allow it, this could allow it too
}
private void OnCombinedSplit(Entity<CombinedFilterComponent> ent, ref AutomationFilterSplitEvent args)
{
var a = GetSplitSize(ent.Comp.FilterA.Item);
var b = GetSplitSize(ent.Comp.FilterB.Item);
args.Size = Math.Max(a, b);
}
/* Pressure filter */
private void OnPressureSetMin(Entity<PressureFilterComponent> ent, ref PressureFilterSetMinMessage args)
{
var min = args.Min;
if (min == ent.Comp.Min || min > ent.Comp.Max || min < 0f)
return;
ent.Comp.Min = min;
Dirty(ent);
}
private void OnPressureSetMax(Entity<PressureFilterComponent> ent, ref PressureFilterSetMaxMessage args)
{
var max = args.Max;
if (max == ent.Comp.Max || max < ent.Comp.Min)
return;
ent.Comp.Max = max;
Dirty(ent);
}
private void OnPressureExamined(Entity<PressureFilterComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
args.PushMarkup(Loc.GetString("pressure-filter-examine", ("min", ent.Comp.Min), ("max", ent.Comp.Max)));
}
/* Filter slot */
private void OnSlotInit(Entity<FilterSlotComponent> ent, ref ComponentInit args)
{
if (!TryComp<ItemSlotsComponent>(ent, out var slots))
return;
if (!_slots.TryGetSlot(ent, ent.Comp.FilterSlotId, out var filterSlot, slots))
{
Log.Warning($"Missing filter slot {ent.Comp.FilterSlotId} on {ToPrettyString(ent)}");
RemCompDeferred<FilterSlotComponent>(ent);
return;
}
ent.Comp.FilterSlot = filterSlot;
}
#region Public API
/// <summary>
/// Returns true if an item is allowed by the filter, false if it's blocked.
/// If there is no filter, items are always allowed.
/// </summary>
public bool IsAllowed(EntityUid? filter, EntityUid item, out bool couldAllow)
{
couldAllow = false;
if (filter is not {} uid)
return true;
var ev = new AutomationFilterEvent(item);
RaiseLocalEvent(uid, ref ev);
couldAllow = ev.CouldAllow;
return ev.Allowed;
}
public bool IsAllowed(EntityUid? filter, EntityUid item) => IsAllowed(filter, item, out _);
/// <summary>
/// Inverse of <see cref="IsAllowed"/>.
/// </summary>
public bool IsBlocked(EntityUid? filter, EntityUid item, out bool couldAllow) => !IsAllowed(filter, item, out couldAllow);
public bool IsBlocked(EntityUid? filter, EntityUid item) => IsBlocked(filter, item, out _);
/// <summary>
/// Returns true if an item can never be allowed by a filter, even if some data about it changes.
/// </summary>
public bool IsAlwaysBlocked(EntityUid? filter, EntityUid item) => IsBlocked(filter, item, out var couldAllow) && !couldAllow;
/// <summary>
/// Gets the split size for a filter.
/// If non-zero then the pulled item is split into a multiple of the return value.
/// If zero then nothing special is done.
/// </summary>
public int GetSplitSize(EntityUid? filter)
{
if (filter is not {} uid)
return 0;
var ev = new AutomationFilterSplitEvent();
RaiseLocalEvent(uid, ref ev);
return ev.Size;
}
public EntityUid? TrySplit(EntityUid? filter, EntityUid item)
{
// if it's 0 don't need to split, take the item out directly
var split = GetSplitSize(filter);
if (split == 0)
return item;
// don't need to split if it's already a multiple of the split size
var stack = Comp<StackComponent>(item);
var excess = stack.Count % split;
if (excess == 0)
return item;
// have to split it, client will return null here
var coords = Transform(item).Coordinates;
return _stack.Split(item, stack.Count - excess, coords, stack);
}
/// <summary>
/// Get the filter in a machine's filter slot, or null if it has none.
/// </summary>
public EntityUid? GetSlot(EntityUid uid)
{
return _slotQuery.CompOrNull(uid)?.Filter;
}
#endregion
}

View File

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Containers.ItemSlots;
using Content.Shared.DeviceLinking;
using Robust.Shared.GameStates;
namespace Content.Shared._Goobstation.Factory.Filters;
/// <summary>
/// Filter that combines 2 other filters using a logical operation.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(AutomationFilterSystem))]
[AutoGenerateComponentState]
public sealed partial class CombinedFilterComponent : Component
{
/// <summary>
/// Name of the first filter slot.
/// </summary>
public const string FilterAName = "combined_filter_a";
/// <summary>
/// Name of the second filter slot.
/// </summary>
public const string FilterBName = "combined_filter_b";
/// <summary>
/// The slot for the first filter.
/// </summary>
[ViewVariables]
public ItemSlot FilterA = default!;
/// <summary>
/// The slot for the second filter.
/// </summary>
[ViewVariables]
public ItemSlot FilterB = default!;
/// <summary>
/// Logic gate operation to check the inputs with.
/// </summary>
[DataField, AutoNetworkedField]
public LogicGate Gate = LogicGate.Or;
}

View File

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Containers.ItemSlots;
using Robust.Shared.GameStates;
namespace Content.Shared._Goobstation.Factory.Filters;
/// <summary>
/// Component for machines that have a filter slot.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(AutomationFilterSystem))]
public sealed partial class FilterSlotComponent : Component
{
/// <summary>
/// Item slot that stores a filter.
/// </summary>
[DataField]
public string FilterSlotId = "filter_slot";
/// <summary>
/// The filter slot cached on init.
/// </summary>
[ViewVariables]
public ItemSlot FilterSlot = default!;
/// <summary>
/// The currently inserted filter.
/// </summary>
[ViewVariables]
public EntityUid? Filter => FilterSlot.Item;
}

View File

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Factory.Filters;
/// <summary>
/// A filter that requires items to have the exact same label as a set string.
/// Items without a label will always fail it.
/// Set labels using a hand labeler.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(AutomationFilterSystem))]
[AutoGenerateComponentState]
public sealed partial class LabelFilterComponent : Component
{
/// <summary>
/// The label to require.
/// </summary>
[DataField, AutoNetworkedField]
public string Label = string.Empty;
/// <summary>
/// Max length for <see cref="Label"/>.
/// </summary>
[DataField]
public int MaxLength = 50;
}
[Serializable, NetSerializable]
public enum LabelFilterUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed partial class LabelFilterSetLabelMessage(string label) : BoundUserInterfaceMessage
{
public readonly string Label = label;
}

View File

@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Factory.Filters;
/// <summary>
/// A filter that requires items to have the exact same name as a set string.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(AutomationFilterSystem))]
[AutoGenerateComponentState]
public sealed partial class NameFilterComponent : Component
{
/// <summary>
/// The string to compare to the item name.
/// </summary>
[DataField, AutoNetworkedField]
public string Name = string.Empty;
/// <summary>
/// Max length for <see cref="Name"/>.
/// </summary>
[DataField]
public int MaxLength = 50;
/// <summary>
/// The filtering mode to use with <see cref="Name"/>.
/// </summary>
[DataField, AutoNetworkedField]
public NameFilterMode Mode = NameFilterMode.Contain;
}
[Serializable, NetSerializable]
public enum NameFilterMode : byte
{
// Name must contain a string somewhere
Contain,
// Name must start with a string
Start,
// Name must end with a string
End,
// Name must match exactly, even if it's labelled
Match
}
[Serializable, NetSerializable]
public enum NameFilterUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed partial class NameFilterSetNameMessage(string name) : BoundUserInterfaceMessage
{
public readonly string Name = name;
}
[Serializable, NetSerializable]
public sealed partial class NameFilterSetModeMessage(NameFilterMode mode) : BoundUserInterfaceMessage
{
public readonly NameFilterMode Mode = mode;
}

View File

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Atmos;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Factory.Filters;
/// <summary>
/// Requires that the pressure of an entity's gas mixture is within some range.
/// Since atmos is server only, client will predict it blocking everything.
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]
public sealed partial class PressureFilterComponent : Component
{
/// <summary>
/// Minimum pressure to require.
/// </summary>
[DataField, AutoNetworkedField]
public float Min;
/// <summary>
/// Maximum pressure to require.
/// </summary>
[DataField, AutoNetworkedField]
public float Max = Atmospherics.OneAtmosphere * 10f;
}
[Serializable, NetSerializable]
public enum PressureFilterUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed partial class PressureFilterSetMinMessage(float min) : BoundUserInterfaceMessage
{
public readonly float Min = min;
}
[Serializable, NetSerializable]
public sealed partial class PressureFilterSetMaxMessage(float max) : BoundUserInterfaceMessage
{
public readonly float Max = max;
}

View File

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Factory.Filters;
/// <summary>
/// A filter that requires items to have a minimum stack size.
/// Non-stackable items will always be blocked.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(AutomationFilterSystem))]
[AutoGenerateComponentState]
public sealed partial class StackFilterComponent : Component
{
/// <summary>
/// Minimum stack size to require.
/// </summary>
[DataField, AutoNetworkedField]
public int Min = 1;
/// <summary>
/// Items must be taken out in chunks of this size.
/// Combining more than stack filter makes it use the highest set chunk size.
/// If 0 then output is not chunked.
/// </summary>
[DataField, AutoNetworkedField]
public int Size;
}
[Serializable, NetSerializable]
public enum StackFilterUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed partial class StackFilterSetMinMessage(int min) : BoundUserInterfaceMessage
{
public readonly int Min = min;
}
[Serializable, NetSerializable]
public sealed partial class StackFilterSetSizeMessage(int size) : BoundUserInterfaceMessage
{
public readonly int Size = size;
}

View File

@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Containers.ItemSlots;
using Content.Shared.DeviceLinking;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Factory;
[RegisterComponent, NetworkedComponent, Access(typeof(SharedInteractorSystem))]
[AutoGenerateComponentState(fieldDeltas: true)]
public sealed partial class InteractorComponent : Component
{
[DataField]
public string ToolContainerId = "interactor_tool";
/// <summary>
/// Fixture to look for target items with.
/// </summary>
[DataField]
public string TargetFixtureId = "interactor_target";
/// <summary>
/// Entities currently colliding with <see cref="TargetFixtureId"/> and whether their CollisionWake was enabled.
/// When entities start to collide they get pushed to the end.
/// When picking up items the last value is taken.
/// This is essentially a FILO queue.
/// </summary>
[DataField, AutoNetworkedField]
public List<(NetEntity, bool)> TargetEntities = new();
}
[Serializable, NetSerializable]
public enum InteractorVisuals : byte
{
State
}
[Serializable, NetSerializable]
public enum InteractorLayers : byte
{
Hand,
Powered
}
[Serializable, NetSerializable]
public enum InteractorState : byte
{
// Inactive with no tool
Empty,
// Inactive with a tool
Inactive,
// Active, with or without a tool
Active
}

View File

@ -0,0 +1,172 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory.Slots;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.DeviceLinking;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared._Goobstation.Factory;
[RegisterComponent, NetworkedComponent, Access(typeof(RoboticArmSystem))]
[AutoGenerateComponentState(true, fieldDeltas: true), AutoGenerateComponentPause]
public sealed partial class RoboticArmComponent : Component
{
#region Linking
/// <summary>
/// Machine linked to the input port.
/// Might not always exist.
/// </summary>
[DataField, AutoNetworkedField]
public NetEntity? InputMachine;
/// <summary>
/// Sink port on this arm that machines link to.
/// </summary>
[DataField]
public ProtoId<SinkPortPrototype> InputPort = "RoboticArmInput";
/// <summary>
/// The source port of the linked input machine.
/// This controls which item slot etc gets pulled from.
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<SourcePortPrototype>? InputMachinePort;
/// <summary>
/// The resolved automation output slot of the input machine to take items from.
/// </summary>
[ViewVariables]
public AutomationSlot? InputSlot;
/// <summary>
/// Machine linked to the output port.
/// Might not always exist.
/// </summary>
[DataField, AutoNetworkedField]
public NetEntity? OutputMachine;
/// <summary>
/// Source port on this arm that machines link from.
/// </summary>
[DataField]
public ProtoId<SourcePortPrototype> OutputPort = "RoboticArmOutput";
/// <summary>
/// The sink port of the linked output machine.
/// This controls which item slot etc gets inserted into.
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<SinkPortPrototype>? OutputMachinePort;
/// <summary>
/// The resolved automation input slot of the output machine to insert items into.
/// </summary>
[ViewVariables]
public AutomationSlot? OutputSlot;
/// <summary>
/// Signal port invoked after an item gets moved.
/// </summary>
[DataField]
public ProtoId<SourcePortPrototype> MovedPort = "RoboticArmMoved";
#endregion
#region Item Slot
/// <summary>
/// Item slot that stores the held item.
/// </summary>
[DataField]
public string ItemSlotId = "robotic_arm_item";
/// <summary>
/// The item slot cached on init.
/// </summary>
[ViewVariables]
public ItemSlot ItemSlot = default!;
/// <summary>
/// The currently held item.
/// </summary>
[ViewVariables]
public EntityUid? HeldItem => ItemSlot.Item;
/// <summary>
/// Whether an item is currently held.
/// </summary>
public bool HasItem => ItemSlot.HasItem;
#endregion
#region Input Items
/// <summary>
/// Fixture to look for input items with when no input machine is linked.
/// </summary>
[DataField]
public string InputFixtureId = "robotic_arm_input";
/// <summary>
/// Items currently colliding with <see cref="InputFixtureId"/> and whether their CollisionWake was enabled.
/// When items start to collide they get pushed to the end.
/// When picking up items the last value is taken.
/// This is essentially a FILO queue.
/// </summary>
[DataField, AutoNetworkedField]
public List<(NetEntity, bool)> InputItems = new();
#endregion
#region Arm Moving
/// <summary>
/// How long it takes to move an item.
/// </summary>
[DataField]
public TimeSpan MoveDelay = TimeSpan.FromSeconds(0.6);
/// <summary>
/// When the arm will next move to the input or output.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField]
public TimeSpan? NextMove;
/// <summary>
/// Sound played when moving an item.
/// </summary>
[DataField]
public SoundSpecifier? MoveSound;
#endregion
#region Power
/// <summary>
/// Power used when idle.
/// </summary>
[DataField]
public float IdlePowerDraw = 50f;
/// <summary>
/// Power used when moving items.
/// </summary>
[DataField]
public float MovingPowerDraw = 200f; // DeltaV - was 3000f
#endregion
}
[Serializable, NetSerializable]
public enum RoboticArmVisuals : byte
{
HasItem
}
[Serializable, NetSerializable]
public enum RoboticArmLayers : byte
{
Arm,
Powered
}

View File

@ -0,0 +1,453 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory.Filters;
using Content.Shared._Goobstation.Factory.Slots;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Examine;
using Content.Shared.Item;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Throwing;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Physics.Events;
using Robust.Shared.Timing;
namespace Content.Shared._Goobstation.Factory;
public sealed class RoboticArmSystem : EntitySystem
{
[Dependency] private readonly AutomationSystem _automation = default!;
[Dependency] private readonly AutomationFilterSystem _filter = default!;
[Dependency] private readonly CollisionWakeSystem _wake = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly ItemSlotsSystem _slots = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedDeviceLinkSystem _device = default!;
[Dependency] private readonly SharedPowerReceiverSystem _power = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TurfSystem _turf = default!;
private EntityQuery<ItemComponent> _itemQuery;
private EntityQuery<ThrownItemComponent> _thrownQuery;
private TimeSpan _nextUpdate = TimeSpan.Zero;
private static readonly TimeSpan _updateDelay = TimeSpan.FromSeconds(0.5);
public override void Initialize()
{
base.Initialize();
_itemQuery = GetEntityQuery<ItemComponent>();
_thrownQuery = GetEntityQuery<ThrownItemComponent>();
SubscribeLocalEvent<RoboticArmComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<RoboticArmComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<RoboticArmComponent, AfterAutoHandleStateEvent>(OnHandleState);
// input items
SubscribeLocalEvent<RoboticArmComponent, StartCollideEvent>(OnStartCollide);
SubscribeLocalEvent<RoboticArmComponent, EndCollideEvent>(OnEndCollide);
// HasItem visuals
SubscribeLocalEvent<RoboticArmComponent, EntInsertedIntoContainerMessage>(OnItemModified);
SubscribeLocalEvent<RoboticArmComponent, EntRemovedFromContainerMessage>(OnItemModified);
// linking
SubscribeLocalEvent<RoboticArmComponent, LinkAttemptEvent>(OnLinkAttempt);
SubscribeLocalEvent<RoboticArmComponent, NewLinkEvent>(OnNewLink);
SubscribeLocalEvent<RoboticArmComponent, PortDisconnectedEvent>(OnPortDisconnected);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var now = _timing.CurTime;
if (_nextUpdate < now)
return;
_nextUpdate += _updateDelay;
var query = EntityQueryEnumerator<RoboticArmComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (!_power.IsPowered(uid))
continue;
if (comp.NextMove is {} nextMove && now < nextMove)
continue;
var ent = (uid, comp);
StopMoving(ent);
if (comp.HeldItem is {} item)
{
if (!TryDrop(ent, item))
continue;
StartMoving(ent);
_device.InvokePort(uid, comp.MovedPort);
}
else if (TryPickupAny(ent))
{
StartMoving(ent);
}
}
}
private void OnInit(Entity<RoboticArmComponent> ent, ref ComponentInit args)
{
_device.EnsureSinkPorts(ent, ent.Comp.InputPort);
_device.EnsureSourcePorts(ent, ent.Comp.OutputPort, ent.Comp.MovedPort);
UpdateSlots(ent);
UpdateItemSlots(ent);
}
private void OnExamined(Entity<RoboticArmComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
using (args.PushGroup(nameof(RoboticArmComponent)))
{
args.PushMarkup(_filter.GetSlot(ent) is {} filter
? Loc.GetString("robotic-arm-examine-filter", ("filter", filter))
: Loc.GetString("robotic-arm-examine-no-filter"));
args.PushMarkup(ent.Comp.HeldItem is {} item
? Loc.GetString("robotic-arm-examine-item", ("item", item))
: Loc.GetString("robotic-arm-examine-no-item"));
}
}
private void OnHandleState(Entity<RoboticArmComponent> ent, ref AfterAutoHandleStateEvent args)
{
// incase client didnt predict linked port changing, update them
UpdateSlots(ent);
}
private void OnStartCollide(Entity<RoboticArmComponent> ent, ref StartCollideEvent args)
{
// only care about items in the input area
if (args.OurFixtureId != ent.Comp.InputFixtureId)
return;
AddInput(ent, args.OtherEntity);
}
private void AddInput(Entity<RoboticArmComponent> ent, EntityUid item)
{
// never pick up non-items
if (!_itemQuery.HasComp(item))
return;
// thrown items move too fast to be caught...
if (_thrownQuery.HasComp(item))
return;
// ignore items filters will never allow
// not using IsBlocked since gas tanks can change pressure in a canister and need to be checked
if (_filter.IsAlwaysBlocked(_filter.GetSlot(ent), item))
return;
var wake = CompOrNull<CollisionWakeComponent>(item);
var wakeEnabled = wake?.Enabled ?? false;
// need to only get EndCollide when it leaves the area, not when it sleeps
_wake.SetEnabled(item, false, wake);
ent.Comp.InputItems.Add((GetNetEntity(item), wakeEnabled));
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.InputItems));
}
private void OnEndCollide(Entity<RoboticArmComponent> ent, ref EndCollideEvent args)
{
// only care about items leaving the input area
if (args.OurFixtureId != ent.Comp.InputFixtureId)
return;
var item = GetNetEntity(args.OtherEntity);
var i = ent.Comp.InputItems.FindIndex(pair => pair.Item1 == item);
if (i < 0)
return;
var wake = ent.Comp.InputItems[i].Item2;
ent.Comp.InputItems.RemoveAt(i);
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.InputItems));
_wake.SetEnabled(args.OtherEntity, wake); // don't break conveyors for skipped items
}
private void OnItemModified<T>(Entity<RoboticArmComponent> ent, ref T args) where T: ContainerModifiedMessage
{
if (args.Container.ID != ent.Comp.ItemSlotId)
return;
// need to do this here for flatpacking at least from PVS stuff
UpdateItemSlots(ent);
_appearance.SetData(ent, RoboticArmVisuals.HasItem, ent.Comp.HasItem);
}
private void OnLinkAttempt(Entity<RoboticArmComponent> ent, ref LinkAttemptEvent args)
{
// only prevent linking machines, don't care about control ports
var linkingOutput = args.SourcePort == ent.Comp.OutputPort;
var linkingInput = args.SinkPort == ent.Comp.InputPort;
if (!linkingOutput && !linkingInput)
return;
if (ent.Owner == args.Source && linkingOutput)
{
// only 1 machine
if (GetOutputMachine(ent) != null)
{
args.Cancel();
return;
}
// make sure the port is for an automation slot
if (!_automation.HasSlot(args.Sink, args.SinkPort, input: true))
{
args.Cancel();
return;
}
}
else if (ent.Owner == args.Sink && linkingInput)
{
// only 1 machine
if (GetInputMachine(ent) != null)
{
args.Cancel();
return;
}
// make sure the port is for an automation slot
if (!_automation.HasSlot(args.Source, args.SourcePort, input: false))
{
args.Cancel();
return;
}
}
}
private void OnNewLink(Entity<RoboticArmComponent> ent, ref NewLinkEvent args)
{
if (args.SinkPort == ent.Comp.InputPort)
{
ent.Comp.InputMachine = GetNetEntity(args.Source);
ent.Comp.InputMachinePort = args.SourcePort;
ent.Comp.InputSlot = _automation.GetSlot(args.Source, args.SourcePort, input: false);
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.InputMachine));
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.InputMachinePort));
}
else if (args.SourcePort == ent.Comp.OutputPort)
{
ent.Comp.OutputMachine = GetNetEntity(args.Sink);
ent.Comp.OutputMachinePort = args.SinkPort;
ent.Comp.OutputSlot = _automation.GetSlot(args.Sink, args.SinkPort, input: true);
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.OutputMachine));
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.OutputMachinePort));
}
}
private void OnPortDisconnected(Entity<RoboticArmComponent> ent, ref PortDisconnectedEvent args)
{
// this event is shit and doesnt have source/sink entity and port just 1 string
// so if you made InputPort and OutputPort the same string it would silently break
// absolute supercode
if (args.Port == ent.Comp.InputPort)
{
ent.Comp.InputMachine = null;
ent.Comp.InputMachinePort = null;
ent.Comp.InputSlot = null;
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.InputMachine));
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.InputMachinePort));
}
else if (args.Port == ent.Comp.OutputPort)
{
ent.Comp.OutputMachine = null;
ent.Comp.OutputMachinePort = null;
ent.Comp.OutputSlot = null;
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.OutputMachine));
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.OutputMachinePort));
}
}
/// <summary>
/// If a machine is linked for the arm's output, tries to insert into it.
/// If there is no machine linked it just gets dropped.
/// </summary>
public bool TryDrop(Entity<RoboticArmComponent> ent, EntityUid item)
{
if (GetOutputMachine(ent) is {} machine && ent.Comp.OutputSlot is {} slot)
return TryInsert(ent, item, machine, slot);
// no dropping items into walls
if (IsOutputBlocked(ent))
return false;
// nothing linked, just drop it there
_transform.SetCoordinates(item, OutputPosition(ent));
return true;
}
public bool TryInsert(Entity<RoboticArmComponent> ent, EntityUid item, EntityUid machine, AutomationSlot slot)
{
// prevent linking a machine then moving it far away, it has to be at the output area
var coords = OutputPosition(ent);
if (!_transform.InRange(Transform(machine).Coordinates, coords, 0.25f))
return false;
return slot.Insert(item);
}
public bool TryPickupAny(Entity<RoboticArmComponent> ent)
{
if (GetInputMachine(ent) is {} machine && ent.Comp.InputSlot is {} slot)
return TryPickupFrom(ent, machine, slot);
var count = ent.Comp.InputItems.Count;
if (count == 0)
return false;
var output = ent.Comp.OutputSlot;
if (output == null && IsOutputBlocked(ent))
return false;
var filter = _filter.GetSlot(ent);
// check them in reverse since removing near the end is cheaper
var found = EntityUid.Invalid;
for (var i = count - 1; i >= 0; i--)
{
var netEnt = ent.Comp.InputItems[i].Item1;
if (!TryGetEntity(netEnt, out var item))
continue;
if (_filter.IsBlocked(filter, item.Value))
continue;
// make sure the destination will accept it or it gets stuck
if (output?.CanInsert(item.Value) ?? true)
{
ent.Comp.InputItems.RemoveAt(i);
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.InputItems));
found = item.Value;
break;
}
}
// nothing :(
if (!found.Valid)
return false;
// no longer need this
_wake.SetEnabled(found, false);
// insert it into the arm slot
return _slots.TryInsert(ent, ent.Comp.ItemSlot, found, user: null);
}
public bool TryPickupFrom(Entity<RoboticArmComponent> ent, EntityUid machine, AutomationSlot slot)
{
// prevent linking a machine then moving it far away, it has to be at the input area
var coords = InputPosition(ent);
if (!_transform.InRange(Transform(machine).Coordinates, coords, 0.25f))
return false;
var filter = _filter.GetSlot(ent);
if (slot.GetItem(filter) is not {} item)
return false;
// client can't predict splitting because it spawns entities
if (_filter.TrySplit(filter, item) is not {} stack)
return false;
return _slots.TryInsert(ent, ent.Comp.ItemSlot, stack, user: null);
}
private void UpdateSlots(Entity<RoboticArmComponent> ent)
{
if (GetInputMachine(ent) is {} input && ent.Comp.InputMachinePort is {} inPort)
ent.Comp.InputSlot = _automation.GetSlot(input, inPort, input: false);
if (GetOutputMachine(ent) is {} output && ent.Comp.OutputMachinePort is {} outPort)
ent.Comp.OutputSlot = _automation.GetSlot(output, outPort, input: true);
}
private void UpdateItemSlots(Entity<RoboticArmComponent> ent)
{
if (ent.Comp.ItemSlot != null)
return;
if (!TryComp<ItemSlotsComponent>(ent, out var slots))
return;
if (!_slots.TryGetSlot(ent, ent.Comp.ItemSlotId, out var slot, slots))
{
Log.Warning($"Missing item slot {ent.Comp.ItemSlotId} on robotic arm {ToPrettyString(ent)}");
RemCompDeferred<RoboticArmComponent>(ent);
return;
}
ent.Comp.ItemSlot = slot;
}
private bool IsOutputBlocked(EntityUid uid)
{
var coords = OutputPosition(uid);
return coords.GetTileRef(EntityManager, _map) is {} turf &&
_turf.IsTileBlocked(turf, CollisionGroup.MachineMask);
}
private void StartMoving(Entity<RoboticArmComponent> ent)
{
//SetPowerDraw(ent, ent.Comp.MovingPowerDraw); - ported from Impstation, static power draw to prever seizure inducing power flashes
ent.Comp.NextMove = _timing.CurTime + ent.Comp.MoveDelay;
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.NextMove));
}
private void StopMoving(Entity<RoboticArmComponent> ent)
{
// SetPowerDraw(ent, ent.Comp.IdlePowerDraw); - ported from Impstation, static power draw to prever seizure inducing power flashes
ent.Comp.NextMove = null;
DirtyField(ent, ent.Comp, nameof(RoboticArmComponent.NextMove));
}
// private void SetPowerDraw(EntityUid uid, float draw) - ported from Impstation, static power draw to prever seizure inducing power flashes
// {
// SharedApcPowerReceiverComponent? receiver = null;
// if (_power.ResolveApc(uid, ref receiver))
// _power.SetLoad(receiver, draw);
// }
public EntityCoordinates OutputPosition(EntityUid uid)
{
var xform = Transform(uid);
var offset = xform.LocalRotation.ToVec();
// positive would be where the input fixture is...
return xform.Coordinates.Offset(-offset);
}
public EntityCoordinates InputPosition(EntityUid uid)
{
var xform = Transform(uid);
var offset = xform.LocalRotation.ToVec();
return xform.Coordinates.Offset(offset);
}
private EntityUid? GetInputMachine(RoboticArmComponent comp)
{
TryGetEntity(comp.InputMachine, out var machine);
return machine;
}
private EntityUid? GetOutputMachine(RoboticArmComponent comp)
{
TryGetEntity(comp.OutputMachine, out var machine);
return machine;
}
}

View File

@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Construction;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Examine;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Shared._Goobstation.Factory;
public abstract class SharedConstructorSystem : EntitySystem
{
[Dependency] protected readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] protected readonly IPrototypeManager Proto = default!;
[Dependency] protected readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ConstructorComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<ConstructorComponent, ConstructedEvent>(OnConstructed);
Subs.BuiEvents<ConstructorComponent>(ConstructorUiKey.Key, subs =>
{
subs.Event<ConstructorSetProtoMessage>(OnSetProto);
});
}
private void OnExamined(Entity<ConstructorComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
var msg = ent.Comp.Construction is {} id
? Loc.GetString("constructor-examine", ("name", Proto.Index(id)))
: Loc.GetString("constructor-examine-unset");
args.PushMarkup(msg);
}
private void OnConstructed(Entity<ConstructorComponent> ent, ref ConstructedEvent args) =>
_transform.SetCoordinates(args.Entity, OutputPosition(ent));
private void OnSetProto(Entity<ConstructorComponent> ent, ref ConstructorSetProtoMessage args)
{
if (ent.Comp.Construction == args.Id
|| !Proto.HasIndex(args.Id))
return;
ent.Comp.Construction = args.Id;
Dirty(ent);
_adminLogger.Add(LogType.Construction, LogImpact.Low, $"{ToPrettyString(args.Actor):user} set {ToPrettyString(ent):target} construction to {args.Id}");
}
public EntityCoordinates OutputPosition(EntityUid uid)
{
var xform = Transform(uid);
var offset = xform.LocalRotation.ToVec();
return xform.Coordinates.Offset(offset);
}
}

View File

@ -0,0 +1,180 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.DoAfter;
using Content.Shared._Goobstation.Factory.Filters;
using Content.Shared.DeviceLinking;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Throwing;
using Robust.Shared.Containers;
using Robust.Shared.Physics.Events;
namespace Content.Shared._Goobstation.Factory;
public abstract class SharedInteractorSystem : EntitySystem
{
[Dependency] private readonly AutomationSystem _automation = default!;
[Dependency] private readonly AutomationFilterSystem _filter = default!;
[Dependency] private readonly CollisionWakeSystem _wake = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] protected readonly StartableMachineSystem Machine = default!;
private EntityQuery<ActiveDoAfterComponent> _doAfterQuery;
private EntityQuery<HandsComponent> _handsQuery;
private EntityQuery<ThrownItemComponent> _thrownQuery;
public override void Initialize()
{
base.Initialize();
_doAfterQuery = GetEntityQuery<ActiveDoAfterComponent>();
_handsQuery = GetEntityQuery<HandsComponent>();
_thrownQuery = GetEntityQuery<ThrownItemComponent>();
SubscribeLocalEvent<InteractorComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<InteractorComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<InteractorComponent, DoAfterEndedEvent>(OnDoAfterEnded);
// target entities
SubscribeLocalEvent<InteractorComponent, StartCollideEvent>(OnStartCollide);
SubscribeLocalEvent<InteractorComponent, EndCollideEvent>(OnEndCollide);
// hand visuals
SubscribeLocalEvent<InteractorComponent, EntInsertedIntoContainerMessage>(OnItemModified);
SubscribeLocalEvent<InteractorComponent, EntRemovedFromContainerMessage>(OnItemModified);
}
private void OnInit(Entity<InteractorComponent> ent, ref ComponentInit args)
{
UpdateAppearance(ent);
}
private void OnExamined(Entity<InteractorComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
args.PushMarkup(_filter.GetSlot(ent) is {} filter
? Loc.GetString("robotic-arm-examine-filter", ("filter", filter))
: Loc.GetString("robotic-arm-examine-no-filter"));
}
private void OnStartCollide(Entity<InteractorComponent> ent, ref StartCollideEvent args)
{
// only care about entities in the target area
if (args.OurFixtureId != ent.Comp.TargetFixtureId)
return;
AddTarget(ent, args.OtherEntity);
}
private void AddTarget(Entity<InteractorComponent> ent, EntityUid target)
{
if (_thrownQuery.HasComp(target) // thrown items move too fast to be "clicked" on...
|| _filter.IsBlocked(_filter.GetSlot(ent), target)) // ignore non-filtered entities
return;
var wake = CompOrNull<CollisionWakeComponent>(target);
var wakeEnabled = wake?.Enabled ?? false;
// need to only get EndCollide when it leaves the area, not when it sleeps
_wake.SetEnabled(target, false, wake);
ent.Comp.TargetEntities.Add((GetNetEntity(target), wakeEnabled));
DirtyField(ent, ent.Comp, nameof(InteractorComponent.TargetEntities));
}
private void OnEndCollide(Entity<InteractorComponent> ent, ref EndCollideEvent args)
{
// only care about entities leaving the input area
if (args.OurFixtureId != ent.Comp.TargetFixtureId)
return;
var target = GetNetEntity(args.OtherEntity);
var i = ent.Comp.TargetEntities.FindIndex(pair => pair.Item1 == target);
if (i < 0)
return;
var wake = ent.Comp.TargetEntities[i].Item2;
ent.Comp.TargetEntities.RemoveAt(i);
DirtyField(ent, ent.Comp, nameof(InteractorComponent.TargetEntities));
_wake.SetEnabled(args.OtherEntity, wake); // don't break conveyors for skipped entities
}
private void OnItemModified<T>(Entity<InteractorComponent> ent, ref T args) where T: ContainerModifiedMessage
{
if (args.Container.ID != ent.Comp.ToolContainerId)
return;
UpdateAppearance(ent);
}
private void OnDoAfterEnded(Entity<InteractorComponent> ent, ref DoAfterEndedEvent args)
{
UpdateToolAppearance(ent);
if (args.Target is not { } target)
return;
TryRemoveTarget(ent, target);
if (args.Cancelled)
Machine.Failed(ent.Owner);
else
Machine.Completed(ent.Owner);
}
protected bool HasDoAfter(EntityUid uid) => _doAfterQuery.HasComp(uid);
protected bool InteractWith(Entity<InteractorComponent> ent, EntityUid target)
{
if (_handsQuery.CompOrNull(ent)?.ActiveHandEntity is not {} tool)
return _interaction.InteractHand(ent, target);
var coords = Transform(target).Coordinates;
return _interaction.InteractUsing(ent, tool, target, coords);
}
protected void TryRemoveTarget(Entity<InteractorComponent> ent, EntityUid target)
{
// if it still exists and is still allowed by the filter keep it
if (!TerminatingOrDeleted(target)
&& _filter.IsAllowed(_filter.GetSlot(ent), target))
return;
RemoveTarget(ent, target);
}
protected void RemoveTarget(Entity<InteractorComponent> ent, EntityUid target)
{
// if it no longer exists it should be removed by collision events
if (TerminatingOrDeleted(target))
return;
var netEnt = GetNetEntity(target);
ent.Comp.TargetEntities.RemoveAll(pair => pair.Item1 == netEnt);
DirtyField(ent, ent.Comp, nameof(InteractorComponent.TargetEntities));
}
protected void UpdateAppearance(EntityUid uid)
{
if (HasDoAfter(uid))
UpdateAppearance(uid, InteractorState.Active);
else
UpdateToolAppearance(uid);
}
private void UpdateToolAppearance(EntityUid uid)
{
var state = _handsQuery.CompOrNull(uid)?.ActiveHand?.IsEmpty == false
? InteractorState.Inactive
: InteractorState.Empty;
UpdateAppearance(uid, state);
}
protected void UpdateAppearance(EntityUid uid, InteractorState state) =>
_appearance.SetData(uid, InteractorVisuals.State, state);
}

View File

@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Shared.Containers;
namespace Content.Shared._Goobstation.Factory.Slots;
/// <summary>
/// Abstraction over a <see cref="BaseContainer"/> on the machine.
/// </summary>
public sealed partial class AutomatedContainer : AutomationSlot
{
/// <summary>
/// The ID of the container to use.
/// </summary>
[DataField(required: true)]
public string ContainerId = string.Empty;
[DataField(required: true)]
public int MaxItems;
private SharedContainerSystem _container;
public BaseContainer Container;
public override void Initialize()
{
base.Initialize();
_container = EntMan.System<SharedContainerSystem>();
Container = _container.GetContainer(Owner, ContainerId);
}
public override bool Insert(EntityUid item)
{
return base.Insert(item) && _container.Insert(item, Container);
}
public override bool CanInsert(EntityUid item)
{
return base.CanInsert(item)
&& Container.Count < MaxItems
&& _container.CanInsert(item, Container);
}
public override EntityUid? GetItem(EntityUid? filter)
{
foreach (var item in Container.ContainedEntities)
{
if (_filter.IsAllowed(filter, item))
return item;
}
return null;
}
}

View File

@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
namespace Content.Shared._Goobstation.Factory.Slots;
/// <summary>
/// Abstraction over a specific hand of the machine.
/// </summary>
public sealed partial class AutomatedHand : AutomationSlot
{
/// <summary>
/// The name of the hand to use
/// </summary>
[DataField(required: true)]
public string HandName = string.Empty;
private SharedHandsSystem _hands;
private Hand? _hand;
[ViewVariables]
public Hand? Hand
{
get
{
if (_hand != null)
return _hand;
_hands.TryGetHand(Owner, HandName, out _hand);
return _hand;
}
}
public override void Initialize()
{
base.Initialize();
_hands = EntMan.System<SharedHandsSystem>();
}
public override bool Insert(EntityUid item)
{
return Hand is { } hand
&& base.Insert(item)
&& _hands.TryPickup(Owner, item, hand);
}
public override bool CanInsert(EntityUid item)
{
return Hand is { } hand
&& base.CanInsert(item)
&& _hands.CanPickupToHand(Owner, item, hand);
}
public override EntityUid? GetItem(EntityUid? filter)
{
if (Hand?.HeldEntity is not { } item
|| _filter.IsBlocked(filter, item))
return null;
return item;
}
}

View File

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Containers.ItemSlots;
namespace Content.Shared._Goobstation.Factory.Slots;
/// <summary>
/// Abstraction over an <see cref="ItemSlot"/> on the machine.
/// </summary>
public sealed partial class AutomatedItemSlot : AutomationSlot
{
/// <summary>
/// The name of the slot to automate.
/// </summary>
[DataField(required: true)]
public string SlotId = string.Empty;
private ItemSlotsSystem _slots;
private ItemSlot? _slot;
[ViewVariables]
public ItemSlot Slot
{
get
{
if (_slot is {} slot)
return slot;
if (_slots.TryGetSlot(Owner, SlotId, out _slot))
return _slot;
throw new InvalidOperationException($"Entity {EntMan.ToPrettyString(Owner)} had no item slot {SlotId}");
}
}
public override void Initialize()
{
base.Initialize();
_slots = EntMan.System<ItemSlotsSystem>();
}
public override bool Insert(EntityUid item)
{
return base.Insert(item) &&
_slots.TryInsert(Owner, Slot, item, user: null);
}
public override bool CanInsert(EntityUid item)
{
return base.CanInsert(item) &&
_slots.CanInsert(Owner, usedUid: item, user: null, Slot);
}
public override EntityUid? GetItem(EntityUid? filter)
{
if (Slot.Item is not {} item || _filter.IsBlocked(filter, item))
return null;
return item;
}
}

View File

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Materials;
using Content.Shared.Power.EntitySystems;
namespace Content.Shared._Goobstation.Factory.Slots;
/// <summary>
/// Abstraction over inserting
/// Removing items is not supported.
/// </summary>
public sealed partial class AutomatedMaterialStorage : AutomationSlot
{
private SharedMaterialStorageSystem _material;
private SharedPowerReceiverSystem _power;
private EntityQuery<MaterialComponent> _materialQuery;
private EntityQuery<MaterialStorageComponent> _storageQuery;
private EntityQuery<PhysicalCompositionComponent> _compositionQuery;
public override void Initialize()
{
base.Initialize();
_material = EntMan.System<SharedMaterialStorageSystem>();
_power = EntMan.System<SharedPowerReceiverSystem>();
_materialQuery = EntMan.GetEntityQuery<MaterialComponent>();
_storageQuery = EntMan.GetEntityQuery<MaterialStorageComponent>();
_compositionQuery = EntMan.GetEntityQuery<PhysicalCompositionComponent>();
}
public override bool Insert(EntityUid item)
{
return base.Insert(item) && _material.TryInsertMaterialEntity(user: Owner, item, Owner);
}
public override bool CanInsert(EntityUid item)
{
if (!base.CanInsert(item) || !_storageQuery.TryComp(Owner, out var storage))
return false;
// don't bypass power check for lathes and stuff
if (!_power.IsPowered(Owner))
return false;
// this has to be essentially copypasted because goidacode doesnt have a CanInsertMaterial method
if (!_materialQuery.HasComp(item) || !_compositionQuery.HasComp(item))
return false;
// not checking volume etc since all lathes currently have unlimited capacity
return _whitelist.IsWhitelistPassOrNull(storage.Whitelist, item);
}
}

View File

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.DeviceLinking;
using Robust.Shared.Prototypes;
namespace Content.Shared._Goobstation.Factory.Slots;
/// <summary>
/// Adds no item I/O, only enables signal ports.
/// </summary>
public sealed partial class AutomatedPorts : AutomationSlot
{
[DataField]
public ProtoId<SinkPortPrototype>[] Sinks = [];
[DataField]
public ProtoId<SourcePortPrototype>[] Sources = [];
public override void AddPorts()
{
base.AddPorts();
_device.EnsureSinkPorts(Owner, Sinks);
_device.EnsureSourcePorts(Owner, Sources);
}
public override void RemovePorts()
{
base.RemovePorts();
foreach (var port in Sinks)
{
_device.RemoveSinkPort(Owner, port);
}
foreach (var port in Sources)
{
_device.RemoveSourcePort(Owner, port);
}
}
}

View File

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Storage;
using Content.Shared.Storage.EntitySystems;
namespace Content.Shared._Goobstation.Factory.Slots;
/// <summary>
/// Abstraction over a <see cref="StorageComponent"/> grid inventory.
/// </summary>
public sealed partial class AutomatedStorage : AutomationSlot
{
private SharedStorageSystem _storage;
private StorageComponent _comp;
public override void Initialize()
{
base.Initialize();
_storage = EntMan.System<SharedStorageSystem>();
_comp = EntMan.GetComponent<StorageComponent>(Owner);
}
public override bool Insert(EntityUid item)
{
return base.Insert(item) &&
_storage.Insert(Owner, item, out _, storageComp: _comp);
}
public override bool CanInsert(EntityUid item)
{
return base.CanInsert(item) &&
_storage.CanInsert(Owner, item, out _, storageComp: _comp);
}
public override EntityUid? GetItem(EntityUid? filter)
{
foreach (var item in _comp.Container.ContainedEntities)
{
if (_filter.IsAllowed(filter, item))
return item;
}
return null;
}
}

View File

@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Goobstation.Factory.Filters;
using Content.Shared.DeviceLinking;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared._Goobstation.Factory.Slots;
/// <summary>
/// An abstraction over some way to insert/take an item from a machine.
/// </summary>
[ImplicitDataDefinitionForInheritors]
public abstract partial class AutomationSlot
{
/// <summary>
/// The input port for this slot, or null if can only be used as an output.
/// </summary>
[DataField]
public ProtoId<SinkPortPrototype>? Input;
/// <summary>
/// The output port for this slot, or null if can only be used as an input.
/// </summary>
[DataField]
public ProtoId<SourcePortPrototype>? Output;
/// <summary>
/// Whitelist that can be used in YML regardless of slot type.
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Blacklist that can be used in YML regardless of slot type.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
/// <summary>
/// The automated machine this slot belongs to.
/// </summary>
[ViewVariables]
public EntityUid Owner;
[Dependency] public readonly IEntityManager EntMan = default!;
protected AutomationFilterSystem _filter;
protected EntityWhitelistSystem _whitelist;
protected SharedDeviceLinkSystem _device;
/// <summary>
/// Initialize the slot after <see cref="Owner"/> is set.
/// System dependencies don't work so inheritors have to call <c>base.Initialize()</c> and then add their systems.
/// </summary>
public virtual void Initialize()
{
IoCManager.InjectDependencies(this);
_filter = EntMan.System<AutomationFilterSystem>();
_whitelist = EntMan.System<EntityWhitelistSystem>();
_device = EntMan.System<SharedDeviceLinkSystem>();
}
/// <summary>
/// Try to insert an item into the slot, returning true if it was removed from its previous container.
/// Inheritors must override this and use <c>if (!base.Insert(uid, item)) return false;</c>
/// </summary>
public virtual bool Insert(EntityUid item)
{
return CanInsert(item);
}
/// <summary>
/// Check if an item can be inserted into the slot, returning true if it can.
/// Inheritors must override this and use <c>if (!base.CanInsert(uid, item)) return false;</c>
/// </summary>
public virtual bool CanInsert(EntityUid item)
{
return _whitelist.CheckBoth(item, whitelist: Whitelist, blacklist: Blacklist);
}
/// <summary>
/// Get an item that can be taken from this slot, which has to match a given filter.
/// If there are multiple items, which one returned is arbitrary and should not be relied upon.
/// This should be "pure" and not actually modify anything.
/// </summary>
public virtual EntityUid? GetItem(EntityUid? filter)
{
return null;
}
/// <summary>
/// Called to add all of this slot's ports to the machine.
/// </summary>
public virtual void AddPorts()
{
if (Input is {} input)
_device.EnsureSinkPorts(Owner, input);
if (Output is {} output)
_device.EnsureSourcePorts(Owner, output);
}
/// <summary>
/// Called to remove all of this slot's ports from the machine.
/// </summary>
public virtual void RemovePorts()
{
if (Input is {} input)
_device.RemoveSinkPort(Owner, input);
if (Output is {} output)
_device.RemoveSourcePort(Owner, output);
}
}

View File

@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.DeviceLinking;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._Goobstation.Factory;
/// <summary>
/// Machine that can be started with a signal.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(StartableMachineSystem))]
public sealed partial class StartableMachineComponent : Component
{
/// <summary>
/// Port you invoke to start the machine and raise <see cref="MachineStartedEvent"/>.
/// </summary>
[DataField]
public ProtoId<SinkPortPrototype> StartPort = "Start";
/// <summary>
/// Controls <see cref="AutoStart"/>.
/// Pulses toggle instead of setting true/false.
/// </summary>
[DataField]
public ProtoId<SinkPortPrototype> AutoStartPort = "AutoStart";
/// <summary>
/// Whether starting will work when <c>TryAutoStart</c> is called.
/// </summary>
/// <remarks>
/// Signals aren't predicted yet so not networked.
/// </remarks>
[DataField(serverOnly: true)]
public bool AutoStart;
/// <summary>
/// Queues an auto start for the next tick.
/// </summary>
[DataField(serverOnly: true)]
public bool AutoStartQueued;
[DataField]
public ProtoId<SourcePortPrototype> StartedPort = "Started";
[DataField]
public ProtoId<SourcePortPrototype> CompletedPort = "Completed";
[DataField]
public ProtoId<SourcePortPrototype> FailedPort = "Failed";
}
/// <summary>
/// Raised on the server when the start port is invoked while powered.
/// </summary>
[ByRefEvent]
public readonly record struct MachineStartedEvent();

View File

@ -0,0 +1,144 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
// SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Power.EntitySystems;
namespace Content.Shared._Goobstation.Factory;
public sealed class StartableMachineSystem : EntitySystem
{
[Dependency] private readonly SharedDeviceLinkSystem _device = default!;
[Dependency] private readonly SharedPowerReceiverSystem _power = default!;
private EntityQuery<StartableMachineComponent> _query;
public override void Initialize()
{
base.Initialize();
_query = GetEntityQuery<StartableMachineComponent>();
SubscribeLocalEvent<StartableMachineComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<StartableMachineComponent, SignalReceivedEvent>(OnSignalReceived);
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<StartableMachineComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (!comp.AutoStartQueued)
continue;
comp.AutoStartQueued = false;
TryAutoStart((uid, comp));
}
}
private void OnInit(Entity<StartableMachineComponent> ent, ref ComponentInit args)
{
_device.EnsureSinkPorts(ent, ent.Comp.StartPort, ent.Comp.AutoStartPort);
_device.EnsureSourcePorts(ent, ent.Comp.StartedPort, ent.Comp.CompletedPort, ent.Comp.FailedPort);
}
private void OnSignalReceived(Entity<StartableMachineComponent> ent, ref SignalReceivedEvent args)
{
if (args.Port == ent.Comp.StartPort)
{
TryStart((ent, ent.Comp));
}
else if (args.Port == ent.Comp.AutoStartPort)
{
var state = SignalState.Momentary;
args.Data?.TryGetValue<SignalState>("logic_state", out state);
ent.Comp.AutoStart = state switch
{
SignalState.Momentary => !ent.Comp.AutoStart,
SignalState.High => true,
SignalState.Low => false
};
}
}
#region Public API
/// <summary>
/// Starts the machine if powered.
/// </summary>
public bool TryStart(Entity<StartableMachineComponent?> ent)
{
if (!_query.Resolve(ent, ref ent.Comp)
|| !_power.IsPowered(ent.Owner))
return false;
var ev = new MachineStartedEvent();
RaiseLocalEvent(ent, ref ev);
return true;
}
/// <summary>
/// Starts the machine if powered and autostart is enabled.
/// </summary>
public bool TryAutoStart(Entity<StartableMachineComponent?> ent)
{
if (!_query.Resolve(ent, ref ent.Comp)
|| !ent.Comp.AutoStart)
return false;
return TryStart(ent);
}
/// <summary>
/// Invokes a port if the machine is powered.
/// </summary>
public void InvokeIfPowered(EntityUid uid, string port)
{
if (_power.IsPowered(uid))
_device.InvokePort(uid, port);
}
/// <summary>
/// Invoke the start port if powered.
/// </summary>
public void Started(Entity<StartableMachineComponent?> ent)
{
if (!_query.Resolve(ent, ref ent.Comp))
return;
InvokeIfPowered(ent, ent.Comp.StartedPort);
}
/// <summary>
/// Invoke the completed port if powered.
/// Also queues an autostart if <c>autoStart</c> is true
/// </summary>
public void Completed(Entity<StartableMachineComponent?> ent, bool autoStart = true)
{
if (!_query.Resolve(ent, ref ent.Comp))
return;
InvokeIfPowered(ent, ent.Comp.CompletedPort);
if (autoStart)
ent.Comp.AutoStartQueued = true;
}
/// <summary>
/// Invoke the failed port if powered.
/// </summary>
public void Failed(Entity<StartableMachineComponent?> ent)
{
if (!_query.Resolve(ent, ref ent.Comp))
return;
InvokeIfPowered(ent, ent.Comp.FailedPort);
}
#endregion
}

View File

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.DeviceLinking;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Factory;
/// <summary>
/// Makes a storage check filter slot and invoke signals.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(StorageBinSystem))]
public sealed partial class StorageBinComponent : Component
{
/// <summary>
/// Signal port invoked after inserting an item.
/// </summary>
[DataField]
public ProtoId<SourcePortPrototype> InsertedPort = "StorageInserted";
/// <summary>
/// Signal port invoked after removing an item.
/// </summary>
[DataField]
public ProtoId<SourcePortPrototype> RemovedPort = "StorageRemoved";
}
[Serializable, NetSerializable]
public enum StorageBinLayers : byte
{
Powered
}

View File

@ -0,0 +1,47 @@
using Content.Shared._Goobstation.Factory.Filters;
using Content.Shared.DeviceLinking;
using Robust.Shared.Containers;
namespace Content.Shared._Goobstation.Factory;
public sealed class StorageBinSystem : EntitySystem
{
[Dependency] private readonly AutomationFilterSystem _filter = default!;
[Dependency] private readonly SharedDeviceLinkSystem _device = default!;
public const string ContainerId = "storagebase";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StorageBinComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<StorageBinComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
SubscribeLocalEvent<StorageBinComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
}
private void OnInsertAttempt(Entity<StorageBinComponent> ent, ref ContainerIsInsertingAttemptEvent args)
{
if (args.Container.ID != ContainerId)
return;
if (_filter.IsBlocked(_filter.GetSlot(ent), args.EntityUid))
args.Cancel();
}
private void OnEntInserted(Entity<StorageBinComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != ContainerId)
return;
_device.InvokePort(ent, ent.Comp.InsertedPort);
}
private void OnEntRemoved(Entity<StorageBinComponent> ent, ref EntRemovedFromContainerMessage args)
{
if (args.Container.ID != ContainerId)
return;
_device.InvokePort(ent, ent.Comp.RemovedPort);
}
}

View File

@ -0,0 +1 @@
upgrade-kit-automation = [color=cyan]Automation[/color]: provides [color=green]signal linking[/color] and [color=green]robotic arm item ports[/color].

View File

@ -0,0 +1,2 @@
constructor-examine-unset = There is no construction configured.
constructor-examine = It is configured to construct {INDEFINITE($name)} [bold]{$name}[/bold].

View File

@ -0,0 +1,22 @@
automation-filter-examine-empty = [color=red]This filter isn't configured yet.[/color]
automation-filter-examine-string = This filter is set to '{$name}'
stack-filter-examine = This filter is set to a minimum of [color=green]{$size}[/color] items in a stack.
combined-filter-examine = This filter is set to {INDEFINITE($gate)} [color=green]{$gate}[/color] comparison with its inputs.
pressure-filter-examine = This filter is set to between [color=green]{$min}[/color] kPa and [color=green]{$max}[/color] kPa.
label-filter-window-title = Edit Label Filter
label-filter-placeholder = label to match against
name-filter-window-title = Edit Name Filter
name-filter-mode-Contain = Contain
name-filter-mode-Start = Start with
name-filter-mode-End = End with
name-filter-mode-Match = Match exactly
stack-filter-window-title = Edit Stack Filter
stack-filter-min-stack-size = Min stack size
stack-filter-stack-chunk-size = Out chunk size
pressure-filter-window-title = Edit Pressure Filter
pressure-filter-min-pressure = Min Pressure
pressure-filter-max-pressure = Max Pressure

View File

@ -0,0 +1,4 @@
robotic-arm-examine-no-filter = There is no filter installed.
robotic-arm-examine-filter = A [color=white]{$filter}[/color] is installed.
robotic-arm-examine-no-item = There is no item held.
robotic-arm-examine-item = It is holding {INDEFINITE($item)} [color=white]{$item}[/color].

View File

@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2024 Aidenkrz <aiden@djkraz.com>
# SPDX-FileCopyrightText: 2024 Piras314 <p1r4s@proton.me>
# SPDX-FileCopyrightText: 2024 Theapug <159912420+Teapug@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 Aiden <aiden@djkraz.com>
# SPDX-FileCopyrightText: 2025 John Willis <143434770+CerberusWolfie@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
guide-entry-automation = Automation

View File

@ -0,0 +1,102 @@
# Robotic Arm
signal-port-name-input-machine = Item: Input Machine
signal-port-description-input-machine = A machine automation slot to take items out of, instead of taking them from the floor.
signal-port-name-output-machine = Item: Output Machine
signal-port-description-output-machine = A machine automation slot to insert items into, instead of placing them on the floor.
signal-port-name-item-moved = Item Moved
signal-port-description-item-moved = Signal port that gets pulsed after an item is moved by this arm.
signal-port-name-automation-slot-filter = Item: Filter Slot
signal-port-description-automation-slot-filter = An automation slot for an automation machine's filter.
# Reagent Grinder
signal-port-name-automation-slot-beaker = Item: Beaker Slot
signal-port-description-automation-slot-beaker = An automation slot for a liquid-handling machine's beaker.
signal-port-name-automation-slot-input = Item: Input items
signal-port-description-automation-slot-input = An automation slot for a machine's input item storage.
# Flatpacker
signal-port-name-automation-slot-board = Item: Board Slot
signal-port-description-automation-slot-board = An automation slot for a flatpacker's circuitboard.
signal-port-name-automation-slot-materials = Item: Material Storage
signal-port-description-automation-slot-materials = An automation slot for inserting materials into a machine's storage.
# Disposal Unit
signal-port-name-flush = Flush
signal-port-description-flush = Signal port to toggle a disposal unit's flush mechanism.
signal-port-name-eject = Eject
signal-port-description-eject = Signal port to eject a disposal unit's contents.
signal-port-name-ready = Ready
signal-port-description-ready = Signal port that gets pulsed after a disposal unit becomes fully pressurized.
# Storage Bin
signal-port-name-automation-slot-storage = Item: Storage
signal-port-description-automation-slot-storage = An automation slot for a storage bin's inventory.
signal-port-name-storage-inserted = Inserted
signal-port-description-storage-inserted = Signal port that gets pulsed after an item is inserted into a storage bin.
signal-port-name-storage-removed = Removed
signal-port-description-storage-removed = Signal port that gets pulsed after an item is removed from a storage bin.
# Fax Machine
signal-port-name-automation-slot-paper = Item: Paper
signal-port-description-automation-slot-paper = An automation slot for a fax machine's paper tray.
signal-port-name-fax-copy = Copy Fax
signal-port-description-fax-copy = Signal port to copy a fax machine's paper.
# Constructor / Interactor
signal-port-name-machine-start = Start
signal-port-description-machine-start = Signal port to start a machine once.
signal-port-name-machine-autostart = Auto Start
signal-port-description-machine-autostart = Signal port to control starting after completing automatically.
signal-port-name-machine-started = Started
signal-port-description-machine-started = Signal port that gets pulsed after a machine starts.
signal-port-name-machine-completed = Completed
signal-port-description-machine-completed = Signal port that gets pulsed after a machine completes its work.
signal-port-name-machine-failed = Failed
signal-port-description-machine-failed = Signal port that gets pulsed after a machine fails to start.
# Interactor
signal-port-name-automation-slot-tool = Item: Tool
signal-port-description-automation-slot-tool = An automation slot for an interactor's held tool.
# Autodoc
signal-port-name-automation-slot-autodoc-hand = Item: Autodoc Hand
signal-port-description-automation-slot-autodoc-hand = An automation slot for an autodoc's held organ/part/etc from STORE ITEM / GRAB ITEM instructions.
# Gas Canister
signal-port-name-automation-slot-gas-tank = Item: Gas Tank
signal-port-description-automation-slot-gas-tank = An automation slot for a gas tank.
# Radiation Collector
signal-port-name-rad-empty = Empty
signal-port-description-rad-empty = Signal port set to HIGH if the tank is missing or below 33% pressure, LOW otherwise.
signal-port-name-rad-low = Low
signal-port-description-rad-low = Signal port set to HIGH if the tank is below 66% pressure, LOW otherwise.
signal-port-name-rad-full = Full
signal-port-description-rad-full = Signal port set to HIGH if the tank is above 66% pressure, LOW otherwise.

View File

@ -67,6 +67,15 @@
- type: ContainerContainer
containers:
Paper: !type:ContainerSlot
- type: AutomationSlots # Goobstation
slots:
- !type:AutomatedItemSlot
input: AutomationSlotPaper
output: AutomationSlotPaper
slotId: Paper
- !type:AutomatedPorts
sinks:
- FaxCopy
- type: DeviceNetworkRequiresPower
- type: DeviceNetwork
deviceNetId: Wireless

View File

@ -69,6 +69,18 @@
components:
- MachineBoard
- ComputerBoard
- type: AutomationSlots # Goobstation
slots:
- !type:AutomatedItemSlot
input: AutomationSlotBoard
output: AutomationSlotBoard
slotId: board_slot
- !type:AutomatedMaterialStorage
input: AutomationSlotMaterials
output: null # no automatic silo stealer...
- !type:AutomatedPorts
sinks:
- On
- type: ContainerContainer
containers:
machine_board: !type:Container

View File

@ -48,6 +48,11 @@
- type: StaticPrice
price: 800
- type: ResearchClient
- type: AutomationSlots # Goobstation
slots:
- !type:AutomatedMaterialStorage
input: AutomationSlotMaterials
output: null
- type: TechnologyDatabase
supportedDisciplines: # DeltaV - don't add it to every map
- Industrial
@ -282,6 +287,7 @@
- CameraBoards
- MechBoards
- ShuttleBoards
- EngineeringBoardsGoob #Goob Factorio port
- type: EmagLatheRecipes
emagDynamicPacks:
- SecurityBoards

View File

@ -99,6 +99,13 @@
microwave_entity_container: !type:Container
machine_board: !type:Container
machine_parts: !type:Container
- type: AutomationSlots # Goobstation
slots:
- !type:AutomatedContainer
input: AutomationSlotInput
output: AutomationSlotInput
containerId: microwave_entity_container
maxItems: 10 # have to manually keep it in sync with capacity on Microwave at the top
- type: EmptyOnMachineDeconstruct
containers:
- microwave_entity_container

View File

@ -40,6 +40,20 @@
state: "grinder_empty"
- type: ApcPowerReceiver
powerLoad: 300
- type: AutomationSlots # Goobstation
slots:
- !type:AutomatedItemSlot
input: AutomationSlotBeaker
output: AutomationSlotBeaker
slotId: beakerSlot
- !type:AutomatedContainer
input: AutomationSlotInput
output: AutomationSlotInput
whitelist:
components:
- Extractable # shitcode doesnt require this with container attempt events just in interaction
containerId: inputContainer
maxItems: 6 # manually have to sync it with ReagentGrinderComponent :
- type: ItemSlots
slots:
beakerSlot:

View File

@ -23,6 +23,11 @@
- Sheet
- RawMaterial
- Ingot
- type: AutomationSlots # Goobstation
slots:
- !type:AutomatedMaterialStorage
input: AutomationSlotMaterials
output: null
- type: ActivatableUI
key: enum.OreSiloUiKey.Key
- type: ActivatableUIRequiresPower

View File

@ -77,6 +77,20 @@
- type: StaticPrice
price: 70
- type: PowerSwitch
- type: AutomationSlots # Goobstation
slots:
- !type:AutomatedContainer
input: AutomationSlotInput
output: AutomationSlotInput
containerId: disposals
maxItems: 30 # disposals doesn't have a limit this is just so you can't use it as an ME system
- !type:AutomatedPorts
sinks:
- Toggle
- DisposalFlush
- DisposalEject
sources:
- DisposalReady
- type: entity
id: DisposalUnit

View File

@ -63,6 +63,18 @@
powerGenerationEfficiency: 1
reactantBreakdownRate: 0.0001
- type: RadiationReceiver
- type: RadCollectorSignal # Goobstation
- type: AutomationSlots # Goobstation
slots:
- !type:AutomatedItemSlot
input: AutomationSlotGasTank
output: AutomationSlotGasTank
slotId: gas_tank
- !type:AutomatedPorts
sources:
- RadEmpty
- RadLow
- RadFull
- type: PowerSupplier
- type: Anchorable
- type: Rotatable

View File

@ -95,6 +95,17 @@
rotationsEnabled: false
volume: 1
- type: ItemSlots
- type: AutomationSlots # Goobstation
slots:
- !type:AutomatedItemSlot
input: AutomationSlotGasTank
output: AutomationSlotGasTank
slotId: tank_slot
- !type:AutomatedPorts
sinks:
- Open
- Close
- Toggle
- type: GasPortable
- type: GasCanister
gasTankSlot:

View File

@ -8,6 +8,7 @@
- Atmospherics
- ShuttleCraft
- Networking
- Automation
- type: guideEntry
id: Construction

View File

@ -28,7 +28,7 @@
recipeUnlocks:
- RadarConsoleCircuitboard
- HandHeldMassScanner
- type: technology
id: AdvancedPowercells
name: research-technology-advanced-powercells
@ -74,6 +74,13 @@
- LatheUpgradeKitHyper
- LatheUpgradeKitCryo
# End DeltaV Additions
# Begin Goobstation Additions
- UpgradeKitAutomation
- RoboticArmCircuitboard
- StorageBinCircuitboard
- InteractorCircuitboard
- ConstructorCircuitboard
# End Goobstation Additions
- SheetifierMachineCircuitboard
- type: technology

View File

@ -12,6 +12,13 @@
sprite: _DV/Structures/Machines/advanced_microwave.rsi
- type: Machine
board: AdvancedMicrowaveMachineCircuitBoard
- type: AutomationSlots # Goobstation factorio port
slots:
- !type:AutomatedContainer
input: AutomationSlotInput
output: AutomationSlotInput
containerId: microwave_entity_container
maxItems: 30 # have to manually keep it in sync with capacity on Microwave at the top
- type: Explosive
explosionType: Radioactive
maxIntensity: 60

View File

@ -90,6 +90,7 @@
- FauxTiles
- Equipment
- UpgradeKits
- UpgradeKits_Goob
- EngineeringHardsuits
- type: Machine
board: EngineeringTechFabCircuitboard

View File

@ -0,0 +1,108 @@
# SPDX-FileCopyrightText: 2024 Aviu00 <93730715+Aviu00@users.noreply.github.com>
# SPDX-FileCopyrightText: 2024 Piras314 <p1r4s@proton.me>
# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
# Robotic Arm
- type: sinkPort
id: RoboticArmInput
name: signal-port-name-input-machine
description: signal-port-description-input-machine
- type: sinkPort
id: AutomationSlotFilter
name: signal-port-name-automation-slot-filter
description: signal-port-description-automation-slot-filter
# Reagent Grinder
- type: sinkPort
id: AutomationSlotBeaker
name: signal-port-name-automation-slot-beaker
description: signal-port-description-automation-slot-beaker
- type: sinkPort
id: AutomationSlotInput
name: signal-port-name-automation-slot-input
description: signal-port-description-automation-slot-input
# Flatpacker
- type: sinkPort
id: AutomationSlotBoard
name: signal-port-name-automation-slot-board
description: signal-port-description-automation-slot-board
- type: sinkPort
id: AutomationSlotMaterials
name: signal-port-name-automation-slot-materials
description: signal-port-description-automation-slot-materials
# Storage Bin
- type: sinkPort
id: AutomationSlotStorage
name: signal-port-name-automation-slot-storage
description: signal-port-description-automation-slot-storage
# Constructor / Interactor
- type: sinkPort
id: Start
name: signal-port-name-machine-start
description: signal-port-description-machine-start
- type: sinkPort
id: AutoStart
name: signal-port-name-machine-autostart
description: signal-port-description-machine-autostart
# Interactor
- type: sinkPort
id: AutomationSlotTool
name: signal-port-name-automation-slot-tool
description: signal-port-description-automation-slot-tool
# Autodoc
- type: sinkPort
id: AutomationSlotAutodocHand
name: signal-port-name-automation-slot-autodoc-hand
description: signal-port-description-automation-slot-autodoc-hand
# Gas Canister
- type: sinkPort
id: AutomationSlotGasTank
name: signal-port-name-automation-slot-gas-tank
description: signal-port-description-automation-slot-gas-tank
# Fax Machine
- type: sinkPort
id: AutomationSlotPaper
name: signal-port-name-automation-slot-paper
description: signal-port-description-automation-slot-paper
- type: sinkPort
id: FaxCopy
name: signal-port-name-fax-copy
description: signal-port-description-fax-copy
# Disposal Unit
- type: sinkPort
id: DisposalFlush
name: signal-port-name-flush
description: signal-port-description-flush
- type: sinkPort
id: DisposalEject
name: signal-port-name-eject
description: signal-port-description-eject

View File

@ -0,0 +1,133 @@
# SPDX-FileCopyrightText: 2024 Aviu00 <93730715+Aviu00@users.noreply.github.com>
# SPDX-FileCopyrightText: 2024 Piras314 <p1r4s@proton.me>
# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 Aiden <aiden@djkraz.com>
# SPDX-FileCopyrightText: 2025 Fishbait <Fishbait@git.ml>
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
# SPDX-FileCopyrightText: 2025 fishbait <gnesse@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
# Robotic Arm
- type: sourcePort
id: RoboticArmOutput
name: signal-port-name-output-machine
description: signal-port-description-output-machine
- type: sourcePort
id: RoboticArmMoved
name: signal-port-name-item-moved
description: signal-port-description-item-moved
- type: sourcePort
id: AutomationSlotFilter
name: signal-port-name-automation-slot-filter
description: signal-port-description-automation-slot-filter
# Reagent Grinder
- type: sourcePort
id: AutomationSlotBeaker
name: signal-port-name-automation-slot-beaker
description: signal-port-description-automation-slot-beaker
- type: sourcePort
id: AutomationSlotInput
name: signal-port-name-automation-slot-input
description: signal-port-description-automation-slot-input
# Flatpacker
- type: sourcePort
id: AutomationSlotBoard
name: signal-port-name-automation-slot-board
description: signal-port-description-automation-slot-board
# Storage Bin
- type: sourcePort
id: AutomationSlotStorage
name: signal-port-name-automation-slot-storage
description: signal-port-description-automation-slot-storage
- type: sourcePort
id: StorageInserted
name: signal-port-name-storage-inserted
description: signal-port-description-storage-inserted
- type: sourcePort
id: StorageRemoved
name: signal-port-name-storage-removed
description: signal-port-description-storage-removed
# Constructor / Interactor
- type: sourcePort
id: Started
name: signal-port-name-machine-started
description: signal-port-description-machine-started
- type: sourcePort
id: Completed
name: signal-port-name-machine-completed
description: signal-port-description-machine-completed
- type: sourcePort
id: Failed
name: signal-port-name-machine-failed
description: signal-port-description-machine-failed
# Interactor
- type: sourcePort
id: AutomationSlotTool
name: signal-port-name-automation-slot-tool
description: signal-port-description-automation-slot-tool
# Autodoc
- type: sourcePort
id: AutomationSlotAutodocHand
name: signal-port-name-automation-slot-autodoc-hand
description: signal-port-description-automation-slot-autodoc-hand
# Gas Canister
- type: sourcePort
id: AutomationSlotGasTank
name: signal-port-name-automation-slot-gas-tank
description: signal-port-description-automation-slot-gas-tank
# Radiation Collector
- type: sourcePort
id: RadEmpty
name: signal-port-name-rad-empty
description: signal-port-description-rad-empty
- type: sourcePort
id: RadLow
name: signal-port-name-rad-low
description: signal-port-description-rad-low
- type: sourcePort
id: RadFull
name: signal-port-name-rad-full
description: signal-port-description-rad-full
# Fax Machine
- type: sourcePort
id: AutomationSlotPaper
name: signal-port-name-automation-slot-paper
description: signal-port-description-automation-slot-paper
# Disposal Unit
- type: sourcePort
id: DisposalReady
name: signal-port-name-ready
description: signal-port-description-ready

View File

@ -0,0 +1,79 @@
# SPDX-FileCopyrightText: 2024 Aviu00 <93730715+Aviu00@users.noreply.github.com>
# SPDX-FileCopyrightText: 2024 Piras314 <p1r4s@proton.me>
# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: entity
parent: BaseMachineCircuitboard
id: RoboticArmCircuitboard
name: robotic arm machine board
description: A machine printed circuit board for a robotic arm.
components:
- type: Sprite
state: engineering
- type: MachineBoard
prototype: RoboticArm
stackRequirements:
Manipulator: 4
Steel: 6
Cable: 5
tagRequirements:
BorgArm:
amount: 2
defaultPrototype: LeftArmBorg
- type: entity
parent: BaseMachineCircuitboard
id: ConstructorCircuitboard
name: constructor machine board
description: A machine printed circuit board for a constructor.
components:
- type: Sprite
state: engineering
- type: MachineBoard
prototype: Constructor
stackRequirements:
Manipulator: 3
MatterBin: 2
Steel: 10
Cable: 5
tagRequirements:
BorgArm:
amount: 4
defaultPrototype: LeftArmBorg
- type: entity
parent: BaseMachineCircuitboard
id: StorageBinCircuitboard
name: storage bin machine board
description: A machine printed circuit board for a storage bin.
components:
- type: MachineBoard
prototype: StorageBin
stackRequirements:
MatterBin: 2
Manipulator: 2
Steel: 1
- type: entity
parent: BaseMachineCircuitboard
id: InteractorCircuitboard
name: interactor machine board
description: A machine printed circuit board for an interactor.
components:
- type: Sprite
state: engineering
- type: MachineBoard
prototype: Interactor
stackRequirements:
Manipulator: 4
Plastic: 4
Cable: 5
tagRequirements:
BorgArm:
amount: 1
defaultPrototype: LeftArmBorg

View File

@ -0,0 +1,114 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: entity
abstract: true
parent: BaseItem
id: BaseAutomationFilter
name: filter
description: A filter that can be installed in factory machines.
components:
- type: Sprite
sprite: _Goobstation/Objects/Misc/filter.rsi
state: icon
- type: Item
size: Tiny
- type: AutomationFilter
- type: Construction
graph: AutomationFilter
- type: GuideHelp
guides:
- Automation
- type: entity
parent: BaseAutomationFilter
id: AutomationFilterLabel
name: label filter
description: A filter that can be installed in factory machines. This one scans labels of attached items.
components:
- type: LabelFilter
- type: ActivatableUI
key: enum.LabelFilterUiKey.Key
- type: UserInterface
interfaces:
enum.LabelFilterUiKey.Key:
type: LabelFilterBUI
- type: Construction
node: label
- type: entity
parent: BaseAutomationFilter
id: AutomationFilterName
name: name filter
description: A filter that can be installed in factory machines. This one uses complex AI vision technology to identify items.
components:
- type: NameFilter
- type: ActivatableUI
key: enum.NameFilterUiKey.Key
- type: UserInterface
interfaces:
enum.NameFilterUiKey.Key:
type: NameFilterBUI
- type: Construction
node: name
- type: entity
parent: BaseAutomationFilter
id: AutomationFilterStack
name: stack filter
description: A filter that can be installed in factory machines. This one weighs items to compare it to a stack size.
components:
- type: StackFilter
- type: ActivatableUI
key: enum.StackFilterUiKey.Key
- type: UserInterface
interfaces:
enum.StackFilterUiKey.Key:
type: StackFilterBUI
- type: Construction
node: stack
- type: entity
parent: BaseAutomationFilter
id: AutomationFilterPressure
name: pressure filter
description: A filter that can be installed in factory machines. This one has a barometer to check the pressure of gases.
components:
- type: PressureFilter
- type: ActivatableUI
key: enum.PressureFilterUiKey.Key
- type: UserInterface
interfaces:
enum.PressureFilterUiKey.Key:
type: PressureFilterBUI
- type: Construction
node: pressure
- type: entity
parent: BaseAutomationFilter
id: AutomationFilterCombined
name: combined filter
description: A filter that can be installed in factory machines. This one uses a logic gate to combine 2 installed item filters.
components:
- type: CombinedFilter
- type: ItemSlots
slots:
combined_filter_a:
name: Filter A
whitelist:
components:
- AutomationFilter
combined_filter_b:
name: Filter B
whitelist:
components:
- AutomationFilter
- type: ContainerContainer
containers:
combined_filter_a: !type:ContainerSlot
combined_filter_b: !type:ContainerSlot
- type: Construction
node: combined

View File

@ -0,0 +1,46 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: entity
parent: BaseItem
id: UpgradeKitAutomation
name: automation upgrade kit
description: An upgrade kit with all the parts needed to upgrade a machine. This one allows extra automation options by linking robotic arms.
components:
- type: Sprite
sprite: _DV/Objects/Tools/lathe_upgrade_kit.rsi
state: icon
- type: UpgradeKit
whitelist:
components:
- AutomationSlots # automation needs code to support it, can't work on literally any machine
blacklist:
components:
- Automated
- UpgradedMachine
components:
- type: UpgradedMachine
upgrade: upgrade-kit-automation
- type: Automated
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: BasicDevice
# for entities that come pre-automated
- type: entity
abstract: true
id: BaseAutomatedMachine
components:
- type: AutomationSlots
- type: Automated
- type: UpgradedMachine
upgrade: upgrade-kit-automation
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: BasicDevice
- type: GuideHelp
guides:
- Automation

View File

@ -0,0 +1,46 @@
# SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: entity
parent: StorageBin
id: Constructor
name: constructor
description: The machine putting assistants out of a job, it can build anything using supplied materials.
components:
# Appearance
- type: Sprite
sprite: _Goobstation/Structures/Machines/constructor.rsi
# Physics
- type: Transform
noRot: false
- type: Rotatable
rotateWhileAnchored: true
# Construction
- type: Machine
board: ConstructorCircuitboard
# UI
- type: ActivatableUI
key: enum.ConstructorUiKey.Key
- type: UserInterface
interfaces:
enum.StorageUiKey.Key:
type: StorageBoundUserInterface
enum.ConstructorUiKey.Key:
type: ConstructorBUI
# Constructor
- type: Constructor
- type: DoAfter
- type: StartableMachine
- type: DeviceLinkSink
ports:
- Start
- AutoStart
- type: DeviceLinkSource
ports:
- Started
- Completed
- Failed
# Power
- type: ApcPowerReceiver
powerLoad: 6000

View File

@ -0,0 +1,134 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: entity
parent: [ ConstructibleMachine, BaseMachinePowered, BaseAutomatedMachine ]
id: Interactor
name: interactor
description: A robotic actuator specialized in interacting with objects using tools.
components:
# Visuals
- type: Sprite
sprite: _Goobstation/Structures/Machines/interactor.rsi
layers:
- state: base
- state: empty
map: [ enum.InteractorLayers.Hand ]
- state: empty-powered
shader: unshaded
visible: false
map: [ enum.InteractorLayers.Powered ]
- type: Appearance
- type: GenericVisualizer
visuals:
enum.PowerDeviceVisuals.Powered:
enum.InteractorLayers.Powered:
True: { visible: true }
False: { visible: false }
enum.InteractorVisuals.State:
enum.InteractorLayers.Hand:
Empty: { state: empty }
Inactive: { state: inactive }
Active: { state: active }
enum.InteractorLayers.Powered:
Empty: { state: empty-powered }
Inactive: { state: inactive-powered }
Active: { state: active-powered }
# Physics
- type: Transform
noRot: false
- type: Rotatable
rotateWhileAnchored: true
- type: Fixtures
fixtures:
fix1:
shape: !type:PhysShapeCircle
radius: 0.25
density: 200
mask:
- MachineMask
layer:
- MachineLayer
interactor_target:
shape: !type:PhysShapeAabb
bounds: "-0.45,-1.45,0.45,-0.55"
density: 100
hard: false
layer:
- Impassable
# Construction
- type: Machine
board: InteractorCircuitboard
- type: Construction
containers:
- machine_board
- machine_parts
- interactor_tool
- filter_slot
- type: EmptyOnMachineDeconstruct
containers:
- interactor_tool
- filter_slot
- type: ContainerContainer
containers:
machine_board: !type:Container
machine_parts: !type:Container
interactor_tool: !type:ContainerSlot
filter_slot: !type:ContainerSlot
# Interactor
- type: ItemSlots
slots:
filter_slot:
whitelist:
components:
- AutomationFilter
- type: Interactor
- type: StartableMachine
- type: FilterSlot
- type: AutomationSlots
slots:
- !type:AutomatedHand
input: AutomationSlotTool
output: AutomationSlotTool
handName: interactor_tool
- !type:AutomatedItemSlot
input: AutomationSlotFilter
output: AutomationSlotFilter
slotId: filter_slot
# Fake interaction stuff
- type: DoAfter
raiseEndedEvent: true # so Completed can be fired off
- type: Hands
showInHands: false
- type: Strippable
handDelay: 0.5
- type: UserInterface # need stripping to be able to add/remove items without a robotic arm
interfaces:
enum.StrippingUiKey.Key:
type: StrippableBoundUserInterface
- type: HandsFill
hands:
interactor_tool: null # no tool by default
- type: ComplexInteraction
# Linking
- type: DeviceLinkSink
ports:
- Start
- AutoStart
- type: DeviceLinkSource
ports:
- Started
- Completed
- Failed
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: BasicDevice
- type: WirelessNetworkConnection
range: 5
# Power
- type: ApcPowerReceiver
powerLoad: 3500
- type: PowerSwitch

View File

@ -0,0 +1,121 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: entity
parent: [ ConstructibleMachine, BaseMachinePowered ]
id: RoboticArm
name: robotic arm
description: A high-tech robotic arm capable of moving items to and from machines that have automation upgrades.
components:
# Visuals
- type: Sprite
sprite: _Goobstation/Structures/Machines/robotic_arm.rsi
layers:
- state: base
- state: arm-long
map: [ "enum.RoboticArmLayers.Arm" ]
- state: powered
shader: unshaded
visible: false
map: [ "enum.RoboticArmLayers.Powered" ]
- type: Appearance
- type: GenericVisualizer
visuals:
enum.RoboticArmVisuals.HasItem:
enum.RoboticArmLayers.Arm:
# extended when waiting for items like factorio
True: { state: arm-short }
False: { state: arm-long }
enum.PowerDeviceVisuals.Powered:
enum.RoboticArmLayers.Powered:
True: { visible: true }
False: { visible: false }
# Physics
- type: Transform
noRot: false
- type: Rotatable
rotateWhileAnchored: true
- type: Fixtures
fixtures:
fix1:
shape: !type:PhysShapeCircle
radius: 0.25
density: 200
mask:
- MachineMask
layer:
- MachineLayer
robotic_arm_input:
shape: !type:PhysShapeAabb
bounds: "0.55,-0.45,1.45,0.45"
density: 100
hard: false
layer:
- Impassable
# Construction
- type: Machine
board: RoboticArmCircuitboard
- type: Construction
containers:
- machine_board
- machine_parts
- robotic_arm_item
- filter_slot
- type: EmptyOnMachineDeconstruct
containers:
- robotic_arm_item
- filter_slot
- type: ContainerContainer
containers:
machine_board: !type:Container
machine_parts: !type:Container
robotic_arm_item: !type:ContainerSlot
filter_slot: !type:ContainerSlot
# Arm
- type: ItemSlots
slots:
robotic_arm_item:
insertOnInteract: false
insertSound: null # it plays twice because theres no user to pass to PlayPredicted
whitelist:
components:
- Item
filter_slot:
whitelist:
components:
- AutomationFilter
- type: RoboticArm
- type: FilterSlot
- type: AutomationSlots
slots:
- !type:AutomatedItemSlot
input: AutomationSlotFilter
output: AutomationSlotFilter
slotId: filter_slot
# Linking
- type: DeviceLinkSink
ports:
- RoboticArmInput
# - On
# - Off
# TODO: ports to disable it
- type: DeviceLinkSource
ports:
- RoboticArmOutput
- RoboticArmMoved
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: BasicDevice
- type: WirelessNetworkConnection
range: 5
# Power
- type: ApcPowerReceiver
powerLoad: 300 # imp, static 300 power draw instead of idle 50 -> active 3000
- type: PowerSwitch
# Guide
- type: GuideHelp
guides:
- Automation

View File

@ -0,0 +1,88 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: entity
parent: [ ConstructibleMachine, BaseMachinePowered, BaseAutomatedMachine ]
id: StorageBin
name: storage bin
description: An electronically controlled storage bin intended for use with robotic arms.
components:
# Visuals
- type: Sprite
sprite: _Goobstation/Structures/Machines/storage_bin.rsi
layers:
- state: icon
- state: powered
shader: unshaded
visible: false
map: [ "enum.StorageBinLayers.Powered" ]
- type: Appearance
- type: GenericVisualizer
visuals:
enum.PowerDeviceVisuals.Powered:
enum.StorageBinLayers.Powered:
True: { visible: true }
False: { visible: false }
# Physics
- type: Fixtures
fixtures:
fix1:
shape: !type:PhysShapeCircle
radius: 0.3
density: 190
mask:
- MachineMask
layer:
- MachineLayer
# Construction
- type: Machine
board: StorageBinCircuitboard
- type: Construction
containers:
- machine_board
- machine_parts
- storagebase
- filter_slot
- type: EmptyOnMachineDeconstruct
containers:
- storagebase
- filter_slot
- type: ContainerContainer
containers:
machine_board: !type:Container
machine_parts: !type:Container
storagebase: !type:Container
filter_slot: !type:ContainerSlot
# Storage
- type: Storage
grid:
- 0,0,9,5
clickInsert: false # be nice to multitools
maxItemSize: Huge
- type: UserInterface
interfaces:
enum.StorageUiKey.Key:
type: StorageBoundUserInterface
- type: StorageBin
- type: FilterSlot
- type: ItemSlots
slots:
filter_slot:
whitelist:
components:
- AutomationFilter
- type: AutomationSlots
slots:
- !type:AutomatedItemSlot
input: AutomationSlotFilter
output: AutomationSlotFilter
slotId: filter_slot
- !type:AutomatedStorage
input: AutomationSlotStorage
output: AutomationSlotStorage
# Power
- type: ApcPowerReceiver
powerLoad: 200

View File

@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2024 Piras314 <p1r4s@proton.me>
# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: guideEntry
id: Automation
name: guide-entry-automation
text: "/ServerInfo/_Goobstation/Guidebook/Engineering/Automation.xml"

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