Data-oriented Construction System (#2152)

- Powerful
- Data-oriented
- Approved by PJB
- Powered by node graphs and AI pathfinding
- Coded by the same nerd who brought you atmos

Co-authored-by: Exp <theexp111@gmail.com>
This commit is contained in:
Víctor Aguilera Puerto
2020-10-08 17:41:23 +02:00
committed by GitHub
parent a6647e8de1
commit 745401a41e
261 changed files with 3886 additions and 11986 deletions

View File

@@ -1,20 +1,28 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Client.GameObjects.EntitySystems;
using Content.Client.Utility;
using Content.Shared.Construction;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.Materials;
using Microsoft.CodeAnalysis.Options;
using Robust.Client.Graphics;
using Robust.Client.Interfaces.Placement;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.Placement;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.Utility;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
@@ -25,376 +33,424 @@ namespace Content.Client.Construction
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
[Dependency] private readonly IPlacementManager _placementManager = default!;
private readonly Button BuildButton;
private readonly Button EraseButton;
private readonly LineEdit SearchBar;
private readonly Tree RecipeList;
private readonly TextureRect InfoIcon;
private readonly Label InfoLabel;
private readonly ItemList StepList;
protected override Vector2? CustomSize => (720, 320);
private CategoryNode RootCategory;
private ConstructionPrototype? _selected;
private string[] _categories = Array.Empty<string>();
// This list is flattened in such a way that the top most deepest category is first.
private List<CategoryNode> FlattenedCategories;
private readonly PlacementManager Placement;
protected override Vector2? CustomSize => (500, 350);
private readonly ItemList _recipes;
private readonly ItemList _stepList;
private readonly Button _buildButton;
private readonly Button _eraseButton;
private readonly LineEdit _searchBar;
private readonly OptionButton _category;
private readonly TextureRect _targetTexture;
private readonly RichTextLabel _targetName;
private readonly RichTextLabel _targetDescription;
public ConstructionMenu()
{
IoCManager.InjectDependencies(this);
Placement = (PlacementManager) IoCManager.Resolve<IPlacementManager>();
Placement.PlacementChanged += OnPlacementChanged;
_placementManager.PlacementChanged += PlacementChanged;
Title = "Construction";
var hSplitContainer = new HSplitContainer();
var hbox = new HBoxContainer() {SizeFlagsHorizontal = SizeFlags.FillExpand};
// Left side
var recipes = new VBoxContainer {CustomMinimumSize = new Vector2(150.0f, 0.0f)};
SearchBar = new LineEdit {PlaceHolder = "Search"};
RecipeList = new Tree {SizeFlagsVertical = SizeFlags.FillExpand, HideRoot = true};
recipes.AddChild(SearchBar);
recipes.AddChild(RecipeList);
hSplitContainer.AddChild(recipes);
var recipeContainer = new VBoxContainer() {SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.45f};
// Right side
var guide = new VBoxContainer();
var info = new HBoxContainer();
InfoIcon = new TextureRect();
InfoLabel = new Label
{
SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsVertical = SizeFlags.ShrinkCenter
};
info.AddChild(InfoIcon);
info.AddChild(InfoLabel);
guide.AddChild(info);
var searchContainer = new HBoxContainer() {SizeFlagsVertical = SizeFlags.FillExpand, SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.1f};
_searchBar = new LineEdit() {PlaceHolder = "Search", SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.6f};
_category = new OptionButton() {SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.4f};
var stepsLabel = new Label
{
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
Text = "Steps"
};
guide.AddChild(stepsLabel);
_recipes = new ItemList() {SelectMode = ItemList.ItemListSelectMode.Single, SizeFlagsVertical = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.9f};
StepList = new ItemList
{
SizeFlagsVertical = SizeFlags.FillExpand, SelectMode = ItemList.ItemListSelectMode.None
};
guide.AddChild(StepList);
var spacer = new Control() {SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.05f};
var buttonsContainer = new HBoxContainer();
BuildButton = new Button
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
TextAlign = Label.AlignMode.Center,
Text = "Build!",
Disabled = true,
ToggleMode = true
};
EraseButton = new Button
{
TextAlign = Label.AlignMode.Center, Text = "Clear Ghosts", ToggleMode = true
};
buttonsContainer.AddChild(BuildButton);
buttonsContainer.AddChild(EraseButton);
guide.AddChild(buttonsContainer);
var stepsContainer = new VBoxContainer() {SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.45f};
var targetContainer = new HBoxContainer() {Align = BoxContainer.AlignMode.Center, SizeFlagsVertical = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.25f};
_targetTexture = new TextureRect() {SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.15f, Stretch = TextureRect.StretchMode.KeepCentered};
var targetInfoContainer = new VBoxContainer() {SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.85f};
_targetName = new RichTextLabel() {SizeFlagsVertical = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.1f};
_targetDescription = new RichTextLabel() {SizeFlagsVertical = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.9f};
hSplitContainer.AddChild(guide);
Contents.AddChild(hSplitContainer);
_stepList = new ItemList() {SizeFlagsVertical = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.75f, SelectMode = ItemList.ItemListSelectMode.None};
BuildButton.OnToggled += OnBuildToggled;
EraseButton.OnToggled += OnEraseToggled;
SearchBar.OnTextChanged += OnTextEntered;
RecipeList.OnItemSelected += OnItemSelected;
var buttonContainer = new VBoxContainer() {SizeFlagsVertical = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.1f};
_buildButton = new Button() {Disabled = true, ToggleMode = true, Text = Loc.GetString("Place construction ghost"), SizeFlagsVertical = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.5f};
PopulatePrototypeList();
PopulateTree();
var eraseContainer = new HBoxContainer() {SizeFlagsVertical = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.5f};
_eraseButton = new Button() {Text = Loc.GetString("Eraser Mode"), ToggleMode = true, SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.7f};
var clearButton = new Button() {Text = Loc.GetString("Clear All"), SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 0.3f};
recipeContainer.AddChild(searchContainer);
recipeContainer.AddChild(_recipes);
searchContainer.AddChild(_searchBar);
searchContainer.AddChild(_category);
targetInfoContainer.AddChild(_targetName);
targetInfoContainer.AddChild(_targetDescription);
targetContainer.AddChild(_targetTexture);
targetContainer.AddChild(targetInfoContainer);
stepsContainer.AddChild(targetContainer);
stepsContainer.AddChild(_stepList);
eraseContainer.AddChild(_eraseButton);
eraseContainer.AddChild(clearButton);
buttonContainer.AddChild(_buildButton);
buttonContainer.AddChild(eraseContainer);
stepsContainer.AddChild(buttonContainer);
hbox.AddChild(recipeContainer);
hbox.AddChild(spacer);
hbox.AddChild(stepsContainer);
Contents.AddChild(hbox);
_recipes.OnItemSelected += RecipeSelected;
_recipes.OnItemDeselected += RecipeDeselected;
_searchBar.OnTextChanged += SearchTextChanged;
_category.OnItemSelected += CategorySelected;
_buildButton.OnToggled += BuildButtonToggled;
clearButton.OnPressed += ClearAllButtonPressed;
_eraseButton.OnToggled += EraseButtonToggled;
PopulateCategories();
PopulateAll();
}
private void PlacementChanged(object? sender, EventArgs e)
{
_buildButton.Pressed = false;
_eraseButton.Pressed = false;
}
private void PopulateAll()
{
foreach (var recipe in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
_recipes.Add(GetItem(recipe, _recipes));
}
}
private static ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
{
return new ItemList.Item(itemList)
{
Metadata = recipe,
Text = recipe.Name,
Icon = recipe.Icon.Frame0(),
TooltipEnabled = true,
TooltipText = recipe.Description,
};
}
private void PopulateBy(string search, string category)
{
_recipes.Clear();
foreach (var recipe in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
if (!string.IsNullOrEmpty(search))
{
if (!recipe.Name.ToLowerInvariant().Contains(search.Trim().ToLowerInvariant()))
continue;
}
if (!string.IsNullOrEmpty(category) && category != Loc.GetString("All"))
{
if (recipe.Category != category)
continue;
}
_recipes.Add(GetItem(recipe, _recipes));
}
}
private void PopulateCategories()
{
var uniqueCategories = new HashSet<string>();
// hard-coded to show all recipes
uniqueCategories.Add(Loc.GetString("All"));
foreach (var prototype in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
var category = Loc.GetString(prototype.Category);
if (!string.IsNullOrEmpty(category))
uniqueCategories.Add(category);
}
_category.Clear();
var array = uniqueCategories.ToArray();
Array.Sort(array);
for (var i = 0; i < array.Length; i++)
{
var category = array[i];
_category.AddItem(category, i);
}
_categories = array;
}
private void PopulateInfo(ConstructionPrototype prototype)
{
ClearInfo();
var isItem = prototype.Type == ConstructionType.Item;
_buildButton.Disabled = false;
_buildButton.Text = Loc.GetString(!isItem ? "Place construction ghost" : "Craft");
_targetName.SetMessage(prototype.Name);
_targetDescription.SetMessage(prototype.Description);
_targetTexture.Texture = prototype.Icon.Frame0();
if (!_prototypeManager.TryIndex(prototype.Graph, out ConstructionGraphPrototype graph))
return;
var startNode = graph.Nodes[prototype.StartNode];
var targetNode = graph.Nodes[prototype.TargetNode];
var path = graph.Path(startNode.Name, targetNode.Name);
var current = startNode;
var stepNumber = 1;
Texture? GetTextureForStep(ConstructionGraphStep step)
{
switch (step)
{
case MaterialConstructionGraphStep materialStep:
switch (materialStep.Material)
{
case StackType.Metal:
return _resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/metal.png");
case StackType.Glass:
return _resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/glass.png");
case StackType.Plasteel:
return _resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/plasteel.png");
case StackType.Phoron:
return _resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/phoron.png");
case StackType.Cable:
return _resourceCache.GetTexture("/Textures/Objects/Tools/cables.rsi/coil-30.png");
}
break;
case ToolConstructionGraphStep toolStep:
switch (toolStep.Tool)
{
case ToolQuality.Anchoring:
return _resourceCache.GetTexture("/Textures/Objects/Tools/wrench.rsi/icon.png");
case ToolQuality.Prying:
return _resourceCache.GetTexture("/Textures/Objects/Tools/crowbar.rsi/icon.png");
case ToolQuality.Screwing:
return _resourceCache.GetTexture("/Textures/Objects/Tools/screwdriver.rsi/screwdriver-map.png");
case ToolQuality.Cutting:
return _resourceCache.GetTexture("/Textures/Objects/Tools/wirecutters.rsi/cutters-map.png");
case ToolQuality.Welding:
return _resourceCache.GetTexture("/Textures/Objects/Tools/welder.rsi/welder.png");
case ToolQuality.Multitool:
return _resourceCache.GetTexture("/Textures/Objects/Tools/multitool.rsi/multitool.png");
}
break;
case ComponentConstructionGraphStep componentStep:
return componentStep.Icon?.Frame0();
case PrototypeConstructionGraphStep prototypeStep:
return prototypeStep.Icon?.Frame0();
case NestedConstructionGraphStep _:
return null;
}
return null;
}
foreach (var node in path)
{
var edge = current.GetEdge(node.Name);
var firstNode = current == startNode;
if (firstNode)
{
_stepList.AddItem(isItem
? Loc.GetString($"{stepNumber++}. To craft this item, you need:")
: Loc.GetString($"{stepNumber++}. To build this, first you need:"));
}
foreach (var step in edge.Steps)
{
var icon = GetTextureForStep(step);
switch (step)
{
case MaterialConstructionGraphStep materialStep:
_stepList.AddItem(
!firstNode
? Loc.GetString(
"{0}. Add {1}x {2}.", stepNumber++, materialStep.Amount, materialStep.Material)
: Loc.GetString(" {0}x {1}", materialStep.Amount, materialStep.Material), icon);
break;
case ToolConstructionGraphStep toolStep:
_stepList.AddItem(Loc.GetString("{0}. Use a {1}.", stepNumber++, toolStep.Tool.GetToolName()), icon);
break;
case PrototypeConstructionGraphStep prototypeStep:
_stepList.AddItem(Loc.GetString("{0}. Add {1}.", stepNumber++, prototypeStep.Name), icon);
break;
case ComponentConstructionGraphStep componentStep:
_stepList.AddItem(Loc.GetString("{0}. Add {1}.", stepNumber++, componentStep.Name), icon);
break;
case NestedConstructionGraphStep nestedStep:
var parallelNumber = 1;
_stepList.AddItem(Loc.GetString("{0}. In parallel...", stepNumber++));
foreach (var steps in nestedStep.Steps)
{
var subStepNumber = 1;
foreach (var subStep in steps)
{
icon = GetTextureForStep(subStep);
switch (subStep)
{
case MaterialConstructionGraphStep materialStep:
if (!isItem)
_stepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}x {4}.", stepNumber, parallelNumber, subStepNumber++, materialStep.Amount, materialStep.Material), icon);
break;
case ToolConstructionGraphStep toolStep:
_stepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Use a {3}.", stepNumber, parallelNumber, subStepNumber++, toolStep.Tool.GetToolName()), icon);
break;
case PrototypeConstructionGraphStep prototypeStep:
_stepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}.", stepNumber, parallelNumber, subStepNumber++, prototypeStep.Name), icon);
break;
case ComponentConstructionGraphStep componentStep:
_stepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}.", stepNumber, parallelNumber, subStepNumber++, componentStep.Name), icon);
break;
}
}
parallelNumber++;
}
break;
}
}
current = node;
}
}
private void ClearInfo()
{
_buildButton.Disabled = true;
_targetName.SetMessage(string.Empty);
_targetDescription.SetMessage(string.Empty);
_targetTexture.Texture = null;
_stepList.Clear();
}
private void RecipeSelected(ItemList.ItemListSelectedEventArgs obj)
{
_selected = (ConstructionPrototype) obj.ItemList[obj.ItemIndex].Metadata!;
PopulateInfo(_selected);
}
private void RecipeDeselected(ItemList.ItemListDeselectedEventArgs obj)
{
_selected = null;
ClearInfo();
}
private void CategorySelected(OptionButton.ItemSelectedEventArgs obj)
{
_category.SelectId(obj.Id);
PopulateBy(_searchBar.Text, _categories[obj.Id]);
}
private void SearchTextChanged(LineEdit.LineEditEventArgs obj)
{
PopulateBy(_searchBar.Text, _categories[_category.SelectedId]);
}
private void BuildButtonToggled(BaseButton.ButtonToggledEventArgs args)
{
if (args.Pressed)
{
if (_selected == null) return;
var constructSystem = EntitySystem.Get<ConstructionSystem>();
if (_selected.Type == ConstructionType.Item)
{
constructSystem.TryStartItemConstruction(_selected.ID);
_buildButton.Pressed = false;
return;
}
_placementManager.BeginPlacing(new PlacementInformation()
{
IsTile = false,
PlacementOption = _selected.PlacementMode,
}, new ConstructionPlacementHijack(constructSystem, _selected));
}
else
{
_placementManager.Clear();
}
_buildButton.Pressed = args.Pressed;
}
private void EraseButtonToggled(BaseButton.ButtonToggledEventArgs args)
{
if (args.Pressed) _placementManager.Clear();
_placementManager.ToggleEraserHijacked(new ConstructionPlacementHijack(_systemManager.GetEntitySystem<ConstructionSystem>(), null));
_eraseButton.Pressed = args.Pressed;
}
private void ClearAllButtonPressed(BaseButton.ButtonEventArgs obj)
{
var constructionSystem = EntitySystem.Get<ConstructionSystem>();
constructionSystem.ClearAllGhosts();
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
Placement.PlacementChanged -= OnPlacementChanged;
}
}
private void OnItemSelected()
{
var prototype = (ConstructionPrototype) RecipeList.Selected.Metadata;
if (prototype == null)
{
InfoLabel.Text = "";
InfoIcon.Texture = null;
StepList.Clear();
BuildButton.Disabled = true;
}
else
{
BuildButton.Disabled = false;
InfoLabel.Text = prototype.Description;
InfoIcon.Texture = prototype.Icon.Frame0();
StepList.Clear();
foreach (var forward in prototype.Stages.Select(a => a.Forward))
{
if (forward == null)
{
continue;
}
Texture icon;
string text;
switch (forward)
{
case ConstructionStepMaterial mat:
switch (mat.Material)
{
case ConstructionStepMaterial.MaterialType.Metal:
icon = _resourceCache.GetResource<TextureResource>(
"/Textures/Objects/Materials/sheet_metal.png");
text = $"Metal x{mat.Amount}";
break;
case ConstructionStepMaterial.MaterialType.Glass:
icon = _resourceCache.GetResource<TextureResource>(
"/Textures/Objects/Materials/sheet_glass.png");
text = $"Glass x{mat.Amount}";
break;
case ConstructionStepMaterial.MaterialType.Cable:
icon = _resourceCache.GetResource<TextureResource>(
"/Textures/Objects/Tools/cable_coil.png");
text = $"Cable Coil x{mat.Amount}";
break;
default:
throw new NotImplementedException();
}
break;
case ConstructionStepTool tool:
switch (tool.ToolQuality)
{
case ToolQuality.Anchoring:
icon = _resourceCache.GetResource<TextureResource>("/Textures/Objects/Tools/wrench.rsi/icon.png");
text = "Wrench";
break;
case ToolQuality.Prying:
icon = _resourceCache.GetResource<TextureResource>("/Textures/Objects/Tools/crowbar.rsi/icon.png");
text = "Crowbar";
break;
case ToolQuality.Screwing:
icon = _resourceCache.GetResource<TextureResource>(
"/Textures/Objects/Tools/screwdriver.rsi/screwdriver-map.png");
text = "Screwdriver";
break;
case ToolQuality.Welding:
icon = _resourceCache.GetResource<RSIResource>("/Textures/Objects/tools.rsi")
.RSI["welder"].Frame0;
text = $"Welding tool ({tool.Amount} fuel)";
break;
case ToolQuality.Cutting:
icon = _resourceCache.GetResource<TextureResource>(
"/Textures/Objects/Tools/wirecutters.rsi/cutters-map.png");
text = "Wirecutters";
break;
default:
throw new NotImplementedException();
}
break;
default:
throw new NotImplementedException();
}
StepList.AddItem(text, icon, false);
}
}
}
private void OnTextEntered(LineEdit.LineEditEventArgs args)
{
var str = args.Text;
PopulateTree(string.IsNullOrWhiteSpace(str) ? null : str.ToLowerInvariant());
}
private void OnBuildToggled(BaseButton.ButtonToggledEventArgs args)
{
if (args.Pressed)
{
var prototype = (ConstructionPrototype) RecipeList.Selected.Metadata;
if (prototype == null)
{
return;
}
if (prototype.Type == ConstructionType.Item)
{
var constructSystem = _systemManager.GetEntitySystem<ConstructionSystem>();
constructSystem.TryStartItemConstruction(prototype.ID);
BuildButton.Pressed = false;
return;
}
Placement.BeginHijackedPlacing(
new PlacementInformation
{
IsTile = false,
PlacementOption = prototype.PlacementMode
},
new ConstructionPlacementHijack(_systemManager.GetEntitySystem<ConstructionSystem>(), prototype));
}
else
{
Placement.Clear();
}
BuildButton.Pressed = args.Pressed;
}
private void OnEraseToggled(BaseButton.ButtonToggledEventArgs args)
{
if (args.Pressed) Placement.Clear();
Placement.ToggleEraserHijacked(new ConstructionPlacementHijack(_systemManager.GetEntitySystem<ConstructionSystem>(), null));
EraseButton.Pressed = args.Pressed;
}
private void OnPlacementChanged(object sender, EventArgs e)
{
BuildButton.Pressed = false;
EraseButton.Pressed = false;
}
private void PopulatePrototypeList()
{
RootCategory = new CategoryNode("", null);
var count = 1;
foreach (var prototype in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
var currentNode = RootCategory;
foreach (var category in prototype.CategorySegments)
{
if (!currentNode.ChildCategories.TryGetValue(category, out var subNode))
{
count++;
subNode = new CategoryNode(category, currentNode);
currentNode.ChildCategories.Add(category, subNode);
}
currentNode = subNode;
}
currentNode.Prototypes.Add(prototype);
}
// Do a pass to sort the prototype lists and flatten the hierarchy.
void Recurse(CategoryNode node)
{
// I give up we're using recursion to flatten this.
// There probably IS a way to do it.
// I'm too stupid to think of what that way is.
foreach (var child in node.ChildCategories.Values)
{
Recurse(child);
}
node.Prototypes.Sort(ComparePrototype);
FlattenedCategories.Add(node);
node.FlattenedIndex = FlattenedCategories.Count - 1;
}
FlattenedCategories = new List<CategoryNode>(count);
Recurse(RootCategory);
}
private void PopulateTree(string searchTerm = null)
{
RecipeList.Clear();
var categoryItems = new Tree.Item[FlattenedCategories.Count];
categoryItems[RootCategory.FlattenedIndex] = RecipeList.CreateItem();
// Yay more recursion.
Tree.Item ItemForNode(CategoryNode node)
{
if (categoryItems[node.FlattenedIndex] != null)
{
return categoryItems[node.FlattenedIndex];
}
var item = RecipeList.CreateItem(ItemForNode(node.Parent));
item.Text = node.Name;
item.Selectable = false;
categoryItems[node.FlattenedIndex] = item;
return item;
}
foreach (var node in FlattenedCategories)
{
foreach (var prototype in node.Prototypes)
{
if (searchTerm != null)
{
var found = false;
// TODO: don't run ToLowerInvariant() constantly.
if (prototype.Name.ToLowerInvariant().IndexOf(searchTerm, StringComparison.Ordinal) != -1)
{
found = true;
}
else
{
foreach (var keyw in prototype.Keywords.Concat(prototype.CategorySegments))
{
// TODO: don't run ToLowerInvariant() constantly.
if (keyw.ToLowerInvariant().IndexOf(searchTerm, StringComparison.Ordinal) != -1)
{
found = true;
break;
}
}
}
if (!found)
{
continue;
}
}
var subItem = RecipeList.CreateItem(ItemForNode(node));
subItem.Text = prototype.Name;
subItem.Metadata = prototype;
}
}
}
private static int ComparePrototype(ConstructionPrototype x, ConstructionPrototype y)
{
return string.Compare(x.Name, y.Name, StringComparison.Ordinal);
}
private class CategoryNode
{
public readonly string Name;
public readonly CategoryNode Parent;
public readonly SortedDictionary<string, CategoryNode>
ChildCategories = new SortedDictionary<string, CategoryNode>();
public readonly List<ConstructionPrototype> Prototypes = new List<ConstructionPrototype>();
public int FlattenedIndex = -1;
public CategoryNode(string name, CategoryNode parent)
{
Name = name;
Parent = parent;
_placementManager.PlacementChanged -= PlacementChanged;
}
}
}

