Exemplo n.º 1
0
        public System.Collections.IEnumerator GeneratePassengerJobs()
        {
            PassengerJobs.ModEntry.Logger.Log($"Generating jobs at {Controller.stationInfo.Name}");

            // Create passenger hauls until >= half the platforms are filled
            int attemptCounter;

            for (attemptCounter = 2; attemptCounter > 0; attemptCounter--)
            {
                // break if there are no more available outbound platforms
                if (TrackOrg.FilterOutReservedTracks(TrackOrg.FilterOutOccupiedTracks(PlatformTracks)).Count == 0)
                {
                    break;
                }

                try
                {
                    GenerateNewTransportJob();
                }
                catch (Exception ex)
                {
                    PassengerJobs.ModEntry.Logger.LogException(ex);
                }

                yield return(null);
            }

            // Create commuter hauls until >= half of storage tracks are filled
            var existingChains  = Controller.ProceduralJobsController.GetCurrentJobChains();
            int nExtantCommutes = existingChains.Count(c => c is CommuterChainController);

            double totalTrackSpace = StorageTracks.Select(t => t.length).Sum();

            // generate max 3 commuter chains from each station
            int nToGenerate = 3 - nExtantCommutes;

            PassengerJobs.ModEntry.Logger.Log($"{Controller.stationInfo.YardID} has {nExtantCommutes} commute jobs, generating up to {nToGenerate} additional");

            for (attemptCounter = 5; attemptCounter > 0; attemptCounter--)
            {
                if (nToGenerate <= 0)
                {
                    break;
                }

                // break on storage tracks >60% full
                double reservedSpace = StorageTracks.Select(t => TrackOrg.GetReservedSpace(t)).Sum();
                if ((reservedSpace / totalTrackSpace) >= 0.6d)
                {
                    break;
                }

                yield return(new WaitForSeconds(0.2f));

                try
                {
                    var result = GenerateNewCommuterRun();
                    if (result != null)
                    {
                        nToGenerate -= 1;
                    }
                }
                catch (Exception ex)
                {
                    PassengerJobs.ModEntry.Logger.LogException(ex);
                }
            }

            GenerationRoutine = null;
            yield break;
        }
