) GenerateCargoTypesForExistingCars(
            List <TrainCar> orderedTrainCars,
            List <CargoGroup> availableCargoGroups,
            System.Random rng)
        {
            List <CarsPerCargoType>  carsPerCargoTypes = new List <CarsPerCargoType>();
            List <List <CargoType> > orderedCargoTypesPerTrainCar
                = (from tc in orderedTrainCars select GetCargoTypesForCarType(tc.carType)).ToList();
            // find cargo groups that satisfy at least one cargo type for every train car
            List <CargoGroup> filteredCargoGroups = availableCargoGroups
                                                    .Where(cg => orderedCargoTypesPerTrainCar.All(cts => cts.Intersect(cg.cargoTypes).Count() > 0))
                                                    .ToList();

            if (filteredCargoGroups.Count == 0)
            {
                return(null, null);
            }
            CargoGroup       pickedCargoGroup  = GetRandomFromEnumerable <CargoGroup>(filteredCargoGroups, rng);
            List <CargoType> pickedCargoTypes  = pickedCargoGroup.cargoTypes;
            List <CargoType> orderedCargoTypes = orderedCargoTypesPerTrainCar.Select(
                cts => GetRandomFromEnumerable <CargoType>(cts.Intersect(pickedCargoTypes).ToList(), rng)
                ).ToList();

            return(orderedCargoTypes, pickedCargoGroup);
        }
Example #2
0
        public async Task OnGetUiFormat_GoodData_ExpectNoCargo()
        {
            byte       testGroupCode    = 101;
            byte       testGroupCode2   = 102;
            var        testDescription  = "TEST_DESCRIPTION";
            var        testDescription2 = "TEST_DESCRIPTION2";
            string     CargoGroupKey    = "CargoGroupKey";
            string     CargoCategoryKey = "CargoCategoryKey";
            string     expectedResult   = "NoCargo";
            CargoGroup CargoGroup       = new CargoGroup()

            {
                Description = testDescription, IsUnitised = true, GroupCode = testGroupCode, CargoCategory = new List <CargoCategory>()
            };

            var CargoCategories = new List <CargoCategory>()
            {
                new CargoCategory {
                    GroupCode = testGroupCode, CategoryCode = 25, Description = testDescription
                },
                new CargoCategory {
                    GroupCode = testGroupCode2, CategoryCode = 26, Description = testDescription2
                }
            };

            await actualContext.CargoGroup.AddRangeAsync(CargoGroup);

            await actualContext.CargoCategory.AddRangeAsync(CargoCategories);

            actualContext.SaveChanges();

            tempData.Put(CargoGroupKey, CargoGroup);
            tempData.Put(CargoCategoryKey, CargoCategories);

            var model = new CargoDetailsModel(actualContext, cargoPortValidateService)
            {
                PageContext = pageContext,
                TempData    = tempData,
                Url         = new UrlHelper(actionContext)
            };

            var result = model.OnGetUiFormat(testDescription);

            Assert.AreEqual(result.Value, expectedResult);
            Assert.IsInstanceOfType(result, typeof(JsonResult));
        }
            ) GenerateBaseCargoTrainData(
            int minCountCars,
            int maxCountCars,
            List <CargoGroup> availableCargoGroups,
            System.Random rng)
        {
            List <CarTypesPerCargoType> carTypesPerCargoTypes = new List <CarTypesPerCargoType>();
            List <TrainCarType>         allCarTypes           = new List <TrainCarType>();
            int              countCarsInTrain = rng.Next(minCountCars, maxCountCars + 1);
            CargoGroup       pickedCargoGroup = GetRandomFromEnumerable <CargoGroup>(availableCargoGroups, rng);
            List <CargoType> pickedCargoTypes = pickedCargoGroup.cargoTypes;

            pickedCargoTypes = GetMultipleRandomsFromList <CargoType>(
                pickedCargoTypes,
                Math.Min(countCarsInTrain, rng.Next(1, pickedCargoTypes.Count + 1)),
                rng
                );
            int countCargoTypes             = pickedCargoTypes.Count;
            int countCarsPerCargoType       = countCarsInTrain / countCargoTypes;
            int countCargoTypesWithExtraCar = countCarsInTrain % countCargoTypes;

            for (int i = 0; i < countCargoTypes; i++)
            {
                int countCars = i < countCargoTypesWithExtraCar ? countCarsPerCargoType + 1 : countCarsPerCargoType;
                List <CargoContainerType> cargoContainerTypesThatSupportCargoType
                    = CargoTypes.GetCarContainerTypesThatSupportCargoType(pickedCargoTypes[i]);
                List <TrainCarType> trainCarTypesThatAreSpecificContainerType
                    = CargoTypes.GetTrainCarTypesThatAreSpecificContainerType(
                          GetRandomFromEnumerable <CargoContainerType>(cargoContainerTypesThatSupportCargoType, rng)
                          );
                List <TrainCarType> trainCarTypes = new List <TrainCarType>();
                for (int j = 0; j < countCars; j++)
                {
                    trainCarTypes.Add(
                        GetRandomFromEnumerable <TrainCarType>(trainCarTypesThatAreSpecificContainerType, rng));
                }
                carTypesPerCargoTypes
                .Add(new CarTypesPerCargoType(trainCarTypes, pickedCargoTypes[i], (float)trainCarTypes.Count));
                allCarTypes.AddRange(trainCarTypes);
            }
            return(carTypesPerCargoTypes, allCarTypes, pickedCargoGroup);
        }
        public async Task IsValidCategory_WhenPassed_ShouldReturnValidCode()
        {
            string strcategory1   = "Crude Oil";
            string strcategory2   = "Oil Products";
            byte   categoryCode12 = 12;
            byte   categoryCode13 = 13;
            byte   groupCode      = 10;

            var CargoGroup = new CargoGroup()
            {
                Description   = strcategory1,
                IsUnitised    = true,
                GroupCode     = groupCode,
                CargoCategory = new List <CargoCategory>()
            };

            var CargoCategories = new List <CargoCategory>()
            {
                new CargoCategory {
                    GroupCode = groupCode, CategoryCode = categoryCode12, Description = strcategory1
                },
                new CargoCategory {
                    GroupCode = groupCode, CategoryCode = categoryCode13, Description = strcategory2
                }
            };

            await actualContext.CargoGroup.AddRangeAsync(CargoGroup);

            await actualContext.CargoCategory.AddRangeAsync(CargoCategories);

            actualContext.SaveChanges();

            var result   = _helperService.GetCategoryCodeByDescription(strcategory1);
            var category = CargoCategories.Find(x => x.Description == strcategory1).CategoryCode;

            Assert.IsNotNull(result);
            Assert.IsInstanceOfType(result, typeof(byte));
            Assert.IsNotNull(category);
            Assert.AreEqual(result, category);
        }
