Aetheria/Assets/Scripts/ServerShared/Entity.cs

660 lines
25 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JsonKnownTypes;
using MessagePack;
using Newtonsoft.Json;
using Unity.Mathematics;
using static Unity.Mathematics.math;
[MessagePackObject,
Union(0, typeof(Ship)),
Union(1, typeof(OrbitalEntity)),
JsonObject(MemberSerialization.OptIn), JsonConverter(typeof(JsonKnownTypesConverter<Entity>))]
public abstract class Entity : DatabaseEntry, IMessagePackSerializationCallbackReceiver, INamedEntry
{
[JsonProperty("temperature"), Key(1)]
public float Temperature;
[JsonProperty("energy"), Key(2)]
public float Energy;
[JsonProperty("position"), Key(3)]
public float2 Position;
[JsonProperty("direction"), Key(4)]
public float2 Direction = float2(0,1);
[JsonProperty("velocity"), Key(5)]
public float2 Velocity;
[JsonProperty("cargo"), Key(6)]
public readonly HashSet<Guid> Cargo; // Type: ItemInstance
[JsonProperty("gear"), Key(7)]
public readonly List<Guid> Gear;
[JsonProperty("parent"), Key(8)]
public Guid Parent; // Type: Entity
[JsonProperty("children"), Key(9)]
public List<Guid> Children = new List<Guid>(); // Type: Entity
[JsonProperty("hull"), Key(10)]
public Guid Hull; // Type: Gear
[JsonProperty("persistedBehaviorData"), Key(11)]
public Dictionary<Guid, PersistentBehaviorData[]> PersistedBehaviors;
[JsonProperty("corporation"), Key(12)]
public Guid Corporation;
[JsonProperty("name"), Key(13)]
public string Name;
[JsonProperty("population"), Key(14)]
public int Population;
[JsonProperty("personality"), Key(15)]
public Dictionary<Guid, float> Personality = new Dictionary<Guid, float>();
[JsonProperty("incompleteGear"), Key(16)]
public Dictionary<Guid, double> IncompleteGear = new Dictionary<Guid, double>();
//public List<IncompleteItem> IncompleteGear = new List<IncompleteItem>();
[JsonProperty("incompleteCargo"), Key(17)]
public Dictionary<Guid, double> IncompleteCargo = new Dictionary<Guid, double>();
//public List<IncompleteItem> IncompleteCargo = new List<IncompleteItem>();
[IgnoreMember] public Guid Zone;
[IgnoreMember] public List<Hardpoint> Hardpoints;
[IgnoreMember] public readonly Dictionary<object, float> VisibilitySources = new Dictionary<object, float>();
[IgnoreMember] public readonly Dictionary<string, float> Messages = new Dictionary<string, float>();
// [IgnoreMember] public Dictionary<Gear, EquippableItemData> GearData;
// [IgnoreMember] public Dictionary<Gear, List<IBehavior>> ItemBehaviors;
private bool _inventoryChanged;
[IgnoreMember] public float Mass { get; private set; }
[IgnoreMember] public float OccupiedCapacity { get; private set; }
[IgnoreMember] public float ThermalMass { get; private set; }
[IgnoreMember] public float Visibility => VisibilitySources.Values.Sum();
public event Action OnInventoryUpdate;
public float Capacity
{
get
{
var hull = Context.Cache.Get<Gear>(Hull);
var hullData = Context.Cache.Get<HullData>(hull.Data);
return Context.Evaluate(hullData.Capacity, hull, this);
}
}
[IgnoreMember] public string EntryName
{
get => Name;
set => Name = value;
}
public Entity(GameContext context, Guid hull, IEnumerable<Guid> gear, IEnumerable<Guid> cargo, Guid zone, Guid corporation)
{
Context = context;
Zone = zone;
Corporation = corporation;
Gear = gear.ToList();
Cargo = new HashSet<Guid>(cargo);
Hull = hull;
Hydrate();
}
public void Hydrate()
{
//var gearIDs = EquippedItems.Append(Hull);
//var gear = gearIDs.ToDictionary(id => id, id => Context.Cache.Get<Gear>(id));
//GearData = gear.Values.ToDictionary(g => g, g => g.ItemData);
Hardpoints = new List<Hardpoint>();
Hardpoints.Add(new Hardpoint
{
HardpointData = new HardpointData{ Type = HardpointType.Hull }
});
var hull = Context.Cache.Get<Gear>(Hull);
Equip(hull, Hardpoints[0]);
var hullData = Context.Cache.Get<HullData>(hull.Data);
Hardpoints.AddRange(hullData.Hardpoints.Select(hd => new Hardpoint {HardpointData = hd}));
// Create a copy of the gear list and remove every item actually equipped
var remainingGear = new List<Guid>(Gear);
foreach (var gearID in Gear)
if (Equip(gearID))
remainingGear.Remove(gearID);
foreach (var remaining in remainingGear)
{
Context.Log($"Item {Context.Cache.Get<Gear>(remaining).Name} equipped on entity {Name} has no compatible hardpoint, moved to cargo.");
Gear.Remove(remaining);
Cargo.Add(remaining);
}
foreach (var hardpoint in Hardpoints)
if(hardpoint.Gear!=null)
foreach (var behavior in hardpoint.Behaviors
.Where(behavior => behavior is IInitializableBehavior))
((IInitializableBehavior) behavior).Initialize();
RecalculateMass();
}
public void HydrateHardpoint(Hardpoint hardpoint)
{
if (hardpoint.Gear == null)
{
Context.Log("Attempted to hydrate hardpoint with no gear on it!");
return;
}
hardpoint.Behaviors = hardpoint.Gear.ItemData.Behaviors
.Select(bd => bd.CreateInstance(Context, this, hardpoint.Gear))
.ToArray();
hardpoint.BehaviorGroups = hardpoint.Behaviors
.GroupBy(b => b.Data.Group)
.OrderBy(g=>g.Key)
.Select(g=>new BehaviorGroup {
Behaviors = g.ToArray(),
//Axis = (IAnalogBehavior) g.FirstOrDefault(b=>b is IAnalogBehavior),
Switch = (Switch) g.FirstOrDefault(b=>b is Switch),
Trigger = (Trigger) g.FirstOrDefault(b=>b is Trigger)
})
.ToArray();
}
public void Equip(Gear gear, Hardpoint hardpoint)
{
// Hardpoint is occupied, remove existing item from gear list
if (hardpoint.Gear != null)
{
Gear.Remove(hardpoint.Gear.ID);
Cargo.Add(hardpoint.Gear.ID);
}
hardpoint.Gear = gear;
HydrateHardpoint(hardpoint);
_inventoryChanged = true;
}
public bool Equip(Guid gearID, bool force = false)
{
// Check all portions of inventory for a matching item
if (Cargo.Contains(gearID)) Cargo.Remove(gearID);
else if (IncompleteGear.ContainsKey(gearID))
IncompleteGear.Remove(gearID);
else if(!Gear.Contains(gearID))
throw new ArgumentException("Attempted to equip gear which is not present in inventory!");
var gear = Context.Cache.Get<Gear>(gearID);
var gearData = Context.Cache.Get<GearData>(gear.Data);
// Find an empty hardpoint of the correct type
var hardpoint = Hardpoints
.FirstOrDefault(hp => hp.Gear == null && hp.HardpointData.Type == gearData.HardpointType);
// With force option enabled, we'll try to find an empty hardpoint but otherwise any will do
if (hardpoint == null && force)
hardpoint = Hardpoints
.FirstOrDefault(hp => hp.HardpointData.Type == gearData.HardpointType);
// No compatible hardpoint found, return false
if (hardpoint == null)
return false;
Equip(gear, hardpoint);
return true;
}
public CraftedItemInstance AddCargo(CraftedItemInstance item)
{
Cargo.Add(item.ID);
Mass += item.Mass;
ThermalMass += item.ThermalMass;
OccupiedCapacity += item.Size;
_inventoryChanged = true;
return item;
}
public CraftedItemInstance RemoveCargo(CraftedItemInstance item)
{
if(!Cargo.Contains(item.ID))
throw new ArgumentException("Attempted to remove an item which is not in cargo!");
Cargo.Remove(item.ID);
Mass -= item.Mass;
ThermalMass -= item.ThermalMass;
OccupiedCapacity -= item.Size;
_inventoryChanged = true;
return item;
}
public SimpleCommodity AddCargo(SimpleCommodity item)
{
// Regardless of whether a match is found, the item's mass and size are added to the entity
Mass += item.Mass;
ThermalMass += item.ThermalMass;
OccupiedCapacity += item.Size;
// Try to find an matching instance of the same commodity in cargo
var existingCommodityID = Cargo.FirstOrDefault(id =>
Context.Cache.Get<ItemInstance>(id) is SimpleCommodity simpleCommodity &&
simpleCommodity.ItemData == item.ItemData);
// A match was found, delete the original item and increment the quantity of the match
if (existingCommodityID != Guid.Empty)
{
var existingCommodity = Context.Cache.Get<SimpleCommodity>(existingCommodityID);
existingCommodity.Quantity += item.Quantity;
Context.Cache.Delete(item);
return existingCommodity;
}
// No match was found, simply add the item instance to the cargo
Cargo.Add(item.ID);
_inventoryChanged = true;
return item;
}
public SimpleCommodity RemoveCargo(SimpleCommodity item, int quantity)
{
if(!Cargo.Contains(item.ID))
throw new ArgumentException("Attempted to remove an item which is not in cargo!");
// If the quantity matches exactly, huzzah, just remove the instance from cargo
if (item.Quantity == quantity)
Cargo.Remove(item.ID);
else
{
// Quantity does not match, create new item instance and decrement source quantity
var newSimpleCommodity = new SimpleCommodity
{
Context = Context,
Data = item.Data,
Quantity = quantity
};
item.Quantity -= quantity;
item = newSimpleCommodity;
}
Mass -= item.Mass;
ThermalMass -= item.ThermalMass;
OccupiedCapacity -= item.Size;
_inventoryChanged = true;
return item;
}
public void AddChild(Entity entity)
{
Mass += entity.Mass;
ThermalMass += entity.ThermalMass;
Children.Add(entity.ID);
}
public void RemoveChild(Entity entity)
{
Mass -= entity.Mass;
ThermalMass -= entity.ThermalMass;
Children.Remove(entity.ID);
}
public Guid Build(BlueprintData blueprint, float quality, string name, bool direct = false)
{
if (direct)
{
if(blueprint.FactoryItem!=Guid.Empty)
throw new ArgumentException("Attempted to directly build a blueprint which requires a factory!");
var blueprintItem = Context.Cache.Get<GearData>(blueprint.Item);
if(blueprintItem == null)
throw new ArgumentException("Attempted to directly build a blueprint for a missing or incompatible item!");
}
//var newItemID = Guid.Empty;
// GetBlueprintIngredients will assign the output lists with the items matching the blueprint, and return false if they are not found
if (!GetBlueprintIngredients(blueprint, out var simpleIngredients, out var compoundIngredients))
return Guid.Empty;
// These will be bundled with the data for the compound commodity instance
var ingredients = new List<ItemInstance>();
ingredients.AddRange(compoundIngredients.Select(RemoveCargo));
ingredients.AddRange(simpleIngredients.Select(sc => RemoveCargo(sc, blueprint.Ingredients[sc.Data])));
var blueprintItemData = Context.Cache.Get(blueprint.Item);
// Creating new crafted item instances is a bit more complicated
if (blueprintItemData is CraftedItemData)
{
CraftedItemInstance newItem;
if (blueprintItemData is EquippableItemData equippableItemData)
{
var newGear = new Gear
{
Context = Context,
Data = blueprintItemData.ID,
Ingredients = ingredients.Select(ii=>ii.ID).ToList(),
Quality = quality,
Blueprint = blueprint.ID,
Name = name,
SourceEntity = ID
};
newGear.Durability = Context.Evaluate(equippableItemData.Durability, newGear);
newItem = newGear;
}
else
{
newItem = new CompoundCommodity
{
Context = Context,
Data = blueprintItemData.ID,
Ingredients = ingredients.Select(ii=>ii.ID).ToList(),
Quality = quality,
Blueprint = blueprint.ID,
Name = name,
SourceEntity = ID
};
}
Context.Cache.Add(newItem);
if (direct)
IncompleteGear[newItem.ID] = blueprint.ProductionTime;
else
IncompleteCargo[newItem.ID] = blueprint.ProductionTime;
return newItem.ID;
}
var newSimpleCommodity = new SimpleCommodity
{
Context = Context,
Data = blueprintItemData.ID,
Quantity = 1
};
Context.Cache.Add(newSimpleCommodity);
IncompleteCargo[newSimpleCommodity.ID] = blueprint.ProductionTime;
return newSimpleCommodity.ID;
}
public bool GetBlueprintIngredients(BlueprintData blueprint, out List<SimpleCommodity> simpleIngredients,
out List<CompoundCommodity> compoundIngredients)
{
simpleIngredients = new List<SimpleCommodity>();
compoundIngredients = new List<CompoundCommodity>();
var hasAllIngredients = true;
var cargoInstances = Cargo.Select(c => Context.Cache.Get<ItemInstance>(c));
foreach (var kvp in blueprint.Ingredients)
{
var itemData = Context.Cache.Get(kvp.Key);
if (itemData is SimpleCommodityData)
{
var matchingItem = cargoInstances.FirstOrDefault(ii =>
{
if (!(ii is SimpleCommodity simpleCommodity)) return false;
return simpleCommodity.Data == itemData.ID && simpleCommodity.Quantity >= kvp.Value;
}) as SimpleCommodity;
hasAllIngredients = hasAllIngredients && matchingItem != null;
if(matchingItem != null)
simpleIngredients.Add(matchingItem);
}
else
{
var matchingItems =
cargoInstances.Where(ii => (ii as CompoundCommodity)?.Data == itemData.ID).Cast<CompoundCommodity>().ToArray();
hasAllIngredients = hasAllIngredients && matchingItems.Length >= kvp.Value;
if(matchingItems.Length >= kvp.Value)
compoundIngredients.AddRange(matchingItems.Take(kvp.Value));
}
}
return hasAllIngredients;
}
public void RecalculateMass()
{
Mass = Context.Cache.Get<Gear>(Hull).Mass +
Gear.Select(i => Context.Cache.Get<Gear>(i)).Sum(i => i.Mass) +
IncompleteGear.Select(i => Context.Cache.Get<Gear>(i.Key)).Sum(i => i.Mass) +
Cargo.Select(i => Context.Cache.Get<ItemInstance>(i)).Sum(ii => ii.Mass) +
IncompleteCargo.Select(i => Context.Cache.Get<ItemInstance>(i.Key)).Sum(ii => ii.Mass) +
Children.Select(i => Context.Cache.Get<Entity>(i)).Sum(c=>c.Mass);
ThermalMass = Context.Cache.Get<Gear>(Hull).ThermalMass +
Gear.Select(i => Context.Cache.Get<Gear>(i)).Sum(i => i.ThermalMass) +
IncompleteGear.Select(i => Context.Cache.Get<Gear>(i.Key)).Sum(i => i.ThermalMass) +
Cargo.Select(i => Context.Cache.Get<ItemInstance>(i)).Sum(ii => ii.ThermalMass) +
IncompleteCargo.Select(i => Context.Cache.Get<ItemInstance>(i.Key)).Sum(ii => ii.ThermalMass) +
Children.Select(i => Context.Cache.Get<Entity>(i)).Sum(c=>c.ThermalMass);
OccupiedCapacity = Gear.Select(i => Context.Cache.Get<Gear>(i)).Sum(i => i.Size) +
Cargo.Select(i => Context.Cache.Get<ItemInstance>(i)).Sum(ii => ii.Size);
}
public void ClearInventoryListeners()
{
OnInventoryUpdate = null;
}
public T GetBehavior<T>() where T : class, IBehavior
{
foreach (var hardpoint in Hardpoints)
if(hardpoint.Gear!=null)
foreach (var behavior in hardpoint.Behaviors)
if (behavior is T b)
return b;
return null;
}
public IEnumerable<T> GetBehaviors<T>() where T : class, IBehavior
{
foreach (var hardpoint in Hardpoints)
if(hardpoint.Gear!=null)
foreach (var behavior in hardpoint.Behaviors)
if (behavior is T b)
yield return b;
}
public IEnumerable<T> GetBehaviorData<T>() where T : BehaviorData
{
foreach (var hardpoint in Hardpoints)
if(hardpoint.Gear!=null)
foreach (var behavior in hardpoint.Behaviors)
if (behavior.Data is T b)
yield return b;
}
public Switch GetSwitch<T>() where T : class, IBehavior
{
foreach (var hardpoint in Hardpoints)
if(hardpoint.Gear!=null)
foreach (var group in hardpoint.BehaviorGroups)
foreach(var behavior in group.Behaviors)
if (behavior is T)
return group.Switch;
return null;
}
public Trigger GetTrigger<T>() where T : class, IBehavior
{
foreach (var hardpoint in Hardpoints)
if(hardpoint.Gear!=null)
foreach (var group in hardpoint.BehaviorGroups)
foreach(var behavior in group.Behaviors)
if (behavior is T)
return group.Trigger;
return null;
}
public void AddHeat(float heat)
{
if (Parent != Guid.Empty)
{
// var parent = Context.Cache.Get<Entity>(Parent);
// parent.AddHeat(heat);
}
else
Temperature += heat / ThermalMass;
}
public virtual void Update(float delta)
{
foreach (var hardpoint in Hardpoints.Where(hp=>hp.Gear!=null))
{
foreach (var group in hardpoint.BehaviorGroups)
foreach(var behavior in group.Behaviors)
if(!behavior.Update(delta))
break;
foreach (var behavior in hardpoint.Behaviors.Where(b => b is IAlwaysUpdatedBehavior).Cast<IAlwaysUpdatedBehavior>())
behavior.AlwaysUpdate(delta);
if(hardpoint.Gear.ItemData.Performance(Temperature) < .01f)
hardpoint.Gear.Durability -= delta;
}
if (Parent != Guid.Empty)
{
var parent = Context.Cache.Get<Entity>(Parent);
Position = parent.Position;
Velocity = parent.Velocity;
Temperature = parent.Temperature;
}
// Processing stale item data properly was a pain in my ass, so the shortcut is just persisting and restoring everything
// if (Hardpoints.Any(hp => hp.Gear.ItemData != hp.ItemData))
// {
// OnBeforeSerialize();
// OnAfterDeserialize();
// }
foreach (var message in Messages.Keys.ToArray())
{
Messages[message] = Messages[message] - delta;
if (Messages[message] < 0)
Messages.Remove(message);
}
foreach (var incompleteGear in IncompleteGear.Keys.ToArray())
{
IncompleteGear[incompleteGear] = IncompleteGear[incompleteGear] - delta;
if (IncompleteGear[incompleteGear] < 0) Equip(incompleteGear);
}
// Here's the PITA way
// foreach (var staleItem in GearData
// .Where(kvp=>kvp.Key.ItemData!=kvp.Value)
// .Select(kvp=>kvp.Key))
// {
// // Item is stale, there's an updated one in the database cache
// //var actualItem = Context.Cache.Get<Gear>(staleItem.ID);
// var staleBehaviors = ItemBehaviors[staleItem];
// ItemBehaviors.Remove(staleItem);
//
// // We'll be reloading the item along with its behaviors, some data must be persisted
// var persistentData = staleBehaviors
// .Where(b => b is IPersistentBehavior)
// .Cast<IPersistentBehavior>()
// .Select(b => b.Store());
//
// // Regenerate the behavior list for the updated item
// ItemBehaviors[staleItem] = staleItem.ItemData.Behaviors
// .Select(bd => bd.CreateInstance(Context, this, staleItem))
// .OrderBy(b => b.GetType().GetCustomAttribute<UpdateOrderAttribute>()?.Order ?? 0).ToList();
//
// foreach(var behavior in ItemBehaviors[staleItem])
// behavior.Initialize();
//
// // Restore state for persisted behaviors
// foreach(var persistentBehaviorData in ItemBehaviors[staleItem]
// .Where(b => b is IPersistentBehavior)
// .Cast<IPersistentBehavior>()
// .Zip(persistentData, (behavior, data) => new {behavior, data}))
// persistentBehaviorData.behavior.Restore(persistentBehaviorData.data);
//
// // Regenerate activated behavior bindings
// if (Bindings.ContainsKey(staleItem))
// {
// Bindings.Remove(staleItem);
// Bindings[staleItem] = ItemBehaviors[staleItem]
// .Where(b => b is IActivatedBehavior)
// .Cast<IActivatedBehavior>().ToList();
// }
//
// // Remove axis bindings and overrides for analog behaviors
// foreach (var axis in staleBehaviors.Where(b => b is IAnalogBehavior).Cast<IAnalogBehavior>())
// {
// Axes.Remove(axis);
// if (AxisOverrides.ContainsKey(axis))
// AxisOverrides.Remove(axis);
// }
//
// // Regenerate axis bindings for analog behaviors
// foreach (var axis in ItemBehaviors[staleItem].Where(b => b is IAnalogBehavior).Cast<IAnalogBehavior>())
// Axes[axis] = 0;
// }
}
public void SetMessage(string message)
{
Messages[message] = Context.GlobalData.MessageDuration;
}
public void OnBeforeSerialize()
{
// Filter item behavior collections by those with any persistent behaviors
// For each item create an object containing the item ID and a list of persistent behaviors
// Then turn that into a dictionary mapping from item ID to an array of every behaviors persistent data
PersistedBehaviors = Hardpoints
.Where(hp => hp.Behaviors.Any(b=>b is IPersistentBehavior))
.Select(hp => new {gear=hp.Gear, behaviors = hp.Behaviors
.Where(b=>b is IPersistentBehavior)
.Cast<IPersistentBehavior>()})
.ToDictionary(x=> x.gear.ID, x=>x.behaviors.Select(b => b.Store()).ToArray());
}
public void OnAfterDeserialize()
{
Hydrate();
// Iterate only over the behaviors of items which contain persistent data
// Filter the behaviors for each item to get the persistent ones, then cast them and combine with the persisted data array for that item
foreach (var persistentBehaviorData in Hardpoints
.Where(hardpoint => PersistedBehaviors.ContainsKey(hardpoint.Gear.ID)).
SelectMany(hardpoint => hardpoint.Behaviors
.Where(b=> b is IPersistentBehavior)
.Cast<IPersistentBehavior>()
.Zip(PersistedBehaviors[hardpoint.Gear.ID], (behavior, data) => new{behavior, data})))
persistentBehaviorData.behavior.Restore(persistentBehaviorData.data);
}
}
//[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public class Hardpoint
{
public Gear Gear;
public EquippableItemData ItemData;
public HardpointData HardpointData;
public IBehavior[] Behaviors;
public BehaviorGroup[] BehaviorGroups;
}
public class BehaviorGroup
{
public IBehavior[] Behaviors;
public Trigger Trigger;
public Switch Switch;
//public IAnalogBehavior Axis;
}