Exemplo n.º 2
0
        public PassengerTransportChainController GenerateNewTransportJob(TrainCarsPerLogicTrack consistInfo = null, SpecialTrain prevSpecial = null)
        {
            int nTotalCars;
            List <TrainCarType> jobCarTypes = null;
            float trainLength;
            Track startSiding;

            // Establish the starting consist and its storage location
            if (consistInfo == null)
            {
                // generate a consist
                nTotalCars = Rand.Next(MIN_CARS_EXPRESS, MAX_CARS_EXPRESS + 1);

                float singleCarLength = TrackOrg.GetCarTypeLength(TrainCarType.PassengerRed);
                trainLength = (singleCarLength * nTotalCars) + TrackOrg.GetSeparationLengthBetweenCars(nTotalCars);

                // pick start storage track
                var emptyTracks = TrackOrg.FilterOutOccupiedTracks(StorageTracks);
                startSiding = TrackOrg.GetTrackWithEnoughFreeSpace(emptyTracks, trainLength);

                if (startSiding == null)
                {
                    startSiding = TrackOrg.GetTrackWithEnoughFreeSpace(StorageTracks, trainLength);
                }
            }
            else
            {
                // Use existing consist
                nTotalCars  = consistInfo.cars.Count;
                jobCarTypes = consistInfo.cars.Select(car => car.carType).ToList();
                trainLength = TrackOrg.GetTotalCarTypesLength(jobCarTypes) + TrackOrg.GetSeparationLengthBetweenCars(nTotalCars);
                startSiding = consistInfo.track;
            }

            if (startSiding == null)
            {
                //PassengerJobs.ModEntry.Logger.Log($"No available starting siding for express job at {Controller.stationInfo.Name}");
                return(null);
            }

            // Try to find a loading platform to use
            PlatformDefinition loadingPlatform = PlatformManager.PickPlatform(Controller.stationInfo.YardID);

            // Choose a route
            var               destPool    = PassDestinations.Values.ToList();
            Track             destSiding  = null;
            StationController destStation = null;

            // this prevents generating jobs like "ChainJob[Passenger]: FF - FF (FF-PE-47)"
            destPool.Remove(Controller);

            while ((destSiding == null) && (destPool.Count > 0))
            {
                // search the possible destinations 1 by 1 until we find an opening (or we don't)
                destStation = destPool.ChooseOne(Rand);

                // pick ending platform
                PassengerJobGenerator destGenerator = LinkedGenerators[destStation];
                destSiding = TrackOrg.GetTrackWithEnoughFreeSpace(destGenerator.StorageTracks, trainLength);

                // remove this station from the pool
                destPool.Remove(destStation);
            }

            if (destSiding == null)
            {
                //PassengerJobs.ModEntry.Logger.Log($"No available destination siding for new job at {Controller.stationInfo.Name}");
                return(null);
            }

            // we found a route :D
            // if we're creating a new consist, check if it can be a special train
            // let's try 2/3 chance of special train, 1/3 normal gen
            SpecialTrain  specialInfo = null;
            List <string> skinList    = null;

            if (consistInfo == null)
            {
                // default 67% chance, or as configured (if there is a special available)
                double choice = Rand.NextDouble();

                if ((choice <= PassengerJobs.Settings.NamedTrainProbability) &&
                    (SpecialConsistManager.GetTrainForRoute(Controller.stationInfo.YardID, destStation.stationInfo.YardID) is SpecialTrain special))
                {
                    specialInfo = special;
                    IEnumerable <SpecialTrainSkin> consistSkins = special.Skins.ChooseMany(Rand, nTotalCars);
                    jobCarTypes = consistSkins.Select(s => s.CarType).ToList();
                    skinList    = consistSkins.Select(s => s.Name).ToList();
                }
                else
                {
                    // normal consist generation
                    if (PassengerJobs.Settings.UniformConsists)
                    {
                        TrainCarType carType = PassCarTypes.ChooseOne(Rand);
                        jobCarTypes = Enumerable.Repeat(carType, nTotalCars).ToList();
                    }
                    else
                    {
                        jobCarTypes = PassCarTypes.ChooseMany(Rand, nTotalCars);
                    }
                }
            }
            else
            {
                // extant consist, use existing special (if it exists)
                specialInfo = prevSpecial;
            }

            // Try to find an unloading platform
            PlatformDefinition unloadingPlatform = PlatformManager.PickPlatform(destStation.stationInfo.YardID);

            // create job chain controller
            var chainJobObject = new GameObject($"ChainJob[Passenger]: {Controller.logicStation.ID} - {destStation.logicStation.ID}");

            chainJobObject.transform.SetParent(Controller.transform);
            var chainController = new PassengerTransportChainController(chainJobObject);

            StaticPassengerJobDefinition jobDefinition;

            //--------------------------------------------------------------------------------------------------------------------------------
            // Create transport leg job
            var chainData = new StationsChainData(Controller.stationInfo.YardID, destStation.stationInfo.YardID);
            PaymentCalculationData transportPaymentData = GetJobPaymentData(jobCarTypes);

            // calculate haul payment
            float haulDistance     = JobPaymentCalculator.GetDistanceBetweenStations(Controller, destStation);
            float bonusLimit       = JobPaymentCalculator.CalculateHaulBonusTimeLimit(haulDistance, false);
            float transportPayment = JobPaymentCalculator.CalculateJobPayment(JobType.Transport, haulDistance, transportPaymentData);

            // calculate additional payment for shunting
            const float            shuntDistance    = 500f;
            PaymentCalculationData emptyPaymentData = GetJobPaymentData(jobCarTypes, true);
            float platformBonusTime = JobPaymentCalculator.CalculateShuntingBonusTimeLimit(1) * 0.7f + PlatformController.START_XFER_DELAY;

            if (loadingPlatform?.Initialized == true)
            {
                float loadPayment = JobPaymentCalculator.CalculateJobPayment(JobType.ShuntingLoad, shuntDistance, emptyPaymentData);
                transportPayment += loadPayment;
                bonusLimit       += platformBonusTime;
            }

            if (unloadingPlatform?.Initialized == true)
            {
                float unloadPayment = JobPaymentCalculator.CalculateJobPayment(JobType.ShuntingUnload, shuntDistance, emptyPaymentData);
                transportPayment += unloadPayment;
                bonusLimit       += platformBonusTime;
            }

            // scale job payment depending on settings
            float wageScale = PassengerJobs.Settings.UseCustomWages ? BASE_WAGE_SCALE : 1;

            transportPayment = Mathf.Round(transportPayment * wageScale);

            if (consistInfo == null)
            {
                jobDefinition = PopulateTransportJobAndSpawn(
                    chainController, Controller.logicStation, startSiding, destSiding,
                    jobCarTypes, chainData, bonusLimit, transportPayment, true, skinList);
            }
            else
            {
                chainController.trainCarsForJobChain = consistInfo.cars;

                jobDefinition = PopulateTransportJobExistingCars(
                    chainController, Controller.logicStation, startSiding, destSiding,
                    consistInfo.LogicCars, chainData, bonusLimit, transportPayment);
            }

            if (jobDefinition == null)
            {
                PassengerJobs.ModEntry.Logger.Warning($"Failed to generate transport job definition for {chainController.jobChainGO.name}");
                chainController.DestroyChain();
                return(null);
            }
            jobDefinition.subType           = PassJobType.Express;
            jobDefinition.specialDefinition = specialInfo;

            // Setup any warehouse tasks
            if (loadingPlatform?.Initialized == true)
            {
                jobDefinition.loadMachine = loadingPlatform.Controller.LogicMachine;
            }

            if (unloadingPlatform?.Initialized == true)
            {
                jobDefinition.unloadMachine = unloadingPlatform.Controller.LogicMachine;
            }

            chainController.AddJobDefinitionToChain(jobDefinition);

            // Finalize job
            chainController.FinalizeSetupAndGenerateFirstJob();
            PassengerJobs.ModEntry.Logger.Log($"Generated new passenger haul job: {chainJobObject.name} ({chainController.currentJobInChain.ID})");

            return(chainController);
        }
