Why good meals go to waste

This is a continuation of

and mostly for devs.

Looking at the code of ConsumerProducer, in particular at its method UpdateSimulationThread() we can learn that the basic order is to first calculate incoming traffic and then outgoing traffic.

The incoming traffic (UpdateIncomingTraffic()) is receiving any car that made it inside and, if playing with return trips, send a return trip on the road if the road is free. Otherwise the return trip simply won’t happen.

The outgoing traffic (UpdateOutgoingTraffic()) is where it gets interesting. Every 50th frame it will compute only. Also, there is a check for a free outgoing lane at the beginning but it is not functional:

		if (building.outgoingLane != null)
		{
			building.outgoingLane.CanPushOntoLane();
		}

This should be:

		if (building.outgoingLane != null || !building.outgoingLane.CanPushOntoLane())
		{
			return;
		}

Since this check doesn’t work, what can and will happen is that a new delivery is requested and when it is later sent (in the same frame I think?), it may fail to do so because UpdateIncomingTraffic() just sent a return truck on the outgoingLane, causing the normal delivery (loaded with a good meal or some other good) to be destroyed:

				if (!deliverySuggestion.producer.building.outgoingLane.TryPushOntoLane(car))
				{
					car.Destroy();
				}

This code fragment is from OrderManager.UpdateSimulationThread(), which is responsible for putting the deliveries onto the road… without any check whether they are free or not. Since return trips happen immediately when a car needs to turn around in the factory but normal deliveries are only checked every 50th frame and have a non-working CanPushOntoLane check, if a return trip happens in the roughly 50 frames before a delivery is made, then the delivery will be destroyed and the good lost. So that makes the calculation off by quite much. This mostly happens to good meals for reasons related to the production numbers, in particular because of the 6s production time and 2 goods being produced at the same time.


So fixing the check about the outgoing lane being free is one point that helps tremendously. Something else that I noticed is that the delivery goes through the OrderManager, but I don’t see any reason for that all with my limited understanding of the code base. It seems much easier to dispatch the car straight away from ConsumerProducer.UpdateOutgoingTraffic(). I build a mod to test this patch and it works with no perceived performance penalty. If anything it seems to run even a little better. The (harmony-patched) version of the method now looks like this, __instance roughly being this and return false just indicating to skip the original method entirely.:

        [HarmonyPatch("UpdateOutgoingTraffic")]
        public static bool Prefix(ConsumerProducer __instance) {
            if ((Old.GetSimulator().simulationFrame + __instance.building.GetID()) % 10 != 0)
            {
                return false;
            }
            if (__instance.building.outgoingLane == null || !__instance.building.outgoingLane.CanPushOntoLane())
            {
                return false;
            }
            int producableIndex = 0;
            foreach (Resource producable in __instance.productionLogic.GetProducables())
            {
                PipeComponent pipeComponentForResource = __instance.building.GetPipeComponentForResource(producable);
                if (pipeComponentForResource != null)
                {
                    __instance.outgoingStorage[producableIndex] -= pipeComponentForResource.PutIntoComponent(__instance.outgoingStorage[producableIndex]);
                }
                else
                {
                    int deliverySize = Mathf.Min(WorldScripts.Inst.worldSettings.GetDefaultCarTransportCapacity(), producable.maxOrderStackSize);
                    if (__instance.outgoingStorage[producableIndex] >= deliverySize)
                    {
                        List<Pair<ConsumerProducer, float>> UpdateOutgoingTraffic_suitableConsumers = new List<Pair<ConsumerProducer, float>>();
                        WorldScripts.Inst.orderManager.GetInterestedConsumers(producable, UpdateOutgoingTraffic_suitableConsumers, __instance);
                        foreach (Pair<ConsumerProducer, float> suitableConsumers in UpdateOutgoingTraffic_suitableConsumers.OrderBy((Pair<ConsumerProducer, float> x) => __instance.GetConsumptionPriorityQuick(x.first, x.second)))
                        {
                            if (suitableConsumers.second > 0f)
                            {
                                List<PathPart> path = PathFinderFacadeRoads.GetPath(__instance.building.outgoingLane, suitableConsumers.first.building.incomingLane);
                                if (path != null)
                                {
                                    __instance.outgoingStorage[producableIndex] -= deliverySize;
                                    Car car = new Car(producable, deliverySize, __instance.building, suitableConsumers.first.building, path);
                                    if (!__instance.building.outgoingLane.TryPushOntoLane(car))
                                    {
                                        car.Destroy();
                                    }
                                    return false;
                                }
                            }
                        }
                    }
                }
                producableIndex++;
            }
            return false;
        }