View File

@@ -1,8 +1,9 @@
using Content.Shared.Construction;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -11,6 +12,8 @@ namespace Content.Client.GameObjects.Components.Construction
[RegisterComponent]
public class ConstructionGhostComponent : Component, IExamine
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override string Name => "ConstructionGhost";
[ViewVariables] public ConstructionPrototype Prototype { get; set; }
@@ -18,8 +21,13 @@ namespace Content.Client.GameObjects.Components.Construction
void IExamine.Examine(FormattedMessage message, bool inDetailsRange)
{
message.AddText(Loc.GetString("Building: {0}\n", Prototype.Name));
EntitySystem.Get<SharedConstructionSystem>().DoExamine(message, Prototype, 0, inDetailsRange);
message.AddMarkup(Loc.GetString("Building: [color=cyan]{0}[/color]\n", Prototype.Name));
if (!_prototypeManager.TryIndex(Prototype.Graph, out ConstructionGraphPrototype graph)) return;
var startNode = graph.Nodes[Prototype.StartNode];
var path = graph.Path(Prototype.StartNode, Prototype.TargetNode);
var edge = startNode.GetEdge(path[0].Name);
edge.Steps[0].DoExamine(message, inDetailsRange);
}
}
}

View File

@@ -1,4 +1,5 @@
using Content.Client.GameObjects.Components.IconSmoothing;
using Content.Shared.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
@@ -35,6 +36,7 @@ namespace Content.Client.GameObjects.Components
Sprite.LayerSetDirOffset(ReinforcedCornerLayers.NW, DirectionOffset.Flip);
Sprite.LayerMapSet(ReinforcedCornerLayers.SW, Sprite.AddLayerState(state0));
Sprite.LayerSetDirOffset(ReinforcedCornerLayers.SW, DirectionOffset.Clockwise);
Sprite.LayerMapSet(ReinforcedWallVisualLayers.Deconstruction, Sprite.AddBlankLayer());
}
internal override void CalculateNewSprite()

View File

@@ -0,0 +1,42 @@
using Content.Shared.GameObjects.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
namespace Content.Client.GameObjects.Components
{
[UsedImplicitly]
public class ReinforcedWallVisualizer : AppearanceVisualizer
{
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (component.TryGetData(ReinforcedWallVisuals.DeconstructionStage, out int stage))
{
SetDeconstructionStage(component, stage);
}
}
public void SetDeconstructionStage(AppearanceComponent component, int stage)
{
var entity = component.Owner;
if (!entity.TryGetComponent(out ISpriteComponent sprite)) return;
if (stage < 0)
{
sprite.LayerSetVisible(ReinforcedWallVisualLayers.Deconstruction, false);
return;
}
sprite.LayerSetVisible(ReinforcedWallVisualLayers.Deconstruction, true);
sprite.LayerSetState(ReinforcedWallVisualLayers.Deconstruction, $"reinf_construct-{stage}");
}
}
public enum ReinforcedWallVisualLayers
{
Deconstruction,
}
}

View File

@@ -11,6 +11,7 @@ using Robust.Shared.ViewVariables;
namespace Content.Client.GameObjects.Components
{
[RegisterComponent]
[ComponentReference(typeof(SharedStackComponent))]
public class StackComponent : SharedStackComponent, IItemStatus
{
[ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;

View File

@@ -1,4 +1,5 @@
using Content.Client.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
@@ -9,10 +10,9 @@ using static Content.Client.GameObjects.Components.IconSmoothing.IconSmoothCompo
namespace Content.Client.GameObjects.Components
{
[RegisterComponent]
public sealed class WindowComponent : Component
[ComponentReference(typeof(SharedWindowComponent))]
public sealed class WindowComponent : SharedWindowComponent
{
public override string Name => "Window";
private string _stateBase;
private ISpriteComponent _sprite;
private SnapGridComponent _snapGrid;

View File

@@ -5,6 +5,7 @@ using Content.Client.UserInterface;
using Content.Shared.Construction;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Input;
using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.GameObjects.EntitySystems;
@@ -152,11 +153,20 @@ namespace Content.Client.GameObjects.EntitySystems
/// </summary>
public void SpawnGhost(ConstructionPrototype prototype, EntityCoordinates loc, Direction dir)
{
if (GhostPresent(loc))
var user = _playerManager.LocalPlayer?.ControlledEntity;
// This InRangeUnobstructed should probably be replaced with "is there something blocking us in that tile?"
if (user == null || GhostPresent(loc) || !user.InRangeUnobstructed(loc, 20f, ignoreInsideBlocker:prototype.CanBuildInImpassable))
{
return;
}
foreach (var condition in prototype.Conditions)
{
if (!condition.Condition(user, loc, dir))
return;
}
var ghost = _entityManager.SpawnEntity("constructionghost", loc);
var comp = ghost.GetComponent<ConstructionGhostComponent>();
comp.Prototype = prototype;
@@ -214,5 +224,18 @@ namespace Content.Client.GameObjects.EntitySystems
_ghosts.Remove(ghostId);
}
}
/// <summary>
/// Removes all construction ghosts.
/// </summary>
public void ClearAllGhosts()
{
foreach (var (_, ghost) in _ghosts)
{
ghost.Owner.Delete();
}
_ghosts.Clear();
}
}
}

View File

@@ -131,7 +131,12 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter
if (doAfter.BreakOnTargetMove)
{
var targetEntity = _entityManager.GetEntity(doAfter.TargetUid);
if (!_entityManager.TryGetEntity(doAfter.TargetUid, out var targetEntity))
{
// Cancel if the target entity doesn't exist.
doAfterComponent.Cancel(id, currentTime);
continue;
}
if (targetEntity.Transform.Coordinates != doAfter.TargetGrid)
{

View File

@@ -179,6 +179,8 @@
"Butcherable",
"Rehydratable",
"Headset",
"ComputerBoard",
"BreakableConstruction",
};
}
}

View File

@@ -819,9 +819,7 @@ namespace Content.Server.Atmos
var tileRef = GridIndices.GetTileRef(GridIndex);
if (tileRef == null) return;
foreach (var entity in tileRef?.GetEntitiesInTileFast(_gridTileLookupSystem))
foreach (var entity in tileRef.GetEntitiesInTileFast(_gridTileLookupSystem))
{
foreach (var fireAct in entity.GetAllComponents<IFireAct>())
{

View File

@@ -0,0 +1,68 @@
#nullable enable
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Construction;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Server.GameObjects.Components.Container;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
namespace Content.Server.Construction.Completions
{
[UsedImplicitly]
public class BuildComputer : IGraphAction
{
public string Container { get; private set; } = string.Empty;
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Container, "container", string.Empty);
}
public async Task PerformAction(IEntity entity, IEntity? user)
{
if (!entity.TryGetComponent(out ContainerManagerComponent? containerManager))
{
Logger.Warning($"Computer entity {entity} did not have a container manager! Aborting build computer action.");
return;
}
if (!containerManager.TryGetContainer(Container, out var container))
{
Logger.Warning($"Computer entity {entity} did not have the specified '{Container}' container! Aborting build computer action.");
return;
}
if (container.ContainedEntities.Count != 1)
{
Logger.Warning($"Computer entity {entity} did not have exactly one item in the specified '{Container}' container! Aborting build computer action.");
}
var board = container.ContainedEntities[0];
if (!board.TryGetComponent(out ComputerBoardComponent? boardComponent))
{
Logger.Warning($"Computer entity {entity} had an invalid entity in container \"{Container}\"! Aborting build computer action.");
return;
}
var entityManager = entity.EntityManager;
container.Remove(board);
var computer = entityManager.SpawnEntity(boardComponent.Prototype, entity.Transform.Coordinates);
computer.Transform.LocalRotation = entity.Transform.LocalRotation;
var computerContainer = ContainerManagerComponent.Ensure<Container>(Container, computer);
computerContainer.Insert(board);
if (computer.TryGetComponent(out ConstructionComponent? construction))
{
// We only add this container. If some construction needs to take other containers into account, fix this.
construction.AddContainer(Container);
}
entity.Delete();
}
}
}

View File

@@ -0,0 +1,24 @@
#nullable enable
using System.Threading.Tasks;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Server.Construction.Completions
{
[UsedImplicitly]
public class DeleteEntity : IGraphAction
{
public void ExposeData(ObjectSerializer serializer)
{
}
public async Task PerformAction(IEntity entity, IEntity? user)
{
if (entity.Deleted) return;
entity.Delete();
}
}
}

View File

@@ -0,0 +1,37 @@
#nullable enable
using System.Linq;
using System.Threading.Tasks;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Server.GameObjects.Components.Container;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Server.Construction.Completions
{
[UsedImplicitly]
public class EmptyContainer : IGraphAction
{
public string Container { get; private set; } = string.Empty;
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Container, "container", string.Empty);
}
public async Task PerformAction(IEntity entity, IEntity? user)
{
if (entity.Deleted) return;
if (!entity.TryGetComponent(out ContainerManagerComponent? containerManager) ||
!containerManager.TryGetContainer(Container, out var container)) return;
foreach (var ent in container.ContainedEntities.ToArray())
{
if (ent == null || ent.Deleted) continue;
container.ForceRemove(ent);
ent.Transform.Coordinates = entity.Transform.Coordinates;
}
}
}
}

View File

@@ -0,0 +1,39 @@
#nullable enable
using System.Threading.Tasks;
using Content.Shared.Audio;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Server.Construction.Completions
{
[UsedImplicitly]
public class PlaySound : IGraphAction
{
public string SoundCollection { get; private set; } = string.Empty;
public string Sound { get; private set; } = string.Empty;
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Sound, "sound", string.Empty);
serializer.DataField(this, x => x.SoundCollection, "soundCollection", string.Empty);
}
public async Task PerformAction(IEntity entity, IEntity? user)
{
var sound = GetSound();
if (string.IsNullOrEmpty(sound)) return;
EntitySystem.Get<AudioSystem>().PlayFromEntity(sound, entity, AudioHelpers.WithVariation(0.125f));
}
private string GetSound()
{
return !string.IsNullOrEmpty(SoundCollection) ? AudioHelpers.GetRandomFileFromSoundCollection(SoundCollection) : Sound;
}
}
}

View File

@@ -0,0 +1,33 @@
#nullable enable
using System.Threading.Tasks;
using Content.Shared.Construction;
using Content.Shared.Interfaces;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Server.Construction.Completions
{
[UsedImplicitly]
public class PopupUser : IGraphAction
{
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Text, "text", string.Empty);
serializer.DataField(this, x => x.Cursor, "cursor", false);
}
public bool Cursor { get; private set; } = false;
public string Text { get; private set; } = string.Empty;
public async Task PerformAction(IEntity entity, IEntity? user)
{
if (user == null) return;
if(Cursor)
user.PopupMessageCursor(Text);
else
entity.PopupMessage(user, Text);
}
}
}

View File

@@ -0,0 +1,28 @@
#nullable enable
using System.Threading.Tasks;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Server.Construction.Completions
{
[UsedImplicitly]
public class SetAnchor : IGraphAction
{
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Value, "value", true);
}
public bool Value { get; private set; } = true;
public async Task PerformAction(IEntity entity, IEntity? user)
{
if (!entity.TryGetComponent(out CollidableComponent? collidable)) return;
collidable.Anchored = Value;
}
}
}

View File

@@ -0,0 +1,29 @@
#nullable enable
using System.Threading.Tasks;
using Content.Server.Utility;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Components.Transform;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Server.Construction.Completions
{
[UsedImplicitly]
public class SnapToGrid : IGraphAction
{
public SnapGridOffset Offset { get; private set; } = SnapGridOffset.Center;
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Offset, "offset", SnapGridOffset.Center);
}
public async Task PerformAction(IEntity entity, IEntity? user)
{
if (entity.Deleted) return;
entity.SnapToGrid(Offset);
}
}
}

View File

