diff --git a/Automation/ScriptingEngine/ScriptableComponents/Components/DistrictScriptableComponent.cs b/Automation/ScriptingEngine/ScriptableComponents/Components/DistrictScriptableComponent.cs index 4af33006..dcb78cfe 100644 --- a/Automation/ScriptingEngine/ScriptableComponents/Components/DistrictScriptableComponent.cs +++ b/Automation/ScriptingEngine/ScriptableComponents/Components/DistrictScriptableComponent.cs @@ -3,6 +3,7 @@ // License: Public Domain using System; +using System.Collections.Generic; using IgorZ.Automation.AutomationSystem; using IgorZ.Automation.ScriptingEngine.Core; using IgorZ.Automation.ScriptingEngine.Expressions; @@ -10,6 +11,8 @@ using Timberborn.Bots; using Timberborn.DwellingSystem; using Timberborn.GameDistricts; +using Timberborn.Population; +using Timberborn.WorkSystem; namespace IgorZ.Automation.ScriptingEngine.ScriptableComponents.Components; @@ -18,10 +21,14 @@ sealed class DistrictScriptableComponent : ScriptableComponentBase { const string BotPopulationSignalLocKey = "IgorZ.Automation.Scriptable.District.Signal.Bots"; const string BeaversPopulationSignalLocKey = "IgorZ.Automation.Scriptable.District.Signal.Beavers"; const string NumberOfBedsSignalLocKey = "IgorZ.Automation.Scriptable.District.Signal.NumberOfBeds"; + const string UnemployedBeaversSignalLocKey = "IgorZ.Automation.Scriptable.District.Signal.UnemployedBeavers"; + const string UnemployedBotsSignalLocKey = "IgorZ.Automation.Scriptable.District.Signal.UnemployedBots"; const string BotPopulationSignalName = "District.Bots"; const string BeaverPopulationSignalName = "District.Beavers"; const string NumberOfBedsSignalName = "District.NumberOfBeds"; + const string UnemployedBeaversSignalName = "District.UnemployedBeavers"; + const string UnemployedBotsSignalName = "District.UnemployedBots"; #region ScriptableComponentBase implementation @@ -30,8 +37,9 @@ sealed class DistrictScriptableComponent : ScriptableComponentBase { /// public override string[] GetSignalNamesForBuilding(AutomationBehavior behavior) { - return behavior.GetComponentFast() - ? [BeaverPopulationSignalName, BotPopulationSignalName, NumberOfBedsSignalName] + return behavior.GetComponentFast() + ? [BeaverPopulationSignalName, BotPopulationSignalName, NumberOfBedsSignalName, + UnemployedBeaversSignalName, UnemployedBotsSignalName] : []; } @@ -45,6 +53,8 @@ public override Func GetSignalSource(string name, AutomationBehavio BeaverPopulationSignalName => () => BeaverPopulationSignal(districtBuilding), BotPopulationSignalName => () => BotPopulationSignal(districtBuilding), NumberOfBedsSignalName => () => NumberOfBedsSignal(districtBuilding), + UnemployedBeaversSignalName => () => UnemployedBeaversSignal(districtBuilding), + UnemployedBotsSignalName => () => UnemployedBotsSignal(districtBuilding), _ => throw new UnknownSignalException(name), }; } @@ -59,6 +69,8 @@ public override SignalDef GetSignalDefinition(string name, AutomationBehavior be BeaverPopulationSignalName => BeaverPopulationSignalDef, BotPopulationSignalName => BotPopulationSignalDef, NumberOfBedsSignalName => NumberOfBedsSignalDef, + UnemployedBeaversSignalName => UnemployedBeaversSignalDef, + UnemployedBotsSignalName => UnemployedBotsSignalDef, _ => throw new UnknownSignalException(name), }; } @@ -66,7 +78,8 @@ public override SignalDef GetSignalDefinition(string name, AutomationBehavior be /// public override void RegisterSignalChangeCallback(SignalOperator signalOperator, ISignalListener host) { var name = signalOperator.SignalName; - if (name is not (BeaverPopulationSignalName or BotPopulationSignalName or NumberOfBedsSignalName)) { + if (name is not (BeaverPopulationSignalName or BotPopulationSignalName or NumberOfBedsSignalName + or UnemployedBeaversSignalName or UnemployedBotsSignalName)) { throw new InvalidOperationException("Unknown signal: " + name); } host.Behavior.GetOrCreate().AddSignal(signalOperator, host); @@ -111,6 +124,26 @@ public override void UnregisterSignalChangeCallback(SignalOperator signalOperato }; SignalDef _numberOfBedsSignalDef; + SignalDef UnemployedBeaversSignalDef => _unemployedBeaversSignalDef ??= new SignalDef { + ScriptName = UnemployedBeaversSignalName, + DisplayName = Loc.T(UnemployedBeaversSignalLocKey), + Result = new ValueDef { + ValueType = ScriptValue.TypeEnum.Number, + ValueValidator = ValueDef.RangeCheckValidatorInt(min: 0), + }, + }; + SignalDef _unemployedBeaversSignalDef; + + SignalDef UnemployedBotsSignalDef => _unemployedBotsSignalDef ??= new SignalDef { + ScriptName = UnemployedBotsSignalName, + DisplayName = Loc.T(UnemployedBotsSignalLocKey), + Result = new ValueDef { + ValueType = ScriptValue.TypeEnum.Number, + ValueValidator = ValueDef.RangeCheckValidatorInt(min: 0), + }, + }; + SignalDef _unemployedBotsSignalDef; + static ScriptValue BeaverPopulationSignal(DistrictBuilding districtBuilding) { return ScriptValue.FromInt(districtBuilding.District?.DistrictPopulation.Beavers.Count ?? 0); } @@ -128,6 +161,27 @@ static ScriptValue NumberOfBedsSignal(DistrictBuilding districtBuilding) { return ScriptValue.FromInt(statistics.FreeBeds + statistics.OccupiedBeds); } + static ScriptValue UnemployedBeaversSignal(DistrictBuilding districtBuilding) { + var district = districtBuilding.District; + if (!district) { + return ScriptValue.FromInt(0); + } + PopDataCollector.CollectData(district, PopData); + return ScriptValue.FromInt(PopData.BeaverWorkplaceData.Unemployed); + } + + static ScriptValue UnemployedBotsSignal(DistrictBuilding districtBuilding) { + var district = districtBuilding.District; + if (!district) { + return ScriptValue.FromInt(0); + } + PopDataCollector.CollectData(district, PopData); + return ScriptValue.FromInt(PopData.BotWorkplaceData.Unemployed); + } + + static readonly PopulationDataCollector PopDataCollector = new(); + static readonly PopulationData PopData = new(); + #endregion #region Implementation @@ -145,6 +199,7 @@ static ScriptValue NumberOfBedsSignal(DistrictBuilding districtBuilding) { sealed class DistrictChangeTracker : AbstractStatusTracker { DistrictCenter _currentDistrictCenter; + readonly List _trackedWorkplaces = new(); void Start() { var districtBuilding = GetComponentFast(); @@ -154,6 +209,7 @@ void Start() { } void UpdateDistrictCenter() { + UnsubscribeFromWorkplaces(); if (_currentDistrictCenter) { _currentDistrictCenter.DistrictPopulation.CitizenAssigned -= OnCitizenAssigned; _currentDistrictCenter.DistrictPopulation.CitizenUnassigned -= OnCitizenUnassigned; @@ -166,9 +222,28 @@ void UpdateDistrictCenter() { _currentDistrictCenter.DistrictPopulation.CitizenUnassigned += OnCitizenUnassigned; _currentDistrictCenter.DistrictBuildingRegistry.FinishedBuildingRegistered += FinishedBuildingRegisteredEvent; _currentDistrictCenter.DistrictBuildingRegistry.FinishedBuildingUnregistered += FinishedBuildingUnregisteredEvent; + SubscribeToWorkplaces(); + } + } + + void SubscribeToWorkplaces() { + foreach (var workplace in _currentDistrictCenter.DistrictBuildingRegistry.GetEnabledBuildings()) { + workplace.WorkerAssigned += OnWorkerAssignmentChanged; + workplace.WorkerUnassigned += OnWorkerAssignmentChanged; + _trackedWorkplaces.Add(workplace); } } + void UnsubscribeFromWorkplaces() { + foreach (var workplace in _trackedWorkplaces) { + if (workplace) { + workplace.WorkerAssigned -= OnWorkerAssignmentChanged; + workplace.WorkerUnassigned -= OnWorkerAssignmentChanged; + } + } + _trackedWorkplaces.Clear(); + } + void OnDistrictChangedEvent(object obj, EventArgs args) { UpdateDistrictCenter(); OnPopulationChangedEvent(); @@ -185,18 +260,35 @@ void OnCitizenUnassigned(object sender, CitizenUnassignedEventArgs args) { void OnPopulationChangedEvent(Citizen citizen = null) { if (!citizen || citizen.GetComponentFast()) { ScheduleSignal(BotPopulationSignalName, ignoreErrors: true); + ScheduleSignal(UnemployedBotsSignalName, ignoreErrors: true); } if (!citizen || !citizen.GetComponentFast()) { ScheduleSignal(BeaverPopulationSignalName, ignoreErrors: true); + ScheduleSignal(UnemployedBeaversSignalName, ignoreErrors: true); } } + void OnWorkerAssignmentChanged(object sender, WorkerChangedEventArgs args) { + ScheduleSignal(UnemployedBeaversSignalName, ignoreErrors: true); + ScheduleSignal(UnemployedBotsSignalName, ignoreErrors: true); + } + void FinishedBuildingRegisteredEvent(object sender, FinishedBuildingRegisteredEventArgs arg) { ScheduleSignal(NumberOfBedsSignalName, ignoreErrors: true); + // Re-subscribe to pick up the new workplace's worker events. + UnsubscribeFromWorkplaces(); + SubscribeToWorkplaces(); + ScheduleSignal(UnemployedBeaversSignalName, ignoreErrors: true); + ScheduleSignal(UnemployedBotsSignalName, ignoreErrors: true); } void FinishedBuildingUnregisteredEvent(object sender, FinishedBuildingUnregisteredEventArgs arg) { ScheduleSignal(NumberOfBedsSignalName, ignoreErrors: true); + // Re-subscribe to drop the destroyed workplace's worker events. + UnsubscribeFromWorkplaces(); + SubscribeToWorkplaces(); + ScheduleSignal(UnemployedBeaversSignalName, ignoreErrors: true); + ScheduleSignal(UnemployedBotsSignalName, ignoreErrors: true); } } diff --git a/Automation/ScriptingEngine/ScriptableComponents/Components/WorkplaceScriptableComponent.cs b/Automation/ScriptingEngine/ScriptableComponents/Components/WorkplaceScriptableComponent.cs index b708ce70..c9ee11b4 100644 --- a/Automation/ScriptingEngine/ScriptableComponents/Components/WorkplaceScriptableComponent.cs +++ b/Automation/ScriptingEngine/ScriptableComponents/Components/WorkplaceScriptableComponent.cs @@ -7,6 +7,7 @@ using IgorZ.Automation.ScriptingEngine.Core; using IgorZ.Automation.ScriptingEngine.Expressions; using Timberborn.BaseComponentSystem; +using Timberborn.PrioritySystem; using Timberborn.WorkSystem; namespace IgorZ.Automation.ScriptingEngine.ScriptableComponents.Components; @@ -15,19 +16,68 @@ sealed class WorkplaceScriptableComponent : ScriptableComponentBase { const string RemoveWorkersActionLocKey = "IgorZ.Automation.Scriptable.Workplace.Action.RemoveWorkers"; const string SetWorkersActionLocKey = "IgorZ.Automation.Scriptable.Workplace.Action.SetWorkers"; + const string SetPriorityActionLocKey = "IgorZ.Automation.Scriptable.Workplace.Action.SetPriority"; + const string AssignedWorkersSignalLocKey = "IgorZ.Automation.Scriptable.Workplace.Signal.AssignedWorkers"; const string RemoveWorkersActionName = "Workplace.RemoveWorkers"; const string SetWorkersActionName = "Workplace.SetWorkers"; + const string SetPriorityActionName = "Workplace.SetPriority"; + const string AssignedWorkersSignalName = "Workplace.AssignedWorkers"; #region ScriptableComponentBase implementation /// public override string Name => "Workplace"; + /// + public override string[] GetSignalNamesForBuilding(AutomationBehavior behavior) { + var workplace = GetWorkplace(behavior, throwIfNotFound: false); + return workplace ? [AssignedWorkersSignalName] : []; + } + + /// + public override Func GetSignalSource(string name, AutomationBehavior behavior) { + var workplace = GetWorkplace(behavior); + return name switch { + AssignedWorkersSignalName => () => ScriptValue.FromInt(workplace.NumberOfAssignedWorkers), + _ => throw new UnknownSignalException(name), + }; + } + + /// + public override SignalDef GetSignalDefinition(string name, AutomationBehavior behavior) { + var workplace = GetWorkplace(behavior); + return name switch { + AssignedWorkersSignalName => LookupSignalDef( + AssignedWorkersSignalName + "-" + workplace.MaxWorkers, + () => MakeAssignedWorkersSignalDef(workplace)), + _ => throw new UnknownSignalException(name), + }; + } + + /// + public override void RegisterSignalChangeCallback(SignalOperator signalOperator, ISignalListener host) { + if (signalOperator.SignalName is not AssignedWorkersSignalName) { + throw new InvalidOperationException("Unknown signal: " + signalOperator.SignalName); + } + host.Behavior.GetOrCreate().AddSignal(signalOperator, host); + } + + /// + public override void UnregisterSignalChangeCallback(SignalOperator signalOperator, ISignalListener host) { + host.Behavior.GetOrThrow().RemoveSignal(signalOperator, host); + } + /// public override string[] GetActionNamesForBuilding(AutomationBehavior behavior) { var workplace = GetWorkplace(behavior, throwIfNotFound: false); - return workplace ? [RemoveWorkersActionName, SetWorkersActionName] : []; + if (!workplace) { + return []; + } + var workplacePriority = behavior.GetComponentFast(); + return workplacePriority + ? [RemoveWorkersActionName, SetWorkersActionName, SetPriorityActionName] + : [RemoveWorkersActionName, SetWorkersActionName]; } /// @@ -36,6 +86,7 @@ public override Action GetActionExecutor(string name, AutomationB return name switch { RemoveWorkersActionName => _ => ResetWorkersAction(workplace), SetWorkersActionName => args => SetWorkersAction(workplace, args), + SetPriorityActionName => args => SetPriorityAction(behavior, args), _ => throw new UnknownActionException(name), }; } @@ -47,12 +98,30 @@ public override ActionDef GetActionDefinition(string name, AutomationBehavior be return name switch { RemoveWorkersActionName => RemoveWorkersActionDef, SetWorkersActionName => LookupActionDef(key, () => MakeSetWorkersActionDef(workplace)), + SetPriorityActionName => SetPriorityActionDef, _ => throw new UnknownActionException(name), }; } #endregion + #region Signals + + SignalDef MakeAssignedWorkersSignalDef(Workplace workplace) { + return new SignalDef { + ScriptName = AssignedWorkersSignalName, + DisplayName = Loc.T(AssignedWorkersSignalLocKey), + Result = new ValueDef { + ValueType = ScriptValue.TypeEnum.Number, + ValueFormatter = x => x.AsFloat.ToString("0"), + ValueValidator = ValueDef.RangeCheckValidatorInt(0, workplace.MaxWorkers), + ValueUiHint = GetArgumentMinMaxValueHint(0, workplace.MaxWorkers), + }, + }; + } + + #endregion + #region Actions ActionDef RemoveWorkersActionDef => _removeWorkersActionDef ??= new ActionDef { @@ -77,6 +146,24 @@ ActionDef MakeSetWorkersActionDef(Workplace workplace) { }; } + ActionDef SetPriorityActionDef => _setPriorityActionDef ??= new ActionDef { + ScriptName = SetPriorityActionName, + DisplayName = Loc.T(SetPriorityActionLocKey), + Arguments = [ + new ValueDef { + ValueType = ScriptValue.TypeEnum.String, + Options = [ + ("VeryLow", Loc.T("Priorities.VeryLow")), + ("Low", Loc.T("Priorities.Low")), + ("Normal", Loc.T("Priorities.Normal")), + ("High", Loc.T("Priorities.High")), + ("VeryHigh", Loc.T("Priorities.VeryHigh")), + ], + }, + ], + }; + ActionDef _setPriorityActionDef; + static void ResetWorkersAction(Workplace building) { building.DesiredWorkers = 0; building.UnassignWorkerIfOverstaffed(); @@ -95,6 +182,22 @@ static void SetWorkersAction(Workplace building, ScriptValue[] args) { building.UnassignWorkerIfOverstaffed(); } + static void SetPriorityAction(AutomationBehavior behavior, ScriptValue[] args) { + AssertActionArgsCount(SetPriorityActionName, args, 1); + var priorityName = args[0].AsString; + if (!Enum.TryParse(priorityName, out var priority)) { + throw new ScriptError.ValueOutOfRange($"Unknown priority: {priorityName}"); + } + var workplacePriority = behavior.GetComponentFast(); + if (!workplacePriority) { + throw new ScriptError.BadStateError(behavior, "Building doesn't have WorkplacePriority"); + } + if (workplacePriority.Priority == priority) { + return; + } + workplacePriority.SetPriority(priority); + } + #endregion #region Implementation @@ -108,4 +211,21 @@ static Workplace GetWorkplace(BaseComponent building, bool throwIfNotFound = tru } #endregion + + #region Workplace change tracker + + sealed class WorkplaceChangeTracker : AbstractStatusTracker { + + void Start() { + var workplace = GetComponentFast(); + workplace.WorkerAssigned += OnWorkerChanged; + workplace.WorkerUnassigned += OnWorkerChanged; + } + + void OnWorkerChanged(object sender, WorkerChangedEventArgs args) { + ScheduleSignal(AssignedWorkersSignalName, ignoreErrors: true); + } + } + + #endregion } diff --git a/ModsUnityProject/Assets/Mods/Automation/Data/Localizations/enUS.txt b/ModsUnityProject/Assets/Mods/Automation/Data/Localizations/enUS.txt index 622765f3..e0e37917 100644 --- a/ModsUnityProject/Assets/Mods/Automation/Data/Localizations/enUS.txt +++ b/ModsUnityProject/Assets/Mods/Automation/Data/Localizations/enUS.txt @@ -138,6 +138,8 @@ IgorZ.Automation.Scriptable.Debug.Signal.Ticker,"ticker","Name of the condition IgorZ.Automation.Scriptable.District.Signal.Beavers,"beavers in district","Name of the condition for the current beaver population checking" IgorZ.Automation.Scriptable.District.Signal.Bots,"bots in district","Name of the condition for the current bot population checking" IgorZ.Automation.Scriptable.District.Signal.NumberOfBeds,"beds in district","Name of the condition for the current number of beds checking" +IgorZ.Automation.Scriptable.District.Signal.UnemployedBeavers,"unemployed beavers in district","Name of the condition for the current number of unemployed beaver workers" +IgorZ.Automation.Scriptable.District.Signal.UnemployedBots,"unemployed bots in district","Name of the condition for the current number of unemployed bot workers" IgorZ.Automation.Scriptable.Dynamite.Action.Detonate,"detonate dynamite","Name of the action that immediately detonates the dynamite" IgorZ.Automation.Scriptable.Dynamite.Action.DetonateAndRepeat,"detonate dynamite and add another {0} times","Name of the action that detonates the dynamite and places new dynamites the specified number of times" IgorZ.Automation.Scriptable.Floodgate.Action.SetHeight,"set gate height to {0}","Change the floodgate height to the specified value" @@ -161,7 +163,9 @@ IgorZ.Automation.Scriptable.StreamGauge.Signal.Current,"current","Name of the co IgorZ.Automation.Scriptable.StreamGauge.Signal.Depth,"depth","Name of the condition for the current water level checking" IgorZ.Automation.Scriptable.Weather.Signal.Season,"season","Name of the condition for the current season checking" IgorZ.Automation.Scriptable.Workplace.Action.RemoveWorkers,"remove workers","Name of the action that removes all workers from the workshop" +IgorZ.Automation.Scriptable.Workplace.Action.SetPriority,"set worker priority to {0}","Name of the action that sets the workplace priority level" IgorZ.Automation.Scriptable.Workplace.Action.SetWorkers,"assign {0} workers","Name of the action that sets the number of workers in the building" +IgorZ.Automation.Scriptable.Workplace.Signal.AssignedWorkers,"assigned workers","Name of the condition for the current number of assigned workers" IgorZ.Automation.Scripting.Expressions.AndOperator,"and","String to join components of the expression with condition AND. Used to represent the condition in the panel" IgorZ.Automation.Scripting.Expressions.OrOperator,"or","String to join components of the expression with condition OR. Used to represent the condition in the panel" IgorZ.Automation.Scripting.Expressions.NotOperator,"NOT","NOT operator name to represent the condition in the panel" diff --git a/ModsUnityProject/Assets/Mods/Automation/Data/Localizations/ruRU.txt b/ModsUnityProject/Assets/Mods/Automation/Data/Localizations/ruRU.txt index 341e4eba..32f84a25 100644 --- a/ModsUnityProject/Assets/Mods/Automation/Data/Localizations/ruRU.txt +++ b/ModsUnityProject/Assets/Mods/Automation/Data/Localizations/ruRU.txt @@ -139,6 +139,8 @@ IgorZ.Automation.Scriptable.Debug.Signal.Ticker,"тикер","Name of the condit IgorZ.Automation.Scriptable.District.Signal.Beavers,"бобров в районе","Name of the condition for the current beaver population checking" IgorZ.Automation.Scriptable.District.Signal.Bots,"ботов в районе","Name of the condition for the current bot population checking" IgorZ.Automation.Scriptable.District.Signal.NumberOfBeds,"постелей в районе","Name of the condition for the current number of beds checking" +IgorZ.Automation.Scriptable.District.Signal.UnemployedBeavers,"безработных бобров в районе","Name of the condition for the current number of unemployed beaver workers" +IgorZ.Automation.Scriptable.District.Signal.UnemployedBots,"безработных ботов в районе","Name of the condition for the current number of unemployed bot workers" IgorZ.Automation.Scriptable.Dynamite.Action.Detonate,"активировать динамит","Name of the action that immediately detonates the dynamite" IgorZ.Automation.Scriptable.Dynamite.Action.DetonateAndRepeat,"активировать динамит и повторить ещё {0} раза","Name of the action that detonates the dynamite and places new dynamites the specified number of times" IgorZ.Automation.Scriptable.Floodgate.Action.SetHeight,"выставить высоту затвора в {0}","Change the floodgate height to the specified value" @@ -162,7 +164,9 @@ IgorZ.Automation.Scriptable.StreamGauge.Signal.Current,"течение","Name of IgorZ.Automation.Scriptable.StreamGauge.Signal.Depth,"глубина","Name of the condition for the current water level checking" IgorZ.Automation.Scriptable.Weather.Signal.Season,"сезон","Name of the condition for the current season checking" IgorZ.Automation.Scriptable.Workplace.Action.RemoveWorkers,"снять работников","Name of the action that removes all workers from the workshop" +IgorZ.Automation.Scriptable.Workplace.Action.SetPriority,"установить приоритет работников на {0}","Name of the action that sets the workplace priority level" IgorZ.Automation.Scriptable.Workplace.Action.SetWorkers,"назначить {0} работников","Name of the action that sets the number of workers in the workshop" +IgorZ.Automation.Scriptable.Workplace.Signal.AssignedWorkers,"назначенные работники","Name of the condition for the current number of assigned workers" IgorZ.Automation.Scripting.Expressions.AndOperator,"и","String to join components of the expression with condition AND. Used to represent the condition in the panel" IgorZ.Automation.Scripting.Expressions.OrOperator,"или","String to join components of the expression with condition OR. Used to represent the condition in the panel" IgorZ.Automation.Scripting.Expressions.NotOperator,"НЕ","NOT operator name to represent the condition in the panel"