-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEventListener.cs
More file actions
356 lines (315 loc) · 14.1 KB
/
EventListener.cs
File metadata and controls
356 lines (315 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
using Bindito.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using Timberborn.Buildings;
using Timberborn.BuildingsBlocking;
using Timberborn.GameDistricts;
using Timberborn.Goods;
using Timberborn.InventorySystem;
using Timberborn.SingletonSystem;
using Timberborn.Stockpiles;
using Timberborn.TimeSystem;
using Timberborn.Workshops;
namespace AutoRecipe
{
public class EventListener : ILoadableSingleton
{
private EventBus eventBus;
private DistrictCenterRegistry centerRegistry;
[Inject]
public void InjectDependencies(EventBus inEventBus, DistrictCenterRegistry inRegistry)
{
eventBus = inEventBus;
centerRegistry = inRegistry;
}
public void Load()
{
//Register for events from the bus
eventBus.Register(this);
}
[OnEvent]
public void OnDistrictCenterRegistryChanged(DistrictCenterRegistryChangedEvent districtCenterRegistryChanged)
{
//Update registrations for building added and removed events
foreach (DistrictCenter current in centerRegistry.FinishedDistrictCenters)
{
//Get building registered and unregistered events
current.DistrictBuildingRegistry.FinishedBuildingRegistered -= DistrictBuildingRegistry_FinishedBuildingRegistered;
current.DistrictBuildingRegistry.FinishedBuildingRegistered += DistrictBuildingRegistry_FinishedBuildingRegistered;
current.DistrictBuildingRegistry.FinishedBuildingUnregistered -= DistrictBuildingRegistry_FinishedBuildingUnregistered;
current.DistrictBuildingRegistry.FinishedBuildingUnregistered += DistrictBuildingRegistry_FinishedBuildingUnregistered;
}
}
public void DistrictBuildingRegistry_FinishedBuildingRegistered(object sender, FinishedBuildingRegisteredEventArgs e)
{
//Manufactories can make goods and have recipes
Manufactory manufactory;
if (e.Building.TryGetComponentFast<Manufactory>(out manufactory))
{
if (!manufactory.name.Contains("Refinery"))
{
//Attach to production finished event
manufactory.ProductionFinished -= Manufactory_ProductionFinished;
manufactory.ProductionFinished += Manufactory_ProductionFinished;
//Attempt a swap to set a reasonable recipe to start with
AttemptRecipeSwap(manufactory, null);
}
}
}
public void DistrictBuildingRegistry_FinishedBuildingUnregistered(object sender, FinishedBuildingUnregisteredEventArgs e)
{
//If this is a manufactory, Detach from its ProductionFinished event
Manufactory manufactory;
if (e.Building.TryGetComponentFast<Manufactory>(out manufactory))
{
manufactory.ProductionFinished -= Manufactory_ProductionFinished;
}
}
private void Manufactory_ProductionFinished(object sender, EventArgs e)
{
//Cast sender
Manufactory manufactory = (Manufactory)sender;
//If there is no recipe selected, attempt a swap
if (manufactory.CurrentRecipe == null)
{
AttemptRecipeSwap(manufactory, null);
return;
}
//Get the inventory state in a way that we have access to here
Dictionary<string, StorageData> inventory = new Dictionary<string, StorageData>();
UpdateStorageData(manufactory.GetComponentFast<Building>(), manufactory.Inventory, inventory);
//Check for ingredients and space for products at the manufactory itself
foreach (GoodAmount ingredient in manufactory.CurrentRecipe.Ingredients)
{
if (!inventory.ContainsKey(ingredient.GoodId) || (inventory[ingredient.GoodId].Stock < ingredient.Amount))
{
AttemptRecipeSwap(manufactory, null);
return;
}
}
//Check for space for products
foreach (GoodAmount product in manufactory.CurrentRecipe.Products)
{
if (!inventory.ContainsKey(product.GoodId) || (inventory[product.GoodId].Capacity - inventory[product.GoodId].Stock < product.Amount))
{
AttemptRecipeSwap(manufactory, null);
return;
}
}
}
[OnEvent]
public void OnDaytimeStart(DaytimeStartEvent daytimeStarted)
{
//Get a list of all the districts
DistrictCenter[] centers = centerRegistry.FinishedDistrictCenters.ToArray();
//Go through each district and attempt swaps on all manufactories
foreach (DistrictCenter center in centers)
{
//Compute center inventory
Dictionary<string, StorageData> districtInventory = GetInventoryData(center);
//Get the list of manufactories and attempt swaps
foreach (Manufactory manufactory in center.DistrictBuildingRegistry.GetEnabledBuildingsInstant<Manufactory>())
{
//Don't attempt a swap if this manufactory is paused
PausableBuilding pause = manufactory.GetComponentFast<PausableBuilding>();
if (pause == null || pause.Paused)
{
continue;
}
//Don't attempt a swap if production is in progress
if (manufactory.ProductionProgress != 0)
{
continue;
}
//Don't swap recipes at the refinery
if (manufactory.name.Contains("Refinery"))
{
continue;
}
//This manufactory might be stuck, attempt a swap
AttemptRecipeSwap(manufactory, districtInventory);
}
}
}
public void AttemptRecipeSwap(Manufactory manufactory, Dictionary<string, StorageData> districtInventory)
{
//Get a list of recipes that are considerable without computing inventory
List<RecipeSpecification> validRecipes = new List<RecipeSpecification>();
foreach (RecipeSpecification recipe in manufactory.ProductionRecipes)
{
//Don't consider recipes with no goods outputs
if (recipe.Products.Count == 0)
{
continue;
}
//All checks passed
validRecipes.Add(recipe);
}
//If there are no valid choices, there's nothing to do here
if (validRecipes.Count == 0)
{
return;
}
//If there's only one valid choice, there's no reason to waste time computing inventory levels
if (validRecipes.Count == 1)
{
manufactory.SetRecipe(validRecipes[0]);
return;
}
//Get inventory levels if required
if (districtInventory == null)
{
DistrictBuilding districtBuilding;
if (!manufactory.TryGetComponentFast<DistrictBuilding>(out districtBuilding))
{
manufactory.SetRecipe(null);
return;
}
DistrictCenter center = districtBuilding.District;
if (center == null)
{
manufactory.SetRecipe(null);
return;
}
districtInventory = GetInventoryData(center);
}
//Use the inventory data to decide which recipe to swap to (if any)
string minGoodId = "";
StorageData minStorage = null;
RecipeSpecification minRecipe = null;
bool currentRecipeValid = false;
foreach (RecipeSpecification currentRecipe in validRecipes)
{
//Check for recipe validity
if (!CheckRecipeValid(districtInventory, currentRecipe))
{
continue;
}
//Mark the current recipe as valid
if (currentRecipe.Equals(manufactory.CurrentRecipe))
{
currentRecipeValid = true;
}
//Check storage levels and pick the recipe with the lowest % filled storage
foreach (GoodAmount currentProduct in currentRecipe.Products)
{
if (districtInventory[currentProduct.GoodId].CompareTo(minStorage) < 0)
{
minGoodId = currentProduct.GoodId;
minStorage = districtInventory[currentProduct.GoodId];
minRecipe = currentRecipe;
}
}
}
//If no valid recipe was found, no swap is needed
if (minRecipe == null)
{
return;
}
//If there is no current recipe, we should definitely swap to whatever was found
if (manufactory.CurrentRecipe == null)
{
manufactory.SetRecipe(minRecipe);
return;
}
//If the current recipe already produces the minimum storage output and is valid, don't swap
foreach (GoodAmount product in manufactory.CurrentRecipe.Products)
{
if (product.GoodId.Equals(minGoodId) && currentRecipeValid)
{
return;
}
}
//If the new recipe and the old recipe are the same, don't swap
if (minRecipe.Equals(manufactory.CurrentRecipe))
{
return;
}
//Swap the recipe to teh best candidate
manufactory.SetRecipe(minRecipe);
//Remove the ingredients for this recipe from the district storage tracker
foreach (GoodAmount current in minRecipe.Ingredients)
{
districtInventory[current.GoodId].RemoveStock(current.Amount);
}
}
public Dictionary<string, StorageData> GetInventoryData(DistrictCenter center)
{
//Get all buildings in this district that are not paused, and get a list of all manufactories
List<Building> buildings = new List<Building>();
foreach (Building current in center.DistrictBuildingRegistry.GetEnabledBuildingsInstant<Building>())
{
PausableBuilding pause = current.GetComponentFast<PausableBuilding>();
if (pause != null && !pause.Paused)
{
buildings.Add(current);
}
}
//Build inventory stats and get list of valid manufactories to consider swapping recipes at
Dictionary<string, StorageData> districtInventory = new Dictionary<string, StorageData>();
foreach (Building currentBuilding in buildings)
{
//Update inventory: Do not include inventories for construction sites or district crossings
Inventory inventory = currentBuilding.GetComponentFast<Inventory>();
if (inventory != null && !inventory.ComponentName.Contains("ConstructionSite") && !inventory.ComponentName.Contains("DistrictCrossing"))
{
UpdateStorageData(currentBuilding, inventory, districtInventory);
}
}
return districtInventory;
}
public void UpdateStorageData(Building currentBuilding, Inventory inventory, Dictionary<string, StorageData> toUpdate)
{
//Use allowed goods mode for Stockpiles and District Centers
if (currentBuilding.GetComponentFast<Stockpile>() == null && currentBuilding.GetComponentFast<DistrictCenter>() == null)
{
//Update capacity
foreach (StorableGoodAmount current in inventory.AllowedGoods)
{
toUpdate.TryAdd(current.StorableGood.GoodId, new StorageData());
toUpdate[current.StorableGood.GoodId].AddCapacity(current.Amount);
}
}
else
{
//Get capacities of current stock if allowed goods mode is not requested
List<GoodAmount> capacities = new List<GoodAmount>();
inventory.GetCapacity(capacities);
//Update capacity
foreach (GoodAmount current in capacities)
{
toUpdate.TryAdd(current.GoodId, new StorageData());
toUpdate[current.GoodId].AddCapacity(current.Amount);
}
}
//Update stock
foreach (GoodAmount current in inventory.Stock)
{
toUpdate.TryAdd(current.GoodId, new StorageData());
toUpdate[current.GoodId].AddStock(current.Amount);
}
}
public bool CheckRecipeValid(Dictionary<string, StorageData> districtInventory, RecipeSpecification recipe)
{
//Check for supply of all ingredients
foreach (GoodAmount ingredient in recipe.Ingredients)
{
if (!districtInventory.Keys.Contains(ingredient.GoodId) || districtInventory[ingredient.GoodId].Stock < ingredient.Amount)
{
return false;
}
}
//Check for space for all products
foreach (GoodAmount product in recipe.Products)
{
if (!districtInventory.Keys.Contains(product.GoodId) || districtInventory[product.GoodId].Capacity - districtInventory[product.GoodId].Stock < product.Amount)
{
return false;
}
}
//All checks OK
return true;
}
}
}