@@ -0,0 +1,36 @@
#nullable enable
using System.Threading.Tasks;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
namespace Content.Server.Construction.Completions
{
[UsedImplicitly]
public class SpawnPrototype : IGraphAction
{
public string Prototype { get; private set; } = string.Empty;
public int Amount { get; private set; } = 1;
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Prototype, "prototype", string.Empty);
serializer.DataField(this, x => x.Amount, "amount", 1);
}
public async Task PerformAction(IEntity entity, IEntity? user)
{
if (entity.Deleted || string.IsNullOrEmpty(Prototype)) return;
var entityManager = IoCManager.Resolve<IEntityManager>();
var coordinates = entity.Transform.Coordinates;
for (var i = 0; i < Amount; i++)
{
entityManager.SpawnEntity(Prototype, coordinates);
}
}
}
}

View File

@@ -0,0 +1,33 @@
#nullable enable
using System.Threading.Tasks;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Server.Construction.Completions
{
[UsedImplicitly]
public class SpriteChange : IGraphAction
{
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.SpriteSpecifier, "specifier", SpriteSpecifier.Invalid);
serializer.DataField(this, x => x.Layer, "layer", 0);
}
public int Layer { get; private set; } = 0;
public SpriteSpecifier? SpriteSpecifier { get; private set; } = SpriteSpecifier.Invalid;
public async Task PerformAction(IEntity entity, IEntity? user)
{
if (entity.Deleted || SpriteSpecifier == null || SpriteSpecifier == SpriteSpecifier.Invalid) return;
if (!entity.TryGetComponent(out SpriteComponent? sprite)) return;
sprite.LayerSetSprite(Layer, SpriteSpecifier);
}
}
}

View File

@@ -0,0 +1,39 @@
#nullable enable
using System.Threading.Tasks;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Server.Construction.Completions
{
[UsedImplicitly]
public class SpriteStateChange : IGraphAction
{
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.State, "state", string.Empty);
serializer.DataField(this, x => x.Layer, "layer", 0);
}
public int Layer { get; private set; } = 0;
public string? State { get; private set; } = string.Empty;
public async Task StepCompleted(IEntity entity, IEntity user)
{
await PerformAction(entity, user);
}
public async Task PerformAction(IEntity entity, IEntity? user)
{
if (entity.Deleted || string.IsNullOrEmpty(State)) return;
if (!entity.TryGetComponent(out SpriteComponent? sprite)) return;
sprite.LayerSetState(Layer, State);
}
}
}

View File

@@ -0,0 +1,49 @@
#nullable enable
using System.Threading.Tasks;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
namespace Content.Server.Construction.Completions
{
[UsedImplicitly]
public class VisualizerDataInt : IGraphAction
{
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
public VisualizerDataInt()
{
IoCManager.InjectDependencies(this);
}
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Key, "key", string.Empty);
serializer.DataField(this, x => x.Data, "data", 0);
}
public string? Key { get; private set; } = string.Empty;
public int Data { get; private set; } = 0;
public async Task PerformAction(IEntity entity, IEntity? user)
{
if (string.IsNullOrEmpty(Key)) return;
if (entity.TryGetComponent(out AppearanceComponent? appearance))
{
if(_reflectionManager.TryParseEnumReference(Key, out var @enum))
{
appearance.SetData(@enum, Data);
}
else
{
appearance.SetData(Key, Data);
}
}
}
}
}

View File

@@ -0,0 +1,61 @@
using System.Threading.Tasks;
using Content.Shared.Construction;
using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
namespace Content.Server.Construction.Conditions
{
/// <summary>
/// Makes the condition fail if any entities on a tile have (or not) a component.
/// </summary>
[UsedImplicitly]
public class ComponentInTile : IEdgeCondition
{
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
public ComponentInTile()
{
IoCManager.InjectDependencies(this);
}
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Component, "component", string.Empty);
serializer.DataField(this, x => x.HasEntity, "hasEntity", true);
}
/// <summary>
/// If true, any entity on the tile must have the component.
/// If false, no entity on the tile must have the component.
/// </summary>
public bool HasEntity { get; private set; }
/// <summary>
/// The component name in question.
/// </summary>
public string Component { get; private set; }
public async Task<bool> Condition(IEntity entity)
{
if (string.IsNullOrEmpty(Component)) return false;
var type = _componentFactory.GetRegistration(Component).Type;
var indices = entity.Transform.Coordinates.ToMapIndices(entity.EntityManager, _mapManager);
var entities = indices.GetEntitiesInTile(entity.Transform.GridID, true, entity.EntityManager);
foreach (var ent in entities)
{
if (ent.HasComponent(type))
return HasEntity;
}
return !HasEntity;
}
}
}

View File

@@ -0,0 +1,41 @@
#nullable enable
using System.Threading.Tasks;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Server.GameObjects.Components.Container;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Server.Construction.Conditions
{
[UsedImplicitly]
public class ContainerEmpty : IEdgeCondition
{
public string Container { get; private set; } = string.Empty;
public string Text { get; private set; } = string.Empty;
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Container, "container", string.Empty);
serializer.DataField(this, x => x.Text, "text", string.Empty);
}
public async Task<bool> Condition(IEntity entity)
{
if (!entity.TryGetComponent(out ContainerManagerComponent? containerManager) ||
!containerManager.TryGetContainer(Container, out var container)) return true;
return container.ContainedEntities.Count == 0;
}
public void DoExamine(IEntity entity, FormattedMessage message, bool inDetailsRange)
{
if (!entity.TryGetComponent(out ContainerManagerComponent? containerManager) ||
!containerManager.TryGetContainer(Container, out var container)) return;
if (container.ContainedEntities.Count != 0)
message.AddMarkup(Text);
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Threading.Tasks;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Server.Construction.Conditions
{
[UsedImplicitly]
public class EntityAnchored : IEdgeCondition
{
public bool Anchored { get; private set; }
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Anchored, "anchored", true);
}
public async Task<bool> Condition(IEntity entity)
{
if (!entity.TryGetComponent(out ICollidableComponent collidable)) return false;
return collidable.Anchored == Anchored;
}
public void DoExamine(IEntity entity, FormattedMessage message, bool inDetailsRange)
{
if (!entity.TryGetComponent(out ICollidableComponent collidable)) return;
if(Anchored && !collidable.Anchored)
message.AddMarkup("First, anchor it.\n");
if(!Anchored && collidable.Anchored)
message.AddMarkup("First, unanchor it.\n");
}
}
}

View File

@@ -0,0 +1,40 @@
using System.Threading.Tasks;
using Content.Server.GameObjects.Components;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Server.Construction.Conditions
{
[UsedImplicitly]
public class WirePanel : IEdgeCondition
{
public bool Open { get; private set; }
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Open, "open", true);
}
public async Task<bool> Condition(IEntity entity)
{
if (!entity.TryGetComponent(out WiresComponent wires)) return false;
return wires.IsPanelOpen == Open;
}
public void DoExamine(IEntity entity, FormattedMessage message, bool inDetailsRange)
{
if (!entity.TryGetComponent(out WiresComponent wires)) return;
if(Open && !wires.IsPanelOpen)
message.AddMarkup(Loc.GetString("First, open the maintenance panel.\n"));
if(!Open && wires.IsPanelOpen)
message.AddMarkup(Loc.GetString("First, close the maintenance panel.\n"));
}
}
}

View File

@@ -26,7 +26,7 @@ namespace Content.Server.GameObjects.Components.Access
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class IdCardConsoleComponent : SharedIdCardConsoleComponent, IActivate, IInteractUsing
public class IdCardConsoleComponent : SharedIdCardConsoleComponent, IActivate, IInteractUsing, IBreakAct
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -307,6 +307,15 @@ namespace Content.Server.GameObjects.Components.Access
}
}
public void OnBreak(BreakageEventArgs eventArgs)
{
var privileged = _privilegedIdContainer.ContainedEntity;
if (privileged != null)
_privilegedIdContainer.Remove(privileged);
var target = _targetIdContainer.ContainedEntity;
if (target != null)
_targetIdContainer.Remove(target);
}
}
}

View File

@@ -7,6 +7,7 @@ using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components
@@ -16,6 +17,15 @@ namespace Content.Server.GameObjects.Components
{
public override string Name => "Anchorable";
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Tool, "tool", ToolQuality.Anchoring);
}
[ViewVariables]
public ToolQuality Tool { get; private set; } = ToolQuality.Anchoring;
[ViewVariables]
int IInteractUsing.Priority => 1;
@@ -37,7 +47,7 @@ namespace Content.Server.GameObjects.Components
{
if (utilizing == null ||
!utilizing.TryGetComponent(out ToolComponent? tool) ||
!(await tool.UseTool(user, Owner, 0.5f, ToolQuality.Anchoring)))
!(await tool.UseTool(user, Owner, 0.5f, Tool)))
{
return false;
}

View File

@@ -73,7 +73,7 @@ namespace Content.Server.GameObjects.Components.Atmos
public override bool CanClose(IEntity user) => true;
public override bool CanOpen(IEntity user) => CanOpen();
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
public override async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!eventArgs.Using.TryGetComponent<ToolComponent>(out var tool))
return false;

View File

@@ -0,0 +1,22 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Construction
{
[RegisterComponent]
public class ComputerBoardComponent : Component
{
public override string Name => "ComputerBoard";
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Prototype, "prototype", string.Empty);
}
[ViewVariables]
public string Prototype { get; private set; }
}
}

View File

@@ -1,49 +1,545 @@
using Content.Shared.Construction;
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Stack;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Shared.Construction;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Construction
{
/// <summary>
/// Holds data about an entity that is in the process of being constructed or destructed.
/// </summary>
[RegisterComponent]
public class ConstructionComponent : Component, IExamine
public class ConstructionComponent : Component, IExamine, IInteractUsing
{
/// <inheritdoc />
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
public override string Name => "Construction";
/// <summary>
/// The current construction recipe being used to build this entity.
/// </summary>
[ViewVariables]
public ConstructionPrototype Prototype { get; set; }
private bool _handling = false;
private TaskCompletionSource<object>? _handlingTask = null;
private string _graphIdentifier = string.Empty;
private string _startingNodeIdentifier = string.Empty;
/// <summary>
/// The current stage of construction.
/// </summary>
[ViewVariables]
public int Stage { get; set; }
private HashSet<string> _containers = new HashSet<string>();
[ViewVariables]
private List<List<ConstructionGraphStep>>? _edgeNestedStepProgress = null;
private ConstructionGraphNode? _target = null;
[ViewVariables]
public ConstructionGraphPrototype GraphPrototype { get; private set; } = null!;
[ViewVariables]
public ConstructionGraphNode Node { get; private set; } = null!;
[ViewVariables]
public ConstructionGraphEdge? Edge { get; private set; } = null;
public IReadOnlyCollection<string> Containers => _containers;
[ViewVariables]
public ConstructionGraphNode? Target
{
get => _target;
set
{
ClearTarget();
_target = value;
UpdateTarget();
}
}
[ViewVariables]
public ConstructionGraphEdge? TargetNextEdge { get; private set; } = null;
[ViewVariables]
public Queue<ConstructionGraphNode>? TargetPathfinding { get; private set; } = null;
[ViewVariables]
public int EdgeStep { get; private set; } = 0;
/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataReadWriteFunction("prototype", null,
value => Prototype = value, () => Prototype);
serializer.DataField(ref _graphIdentifier, "graph", string.Empty);
serializer.DataField(ref _startingNodeIdentifier, "node", string.Empty);
}
serializer.DataReadWriteFunction("stage", 0,
value => Stage = value, () => Stage);
public void ClearTarget()
{
_target = null;
TargetNextEdge = null;
TargetPathfinding = null;
}
public void UpdateTarget()
{
// Can't pathfind without a target.
if (Target == null) return;
// If we're at our target, stop pathfinding.
if (Target == Node)
{
ClearTarget();
return;
}
// If we don't have the path, set it!
if (TargetPathfinding == null)
{
var path = GraphPrototype.Path(Node.Name, Target.Name);
if (path == null)
{
ClearTarget();
return;
}
TargetPathfinding = new Queue<ConstructionGraphNode>(path);
}
// Dequeue the pathfinding queue if the next is the node we're at.
if (TargetPathfinding.Peek() == Node)
TargetPathfinding.Dequeue();
// If we went the wrong way, we stop pathfinding.
if (Edge != null && TargetNextEdge != Edge)
{
ClearTarget();
return;
}
// Let's set the next target edge.
if (Edge == null && TargetNextEdge == null)
TargetNextEdge = Node.GetEdge(TargetPathfinding.Peek().Name);
}
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (_handling)
return true;
_handlingTask = new TaskCompletionSource<object>();
_handling = true;
bool result;
if (Edge == null)
result = await HandleNode(eventArgs);
else
result = await HandleEdge(eventArgs);
_handling = false;
_handlingTask.SetResult(null!);
return result;
}
private async Task<bool> HandleNode(InteractUsingEventArgs eventArgs)
{
EdgeStep = 0;
foreach (var edge in Node.Edges)
{
if(edge.Steps.Count == 0)
throw new InvalidDataException($"Edge to \"{edge.Target}\" in node \"{Node.Name}\" of graph \"{GraphPrototype.ID}\" doesn't have any steps!");
var firstStep = edge.Steps[0];
switch (firstStep)
{
case MaterialConstructionGraphStep _:
case ToolConstructionGraphStep _:
case PrototypeConstructionGraphStep _:
case ComponentConstructionGraphStep _:
if (await HandleStep(eventArgs, edge, firstStep))
{
if(edge.Steps.Count > 1)
Edge = edge;
return true;
}
break;
case NestedConstructionGraphStep nestedStep:
throw new IndexOutOfRangeException($"Nested construction step not supported as the first step in an edge! Graph: {GraphPrototype.ID} Node: {Node.Name} Edge: {edge.Target}");
}
}
return false;
}
private async Task<bool> HandleStep(InteractUsingEventArgs eventArgs, ConstructionGraphEdge? edge = null, ConstructionGraphStep? step = null, bool nested = false)
{
edge ??= Edge;
step ??= edge?.Steps[EdgeStep];
if (edge == null || step == null)
return false;
foreach (var condition in edge.Conditions)
{
if (!await condition.Condition(Owner)) return false;
}
var handled = false;
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
var doAfterArgs = new DoAfterEventArgs(eventArgs.User, step.DoAfter, default, eventArgs.Target)
{
BreakOnDamage = false,
BreakOnStun = true,
BreakOnTargetMove = true,
BreakOnUserMove = true,
NeedHand = true,
};
switch (step)
{
case ToolConstructionGraphStep toolStep:
// Gotta take welder fuel into consideration.
if (toolStep.Tool == ToolQuality.Welding)
{
if (eventArgs.Using.TryGetComponent(out WelderComponent? welder) &&
await welder.UseTool(eventArgs.User, Owner, step.DoAfter, toolStep.Tool, toolStep.Fuel))
{
handled = true;
}
break;
}
if (eventArgs.Using.TryGetComponent(out ToolComponent? tool) &&
await tool.UseTool(eventArgs.User, Owner, step.DoAfter, toolStep.Tool))
{
handled = true;
}
break;
// To prevent too much code duplication.
case EntityInsertConstructionGraphStep insertStep:
var valid = false;
var entityUsing = eventArgs.Using;
switch (insertStep)
{
case PrototypeConstructionGraphStep prototypeStep:
if (prototypeStep.EntityValid(eventArgs.Using)
&& await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Finished)
{
valid = true;
}
break;
case ComponentConstructionGraphStep componentStep:
if (componentStep.EntityValid(eventArgs.Using)
&& await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Finished)
{
valid = true;
}
break;
case MaterialConstructionGraphStep materialStep:
if (materialStep.EntityValid(eventArgs.Using, out var sharedStack)
&& await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Finished)
{
var stack = (StackComponent) sharedStack;
valid = stack.Split(materialStep.Amount, eventArgs.User.Transform.Coordinates, out entityUsing);
}
break;
}
if (!valid || entityUsing == null) break;
if(string.IsNullOrEmpty(insertStep.Store))
{
entityUsing.Delete();
}
else
{
_containers.Add(insertStep.Store);
var container = ContainerManagerComponent.Ensure<Container>(insertStep.Store, Owner);
container.Insert(entityUsing);
}
handled = true;
break;
case NestedConstructionGraphStep nestedStep:
if(_edgeNestedStepProgress == null)
_edgeNestedStepProgress = new List<List<ConstructionGraphStep>>(nestedStep.Steps);
foreach (var list in _edgeNestedStepProgress.ToArray())
{
if (list.Count == 0)
{
_edgeNestedStepProgress.Remove(list);
continue;
}
if (!await HandleStep(eventArgs, edge, list[0], true)) continue;
list.RemoveAt(0);
// We check again...
if (list.Count == 0)
_edgeNestedStepProgress.Remove(list);
}
if (_edgeNestedStepProgress.Count == 0)
handled = true;
break;
}
if (handled)
{
foreach (var completed in step.Completed)
{
await completed.PerformAction(Owner, eventArgs.User);
if (Owner.Deleted)
return false;
}
}
if (nested && handled) return true;
if (!handled) return false;
EdgeStep++;
if (edge.Steps.Count == EdgeStep)
{
await HandleCompletion(edge, eventArgs.User);
}
UpdateTarget();
return true;
}
private async Task<bool> HandleCompletion(ConstructionGraphEdge edge, IEntity user)
{
if (edge.Steps.Count != EdgeStep)
{
return false;
}
Edge = edge;
UpdateTarget();
TargetNextEdge = null;
Edge = null;
Node = GraphPrototype.Nodes[edge.Target];
// Perform node actions!
foreach (var action in Node.Actions)
{
await action.PerformAction(Owner, user);
if (Owner.Deleted)
return false;
}
if (Target == Node)
ClearTarget();
foreach (var completed in edge.Completed)
{
await completed.PerformAction(Owner, user);
if (Owner.Deleted) return true;
}
await HandleEntityChange(Node, user);
return true;
}
private async Task<bool> HandleEdge(InteractUsingEventArgs eventArgs)
{
if (Edge == null || EdgeStep >= Edge.Steps.Count) return false;
return await HandleStep(eventArgs, Edge, Edge.Steps[EdgeStep]);
}
private async Task<bool> HandleEntityChange(ConstructionGraphNode node, IEntity? user = null)
{
if (node.Entity == Owner.Prototype?.ID || string.IsNullOrEmpty(node.Entity)) return false;
var entity = _entityManager.SpawnEntity(node.Entity, Owner.Transform.Coordinates);
entity.Transform.LocalRotation = Owner.Transform.LocalRotation;
if (entity.TryGetComponent(out ConstructionComponent? construction))
{
if(construction.GraphPrototype != GraphPrototype)
throw new Exception($"New entity {node.Entity}'s graph {construction.GraphPrototype.ID} isn't the same as our graph {GraphPrototype.ID} on node {node.Name}!");
construction.Node = node;
construction.Target = Target;
construction._containers = new HashSet<string>(_containers);
}
if (Owner.TryGetComponent(out ContainerManagerComponent? containerComp))
{
foreach (var container in _containers)
{
var otherContainer = ContainerManagerComponent.Ensure<Container>(container, entity);
var ourContainer = containerComp.GetContainer(container);
foreach (var ent in ourContainer.ContainedEntities.ToArray())
{
ourContainer.ForceRemove(ent);
otherContainer.Insert(ent);
}
}
}
if (Owner.TryGetComponent(out CollidableComponent? collidable) &&
entity.TryGetComponent(out CollidableComponent? otherCollidable))
{
otherCollidable.Anchored = collidable.Anchored;
}
Owner.Delete();
foreach (var action in node.Actions)
{
await action.PerformAction(entity, user);
if (entity.Deleted)
return false;
}
return true;
}
public bool AddContainer(string id)
{
return _containers.Add(id);
}
public override void Initialize()
{
base.Initialize();
if (string.IsNullOrEmpty(_graphIdentifier))
{
Logger.Error($"Prototype {Owner.Prototype?.ID}'s construction component didn't have a graph identifier!");
return;
}
if (_prototypeManager.TryIndex(_graphIdentifier, out ConstructionGraphPrototype graph))
{
GraphPrototype = graph;
if (GraphPrototype.Nodes.TryGetValue(_startingNodeIdentifier, out var node))
{
Node = node;
foreach (var action in Node.Actions)
{
action.PerformAction(Owner, null);
if (Owner.Deleted)
return;
}
}
else
{
Logger.Error($"Couldn't find node {_startingNodeIdentifier} in graph {_graphIdentifier} in construction component!");
}
}
else
{
Logger.Error($"Couldn't find prototype {_graphIdentifier} in construction component!");
}
}
public async Task ChangeNode(string node)
{
var graphNode = GraphPrototype.Nodes[node];
if (_handling && _handlingTask?.Task != null)
await _handlingTask.Task;
Edge = null;
Node = graphNode;
foreach (var action in Node.Actions)
{
await action.PerformAction(Owner, null);
if (Owner.Deleted)
return;
}
await HandleEntityChange(graphNode);
}
void IExamine.Examine(FormattedMessage message, bool inDetailsRange)
{
EntitySystem.Get<SharedConstructionSystem>().DoExamine(message, Prototype, Stage, inDetailsRange);
if(Target != null)
message.AddMarkup(Loc.GetString("To create {0}...\n", Target.Name));
if (Edge == null && TargetNextEdge != null)
{
foreach (var condition in TargetNextEdge.Conditions)
{
condition.DoExamine(Owner, message, inDetailsRange);
}
TargetNextEdge.Steps[0].DoExamine(message, inDetailsRange);
return;
}
if (Edge != null)
{
foreach (var condition in Edge.Conditions)
{
condition.DoExamine(Owner, message, inDetailsRange);
}
}
if (_edgeNestedStepProgress == null)
{
if(EdgeStep < Edge?.Steps.Count)
Edge.Steps[EdgeStep].DoExamine(message, inDetailsRange);
return;
}
foreach (var list in _edgeNestedStepProgress)
{
if(list.Count == 0) continue;
list[0].DoExamine(message, inDetailsRange);
}
}
}
}

