using Content.Server.StationRecords.Systems;
using Content.Shared.Dataset;
using Content.Shared.Silicons.Laws;
using Content.Shared.Station;
using Content.Shared.StationRecords;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Silicons.Laws;
///
/// This handles generating random ion laws.
///
public sealed class IonLawSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedStationSystem _stationSystem = default!;
[Dependency] private readonly StationRecordsSystem _stationRecordsSystem = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
private readonly Dictionary> _selectors = new();
private IonLawPrototype? _ionLaw;
public override void Initialize()
{
base.Initialize();
_sawmill = _logManager.GetSawmill("ion-law");
SubscribeLocalEvent(OnPrototypesReloaded);
BuildSelectors();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj)
{
if (obj.ByType.ContainsKey(typeof(DatasetPrototype)))
BuildSelectors();
}
private void BuildSelectors()
{
_selectors.Clear();
DatasetFill DatasetFill(ProtoId datasetId) => new() { Dataset = datasetId };
RandomManifestFill RandomManifestFill(ProtoId fallback) => new() { FallbackDataset = fallback };
ConstantFill ConstantFill(bool val) => new() { BoolValue = val };
AddSelector("ION-NUMBER-BASE", DatasetFill("IonStormNumberBase"));
AddSelector("ION-NUMBER-MOD", DatasetFill("IonStormNumberMod"));
AddSelector("ION-ADJECTIVE", DatasetFill("IonStormAdjectives"));
AddSelector("ION-SUBJECT", DatasetFill("IonStormThreats"));
AddSelector("ION-SUBJECT", DatasetFill("IonStormCrew"));
AddSelector("ION-SUBJECT", DatasetFill("IonStormObjects"));
AddSelector("ION-SUBJECT", RandomManifestFill("IonStormCrew"));
AddSelector("ION-WHO", DatasetFill("IonStormCrew"));
AddSelector("ION-WHO", RandomManifestFill("IonStormCrew"));
AddSelector("ION-MUST", DatasetFill("IonStormMusts"));
AddSelector("ION-THING", DatasetFill("IonStormObjects"));
AddSelector("ION-THING", DatasetFill("IonStormConcepts"));
AddSelector("ION-JOB", DatasetFill("IonStormCrew"));
AddSelector("ION-JOB", RandomManifestFill("IonStormCrew"));
AddSelector("ION-WHO-GENERAL", DatasetFill("IonStormAreas"));
AddSelector("ION-WHO-GENERAL", RandomManifestFill("IonStormCrew"));
AddSelector("ION-PLURAL", ConstantFill(true));
AddSelector("ION-PLURAL", ConstantFill(false));
AddSelector("ION-REQUIRE", DatasetFill("IonStormConcepts"));
AddSelector("ION-REQUIRE", DatasetFill("IonStormRequires"));
AddSelector("ION-SEVERITY", DatasetFill("IonStormAllergySeverities"));
AddSelector("ION-ALLERGY", DatasetFill("IonStormConcepts"));
AddSelector("ION-ALLERGY", DatasetFill("IonStormAllergies"));
AddSelector("ION-FEELING", DatasetFill("IonStormFeelings"));
AddSelector("ION-CONCEPT", DatasetFill("IonStormConcepts"));
AddSelector("ION-FOOD", DatasetFill("IonStormFoods"));
AddSelector("ION-DRINK", DatasetFill("IonStormDrinks"));
AddSelector("ION-CHANGE", DatasetFill("IonStormCrew"));
AddSelector("ION-CHANGE", RandomManifestFill("IonStormCrew"));
AddSelector("ION-WHO-RANDOM", DatasetFill("IonStormCrew"));
AddSelector("ION-WHO-RANDOM", RandomManifestFill("IonStormCrew"));
AddSelector("ION-AREA", DatasetFill("IonStormAreas"));
AddSelector("ION-PART", ConstantFill(true));
AddSelector("ION-PART", ConstantFill(false));
AddSelector("ION-OBJECT", DatasetFill("IonStormObjects"));
AddSelector("ION-HARM-PROTECT", DatasetFill("IonStormConcepts"));
AddSelector("ION-HARM-PROTECT", DatasetFill("IonStormCrew"));
AddSelector("ION-HARM-PROTECT", RandomManifestFill("IonStormCrew"));
AddSelector("ION-VERB", DatasetFill("IonStormVerbs"));
}
///
/// Adds a selector to the cache.
/// Used for picking datasets to fill the keys in the strings for the Ion Laws.
///
/// The key in the strings file to fill.
/// The type of dataset to use and the prototype ID for it, if it takes one.
private void AddSelector(string key, IonLawSelector selector)
{
if (!_selectors.ContainsKey(key))
_selectors[key] = new List();
_selectors[key].Add(selector);
}
///
/// Generates a random ion law by picking an ion law prototype and filling its placeholders with random values, from datasets.
///
/// A formatted string representing the new ion law.
public string GetIonLaw()
{
var laws = _prototypeManager.EnumeratePrototypes().ToList();
if (laws.Count == 0)
{
_sawmill.Error("No Ion Laws found");
return Loc.GetString("ion-law-error-no-protos");
}
var totalWeight = laws.Sum(p => p.Weight);
if (totalWeight <= 0)
{
// if all weights are 0, teat them as equal and just pick one at random.
_ionLaw = _random.Pick(laws);
}
else
{
var value = _random.NextFloat() * totalWeight;
IonLawPrototype? ionLaw = null;
foreach (var law in laws)
{
if (law.Weight <= 0)
continue;
ionLaw = law;
value -= law.Weight;
if (value <= 0)
{
break;
}
}
_ionLaw = ionLaw;
}
if (_ionLaw == null)
{
_sawmill.Error("Ion Law was null");
return Loc.GetString("ion-law-error-was-null");
}
return Loc.GetString(_ionLaw.LawString, ("ion", 0));
}
///
/// Gets a value for a specific selector and index, generating it if it doesn't exist in the cache.
/// This allows laws to reference the same generated value multiple times using the same index.
///
/// The key of the selector list to pick from (e.g.: "ION-WHO").
/// A string or object representing the generated law component.
public object GetOrGenerateValue(string selectorName)
{
if (!_selectors.TryGetValue(selectorName, out var selectors))
{
_sawmill.Error("No selectors for Ion Laws found");
return Loc.GetString("ion-law-error-no-selectors");
}
var availableSelectors = selectors.ToList();
while (availableSelectors.Count > 0)
{
var selector = PickSelector(availableSelectors, _ionLaw);
if (selector == null)
break;
var newValue = GetSelectorValue(selector);
if (newValue is string s && string.IsNullOrWhiteSpace(s))
{
availableSelectors.Remove(selector);
continue;
}
return newValue;
}
_sawmill.Error("No available selectors found for the Ion Law found - this should never happen, selector was: " + selectorName);
return Loc.GetString("ion-law-error-no-available-selectors");
}
private IonLawSelector? PickSelector(List selectors, IonLawPrototype? law)
{
var weightedSelectors = new List<(IonLawSelector, float)>();
foreach (var s in selectors)
{
var weight = s.Weight;
if (s is DatasetFill df)
{
if (law != null && law.SelectorWeightAdjust.TryGetValue(df.Dataset.Id, out var adjustedWeight))
weight = adjustedWeight;
}
else if (s is RandomManifestFill)
{
if (law != null && law.SelectorWeightAdjust.TryGetValue("RandomManifestFill", out var adjustedWeight))
{
weight = adjustedWeight;
}
}
if (weight > 0)
weightedSelectors.Add((s, weight));
}
var totalWeight = weightedSelectors.Sum(s => s.Item2);
if (totalWeight <= 0)
return null;
var value = _random.NextFloat() * totalWeight;
foreach (var (selector, selectorWeight) in weightedSelectors)
{
value -= selectorWeight;
if (value <= 0)
return selector;
}
return weightedSelectors.Last().Item1;
}
private object GetSelectorValue(IonLawSelector selector)
{
switch (selector)
{
case DatasetFill datasetFill:
if (_prototypeManager.TryIndex(datasetFill.Dataset, out var dataset) && dataset.Values.Any())
{
return _random.Pick(dataset.Values);
}
_sawmill.Error("Selected DataSet (" + selector + ") was empty or not found" );
return Loc.GetString("ion-law-error-dataset-empty-or-not-found");
case RandomManifestFill randomManifestFill:
var stations = _stationSystem.GetStations();
if (stations.Count > 0)
{
var station = _random.Pick(stations);
if (TryComp(station, out StationRecordsComponent? stationRecords) &&
_stationRecordsSystem.TryGetRandomRecord((station, stationRecords), out GeneralStationRecord? record))
{
var upperName = "'" + record.Name.ToUpper() + "'";
return upperName;
}
}
// Fallback to dataset if no manifest record found or stations are empty
if (_prototypeManager.TryIndex(randomManifestFill.FallbackDataset, out var fallbackDataset) && fallbackDataset.Values.Any())
{
return _random.Pick(fallbackDataset.Values);
}
_sawmill.Error("Fallback DataSet (" + selector + ") was empty or not found" );
return Loc.GetString("ion-law-error-fallback-dataset-empty-or-not-found");
case ConstantFill constantFill:
if (constantFill.BoolValue.HasValue)
return constantFill.BoolValue.Value;
_sawmill.Error("The selected Constant Fill did not have a value: " + constantFill );
return Loc.GetString("ion-law-error-no-bool-value");
default:
{
_sawmill.Error("Selected DataSet (" + selector + ") was not selected" );
return Loc.GetString("ion-law-error-no-selector-selected");
}
}
}
}