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; public class GameContext { public Random Random = new Random((uint) (DateTime.Now.Ticks%uint.MaxValue)); public Dictionary ZonePlanets = new Dictionary(); public Dictionary> ZoneEntities = new Dictionary>(); public Dictionary MapLayers = new Dictionary(); 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 HashSet _loadedZones = new HashSet(); private HashSet _orbits = new HashSet(); private Dictionary _orbitVelocities = new Dictionary(); private Dictionary _orbitPositions = new Dictionary(); private Dictionary _previousOrbitPositions = new Dictionary(); private Dictionary _asteroidPositions = new Dictionary(); private Dictionary _asteroidPositionsUpdated = new Dictionary(); private Guid _forceLoadZone; private Type[] _statObjects; public GlobalData GlobalData => _globalData ?? (_globalData = Cache.GetAll().FirstOrDefault()); public DatabaseCache Cache { get; } public Type[] StatObjects => _statObjects ?? (_statObjects = typeof(BehaviorData).GetAllChildClasses() .Concat(typeof(EquippableItemData).GetAllChildClasses()).ToArray()); 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}"); } } public float2 WormholePosition(Guid zone, Guid other) { var zoneData = Cache.Get(zone); var direction = Cache.Get(other).Position - zoneData.Position; return normalize(direction) * zoneData.Radius * .95f; } // private readonly Dictionary Tier = new Dictionary(); public GameContext(DatabaseCache cache, Action logger) { Cache = cache; _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() { _previousOrbitPositions = _orbitPositions; _orbitPositions = new Dictionary(_orbits.Count); foreach(var orbit in _orbits) { _orbitPositions[orbit] = GetOrbitPosition(orbit); _orbitVelocities[orbit] = _previousOrbitPositions.ContainsKey(orbit) ? (_orbitPositions[orbit] - _previousOrbitPositions[orbit]) / _deltaTime : float2.zero; } foreach (var asteroids in _asteroidPositions.Keys) _asteroidPositionsUpdated[asteroids] = false; 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.TaskType == tasks.Key && controller.Available).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], GalaxyZones[task.Zone], true); foreach (var controller in availableControllers.Skip(1)) { var path = FindPath(GalaxyZones[controller.Zone], GalaxyZones[task.Zone], true); if (path.Count < nearestControllerPath.Count) { nearestControllerPath = path; nearestController = controller; } } task.Reserved = true; nearestController.AssignTask(task.ID, nearestControllerPath); } } } foreach (var kvp in ZoneEntities) { foreach (var entity in kvp.Value.Values) entity.Update(_deltaTime); } } public IEnumerable GetEntities(Guid zone) { return ZoneEntities[zone].Values; } public void LoadZone(Guid zone) { var zoneData = Cache.Get(zone); _loadedZones.Add(zone); foreach (var orbit in zoneData.Orbits) _orbits.Add(orbit); // TODO: Associate planets with stored entities for planetary colonies ZonePlanets[zone] = zoneData.Planets; // TODO: Load stored entities ZoneEntities[zone] = new Dictionary(); } public void UnloadZone(Guid zone) { var zoneData = Cache.Get(zone); _loadedZones.Remove(zone); ZoneEntities.Remove(zone); ZonePlanets.Remove(zone); foreach (var orbit in zoneData.Orbits) _orbits.Remove(orbit); } 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; } } // Determine orbital position recursively, caching parent positions to avoid repeated calculations public float2 GetOrbitPosition(Guid orbit) { // Root orbit is fixed at center if(orbit==Guid.Empty) return float2.zero; if (!_orbitPositions.ContainsKey(orbit)) { var orbitData = Cache.Get(orbit); _orbitPositions[orbit] = GetOrbitPosition(orbitData.Parent) + (orbitData.Period < .01f ? float2.zero : OrbitData.Evaluate((float) frac(Time / -orbitData.Period * GlobalData.OrbitSpeedMultiplier + orbitData.Phase)) * orbitData.Distance); } var position = _orbitPositions[orbit]; if (float.IsNaN(position.x)) { Log("Orbit position is NaN, something went very wrong!"); return float2.zero; } return _orbitPositions[orbit]; } public float2 GetOrbitVelocity(Guid orbit) { return _orbitVelocities.ContainsKey(orbit) ? _orbitVelocities[orbit] : float2.zero; } public float3[] GetAsteroidTransforms(Guid planetDataID) { var planetData = Cache.Get(planetDataID); if (!_asteroidPositions.ContainsKey(planetDataID)) { _asteroidPositions[planetDataID] = new float3[planetData.Asteroids.Length]; _asteroidPositionsUpdated[planetDataID] = false; } if (_asteroidPositionsUpdated[planetDataID]) return _asteroidPositions[planetDataID]; for (var i = 0; i < planetData.Asteroids.Length; i++) { _asteroidPositions[planetDataID][i] = float3( OrbitData.Evaluate((float) frac(Time / -OrbitalPeriod(planetData.Asteroids[i].x) * GlobalData.OrbitSpeedMultiplier + planetData.Asteroids[i].y)) * planetData.Asteroids[i].x, (float) (Time * planetData.Asteroids[i].w % 360.0)); } return _asteroidPositions[planetDataID]; } public OrbitData CreateOrbit(Guid parent, float2 position) { var parentOrbit = Cache.Get(parent); var parentPosition = GetOrbitPosition(parent); var delta = position - parentPosition; var distance = length(delta); var period = OrbitalPeriod(distance); var phase = atan2(delta.y, delta.x) / (PI * 2); var storedPhase = (float) frac(Time / -period * GlobalData.OrbitSpeedMultiplier - phase); var orbit = new OrbitData { Context = this, Distance = distance, Parent = parent, Period = period, Phase = storedPhase, Zone = parentOrbit.Zone }; Cache.Add(orbit); return orbit; } public float OrbitalPeriod(float distance) { return pow(distance, GlobalData.OrbitPeriodExponent) * GlobalData.OrbitPeriodMultiplier; } 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 GetHeatCapacity(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(); if(ingredients.Length != 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 zone, 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(zone) 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.Items .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 cargoData = loadoutData.Items .Skip(hullData.Hardpoints.Count) .Where(id => id != Guid.Empty) .Select(id => Cache.Get(id)) .ToArray(); if (cargoData.Any(g=>g==null)) { _logger("Attempted to spawn loadout with invalid cargo 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 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; foreach(var c in simpleCargo) Cache.Add(c); var hull = CreateInstance(loadoutData.Hull, GlobalData.StartingGearQualityMin, GlobalData.StartingGearQualityMax); if(hull==null) return null; // Load target zone if not yet loaded if (!_loadedZones.Contains(zone)) LoadZone(zone); if (hullData.HullType == HullType.Ship) { var entity = new Ship(this, hull.ID, gear.Select(g => g.ID), cargo.Select(c => c.ID).Concat(simpleCargo.Select(c => c.ID)), zone) { Name = $"{loadoutData.Name} {Random.NextInt(1,255):X}" }; Cache.Add(entity); ZoneEntities[zone][entity.ID] = entity; return entity; } if (hullData.HullType == HullType.Station) { var distance = lerp(.2f, .8f, Random.NextFloat()) * zoneData.Radius; var orbit = new OrbitData { Context = this, Distance = distance, Parent = _orbits .Select(id => Cache.Get(id)) .First(orbitData => orbitData.Zone == zone && orbitData.Parent == Guid.Empty).ID, Period = OrbitalPeriod(distance), Phase = Random.NextFloat(), Zone = zone }; Cache.Add(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) { Name = $"{loadoutData.Name} {Random.NextInt(1,255):X}" }; Cache.Add(entity); ZoneEntities[zone][entity.ID] = entity; return entity; } return null; } public SimpleCommodity CreateInstance(Guid data, int count) { var item = Cache.Get(data); if (item != null) return new SimpleCommodity { Data = data, Quantity = count }; _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; } var ingredients = blueprint.Ingredients.SelectMany(ci => { var ingredient = Cache.Get(ci.Key); return ingredient is SimpleCommodityData ? (IEnumerable) new[] {CreateInstance(ci.Key, ci.Value)} : Enumerable.Range(0, ci.Value).Select(i => CreateInstance(ci.Key, qualityMin, qualityMax)); }) .ToList(); 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 }; 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 }; Cache.Add(newCommodity); return newCommodity; } class DijkstraNode { public float Cost; public DijkstraNode Parent; public SimplifiedZoneData Zone; } public List FindPath(SimplifiedZoneData source, SimplifiedZoneData 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 void SetParent(Entity child, Entity parent) { child.Parent = parent.ID; parent.Children.Add(child.ID); parent.RecalculateMass(); } public void RemoveParent(Entity child) { if (child.Parent == Guid.Empty) return; var parent = Cache.Get(child.Parent); parent.Children.Remove(child.ID); child.Parent = Guid.Empty; } }