Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
// License: Public Domain

using System;
using System.Collections.Generic;
using IgorZ.Automation.AutomationSystem;
using IgorZ.Automation.ScriptingEngine.Core;
using IgorZ.Automation.ScriptingEngine.Expressions;
using Timberborn.BaseComponentSystem;
using Timberborn.Bots;
using Timberborn.DwellingSystem;
using Timberborn.GameDistricts;
using Timberborn.Population;
using Timberborn.WorkSystem;

namespace IgorZ.Automation.ScriptingEngine.ScriptableComponents.Components;

Expand All @@ -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

Expand All @@ -30,8 +37,9 @@ sealed class DistrictScriptableComponent : ScriptableComponentBase {

/// <inheritdoc/>
public override string[] GetSignalNamesForBuilding(AutomationBehavior behavior) {
return behavior.GetComponentFast<DistrictBuilding>()
? [BeaverPopulationSignalName, BotPopulationSignalName, NumberOfBedsSignalName]
return behavior.GetComponentFast<DistrictBuilding>()
? [BeaverPopulationSignalName, BotPopulationSignalName, NumberOfBedsSignalName,
UnemployedBeaversSignalName, UnemployedBotsSignalName]
: [];
}

Expand All @@ -45,6 +53,8 @@ public override Func<ScriptValue> GetSignalSource(string name, AutomationBehavio
BeaverPopulationSignalName => () => BeaverPopulationSignal(districtBuilding),
BotPopulationSignalName => () => BotPopulationSignal(districtBuilding),
NumberOfBedsSignalName => () => NumberOfBedsSignal(districtBuilding),
UnemployedBeaversSignalName => () => UnemployedBeaversSignal(districtBuilding),
UnemployedBotsSignalName => () => UnemployedBotsSignal(districtBuilding),
_ => throw new UnknownSignalException(name),
};
}
Expand All @@ -59,14 +69,17 @@ public override SignalDef GetSignalDefinition(string name, AutomationBehavior be
BeaverPopulationSignalName => BeaverPopulationSignalDef,
BotPopulationSignalName => BotPopulationSignalDef,
NumberOfBedsSignalName => NumberOfBedsSignalDef,
UnemployedBeaversSignalName => UnemployedBeaversSignalDef,
UnemployedBotsSignalName => UnemployedBotsSignalDef,
_ => throw new UnknownSignalException(name),
};
}

/// <inheritdoc/>
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<DistrictChangeTracker>().AddSignal(signalOperator, host);
Expand Down Expand Up @@ -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);
}
Expand All @@ -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
Expand All @@ -145,6 +199,7 @@ static ScriptValue NumberOfBedsSignal(DistrictBuilding districtBuilding) {
sealed class DistrictChangeTracker : AbstractStatusTracker {

DistrictCenter _currentDistrictCenter;
readonly List<Workplace> _trackedWorkplaces = new();

void Start() {
var districtBuilding = GetComponentFast<DistrictBuilding>();
Expand All @@ -154,6 +209,7 @@ void Start() {
}

void UpdateDistrictCenter() {
UnsubscribeFromWorkplaces();
if (_currentDistrictCenter) {
_currentDistrictCenter.DistrictPopulation.CitizenAssigned -= OnCitizenAssigned;
_currentDistrictCenter.DistrictPopulation.CitizenUnassigned -= OnCitizenUnassigned;
Expand All @@ -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>()) {
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();
Expand All @@ -185,18 +260,35 @@ void OnCitizenUnassigned(object sender, CitizenUnassignedEventArgs args) {
void OnPopulationChangedEvent(Citizen citizen = null) {
if (!citizen || citizen.GetComponentFast<BotSpec>()) {
ScheduleSignal(BotPopulationSignalName, ignoreErrors: true);
ScheduleSignal(UnemployedBotsSignalName, ignoreErrors: true);
}
if (!citizen || !citizen.GetComponentFast<BotSpec>()) {
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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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

/// <inheritdoc/>
public override string Name => "Workplace";

/// <inheritdoc/>
public override string[] GetSignalNamesForBuilding(AutomationBehavior behavior) {
var workplace = GetWorkplace(behavior, throwIfNotFound: false);
return workplace ? [AssignedWorkersSignalName] : [];
}

/// <inheritdoc/>
public override Func<ScriptValue> GetSignalSource(string name, AutomationBehavior behavior) {
var workplace = GetWorkplace(behavior);
return name switch {
AssignedWorkersSignalName => () => ScriptValue.FromInt(workplace.NumberOfAssignedWorkers),
_ => throw new UnknownSignalException(name),
};
}

/// <inheritdoc/>
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),
};
}

/// <inheritdoc/>
public override void RegisterSignalChangeCallback(SignalOperator signalOperator, ISignalListener host) {
if (signalOperator.SignalName is not AssignedWorkersSignalName) {
throw new InvalidOperationException("Unknown signal: " + signalOperator.SignalName);
}
host.Behavior.GetOrCreate<WorkplaceChangeTracker>().AddSignal(signalOperator, host);
}

/// <inheritdoc/>
public override void UnregisterSignalChangeCallback(SignalOperator signalOperator, ISignalListener host) {
host.Behavior.GetOrThrow<WorkplaceChangeTracker>().RemoveSignal(signalOperator, host);
}

/// <inheritdoc/>
public override string[] GetActionNamesForBuilding(AutomationBehavior behavior) {
var workplace = GetWorkplace(behavior, throwIfNotFound: false);
return workplace ? [RemoveWorkersActionName, SetWorkersActionName] : [];
if (!workplace) {
return [];
}
var workplacePriority = behavior.GetComponentFast<WorkplacePriority>();
return workplacePriority
? [RemoveWorkersActionName, SetWorkersActionName, SetPriorityActionName]
: [RemoveWorkersActionName, SetWorkersActionName];
}

/// <inheritdoc/>
Expand All @@ -36,6 +86,7 @@ public override Action<ScriptValue[]> GetActionExecutor(string name, AutomationB
return name switch {
RemoveWorkersActionName => _ => ResetWorkersAction(workplace),
SetWorkersActionName => args => SetWorkersAction(workplace, args),
SetPriorityActionName => args => SetPriorityAction(behavior, args),
_ => throw new UnknownActionException(name),
};
}
Expand All @@ -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 {
Expand All @@ -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();
Expand All @@ -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<Priority>(priorityName, out var priority)) {
throw new ScriptError.ValueOutOfRange($"Unknown priority: {priorityName}");
}
var workplacePriority = behavior.GetComponentFast<WorkplacePriority>();
if (!workplacePriority) {
throw new ScriptError.BadStateError(behavior, "Building doesn't have WorkplacePriority");
}
if (workplacePriority.Priority == priority) {
return;
}
workplacePriority.SetPriority(priority);
}

#endregion

#region Implementation
Expand All @@ -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>();
workplace.WorkerAssigned += OnWorkerChanged;
workplace.WorkerUnassigned += OnWorkerChanged;
}

void OnWorkerChanged(object sender, WorkerChangedEventArgs args) {
ScheduleSignal(AssignedWorkersSignalName, ignoreErrors: true);
}
}

#endregion
}
Loading