Example #5
0
        public static JobChainControllerWithEmptyHaulGeneration GenerateShuntingUnloadJobWithCarSpawning(
            StationController destinationStation,
            bool forceLicenseReqs,
            System.Random rng)
        {
            Debug.Log("[PersistentJobs] unload: generating with car spawning");
            YardTracksOrganizer yto = YardTracksOrganizer.Instance;
            List <CargoGroup>   availableCargoGroups = destinationStation.proceduralJobsRuleset.inputCargoGroups;
            int countTrainCars = rng.Next(
                destinationStation.proceduralJobsRuleset.minCarsPerJob,
                destinationStation.proceduralJobsRuleset.maxCarsPerJob);

            if (forceLicenseReqs)
            {
                Debug.Log("[PersistentJobs] unload: forcing license requirements");
                if (!LicenseManager.IsJobLicenseAcquired(JobLicenses.Shunting))
                {
                    Debug.LogError("[PersistentJobs] unload: Trying to generate a ShuntingUnload job with " +
                                   "forceLicenseReqs=true should never happen if player doesn't have Shunting license!");
                    return(null);
                }
                availableCargoGroups
                    = (from cg in availableCargoGroups
                       where LicenseManager.IsLicensedForJob(cg.CargoRequiredLicenses)
                       select cg).ToList();
                countTrainCars
                    = Math.Min(countTrainCars, LicenseManager.GetMaxNumberOfCarsPerJobWithAcquiredJobLicenses());
            }
            if (availableCargoGroups.Count == 0)
            {
                Debug.LogWarning("[PersistentJobs] unload: no available cargo groups");
                return(null);
            }

            CargoGroup chosenCargoGroup = Utilities.GetRandomFromEnumerable(availableCargoGroups, rng);

            // choose cargo & trainCar types
            Debug.Log("[PersistentJobs] unload: choosing cargo & trainCar types");
            List <CargoType>    availableCargoTypes  = chosenCargoGroup.cargoTypes;
            List <CargoType>    orderedCargoTypes    = new List <CargoType>();
            List <TrainCarType> orderedTrainCarTypes = new List <TrainCarType>();

            for (int i = 0; i < countTrainCars; i++)
            {
                CargoType chosenCargoType = Utilities.GetRandomFromEnumerable(availableCargoTypes, rng);
                List <CargoContainerType> availableContainers
                    = CargoTypes.GetCarContainerTypesThatSupportCargoType(chosenCargoType);
                CargoContainerType  chosenContainerType = Utilities.GetRandomFromEnumerable(availableContainers, rng);
                List <TrainCarType> availableTrainCarTypes
                    = CargoTypes.GetTrainCarTypesThatAreSpecificContainerType(chosenContainerType);
                TrainCarType chosenTrainCarType = Utilities.GetRandomFromEnumerable(availableTrainCarTypes, rng);
                orderedCargoTypes.Add(chosenCargoType);
                orderedTrainCarTypes.Add(chosenTrainCarType);
            }
            float approxTrainLength = yto.GetTotalCarTypesLength(orderedTrainCarTypes)
                                      + yto.GetSeparationLengthBetweenCars(countTrainCars);

            // choose starting track
            Debug.Log("[PersistentJobs] unload: choosing starting track");
            Track startingTrack
                = Utilities.GetTrackThatHasEnoughFreeSpace(yto, destinationStation.logicStation.yard.TransferInTracks, approxTrainLength);

            if (startingTrack == null)
            {
                Debug.LogWarning("[PersistentJobs] unload: Couldn't find startingTrack with enough free space for train!");
                return(null);
            }

            // choose random starting station
            // no need to ensure it has has free space; this is just a back story
            Debug.Log("[PersistentJobs] unload: choosing origin (inconsequential)");
            List <StationController> availableOrigins = new List <StationController>(chosenCargoGroup.stations);
            StationController        startingStation  = Utilities.GetRandomFromEnumerable(availableOrigins, rng);

            // spawn trainCars
            Debug.Log("[PersistentJobs] unload: spawning trainCars");
            RailTrack       railTrack        = SingletonBehaviour <LogicController> .Instance.LogicToRailTrack[startingTrack];
            List <TrainCar> orderedTrainCars = CarSpawner.SpawnCarTypesOnTrack(
                orderedTrainCarTypes,
                railTrack,
                true,
                0.0,
                false,
                true);

            if (orderedTrainCars == null)
            {
                Debug.LogWarning("[PersistentJobs] unload: Failed to spawn trainCars!");
                return(null);
            }

            JobChainControllerWithEmptyHaulGeneration jcc = GenerateShuntingUnloadJobWithExistingCars(
                startingStation,
                startingTrack,
                destinationStation,
                orderedTrainCars,
                orderedCargoTypes,
                rng,
                true);

            if (jcc == null)
            {
                Debug.LogWarning("[PersistentJobs] unload: Couldn't generate job chain. Deleting spawned trainCars!");
                SingletonBehaviour <CarSpawner> .Instance.DeleteTrainCars(orderedTrainCars, true);

                return(null);
            }

            return(jcc);
        }