View File

@@ -0,0 +1,45 @@
#nullable enable
using System;
using Content.Server.GameObjects.Components.Construction;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Damage
{
[RegisterComponent]
[ComponentReference(typeof(IDamageableComponent))]
public class BreakableConstructionComponent : RuinableComponent
{
private ActSystem _actSystem = default!;
public override string Name => "BreakableConstruction";
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Node, "node", string.Empty);
}
public override void Initialize()
{
base.Initialize();
_actSystem = EntitySystem.Get<ActSystem>();
}
public string Node { get; private set; } = string.Empty;
protected override async void DestructionBehavior()
{
if (Owner.Deleted || !Owner.TryGetComponent(out ConstructionComponent? construction) || string.IsNullOrEmpty(Node)) return;
_actSystem.HandleBreakage(Owner);
await construction.ChangeNode(Node);
}
}
}

View File

@@ -26,7 +26,7 @@ namespace Content.Server.GameObjects.Components.Damage
/// <summary>
/// Entity spawned upon destruction.
/// </summary>
public string SpawnOnDestroy { get; set; }
public string SpawnOnDestroy { get; private set; }
void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
{
@@ -39,7 +39,7 @@ namespace Content.Server.GameObjects.Components.Damage
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, d => d.SpawnOnDestroy, "spawnondestroy", string.Empty);
serializer.DataField(this, d => d.SpawnOnDestroy, "spawnOnDestroy", string.Empty);
}
public override void Initialize()

View File

@@ -126,6 +126,17 @@ namespace Content.Server.GameObjects.Components.GUI
base.OnRemove();
}
public IEnumerable<IEntity> GetAllHeldItems()
{
foreach (var (_, container) in _slotContainers)
{
foreach (var entity in container.ContainedEntities)
{
yield return entity;
}
}
}
/// <summary>
/// Helper to get container name for specified slot on this component
/// </summary>

View File

@@ -4,7 +4,6 @@ using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.MachineLinking;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.Interfaces;
@@ -56,21 +55,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
}
}
public override void HandleMessage(ComponentMessage message, IComponent component)
{
base.HandleMessage(message, component);
switch (message)
{
case BeginDeconstructCompMsg msg:
if (!msg.BlockDeconstruct && !(_lightBulbContainer.ContainedEntity is null))
{
Owner.PopupMessage(msg.User, Loc.GetString("Remove the bulb."));
msg.BlockDeconstruct = true;
}
break;
}
}
// TODO CONSTRUCTION make this use a construction graph
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{

View File

@@ -73,14 +73,7 @@ namespace Content.Server.GameObjects.Components.Recycling
{
prototype = null;
var constructionSystem = EntitySystem.Get<ConstructionSystem>();
var entityId = entity.MetaData.EntityPrototype?.ID;
if (entityId == null ||
!constructionSystem.CraftRecipes.TryGetValue(entityId, out prototype))
{
return false;
}
// TODO CONSTRUCTION fix this
return Powered;
}
@@ -105,17 +98,7 @@ namespace Content.Server.GameObjects.Components.Recycling
return;
}
var constructionSystem = EntitySystem.Get<ConstructionSystem>();
var recyclerPosition = Owner.Transform.MapPosition;
foreach (var stage in prototype.Stages)
{
if (!(stage.Forward is ConstructionStepMaterial step))
{
continue;
}
constructionSystem.SpawnIngredient(recyclerPosition, step);
}
// TODO CONSTRUCTION fix this
entity.Delete();
}

View File

@@ -1,10 +1,14 @@
using System;
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Timers;
@@ -16,8 +20,11 @@ namespace Content.Server.GameObjects.Components.Stack
// TODO: Naming and presentation and such could use some improvement.
[RegisterComponent]
[ComponentReference(typeof(SharedStackComponent))]
public class StackComponent : SharedStackComponent, IInteractUsing, IExamine
{
[Dependency] private IEntityManager _entityManager = default!;
private bool _throwIndividually = false;
public override int Count
@@ -57,6 +64,33 @@ namespace Content.Server.GameObjects.Components.Stack
return false;
}
/// <summary>
/// Attempts to split this stack in two.
/// </summary>
/// <param name="amount">amount the new stack will have</param>
/// <param name="spawnPosition">the position the new stack will spawn at</param>
/// <param name="stack">the new stack</param>
/// <returns></returns>
public bool Split(int amount, EntityCoordinates spawnPosition, [NotNullWhen(true)] out IEntity? stack)
{
if (Count >= amount)
{
Count -= amount;
stack = _entityManager.SpawnEntity(Owner.Prototype?.ID, spawnPosition);
if (stack.TryGetComponent(out StackComponent? stackComp))
{
stackComp.Count = amount;
}
return true;
}
stack = null;
return false;
}
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (eventArgs.Using.TryGetComponent<StackComponent>(out var stack))

View File

@@ -0,0 +1,11 @@
using Content.Shared.GameObjects.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components
{
[RegisterComponent]
[ComponentReference(typeof(SharedWindowComponent))]
public class WindowComponent : SharedWindowComponent
{
}
}

View File

