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:
parent
e18cda9df8
commit
f7db03182a
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} " +
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
upgrade-kit-automation = [color=cyan]Automation[/color]: provides [color=green]signal linking[/color] and [color=green]robotic arm item ports[/color].
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
constructor-examine-unset = There is no construction configured.
|
||||
constructor-examine = It is configured to construct {INDEFINITE($name)} [bold]{$name}[/bold].
|
||||
|
|
@ -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
|
||||
|
|
@ -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].
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
- Atmospherics
|
||||
- ShuttleCraft
|
||||
- Networking
|
||||
- Automation
|
||||
|
||||
- type: guideEntry
|
||||
id: Construction
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@
|
|||
- FauxTiles
|
||||
- Equipment
|
||||
- UpgradeKits
|
||||
- UpgradeKits_Goob
|
||||
- EngineeringHardsuits
|
||||
- type: Machine
|
||||
board: EngineeringTechFabCircuitboard
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
Loading…
Reference in New Issue