Skip to content
Closed
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.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full re-subscription on every event can be a problem on big game setups or when many buildings get destroyed at once (e.g. when detonating dynamates). Dam construction is another example of many objects being created without any impact to the workplaces, but they will trigger re-subscription and the relevant events.

The event has the entity that was unregistered. If it's not a workplace - skip. Otherwise, it's only one element to update: subscribe or unsubscribe.

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) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong signal names should be checked here too. It helps catching errors in the core implementation.

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>();
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is known at the stage of getting action def/executor. It should be checked there to allow the parser detecting improper usage, and the priority instance can be passed instead of the behavior.

GetActionNamesForBuilding is only used by the UI constructor, in the plain text mode player can specify any action/signal.

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() {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, in v1.0 the concept of dynamic components has significantly changed. They are not MonoBehaviours anymore.

var workplace = GetComponentFast<Workplace>();
workplace.WorkerAssigned += OnWorkerChanged;
workplace.WorkerUnassigned += OnWorkerChanged;
}

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

#endregion
}
Loading