Example #6
0
        private void CargoItemValidation(MSD1 localMSD1, CargoCategory currentCargoCategory, CargoGroup currentCargoGroup)
        {
            ManualModelValidation(localMSD1);

            CargoCategoryHardValidations(localMSD1, currentCargoCategory);
            bool selectedCategory12 = currentCargoCategory.CategoryCode == (int)PortCategory.CrudeOilCode;
            bool selectedCategory13 = currentCargoCategory.CategoryCode == (int)PortCategory.OilProductsCode;

            bool isValidCategory = _cargoPortValidateService.IsValidPortForCategory(currentCargoCategory.CategoryCode, localMSD1.ReportingPort, localMSD1.AssociatedPort, localMSD1.IsInbound);

            if (!isValidCategory)
            {
                if (selectedCategory12)
                {
                    ModelState.AddModelError("CargoItem.Category", $"Crude Oil cannot be carried through " + localMSD1.ReportingPort);
                }
                if (selectedCategory13)
                {
                    var portName = (localMSD1.IsInbound == true ? localMSD1.AssociatedPort : localMSD1.ReportingPort);
                    ModelState.AddModelError("CargoItem.Category", $"Oil Products cannot be carried through " + portName);
                }
            }

            bool isValidCategoryVessel = _cargoPortValidateService.IsValidVesselCargo(currentCargoCategory.CategoryCode, localMSD1.Imo);

            if (!isValidCategoryVessel)
            {
                ModelState.AddModelError("CargoItem.Category", $"Category '" + currentCargoCategory.Description + "' is not valid for the vessel '" + localMSD1.Imo + "-" + localMSD1.ShipName + "'");
            }
        }
        // override/replacement for UnusedTrainCarDeleter.TrainCarsDeleteCheck coroutine
        // tries to generate new jobs for the train cars marked for deletion
        public static IEnumerator TrainCarsCreateJobOrDeleteCheck(float period, float interopPeriod)
        {
            List <TrainCar> trainCarsToDelete              = null;
            List <TrainCar> trainCarCandidatesForDelete    = null;
            Traverse        unusedTrainCarDeleterTraverser = null;
            List <TrainCar> unusedTrainCarsMarkedForDelete = null;
            Dictionary <TrainCar, DV.CarVisitChecker> carVisitCheckersMap = null;
            Traverse AreDeleteConditionsFulfilledMethod = null;

            try
            {
                trainCarsToDelete              = new List <TrainCar>();
                trainCarCandidatesForDelete    = new List <TrainCar>();
                unusedTrainCarDeleterTraverser = Traverse.Create(SingletonBehaviour <UnusedTrainCarDeleter> .Instance);
                unusedTrainCarsMarkedForDelete = unusedTrainCarDeleterTraverser
                                                 .Field("unusedTrainCarsMarkedForDelete")
                                                 .GetValue <List <TrainCar> >();
                carVisitCheckersMap = unusedTrainCarDeleterTraverser
                                      .Field("carVisitCheckersMap")
                                      .GetValue <Dictionary <TrainCar, DV.CarVisitChecker> >();
                AreDeleteConditionsFulfilledMethod
                    = unusedTrainCarDeleterTraverser.Method("AreDeleteConditionsFulfilled", new Type[] { typeof(TrainCar) });
            }
            catch (Exception e)
            {
                Main.modEntry.Logger.Error(
                    $"Exception thrown during TrainCarsCreateJobOrDeleteCheck setup:\n{e.ToString()}");
                Main.OnCriticalFailure();
            }
            for (; ;)
            {
                yield return(WaitFor.SecondsRealtime(period));

                try
                {
                    if (PlayerManager.PlayerTransform == null || FastTravelController.IsFastTravelling)
                    {
                        continue;
                    }

                    if (unusedTrainCarsMarkedForDelete.Count == 0)
                    {
                        if (carVisitCheckersMap.Count != 0)
                        {
                            carVisitCheckersMap.Clear();
                        }
                        continue;
                    }
                }
                catch (Exception e)
                {
                    Main.modEntry.Logger.Error(
                        $"Exception thrown during TrainCarsCreateJobOrDeleteCheck skip checks:\n{e.ToString()}");
                    Main.OnCriticalFailure();
                }

                Debug.Log("[PersistentJobs] collecting deletion candidates... (coroutine)");
                try
                {
                    trainCarCandidatesForDelete.Clear();
                    for (int i = unusedTrainCarsMarkedForDelete.Count - 1; i >= 0; i--)
                    {
                        TrainCar trainCar = unusedTrainCarsMarkedForDelete[i];
                        if (trainCar == null)
                        {
                            unusedTrainCarsMarkedForDelete.RemoveAt(i);
                        }
                        else if (AreDeleteConditionsFulfilledMethod.GetValue <bool>(trainCar))
                        {
                            unusedTrainCarsMarkedForDelete.RemoveAt(i);
                            trainCarCandidatesForDelete.Add(trainCar);
                        }
                    }
                    Debug.Log(
                        $"[PersistentJobs] found {trainCarCandidatesForDelete.Count} cars marked for deletion (coroutine)");
                    if (trainCarCandidatesForDelete.Count == 0)
                    {
                        continue;
                    }
                }
                catch (Exception e)
                {
                    Main.modEntry.Logger.Error(
                        $"Exception thrown during TrainCarsCreateJobOrDeleteCheck delete candidate collection:\n{e.ToString()}");
                    Main.OnCriticalFailure();
                }

                yield return(WaitFor.SecondsRealtime(interopPeriod));

                // ------ BEGIN JOB GENERATION ------
                // group trainCars by trainset
                Debug.Log("[PersistentJobs] grouping trainCars by trainSet... (coroutine)");
                Dictionary <Trainset, List <TrainCar> > paxTrainCarsPerTrainSet    = null;
                Dictionary <Trainset, List <TrainCar> > emptyTrainCarsPerTrainSet  = null;
                Dictionary <Trainset, List <TrainCar> > loadedTrainCarsPerTrainSet = null;
                try
                {
                    List <TrainCar> paxTrainCars = trainCarCandidatesForDelete
                                                   .Where(tc => Utilities.IsPassengerCar(tc.carType))
                                                   .ToList();
                    List <TrainCar> nonLocoOrPaxTrainCars = trainCarCandidatesForDelete
                                                            .Where(tc => !CarTypes.IsAnyLocomotiveOrTender(tc.carType) && !Utilities.IsPassengerCar(tc.carType))
                                                            .ToList();
                    List <TrainCar> emptyFreightCars = nonLocoOrPaxTrainCars
                                                       .Where(tc => tc.logicCar.CurrentCargoTypeInCar == CargoType.None ||
                                                              tc.logicCar.LoadedCargoAmount < 0.001f)
                                                       .ToList();
                    List <TrainCar> loadedFreightCars = nonLocoOrPaxTrainCars
                                                        .Where(tc => tc.logicCar.CurrentCargoTypeInCar != CargoType.None &&
                                                               tc.logicCar.LoadedCargoAmount >= 0.001f)
                                                        .ToList();

                    paxTrainCarsPerTrainSet = JobProceduralGenerationUtilities
                                              .GroupTrainCarsByTrainset(paxTrainCars);
                    emptyTrainCarsPerTrainSet = JobProceduralGenerationUtilities
                                                .GroupTrainCarsByTrainset(emptyFreightCars);
                    loadedTrainCarsPerTrainSet = JobProceduralGenerationUtilities
                                                 .GroupTrainCarsByTrainset(loadedFreightCars);
                }
                catch (Exception e)
                {
                    Main.modEntry.Logger.Error(
                        $"Exception thrown during TrainCarsCreateJobOrDeleteCheck trainset grouping:\n{e.ToString()}");
                    Main.OnCriticalFailure();
                }
                Debug.Log(
                    $"[PersistentJobs]\n" +
                    $"    found {paxTrainCarsPerTrainSet.Count} passenger trainSets,\n" +
                    $"    {emptyTrainCarsPerTrainSet.Count} empty trainSets,\n" +
                    $"    and {loadedTrainCarsPerTrainSet.Count} loaded trainSets (coroutine)");

                yield return(WaitFor.SecondsRealtime(interopPeriod));

                // group trainCars sets by nearest stationController
                Debug.Log("[PersistentJobs] grouping trainSets by nearest station... (coroutine)");
                Dictionary <StationController, List <(List <TrainCar>, List <CargoGroup>)> > paxTcsPerSc          = null;
                Dictionary <StationController, List <(List <TrainCar>, List <CargoGroup>)> > emptyCgsPerTcsPerSc  = null;
                Dictionary <StationController, List <(List <TrainCar>, List <CargoGroup>)> > loadedCgsPerTcsPerSc = null;
                try
                {
                    paxTcsPerSc = JobProceduralGenerationUtilities
                                  .GroupTrainCarSetsByNearestStation(paxTrainCarsPerTrainSet);
                    emptyCgsPerTcsPerSc = JobProceduralGenerationUtilities
                                          .GroupTrainCarSetsByNearestStation(emptyTrainCarsPerTrainSet);
                    loadedCgsPerTcsPerSc = JobProceduralGenerationUtilities
                                           .GroupTrainCarSetsByNearestStation(loadedTrainCarsPerTrainSet);
                }
                catch (Exception e)
                {
                    Main.modEntry.Logger.Error(
                        $"Exception thrown during TrainCarsCreateJobOrDeleteCheck station grouping:\n{e.ToString()}");
                    Main.OnCriticalFailure();
                }
                Debug.Log(
                    $"[PersistentJobs]\n" +
                    $"    found {paxTcsPerSc.Count} stations for passenger trainSets\n," +
                    $"    {emptyCgsPerTcsPerSc.Count} stations for empty trainSets\n," +
                    $"    and {loadedCgsPerTcsPerSc.Count} stations for loaded trainSets (coroutine)");

                yield return(WaitFor.SecondsRealtime(interopPeriod));

                // populate possible cargoGroups per group of trainCars
                Dictionary <StationController, List <List <TrainCar> > > emptyTcsPerSc = null;
                Debug.Log("[PersistentJobs] populating cargoGroups... (coroutine)");
                try
                {
                    JobProceduralGenerationUtilities.PopulateCargoGroupsPerTrainCarSet(emptyCgsPerTcsPerSc);
                    JobProceduralGenerationUtilities.PopulateCargoGroupsPerLoadedTrainCarSet(loadedCgsPerTcsPerSc);
                    emptyTcsPerSc = JobProceduralGenerationUtilities.ExtractEmptyHaulTrainSets(emptyCgsPerTcsPerSc);
                }
                catch (Exception e)
                {
                    Main.modEntry.Logger.Error(
                        $"Exception thrown during TrainCarsCreateJobOrDeleteCheck cargoGroup population:\n{e.ToString()}");
                    Main.OnCriticalFailure();
                }

                yield return(WaitFor.SecondsRealtime(interopPeriod));

                // pick new jobs for the trainCars at each station
                Debug.Log("[PersistentJobs] picking jobs... (coroutine)");
                System.Random rng = new System.Random(Environment.TickCount);
                List <(StationController, List <CarsPerTrack>, StationController, List <TrainCar>, List <CargoType>)>
                shuntingLoadJobInfos = null;
                List <(StationController, Track, StationController, List <TrainCar>, List <CargoType>)>
                transportJobInfos = null;
                List <(StationController, Track, StationController, List <TrainCar>, List <CargoType>)>
                shuntingUnloadJobInfos = null;
                try
                {
                    shuntingLoadJobInfos = ShuntingLoadJobProceduralGenerator
                                           .ComputeJobInfosFromCargoGroupsPerTrainCarSetPerStation(emptyCgsPerTcsPerSc, rng);

                    transportJobInfos = TransportJobProceduralGenerator
                                        .ComputeJobInfosFromCargoGroupsPerTrainCarSetPerStation(
                        loadedCgsPerTcsPerSc.Select(kv => (
                                                        kv.Key,
                                                        kv.Value.Where(tpl => {
                        CargoGroup cg0 = tpl.Item2.FirstOrDefault();
                        return(cg0 != null && kv.Key.proceduralJobsRuleset.outputCargoGroups.Contains(cg0));
                    }).ToList()))
                        .Where(tpl => tpl.Item2.Count > 0)
                        .ToDictionary(tpl => tpl.Item1, tpl => tpl.Item2),
                        rng);

                    shuntingUnloadJobInfos = ShuntingUnloadJobProceduralGenerator
                                             .ComputeJobInfosFromCargoGroupsPerTrainCarSetPerStation(
                        loadedCgsPerTcsPerSc.Select(kv => (
                                                        kv.Key,
                                                        kv.Value.Where(tpl => {
                        CargoGroup cg0 = tpl.Item2.FirstOrDefault();
                        return(cg0 != null && kv.Key.proceduralJobsRuleset.inputCargoGroups.Contains(cg0));
                    }).ToList()))
                        .Where(tpl => tpl.Item2.Count > 0)
                        .ToDictionary(tpl => tpl.Item1, tpl => tpl.Item2),
                        rng);
                }
                catch (Exception e)
                {
                    Main.modEntry.Logger.Error(
                        $"Exception thrown during TrainCarsCreateJobOrDeleteCheck job info selection:\n{e.ToString()}");
                    Main.OnCriticalFailure();
                }
                Debug.Log(
                    $"[PersistentJobs]\n" +
                    $"    chose {shuntingLoadJobInfos.Count} shunting load jobs,\n" +
                    $"    {transportJobInfos.Count} transport jobs,\n" +
                    $"    {shuntingUnloadJobInfos.Count} shunting unload jobs,\n" +
                    $"    and {emptyTcsPerSc.Aggregate(0, (acc, kv) => acc + kv.Value.Count)} empty haul jobs (coroutine)");

                yield return(WaitFor.SecondsRealtime(interopPeriod));

                // try to generate jobs
                Debug.Log("[PersistentJobs] generating jobs... (coroutine)");
                List <JobChainController>        paxJobChainControllers            = null;
                IEnumerable <JobChainController> shuntingLoadJobChainControllers   = null;
                IEnumerable <JobChainController> transportJobChainControllers      = null;
                IEnumerable <JobChainController> shuntingUnloadJobChainControllers = null;
                IEnumerable <JobChainController> emptyHaulJobChainControllers      = null;
                try
                {
                    paxJobChainControllers = JobProceduralGenerationUtilities.TryToGeneratePassengerJobs(paxTcsPerSc);
                    shuntingLoadJobChainControllers
                        = ShuntingLoadJobProceduralGenerator.doJobGeneration(shuntingLoadJobInfos, rng);
                    transportJobChainControllers
                        = TransportJobProceduralGenerator.doJobGeneration(transportJobInfos, rng);
                    shuntingUnloadJobChainControllers
                        = ShuntingUnloadJobProceduralGenerator.doJobGeneration(shuntingUnloadJobInfos, rng);
                    emptyHaulJobChainControllers = emptyTcsPerSc.Aggregate(
                        new List <JobChainController>(),
                        (list, kv) =>
                    {
                        list.AddRange(
                            kv.Value.Select(tcs => EmptyHaulJobProceduralGenerator
                                            .GenerateEmptyHaulJobWithExistingCars(kv.Key, tcs[0].logicCar.CurrentTrack, tcs, rng)));
                        return(list);
                    });
                }
                catch (Exception e)
                {
                    Main.modEntry.Logger.Error(
                        $"Exception thrown during TrainCarsCreateJobOrDeleteCheck job generation:\n{e.ToString()}");
                    Main.OnCriticalFailure();
                }
                Debug.Log(
                    $"[PersistentJobs]\n" +
                    $"    generated {paxJobChainControllers.Where(jcc => jcc != null).Count()} passenger jobs,\n" +
                    $"    {shuntingLoadJobChainControllers.Where(jcc => jcc != null).Count()} shunting load jobs,\n" +
                    $"    {transportJobChainControllers.Where(jcc => jcc != null).Count()} transport jobs,\n" +
                    $"    {shuntingUnloadJobChainControllers.Where(jcc => jcc != null).Count()} shunting unload jobs,\n" +
                    $"    and {emptyHaulJobChainControllers.Where(jcc => jcc != null).Count()} empty haul jobs (coroutine)");

                yield return(WaitFor.SecondsRealtime(interopPeriod));

                // finalize jobs & preserve job train cars
                Debug.Log("[PersistentJobs] finalizing jobs... (coroutine)");
                int totalCarsPreserved = 0;
                try
                {
                    foreach (var jcc in paxJobChainControllers)
                    {
                        if (jcc != null)
                        {
                            jcc.trainCarsForJobChain.ForEach(tc =>
                            {
                                trainCarCandidatesForDelete.Remove(tc);
                            });
                            totalCarsPreserved += jcc.trainCarsForJobChain.Count;
                            // generation has already taken care of converting player spawned cars and finalizing the job chain
                        }
                    }
                    foreach (JobChainController jcc in shuntingLoadJobChainControllers)
                    {
                        if (jcc != null)
                        {
                            jcc.trainCarsForJobChain.ForEach(tc =>
                            {
                                // force job's train cars to not be treated as player spawned
                                // DV will complain if we don't do this
                                Utilities.ConvertPlayerSpawnedTrainCar(tc);
                                trainCarCandidatesForDelete.Remove(tc);
                            });
                            totalCarsPreserved += jcc.trainCarsForJobChain.Count;
                            jcc.FinalizeSetupAndGenerateFirstJob();
                        }
                    }

                    foreach (JobChainController jcc in transportJobChainControllers)
                    {
                        if (jcc != null)
                        {
                            jcc.trainCarsForJobChain.ForEach(tc =>
                            {
                                // force job's train cars to not be treated as player spawned
                                // DV will complain if we don't do this
                                Utilities.ConvertPlayerSpawnedTrainCar(tc);
                                trainCarCandidatesForDelete.Remove(tc);
                            });
                            totalCarsPreserved += jcc.trainCarsForJobChain.Count;
                            jcc.FinalizeSetupAndGenerateFirstJob();
                        }
                    }

                    foreach (JobChainController jcc in shuntingUnloadJobChainControllers)
                    {
                        if (jcc != null)
                        {
                            jcc.trainCarsForJobChain.ForEach(tc =>
                            {
                                // force job's train cars to not be treated as player spawned
                                // DV will complain if we don't do this
                                Utilities.ConvertPlayerSpawnedTrainCar(tc);
                                trainCarCandidatesForDelete.Remove(tc);
                            });
                            totalCarsPreserved += jcc.trainCarsForJobChain.Count;
                            jcc.FinalizeSetupAndGenerateFirstJob();
                        }
                    }

                    foreach (JobChainController jcc in emptyHaulJobChainControllers)
                    {
                        if (jcc != null)
                        {
                            jcc.trainCarsForJobChain.ForEach(tc =>
                            {
                                // force job's train cars to not be treated as player spawned
                                // DV will complain if we don't do this
                                Utilities.ConvertPlayerSpawnedTrainCar(tc);
                                trainCarCandidatesForDelete.Remove(tc);
                            });
                            totalCarsPreserved += jcc.trainCarsForJobChain.Count;
                            jcc.FinalizeSetupAndGenerateFirstJob();
                        }
                    }
                }
                catch (Exception e)
                {
                    Main.modEntry.Logger.Error(
                        $"Exception thrown during TrainCarsCreateJobOrDeleteCheck trainCar preservation:\n{e.ToString()}");
                    Main.OnCriticalFailure();
                }

                yield return(WaitFor.SecondsRealtime(interopPeriod));

                // preserve all trainCars that are not locomotives
                Debug.Log("[PersistentJobs] preserving cars... (coroutine)");
                try
                {
                    foreach (TrainCar tc in new List <TrainCar>(trainCarCandidatesForDelete))
                    {
                        if (tc.playerSpawnedCar || !CarTypes.IsAnyLocomotiveOrTender(tc.carType))
                        {
                            trainCarCandidatesForDelete.Remove(tc);
                            unusedTrainCarsMarkedForDelete.Add(tc);
                            totalCarsPreserved += 1;
                        }
                    }
                    Debug.Log($"[PersistentJobs] preserved {totalCarsPreserved} cars (coroutine)");
                }
                catch (Exception e)
                {
                    Main.modEntry.Logger.Error(
                        $"Exception thrown during TrainCarsCreateJobOrDeleteCheck trainCar preservation:\n{e.ToString()}");
                    Main.OnCriticalFailure();
                }
                // ------ END JOB GENERATION ------

                yield return(WaitFor.SecondsRealtime(interopPeriod));

                Debug.Log("[PersistentJobs] deleting cars... (coroutine)");
                try
                {
                    trainCarsToDelete.Clear();
                    for (int j = trainCarCandidatesForDelete.Count - 1; j >= 0; j--)
                    {
                        TrainCar trainCar2 = trainCarCandidatesForDelete[j];
                        if (trainCar2 == null)
                        {
                            trainCarCandidatesForDelete.RemoveAt(j);
                        }
                        else if (AreDeleteConditionsFulfilledMethod.GetValue <bool>(trainCar2))
                        {
                            trainCarCandidatesForDelete.RemoveAt(j);
                            carVisitCheckersMap.Remove(trainCar2);
                            trainCarsToDelete.Add(trainCar2);
                        }
                        else
                        {
                            Debug.LogWarning(
                                $"Returning {trainCar2.name} to unusedTrainCarsMarkedForDelete list. PlayerTransform was outside" +
                                " of DELETE_SQR_DISTANCE_FROM_TRAINCAR range of train car, but after short period it" +
                                " was back in range!");
                            trainCarCandidatesForDelete.RemoveAt(j);
                            unusedTrainCarsMarkedForDelete.Add(trainCar2);
                        }
                    }
                    if (trainCarsToDelete.Count != 0)
                    {
                        SingletonBehaviour <CarSpawner> .Instance
                        .DeleteTrainCars(new List <TrainCar>(trainCarsToDelete), false);
                    }
                    Debug.Log($"[PersistentJobs] deleted {trainCarsToDelete.Count} cars (coroutine)");
                }
                catch (Exception e)
                {
                    Main.modEntry.Logger.Error(
                        $"Exception thrown during TrainCarsCreateJobOrDeleteCheck car deletion:\n{e.ToString()}");
                    Main.OnCriticalFailure();
                }
            }
        }
            static bool Prefix(
                UnusedTrainCarDeleter __instance,
                List <TrainCar> ___unusedTrainCarsMarkedForDelete,
                Dictionary <TrainCar, CarVisitChecker> ___carVisitCheckersMap)
            {
                if (Main.modEntry.Active)
                {
                    try
                    {
                        if (___unusedTrainCarsMarkedForDelete.Count == 0)
                        {
                            return(false);
                        }

                        Debug.Log("[PersistentJobs] collecting deletion candidates...");
                        List <TrainCar> trainCarsToDelete = new List <TrainCar>();
                        for (int i = ___unusedTrainCarsMarkedForDelete.Count - 1; i >= 0; i--)
                        {
                            TrainCar trainCar = ___unusedTrainCarsMarkedForDelete[i];
                            if (trainCar == null)
                            {
                                ___unusedTrainCarsMarkedForDelete.RemoveAt(i);
                                continue;
                            }
                            bool areDeleteConditionsFulfilled = Traverse.Create(__instance)
                                                                .Method("AreDeleteConditionsFulfilled", new Type[] { typeof(TrainCar) })
                                                                .GetValue <bool>(trainCar);
                            if (areDeleteConditionsFulfilled)
                            {
                                ___unusedTrainCarsMarkedForDelete.RemoveAt(i);
                                trainCarsToDelete.Add(trainCar);
                            }
                        }
                        Debug.Log(
                            $"[PersistentJobs] found {trainCarsToDelete.Count} cars marked for deletion");
                        if (trainCarsToDelete.Count == 0)
                        {
                            return(false);
                        }

                        // ------ BEGIN JOB GENERATION ------
                        // group trainCars by trainset
                        Debug.Log("[PersistentJobs] grouping trainCars by trainSet...");
                        List <TrainCar> paxTrainCars = trainCarsToDelete
                                                       .Where(tc => Utilities.IsPassengerCar(tc.carType))
                                                       .ToList();
                        List <TrainCar> nonLocoOrPaxTrainCars = trainCarsToDelete
                                                                .Where(tc => !CarTypes.IsAnyLocomotiveOrTender(tc.carType) && !Utilities.IsPassengerCar(tc.carType))
                                                                .ToList();
                        List <TrainCar> emptyFreightCars = nonLocoOrPaxTrainCars
                                                           .Where(tc => tc.logicCar.CurrentCargoTypeInCar == CargoType.None ||
                                                                  tc.logicCar.LoadedCargoAmount < 0.001f)
                                                           .ToList();
                        List <TrainCar> loadedFreightTrainCars = nonLocoOrPaxTrainCars
                                                                 .Where(tc => tc.logicCar.CurrentCargoTypeInCar != CargoType.None &&
                                                                        tc.logicCar.LoadedCargoAmount >= 0.001f)
                                                                 .ToList();
                        var paxTrainCarsPerTrainSet    = JobProceduralGenerationUtilities.GroupTrainCarsByTrainset(paxTrainCars);
                        var emptyTrainCarsPerTrainSet  = JobProceduralGenerationUtilities.GroupTrainCarsByTrainset(emptyFreightCars);
                        var loadedTrainCarsPerTrainSet = JobProceduralGenerationUtilities.GroupTrainCarsByTrainset(loadedFreightTrainCars);
                        Debug.Log(
                            $"[PersistentJobs]\n" +
                            $"    found {paxTrainCarsPerTrainSet.Count} passenger trainSets,\n" +
                            $"    {emptyTrainCarsPerTrainSet.Count} empty trainSets,\n" +
                            $"    and {loadedTrainCarsPerTrainSet.Count} loaded trainSets");

                        // group trainCars sets by nearest stationController
                        Debug.Log("[PersistentJobs] grouping trainSets by nearest station...");
                        var paxTcsPerSc = JobProceduralGenerationUtilities.GroupTrainCarSetsByNearestStation(paxTrainCarsPerTrainSet);
                        Dictionary <StationController, List <(List <TrainCar>, List <CargoGroup>)> > emptyCgsPerTcsPerSc
                            = JobProceduralGenerationUtilities.GroupTrainCarSetsByNearestStation(emptyTrainCarsPerTrainSet);
                        Dictionary <StationController, List <(List <TrainCar>, List <CargoGroup>)> > loadedCgsPerTcsPerSc
                            = JobProceduralGenerationUtilities.GroupTrainCarSetsByNearestStation(loadedTrainCarsPerTrainSet);
                        Debug.Log(
                            $"[PersistentJobs]\n" +
                            $"    found {paxTcsPerSc.Count} stations for passenger trainSets,\n" +
                            $"    {emptyCgsPerTcsPerSc.Count} stations for empty trainSets,\n" +
                            $"    and {loadedCgsPerTcsPerSc.Count} stations for loaded trainSets");

                        // populate possible cargoGroups per group of trainCars
                        Debug.Log("[PersistentJobs] populating cargoGroups...");
                        JobProceduralGenerationUtilities.PopulateCargoGroupsPerTrainCarSet(emptyCgsPerTcsPerSc);
                        JobProceduralGenerationUtilities.PopulateCargoGroupsPerLoadedTrainCarSet(loadedCgsPerTcsPerSc);
                        Dictionary <StationController, List <List <TrainCar> > > emptyTcsPerSc
                            = JobProceduralGenerationUtilities.ExtractEmptyHaulTrainSets(emptyCgsPerTcsPerSc);

                        // pick new jobs for the trainCars at each station
                        Debug.Log("[PersistentJobs] picking jobs...");
                        System.Random rng = new System.Random(Environment.TickCount);
                        List <(StationController, List <CarsPerTrack>, StationController, List <TrainCar>, List <CargoType>)>
                        shuntingLoadJobInfos = ShuntingLoadJobProceduralGenerator
                                               .ComputeJobInfosFromCargoGroupsPerTrainCarSetPerStation(emptyCgsPerTcsPerSc, rng);
                        List <(StationController, Track, StationController, List <TrainCar>, List <CargoType>)>
                        transportJobInfos = TransportJobProceduralGenerator
                                            .ComputeJobInfosFromCargoGroupsPerTrainCarSetPerStation(
                            loadedCgsPerTcsPerSc.Select(kv => (
                                                            kv.Key,
                                                            kv.Value.Where(tpl => {
                            CargoGroup cg0 = tpl.Item2.FirstOrDefault();
                            return(cg0 != null && kv.Key.proceduralJobsRuleset.outputCargoGroups.Contains(cg0));
                        }).ToList()))
                            .Where(tpl => tpl.Item2.Count > 0)
                            .ToDictionary(tpl => tpl.Item1, tpl => tpl.Item2),
                            rng);
                        List <(StationController, Track, StationController, List <TrainCar>, List <CargoType>)>
                        shuntingUnloadJobInfos = ShuntingUnloadJobProceduralGenerator
                                                 .ComputeJobInfosFromCargoGroupsPerTrainCarSetPerStation(
                            loadedCgsPerTcsPerSc.Select(kv => (
                                                            kv.Key,
                                                            kv.Value.Where(tpl => {
                            CargoGroup cg0 = tpl.Item2.FirstOrDefault();
                            return(cg0 != null && kv.Key.proceduralJobsRuleset.inputCargoGroups.Contains(cg0));
                        }).ToList()))
                            .Where(tpl => tpl.Item2.Count > 0)
                            .ToDictionary(tpl => tpl.Item1, tpl => tpl.Item2),
                            rng);
                        Debug.Log(
                            $"[PersistentJobs]\n" +
                            $"    chose {shuntingLoadJobInfos.Count} shunting load jobs,\n" +
                            $"    {transportJobInfos.Count} transport jobs,\n" +
                            $"    {shuntingUnloadJobInfos.Count} shunting unload jobs,\n" +
                            $"    and {emptyTcsPerSc.Aggregate(0, (acc, kv) => acc + kv.Value.Count)} empty haul jobs");

                        // try to generate jobs
                        Debug.Log("[PersistentJobs] generating jobs...");
                        List <JobChainController>        paxJobChainControllers = JobProceduralGenerationUtilities.TryToGeneratePassengerJobs(paxTcsPerSc);
                        IEnumerable <JobChainController> shuntingLoadJobChainControllers
                            = ShuntingLoadJobProceduralGenerator.doJobGeneration(shuntingLoadJobInfos, rng);
                        IEnumerable <JobChainController> transportJobChainControllers
                            = TransportJobProceduralGenerator.doJobGeneration(transportJobInfos, rng);
                        IEnumerable <JobChainController> shuntingUnloadJobChainControllers
                            = ShuntingUnloadJobProceduralGenerator.doJobGeneration(shuntingUnloadJobInfos, rng);
                        IEnumerable <JobChainController> emptyHaulJobChainControllers = emptyTcsPerSc.Aggregate(
                            new List <JobChainController>(),
                            (list, kv) =>
                        {
                            list.AddRange(
                                kv.Value.Select(tcs => EmptyHaulJobProceduralGenerator
                                                .GenerateEmptyHaulJobWithExistingCars(kv.Key, tcs[0].logicCar.CurrentTrack, tcs, rng)));
                            return(list);
                        });
                        Debug.Log(
                            $"[PersistentJobs]\n" +
                            $"    generated {paxJobChainControllers.Where(jcc => jcc != null).Count()} passenger jobs,\n" +
                            $"    {shuntingLoadJobChainControllers.Where(jcc => jcc != null).Count()} shunting load jobs,\n" +
                            $"    {transportJobChainControllers.Where(jcc => jcc != null).Count()} transport jobs,\n" +
                            $"    {shuntingUnloadJobChainControllers.Where(jcc => jcc != null).Count()} shunting unload jobs,\n" +
                            $"    and {emptyHaulJobChainControllers.Where(jcc => jcc != null).Count()} empty haul jobs");

                        // finalize jobs & preserve job train cars
                        Debug.Log("[PersistentJobs] finalizing jobs...");
                        int totalCarsPreserved = 0;
                        foreach (var jcc in paxJobChainControllers)
                        {
                            if (jcc != null)
                            {
                                jcc.trainCarsForJobChain.ForEach(tc =>
                                {
                                    trainCarsToDelete.Remove(tc);
                                });
                                totalCarsPreserved += jcc.trainCarsForJobChain.Count;
                                // generation has already taken care of converting player spawned cars and finalizing the job chain
                            }
                        }
                        foreach (JobChainController jcc in shuntingLoadJobChainControllers)
                        {
                            if (jcc != null)
                            {
                                jcc.trainCarsForJobChain.ForEach(tc =>
                                {
                                    // force job's train cars to not be treated as player spawned
                                    // DV will complain if we don't do this
                                    Utilities.ConvertPlayerSpawnedTrainCar(tc);
                                    trainCarsToDelete.Remove(tc);
                                });
                                totalCarsPreserved += jcc.trainCarsForJobChain.Count;
                                jcc.FinalizeSetupAndGenerateFirstJob();
                            }
                        }
                        foreach (JobChainController jcc in transportJobChainControllers)
                        {
                            if (jcc != null)
                            {
                                jcc.trainCarsForJobChain.ForEach(tc =>
                                {
                                    // force job's train cars to not be treated as player spawned
                                    // DV will complain if we don't do this
                                    Utilities.ConvertPlayerSpawnedTrainCar(tc);
                                    trainCarsToDelete.Remove(tc);
                                });
                                totalCarsPreserved += jcc.trainCarsForJobChain.Count;
                                jcc.FinalizeSetupAndGenerateFirstJob();
                            }
                        }
                        foreach (JobChainController jcc in shuntingUnloadJobChainControllers)
                        {
                            if (jcc != null)
                            {
                                jcc.trainCarsForJobChain.ForEach(tc =>
                                {
                                    // force job's train cars to not be treated as player spawned
                                    // DV will complain if we don't do this
                                    Utilities.ConvertPlayerSpawnedTrainCar(tc);
                                    trainCarsToDelete.Remove(tc);
                                });
                                totalCarsPreserved += jcc.trainCarsForJobChain.Count;
                                jcc.FinalizeSetupAndGenerateFirstJob();
                            }
                        }
                        foreach (JobChainController jcc in emptyHaulJobChainControllers)
                        {
                            if (jcc != null)
                            {
                                jcc.trainCarsForJobChain.ForEach(tc =>
                                {
                                    // force job's train cars to not be treated as player spawned
                                    // DV will complain if we don't do this
                                    Utilities.ConvertPlayerSpawnedTrainCar(tc);
                                    trainCarsToDelete.Remove(tc);
                                });
                                totalCarsPreserved += jcc.trainCarsForJobChain.Count;
                                jcc.FinalizeSetupAndGenerateFirstJob();
                            }
                        }

                        // preserve all trainCars that are not locos
                        Debug.Log("[PersistentJobs] preserving cars...");
                        foreach (TrainCar tc in new List <TrainCar>(trainCarsToDelete))
                        {
                            if (tc.playerSpawnedCar || !CarTypes.IsAnyLocomotiveOrTender(tc.carType))
                            {
                                trainCarsToDelete.Remove(tc);
                                ___unusedTrainCarsMarkedForDelete.Add(tc);
                                totalCarsPreserved += 1;
                            }
                        }
                        Debug.Log($"[PersistentJobs] preserved {totalCarsPreserved} cars");
                        // ------ END JOB GENERATION ------

                        Debug.Log("[PersistentJobs] deleting cars...");
                        foreach (TrainCar tc in trainCarsToDelete)
                        {
                            ___unusedTrainCarsMarkedForDelete.Remove(tc);
                            ___carVisitCheckersMap.Remove(tc);
                        }
                        SingletonBehaviour <CarSpawner> .Instance.DeleteTrainCars(trainCarsToDelete, true);

                        Debug.Log($"[PersistentJobs] deleted {trainCarsToDelete.Count} cars");
                        return(false);
                    }
                    catch (Exception e)
                    {
                        Main.modEntry.Logger.Error(
                            $"Exception thrown during {"UnusedTrainCarDeleter"}.{"InstantConditionalDeleteOfUnusedCars"} {"prefix"} patch:" +
                            $"\n{e.ToString()}");
                        Main.OnCriticalFailure();
                    }
                }
                return(true);
            }
        public static JobChainControllerWithEmptyHaulGeneration GenerateShuntingLoadJobWithCarSpawning(
            StationController startingStation,
            bool forceLicenseReqs,
            System.Random rng)
        {
            Debug.Log("[PersistentJobs] load: generating with car spawning");
            YardTracksOrganizer yto = YardTracksOrganizer.Instance;
            List <CargoGroup>   availableCargoGroups = startingStation.proceduralJobsRuleset.outputCargoGroups;
            int countTrainCars = rng.Next(
                startingStation.proceduralJobsRuleset.minCarsPerJob,
                startingStation.proceduralJobsRuleset.maxCarsPerJob);

            if (forceLicenseReqs)
            {
                Debug.Log("[PersistentJobs] load: forcing license requirements");
                if (!LicenseManager.IsJobLicenseAcquired(JobLicenses.Shunting))
                {
                    Debug.LogError("Trying to generate a ShuntingLoad job with forceLicenseReqs=true should " +
                                   "never happen if player doesn't have Shunting license!");
                    return(null);
                }
                availableCargoGroups
                    = (from cg in availableCargoGroups
                       where LicenseManager.IsLicensedForJob(cg.CargoRequiredLicenses)
                       select cg).ToList();
                countTrainCars = Math.Min(countTrainCars, LicenseManager.GetMaxNumberOfCarsPerJobWithAcquiredJobLicenses());
            }
            if (availableCargoGroups.Count == 0)
            {
                Debug.LogWarning("[PersistentJobs] load: no available cargo groups");
                return(null);
            }

            CargoGroup chosenCargoGroup = Utilities.GetRandomFromEnumerable(availableCargoGroups, rng);

            // choose cargo & trainCar types
            Debug.Log("[PersistentJobs] load: choosing cargo & trainCar types");
            List <CargoType>    availableCargoTypes  = chosenCargoGroup.cargoTypes;
            List <CargoType>    orderedCargoTypes    = new List <CargoType>();
            List <TrainCarType> orderedTrainCarTypes = new List <TrainCarType>();

            for (int i = 0; i < countTrainCars; i++)
            {
                CargoType chosenCargoType = Utilities.GetRandomFromEnumerable(availableCargoTypes, rng);
                List <CargoContainerType> availableContainers
                    = CargoTypes.GetCarContainerTypesThatSupportCargoType(chosenCargoType);
                CargoContainerType  chosenContainerType = Utilities.GetRandomFromEnumerable(availableContainers, rng);
                List <TrainCarType> availableTrainCarTypes
                    = CargoTypes.GetTrainCarTypesThatAreSpecificContainerType(chosenContainerType);
                TrainCarType chosenTrainCarType = Utilities.GetRandomFromEnumerable(availableTrainCarTypes, rng);
                orderedCargoTypes.Add(chosenCargoType);
                orderedTrainCarTypes.Add(chosenTrainCarType);
            }

            // choose starting tracks
            int maxCountTracks = startingStation.proceduralJobsRuleset.maxShuntingStorageTracks;
            int countTracks    = rng.Next(1, maxCountTracks + 1);

            // bias toward less than max number of tracks for shorter trains
            if (orderedTrainCarTypes.Count < 2 * maxCountTracks)
            {
                countTracks = rng.Next(0, Mathf.FloorToInt(1.5f * maxCountTracks)) % maxCountTracks + 1;
            }
            Debug.Log(string.Format("[PersistentJobs] load: choosing {0} starting tracks", countTracks));
            int          countCarsPerTrainset       = countTrainCars / countTracks;
            int          countTrainsetsWithExtraCar = countTrainCars % countTracks;
            List <Track> tracks = new List <Track>();

            do
            {
                tracks.Clear();
                for (int i = 0; i < countTracks; i++)
                {
                    int rangeStart = i * countCarsPerTrainset + Math.Min(i, countTrainsetsWithExtraCar);
                    int rangeCount = i < countTrainsetsWithExtraCar ? countCarsPerTrainset + 1 : countCarsPerTrainset;
                    List <TrainCarType> trainCarTypesPerTrack = orderedTrainCarTypes.GetRange(rangeStart, rangeCount);
                    float approxTrainLengthPerTrack           = yto.GetTotalCarTypesLength(trainCarTypesPerTrack)
                                                                + yto.GetSeparationLengthBetweenCars(trainCarTypesPerTrack.Count);
                    Track track = Utilities.GetTrackThatHasEnoughFreeSpace(
                        yto,
                        startingStation.logicStation.yard.StorageTracks.Except(tracks).ToList(),
                        approxTrainLengthPerTrack / (float)countTracks);
                    if (track == null)
                    {
                        break;
                    }
                    tracks.Add(track);
                }
            } while (tracks.Count < countTracks--);
            if (tracks.Count == 0)
            {
                Debug.LogWarning("[PersistentJobs] load: Couldn't find startingTrack with enough free space for train!");
                return(null);
            }

            // choose random destination station that has at least 1 available track
            Debug.Log("[PersistentJobs] load: choosing destination");
            float approxTrainLength = yto.GetTotalCarTypesLength(orderedTrainCarTypes)
                                      + yto.GetSeparationLengthBetweenCars(countTrainCars);
            List <StationController> availableDestinations = new List <StationController>(chosenCargoGroup.stations);
            StationController        destStation           = null;
            Track destinationTrack = null;

            while (availableDestinations.Count > 0 && destinationTrack == null)
            {
                destStation = Utilities.GetRandomFromEnumerable(availableDestinations, rng);
                availableDestinations.Remove(destStation);
                destinationTrack = Utilities.GetTrackThatHasEnoughFreeSpace(
                    yto,
                    yto.FilterOutOccupiedTracks(destStation.logicStation.yard.TransferInTracks),
                    approxTrainLength);
            }
            if (destinationTrack == null)
            {
                Debug.LogWarning("Couldn't find a station with enough free space for train!");
                return(null);
            }

            // spawn trainCars & form carsPerStartingTrack
            Debug.Log("[PersistentJobs] load: spawning trainCars");
            List <TrainCar>     orderedTrainCars     = new List <TrainCar>();
            List <CarsPerTrack> carsPerStartingTrack = new List <CarsPerTrack>();

            for (int i = 0; i < tracks.Count; i++)
            {
                int rangeStart = i * countCarsPerTrainset + Math.Min(i, countTrainsetsWithExtraCar);
                int rangeCount = i < countTrainsetsWithExtraCar ? countCarsPerTrainset + 1 : countCarsPerTrainset;
                Debug.Log(string.Format(
                              "[PersistentJobs] load: spawning cars in range [{0}-{1}) from total range [0-{2})",
                              rangeStart,
                              rangeStart + rangeCount,
                              orderedTrainCarTypes.Count));
                Track           startingTrack = tracks[i];
                RailTrack       railTrack     = SingletonBehaviour <LogicController> .Instance.LogicToRailTrack[startingTrack];
                List <TrainCar> spawnedCars   = CarSpawner.SpawnCarTypesOnTrack(
                    orderedTrainCarTypes.GetRange(rangeStart, rangeCount),
                    railTrack,
                    true,
                    0.0,
                    false,
                    true);
                if (spawnedCars == null)
                {
                    Debug.LogWarning("[PersistentJobs] load: Failed to spawn some trainCars!");
                    SingletonBehaviour <CarSpawner> .Instance.DeleteTrainCars(orderedTrainCars, true);

                    return(null);
                }
                orderedTrainCars.AddRange(spawnedCars);
                carsPerStartingTrack.Add(
                    new CarsPerTrack(startingTrack, (from car in spawnedCars select car.logicCar).ToList()));
            }

            JobChainControllerWithEmptyHaulGeneration jcc = GenerateShuntingLoadJobWithExistingCars(
                startingStation,
                carsPerStartingTrack,
                destStation,
                orderedTrainCars,
                orderedCargoTypes,
                rng,
                true);

            if (jcc == null)
            {
                Debug.LogWarning("[PersistentJobs] load: Couldn't generate job chain. Deleting spawned trainCars!");
                SingletonBehaviour <CarSpawner> .Instance.DeleteTrainCars(orderedTrainCars, true);

                return(null);
            }

            return(jcc);
        }