using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Unity.Mathematics; using static Unity.Mathematics.math; using float2 = Unity.Mathematics.float2; using Random = Unity.Mathematics.Random; using JM.LinqFaster; using UniRx; using float4 = Unity.Mathematics.float4; public class GameContext { public Random Random = new Random((uint) (DateTime.Now.Ticks%uint.MaxValue)); public Dictionary MapLayers = new Dictionary(); public SimpleCommodityData[] Resources; public Dictionary> CorporationControllers = new Dictionary>(); public Dictionary GalaxyZones; private Action _logger; private Dictionary AffectedStats = new Dictionary(); private double _time; private float _deltaTime; private GlobalData _globalData; private Dictionary _zones = new Dictionary(); // private Guid _forceLoadZone; public GlobalData GlobalData => _globalData ?? (_globalData = Cache.GetAll().FirstOrDefault()); public DatabaseCache Cache { get; } // public Guid ForceLoadZone // { // get => _forceLoadZone; // set // { // if(_forceLoadZone != Guid.Empty && ZoneEntities[_forceLoadZone].Count == 0) // UnloadZone(_forceLoadZone); // _forceLoadZone = value; // if(!_loadedZones.Contains(_forceLoadZone)) // LoadZone(_forceLoadZone); // } // } public double Time { get => _time; set { _deltaTime = (float) (value - _time); _time = value; //Log($"GameContext delta time: {_deltaTime}"); } } // private readonly Dictionary Tier = new Dictionary(); public GameContext(DatabaseCache cache, Action logger) { Cache = cache; MapLayers = cache.GetAll().ToDictionary(ml => ml.Name); Resources = cache.GetAll().Where(i => i.ResourceDensity.Any()).ToArray(); _logger = logger; // var globalData = Cache.GetAll().FirstOrDefault(); // if (globalData == null) // { // globalData = new GlobalData(); // Cache.Add(globalData); // } } public void Log(string s) { _logger(s); } // public void Update() // { // foreach(var zone in _zones.Values) // zone.Update((float) Time, _deltaTime); // // foreach (var corporation in Cache.GetAll()) // { // foreach (var tasks in corporation.Tasks // .Select(id => Cache.Get(id)) // Fetch the tasks from the database cache // .Where(task => !task.Reserved) // Filter out tasks that have already been reserved // .GroupBy(task => task.Type)) // Group tasks by type // { // // Create a list of available controllers for this task type // var availableControllers = CorporationControllers[corporation.ID] // .Where(controller => controller.Available && controller.TaskType == tasks.Key).ToList(); // // // Iterate over the highest priority tasks for which controllers are available // foreach (var task in tasks.OrderByDescending(task => task.Priority).Take(availableControllers.Count)) // { // // Find the nearest controller for this task // IController nearestController = availableControllers[0]; // List nearestControllerPath = FindPath(GalaxyZones[availableControllers.First().Zone.Data.ID], GalaxyZones[task.Zone], true); // foreach (var controller in availableControllers.Skip(1)) // { // var path = FindPath(GalaxyZones[controller.Zone.Data.ID], GalaxyZones[task.Zone], true); // if (path.Count < nearestControllerPath.Count) // { // nearestControllerPath = path; // nearestController = controller; // } // } // task.Reserved = true; // nearestController.AssignTask(task.ID); // } // } // } // // } // public Zone GetZone(Guid zone) // { // if (_zones.ContainsKey(zone)) return _zones[zone]; // // // var zoneData = Cache.Get(zone); // // if (zoneData == null) throw new ArgumentException("Zone parameter is not a valid zone ID!", nameof(zone)); // // // // return GetZone(zoneData); // return null; // } // // public Zone GetZone(ZonePack zonePack) // { // var z = new Zone(this, zonePack); // _zones.Add(zonePack.Data.ID, z); // return z; // } // // public void UnloadZone(Guid zone) // { // _zones.Remove(zone); // } // public void Warp(Entity entity, Guid targetZone) // { // if (length(entity.Position - WormholePosition(entity.Zone, targetZone)) < GlobalData.WarpDistance) // { // var sourceZone = entity.Zone; // // // Remove entity from source zone // ZoneEntities[sourceZone].Remove(entity.ID); // // // Unload source zone if empty and unobserved // if(ForceLoadZone != sourceZone && ZoneEntities[sourceZone].Count == 0) // UnloadZone(sourceZone); // // // Load target zone if not yet loaded // if(!_loadedZones.Contains(targetZone)) // LoadZone(targetZone); // // entity.Zone = targetZone; // entity.Position = WormholePosition(targetZone, sourceZone); // ZoneEntities[targetZone][entity.ID] = entity; // } // } public void PlaceMegas() { var megas = Cache.GetAll(); var availableZones = Cache.GetAll() .Where(z => megas.All(m => m.HomeZone != z.ID)) .ToList(); foreach(var megaPlacement in megas .OrderBy(m=>m.PlacementType) .GroupBy(m=>m.PlacementType)) switch (megaPlacement.Key) { case MegaPlacementType.Mass: foreach (var mega in megaPlacement) { var zone = availableZones .MaxBy(z => z.Mass); availableZones.Remove(zone); mega.HomeZone = zone.ID; } break; // case MegaPlacementType.Planets: // foreach (var mega in megaPlacement) // { // var zone = availableZones // .MaxBy(z => z.Planets.Length); // availableZones.Remove(zone); // mega.HomeZone = zone.ID; // } // break; // case MegaPlacementType.Resources: // foreach (var mega in megaPlacement) // { // var zone = availableZones // .MaxBy(z => z.Planets // .SelectMany(id=>Cache.Get(id).Resources // .Select(r=>r.Key)) // .Distinct() // .Count()); // availableZones.Remove(zone); // mega.HomeZone = zone.ID; // } // break; case MegaPlacementType.Connected: foreach (var mega in megaPlacement) { var zone = availableZones .OrderByDescending(z => z.Wormholes.Count) .ThenBy(z=>megas .Sum(m=>m.HomeZone==Guid.Empty?0:lengthsq(Cache.Get(m.HomeZone).Position - z.Position))) .First(); availableZones.Remove(zone); mega.HomeZone = zone.ID; } break; case MegaPlacementType.Isolated: foreach (var mega in megaPlacement) { var zone = availableZones .Where(z=>z.Wormholes.Count==1) .MaxBy(z => { int soloChain = 0; var previous = z; var next = Cache.Get(z.Wormholes.First()); while (next.Wormholes.Count == 2) { var temp = next; next = Cache.Get(next.Wormholes.First(w=>w != previous.ID)); previous = temp; soloChain++; } return soloChain; }); availableZones.Remove(zone); mega.HomeZone = zone.ID; } break; default: throw new ArgumentOutOfRangeException(); } } // public int ItemTier(CraftedItemData itemData) // { // if (Tier.ContainsKey(itemData)) return Tier[itemData]; // // Tier[itemData] = itemData.Ingredients.Keys.Max(ci => _cache.Get(ci) is CraftedItemData craftableIngredient ? ItemTier(craftableIngredient) : 0); // // return Tier[itemData]; // } public ItemData GetData(ItemInstance item) { return Cache.Get(item.Data); } public SimpleCommodityData GetData(SimpleCommodity item) { return Cache.Get(item.Data); } public CraftedItemData GetData(CraftedItemInstance item) { return Cache.Get(item.Data); } public EquippableItemData GetData(Gear gear) { return Cache.Get(gear.Data); } public float GetMass(ItemInstance item) { var data = GetData(item); switch (item) { case CraftedItemInstance _: return data.Mass; case SimpleCommodity commodity: return data.Mass * commodity.Quantity; } return 0; } public float GetSize(ItemInstance item) { var data = GetData(item); switch (item) { case CraftedItemInstance _: return data.Size; case SimpleCommodity commodity: return data.Size * commodity.Quantity; } return 0; } public float GetThermalMass(ItemInstance item) { var data = GetData(item); switch (item) { case CraftedItemInstance _: return data.Mass * data.SpecificHeat; case SimpleCommodity commodity: return data.Mass * data.SpecificHeat * commodity.Quantity; } return 0; } public PerformanceStat GetAffectedStat(BlueprintData blueprint, BlueprintStatEffect effect) { // We've already cached this effect's stat, return it directly if (AffectedStats.ContainsKey(effect)) return AffectedStats[effect]; // Get the data for the item the blueprint is for, return null if not found or not equippable var blueprintItem = Cache.Get(blueprint.Item); if (blueprintItem == null) { _logger($"Attempted to get stat effect but Blueprint {blueprint.ID} is not for an equippable item!"); return AffectedStats[effect] = null; } // Get the first behavior of the type specified in the blueprint stat effect, return null if not found var effectObject = effect.StatReference.Target == blueprintItem.Name ? (object) blueprintItem : blueprintItem.Behaviors.FirstOrDefault(b => b.GetType().Name == effect.StatReference.Target); if (effectObject == null) { _logger($"Attempted to get stat effect for Blueprint {blueprint.ID} but stat references missing behavior \"{effect.StatReference.Target}\"!"); return AffectedStats[effect] = null; } // Get the first field in the behavior matching the name specified in the stat effect, return null if not found var type = effectObject.GetType(); var field = type.GetField(effect.StatReference.Stat); if (field == null) { _logger($"Attempted to get stat effect for Blueprint {blueprint.ID} but object {effect.StatReference.Target} does not have a stat named \"{effect.StatReference.Stat}\"!"); return AffectedStats[effect] = null; } // Finally we've confirmed the stat effect is valid, return the affected stat return AffectedStats[effect] = field.GetValue(effectObject) as PerformanceStat; } private readonly Dictionary ItemQuality = new Dictionary(); public float CompoundQuality(CraftedItemInstance item) { if (ItemQuality.ContainsKey(item)) return ItemQuality[item]; var quality = item.Quality; var craftedIngredients = item.Ingredients.Where(i => i is CraftedItemInstance).ToArray(); if (craftedIngredients.Length > 0) { var ingredientQualityWeight = Cache.Get(item.Data).IngredientQualityWeight; quality = quality * (1 - ingredientQualityWeight) + craftedIngredients.Cast().Average(CompoundQuality) * ingredientQualityWeight; } ItemQuality[item] = quality; return ItemQuality[item]; } // Determine quality of either the item itself or the specific ingredient this stat depends on public float Quality(PerformanceStat stat, Gear item) { var blueprint = Cache.Get(item.Blueprint); var activeEffects = blueprint.StatEffects.Where(x => GetAffectedStat(blueprint, x) == stat).ToArray(); float quality; if (!activeEffects.Any()) quality = CompoundQuality(item); else { var ingredientInstances = item.Ingredients.Select(i => Cache.Get(i)).ToArray(); var ingredients = ingredientInstances.Where(i => activeEffects.Any(e => e.Ingredient == i.Data)).ToArray(); var distinctIngredients = ingredients.Select(i => i.Data).Distinct(); if(distinctIngredients.Count() != activeEffects.Length) { _logger($"Item {item.ID} does not have the ingredients specified by the stat effects of its blueprint!"); return 0; } float sum = 0; foreach (var i in ingredients) { if (i is CraftedItemInstance ci) sum += CompoundQuality(ci); else _logger($"Blueprint stat effect for item {item.ID} specifies invalid (non crafted) ingredient!"); } quality = sum / ingredients.Length; } return quality; } // Returns stat when not equipped public float Evaluate(PerformanceStat stat, Gear item) { var quality = pow(Quality(stat, item), stat.QualityExponent); var result = lerp(stat.Min, stat.Max, quality); if (float.IsNaN(result)) return stat.Min; return result; } // Returns stat using ship temperature and modifiers public float Evaluate(PerformanceStat stat, Gear item, Entity entity) { var itemData = GetData(item); var heat = !stat.HeatDependent ? 1 : pow(itemData.Performance(entity.Temperature), Evaluate(itemData.HeatExponent,item)); var durability = !stat.DurabilityDependent ? 1 : pow(item.Durability / Evaluate(itemData.Durability, item), Evaluate(itemData.DurabilityExponent,item)); var quality = pow(Quality(stat, item), stat.QualityExponent); var scaleModifier = stat.GetScaleModifiers(entity).Values.Aggregate(1.0f, (current, mod) => current * mod); var constantModifier = stat.GetConstantModifiers(entity).Values.Sum(); var result = lerp(stat.Min, stat.Max, heat * durability * quality) * scaleModifier + constantModifier; if (float.IsNaN(result)) return stat.Min; return result; } public Entity CreateEntity(Guid zoneID, Guid corporation, Guid loadout) { if (!(Cache.Get(loadout) is LoadoutData loadoutData)) { _logger("Attempted to spawn invalid loadout ID"); return null; } if (!(Cache.Get(loadoutData.Hull) is HullData hullData)) { _logger("Attempted to spawn loadout with invalid hull ID"); return null; } if (!(Cache.Get(zoneID) is ZoneData zoneData)) { _logger("Attempted to spawn entity with invalid zone ID"); return null; } if (!(Cache.Get(corporation) is Corporation corpData)) { _logger("Attempted to spawn entity with invalid corporation ID"); return null; } var gearData = loadoutData.Gear .Take(hullData.Hardpoints.Count) .Where(id => id != Guid.Empty) .Select(id => Cache.Get(id)) .ToArray(); if (gearData.Any(g=>g==null)) { _logger("Attempted to spawn loadout with invalid gear ID"); return null; } var gear = gearData .Select(gd => CreateInstance(gd.ID, GlobalData.StartingGearQualityMin, GlobalData.StartingGearQualityMax)) .ToArray(); if (gear.Any(g=>g==null)) return null; foreach(var g in gear) Cache.Add(g); var cargoData = loadoutData.CompoundCargo .SelectMany(x => Enumerable.Repeat(Cache.Get(x.Key), x.Value)) .ToArray(); if (cargoData.Any(g=>g==null)) { _logger("Attempted to spawn loadout with invalid cargo ID"); return null; } var cargo = cargoData .Select(gd => CreateInstance(gd.ID, GlobalData.StartingGearQualityMin, GlobalData.StartingGearQualityMax)) .ToArray(); if (cargo.Any(g=>g==null)) return null; foreach(var c in cargo) Cache.Add(c); var simpleCargo = loadoutData.SimpleCargo? .Select(x => CreateInstance(x.Key, x.Value)) .ToArray() ?? new SimpleCommodity[0]; if (simpleCargo.Any(g=>g==null)) return null; var hull = CreateInstance(loadoutData.Hull, GlobalData.StartingGearQualityMin, GlobalData.StartingGearQualityMax); if(hull==null) return null; // Get target zone var zone = _zones[zoneID]; if (hullData.HullType == HullType.Ship) { var entity = CreateShip(hull.ID, gear.Select(g => g.ID), cargo.Select(c => c.ID).Concat(simpleCargo.Select(c => c.ID)), zone, corporation, Guid.Empty, loadoutData.Name); entity.Active = true; return entity; } if (hullData.HullType == HullType.Station) { var distance = lerp(.2f, .8f, Random.NextFloat()) * zoneData.Radius; var orbit = new OrbitData { Context = this, Distance = new ReactiveProperty(distance), Parent = zone.Orbits.Values.Select(o=>o.Data) .First(orbitData => orbitData.Parent == Guid.Empty).ID, Phase = Random.NextFloat() }; zone.AddOrbit(orbit); var entity = new OrbitalEntity(this, hull.ID, gear.Select(g => g.ID), cargo.Select(c => c.ID).Concat(simpleCargo.Select(c => c.ID)), orbit.ID, zone, corporation) { Name = $"{loadoutData.Name} {Random.NextInt(1,255):X}", Temperature = 293, Population = 4 }; entity.Active = true; var parentCorp = Cache.Get(corpData.Parent); foreach (var attribute in parentCorp.Personality) entity.Personality[attribute.Key] = attribute.Value; Cache.Add(entity); zone.Entities[entity.ID] = entity; return entity; } return null; } public Ship CreateShip(Guid hull, IEnumerable gear, IEnumerable cargo, Zone zone, Guid corporation, Guid homeEntity, string typeName) { var ship = new Ship(this, hull, gear, cargo, zone, corporation) { Name = $"{typeName} {Random.NextInt(1,255):X}", HomeEntity = homeEntity }; Cache.Add(ship); zone.Entities[ship.ID] = ship; return ship; } public SimpleCommodity CreateInstance(Guid data, int count) { var item = Cache.Get(data); if (item != null) { var newItem = new SimpleCommodity { Context = this, Data = data, Quantity = count }; Cache.Add(newItem); return newItem; } _logger("Attempted to create Simple Commodity instance using missing or incorrect item id"); return null; } public CraftedItemInstance CreateInstance(Guid data, float qualityMin, float qualityMax) { var item = Cache.Get(data); if (item == null) { _logger("Attempted to create crafted item instance using missing or incorrect item id!"); return null; } var blueprint = Cache.GetAll().FirstOrDefault(b => b.Item == data); if (blueprint == null) { _logger("Attempted to create crafted item instance which has no blueprint!"); return null; } bool invalidIngredientFound = false; ItemData invalidIngredient = null; var ingredients = new List(); foreach (var ingredient in blueprint.Ingredients) { var itemData = Cache.Get(ingredient.Key); if (itemData is SimpleCommodityData) { var itemInstance = CreateInstance(ingredient.Key, ingredient.Value); if(itemInstance == null) { invalidIngredientFound = true; invalidIngredient = itemData; } else ingredients.Add(itemInstance); } else { for (int i = 0; i < ingredient.Value; i++) { var itemInstance = CreateInstance(ingredient.Key, qualityMin, qualityMax); if (itemInstance == null) { invalidIngredientFound = true; invalidIngredient = itemData; break; } ingredients.Add(itemInstance); } } } if (invalidIngredientFound) { _logger($"Unable to create crafted item ingredient: {invalidIngredient?.Name??"null"} for item {item.Name}"); } Cache.AddAll(ingredients); if (item is EquippableItemData equippableItemData) { var newGear = new Gear { Context = this, Data = data, Ingredients = ingredients.Select(i=>i.ID).ToList(), Quality = Random.NextFloat(qualityMin, qualityMax), Blueprint = blueprint.ID, Name = $"{item.Name}" }; newGear.Durability = Evaluate(equippableItemData.Durability, newGear); Cache.Add(newGear); return newGear; } var newCommodity = new CompoundCommodity { Context = this, Data = data, Ingredients = ingredients.Select(i=>i.ID).ToList(), Quality = Random.NextFloat(qualityMin, qualityMax), Blueprint = blueprint.ID, Name = $"{item.Name}" }; Cache.Add(newCommodity); return newCommodity; } class DijkstraNode { public float Cost; public DijkstraNode Parent; public ZoneDefinition Zone; } public List FindPath(ZoneDefinition source, ZoneDefinition target, bool bestFirst = false) { SortedList members = new SortedList{{0,new DijkstraNode{Zone = source}}}; List searched = new List(); while (true) { var s = members.FirstOrDefault(m => !searched.Contains(m.Value)).Value; // Lowest cost unsearched node if (s == null) return null; // No vertices left unsearched if (s.Zone == target) // We found the path { Stack path = new Stack(); // Since we start at the end, use a LIFO collection path.Push(s); while(path.Peek().Parent!=null) // Keep pushing until we reach the start, which has no parent path.Push(path.Peek().Parent); return path.Select(dv => dv.Zone).ToList(); } // For each adjacent star (filter already visited zones unless heuristic is in use) foreach (var dijkstraStar in s.Zone.Links.WhereSelectF(i => !bestFirst || members.All(m => m.Value.Zone != GalaxyZones[i]), // Cost is parent cost plus distance i => new DijkstraNode {Parent = s, Zone = GalaxyZones[i], Cost = s.Cost + length(s.Zone.Position - GalaxyZones[i].Position)})) // Add new member to list, sorted by cost plus optional heuristic members.Add(bestFirst ? dijkstraStar.Cost + length(dijkstraStar.Zone.Position - target.Position) : dijkstraStar.Cost, dijkstraStar); searched.Add(s); } } // public bool MoveCargo(Entity source, Entity target, ItemInstance item, int quantity = int.MaxValue) // { // if (item is CraftedItemInstance craftedItemInstance) // { // var craftedItemData = Cache.Get(craftedItemInstance.Data); // if (target.Capacity - target.OccupiedCapacity > craftedItemData.Size) // { // // Target has the cargo capacity for the item, simply move the instance // source.Cargo.Remove(item.ID); // target.Cargo.Add(item.ID); // source.RecalculateMass(); // target.RecalculateMass(); // return true; // } // return false; // } // // if (item is SimpleCommodity simpleCommoditySource) // { // quantity = min(quantity, simpleCommoditySource.Quantity); // var simpleCommodityData = Cache.Get(simpleCommoditySource.Data); // var spareCapacity = (int) (target.Capacity - target.OccupiedCapacity); // var simpleCommodityTarget = target.Cargo // .Select(i => Cache.Get(i)) // .FirstOrDefault(i => i.Data == simpleCommodityData.ID) as SimpleCommodity; // var success = spareCapacity >= quantity; // quantity = min(quantity, spareCapacity); // if (simpleCommodityTarget == null) // { // if (quantity == simpleCommoditySource.Quantity) // { // // Target has the cargo capacity to hold the full quantity, // // and the target has no matching item instance; // // simply move the existing item instance to the target // source.Cargo.Remove(simpleCommoditySource.ID); // target.Cargo.Add(simpleCommoditySource.ID); // source.RecalculateMass(); // target.RecalculateMass(); // return success; // } // else // { // // Target has the cargo capacity to hold the desired quantity // // and the target has no matching item instance; // // Create a new item instance on the target and decrement the quantity of the source instance // var newSimpleCommodity = new SimpleCommodity // { // Context = this, // Data = simpleCommodityData.ID, // Quantity = simpleCommoditySource.Quantity // }; // Cache.Add(newSimpleCommodity); // target.Cargo.Add(newSimpleCommodity.ID); // simpleCommoditySource.Quantity -= quantity; // source.RecalculateMass(); // target.RecalculateMass(); // return success; // } // } // else // { // if (quantity == simpleCommoditySource.Quantity) // { // // Target has the cargo capacity to hold the full quantity, // // and there is already a matching item instance; // // delete the source entity's item instance and increment the target's quantity // source.Cargo.Remove(simpleCommoditySource.ID); // Cache.Delete(simpleCommoditySource); // simpleCommodityTarget.Quantity += quantity; // source.RecalculateMass(); // target.RecalculateMass(); // return success; // } // else // { // // Target has the cargo capacity to hold the desired quantity, // // and there is already a matching item instance; // // decrement the source's quantity and increment the target's quantity // simpleCommoditySource.Quantity -= quantity; // simpleCommodityTarget.Quantity += quantity; // source.RecalculateMass(); // target.RecalculateMass(); // return success; // } // } // // } // // return false; // } }