Upgradeable machines. (#2675)

* Start work on upgradeable machines.

* Upgradeable machines work

* Component requirements for upgradeable machines

* Better container handling

* Remember to not push submodule updates in your PRs, kids!

* Refresh parts after building a machine.

* NetSync false

* Address some reviews, fix some bugs

* Nullable stackhelpers dependencies

* Use container helper method to delete all entities in containers

* Nullable string in AddContainer

* Better examine for machine frame and construction in general

* Machine breakage

* Nullable node

* nullable GraphPrototype

* Re-save saltern for autolathe parts

* Fix SaveLoadSave
This commit is contained in:
Vera Aguilera Puerto
2020-12-03 22:49:00 +01:00
committed by GitHub
parent ba2bdec13b
commit c3341132c5
36 changed files with 5270 additions and 3703 deletions

View File

@@ -48,10 +48,10 @@ namespace Content.Server.GameObjects.Components.Construction
private ConstructionGraphNode? _target = null;
[ViewVariables]
public ConstructionGraphPrototype GraphPrototype { get; private set; } = null!;
public ConstructionGraphPrototype? GraphPrototype { get; private set; }
[ViewVariables]
public ConstructionGraphNode Node { get; private set; } = null!;
public ConstructionGraphNode? Node { get; private set; } = null;
[ViewVariables]
public ConstructionGraphEdge? Edge { get; private set; } = null;
@@ -88,6 +88,17 @@ namespace Content.Server.GameObjects.Components.Construction
serializer.DataField(ref _startingNodeIdentifier, "node", string.Empty);
}
/// <summary>
/// Attempts to set a new pathfinding target.
/// </summary>
public void SetNewTarget(string node)
{
if (GraphPrototype != null && GraphPrototype.Nodes.TryGetValue(node, out var target))
{
Target = target;
}
}
public void ClearTarget()
{
_target = null;
@@ -97,8 +108,8 @@ namespace Content.Server.GameObjects.Components.Construction
public void UpdateTarget()
{
// Can't pathfind without a target.
if (Target == null) return;
// Can't pathfind without a target or no node.
if (Target == null || Node == null || GraphPrototype == null) return;
// If we're at our target, stop pathfinding.
if (Target == Node)
@@ -133,7 +144,7 @@ namespace Content.Server.GameObjects.Components.Construction
}
// Let's set the next target edge.
if (Edge == null && TargetNextEdge == null)
if (Edge == null && TargetNextEdge == null && TargetPathfinding != null)
TargetNextEdge = Node.GetEdge(TargetPathfinding.Peek().Name);
}
@@ -161,6 +172,8 @@ namespace Content.Server.GameObjects.Components.Construction
{
EdgeStep = 0;
if (Node == null || GraphPrototype == null) return false;
foreach (var edge in Node.Edges)
{
if(edge.Steps.Count == 0)
@@ -344,7 +357,7 @@ namespace Content.Server.GameObjects.Components.Construction
private async Task<bool> HandleCompletion(ConstructionGraphEdge edge, IEntity user)
{
if (edge.Steps.Count != EdgeStep)
if (edge.Steps.Count != EdgeStep || GraphPrototype == null)
{
return false;
}
@@ -380,6 +393,16 @@ namespace Content.Server.GameObjects.Components.Construction
return true;
}
public void ResetEdge()
{
_edgeNestedStepProgress = null;
TargetNextEdge = null;
Edge = null;
EdgeStep = 0;
UpdateTarget();
}
private async Task<bool> HandleEdge(InteractUsingEventArgs eventArgs)
{
if (Edge == null || EdgeStep >= Edge.Steps.Count) return false;
@@ -389,7 +412,8 @@ namespace Content.Server.GameObjects.Components.Construction
private async Task<bool> HandleEntityChange(ConstructionGraphNode node, IEntity? user = null)
{
if (node.Entity == Owner.Prototype?.ID || string.IsNullOrEmpty(node.Entity)) return false;
if (node.Entity == Owner.Prototype?.ID || string.IsNullOrEmpty(node.Entity)
|| GraphPrototype == null) return false;
var entity = Owner.EntityManager.SpawnEntity(node.Entity, Owner.Transform.Coordinates);
@@ -398,7 +422,7 @@ namespace Content.Server.GameObjects.Components.Construction
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}!");
throw new Exception($"New entity {node.Entity}'s graph {construction.GraphPrototype?.ID ?? null} isn't the same as our graph {GraphPrototype.ID} on node {node.Name}!");
construction.Node = node;
construction.Target = Target;
@@ -461,14 +485,6 @@ namespace Content.Server.GameObjects.Components.Construction
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
{
@@ -481,8 +497,25 @@ namespace Content.Server.GameObjects.Components.Construction
}
}
protected override void Startup()
{
base.Startup();
if (Node == null) return;
foreach (var action in Node.Actions)
{
action.PerformAction(Owner, null);
if (Owner.Deleted)
return;
}
}
public async Task ChangeNode(string node)
{
if (GraphPrototype == null) return;
var graphNode = GraphPrototype.Nodes[node];
if (_handling && _handlingTask?.Task != null)
@@ -508,21 +541,28 @@ namespace Content.Server.GameObjects.Components.Construction
if (Edge == null && TargetNextEdge != null)
{
var preventStepExamine = false;
foreach (var condition in TargetNextEdge.Conditions)
{
condition.DoExamine(Owner, message, inDetailsRange);
preventStepExamine |= condition.DoExamine(Owner, message, inDetailsRange);
}
TargetNextEdge.Steps[0].DoExamine(message, inDetailsRange);
if(!preventStepExamine)
TargetNextEdge.Steps[0].DoExamine(message, inDetailsRange);
return;
}
if (Edge != null)
{
var preventStepExamine = false;
foreach (var condition in Edge.Conditions)
{
condition.DoExamine(Owner, message, inDetailsRange);
preventStepExamine |= condition.DoExamine(Owner, message, inDetailsRange);
}
if (preventStepExamine) return;
}
if (_edgeNestedStepProgress == null)

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using Content.Server.Construction;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Construction
{
[RegisterComponent]
public class MachineBoardComponent : Component, IExamine
{
public override string Name => "MachineBoard";
[ViewVariables]
private Dictionary<MachinePart, int> _requirements;
[ViewVariables]
private Dictionary<StackType, int> _materialRequirements;
[ViewVariables]
private Dictionary<string, ComponentPartInfo> _componentRequirements;
/// <summary>
/// So, what happens if you spawn a machine from the entity spawning menu?
/// It should probably have all parts, including the component parts...
/// This is where this fancy little dictionary comes in!
/// This maps component name types to entity prototype IDs to be used as defaults.
/// </summary>
[ViewVariables]
private Dictionary<string, string> _componentDefaults;
[ViewVariables(VVAccess.ReadWrite)]
public string Prototype { get; private set; }
public IReadOnlyDictionary<MachinePart, int> Requirements => _requirements;
public IReadOnlyDictionary<StackType, int> MaterialRequirements => _materialRequirements;
public IReadOnlyDictionary<string, ComponentPartInfo> ComponentRequirements => _componentRequirements;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.Prototype, "prototype", null);
serializer.DataField(ref _requirements, "requirements", new Dictionary<MachinePart, int>());
serializer.DataField(ref _materialRequirements, "materialRequirements", new Dictionary<StackType, int>());
serializer.DataField(ref _componentRequirements, "componentRequirements", new Dictionary<string, ComponentPartInfo>());
}
public void Examine(FormattedMessage message, bool inDetailsRange)
{
message.AddMarkup(Loc.GetString("Requires:\n"));
foreach (var (part, amount) in Requirements)
{
message.AddMarkup(Loc.GetString("[color=yellow]{0}x[/color] [color=green]{1}[/color]\n", amount, Loc.GetString(part.ToString())));
}
foreach (var (material, amount) in MaterialRequirements)
{
message.AddMarkup(Loc.GetString("[color=yellow]{0}x[/color] [color=green]{1}[/color]\n", amount, Loc.GetString(material.ToString())));
}
foreach (var (_, info) in ComponentRequirements)
{
message.AddMarkup(Loc.GetString("[color=yellow]{0}x[/color] [color=green]{1}[/color]\n", info.Amount, Loc.GetString(info.ExamineName)));
}
}
}
[Serializable]
public struct ComponentPartInfo
{
public int Amount;
public string ExamineName;
public string DefaultPrototype;
}
}

View File

@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.Construction;
using Content.Server.Interfaces.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Server.GameObjects.Components.Construction
{
[RegisterComponent]
public class MachineComponent : Component, IMapInit
{
public override string Name => "Machine";
public string BoardPrototype { get; private set; }
private Container _boardContainer;
private Container _partContainer;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.BoardPrototype, "board", null);
}
public override void Initialize()
{
base.Initialize();
_boardContainer = ContainerManagerComponent.Ensure<Container>(MachineFrameComponent.BoardContainer, Owner);
_partContainer = ContainerManagerComponent.Ensure<Container>(MachineFrameComponent.PartContainer, Owner);
}
protected override void Startup()
{
base.Startup();
CreateBoardAndStockParts();
}
public IEnumerable<MachinePartComponent> GetAllParts()
{
foreach (var entity in _partContainer.ContainedEntities)
{
if (entity.TryGetComponent<MachinePartComponent>(out var machinePart))
yield return machinePart;
}
}
public void RefreshParts()
{
foreach (var refreshable in Owner.GetAllComponents<IRefreshParts>())
{
refreshable.RefreshParts(GetAllParts());
}
}
public void CreateBoardAndStockParts()
{
// Entity might not be initialized yet.
var boardContainer = ContainerManagerComponent.Ensure<Container>(MachineFrameComponent.BoardContainer, Owner, out var existedBoard);
var partContainer = ContainerManagerComponent.Ensure<Container>(MachineFrameComponent.PartContainer, Owner, out var existedParts);
if (string.IsNullOrEmpty(BoardPrototype))
return;
var entityManager = Owner.EntityManager;
if (existedBoard || existedParts)
{
// We're done here, let's suppose all containers are correct just so we don't screw SaveLoadSave.
if (boardContainer.ContainedEntities.Count > 0)
return;
}
var board = entityManager.SpawnEntity(BoardPrototype, Owner.Transform.Coordinates);
if (!_boardContainer.Insert(board))
{
throw new Exception($"Couldn't insert board with prototype {BoardPrototype} to machine with prototype {Owner.Prototype?.ID ?? "N/A"}!");
}
if (!board.TryGetComponent<MachineBoardComponent>(out var machineBoard))
{
throw new Exception($"Entity with prototype {BoardPrototype} doesn't have a {nameof(MachineBoardComponent)}!");
}
foreach (var (part, amount) in machineBoard.Requirements)
{
for (var i = 0; i < amount; i++)
{
var p = entityManager.SpawnEntity(MachinePartComponent.Prototypes[part], Owner.Transform.Coordinates);
if (!partContainer.Insert(p))
throw new Exception($"Couldn't insert machine part of type {part} to machine with prototype {Owner.Prototype?.ID ?? "N/A"}!");
}
}
foreach (var (stackType, amount) in machineBoard.MaterialRequirements)
{
var s = StackHelpers.SpawnStack(stackType, amount, Owner.Transform.Coordinates);
if (!partContainer.Insert(s))
throw new Exception($"Couldn't insert machine material of type {stackType} to machine with prototype {Owner.Prototype?.ID ?? "N/A"}");
}
foreach (var (compName, info) in machineBoard.ComponentRequirements)
{
for (var i = 0; i < info.Amount; i++)
{
var c = entityManager.SpawnEntity(info.DefaultPrototype, Owner.Transform.Coordinates);
if(!partContainer.Insert(c))
throw new Exception($"Couldn't insert machine component part with default prototype '{compName}' to machine with prototype {Owner.Prototype?.ID ?? "N/A"}");
}
}
}
public void MapInit()
{
CreateBoardAndStockParts();
}
}
}

