Aetheria/Assets/Scripts/ServerShared/EntitySerializer.cs

255 lines
10 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using GameCult.Caching;
using MessagePack;
using CultMath;
public static class EntitySerializer
{
public static EntityPack Pack(Entity entity)
{
EntityPack pack;
if (entity is OrbitalEntity orbital)
pack = new OrbitalEntityPack
{
Orbit = new CultRecordRef<OrbitData>(orbital.OrbitData),
SecurityLevel = orbital.SecurityLevel,
SecurityRadius = orbital.SecurityRadius
};
else if (entity is Ship ship)
pack = new ShipPack
{
Position = ship.Position,
Direction = ship.Direction,
IsPlayerShip = ship.IsPlayerShip
};
else throw new ArgumentException("Attempted to pack an instance of abstract class Entity!");
pack.Settings = entity.Settings;
pack.Boss = entity.IsBoss;
pack.Hull = entity.Hull;
pack.Name = entity.Name;
pack.Faction = entity.Faction == null ? default : entity.ItemManager.ItemData.RefOf(entity.Faction);
pack.Equipment = entity.Equipment.Select(e => (e.Position, e.EquippableItem)).ToArray();
pack.CargoBays = entity.CargoBays.Select(e => (e.Position, e.EquippableItem)).ToArray();
pack.DockingBays = entity.DockingBays.Select(e => (e.Position, e.EquippableItem)).ToArray();
pack.DockingBayAssignments = entity.DockingBays.Select(x => entity.Children.IndexOf(x.DockedShip)).ToArray();
pack.CargoContents = entity.CargoBays.Select(b => b.Cargo.Select(i => (i.Value, i.Key)).ToArray()).ToArray();
pack.DockingBayContents = entity.DockingBays.Select(b => b.Cargo.Select(i => (i.Value, i.Key)).ToArray()).ToArray();
pack.Armor = entity.Armor;
pack.Temperature = entity.Temperature;
pack.Conductivity = entity.HullConductivity;
pack.Children = entity.Children.Select(Pack).ToArray();
pack.WeaponGroups = entity.WeaponGroups.Select(wg => wg.items.Select(item => entity.Equipment.IndexOf(item)).ToArray()).ToArray();
return pack;
}
// A saved stack goes back where it was. The bay refuses what cannot sit in it (a stack below one unit, as the typed
// -5 purchase of old saves left, or above MaxStack), and the refusal is logged, never silent.
private static void RestoreStack(ItemManager itemManager, EquippedCargoBay bay, int2 position, ItemInstance item)
{
if (bay.TryStore(item, position)) return;
var quantity = item is SimpleCommodity commodity ? $" x{commodity.Quantity}" : "";
itemManager.Log($"Dropped a saved item the bay refused at {position}: {itemManager.GetData(item).Name}{quantity}");
}
public static Entity Unpack(ItemManager itemManager, Zone zone, EntityPack pack)
{
return pack switch
{
ShipPack shipPack => Unpack(itemManager, zone, shipPack),
OrbitalEntityPack orbitalEntityPack => Unpack(itemManager, zone, orbitalEntityPack),
_ => null
};
}
private static Ship Unpack(ItemManager itemManager, Zone zone, ShipPack pack)
{
var entity = new Ship(itemManager, zone, pack.Hull, pack.Settings);
Restore(itemManager, zone, pack, entity);
entity.Position = pack.Position;
entity.Direction = pack.Direction;
entity.IsPlayerShip = pack.IsPlayerShip;
return entity;
}
private static OrbitalEntity Unpack(ItemManager itemManager, Zone zone, OrbitalEntityPack pack)
{
var entity = new OrbitalEntity(itemManager, zone, pack.Hull, pack.Orbit.Key, pack.Settings);
Restore(itemManager, zone, pack, entity);
entity.SecurityLevel = pack.SecurityLevel;
entity.SecurityRadius = pack.SecurityRadius;
if (pack.Story >= 0) entity.Story = zone.GalaxyZone.Locations[pack.Story];
return entity;
}
private static void Restore(ItemManager itemManager, Zone zone, EntityPack pack, Entity entity)
{
entity.Name = pack.Name;
entity.IsBoss = pack.Boss;
entity.Faction = itemManager.ItemData.Get(pack.Faction);
entity.Children = pack.Children.Select(c =>
{
var child = Unpack(itemManager, zone, c);
child.Parent = entity;
return child;
}).ToList();
foreach (var (position, item) in pack.Equipment) entity.TryEquip(item, position);
foreach (var (position, item) in pack.CargoBays) entity.TryEquip(item, position);
foreach (var (position, item) in pack.DockingBays) entity.TryEquip(item, position);
for (var i = 0; i < pack.DockingBayAssignments.Length; i++)
{
if (pack.DockingBayAssignments[i] != -1)
entity.DockingBays[i].DockedShip = entity.Children[pack.DockingBayAssignments[i]] as Ship;
}
for (var bayIndex = 0; bayIndex < pack.CargoContents.Length; bayIndex++)
if(entity.CargoBays.Count >= bayIndex + 1)
foreach (var (position, item) in pack.CargoContents[bayIndex])
RestoreStack(itemManager, entity.CargoBays[bayIndex], position, item);
for (var bayIndex = 0; bayIndex < pack.DockingBayContents.Length; bayIndex++)
if(entity.DockingBays.Count >= bayIndex + 1)
foreach (var (position, item) in pack.DockingBayContents[bayIndex])
RestoreStack(itemManager, entity.DockingBays[bayIndex], position, item);
entity.Temperature = pack.Temperature;
entity.Armor = pack.Armor;
var hullData = itemManager.GetData(entity.Hull);
foreach(var v in hullData.Shape.Coordinates)
entity.HullConductivity[v.x,v.y] = pack.Conductivity[v.x,v.y];
entity.WeaponGroups = pack.WeaponGroups.Select(itemIndices =>
{
var items = itemIndices.Select(i => entity.Equipment[i]);
return (items.Select(i => i.GetBehavior<Weapon>()).ToList(), items.ToList());
}).ToArray();
}
// Every item instance an entity pack carries, recursively through its children: hull, equipment, cargo and
// docking bay units, and their stored contents. The provenance ledger's GC root walk.
public static IEnumerable<ItemInstance> Items(EntityPack pack)
{
if (pack == null) throw new InvalidOperationException("This run's save is malformed: an entity is null. Start a new game.");
yield return pack.Hull;
foreach (var (_, item) in pack.Equipment) yield return item;
foreach (var (_, item) in pack.CargoBays) yield return item;
foreach (var (_, item) in pack.DockingBays) yield return item;
foreach (var bay in pack.CargoContents)
foreach (var (_, item) in bay) yield return item;
foreach (var bay in pack.DockingBayContents)
foreach (var (_, item) in bay) yield return item;
foreach (var child in pack.Children)
foreach (var item in Items(child)) yield return item;
}
}
[MessagePackObject]
public class SavedStory
{
[Key(0)] public string StoryJson;
[Key(1)] public string StateJson;
}
[MessagePackObject]
public class ShipPack : EntityPack
{
[Key(17)] public float3 Position;
[Key(18)] public float2 Direction;
[Key(19)] public bool IsPlayerShip;
}
[MessagePackObject]
public class OrbitalEntityPack : EntityPack
{
[Key(17)] public CultRecordRef<OrbitData> Orbit;
[Key(18)] public int Story = -1;
[Key(19)] public SecurityLevel SecurityLevel;
[Key(20)] public float SecurityRadius;
}
[MessagePackObject,
Union(0, typeof(OrbitalEntityPack)),
Union(1, typeof(ShipPack))]
public abstract class EntityPack
{
[Key(0)] public string Name;
[Key(1)] public EquippableItem Hull;
[Key(2)] public (int2 position, EquippableItem item)[] Equipment;
[Key(3)] public (int2 position, EquippableItem item)[] CargoBays;
[Key(4)] public (int2 position, EquippableItem item)[] DockingBays;
// Key(5) retired 2026-09-17; a deleted field used it. Do not reuse.
[Key(6)] public float[,] Temperature;
[Key(7)] public float[,] Armor;
[Key(8)] public bool2[,] Conductivity;
[Key(10)] public int[] DockingBayAssignments;
[Key(11)] public (int2 position, ItemInstance item)[][] CargoContents;
[Key(12)] public (int2 position, ItemInstance item)[][] DockingBayContents;
[Key(13)] public EntityPack[] Children;
[Key(14)] public EntitySettings Settings;
[Key(15)] public CultRecordRef<Faction> Faction;
[Key(16)] public int[][] WeaponGroups;
// Keys 17-20 belong to the subclasses; the base's next free key is 21.
[Key(21)] public bool Boss;
private int _price;
public int Price(ItemManager itemManager)
{
if (_price != 0) return _price;
var hullData = itemManager.GetData(Hull);
_price = hullData.Price;
foreach (var (_, item) in Equipment)
{
var itemData = itemManager.GetData(item);
_price += itemData.Price;
}
foreach (var (_, item) in CargoBays)
{
var itemData = itemManager.GetData(item);
_price += itemData.Price;
}
foreach (var (_, item) in DockingBays)
{
var itemData = itemManager.GetData(item);
_price += itemData.Price;
}
foreach (var t in CargoContents)
{
foreach (var (_, item) in t)
{
var itemData = itemManager.GetData(item);
if (item is SimpleCommodity s)
_price += itemData.Price * s.Quantity;
else
_price += itemData.Price;
}
}
foreach (var t in DockingBayContents)
{
foreach (var (_, item) in t)
{
var itemData = itemManager.GetData(item);
if (item is SimpleCommodity s)
_price += itemData.Price * s.Quantity;
else
_price += itemData.Price;
}
}
return _price;
}
}