In the example mod I also changed the 50 frame check to a 10 frame check. I suspect that when the game runs too slow, then 50 frames is actually longer than the production speed of buildings. But my understanding of what happens each frame might be incomplete here. Anyway, you probably don’t gain all that much performance by the mod 50 trick. 10 works fine in my savegames, it feels no different.


Is this the end of wasted good meals and the production numbers not matching? No, unfortunately not. I have 193 meals/minute production versus 182 consumption and it’s not quite sufficient yet. My Player.log is full of reports of cars being deleted from some yet unknown other source, so I suspect some are lost in there and I fear there is yet another issue at work that is making goods disappear.

Also, now a lot of return trips are actually not happening anymore. They check whether the outgoing lane is free at the time the good arrives in the factory, but if it’s not then the return trip is simply skipped. So this kind of patch only shifts the problem from forward cars being deleted and to return cars being deleted, which is not really “fixing” the bug. In order for this to work properly, return routes need to be queued up in the building and sent as soon as the road is free, and with priority over forward deliveries (or else they may queue up indefinitely in the building, like it can happen at station outlets).

Which brings me to my third problem related to good meal factories that still needs to be solved. Even if all of the above is working, you still won’t get the good meals right, at least not if the factory is fully staffed and working quite efficiently. For example, my factories are running at 150+% efficiency. At this rate, they produce every 4s. They also need to send out two goods and 2 return trips within those 4 seconds. I am not exactly sure on the limit, but I think side roads cannot handle that much traffic. Or even if they can, then it’s probably very close to the limit and just a little more efficiency and it’s simply impossible to get all deliveries done. So the balancing of the factory needs to change. Simply make it slower so we need to build more of them would be an easy solution.

I hope I’ve done my research correctly, this was a lot of text :joy:
If anyone wants to test the mod, I can upload it to the workshop. Just haven’t done so yet because it is intruding quite deeply in the core InfraSpace game internals, which is very dangerous to say the least.

1 Like

Thanks for the big investigation. I only skimmed it for now, but some comments already:

The check looks fine in the original source of InfraSpace’s ConsumerProducer:

the reason we need to go through ORdermanager for delivery is here (UpdateSimulationThread):

basically we need to make sure the request counts of consumers are up to date.

Imagine this situation: full storages of electronics, suddenly one microchip factory needs a chip.
immediately, all the electronics factory will decide to send a piece of electronics, flodding the one microchip factory.

But I commend you on the good understanding so far.

You check alright and then assign to canDeliverTrucks, but is this local variable actually checked further down in the code? Normally VS should warn about unused variables and there is no underlining there, so it looks fine, but it’s probably worth double checking if canDeliverTrucks is actually checked before trying to send a car onto the road. I investigated with dnspy, ILspy and the VS built-in decompiler (which is basically ILspy I think?) and they all show me very similar results. The full version of ILspy looks like this:

		if (building.outgoingLane != null)
		{
			building.outgoingLane.CanPushOntoLane();
		}
		else
			_ = 0;

If canDeliverTrucks is indeed only written but never read, then this output by the decompiler makes sense. It can skip the variable entirely and it needs to put it into an IF statement to check the first and if that fails, it assigns a null value to an arbitrary temporary variable. If the first condition is true, it needs to do the second check (CanPushOntoLane), which may have side effects, so it cannot simply remove the entire original line from the compilation.


I had a look at the order manager again and I think I get your point. The thing about “checking every 50 frames” only is the same here though as with UpdateOutgoingTraffic() and can probably be done faster.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.