@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Content.Server.GameObjects.Components.Atmos;
using Content.Shared.Atmos;
@@ -286,7 +287,7 @@ namespace Content.Server.GameObjects.EntitySystems.Atmos
gridAtmosComponents[gridId] = gam;
}
foreach (var invalid in indices)
foreach (var invalid in indices.ToArray())
{
var chunk = GetOrCreateChunk(gridId, invalid);

View File

@@ -1,29 +1,30 @@
using System;
#nullable enable
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Construction;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Stack;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Shared.Construction;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Interfaces;
using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.Player;
using Robust.Server.GameObjects.Components.Container;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timers;
namespace Content.Server.GameObjects.EntitySystems
{
@@ -33,548 +34,425 @@ namespace Content.Server.GameObjects.EntitySystems
[UsedImplicitly]
internal class ConstructionSystem : SharedConstructionSystem
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
private readonly Dictionary<string, ConstructionPrototype> _craftRecipes = new Dictionary<string, ConstructionPrototype>();
private readonly Dictionary<ICommonSession, HashSet<int>> _beingBuilt = new Dictionary<ICommonSession, HashSet<int>>();
public IReadOnlyDictionary<string, ConstructionPrototype> CraftRecipes => _craftRecipes;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
foreach (var prototype in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
_craftRecipes.Add(prototype.Result, prototype);
}
SubscribeNetworkEvent<TryStartStructureConstructionMessage>(HandleStartStructureConstruction);
SubscribeNetworkEvent<TryStartItemConstructionMessage>(HandleStartItemConstruction);
SubscribeLocalEvent<AfterInteractMessage>(HandleToolInteraction);
}
private void HandleStartStructureConstruction(TryStartStructureConstructionMessage msg, EntitySessionEventArgs args)
private IEnumerable<IEntity> EnumerateNearby(IEntity user)
{
var placingEnt = args.SenderSession.AttachedEntity;
var result = TryStartStructureConstruction(placingEnt, msg.Location, msg.PrototypeName, msg.Angle);
if (!result) return;
var responseMsg = new AckStructureConstructionMessage(msg.Ack);
var channel = ((IPlayerSession) args.SenderSession).ConnectedClient;
RaiseNetworkEvent(responseMsg, channel);
}
private void HandleStartItemConstruction(TryStartItemConstructionMessage msg, EntitySessionEventArgs args)
if (user.TryGetComponent(out HandsComponent? hands))
{
var placingEnt = args.SenderSession.AttachedEntity;
TryStartItemConstruction(placingEnt, msg.PrototypeName);
}
private async void HandleToolInteraction(AfterInteractMessage msg)
foreach (var itemComponent in hands?.GetAllHeldItems()!)
{
if(msg.Handled)
return;
// You can only construct/deconstruct things within reach
if(!msg.CanReach)
return;
var targetEnt = msg.Attacked;
var handEnt = msg.ItemInHand;
// A tool has to interact with an entity.
if(targetEnt is null || handEnt is null)
return;
if (!handEnt.InRangeUnobstructed(targetEnt, ignoreInsideBlocker: true))
return;
// Cannot deconstruct an entity with no prototype.
var targetPrototype = targetEnt.MetaData.EntityPrototype;
if (targetPrototype is null)
return;
// the target entity is in the process of being constructed/deconstructed
if (msg.Attacked.TryGetComponent<ConstructionComponent>(out var constructComp))
if (itemComponent.Owner.TryGetComponent(out ServerStorageComponent? storage))
{
var result = await TryConstructEntity(constructComp, handEnt, msg.User);
// TryConstructEntity may delete the existing entity
msg.Handled = result;
}
else // try to start the deconstruction process
foreach (var storedEntity in storage.StoredEntities!)
{
// A tool was not used on the entity.
if (!handEnt.TryGetComponent<IToolComponent>(out var toolComp))
return;
// no known recipe for entity
if (!_craftRecipes.TryGetValue(targetPrototype.ID, out var prototype))
return;
// there is a recipe, but it can't be deconstructed.
var lastStep = prototype.Stages[^1].Backward;
if (!(lastStep is ConstructionStepTool))
return;
// wrong tool
var caps = ((ConstructionStepTool) lastStep).ToolQuality;
if ((toolComp.Qualities & caps) == 0)
return;
// ask around and see if the deconstruction prerequisites are satisfied
// (remove bulbs, approved access, open panels, etc)
var deconCompMsg = new BeginDeconstructCompMsg(msg.User);
targetEnt.SendMessage(null, deconCompMsg);
if(deconCompMsg.BlockDeconstruct)
return;
var deconEntMsg = new BeginDeconstructEntityMsg(msg.User, handEnt, targetEnt);
RaiseLocalEvent(deconEntMsg);
if(deconEntMsg.BlockDeconstruct)
return;
// --- GOOD TO GO ---
msg.Handled = true;
// pop off the material and switch to frame
var targetEntPos = targetEnt.Transform.MapPosition;
if (prototype.Stages.Count <= 2) // there are no intermediate stages
{
targetEnt.Delete();
SpawnIngredient(targetEntPos, prototype.Stages[(prototype.Stages.Count - 2)].Forward as ConstructionStepMaterial);
}
else // replace ent with intermediate
{
// Spawn frame
var frame = SpawnCopyTransform("structureconstructionframe", targetEnt.Transform);
var construction = frame.GetComponent<ConstructionComponent>();
SetupComponent(construction, prototype);
construction.Stage = prototype.Stages.Count - 2;
SetupDeconIntermediateSprite(construction, prototype);
frame.Transform.LocalRotation = targetEnt.Transform.LocalRotation;
if (targetEnt.Prototype.Components.TryGetValue("Item", out var itemProtoComp))
{
if(frame.HasComponent<ItemComponent>())
frame.RemoveComponent<ItemComponent>();
var itemComp = frame.AddComponent<ItemComponent>();
var serializer = YamlObjectSerializer.NewReader(itemProtoComp);
itemComp.ExposeData(serializer);
}
ReplaceInContainerOrGround(targetEnt, frame);
// remove target
targetEnt.Delete();
// spawn material
SpawnIngredient(targetEntPos, prototype.Stages[(prototype.Stages.Count-2)].Forward as ConstructionStepMaterial);
}
}
}
private IEntity SpawnCopyTransform(string prototypeId, ITransformComponent toReplace)
{
var frame = EntityManager.SpawnEntity(prototypeId, toReplace.MapPosition);
frame.Transform.WorldRotation = toReplace.WorldRotation;
frame.Transform.ParentUid = toReplace.ParentUid;
return frame;
}
private static void SetupDeconIntermediateSprite(ConstructionComponent constructionComponent, ConstructionPrototype prototype)
{
if(!constructionComponent.Owner.TryGetComponent<SpriteComponent>(out var spriteComp))
return;
for (var i = prototype.Stages.Count - 1; i >= 0; i--)
{
if (prototype.Stages[i].Icon != null)
{
spriteComp.AddLayerWithSprite(prototype.Stages[1].Icon);
return;
yield return storedEntity;
}
}
spriteComp.AddLayerWithSprite(prototype.Icon);
}
public void SpawnIngredient(MapCoordinates position, ConstructionStepMaterial lastStep)
{
if(lastStep is null)
return;
var material = lastStep.Material;
var quantity = lastStep.Amount;
var matEnt = EntityManager.SpawnEntity(MaterialPrototypes[material], position);
if (matEnt.TryGetComponent<StackComponent>(out var stackComp))
{
stackComp.Count = quantity;
}
else
{
quantity--; // already spawned one above
while (quantity > 0)
{
EntityManager.SpawnEntity(MaterialPrototypes[material], position);
quantity--;
}
}
}
private static readonly Dictionary<ConstructionStepMaterial.MaterialType, string> MaterialPrototypes =
new Dictionary<ConstructionStepMaterial.MaterialType, string>
{
{ ConstructionStepMaterial.MaterialType.Cable, "CableStack1" },
{ ConstructionStepMaterial.MaterialType.Gold, "GoldStack1" },
{ ConstructionStepMaterial.MaterialType.Metal, "SteelSheet1" },
{ ConstructionStepMaterial.MaterialType.Glass, "GlassSheet1" }
};
private bool TryStartStructureConstruction(IEntity placingEnt, EntityCoordinates loc, string prototypeName, Angle angle)
{
var prototype = _prototypeManager.Index<ConstructionPrototype>(prototypeName);
if (!placingEnt.InRangeUnobstructed(loc, ignoreInsideBlocker: prototype.CanBuildInImpassable, popup: true))
{
return false;
}
if (prototype.Stages.Count < 2)
{
throw new InvalidOperationException($"Prototype '{prototypeName}' does not have enough stages.");
}
var stage0 = prototype.Stages[0];
if (!(stage0.Forward is ConstructionStepMaterial matStep))
{
throw new NotImplementedException();
}
// Try to find the stack with the material in the user's hand.
if(!placingEnt.TryGetComponent<HandsComponent>(out var hands))
{
return false;
};
var activeHand = hands.GetActiveHand?.Owner;
if (activeHand == null)
{
return false;
}
if (!activeHand.TryGetComponent(out StackComponent stack) || !MaterialStackValidFor(matStep, stack))
{
return false;
}
if (!stack.Use(matStep.Amount))
{
return false;
}
// OK WE'RE GOOD CONSTRUCTION STARTED.
Get<AudioSystem>().PlayAtCoords("/Audio/Items/deconstruct.ogg", loc);
if (prototype.Stages.Count == 2)
{
// Exactly 2 stages, so don't make an intermediate frame.
var ent = EntityManager.SpawnEntity(prototype.Result, loc);
ent.Transform.LocalRotation = angle;
}
else
{
var frame = EntityManager.SpawnEntity("structureconstructionframe", loc);
var construction = frame.GetComponent<ConstructionComponent>();
SetupComponent(construction, prototype);
frame.Transform.LocalRotation = angle;
}
return true;
}
private void TryStartItemConstruction(IEntity placingEnt, string prototypeName)
{
if (!ActionBlockerSystem.CanInteract(placingEnt)) return;
var prototype = _prototypeManager.Index<ConstructionPrototype>(prototypeName);
if (prototype.Stages.Count < 2)
{
throw new InvalidOperationException($"Prototype '{prototypeName}' does not have enough stages.");
}
var stage0 = prototype.Stages[0];
if (!(stage0.Forward is ConstructionStepMaterial matStep))
{
throw new NotImplementedException();
}
// Try to find the stack with the material in the user's hand.
if (!placingEnt.TryGetComponent(out HandsComponent hands)) return;
var activeHand = hands.GetActiveHand?.Owner;
if (activeHand == null)
{
return;
}
if (!activeHand.TryGetComponent(out StackComponent stack) || !MaterialStackValidFor(matStep, stack))
{
return;
}
if (!stack.Use(matStep.Amount))
{
return;
}
// OK WE'RE GOOD CONSTRUCTION STARTED.
Get<AudioSystem>().PlayFromEntity("/Audio/Items/deconstruct.ogg", placingEnt);
if (prototype.Stages.Count == 2)
{
// Exactly 2 stages, so don't make an intermediate frame.
var ent = SpawnCopyTransform(prototype.Result, placingEnt.Transform);
hands.PutInHandOrDrop(ent.GetComponent<ItemComponent>());
}
else
{
var frame = SpawnCopyTransform("structureconstructionframe", placingEnt.Transform);
var construction = frame.GetComponent<ConstructionComponent>();
SetupComponent(construction, prototype);
var finalPrototype = _prototypeManager.Index<EntityPrototype>(prototype.Result);
if (finalPrototype.Components.TryGetValue("Item", out var itemProtoComp))
{
if(frame.HasComponent<ItemComponent>())
frame.RemoveComponent<ItemComponent>();
var itemComp = frame.AddComponent<ItemComponent>();
var serializer = YamlObjectSerializer.NewReader(itemProtoComp);
itemComp.ExposeData(serializer);
hands.PutInHandOrDrop(itemComp);
}
}
}
private async Task<bool> TryConstructEntity(ConstructionComponent constructionComponent, IEntity handTool, IEntity user)
{
var constructEntity = constructionComponent.Owner;
var spriteComponent = constructEntity.GetComponent<SpriteComponent>();
var transformComponent = constructEntity.GetComponent<ITransformComponent>();
// default interaction check for AttackBy allows inside blockers, so we will check if its blocked if
// we're not allowed to build on impassable stuff
var constructPrototype = constructionComponent.Prototype;
if (constructPrototype.CanBuildInImpassable == false)
{
if (!user.InRangeUnobstructed(constructEntity, popup: true))
return false;
}
var stage = constructPrototype.Stages[constructionComponent.Stage];
if (await TryProcessStep(constructEntity, stage.Forward, handTool, user, transformComponent.Coordinates))
{
constructionComponent.Stage++;
if (constructionComponent.Stage == constructPrototype.Stages.Count - 1)
{
// Oh boy we get to finish construction!
var ent = SpawnCopyTransform(constructPrototype.Result, transformComponent);
ent.Transform.LocalRotation = transformComponent.LocalRotation;
ReplaceInContainerOrGround(constructEntity, ent);
constructEntity.Delete();
return true;
}
stage = constructPrototype.Stages[constructionComponent.Stage];
if (stage.Icon != null)
{
spriteComponent.LayerSetSprite(0, stage.Icon);
yield return itemComponent.Owner;
}
}
else if (await TryProcessStep(constructEntity, stage.Backward, handTool, user, transformComponent.Coordinates))
if (user!.TryGetComponent(out InventoryComponent? inventory))
{
constructionComponent.Stage--;
stage = constructPrototype.Stages[constructionComponent.Stage];
// If forward needed a material, drop it
SpawnIngredient(constructEntity.Transform.MapPosition, stage.Forward as ConstructionStepMaterial);
if (constructionComponent.Stage == 0)
foreach (var held in inventory.GetAllHeldItems())
{
// Deconstruction complete.
constructEntity.Delete();
return true;
}
if (stage.Icon != null)
if (held.TryGetComponent(out ServerStorageComponent? storage))
{
spriteComponent.LayerSetSprite(0, stage.Icon);
foreach (var storedEntity in storage.StoredEntities!)
{
yield return storedEntity;
}
}
return true;
}
private static void ReplaceInContainerOrGround(IEntity oldEntity, IEntity newEntity)
{
var parentEntity = oldEntity.Transform.Parent?.Owner;
if (!(parentEntity is null) && parentEntity.TryGetComponent<IContainerManager>(out var containerMan))
{
if (containerMan.TryGetContainer(oldEntity, out var container))
{
container.ForceRemove(oldEntity);
container.Insert(newEntity);
}
yield return held;
}
}
private async Task<bool> TryProcessStep(IEntity constructEntity, ConstructionStep step, IEntity slapped, IEntity user, EntityCoordinates gridCoords)
foreach (var near in _entityManager.GetEntitiesInRange(user!, 2f, true))
{
if (step == null)
{
return false;
yield return near;
}
}
var sound = EntitySystemManager.GetEntitySystem<AudioSystem>();
private async Task<IEntity?> Construct(IEntity user, string materialContainer, ConstructionGraphPrototype graph, ConstructionGraphEdge edge, ConstructionGraphNode targetNode)
{
// We need a place to hold our construction items!
var container = ContainerManagerComponent.Ensure<Container>(materialContainer, user, out var existed);
if (existed)
{
user.PopupMessageCursor(Loc.GetString("You can't start another construction now!"));
return null;
}
var containers = new Dictionary<string, Container>();
var doAfterTime = 0f;
// HOLY SHIT THIS IS SOME HACKY CODE.
// But I'd rather do this shit than risk having collisions with other containers.
Container GetContainer(string name)
{
if (containers!.ContainsKey(name))
return containers[name];
while (true)
{
var random = _robustRandom.Next();
var c = ContainerManagerComponent.Ensure<Container>(random.ToString(), user!, out var existed);
if (existed) continue;
containers[name] = c;
return c;
}
}
void FailCleanup()
{
foreach (var entity in container!.ContainedEntities.ToArray())
{
container.Remove(entity);
}
foreach (var cont in containers!.Values)
{
foreach (var entity in cont.ContainedEntities.ToArray())
{
cont.Remove(entity);
}
}
// If we don't do this, items are invisible for some fucking reason. Nice.
Timer.Spawn(1, ShutdownContainers);
}
void ShutdownContainers()
{
container!.Shutdown();
foreach (var c in containers!.Values.ToArray())
{
c.Shutdown();
}
}
var failed = false;
var steps = new List<ConstructionGraphStep>();
foreach (var step in edge.Steps)
{
doAfterTime += step.DoAfter;
var handled = false;
switch (step)
{
case ConstructionStepMaterial matStep:
if (!slapped.TryGetComponent(out StackComponent stack)
|| !MaterialStackValidFor(matStep, stack)
|| !stack.Use(matStep.Amount))
case MaterialConstructionGraphStep materialStep:
foreach (var entity in EnumerateNearby(user))
{
return false;
}
if (matStep.Material == ConstructionStepMaterial.MaterialType.Cable)
sound.PlayAtCoords("/Audio/Items/zip.ogg", gridCoords);
else
sound.PlayAtCoords("/Audio/Items/deconstruct.ogg", gridCoords);
return true;
case ConstructionStepTool toolStep:
if (!slapped.TryGetComponent<ToolComponent>(out var tool))
return false;
if (!materialStep.EntityValid(entity, out var sharedStack))
continue;
// Handle welder manually since tool steps specify fuel amount needed, for some reason.
if (toolStep.ToolQuality.HasFlag(ToolQuality.Welding))
return slapped.TryGetComponent<WelderComponent>(out var welder)
&& await welder.UseTool(user, constructEntity, toolStep.DoAfterDelay, toolStep.ToolQuality, toolStep.Amount);
var stack = (StackComponent) sharedStack;
return await tool.UseTool(user, constructEntity, toolStep.DoAfterDelay, toolStep.ToolQuality);
if (!stack.Split(materialStep.Amount, user.ToCoordinates(), out var newStack))
continue;
default:
throw new NotImplementedException();
}
}
// Really this should check the actual materials at play..
private static readonly Dictionary<StackType, ConstructionStepMaterial.MaterialType> StackTypeMap
= new Dictionary<StackType, ConstructionStepMaterial.MaterialType>
if (string.IsNullOrEmpty(materialStep.Store))
{
{ StackType.Cable, ConstructionStepMaterial.MaterialType.Cable },
{ StackType.Gold, ConstructionStepMaterial.MaterialType.Gold },
{ StackType.Glass, ConstructionStepMaterial.MaterialType.Glass },
{ StackType.Metal, ConstructionStepMaterial.MaterialType.Metal }
if (!container.Insert(newStack))
continue;
}
else if (!GetContainer(materialStep.Store).Insert(newStack))
continue;
handled = true;
break;
}
break;
case ComponentConstructionGraphStep componentStep:
foreach (var entity in EnumerateNearby(user))
{
if (!componentStep.EntityValid(entity))
continue;
if (string.IsNullOrEmpty(componentStep.Store))
{
if (!container.Insert(entity))
continue;
}
else if (!GetContainer(componentStep.Store).Insert(entity))
continue;
handled = true;
break;
}
break;
case PrototypeConstructionGraphStep prototypeStep:
foreach (var entity in EnumerateNearby(user))
{
if (!prototypeStep.EntityValid(entity))
continue;
if (string.IsNullOrEmpty(prototypeStep.Store))
{
if (!container.Insert(entity))
continue;
}
else if (!GetContainer(prototypeStep.Store).Insert(entity))
{
continue;
}
handled = true;
break;
}
break;
}
if (handled == false)
{
failed = true;
break;
}
steps.Add(step);
}
if (failed)
{
user.PopupMessageCursor(Loc.GetString("You don't have the materials to build that!"));
FailCleanup();
return null;
}
var doAfterSystem = Get<DoAfterSystem>();
var doAfterArgs = new DoAfterEventArgs(user, doAfterTime)
{
BreakOnDamage = true,
BreakOnStun = true,
BreakOnTargetMove = false,
BreakOnUserMove = true,
NeedHand = true,
};
private static bool MaterialStackValidFor(ConstructionStepMaterial step, StackComponent stack)
if (await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Cancelled)
{
return StackTypeMap.TryGetValue((StackType)stack.StackType, out var should) && should == step.Material;
FailCleanup();
return null;
}
private void SetupComponent(ConstructionComponent constructionComponent, ConstructionPrototype prototype)
var newEntity = _entityManager.SpawnEntity(graph.Nodes[edge.Target].Entity, user.Transform.Coordinates);
// Yes, this should throw if it's missing the component.
var construction = newEntity.GetComponent<ConstructionComponent>();
// We attempt to set the pathfinding target.
construction.Target = targetNode;
// We preserve the containers...
foreach (var (name, cont) in containers)
{
constructionComponent.Prototype = prototype;
constructionComponent.Stage = 1;
var spriteComp = constructionComponent.Owner.GetComponent<SpriteComponent>();
if(prototype.Stages[1].Icon != null)
var newCont = ContainerManagerComponent.Ensure<Container>(name, newEntity);
foreach (var entity in cont.ContainedEntities.ToArray())
{
spriteComp.AddLayerWithSprite(prototype.Stages[1].Icon);
cont.ForceRemove(entity);
newCont.Insert(entity);
}
}
// We now get rid of all them.
ShutdownContainers();
// We have step completed steps!
foreach (var step in steps)
{
foreach (var completed in step.Completed)
{
await completed.PerformAction(newEntity, user);
}
}
// And we also have edge completed effects!
foreach (var completed in edge.Completed)
{
await completed.PerformAction(newEntity, user);
}
return newEntity;
}
private async void HandleStartItemConstruction(TryStartItemConstructionMessage ev, EntitySessionEventArgs args)
{
var constructionPrototype = _prototypeManager.Index<ConstructionPrototype>(ev.PrototypeName);
var constructionGraph = _prototypeManager.Index<ConstructionGraphPrototype>(constructionPrototype.Graph);
var startNode = constructionGraph.Nodes[constructionPrototype.StartNode];
var targetNode = constructionGraph.Nodes[constructionPrototype.TargetNode];
var pathFind = constructionGraph.Path(startNode.Name, targetNode.Name);
var user = args.SenderSession.AttachedEntity;
if (user == null || !ActionBlockerSystem.CanInteract(user)) return;
if (!user.TryGetComponent(out HandsComponent? hands)) return;
foreach (var condition in constructionPrototype.Conditions)
{
if (!condition.Condition(user, user.ToCoordinates(), Direction.South))
return;
}
if(pathFind == null)
throw new InvalidDataException($"Can't find path from starting node to target node in construction! Recipe: {ev.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}");
// No support for conditions here!
foreach (var step in edge.Steps)
{
switch (step)
{
case ToolConstructionGraphStep _:
case NestedConstructionGraphStep _:
throw new InvalidDataException("Invalid first step for construction recipe!");
}
}
var item = await Construct(user, "item_construction", constructionGraph, edge, targetNode);
if(item != null && item.TryGetComponent(out ItemComponent? itemComp))
hands.PutInHandOrDrop(itemComp);
}
private async void HandleStartStructureConstruction(TryStartStructureConstructionMessage ev, EntitySessionEventArgs args)
{
var constructionPrototype = _prototypeManager.Index<ConstructionPrototype>(ev.PrototypeName);
var constructionGraph = _prototypeManager.Index<ConstructionGraphPrototype>(constructionPrototype.Graph);
var startNode = constructionGraph.Nodes[constructionPrototype.StartNode];
var targetNode = constructionGraph.Nodes[constructionPrototype.TargetNode];
var pathFind = constructionGraph.Path(startNode.Name, targetNode.Name);
var user = args.SenderSession.AttachedEntity;
if (_beingBuilt.TryGetValue(args.SenderSession, out var set))
{
if (!set.Add(ev.Ack))
{
user.PopupMessageCursor(Loc.GetString("You are already building that!"));
return;
}
}
else
{
spriteComp.AddLayerWithSprite(prototype.Icon);
var newSet = new HashSet<int> {ev.Ack};
_beingBuilt[args.SenderSession] = newSet;
}
var frame = constructionComponent.Owner;
var finalPrototype = _prototypeManager.Index<EntityPrototype>(prototype.Result);
frame.Name = $"Unfinished {finalPrototype.Name}";
}
}
/// <summary>
/// A system message that is raised when an entity is trying to be deconstructed.
/// </summary>
public class BeginDeconstructEntityMsg : EntitySystemMessage
foreach (var condition in constructionPrototype.Conditions)
{
/// <summary>
/// Entity that initiated the deconstruction.
/// </summary>
public IEntity User { get; }
/// <summary>
/// Tool in the active hand of the user.
/// </summary>
public IEntity Hand { get; }
/// <summary>
/// Target entity that is trying to be deconstructed.
/// </summary>
public IEntity Target { get; }
/// <summary>
/// Set this to true if you would like to block the deconstruction from happening.
/// </summary>
public bool BlockDeconstruct { get; set; }
/// <summary>
/// Constructs a new instance of <see cref="BeginDeconstructEntityMsg"/>.
/// </summary>
/// <param name="user">Entity that initiated the deconstruction.</param>
/// <param name="hand">Tool in the active hand of the user.</param>
/// <param name="target">Target entity that is trying to be deconstructed.</param>
public BeginDeconstructEntityMsg(IEntity user, IEntity hand, IEntity target)
if (!condition.Condition(user, ev.Location, ev.Angle.GetCardinalDir()))
{
User = user;
Hand = hand;
Target = target;
Cleanup();
return;
}
}
/// <summary>
/// A component message that is raised when an entity is trying to be deconstructed.
/// </summary>
public class BeginDeconstructCompMsg : ComponentMessage
void Cleanup()
{
/// <summary>
/// Entity that initiated the deconstruction.
/// </summary>
public IEntity User { get; }
_beingBuilt[args.SenderSession].Remove(ev.Ack);
}
/// <summary>
/// Set this to true if you would like to block the deconstruction from happening.
/// </summary>
public bool BlockDeconstruct { get; set; }
/// <summary>
/// Constructs a new instance of <see cref="BeginDeconstructCompMsg"/>.
/// </summary>
/// <param name="user">Entity that initiated the deconstruction.</param>
public BeginDeconstructCompMsg(IEntity user)
if (user == null
|| !ActionBlockerSystem.CanInteract(user)
|| !user.TryGetComponent(out HandsComponent? hands) || hands.GetActiveHand == null
|| !user.InRangeUnobstructed(ev.Location, ignoreInsideBlocker:constructionPrototype.CanBuildInImpassable))
{
User = user;
Cleanup();
return;
}
if(pathFind == null)
throw new InvalidDataException($"Can't find path from starting node to target node in construction! Recipe: {ev.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}");
var valid = false;
var holding = hands.GetActiveHand?.Owner;
if (holding == null)
{
Cleanup();
return;
}
// No support for conditions here!
foreach (var step in edge.Steps)
{
switch (step)
{
case EntityInsertConstructionGraphStep entityInsert:
if (entityInsert.EntityValid(holding))
valid = true;
break;
case ToolConstructionGraphStep _:
case NestedConstructionGraphStep _:
throw new InvalidDataException("Invalid first step for item recipe!");
}
if (valid)
break;
}
if (!valid)
{
Cleanup();
return;
}
var structure = await Construct(user, (ev.Ack + constructionPrototype.GetHashCode()).ToString(), constructionGraph, edge, targetNode);
if (structure == null)
{
Cleanup();
return;
}
structure.Transform.Coordinates = ev.Location;
structure.Transform.LocalRotation = ev.Angle;
RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack));
Cleanup();
}
}
}