View File

@@ -0,0 +1,301 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Construction;
using Content.Server.GameObjects.Components.Stack;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Construction;
using Content.Shared.GameObjects.Components.Power;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Construction
{
[RegisterComponent]
public class MachineFrameComponent : Component, IInteractUsing
{
[Dependency] private IComponentFactory _componentFactory = default!;
public const string PartContainer = "machine_parts";
public const string BoardContainer = "machine_board";
public override string Name => "MachineFrame";
[ViewVariables]
public bool IsComplete
{
get
{
if (!HasBoard || Requirements == null || MaterialRequirements == null)
return false;
foreach (var (part, amount) in Requirements)
{
if (_progress[part] < amount)
return false;
}
foreach (var (type, amount) in MaterialRequirements)
{
if (_materialProgress[type] < amount)
return false;
}
foreach (var (compName, info) in ComponentRequirements)
{
if (_componentProgress[compName] < info.Amount)
return false;
}
return true;
}
}
[ViewVariables]
public bool HasBoard => _boardContainer?.ContainedEntities.Count != 0;
[ViewVariables]
private Dictionary<MachinePart, int> _progress;
[ViewVariables]
private Dictionary<StackType, int> _materialProgress;
[ViewVariables]
private Dictionary<string, int> _componentProgress;
[ViewVariables]
private Container _boardContainer;
[ViewVariables]
private Container _partContainer;
[ViewVariables]
public IReadOnlyDictionary<MachinePart, int> Requirements { get; private set; }
[ViewVariables]
public IReadOnlyDictionary<StackType, int> MaterialRequirements { get; private set; }
[ViewVariables]
public IReadOnlyDictionary<string, ComponentPartInfo> ComponentRequirements { get; private set; }
public IReadOnlyDictionary<MachinePart, int> Progress => _progress;
public IReadOnlyDictionary<StackType, int> MaterialProgress => _materialProgress;
public IReadOnlyDictionary<string, int> ComponentProgress => _componentProgress;
public override void Initialize()
{
base.Initialize();
_boardContainer = ContainerManagerComponent.Ensure<Container>(BoardContainer, Owner);
_partContainer = ContainerManagerComponent.Ensure<Container>(PartContainer, Owner);
}
protected override void Startup()
{
base.Startup();
RegenerateProgress();
if (Owner.TryGetComponent<ConstructionComponent>(out var construction))
{
// Attempt to set pathfinding to the machine node...
construction.SetNewTarget("machine");
}
}
private void ResetProgressAndRequirements(MachineBoardComponent machineBoard)
{
Requirements = machineBoard.Requirements;
MaterialRequirements = machineBoard.MaterialRequirements;
ComponentRequirements = machineBoard.ComponentRequirements;
_progress = new Dictionary<MachinePart, int>();
_materialProgress = new Dictionary<StackType, int>();
_componentProgress = new Dictionary<string, int>();
foreach (var (machinePart, _) in Requirements)
{
_progress[machinePart] = 0;
}
foreach (var (stackType, _) in MaterialRequirements)
{
_materialProgress[stackType] = 0;
}
foreach (var (compName, _) in ComponentRequirements)
{
_componentProgress[compName] = 0;
}
}
public void RegenerateProgress()
{
AppearanceComponent appearance;
if (!HasBoard)
{
if (Owner.TryGetComponent(out appearance))
{
appearance.SetData(MachineFrameVisuals.State, 1);
}
Requirements = null;
MaterialRequirements = null;
ComponentRequirements = null;
_progress = null;
_materialProgress = null;
_componentProgress = null;
return;
}
var board = _boardContainer.ContainedEntities[0];
if (!board.TryGetComponent<MachineBoardComponent>(out var machineBoard))
return;
if (Owner.TryGetComponent(out appearance))
{
appearance.SetData(MachineFrameVisuals.State, 2);
}
ResetProgressAndRequirements(machineBoard);
foreach (var part in _partContainer.ContainedEntities)
{
if (part.TryGetComponent<MachinePartComponent>(out var machinePart))
{
// Check this is part of the requirements...
if (!Requirements.ContainsKey(machinePart.PartType))
continue;
if (!_progress.ContainsKey(machinePart.PartType))
_progress[machinePart.PartType] = 1;
else
_progress[machinePart.PartType]++;
}
if (part.TryGetComponent<StackComponent>(out var stack))
{
var type = (StackType) stack.StackType;
// Check this is part of the requirements...
if (!MaterialRequirements.ContainsKey(type))
continue;
if (!_materialProgress.ContainsKey(type))
_materialProgress[type] = 1;
else
_materialProgress[type]++;
}
// I have many regrets.
foreach (var (compName, amount) in ComponentRequirements)
{
var registration = _componentFactory.GetRegistration(compName);
if (!part.HasComponent(registration.Type))
continue;
if (!_componentProgress.ContainsKey(compName))
_componentProgress[compName] = 1;
else
_componentProgress[compName]++;
}
}
}
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!HasBoard && eventArgs.Using.TryGetComponent<MachineBoardComponent>(out var machineBoard))
{
if (eventArgs.Using.TryRemoveFromContainer())
{
// Valid board!
_boardContainer.Insert(eventArgs.Using);
// Setup requirements and progress...
ResetProgressAndRequirements(machineBoard);
if (Owner.TryGetComponent<AppearanceComponent>(out var appearance))
{
appearance.SetData(MachineFrameVisuals.State, 2);
}
if (Owner.TryGetComponent(out ConstructionComponent construction))
{
// So prying the components off works correctly.
construction.ResetEdge();
}
return true;
}
}
else if (HasBoard)
{
if (eventArgs.Using.TryGetComponent<MachinePartComponent>(out var machinePart))
{
if (!Requirements.ContainsKey(machinePart.PartType))
return false;
if (_progress[machinePart.PartType] != Requirements[machinePart.PartType]
&& eventArgs.Using.TryRemoveFromContainer() && _partContainer.Insert(eventArgs.Using))
{
_progress[machinePart.PartType]++;
return true;
}
}
if (eventArgs.Using.TryGetComponent<StackComponent>(out var stack))
{
var type = (StackType) stack.StackType;
if (!MaterialRequirements.ContainsKey(type))
return false;
if (_materialProgress[type] == MaterialRequirements[type])
return false;
var needed = MaterialRequirements[type] - _materialProgress[type];
var count = stack.Count;
if (count < needed && stack.Split(count, Owner.Transform.Coordinates, out var newStack))
{
_materialProgress[type] += count;
return true;
}
if (!stack.Split(needed, Owner.Transform.Coordinates, out newStack))
return false;
if(!_partContainer.Insert(newStack))
return false;
_materialProgress[type] += needed;
return true;
}
foreach (var (compName, info) in ComponentRequirements)
{
if (_componentProgress[compName] >= info.Amount)
continue;
var registration = _componentFactory.GetRegistration(compName);
if (!eventArgs.Using.HasComponent(registration.Type))
continue;
if (!eventArgs.Using.TryRemoveFromContainer() || !_partContainer.Insert(eventArgs.Using)) continue;
_componentProgress[compName]++;
return true;
}
}
return false;
}
}
}

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
using Content.Server.Construction;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Construction
{
[RegisterComponent]
public class MachinePartComponent : Component, IExamine
{
// I'm so sorry for hard-coding this. But trust me, it should make things less painful.
public static IReadOnlyDictionary<MachinePart, string> Prototypes { get; } = new Dictionary<MachinePart, string>()
{
{MachinePart.Capacitor, "CapacitorStockPart"},
{MachinePart.ScanningModule, "ScanningModuleStockPart"},
{MachinePart.Manipulator, "MicroManipulatorStockPart"},
{MachinePart.Laser, "MicroLaserStockPart"},
{MachinePart.MatterBin, "MatterBinStockPart"},
{MachinePart.Ansible, "AnsibleSubspaceStockPart"},
{MachinePart.Filter, "FilterSubspaceStockPart"},
{MachinePart.Amplifier, "AmplifierSubspaceStockPart"},
{MachinePart.Treatment, "TreatmentSubspaceStockPart"},
{MachinePart.Analyzer, "AnalyzerSubspaceStockPart"},
{MachinePart.Crystal, "CrystalSubspaceStockPart"},
{MachinePart.Transmitter, "TransmitterSubspaceStockPart"}
};
public override string Name => "MachinePart";
[ViewVariables]
public MachinePart PartType { get; private set; }
[ViewVariables(VVAccess.ReadWrite)]
public int Rating { get; private set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, x => x.PartType, "part", MachinePart.Capacitor);
serializer.DataField(this, x => x.Rating, "rating", 1);
}
public void Examine(FormattedMessage message, bool inDetailsRange)
{
message.AddMarkup(Loc.GetString("[color=white]Rating:[/color] [color=cyan]{0}[/color]\n", Rating));
message.AddMarkup(Loc.GetString("[color=white]Type:[/color] [color=cyan]{0}[/color]\n", PartType));
}
}
}