504 lines
16 KiB
C#
504 lines
16 KiB
C#
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<int, List<EntityModel>> DepletedOrders => _buildOrder.DepletedOrders;
|
|
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, []);
|
|
|
|
var production = entity.Production();
|
|
|
|
var completedTime = atInterval;
|
|
if (production != null) completedTime += production.BuildTime;
|
|
|
|
if (!_buildOrder.CompletedOrders.ContainsKey(completedTime))
|
|
_buildOrder.CompletedOrders.Add(completedTime, []);
|
|
|
|
_buildOrder.StartedOrders[atInterval].Add(entity.Clone());
|
|
_buildOrder.CompletedOrders[completedTime].Add(entity.Clone());
|
|
|
|
_buildOrder.UniqueCompletedTimes.TryAdd(entity.DataType, atInterval);
|
|
|
|
if (!_buildOrder.UniqueCompletedCount.TryAdd(entity.DataType, 1))
|
|
_buildOrder.UniqueCompletedCount[entity.DataType]++;
|
|
|
|
if (!_buildOrder.UniqueCompleted.ContainsKey(entity.DataType))
|
|
_buildOrder.UniqueCompleted.Add(entity.DataType, []);
|
|
|
|
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, []);
|
|
|
|
if (!_buildOrder.CompletedOrders.ContainsKey(_lastInterval))
|
|
_buildOrder.CompletedOrders.Add(_lastInterval, []);
|
|
|
|
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, []);
|
|
|
|
if (!_buildOrder.CompletedOrders.ContainsKey(_lastInterval))
|
|
_buildOrder.CompletedOrders.Add(_lastInterval, []);
|
|
|
|
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
|
|
.Where(supplyAtTime => 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) return;
|
|
|
|
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
|
|
let production = orders.Production()
|
|
where ordersAtTime.Key + (production?.BuildTime ?? 0) <= interval
|
|
select orders).ToList();
|
|
}
|
|
|
|
public List<EntityModel> GetUndepletedHarvestPointsCompletedBefore(int interval)
|
|
{
|
|
return (from ordersAtTime in _buildOrder.StartedOrders
|
|
from orders in ordersAtTime.Value
|
|
let production = orders.Production()
|
|
let harvest = orders.Harvest()
|
|
where harvest != null
|
|
let buildTime = production?.BuildTime ?? 0
|
|
where ordersAtTime.Key + buildTime <= interval
|
|
&& !harvest.IsDepleted(
|
|
interval,
|
|
ordersAtTime.Key + buildTime)
|
|
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 void Reset(string faction)
|
|
{
|
|
_lastInterval = 0;
|
|
_buildOrder.Initialize(faction);
|
|
NotifyDataChanged();
|
|
}
|
|
|
|
public int? WillMeetTrainingQueue(EntityModel entity)
|
|
{
|
|
var supply = entity.Supply();
|
|
var production = entity.Production();
|
|
|
|
var checkedInterval = _lastInterval;
|
|
|
|
if (supply == null || production == null || supply.Takes.Equals(0)) return 1;
|
|
|
|
var producedBy = production.ProducedBy;
|
|
if (producedBy == null) return 1;
|
|
|
|
var uniqueCompleted = _buildOrder.UniqueCompleted[producedBy];
|
|
|
|
var shortestIncrement = int.MaxValue;
|
|
var didDelay = false;
|
|
|
|
var trainingSlots = uniqueCompleted.Sum(productionEntity => productionEntity.Supply()!.Grants);
|
|
|
|
|
|
while (true)
|
|
{
|
|
var usedSlots = 0;
|
|
foreach (var used in
|
|
_buildOrder.TrainingCapacityUsed
|
|
.Where(used =>
|
|
checkedInterval >= used.StartingUsageTime && checkedInterval < used.StopUsageTime))
|
|
{
|
|
usedSlots += used.UsedSlots;
|
|
var duration = used.StopUsageTime - used.StartingUsageTime;
|
|
if (duration < shortestIncrement) shortestIncrement = duration;
|
|
}
|
|
|
|
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."
|
|
});
|
|
|
|
return checkedInterval;
|
|
}
|
|
|
|
checkedInterval += shortestIncrement;
|
|
didDelay = true;
|
|
|
|
if (shortestIncrement == int.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)) continue;
|
|
|
|
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();
|
|
}
|
|
} |