You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
512 lines
16 KiB
512 lines
16 KiB
using System.Text; |
|
using System.Text.Json; |
|
using System.Text.Json.Serialization; |
|
using Model.BuildOrders; |
|
using Model.Entity; |
|
using Model.Entity.Data; |
|
using Model.Feedback; |
|
using Model.Types; |
|
using YamlDotNet.Serialization; |
|
|
|
namespace Services.Immortal; |
|
|
|
public class BuildOrderService : IBuildOrderService |
|
{ |
|
private readonly BuildOrderModel _buildOrder = new(); |
|
|
|
private readonly ITimingService _timingService; |
|
|
|
private readonly IToastService _toastService; |
|
|
|
private int _lastInterval; |
|
|
|
public BuildOrderService(IToastService toastService, ITimingService timingService) |
|
{ |
|
_toastService = toastService; |
|
_timingService = timingService; |
|
|
|
Reset(); |
|
} |
|
|
|
public Dictionary<int, List<EntityModel>> StartedOrders => _buildOrder.StartedOrders; |
|
public Dictionary<int, List<EntityModel>> CompletedOrders => _buildOrder.CompletedOrders; |
|
public Dictionary<string, int> UniqueCompletedTimes => _buildOrder.UniqueCompletedTimes; |
|
public Dictionary<int, int> SupplyCountTimes => _buildOrder.SupplyCountTimes; |
|
|
|
public int GetLastRequestInterval() |
|
{ |
|
return _lastInterval; |
|
} |
|
|
|
public Dictionary<int, List<EntityModel>> GetOrders() |
|
{ |
|
return _buildOrder.StartedOrders; |
|
} |
|
|
|
public void Subscribe(Action action) |
|
{ |
|
OnChange += action; |
|
} |
|
|
|
public void Unsubscribe(Action action) |
|
{ |
|
OnChange -= action; |
|
} |
|
|
|
|
|
public void Add(EntityModel entity, int atInterval) |
|
{ |
|
if (!_buildOrder.StartedOrders.ContainsKey(atInterval)) |
|
_buildOrder.StartedOrders.Add(atInterval, new List<EntityModel>()); |
|
|
|
var production = entity.Production(); |
|
|
|
var completedTime = atInterval; |
|
if (production != null) completedTime += production.BuildTime; |
|
|
|
if (!_buildOrder.CompletedOrders.ContainsKey(completedTime)) |
|
_buildOrder.CompletedOrders.Add(completedTime, new List<EntityModel>()); |
|
|
|
_buildOrder.StartedOrders[atInterval].Add(entity.Clone()); |
|
_buildOrder.CompletedOrders[completedTime].Add(entity.Clone()); |
|
|
|
if (!_buildOrder.UniqueCompletedTimes.ContainsKey(entity.DataType)) |
|
_buildOrder.UniqueCompletedTimes.Add(entity.DataType, atInterval); |
|
|
|
if (!_buildOrder.UniqueCompletedCount.ContainsKey(entity.DataType)) |
|
_buildOrder.UniqueCompletedCount.Add(entity.DataType, 1); |
|
else |
|
_buildOrder.UniqueCompletedCount[entity.DataType]++; |
|
|
|
if (!_buildOrder.UniqueCompleted.ContainsKey(entity.DataType)) |
|
_buildOrder.UniqueCompleted.Add(entity.DataType, new List<EntityModel>()); |
|
|
|
if (entity.Production()?.ProducedBy != null) |
|
_buildOrder.TrainingCapacityUsed.Add(new TrainingCapacityUsedModel |
|
{ |
|
StartingUsageTime = atInterval, |
|
StopUsageTime = completedTime, |
|
UsedSlots = entity.Supply() != null ? entity.Supply()!.Takes : 1, |
|
UsedBuilding = entity.Production()!.ProducedBy |
|
}); |
|
|
|
_buildOrder.UniqueCompleted[entity.DataType].Add(entity); |
|
|
|
if (entity.Supply() != null && entity.Supply()!.Takes > 0) |
|
_buildOrder.CurrentSupplyUsed += entity.Supply()!.Takes; |
|
if (entity.Supply() != null && entity.Supply()!.Grants > 0) |
|
_buildOrder.SupplyCountTimes.Add(_buildOrder.SupplyCountTimes.Last().Key + entity.Supply()!.Grants, |
|
completedTime); |
|
|
|
if (atInterval > _lastInterval) _lastInterval = atInterval; |
|
|
|
NotifyDataChanged(); |
|
} |
|
|
|
public bool AddWait(int forInterval) |
|
{ |
|
if (forInterval < 0) |
|
{ |
|
_toastService.AddToast(new ToastModel |
|
{ SeverityType = SeverityType.Error, Title = "Wait", Message = "This should never happen." }); |
|
return false; |
|
} |
|
|
|
_lastInterval += forInterval; |
|
|
|
if (!_buildOrder.StartedOrders.ContainsKey(_lastInterval)) |
|
_buildOrder.StartedOrders.Add(_lastInterval, new List<EntityModel>()); |
|
|
|
if (!_buildOrder.CompletedOrders.ContainsKey(_lastInterval)) |
|
_buildOrder.CompletedOrders.Add(_lastInterval, new List<EntityModel>()); |
|
|
|
NotifyDataChanged(); |
|
return true; |
|
} |
|
|
|
public bool AddWaitTo(int interval) |
|
{ |
|
if (interval <= _lastInterval) |
|
{ |
|
_toastService.AddToast(new ToastModel |
|
{ |
|
SeverityType = SeverityType.Error, Title = "Logic Error", |
|
Message = "You cannot wait to a time that has already elapsed." |
|
}); |
|
return false; |
|
} |
|
|
|
_lastInterval = interval; |
|
|
|
if (!_buildOrder.StartedOrders.ContainsKey(_lastInterval)) |
|
_buildOrder.StartedOrders.Add(_lastInterval, new List<EntityModel>()); |
|
|
|
if (!_buildOrder.CompletedOrders.ContainsKey(_lastInterval)) |
|
_buildOrder.CompletedOrders.Add(_lastInterval, new List<EntityModel>()); |
|
|
|
NotifyDataChanged(); |
|
return true; |
|
} |
|
|
|
public int? WillMeetRequirements(EntityModel entity) |
|
{ |
|
var requirements = entity.Requirements(); |
|
|
|
if (requirements.Count == 0) return 0; |
|
|
|
var metTime = 0; |
|
foreach (var requiredEntity in requirements) |
|
if (_buildOrder.UniqueCompletedTimes.TryGetValue(requiredEntity.Id, out var completedTime)) |
|
{ |
|
if (completedTime > metTime) metTime = completedTime; |
|
} |
|
else |
|
{ |
|
return null; |
|
} |
|
|
|
return metTime; |
|
} |
|
|
|
public int? WillMeetSupply(EntityModel entity) |
|
{ |
|
var supply = entity.Supply(); |
|
|
|
if (supply == null || supply.Takes.Equals(0)) return 0; |
|
|
|
foreach (var supplyAtTime in _buildOrder.SupplyCountTimes) |
|
if (supply.Takes + _buildOrder.CurrentSupplyUsed <= supplyAtTime.Key) |
|
return supplyAtTime.Value; |
|
|
|
return null; |
|
} |
|
|
|
|
|
public bool Add(EntityModel entity, IEconomyService withEconomy) |
|
{ |
|
var atInterval = _lastInterval; |
|
|
|
if (!HandleSupply(entity, ref atInterval)) return false; |
|
if (!HandleRequirements(entity, ref atInterval)) return false; |
|
if (!HandleEconomy(entity, withEconomy, ref atInterval)) return false; |
|
if (!HandleTrainingQueue(entity, ref atInterval)) return false; |
|
|
|
Add(entity, atInterval); |
|
|
|
return true; |
|
} |
|
|
|
public void RemoveLast() |
|
{ |
|
if (_buildOrder.StartedOrders.Keys.Count > 1) |
|
{ |
|
if (_buildOrder.StartedOrders.Count == 0) |
|
{ |
|
_buildOrder.StartedOrders.Remove(_buildOrder.StartedOrders.Last().Key); |
|
_buildOrder.CompletedOrders.Remove(_buildOrder.CompletedOrders.Last().Key); |
|
|
|
_lastInterval = _buildOrder.StartedOrders.Last().Key; |
|
return; |
|
} |
|
|
|
var lastStarted = _buildOrder.StartedOrders.Keys.Last(); |
|
var lastCompleted = _buildOrder.CompletedOrders.Keys.Last(); |
|
|
|
EntityModel entityRemoved = default!; |
|
|
|
if (_buildOrder.StartedOrders[lastStarted].Count > 0) |
|
{ |
|
entityRemoved = _buildOrder.StartedOrders[lastStarted].Last(); |
|
_buildOrder.StartedOrders[lastStarted].Remove(_buildOrder.StartedOrders[lastStarted].Last()); |
|
_buildOrder.CompletedOrders[lastCompleted].Remove(_buildOrder.CompletedOrders[lastCompleted].Last()); |
|
} |
|
|
|
if (_buildOrder.StartedOrders[lastStarted].Count == 0) _buildOrder.StartedOrders.Remove(lastStarted); |
|
if (_buildOrder.CompletedOrders[lastCompleted].Count == 0) |
|
_buildOrder.CompletedOrders.Remove(lastCompleted); |
|
|
|
if (_buildOrder.StartedOrders.Keys.Count > 0) |
|
_lastInterval = _buildOrder.StartedOrders.Keys.Last(); |
|
else |
|
_lastInterval = 0; |
|
|
|
if (entityRemoved.Supply()?.Grants > 0) |
|
SupplyCountTimes.Remove(SupplyCountTimes.Last().Key); |
|
|
|
if (entityRemoved.Supply()?.Takes > 0) |
|
_buildOrder.CurrentSupplyUsed -= entityRemoved.Supply()!.Takes; |
|
|
|
_buildOrder.UniqueCompletedCount[entityRemoved!.DataType]--; |
|
if (_buildOrder.UniqueCompletedCount[entityRemoved!.DataType] == 0) |
|
UniqueCompletedTimes.Remove(entityRemoved.DataType); |
|
|
|
_buildOrder.UniqueCompleted[entityRemoved.DataType] |
|
.Remove(_buildOrder.UniqueCompleted[entityRemoved.DataType].Last()); |
|
|
|
if (entityRemoved.Production() != null |
|
&& entityRemoved.Production()!.ProducedBy != null |
|
&& entityRemoved.Supply() != null |
|
&& entityRemoved.Supply()!.Takes > 0) |
|
_buildOrder.TrainingCapacityUsed.Remove(_buildOrder.TrainingCapacityUsed.Last()); |
|
|
|
if (entityRemoved.Info().Descriptive == DescriptiveType.Worker) |
|
{ |
|
RemoveLast(); |
|
return; |
|
} |
|
|
|
NotifyDataChanged(); |
|
} |
|
} |
|
|
|
|
|
public string AsJson() |
|
{ |
|
var options = new JsonSerializerOptions |
|
{ |
|
WriteIndented = true |
|
}; |
|
options.Converters.Add(new JsonStringEnumConverter()); |
|
return JsonSerializer.Serialize(_buildOrder, options); |
|
} |
|
|
|
public string BuildOrderAsYaml() |
|
{ |
|
var stringBuilder = new StringBuilder(); |
|
var serializer = new Serializer(); |
|
stringBuilder.AppendLine(serializer.Serialize(_buildOrder)); |
|
var buildOrderText = stringBuilder.ToString(); |
|
return buildOrderText; |
|
} |
|
|
|
|
|
public List<EntityModel> GetCompletedBefore(int interval) |
|
{ |
|
return (from ordersAtTime in _buildOrder.StartedOrders |
|
from orders in ordersAtTime.Value |
|
where ordersAtTime.Key + (orders.Production() == null ? 0 : orders.Production().BuildTime) <= interval |
|
select orders).ToList(); |
|
} |
|
|
|
public List<EntityModel> GetHarvestPointsCompletedBefore(int interval) |
|
{ |
|
return (from ordersAtTime in _buildOrder.StartedOrders |
|
from orders in ordersAtTime.Value |
|
where ordersAtTime.Key + (orders.Production() == null ? 0 : orders.Production().BuildTime) <= interval |
|
where orders.Harvest() != null |
|
select orders).ToList(); |
|
} |
|
|
|
|
|
public void SetName(string name) |
|
{ |
|
_buildOrder.Name = name; |
|
NotifyDataChanged(); |
|
} |
|
|
|
public string GetName() |
|
{ |
|
return _buildOrder.Name; |
|
} |
|
|
|
public void SetNotes(string notes) |
|
{ |
|
_buildOrder.Notes = notes; |
|
NotifyDataChanged(); |
|
} |
|
|
|
public string GetNotes() |
|
{ |
|
return _buildOrder.Notes; |
|
} |
|
|
|
public void DeprecatedSetColor(string color) |
|
{ |
|
} |
|
|
|
public string GetColor() |
|
{ |
|
return ""; |
|
} |
|
|
|
public void Reset() |
|
{ |
|
_lastInterval = 0; |
|
_buildOrder.Initialize(DataType.FACTION_Aru); |
|
NotifyDataChanged(); |
|
} |
|
|
|
public int? WillMeetTrainingQueue(EntityModel entity) |
|
{ |
|
Console.WriteLine($"WillMeetTrainingQueue {entity.Info().Name}"); |
|
|
|
var supply = entity.Supply(); |
|
var production = entity.Production(); |
|
|
|
var checkedInterval = _lastInterval; |
|
|
|
if (supply == null || production == null || supply.Takes.Equals(0)) |
|
{ |
|
Console.WriteLine(supply == null ? "Was Null" : supply.Takes); |
|
|
|
return 1; |
|
} |
|
|
|
var producedBy = production.ProducedBy; |
|
if (producedBy == null) |
|
{ |
|
Console.WriteLine("Produced by Nothing"); |
|
return 1; |
|
} |
|
|
|
var uniqueCompleted = _buildOrder.UniqueCompleted[producedBy]; |
|
|
|
var shortestIncrement = int.MaxValue; |
|
var trainingSlots = 0; |
|
var didDelay = false; |
|
|
|
foreach (var productionEntity in uniqueCompleted) trainingSlots += productionEntity.Supply()!.Grants; |
|
|
|
|
|
while (true) |
|
{ |
|
var usedSlots = 0; |
|
foreach (var used in _buildOrder.TrainingCapacityUsed) |
|
if (checkedInterval >= used.StartingUsageTime && checkedInterval < used.StopUsageTime) |
|
{ |
|
usedSlots += used.UsedSlots; |
|
var duration = used.StopUsageTime - used.StartingUsageTime; |
|
if (duration < shortestIncrement) shortestIncrement = duration; |
|
|
|
Console.WriteLine( |
|
$"Used slots {used.UsedSlots} Duration {duration} Start {used.StartingUsageTime} Stop {used.StopUsageTime} "); |
|
} |
|
|
|
if (usedSlots + supply.Takes <= trainingSlots) |
|
{ |
|
if (didDelay) |
|
_toastService.AddToast(new ToastModel |
|
{ |
|
Title = "Waited", SeverityType = SeverityType.Information, |
|
Message = $"Had to wait {checkedInterval - _lastInterval}s for Training Queue." |
|
}); |
|
|
|
Console.WriteLine($"Time {checkedInterval} did Delay {didDelay}"); |
|
|
|
return checkedInterval; |
|
} |
|
|
|
checkedInterval += shortestIncrement; |
|
didDelay = true; |
|
|
|
if (shortestIncrement == int.MaxValue) |
|
{ |
|
Console.WriteLine("MaxValue"); |
|
return null; |
|
} |
|
} |
|
} |
|
|
|
private event Action OnChange = null!; |
|
|
|
private bool HandleEconomy(EntityModel entity, IEconomyService withEconomy, ref int atInterval) |
|
{ |
|
var production = entity.Production(); |
|
|
|
if (production == null) return true; |
|
|
|
for (var interval = atInterval; interval < withEconomy.GetOverTime().Count; interval++) |
|
{ |
|
var economyAtSecond = withEconomy.GetOverTime()[interval]; |
|
if (economyAtSecond.Alloy >= production.Alloy |
|
&& economyAtSecond.Ether >= production.Ether |
|
&& economyAtSecond.Pyre >= production.Pyre) |
|
{ |
|
atInterval = interval; |
|
|
|
if (entity.EntityType != EntityType.Army) atInterval += _timingService.BuildingInputDelay; |
|
|
|
return true; |
|
} |
|
} |
|
|
|
if (withEconomy.GetOverTime().Last().Ether < production.Ether) |
|
_toastService.AddToast(new ToastModel |
|
{ |
|
Title = "Not Enough Ether", Message = "Build more ether extractors!", |
|
SeverityType = SeverityType.Error |
|
}); |
|
|
|
if (withEconomy.GetOverTime().Last().Alloy < production.Alloy) |
|
_toastService.AddToast(new ToastModel |
|
{ |
|
Title = "Not Enough Alloy", Message = "Build more bases!", |
|
SeverityType = SeverityType.Error |
|
}); |
|
|
|
return false; |
|
} |
|
|
|
private bool HandleSupply(EntityModel entity, ref int atInterval) |
|
{ |
|
var minSupplyInterval = WillMeetSupply(entity); |
|
if (minSupplyInterval == null) |
|
{ |
|
_toastService.AddToast(new ToastModel |
|
{ |
|
Title = "Supply Cap Reached", Message = "Build more supply!", |
|
SeverityType = SeverityType.Error |
|
}); |
|
|
|
return false; |
|
} |
|
|
|
if (minSupplyInterval > atInterval) atInterval = (int)minSupplyInterval; |
|
|
|
return true; |
|
} |
|
|
|
private bool HandleTrainingQueue(EntityModel entity, ref int atInterval) |
|
{ |
|
var minTrainingQueueInterval = WillMeetTrainingQueue(entity); |
|
if (minTrainingQueueInterval == null) |
|
{ |
|
_toastService.AddToast(new ToastModel |
|
{ |
|
Title = "Invalid", Message = "Invalid Training Queue error", |
|
SeverityType = SeverityType.Error |
|
}); |
|
|
|
return false; |
|
} |
|
|
|
if (minTrainingQueueInterval > atInterval) atInterval = (int)minTrainingQueueInterval; |
|
|
|
return true; |
|
} |
|
|
|
|
|
private bool HandleRequirements(EntityModel entity, ref int atInterval) |
|
{ |
|
var minRequirementInterval = WillMeetRequirements(entity); |
|
if (minRequirementInterval == null) |
|
{ |
|
_toastService.AddToast(new ToastModel |
|
{ |
|
Title = "Missing Requirements", Message = "You don't have what's needed for this unit.", |
|
SeverityType = SeverityType.Error |
|
}); |
|
|
|
return false; |
|
} |
|
|
|
if (minRequirementInterval > atInterval) atInterval = (int)minRequirementInterval; |
|
|
|
return true; |
|
} |
|
|
|
private void NotifyDataChanged() |
|
{ |
|
OnChange?.Invoke(); |
|
} |
|
} |