View File

@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components;
using Content.Shared.GameObjects.Components.Damage;
@@ -27,7 +28,7 @@ namespace Content.Server.GameObjects.EntitySystems.DoAfter
var cancelled = new List<DoAfter>(0);
var finished = new List<DoAfter>(0);
foreach (var doAfter in comp.DoAfters)
foreach (var doAfter in comp.DoAfters.ToArray())
{
doAfter.Run(frameTime);

View File

@@ -10,7 +10,6 @@
"SubFloorHide",
"LowWall",
"ReinforcedWall",
"Window",
"CharacterInfo",
"InteractionOutline",
"MeleeWeaponArcAnimation",

View File

@@ -0,0 +1,43 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.Transform;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.Utility
{
public static class SnapgridHelper
{
public static void SnapToGrid(this IEntity entity, SnapGridOffset offset = SnapGridOffset.Center, IEntityManager entityManager = null, IMapManager mapManager = null)
{
entity.Transform.Coordinates = entity.Transform.Coordinates.SnapToGrid(offset, entityManager, mapManager);
}
public static EntityCoordinates SnapToGrid(this EntityCoordinates coordinates,
SnapGridOffset offset = SnapGridOffset.Center, IEntityManager entityManager = null, IMapManager mapManager = null)
{
entityManager ??= IoCManager.Resolve<IEntityManager>();
mapManager ??= IoCManager.Resolve<IMapManager>();
var gridId = coordinates.GetGridId(entityManager);
var tileSize = 1f;
if (gridId.IsValid())
{
var grid = mapManager.GetGrid(gridId);
tileSize = grid.TileSize;
}
var localPos = coordinates.Position;
var x = (int)Math.Floor(localPos.X / tileSize) + tileSize / (offset == SnapGridOffset.Center ? 2f : 0f);
var y = (int)Math.Floor(localPos.Y / tileSize) + tileSize / (offset == SnapGridOffset.Center ? 2f : 0f);
return new EntityCoordinates(coordinates.EntityId, x, y);
}
}
}

View File

@@ -0,0 +1,19 @@
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Construction
{
public abstract class ArbitraryInsertConstructionGraphStep : EntityInsertConstructionGraphStep
{
public string Name { get; private set; }
public SpriteSpecifier Icon { get; private set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Icon, "icon", SpriteSpecifier.Invalid);
serializer.DataField(this, x => x.Name, "name", string.Empty);
}
}
}

View File