Exemplo n.º 3
0
        public static JobChainControllerWithEmptyHaulGeneration GenerateTransportJobWithExistingCars(
            StationController startingStation,
            Track startingTrack,
            StationController destStation,
            List <TrainCar> trainCars,
            List <CargoType> transportedCargoPerCar,
            System.Random rng,
            bool forceCorrectCargoStateOnCars = false)
        {
            Debug.Log("[PersistentJobs] transport: generating with pre-spawned cars");
            YardTracksOrganizer          yto = YardTracksOrganizer.Instance;
            HashSet <CargoContainerType> carContainerTypes = new HashSet <CargoContainerType>();

            foreach (TrainCar trainCar in trainCars)
            {
                carContainerTypes.Add(CargoTypes.CarTypeToContainerType[trainCar.carType]);
            }

            Debug.Log("[PersistentJobs] transport: choosing destination track");
            float approxTrainLength = yto.GetTotalTrainCarsLength(trainCars)
                                      + yto.GetSeparationLengthBetweenCars(trainCars.Count);
            Track destinationTrack = Utilities.GetTrackThatHasEnoughFreeSpace(
                yto,
                yto.FilterOutOccupiedTracks(destStation.logicStation.yard.TransferInTracks),
                approxTrainLength);

            if (destinationTrack == null)
            {
                destinationTrack = Utilities.GetTrackThatHasEnoughFreeSpace(
                    yto,
                    destStation.logicStation.yard.TransferInTracks,
                    approxTrainLength);
            }
            if (destinationTrack == null)
            {
                Debug.LogWarning(string.Format(
                                     "[PersistentJobs] transport: Could not create ChainJob[{0}]: {1} - {2}. " +
                                     "Found no TransferInTrack with enough free space!",
                                     JobType.Transport,
                                     startingStation.logicStation.ID,
                                     destStation.logicStation.ID
                                     ));
                return(null);
            }
            List <TrainCarType> transportedCarTypes = (from tc in trainCars select tc.carType)
                                                      .ToList <TrainCarType>();

            Debug.Log("[PersistentJobs] transport: calculating time/wage/licenses");
            float bonusTimeLimit;
            float initialWage;

            Utilities.CalculateTransportBonusTimeLimitAndWage(
                JobType.Transport,
                startingStation,
                destStation,
                transportedCarTypes,
                transportedCargoPerCar,
                out bonusTimeLimit,
                out initialWage
                );
            JobLicenses requiredLicenses = LicenseManager.GetRequiredLicensesForJobType(JobType.Transport)
                                           | LicenseManager.GetRequiredLicensesForCargoTypes(transportedCargoPerCar)
                                           | LicenseManager.GetRequiredLicenseForNumberOfTransportedCars(trainCars.Count);

            return(TransportJobProceduralGenerator.GenerateTransportChainController(
                       startingStation,
                       startingTrack,
                       destStation,
                       destinationTrack,
                       trainCars,
                       transportedCargoPerCar,
                       trainCars.Select(
                           tc => tc.logicCar.CurrentCargoTypeInCar == CargoType.None ? 1.0f : tc.logicCar.LoadedCargoAmount).ToList(),
                       forceCorrectCargoStateOnCars,
                       bonusTimeLimit,
                       initialWage,
                       requiredLicenses
                       ));
        }
Exemplo n.º 4
0
        public static JobChainControllerWithEmptyHaulGeneration GenerateTransportJobWithCarSpawning(
            StationController startingStation,
            bool forceLicenseReqs,
            System.Random rng)
        {
            Debug.Log("[PersistentJobs] transport: 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] transport: forcing license requirements");
                if (!LicenseManager.IsJobLicenseAcquired(JobLicenses.FreightHaul))
                {
                    Debug.LogError("[PersistentJobs] transport: Trying to generate a Transport job with " +
                                   "forceLicenseReqs=true should never happen if player doesn't have FreightHaul 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] transport: no available cargo groups");
                return(null);
            }

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

            // choose cargo & trainCar types
            Debug.Log("[PersistentJobs] transport: 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] transport: choosing starting track");
            Track startingTrack
                = Utilities.GetTrackThatHasEnoughFreeSpace(yto, startingStation.logicStation.yard.TransferOutTracks, approxTrainLength);

            if (startingTrack == null)
            {
                Debug.LogWarning("[PersistentJobs] transport: 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] transport: choosing destination");
            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("[PersistentJobs] transport: Couldn't find a station with enough free space for train!");
                return(null);
            }

            // spawn trainCars
            Debug.Log("[PersistentJobs] transport: 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] transport: Failed to spawn trainCars!");
                return(null);
            }

            JobChainControllerWithEmptyHaulGeneration jcc = GenerateTransportJobWithExistingCars(
                startingStation,
                startingTrack,
                destStation,
                orderedTrainCars,
                orderedCargoTypes,
                rng,
                true);

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

                return(null);
            }

            return(jcc);
        }
        public static JobChainControllerWithEmptyHaulGeneration GenerateShuntingLoadJobWithExistingCars(
            StationController startingStation,
            List <CarsPerTrack> carsPerStartingTrack,
            StationController destStation,
            List <TrainCar> trainCars,
            List <CargoType> transportedCargoPerCar,
            System.Random rng,
            bool forceCorrectCargoStateOnCars = false)
        {
            Debug.Log("[PersistentJobs] load: generating with pre-spawned cars");
            YardTracksOrganizer          yto = YardTracksOrganizer.Instance;
            HashSet <CargoContainerType> carContainerTypes = new HashSet <CargoContainerType>();

            foreach (TrainCar trainCar in trainCars)
            {
                carContainerTypes.Add(CargoTypes.CarTypeToContainerType[trainCar.carType]);
            }
            float approxTrainLength = yto.GetTotalTrainCarsLength(trainCars)
                                      + yto.GetSeparationLengthBetweenCars(trainCars.Count);

            // choose warehosue machine
            Debug.Log("[PersistentJobs] load: choosing warehouse machine");
            List <WarehouseMachineController> supportedWMCs = startingStation.warehouseMachineControllers
                                                              .Where(wm => wm.supportedCargoTypes.Intersect(transportedCargoPerCar).Count() > 0)
                                                              .ToList();

            if (supportedWMCs.Count == 0)
            {
                Debug.LogWarning(string.Format(
                                     "[PersistentJobs] load: Could not create ChainJob[{0}]: {1} - {2}. Found no supported WarehouseMachine!",
                                     JobType.ShuntingLoad,
                                     startingStation.logicStation.ID,
                                     destStation.logicStation.ID
                                     ));
                return(null);
            }
            WarehouseMachine loadMachine = Utilities.GetRandomFromEnumerable(supportedWMCs, rng).warehouseMachine;

            // choose destination track
            Debug.Log("[PersistentJobs] load: choosing destination track");
            Track destinationTrack = Utilities.GetTrackThatHasEnoughFreeSpace(
                yto,
                yto.FilterOutOccupiedTracks(startingStation.logicStation.yard.TransferOutTracks),
                approxTrainLength
                );

            if (destinationTrack == null)
            {
                destinationTrack = Utilities.GetTrackThatHasEnoughFreeSpace(
                    yto,
                    startingStation.logicStation.yard.TransferOutTracks,
                    approxTrainLength
                    );
            }
            if (destinationTrack == null)
            {
                Debug.LogWarning(string.Format(
                                     "[PersistentJobs] load: Could not create ChainJob[{0}]: {1} - {2}. " +
                                     "Found no TransferOutTrack with enough free space!",
                                     JobType.ShuntingLoad,
                                     startingStation.logicStation.ID,
                                     destStation.logicStation.ID
                                     ));
                return(null);
            }

            Debug.Log("[PersistentJobs] load: calculating time/wage/licenses");
            List <TrainCarType> transportedCarTypes = (from tc in trainCars select tc.carType)
                                                      .ToList <TrainCarType>();
            float bonusTimeLimit;
            float initialWage;

            Utilities.CalculateShuntingBonusTimeLimitAndWage(
                JobType.ShuntingLoad,
                carsPerStartingTrack.Count,
                transportedCarTypes,
                transportedCargoPerCar,
                out bonusTimeLimit,
                out initialWage
                );
            JobLicenses requiredLicenses = LicenseManager.GetRequiredLicensesForJobType(JobType.ShuntingLoad)
                                           | LicenseManager.GetRequiredLicensesForCargoTypes(transportedCargoPerCar)
                                           | LicenseManager.GetRequiredLicenseForNumberOfTransportedCars(trainCars.Count);

            return(GenerateShuntingLoadChainController(
                       startingStation,
                       carsPerStartingTrack,
                       loadMachine,
                       destStation,
                       destinationTrack,
                       trainCars,
                       transportedCargoPerCar,
                       Enumerable.Repeat(1.0f, trainCars.Count).ToList(),
                       forceCorrectCargoStateOnCars,
                       bonusTimeLimit,
                       initialWage,
                       requiredLicenses
                       ));
        }
        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);
        }