@@ -0,0 +1,38 @@
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Construction
{
public class ComponentConstructionGraphStep : ArbitraryInsertConstructionGraphStep
{
public string Component { get; private set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Component, "component", string.Empty);
}
public override bool EntityValid(IEntity entity)
{
foreach (var component in entity.GetAllComponents())
{
if (component.Name == Component)
return true;
}
return false;
}
public override void DoExamine(FormattedMessage message, bool inDetailsRange)
{
message.AddMarkup(string.IsNullOrEmpty(Name)
? Loc.GetString("Next, insert an entity with a {0} component.", Component) // Terrible.
: Loc.GetString("Next, insert {0}", Name));
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using Content.Shared.GameObjects.Components;
using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
namespace Content.Shared.Construction.ConstructionConditions
{
[UsedImplicitly]
public class LowWallInTile : IConstructionCondition
{
public void ExposeData(ObjectSerializer serializer) { }
public bool Condition(IEntity user, EntityCoordinates location, Direction direction)
{
var lowWall = false;
foreach (var entity in location.GetEntitiesInTile(true))
{
if (entity.HasComponent<SharedCanBuildWindowOnTopComponent>())
lowWall = true;
// Already has a window.
if (entity.HasComponent<SharedWindowComponent>())
return false;
}
return lowWall;
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using Content.Shared.GameObjects.Components;
using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
namespace Content.Shared.Construction.ConstructionConditions
{
[UsedImplicitly]
public class NoWindowsInTile : IConstructionCondition
{
public void ExposeData(ObjectSerializer serializer) { }
public bool Condition(IEntity user, EntityCoordinates location, Direction direction)
{
foreach (var entity in location.GetEntitiesInTile(true))
{
if (entity.HasComponent<SharedWindowComponent>())
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.IO;
using Content.Shared.GameObjects.Components.Power;
using Content.Shared.Interfaces;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using YamlDotNet.RepresentationModel;
namespace Content.Shared.Construction
{
[Serializable]
public class ConstructionGraphEdge : IExposeData
{
private List<ConstructionGraphStep> _steps = new List<ConstructionGraphStep>();
private List<IEdgeCondition> _conditions;
private List<IGraphAction> _completed;
[ViewVariables]
public string Target { get; private set; }
[ViewVariables]
public IReadOnlyList<IEdgeCondition> Conditions => _conditions;
[ViewVariables]
public IReadOnlyList<IGraphAction> Completed => _completed;
[ViewVariables]
public IReadOnlyList<ConstructionGraphStep> Steps => _steps;
public void ExposeData(ObjectSerializer serializer)
{
var moduleManager = IoCManager.Resolve<IModuleManager>();
serializer.DataField(this, x => x.Target, "to", string.Empty);
if (!moduleManager.IsServerModule) return;
serializer.DataField(ref _conditions, "conditions", new List<IEdgeCondition>());
serializer.DataField(ref _completed, "completed", new List<IGraphAction>());
}
public void LoadFrom(YamlMappingNode mapping)
{
var serializer = YamlObjectSerializer.NewReader(mapping);
ExposeData(serializer);
if (!mapping.TryGetNode("steps", out YamlSequenceNode stepsMapping)) return;
foreach (var yamlNode in stepsMapping)
{
var stepMapping = (YamlMappingNode) yamlNode;
_steps.Add(LoadStep(stepMapping));
}
}
public static ConstructionGraphStep LoadStep(YamlMappingNode mapping)
{
var stepSerializer = YamlObjectSerializer.NewReader(mapping);
if (mapping.TryGetNode("material", out _))
{
var material = new MaterialConstructionGraphStep();
material.ExposeData(stepSerializer);
return material;
}
if (mapping.TryGetNode("tool", out _))
{
var tool = new ToolConstructionGraphStep();
tool.ExposeData(stepSerializer);
return tool;
}
if (mapping.TryGetNode("prototype", out _))
{
var prototype = new PrototypeConstructionGraphStep();
prototype.ExposeData(stepSerializer);
return prototype;
}
if (mapping.TryGetNode("component", out _))
{
var component = new ComponentConstructionGraphStep();
component.ExposeData(stepSerializer);
return component;
}
if(mapping.TryGetNode("steps", out _))
{
var nested = new NestedConstructionGraphStep();
nested.ExposeData(stepSerializer);
nested.LoadFrom(mapping);
return nested;
}
throw new ArgumentException("Tried to convert invalid YAML node mapping to ConstructionGraphStep!");
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using Content.Shared.Interfaces;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using YamlDotNet.RepresentationModel;
using ObjectSerializer = Robust.Shared.Serialization.ObjectSerializer;
namespace Content.Shared.Construction
{
[Serializable]
public class ConstructionGraphNode
{
private List<IGraphAction> _actions = new List<IGraphAction>();
private List<ConstructionGraphEdge> _edges = new List<ConstructionGraphEdge>();
[ViewVariables]
public string Name { get; private set; }
[ViewVariables]
public IReadOnlyList<ConstructionGraphEdge> Edges => _edges;
[ViewVariables]
public IReadOnlyList<IGraphAction> Actions => _actions;
[ViewVariables]
public string Entity { get; private set; }
public void ExposeData(ObjectSerializer serializer)
{
var moduleManager = IoCManager.Resolve<IModuleManager>();
serializer.DataField(this, x => x.Name, "node", string.Empty);
serializer.DataField(this, x => x.Entity, "entity",string.Empty);
if (!moduleManager.IsServerModule) return;
serializer.DataField(ref _actions, "actions", new List<IGraphAction>());
}
public void LoadFrom(YamlMappingNode mapping)
{
var serializer = YamlObjectSerializer.NewReader(mapping);
ExposeData(serializer);
if (!mapping.TryGetNode("edges", out YamlSequenceNode edgesMapping)) return;
foreach (var yamlNode in edgesMapping)
{
var edgeMapping = (YamlMappingNode) yamlNode;
var edge = new ConstructionGraphEdge();
edge.LoadFrom(edgeMapping);
_edges.Add(edge);
}
}
public ConstructionGraphEdge GetEdge(string target)
{
foreach (var edge in _edges)
{
if (edge.Target == target)
return edge;
}
return null;
}
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.IO;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using YamlDotNet.RepresentationModel;
namespace Content.Shared.Construction
{
[Prototype("constructionGraph")]
public class ConstructionGraphPrototype : IPrototype, IIndexedPrototype
{
private readonly Dictionary<string, ConstructionGraphNode> _nodes = new Dictionary<string, ConstructionGraphNode>();
private Dictionary<ValueTuple<string, string>, ConstructionGraphNode[]> _paths = new Dictionary<ValueTuple<string, string>, ConstructionGraphNode[]>();
private Dictionary<ConstructionGraphNode, ConstructionGraphNode> _pathfinding = new Dictionary<ConstructionGraphNode, ConstructionGraphNode>();
[ViewVariables]
public string ID { get; private set; }
[ViewVariables]
public string Start { get; private set; }
[ViewVariables]
public IReadOnlyDictionary<string, ConstructionGraphNode> Nodes => _nodes;
public void LoadFrom(YamlMappingNode mapping)
{
var serializer = YamlObjectSerializer.NewReader(mapping);
serializer.DataField(this, x => x.ID, "id", string.Empty);
serializer.DataField(this, x => x.Start, "start", string.Empty);
if (!mapping.TryGetNode("graph", out YamlSequenceNode graphMapping)) return;
foreach (var yamlNode in graphMapping)
{
var childMapping = (YamlMappingNode) yamlNode;
var node = new ConstructionGraphNode();
node.LoadFrom(childMapping);
_nodes[node.Name] = node;
}
if(string.IsNullOrEmpty(Start) || !_nodes.ContainsKey(Start))
throw new InvalidDataException($"Starting node for construction graph {ID} is null, empty or invalid!");
_pathfinding = Pathfind(Start);
}
public ConstructionGraphEdge Edge(string startNode, string nextNode)
{
var start = _nodes[startNode];
return start.GetEdge(nextNode);
}
public ConstructionGraphNode[] Path(string startNode, string finishNode)
{
var tuple = new ValueTuple<string, string>(startNode, finishNode);
if (_paths.ContainsKey(tuple))
return _paths[tuple];
var start = _nodes[startNode];
var finish = _nodes[finishNode];
var current = finish;
var path = new List<ConstructionGraphNode>();
while (current != start)
{
path.Add(current);
// No path.
if (current == null || !_pathfinding.ContainsKey(current))
{
// We remember this for next time.
_paths[tuple] = null;
return null;
}
current = _pathfinding[current];
}
path.Reverse();
return _paths[tuple] = path.ToArray();
}
/// <summary>
/// Uses breadth first search for pathfinding.
/// </summary>
/// <param name="start"></param>
private Dictionary<ConstructionGraphNode, ConstructionGraphNode> Pathfind(string start)
{
// TODO: Make this use A* or something, although it's not that important.
var startNode = _nodes[start];
var frontier = new Queue<ConstructionGraphNode>();
var cameFrom = new Dictionary<ConstructionGraphNode, ConstructionGraphNode>();
frontier.Enqueue(startNode);
cameFrom[startNode] = null;
while (frontier.Count != 0)
{
var current = frontier.Dequeue();
foreach (var edge in current.Edges)
{
var edgeNode = _nodes[edge.Target];
if(cameFrom.ContainsKey(edgeNode)) continue;
frontier.Enqueue(edgeNode);
cameFrom[edgeNode] = current;
}
}
return cameFrom;
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using Content.Shared.Interfaces;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Construction
{
[Serializable]
public abstract class ConstructionGraphStep : IExposeData
{
private List<IGraphAction> _completed;
public float DoAfter { get; private set; }
public IReadOnlyList<IGraphAction> Completed => _completed;
public virtual void ExposeData(ObjectSerializer serializer)
{
var moduleManager = IoCManager.Resolve<IModuleManager>();
serializer.DataField(this, x => x.DoAfter, "doAfter", 0f);
if (!moduleManager.IsServerModule) return;
serializer.DataField(ref _completed, "completed", new List<IGraphAction>());
}
public abstract void DoExamine(FormattedMessage message, bool inDetailsRange);
}
}

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.GameObjects.Components.Interactable;
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
@@ -12,164 +9,69 @@ namespace Content.Shared.Construction
[Prototype("construction")]
public class ConstructionPrototype : IPrototype, IIndexedPrototype
{
private string _name;
private string _description;
private SpriteSpecifier _icon;
private List<string> _keywords;
private List<string> _categorySegments;
private List<ConstructionStage> _stages = new List<ConstructionStage>();
private ConstructionType _type;
private string _id;
private string _result;
private string _placementMode;
private bool _canBuildInImpassable;
private List<IConstructionCondition> _conditions;
/// <summary>
/// Friendly name displayed in the construction GUI.
/// </summary>
public string Name => _name;
public string Name { get; private set; }
/// <summary>
/// "Useful" description displayed in the construction GUI.
/// </summary>
public string Description => _description;
public string Description { get; private set; }
/// <summary>
/// The <see cref="ConstructionGraphPrototype"/> this construction will be using.
/// </summary>
public string Graph { get; private set; }
/// <summary>
/// The target <see cref="ConstructionGraphNode"/> this construction will guide the user to.
/// </summary>
public string TargetNode { get; private set; }
/// <summary>
/// The starting <see cref="ConstructionGraphNode"/> this construction will start at.
/// </summary>
public string StartNode { get; private set; }
/// <summary>
/// Texture path inside the construction GUI.
/// </summary>
public SpriteSpecifier Icon => _icon;
public SpriteSpecifier Icon { get; private set; }
/// <summary>
/// If you can start building or complete steps on impassable terrain.
/// </summary>
public bool CanBuildInImpassable => _canBuildInImpassable;
public bool CanBuildInImpassable { get; private set; }
/// <summary>
/// A list of keywords that are used for searching.
/// </summary>
public IReadOnlyList<string> Keywords => _keywords;
public string Category { get; private set; }
/// <summary>
/// The split up segments of the category.
/// </summary>
public IReadOnlyList<string> CategorySegments => _categorySegments;
public ConstructionType Type { get; private set; }
/// <summary>
/// The list of stages of construction.
/// Construction is separated into "stages" which is basically a state in a linear FSM.
/// The stage has forward and optionally backwards "steps" which are the criteria to move around in the FSM.
/// NOTE that the stages are mapped differently than they appear in the prototype.
/// In the prototype, the forward step is displayed as "to move into this stage" and reverse "to move out of"
/// Stage 0 is considered "construction not started" and last stage is considered "construction is finished".
/// As such, neither last or 0 stage have actual stage DATA, only backward/forward steps respectively.
/// This would be akward for a YAML prototype because it's always jagged.
/// </summary>
public IReadOnlyList<ConstructionStage> Stages => _stages;
public string ID { get; private set; }
public ConstructionType Type => _type;
public string PlacementMode { get; private set; }
public string ID => _id;
/// <summary>
/// The prototype name of the entity prototype when construction is done.
/// </summary>
public string Result => _result;
public string PlacementMode => _placementMode;
public IReadOnlyList<IConstructionCondition> Conditions => _conditions;
public void LoadFrom(YamlMappingNode mapping)
{
var ser = YamlObjectSerializer.NewReader(mapping);
_name = ser.ReadDataField<string>("name");
Name = ser.ReadDataField<string>("name");
ser.DataField(ref _id, "id", string.Empty);
ser.DataField(ref _description, "description", string.Empty);
ser.DataField(ref _icon, "icon", SpriteSpecifier.Invalid);
ser.DataField(ref _type, "objectType", ConstructionType.Structure);
ser.DataField(ref _result, "result", null);
ser.DataField(ref _placementMode, "placementMode", "PlaceFree");
ser.DataField(ref _canBuildInImpassable, "canBuildInImpassable", false);
_keywords = ser.ReadDataField<List<string>>("keywords", new List<string>());
{
var cat = ser.ReadDataField<string>("category");
var split = cat.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
_categorySegments = split.ToList();
}
{
SpriteSpecifier nextIcon = null;
ConstructionStep nextBackward = null;
foreach (var stepMap in mapping.GetNode<YamlSequenceNode>("steps").Cast<YamlMappingNode>())
{
var step = ReadStepPrototype(stepMap);
_stages.Add(new ConstructionStage(step, nextIcon, nextBackward));
if (stepMap.TryGetNode("icon", out var node))
{
nextIcon = SpriteSpecifier.FromYaml(node);
}
if (stepMap.TryGetNode("reverse", out YamlMappingNode revMap))
{
nextBackward = ReadStepPrototype(revMap);
}
}
_stages.Add(new ConstructionStage(null, nextIcon, nextBackward));
}
}
ConstructionStep ReadStepPrototype(YamlMappingNode step)
{
int amount = 1;
if (step.TryGetNode("amount", out var node))
{
amount = node.AsInt();
}
if (step.TryGetNode("material", out node))
{
return new ConstructionStepMaterial(
node.AsEnum<ConstructionStepMaterial.MaterialType>(),
amount
);
}
if (step.TryGetNode("tool", out node))
{
return new ConstructionStepTool(
node.AsEnum<ToolQuality>(),
amount
);
}
throw new InvalidOperationException("Not enough data specified to determine step.");
}
}
public sealed class ConstructionStage
{
/// <summary>
/// The icon of the construction frame at this stage.
/// </summary>
public readonly SpriteSpecifier Icon;
/// <summary>
/// The step that should be completed to move away from this stage to the next one.
/// </summary>
public readonly ConstructionStep Forward;
/// <summary>
/// The optional step that can be completed to move away from this stage to the previous one.
/// </summary>
public readonly ConstructionStep Backward;
public ConstructionStage(ConstructionStep forward, SpriteSpecifier icon = null, ConstructionStep backward = null)
{
Icon = icon;
Forward = forward;
Backward = backward;
ser.DataField(this, x => x.ID, "id", string.Empty);
ser.DataField(this, x => x.Graph, "graph", string.Empty);
ser.DataField(this, x => x.TargetNode, "targetNode", string.Empty);
ser.DataField(this, x => x.StartNode, "startNode", string.Empty);
ser.DataField(this, x => x.Description, "description", string.Empty);
ser.DataField(this, x => x.Icon, "icon", SpriteSpecifier.Invalid);
ser.DataField(this, x => x.Type, "objectType", ConstructionType.Structure);
ser.DataField(this, x => x.PlacementMode, "placementMode", "PlaceFree");
ser.DataField(this, x => x.CanBuildInImpassable, "canBuildInImpassable", false);
ser.DataField(this, x => x.Category, "category", string.Empty);
ser.DataField(ref _conditions, "conditions", new List<IConstructionCondition>());
}
}
@@ -178,47 +80,5 @@ namespace Content.Shared.Construction
Structure,
Item,
}
public abstract class ConstructionStep
{
public readonly int Amount;
public readonly float DoAfterDelay;
protected ConstructionStep(int amount, float doAfterDelay = 0f)
{
Amount = amount;
DoAfterDelay = doAfterDelay;
}
}
public class ConstructionStepTool : ConstructionStep
{
public readonly ToolQuality ToolQuality;
public ConstructionStepTool(ToolQuality toolQuality, int amount) : base(amount)
{
ToolQuality = toolQuality;
}
}
public class ConstructionStepMaterial : ConstructionStep
{
public readonly MaterialType Material;
public ConstructionStepMaterial(MaterialType material, int amount) : base(amount)
{
Material = material;
}
public enum MaterialType
{
Metal,
Glass,
Cable,
Gold,
Phoron,
}
}
}

View File

@@ -0,0 +1,19 @@
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Shared.Construction
{
public abstract class EntityInsertConstructionGraphStep : ConstructionGraphStep
{
public string Store { get; private set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Store, "store", string.Empty);
}
public abstract bool EntityValid(IEntity entity);
}
}

View File

@@ -0,0 +1,12 @@
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Shared.Construction
{
public interface IConstructionCondition : IExposeData
{
bool Condition(IEntity user, EntityCoordinates location, Direction direction);
}
}

View File

@@ -0,0 +1,13 @@
using System.Threading.Tasks;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Construction
{
public interface IEdgeCondition : IExposeData
{
Task<bool> Condition(IEntity entity);
void DoExamine(IEntity entity, FormattedMessage message, bool inExamineRange) { }
}
}

View File

@@ -0,0 +1,13 @@
#nullable enable
using System;
using System.Threading.Tasks;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
namespace Content.Shared.Construction
{
public interface IGraphAction : IExposeData
{
Task PerformAction(IEntity entity, IEntity? user);
}
}

View File

@@ -0,0 +1,48 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Materials;
using Content.Shared.Materials;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Construction
{
public class MaterialConstructionGraphStep : EntityInsertConstructionGraphStep
{
// TODO: Make this use the material system.
// TODO TODO: Make the material system not shit.
public StackType Material { get; private set; }
public int Amount { get; private set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Material, "material", StackType.Metal);
serializer.DataField(this, x => x.Amount, "amount", 1);
}
public override void DoExamine(FormattedMessage message, bool inDetailsRange)
{
message.AddMarkup(Loc.GetString("Next, add [color=yellow]{0}x[/color] [color=cyan]{1}[/color].", Amount, Material));
}
public override bool EntityValid(IEntity entity)
{
return entity.TryGetComponent(out SharedStackComponent? stack) && stack.StackType.Equals(Material);
}
public bool EntityValid(IEntity entity, [NotNullWhen(true)] out SharedStackComponent? stack)
{
if(entity.TryGetComponent(out SharedStackComponent? otherStack) && otherStack.StackType.Equals(Material))
stack = otherStack;
else
stack = null;
return stack != null;
}
}
}

View File

@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.IO;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Content.Shared.Construction
{
public class NestedConstructionGraphStep : ConstructionGraphStep
{
public List<List<ConstructionGraphStep>> Steps { get; private set; } = new List<List<ConstructionGraphStep>>();
public void LoadFrom(YamlMappingNode mapping)
{
if (!mapping.TryGetNode("steps", out YamlSequenceNode steps)) return;
foreach (var node in steps)
{
var sequence = (YamlSequenceNode) node;
var list = new List<ConstructionGraphStep>();
foreach (var innerNode in sequence)
{
var stepNode = (YamlMappingNode) innerNode;
var step = ConstructionGraphEdge.LoadStep(stepNode);
if(step is NestedConstructionGraphStep)
throw new InvalidDataException("Can't have nested construction steps inside nested construction steps!");
list.Add(step);
}
Steps.Add(list);
}
}
public override void DoExamine(FormattedMessage message, bool inDetailsRange)
{
}
}
}

View File

@@ -0,0 +1,31 @@
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Construction
{
public class PrototypeConstructionGraphStep : ArbitraryInsertConstructionGraphStep
{
public string Prototype { get; private set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Prototype, "prototype", string.Empty);
}
public override bool EntityValid(IEntity entity)
{
return entity.Prototype?.ID == Prototype;
}
public override void DoExamine(FormattedMessage message, bool inDetailsRange)
{
message.AddMarkup(string.IsNullOrEmpty(Name)
? Loc.GetString("Next, insert {0}", Prototype) // Terrible.
: Loc.GetString("Next, insert {0}", Name));
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using Content.Shared.GameObjects.Components.Interactable;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Construction
{
public class ToolConstructionGraphStep : ConstructionGraphStep
{
public ToolQuality Tool { get; private set; }
public float Fuel { get; private set; }
public string ExamineOverride { get; private set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Tool, "tool", ToolQuality.None);
serializer.DataField(this, x => x.Fuel, "fuel", 10f); // Default fuel cost.
serializer.DataField(this, x => x.ExamineOverride, "examine", string.Empty);
}
public override void DoExamine(FormattedMessage message, bool inDetailsRange)
{
if (!string.IsNullOrEmpty(ExamineOverride))
{
message.AddMarkup(Loc.GetString(ExamineOverride));
return;
}
message.AddMarkup(Loc.GetString($"Next, use a [color=cyan]{Tool.GetToolName()}[/color]."));
}
}
}

View File

@@ -5,6 +5,7 @@ using Content.Shared.Damage;
using Content.Shared.Damage.DamageContainer;
using Content.Shared.Damage.ResistanceSet;
using Content.Shared.Interfaces.GameObjects.Components;
using Mono.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
@@ -157,6 +158,9 @@ namespace Content.Shared.GameObjects.Components.Damage
{
var writeFlags = new List<DamageFlag>();
if (Flags == DamageFlag.None)
return writeFlags;
foreach (var flag in (DamageFlag[]) Enum.GetValues(typeof(DamageFlag)))
{
if ((Flags & flag) == flag)

View File

@@ -16,6 +16,23 @@ namespace Content.Shared.GameObjects.Components.Interactable
Multitool = 1 << 5,
}
public static class ToolQualityHelpers
{
public static string GetToolName(this ToolQuality quality)
{
return quality switch
{
ToolQuality.Anchoring => "Wrench",
ToolQuality.Prying => "Crowbar",
ToolQuality.Screwing => "Screwdriver",
ToolQuality.Cutting => "Wirecutters",
ToolQuality.Welding => "Welding tool",
ToolQuality.Multitool => "Multitool",
_ => throw new ArgumentOutOfRangeException()
};
}
}
public class SharedToolComponent : Component
{
public override string Name => "Tool";

View File

@@ -0,0 +1,11 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components
{
[Serializable, NetSerializable]
public enum ReinforcedWallVisuals
{
DeconstructionStage,
}
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameObjects;
namespace Content.Shared.GameObjects.Components
{
[RegisterComponent]
public class SharedCanBuildWindowOnTopComponent : Component
{
public override string Name => "CanBuildWindowOnTop";
}
}

View File

@@ -125,6 +125,7 @@ namespace Content.Shared.GameObjects.Components
{
Metal,
Glass,
Plasteel,
Cable,
Gold,
Phoron,

View File

@@ -0,0 +1,9 @@
using Robust.Shared.GameObjects;
namespace Content.Shared.GameObjects.Components
{
public class SharedWindowComponent : Component
{
public override string Name => "Window";
}
}

View File

@@ -76,28 +76,5 @@ namespace Content.Shared.GameObjects.EntitySystems
GhostId = ghostId;
}
}
public void DoExamine(FormattedMessage message, ConstructionPrototype prototype, int stage, bool inDetailRange)
{
var stages = prototype.Stages;
if (stage >= 0 && stage < stages.Count)
{
var curStage = stages[stage];
if (curStage.Backward != null && curStage.Backward is ConstructionStepTool)
{
var backward = (ConstructionStepTool) curStage.Backward;
message.AddText(Loc.GetString("To deconstruct: {0}x {1} Tool", backward.Amount, backward.ToolQuality));
}
if (curStage.Forward != null && curStage.Forward is ConstructionStepMaterial)
{
if (curStage.Backward != null)
{
message.AddText("\n");
}
var forward = (ConstructionStepMaterial) curStage.Forward;
message.AddText(Loc.GetString("To construct: {0}x {1}", forward.Amount, forward.Material));
}
}
}
}
}

View File

@@ -5,7 +5,6 @@ using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Physics;
using Content.Shared.Utility;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.Interfaces.Physics;
@@ -29,18 +28,18 @@ namespace Content.Shared.Maps
/// <summary>
/// Attempts to get the turf at map indices with grid id or null if no such turf is found.
/// </summary>
public static TileRef? GetTileRef(this MapIndices mapIndices, GridId gridId, IMapManager? mapManager = null)
public static TileRef GetTileRef(this MapIndices mapIndices, GridId gridId, IMapManager? mapManager = null)
{
if (!gridId.IsValid())
return null;
return default;
mapManager ??= IoCManager.Resolve<IMapManager>();
if (!mapManager.TryGetGrid(gridId, out var grid))
return null;
return default;
if (!grid.TryGetTileRef(mapIndices, out var tile))
return null;
return default;
return tile;
}
@@ -149,12 +148,7 @@ namespace Content.Shared.Maps
/// </summary>
public static IEnumerable<IEntity> GetEntitiesInTile(this MapIndices indices, GridId gridId, bool approximate = false, IEntityManager? entityManager = null)
{
var turf = indices.GetTileRef(gridId);
if (turf == null)
return Enumerable.Empty<IEntity>();
return GetEntitiesInTile(turf.Value, approximate, entityManager);
return GetEntitiesInTile(indices.GetTileRef(gridId), approximate, entityManager);
}
/// <summary>
@@ -194,7 +188,9 @@ namespace Content.Shared.Maps
{
var map = IoCManager.Resolve<IMapManager>();
var tileGrid = map.GetGrid(turf.GridIndex);
var tileBox = Box2.UnitCentered.Scale(tileGrid.TileSize);
// This is scaled to 90 % so it doesn't encompass walls on other tiles.
var tileBox = Box2.UnitCentered.Scale(tileGrid.TileSize).Scale(0.9f);
return tileBox.Translated(tileGrid.GridTileToWorldPos(turf.GridIndices));
}

View File

@@ -101,13 +101,13 @@ namespace Content.Shared.Materials
// All default material params are initialized to 1 because
// I'm too lazy to figure out for which that's necessary to prevent divisions by zero in case left out.
serializer.DataField(ref _density, "density", 1, alwaysWrite: true);
serializer.DataField(ref _electricResistivity, "electricresistivity", 1, alwaysWrite: true);
serializer.DataField(ref _thermalConductivity, "thermalconductivity", 1, alwaysWrite: true);
serializer.DataField(ref _specificHeat, "specificheat", 1, alwaysWrite: true);
serializer.DataField(ref _electricResistivity, "electricResistivity", 1, alwaysWrite: true);
serializer.DataField(ref _thermalConductivity, "thermalConductivity", 1, alwaysWrite: true);
serializer.DataField(ref _specificHeat, "specificHeat", 1, alwaysWrite: true);
serializer.DataField(ref _durability, "durability", 1, alwaysWrite: true);
serializer.DataField(ref _hardness, "hardness", 1, alwaysWrite: true);
serializer.DataField(ref _sharpDamage, "sharpdamage", 1, alwaysWrite: true);
serializer.DataField(ref _bluntDamage, "bluntdamage", 1, alwaysWrite: true);
serializer.DataField(ref _sharpDamage, "sharpDamage", 1, alwaysWrite: true);
serializer.DataField(ref _bluntDamage, "bluntDamage", 1, alwaysWrite: true);
serializer.DataField(ref _icon, "icon", SpriteSpecifier.Invalid, alwaysWrite: true);
}
}

View File

@@ -122,9 +122,6 @@ entities:
pos: -0.5,-0.5
rot: -1.5707963267948966 rad
type: Transform
- flags:
- None
type: Destructible
- uid: 2
type: reinforced_wall
components:
@@ -132,9 +129,6 @@ entities:
pos: -0.5,-1.5
rot: -1.5707963267948966 rad
type: Transform
- flags:
- None
type: Destructible
- uid: 3
type: reinforced_wall
components:
@@ -142,9 +136,6 @@ entities:
pos: -0.5,-2.5
rot: -1.5707963267948966 rad
type: Transform
- flags:
- None
type: Destructible
- uid: 4
type: reinforced_wall
components:
@@ -152,9 +143,6 @@ entities:
pos: 0.5,-2.5
rot: -1.5707963267948966 rad
type: Transform
- flags:
- None
type: Destructible
- uid: 5
type: reinforced_wall
components:
@@ -162,9 +150,6 @@ entities:
pos: 1.5,-2.5
rot: -1.5707963267948966 rad
type: Transform
- flags:
- None
type: Destructible
- uid: 6
type: reinforced_wall
components:
@@ -172,9 +157,6 @@ entities:
pos: 1.5,-1.5
rot: -1.5707963267948966 rad
type: Transform
- flags:
- None
type: Destructible
- uid: 7
type: reinforced_wall
components:
@@ -182,9 +164,6 @@ entities:
pos: 1.5,-0.5
rot: -1.5707963267948966 rad
type: Transform
- flags:
- None
type: Destructible
- uid: 8
type: reinforced_wall
components:
@@ -192,7 +171,4 @@ entities:
pos: 0.5,-0.5
rot: -1.5707963267948966 rad
type: Transform
- flags:
- None
type: Destructible
...

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -92,4 +92,4 @@
components:
- type: StorageFill
contents:
- name: CableStack1
- name: ApcExtensionCableStack1

View File

@@ -1,6 +1,6 @@
- type: latheRecipe
id: MetalStack
icon: Objects/Materials/materials.rsi/sheet_metal.png
icon: Objects/Materials/sheets.rsi/metal.png
result: SteelSheet1
completetime: 500
materials:
@@ -8,7 +8,7 @@
- type: latheRecipe
id: GlassStack
icon: Objects/Materials/materials.rsi/sheet_glass.png
icon: Objects/Materials/sheets.rsi/glass.png
result: GlassSheet1
completetime: 500
materials:

View File

@@ -39,8 +39,8 @@
- type: latheRecipe
id: CableStack
name: cable coil
icon: Objects/Tools/cable_coil.png
result: CableStack1
icon: /Textures/Objects/Tools/cables.rsi/coil-30.png
result: ApcExtensionCableStack1
completetime: 500
materials:
steel: 50

View File

@@ -5,7 +5,7 @@
animationDuration: 1.1
spriteName: youtool
startingInventory:
CableStack1: 10
ApcExtensionCableStack1: 10
Crowbar: 5
Welder: 3
Wirecutter: 5

View File

@@ -116,7 +116,7 @@
name: "glass crate"
id: cargo.glass
description: "50 sheets of glass."
icon: Objects/Materials/sheet_glass.png
icon: Objects/Materials/sheets.rsi/glass.png
product: CrateGlass
cost: 50
category: Engineering
@@ -126,7 +126,7 @@
name: "cable crate"
id: cargo.cable
description: "50 coils of cable."
icon: Objects/Tools/cable_coil.png
icon: /Textures/Objects/Tools/cables.rsi/coil-30.png
product: CrateCable
cost: 50
category: Engineering

View File

@@ -21,7 +21,7 @@
- type: Destructible
deadThreshold: 30
destroySound: /Audio/Effects/metalbreak.ogg
spawnOnDestroy: MetalSheet1
spawnOnDestroy: SteelSheet1
resistances: metallicResistances
- type: entity
@@ -47,5 +47,5 @@
- type: Destructible
deadThreshold: 30
destroySound: /Audio/Effects/metalbreak.ogg
spawnOnDestroy: MetalSheet1
spawnOnDestroy: SteelSheet1
resistances: metallicResistances

View File

@@ -24,7 +24,7 @@
- type: Destructible
maxHP: 500
resistances: metallicResistances
spawnondestroy: AMEPart
spawnOnDestroy: AMEPart
- type: SnapGrid
offset: Center
- type: Airtight

View File

@@ -26,7 +26,6 @@
- Impassable
- MobImpassable
- VaultImpassable
IsScrapingFloor: true
- type: Physics
mass: 25
anchored: true
@@ -67,7 +66,6 @@
- Impassable
- MobImpassable
- VaultImpassable
IsScrapingFloor: true
- type: Physics
mass: 25
anchored: true
@@ -75,7 +73,7 @@
offset: Center
- type: Destructible
deadThreshold: 25
spawnOnDestroy: MetalSheet1
spawnOnDestroy: SteelSheet1
- type: UserInterface
interfaces:
- key: enum.ChemMasterUiKey.Key

View File

@@ -23,7 +23,6 @@
- Impassable
- MobImpassable
- VaultImpassable
IsScrapingFloor: true
- type: Physics
mass: 25
anchored: true

View File

@@ -1,15 +1,10 @@
- type: entity
id: ComputerBase
name: computer
abstract: true
id: ComputerFrame
name: computer frame
components:
- type: Physics
mass: 25
anchored: true
- type: Clickable
- type: InteractionOutline
- type: Collidable
IsScrapingFloor: true
mass: 25
anchored: false
shapes:
- !type:PhysShapeAabb
bounds: "-0.5,-0.25,0.5,0.25"
@@ -22,10 +17,77 @@
- Impassable
- MobImpassable
- VaultImpassable
- type: Clickable
- type: InteractionOutline
- type: Anchorable
- type: Construction
graph: computer
node: frameUnsecured
- type: Sprite
sprite: "Constructible/Misc/stock_parts.rsi"
state: "0"
- type: entity
id: ComputerBroken
name: broken computer
description: This computer has seen better days.
abstract: true # We don't want this to show up in the entity spawner menu.
components:
- type: Collidable
mass: 25
anchored: true
shapes:
- !type:PhysShapeAabb
bounds: "-0.5,-0.25,0.5,0.25"
layer:
- Impassable
- MobImpassable
- VaultImpassable
- Opaque
mask:
- Impassable
- MobImpassable
- VaultImpassable
- type: Clickable
- type: InteractionOutline
- type: Anchorable
- type: Construction
graph: computer
node: monitorBroken
- type: Sprite
sprite: "Constructible/Power/computers.rsi"
state: "broken"
- type: entity
id: ComputerBase
name: computer
abstract: true
components:
- type: Clickable
- type: InteractionOutline
- type: Construction
graph: computer
node: computer
- type: Collidable
mass: 25
anchored: true
shapes:
- !type:PhysShapeAabb
bounds: "-0.5,-0.25,0.5,0.25"
layer:
- Impassable
- MobImpassable
- VaultImpassable
- Opaque
mask:
- Impassable
- MobImpassable
- VaultImpassable
- type: Computer
- type: PowerReceiver
- type: Anchorable
- type: BreakableConstruction
node: monitorBroken
- type: Sprite
sprite: Constructible/Power/computers.rsi

View File

@@ -14,7 +14,6 @@
- Impassable
- MobImpassable
- VaultImpassable
IsScrapingFloor: true
- type: Physics
mass: 25
anchored: true
@@ -68,7 +67,7 @@
- Screwdriver
- Welder
- Wrench
- CableStack1
- CableStack
- Crowbar
- Multitool
- type: Appearance
@@ -116,7 +115,7 @@
- Screwdriver
- Welder
- Wrench
- CableStack1
- CableStack
- Crowbar
- Multitool
- type: UserInterface

View File

@@ -25,7 +25,6 @@
- Impassable
- MobImpassable
- VaultImpassable
IsScrapingFloor: true
- type: Physics
mass: 25
anchored: true

View File

@@ -30,7 +30,6 @@
- Impassable
- MobImpassable
- VaultImpassable
IsScrapingFloor: true
- type: Sprite
netsync: false
sprite: Constructible/Power/microwave.rsi

View File

@@ -14,7 +14,6 @@
- Impassable
- MobImpassable
- VaultImpassable
IsScrapingFloor: true
- type: Physics
mass: 25
anchored: true

View File

@@ -42,7 +42,7 @@
wireType: HighVoltage
- type: Destructible
resistances: metallicResistances
spawnondestroy: HVWireStack1
spawnOnDestroy: HVWireStack1
- type: entity
parent: WireBase
@@ -69,7 +69,7 @@
wireType: MediumVoltage
- type: Destructible
resistances: metallicResistances
spawnondestroy: MVWireStack1
spawnOnDestroy: MVWireStack1
- type: entity
parent: WireBase
@@ -98,4 +98,4 @@
wireType: Apc
- type: Destructible
resistances: metallicResistances
spawnondestroy: ApcExtensionCableStack1
spawnOnDestroy: ApcExtensionCableStack1

View File

@@ -33,7 +33,6 @@
- MobImpassable
- VaultImpassable
- SmallImpassable
IsScrapingFloor: true
- type: Physics
mass: 25
anchored: false

View File

@@ -21,7 +21,6 @@
- MobImpassable
- VaultImpassable
- SmallImpassable
IsScrapingFloor: true
- type: Physics
mass: 15
Anchored: false

View File

@@ -26,7 +26,6 @@
- Opaque
- MobImpassable
- SmallImpassable
IsScrapingFloor: true
- type: Physics
mass: 25
anchored: false

View File

@@ -5,6 +5,10 @@
components:
- type: Clickable
- type: InteractionOutline
- type: Anchorable
- type: Construction
graph: girder
node: girder
- type: Sprite
sprite: Constructible/Structures/Walls/solid.rsi
state: wall_girder

View File

@@ -9,6 +9,7 @@
components:
- type: Clickable
- type: RCDDeconstructWhitelist
- type: CanBuildWindowOnTop
- type: InteractionOutline
- type: Sprite
netsync: false
@@ -24,7 +25,8 @@
layer:
- VaultImpassable
- SmallImpassable
- type: Destructible
- type: BreakableConstruction
node: start
deadThreshold: 100
resistances: metallicResistances
- type: SnapGrid
@@ -33,6 +35,9 @@
- type: LowWall
key: walls
base: metal_
- type: Construction
graph: lowWall
node: lowWall
- type: entity
id: LowWallOverlay

View File

@@ -25,10 +25,6 @@
- MobImpassable
- VaultImpassable
- SmallImpassable
- type: Destructible
deadThreshold: 500
spawnOnDestroy: Girder
resistances: metallicResistances
- type: Occluder
sizeX: 32
sizeY: 32
@@ -239,14 +235,20 @@
- type: Icon
sprite: Constructible/Structures/Walls/solid.rsi
state: rgeneric
- type: Destructible
- type: Construction
graph: girder
node: reinforcedWall
- type: BreakableConstruction
deadThreshold: 600
spawnOnDestroy: Girder
node: girder
resistances: metallicResistances
- type: ReinforcedWall
key: walls
base: solid
reinforcedBase: reinf_over
- type: Appearance
visuals:
- type: ReinforcedWallVisualizer
# Riveting
- type: entity
@@ -308,11 +310,14 @@
- type: Sprite
color: "#889192"
sprite: Constructible/Structures/Walls/solid.rsi
- type: Construction
graph: girder
node: wall
- type: Icon
sprite: Constructible/Structures/Walls/solid.rsi
- type: Destructible
- type: BreakableConstruction
deadThreshold: 300
spawnOnDestroy: Girder
node: girder
destroySound: /Audio/Effects/metalbreak.ogg
resistances: metallicResistances
- type: IconSmooth

View File

@@ -34,6 +34,9 @@
- type: Airtight
- type: Window
base: window
- type: Construction
graph: window
node: window
- type: entity
id: ReinforcedWindow
@@ -50,6 +53,9 @@
resistances: metallicResistances
- type: Window
base: rwindow
- type: Construction
graph: window
node: reinforcedWindow
- type: entity
id: PhoronWindow
@@ -66,3 +72,6 @@
resistances: metallicResistances
- type: Window
base: pwindow
- type: Construction
graph: window
node: phoronWindow

View File

@@ -0,0 +1,66 @@
- type: entity
id: BaseComputerCircuitboard
parent: BaseItem
name: circuit board
abstract: true
components:
- type: ComputerBoard
- type: Sprite
sprite: Constructible/Misc/module.rsi
state: id_mod
- type: entity
id: SupplyComputerCircuitboard
parent: BaseComputerCircuitboard
name: supply computer circuit board
components:
- type: ComputerBoard
prototype: ComputerSupplyOrdering
- type: entity
id: SupplyRequestComputerCircuitboard
parent: BaseComputerCircuitboard
name: supply request computer circuit board
components:
- type: ComputerBoard
prototype: ComputerSupplyRequest
- type: entity
id: ResearchComputerCircuitboard
parent: BaseComputerCircuitboard
name: R&D computer circuit board
components:
- type: ComputerBoard
prototype: ComputerResearchAndDevelopment
- type: entity
id: IDComputerCircuitboard
parent: BaseComputerCircuitboard
name: ID card terminal circuit board
components:
- type: ComputerBoard
prototype: ComputerId
- type: entity
id: BodyScannerComputerCircuitboard
parent: BaseComputerCircuitboard
name: body scanner computer circuit board
components:
- type: ComputerBoard
prototype: computerBodyScanner
- type: entity
id: CommsComputerCircuitboard
parent: BaseComputerCircuitboard
name: communications computer circuit board
components:
- type: ComputerBoard
prototype: ComputerComms
- type: entity
id: SolarControlComputerCircuitboard
parent: BaseComputerCircuitboard
name: solar control computer circuit board
components:
- type: ComputerBoard
prototype: ComputerSolarControl

View File

@@ -4,10 +4,10 @@
# ability to have applied colors yet in GUIs. -Swept
- type: entity
id: CableStack
abstract: true
parent: BaseItem
id: CableStack1
name: cable stack 1
name: cable stack
suffix: Full
components:
- type: Stack
@@ -20,9 +20,10 @@
- type: Clickable
- type: entity
parent: CableStack1
id: HVWireStack
parent: CableStack
name: HV Wire Coil
suffix: Full
components:
- type: Sprite
state: coilhv-30
@@ -33,34 +34,6 @@
wirePrototypeID: HVWire
blockingWireType: HighVoltage
- type: entity
parent: CableStack1
id: MVWireStack
name: MV Wire Coil
components:
- type: Sprite
state: coilmv-30
- type: Item
size: 10
HeldPrefix: coilmv
- type: WirePlacer
wirePrototypeID: MVWire
blockingWireType: MediumVoltage
- type: entity
parent: CableStack1
id: ApcExtensionCableStack
name: Apc Extension Cable Coil
components:
- type: Sprite
state: coillv-30
- type: Item
size: 10
HeldPrefix: coillv
- type: WirePlacer
wirePrototypeID: ApcExtensionCable
blockingWireType: Apc
- type: entity
parent: HVWireStack
id: HVWireStack1
@@ -74,16 +47,19 @@
count: 1
- type: entity
parent: MVWireStack
id: MVWireStack1
suffix: 1
parent: CableStack
id: ApcExtensionCableStack
name: Apc Extension Cable Coil
suffix: Full
components:
- type: Sprite
state: coilmv-10
state: coillv-30
- type: Item
size: 3
- type: Stack
count: 1
size: 10
HeldPrefix: coillv
- type: WirePlacer
wirePrototypeID: ApcExtensionCable
blockingWireType: Apc
- type: entity
parent: ApcExtensionCableStack
@@ -96,3 +72,30 @@
size: 3
- type: Stack
count: 1
- type: entity
parent: CableStack
id: MVWireStack
name: MV Wire Coil
suffix: Full
components:
- type: Sprite
state: coilmv-30
- type: Item
size: 10
HeldPrefix: coilmv
- type: WirePlacer
wirePrototypeID: MVWire
blockingWireType: MediumVoltage
- type: entity
parent: MVWireStack
id: MVWireStack1
suffix: 1
components:
- type: Sprite
state: coilmv-10
- type: Item
size: 3
- type: Stack
count: 1

View File

@@ -113,7 +113,6 @@
bounds: "-0.25,-0.25,0.25,0.25"
mask: [Impassable]
layer: [Clickable]
IsScrapingFloor: true
- type: entity
name: recharger
@@ -143,7 +142,6 @@
bounds: "-0.25,-0.25,0.25,0.25"
mask: [Impassable]
layer: [Clickable]
IsScrapingFloor: true
- type: entity
name: wall recharger
@@ -172,4 +170,3 @@
bounds: "-0.25,-0.25,0.25,0.25"
mask: [Impassable]
layer: [Clickable]
IsScrapingFloor: true

View File

@@ -43,7 +43,6 @@
- Opaque
layer:
- Opaque
IsScrapingFloor: true
- type: Physics
mass: 5
anchored: false
@@ -74,7 +73,6 @@
bounds: "-0.25,-0.25,0.25,0.25"
mask:
- Impassable
IsScrapingFloor: true
- type: Physics
mass: 5
anchored: false

View File

@@ -7,8 +7,6 @@
sprite: Objects/Weapons/Melee/spear.rsi
state: spear
- type: MeleeWeapon
range: 1.5
arcwidth: 0
@@ -19,6 +17,10 @@
sprite: Objects/Weapons/Melee/spear.rsi
prefix: inhand
- type: Construction
graph: spear
node: spear
- type: ItemCooldown
- type: MeleeWeaponAnimation

View File

@@ -12,6 +12,7 @@
name: steel sheet
id: MetalStack
parent: MaterialStack
suffix: Full
components:
- type: Material
materials:
@@ -28,16 +29,19 @@
- type: entity
id: SteelSheet1
name: steel sheet 1
name: steel sheet
parent: MetalStack
suffix: 1
components:
- type: Stack
stacktype: enum.StackType.Metal
count: 1
- type: entity
name: glass sheet
id: GlassStack
parent: MaterialStack
suffix: Full
components:
- type: Material
materials:
@@ -54,16 +58,48 @@
- type: entity
id: GlassSheet1
name: glass sheet 1
name: glass sheet
parent: GlassStack
suffix: 1
components:
- type: Stack
stacktype: enum.StackType.Glass
count: 1
- type: entity
name: plasteel sheet
id: PlasteelStack
parent: MaterialStack
suffix: Full
components:
- type: Material
materials:
- key: enum.MaterialKeys.Stack
mat: plasteel
- type: Stack
stacktype: enum.StackType.Plasteel
- type: Sprite
sprite: Objects/Materials/sheets.rsi
state: plasteel
- type: Item
sprite: Objects/Materials/sheets.rsi
HeldPrefix: plasteel
- type: entity
id: PlasteelSheet1
name: plasteel sheet
parent: PlasteelStack
suffix: 1
components:
- type: Stack
stacktype: enum.StackType.Plasteel
count: 1
- type: entity
name: gold bar
id: GoldStack
parent: MaterialStack
suffix: Full
components:
- type: Material
materials:
@@ -79,6 +115,7 @@
id: GoldStack1
name: gold bar 1
parent: GoldStack
suffix: 1
components:
- type: Sprite
sprite: Objects/Materials/materials.rsi
@@ -99,6 +136,7 @@
name: phoron sheet
id: PhoronStack
parent: MaterialStack
suffix: Full
components:
- type: Material
materials:
@@ -115,8 +153,9 @@
- type: entity
id: PhoronStack1
name: phoron sheet 1
name: phoron sheet
parent: PhoronStack
suffix: 1
components:
- type: Stack
count: 1

View File

@@ -15,7 +15,6 @@
bounds: "-0.25,-0.25,0.25,0.25"
layer:
- Clickable
IsScrapingFloor: true
- type: Physics
mass: 5
- type: Sprite

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