Example #1
0
        protected override ItemPlacementResult DoSanePlacement(MT19337 rng, ItemPlacementContext ctx)
        {
            _sanityCounter = 0;
            var incentiveLocationPool   = _incentivesData.IncentiveLocations.ToList();
            var preBlackOrbLocationPool = _incentivesData.AllValidPreBlackOrbItemLocations.ToList();
            var preBlackOrbUnincentivizedLocationPool = preBlackOrbLocationPool.Where(x => !incentiveLocationPool.Any(y => y.Address == x.Address)).ToList();

            if ((bool)_flags.LooseExcludePlacedDungeons)
            {
                preBlackOrbUnincentivizedLocationPool = IncentivizedDungeons(preBlackOrbUnincentivizedLocationPool);
            }

            Dictionary <MapLocation, Tuple <List <MapChange>, AccessRequirement> > fullLocationRequirements = _overworldMap.FullLocationRequirements;
            Dictionary <MapLocation, OverworldTeleportIndex> overridenOverworld = _overworldMap.OverriddenOverworldLocations;

            List <IRewardSource> placedItems  = null;
            List <Item>          treasurePool = null;
            var itemShopItem = Item.Bottle;

            bool placementFailed;

            do
            {
                placementFailed = false;

                //That number(7.0) is a "tuned" parameter. I divided the number of chests by the number of npcs. Took half of that and looked through some spoiler logs to see if it was too high or too low.
                var balancedPicker = new RewardSourcePicker(0.5, _flags.LooseItemsNpcBalance ? 7.0 : 1.0, _checker);

                _sanityCounter++;
                if (_sanityCounter > 20)
                {
                    throw new InsaneException("Item Placement could not meet incentivization requirements!");
                }
                // 1. (Re)Initialize lists inside of loop
                placedItems = ctx.Forced.ToList();
                var incentives    = ctx.Incentivized.ToList();
                var nonincentives = ctx.Unincentivized.ToList();
                var shards        = ctx.Shards.ToList();
                var removedItems  = ctx.Removed.ToList();
                treasurePool = ctx.AllTreasures.ToList();
                incentives.Shuffle(rng);
                nonincentives.Shuffle(rng);

                while (incentives.Count() > incentiveLocationPool.Count())
                {
                    nonincentives.Add(incentives.SpliceRandom(rng));
                }

                if (((bool)_flags.NPCItems) || ((bool)_flags.NPCFetchItems))
                {
                    // Identify but don't place caravan item first because among incentive locations it has the smallest set of possible items
                    itemShopItem = SelectVendorItem(incentives, nonincentives, treasurePool, incentiveLocationPool, rng);

                    // unrestricted items can get placed anywhere in the game. Everything else will only be placed where we can reach at this point.
                    List <Item> unrestricted = new List <Item> {
                        Item.Key, Item.Canoe, Item.Floater
                    };

                    // We will place these items in this very order so that we can ensure the bridge is early, and we don't need the floater to find the ship.
                    List <Item> fixedPlacements;
                    if (_flags.Archipelago)
                    {
                        fixedPlacements = new List <Item> {
                            Item.Bridge, Item.Key, Item.Canoe
                        };
                    }
                    else
                    {
                        fixedPlacements = new List <Item> {
                            Item.Key, Item.Bridge, Item.Canoe
                        };
                    }

                    List <Item> nextPlacements = new List <Item> {
                        Item.Ship, Item.Canal
                    };
                    List <Item> lastPlacements = new List <Item> {
                        Item.Floater, Item.Lute, Item.Crown, Item.Crystal, Item.Herb, Item.Tnt, Item.Adamant,
                        Item.Slab, Item.Ruby, Item.Rod, Item.Chime, Item.Tail, Item.Cube, Item.Bottle, Item.Oxyale
                    };

                    if ((bool)_flags.EarlierRuby)
                    {
                        nextPlacements.Add(Item.Ruby);
                        lastPlacements.Remove(Item.Ruby);
                    }
                    if ((bool)_flags.IsFloaterRemoved)
                    {
                        lastPlacements.Remove(Item.Floater);
                    }

                    if (_flags.DesertOfDeath && !(bool)_flags.IsShipFree)
                    {
                        nextPlacements.Remove(Item.Ship);
                        fixedPlacements.Add(Item.Ship);
                        unrestricted.Add(Item.Ship);
                    }

                    // Different placement priorities for NoOverworld
                    if (((IItemShuffleFlags)_flags).NoOverworld)
                    {
                        List <Item> nodeItems     = new() { Item.Floater, Item.Canoe };
                        List <Item> corridorItems = new() { Item.Tnt, Item.Ruby };
                        List <Item> fetchItems    = new() { Item.Crown, Item.Herb, Item.Crystal };

                        if ((bool)_flags.Entrances)
                        {
                            nodeItems.Add(Item.Key);
                            corridorItems.Add(Item.Oxyale);
                            corridorItems.Add(Item.Chime);

                            unrestricted    = new List <Item> {
                            };
                            fixedPlacements = new List <Item> {
                                nodeItems.SpliceRandom(rng), corridorItems.SpliceRandom(rng)
                            };
                            nextPlacements = new List <Item> {
                                nodeItems.SpliceRandom(rng), corridorItems.SpliceRandom(rng)
                            };
                            lastPlacements = new List <Item> {
                                Item.Lute, Item.Crown, Item.Crystal, Item.Herb, Item.Adamant,
                                Item.Slab, Item.Tail, Item.Cube, Item.Bottle, Item.Rod, Item.Chime, Item.Bridge, Item.Ship, Item.Canal
                            };

                            lastPlacements.AddRange(nodeItems);
                            lastPlacements.AddRange(corridorItems);
                        }
                        else
                        {
                            unrestricted = new List <Item> {
                                Item.Key
                            };
                            fixedPlacements = new List <Item> {
                                Item.Key, fetchItems.SpliceRandom(rng), fetchItems.SpliceRandom(rng), corridorItems.SpliceRandom(rng)
                            };
                            nextPlacements = new List <Item> {
                                nodeItems.SpliceRandom(rng), corridorItems.SpliceRandom(rng)
                            };
                            lastPlacements = new List <Item> {
                                Item.Lute, Item.Adamant,
                                Item.Slab, Item.Tail, Item.Cube, Item.Bottle, Item.Oxyale, Item.Rod, Item.Chime, Item.Bridge, Item.Ship, Item.Canal
                            };

                            lastPlacements.AddRange(nodeItems);
                            lastPlacements.AddRange(corridorItems);
                            lastPlacements.AddRange(fetchItems);
                        }
                    }

                    nextPlacements.Shuffle(rng);
                    lastPlacements.Shuffle(rng);
                    var allPlacements = fixedPlacements.Concat(nextPlacements).Concat(lastPlacements).Except(removedItems);

                    foreach (var item in allPlacements)
                    {
                        if (placedItems.Any(x => x.Item == item))
                        {
                            continue;
                        }

                        if (item == itemShopItem)
                        {
                            placedItems.Add(new ItemShopSlot(_caravanItemLocation, itemShopItem));
                            itemShopItem = Item.None;
                            continue;
                        }

                        var(_, mapLocations, requirements) = _checker.CheckSanity(placedItems, fullLocationRequirements, _flags);

                        var isIncentive  = incentives.Contains(item);
                        var locationPool = isIncentive ? incentiveLocationPool : preBlackOrbUnincentivizedLocationPool;
                        var itemPool     = isIncentive ? incentives : nonincentives;

                        System.Diagnostics.Debug.Assert(itemPool.Contains(item));

                        // Can we find a home for this item at this point in the exploration?
                        var rewardSources = locationPool.Where(x => !placedItems.Any(y => y.Address == x.Address) &&
                                                               x.Address != ItemLocations.CaravanItemShop1.Address &&
                                                               (unrestricted.Contains(item) || _checker.IsRewardSourceAccessible(x, requirements, mapLocations)))
                                            .ToList();

                        // If we can great, if not, we'll revisit after we get through everything.
                        if (rewardSources.Any())
                        {
                            itemPool.Remove(item);
                            var rewardSource = balancedPicker.Pick(rewardSources, _flags.LooseItemsForwardPlacement && !isIncentive, _flags.LooseItemsSpreadPlacement, isIncentive, rng);
                            placedItems.Add(NewItemPlacement(rewardSource, item));
                        }
                    }
                }

                // This is a junk item that didn't get placed in the above loop.
                if (!placedItems.Any(item => item.Address == _caravanItemLocation.Address))
                {
                    placedItems.Add(new ItemShopSlot(_caravanItemLocation, itemShopItem));
                }

                // 5. Then place all incentive locations that don't have special logic
                var incentiveLocations = incentiveLocationPool.Where(x => !placedItems.Any(y => y.Address == x.Address) && x.Address != ItemLocations.CaravanItemShop1.Address).ToList();
                incentiveLocations.Shuffle(rng);
                foreach (var incentiveLocation in incentiveLocations)
                {
                    if (!incentives.Any())
                    {
                        break;
                    }
                    placedItems.Add(NewItemPlacement(incentiveLocation, incentives.SpliceRandom(rng)));
                }

                // 6. Then place remanining incentive items, unincentivized quest items, and shards in any other chest before ToFR
                var leftoverItems = incentives.Concat(nonincentives).ToList();
                leftoverItems.Shuffle(rng);

                var(_, accessibleMapLocations, fulfilledRequirements) = _checker.CheckSanity(placedItems, fullLocationRequirements, _flags);

                IEnumerable <IRewardSource> looseLocationPool;
                if ((bool)_flags.LooseExcludePlacedDungeons)
                {
                    looseLocationPool = preBlackOrbUnincentivizedLocationPool;
                }
                else
                {
                    looseLocationPool = preBlackOrbLocationPool;
                }
                var leftoverItemLocations = looseLocationPool.Where(x => !placedItems.Any(y => y.Address == x.Address) &&
                                                                    _checker.IsRewardSourceAccessible(x, fulfilledRequirements, accessibleMapLocations)).ToList();


                // Place leftover items before placing shards, because if LooseExcludePlacedDungeons is set,
                // and there are lots of incentive locations, leftoverItemLocations pool may be small.
                foreach (var leftoverItem in leftoverItems)
                {
                    if (!leftoverItemLocations.Any())
                    {
                        placementFailed = true;
                        break;
                    }

                    placedItems.Add(NewItemPlacement(leftoverItemLocations.SpliceRandom(rng), leftoverItem));

                    //If the placed item is a KI, update the accessible locations
                    if (ItemLists.AllQuestItems.Contains(leftoverItem))
                    {
                        (_, accessibleMapLocations, fulfilledRequirements) = _checker.CheckSanity(placedItems, fullLocationRequirements, _flags);

                        leftoverItemLocations = looseLocationPool.Where(x => !placedItems.Any(y => y.Address == x.Address) &&
                                                                        _checker.IsRewardSourceAccessible(x, fulfilledRequirements, accessibleMapLocations)).ToList();
                    }
                }

                if (!placementFailed && shards.Count > 0)
                {
                    // Place shards.  These are not affected by the LooseExcludePlacedDungeons flag.
                    leftoverItemLocations = preBlackOrbLocationPool.Where(x => !placedItems.Any(y => y.Address == x.Address) &&
                                                                          _checker.IsRewardSourceAccessible(x, fulfilledRequirements, accessibleMapLocations)).ToList();

                    foreach (var shard in shards)
                    {
                        if (!leftoverItemLocations.Any())
                        {
                            placementFailed = true;
                            break;
                        }

                        placedItems.Add(NewItemPlacement(leftoverItemLocations.SpliceRandom(rng), shard));
                    }
                }

                // 7. Check sanity and loop if needed
            } while (placementFailed || !_checker.CheckSanity(placedItems, fullLocationRequirements, _flags).Complete);

            return(new ItemPlacementResult {
                PlacedItems = placedItems, RemainingTreasures = treasurePool
            });
        }
        protected override ItemPlacementResult DoSanePlacement(MT19337 rng, ItemPlacementContext ctx)
        {
            exceptionWeights   = _flags.AllowUnsafePlacement || (_flags.Entrances ?? false) ? UnsafeExceptionWeights : SafeExceptionWeights;
            exceptionMinCycles = _flags.AllowUnsafePlacement || (_flags.Entrances ?? false) ? UnsafeExceptioMinCycles : SafeExceptioMinCycles;
            minAccessibility   = _flags.AllowUnsafePlacement || (_flags.Entrances ?? false) ? UnsafeMinAccessibility : SafeMinAccessibility;

            _sanityCounter = 0;
            var incentiveLocationPool   = new HashSet <IRewardSource>(_incentivesData.IncentiveLocations, new RewardSourceEqualityComparer());
            var preBlackOrbLocationPool = _incentivesData.AllValidPreBlackOrbItemLocations.ToList();
            var preBlackOrbUnincentivizedLocationPool = preBlackOrbLocationPool.Where(x => !incentiveLocationPool.Any(y => y.Address == x.Address)).ToList();

            if ((bool)_flags.LooseExcludePlacedDungeons)
            {
                preBlackOrbUnincentivizedLocationPool = IncentivizedDungeons(preBlackOrbUnincentivizedLocationPool);
            }

            var unincentivizedLocationPool = new HashSet <IRewardSource>(preBlackOrbUnincentivizedLocationPool, new RewardSourceEqualityComparer());

            var allRewardSources = preBlackOrbLocationPool.Append(new ItemShopSlot(_caravanItemLocation, Item.None)).ToList();

            List <IRewardSource> placedItems  = null;
            List <Item>          treasurePool = null;

            freeRequirements = BuildFreeRequirements();

            bool placementFailed;

            do
            {
                ((SanityCheckerV2)_checker).Shiplocations.SetShipLocation(255);

                BuildLogic(allRewardSources);

                placementFailed = false;

                var balancedPicker = new RewardSourcePicker(0.5, _flags.LooseItemsNpcBalance ? 7.0 : 1.0, _checker);

                _sanityCounter++;
                if (_sanityCounter > 2)
                {
                    throw new InsaneException("Item Placement could not meet incentivization requirements!");
                }

                placedItems = ctx.Forced.ToList();
                var incentives    = new HashSet <Item>(ctx.Incentivized);
                var nonincentives = ctx.Unincentivized.ToList();
                var shards        = ctx.Shards.ToList();
                treasurePool = ctx.AllTreasures.ToList();
                var state = PlacementState.Normal;

                while (incentives.Count() > incentiveLocationPool.Count())
                {
                    nonincentives.Add(incentives.SpliceRandom(rng));
                }

                if (((bool)_flags.NPCItems) || ((bool)_flags.NPCFetchItems))
                {
                    HashSet <Item> allPlacements = new HashSet <Item>(nonincentives.Concat(incentives));
                    HashSet <Item> allKeyItems   = new HashSet <Item>(MapChangeItems.Concat(FetchQuestItems).Concat(GatingItems).Intersect(allPlacements));

                    if ((bool)_flags.IsFloaterRemoved)
                    {
                        allKeyItems.Remove(Item.Floater);
                    }

                    //The sanity checker currently doesn't allow tracking which shops are available
                    //It could be easily added, but a little randomnes can't hurt(or so I'm thinking)
                    //So it places the vendoritem upfront.
                    var itemShopItem = SelectVendorItem(incentives.ToList(), nonincentives, treasurePool, incentiveLocationPool, rng);
                    placedItems.Add(new ItemShopSlot(_caravanItemLocation, itemShopItem));

                    allPlacements.Remove(itemShopItem);
                    allKeyItems.Remove(itemShopItem);

                    if (_flags.Archipelago && allKeyItems.Contains(Item.Bridge))
                    {
                        var accessibleSources = GetAllAccessibleRewardSources(preBlackOrbUnincentivizedLocationPool, placedItems);

                        var rewardSource = balancedPicker.Pick(accessibleSources, _flags.LooseItemsForwardPlacement, _flags.LooseItemsSpreadPlacement, false, rng);
                        placedItems.Add(NewItemPlacement(rewardSource, Item.Bridge));

                        allPlacements.Remove(Item.Bridge);
                        allKeyItems.Remove(Item.Bridge);
                    }

                    //Since it can waste cycles, this number needs to be high enough to be able to place all incentives and all nonincentives and still have some leeway
                    //When it's done it breaks anyway
                    //When it reached enough cycles so no cycle based restricitons apply and can place neither an incentive nor a nonincentive it also breaks
                    for (int cycle = 1; cycle <= 100; cycle++)
                    {
                        var accessibleSources = GetAllAccessibleRewardSources(preBlackOrbLocationPool, placedItems);

                        //look how many rewardsources of each type are available
                        var incSourceCount = accessibleSources.Where(s => incentiveLocationPool.Contains(s)).Count();
                        var nonSourceCount = accessibleSources.Count - incSourceCount;

                        List <(Item item, int incCount, int nonCount)> candidates = new List <(Item item, int incCount, int nonCount)>();

                        //go through each KI, place it in the testRewardSource, run the sanity checker and see how many RewardSources are available afterwards.
                        foreach (var item in allKeyItems)
                        {
                            var accessibleSources2 = GetAllAccessibleRewardSources(preBlackOrbLocationPool, placedItems, item);

                            var incCount = accessibleSources2.Where(s => incentiveLocationPool.Contains(s)).Count();
                            var nonCount = accessibleSources2.Count - incCount - nonSourceCount;
                            incCount -= incSourceCount;

                            //if the placed item is an incentive, it will take up an incentive location when it will be placed
                            //and vice versa
                            if (incentives.Contains(item))
                            {
                                incCount--;
                            }
                            if (!incentives.Contains(item))
                            {
                                nonCount--;
                            }

                            candidates.Add((item, incCount, nonCount));
                        }

                        //Since it can happen, that neither the ship, nor the canal by themselves open up something, but the combination does,
                        //here the combination is checked
                        //it grabs another Rewardsource, places Ship and Canal, runs the SanityChecker and calculates the results
                        if (allKeyItems.Contains(Item.Ship) && allKeyItems.Contains(Item.Canal))
                        {
                            var accessibleSources2 = GetAllAccessibleRewardSources(preBlackOrbLocationPool, placedItems, Item.Ship, Item.Canal);

                            var incCount = accessibleSources2.Where(s => incentiveLocationPool.Contains(s)).Count();
                            var nonCount = accessibleSources2.Count - incCount - nonSourceCount;
                            incCount -= incSourceCount;

                            if (incentives.Contains(Item.Ship))
                            {
                                incCount--;
                            }
                            if (!incentives.Contains(Item.Ship))
                            {
                                nonCount--;
                            }

                            if (incentives.Contains(Item.Canal))
                            {
                                incCount--;
                            }
                            if (!incentives.Contains(Item.Canal))
                            {
                                nonCount--;
                            }

                            //it gets the ship and canal candidate from the list, later it's put back in
                            var shipCandidate  = candidates.FirstOrDefault(c => c.item == Item.Ship);
                            var canalCandidate = candidates.FirstOrDefault(c => c.item == Item.Canal);

                            candidates.Remove(shipCandidate);
                            candidates.Remove(canalCandidate);

                            //half of the opened up incentive locations and nonincentive locations hould go to either item(rounded down)
                            //In order to continue the seed, Ship and Canal must open up 2 incentive locations, since they may take up 2
                            //it's not an exact estimate, since it doesn'T differentiate between them beeing incentive or nonincentive items
                            incCount /= 2;
                            nonCount /= 2;

                            if (shipCandidate.incCount < incCount && canalCandidate.incCount < incCount)
                            {
                                shipCandidate.incCount  = incCount;
                                canalCandidate.incCount = incCount;
                            }

                            if (shipCandidate.nonCount < nonCount && canalCandidate.nonCount < nonCount)
                            {
                                shipCandidate.nonCount  = nonCount;
                                canalCandidate.nonCount = nonCount;
                            }

                            candidates.Add(shipCandidate);
                            candidates.Add(canalCandidate);
                        }

                        var incKeyItemCount = incentives.Where(i => allKeyItems.Contains(i)).Count();
                        var nonKeyItemCount = nonincentives.Where(i => allKeyItems.Contains(i)).Count();

                        List <(float weight, Item item)> weightedCandidates;

                        var incItemCount = allPlacements.Where(i => incentives.Contains(i)).Count();

                        //here it randomly decides whether to place and incentive or a non incentive
                        //the chance is based on the number of incentive and nonincentive items
                        //If however it couldn't place an incentive item in the last cycle, it'll always try a nonincentive this time
                        //and vice versa
                        if (state == PlacementState.Incentive || state == PlacementState.Normal && rng.Between(1, allPlacements.Count) <= incItemCount || _flags.LaterLoose && state == PlacementState.Normal && cycle <= 5)
                        {
                            //Filter out nonincentive items
                            candidates = candidates.Where(c => incentives.Contains(c.item)).ToList();
                            var nonKeyCandidates = allPlacements.Where(i => !allKeyItems.Contains(i) && incentives.Contains(i));

                            if (incSourceCount <= 1 || incSourceCount <= CriticalIncentiveSourceCount && allKeyItems.Contains(Item.Ship) && allKeyItems.Contains(Item.Canal))
                            {
                                //we are in incentive critical mode
                                //the function takes weights for different categories of items(depending of how they're expected they change the amount of incentive locations available)
                                //bin0 doesn't open up a new location(it therefore reduces the incentive location count by one)
                                //bin1 opens up 1 location and therefore keeps the count the same
                                //bin2 opens up 2 locations and therefore increases the amount by 1
                                //bin3 opens up more than two locations. It has two parameters here, more on that further down

                                //Anyway, we can't afford to reduce the incentive location count here(bin0)
                                //We also don't neccesarily want to keep it the same, but that's okish(bin1)
                                //we want to increase the amount(bin2, bin3)
                                weightedCandidates = AssignWeights(candidates, 0.0f, 0.5f, 1.0f, 1.0f, 0.5f, cycle, accessibleSources.Count);
                            }
                            else if (incSourceCount <= SafeIncentiveSourceCount)
                            {
                                //We have 1 incentive location to squander, not a lot, but sufficient
                                //We don't want to reduce it or keep it the same, but it would still be ok(bin0, bin1)
                                //We'd like to increase it a little(bin2)
                                //but not too much(bin3)
                                weightedCandidates = AssignWeights(candidates, 0.5f, 0.5f, 1.0f, 0.5f, 0.25f, cycle, accessibleSources.Count);

                                //it's also ok to place a NonKeyItem, wa have an incentive location to squander after all
                                //but don't make it too likely
                                weightedCandidates.AddRange(nonKeyCandidates.Select(i => (GetItemWeight(i, 0.5f, cycle, accessibleSources.Count), i)));
                            }
                            else
                            {
                                //We have at least 2 incentive locations to squander(considered a lot)
                                //We like reducing it by one(for variety)
                                //We like keeping it the same(for variety)
                                //We also like to increase it slightly(for variety)
                                //But we don't like to increase it a lot, but then again it's not a problem, so make it less likely
                                weightedCandidates = AssignWeights(candidates, 1.0f, 1.0f, 1.0f, 0.25f, 0.25f, cycle, accessibleSources.Count);

                                //Also we like placing NoneKeyItems
                                weightedCandidates.AddRange(nonKeyCandidates.Select(i => (GetItemWeight(i, 1.0f, cycle, accessibleSources.Count), i)));
                            }

                            //If we didn't find any candidates, but we have enough locations to place all remaining items, then we just do that
                            //Otherwise the algorithm would refuse to give up the last 2 incentive locations
                            if (weightedCandidates.Sum(i => i.weight) == 0 && incItemCount <= incSourceCount)
                            {
                                weightedCandidates.AddRange(allPlacements.Where(i => incentives.Contains(i)).Select(i => (1.0f, i)));
                            }

                            //If we don't have any candidates and not enough incentive locations to place all, we can't place an incentive in this cycle
                            if (weightedCandidates.Sum(i => i.weight) == 0)
                            {
                                //If it's at least cycle 15 and therefore no cycle based restricctions apply
                                //And in the last cycle we tried to place a NonIncentive
                                //we had all KI options available, but couldn't place any of them, so we're doomed
                                if (cycle >= 15 && state != PlacementState.Normal)
                                {
                                    placementFailed = true;
                                    break;
                                }

                                //since we couldn't place an incentive, we won't be able to do so next cycle(same results), so we don't try to do that next cycle
                                //This heavily speeds up the situations when there are very few loose(low chance of trying to place a loose), but a loose is required to go on.
                                state = PlacementState.NonIncentice;
                                continue;
                            }
                        }
                        else
                        {
                            //same as above, but for nonincentive items.
                            //The main difference is that the RewardSource thresholds are way higher
                            //While it's not a safety concern, cou generally want a lot more chest's available than neccesary to be in the confort zone
                            candidates = candidates.Where(c => !incentives.Contains(c.item)).ToList();
                            var nonKeyCandidates = allPlacements.Where(i => !allKeyItems.Contains(i) && !incentives.Contains(i));

                            if (nonSourceCount <= CriticalNonIncentiveSourceCount)
                            {
                                weightedCandidates = AssignWeights2(candidates, 0.0f, 0.5f, 1.0f, 1.0f, 0.5f, cycle, accessibleSources.Count);
                            }
                            else if (nonSourceCount <= SafeNonIncentiveSourceCount)
                            {
                                weightedCandidates = AssignWeights2(candidates, 0.5f, 0.5f, 1.0f, 0.5f, 0.25f, cycle, accessibleSources.Count);
                                weightedCandidates.AddRange(nonKeyCandidates.Select(i => (GetItemWeight(i, 0.5f, cycle, accessibleSources.Count), i)));
                            }
                            else
                            {
                                weightedCandidates = AssignWeights2(candidates, 1.0f, 1.0f, 1.0f, 0.25f, 0.25f, cycle, accessibleSources.Count);
                                weightedCandidates.AddRange(nonKeyCandidates.Select(i => (GetItemWeight(i, 1.0f, cycle, accessibleSources.Count), i)));
                            }

                            if (weightedCandidates.Sum(i => i.weight) == 0)
                            {
                                if (cycle >= 15 && state != PlacementState.Normal)
                                {
                                    placementFailed = true;
                                    break;
                                }

                                state = PlacementState.Incentive;
                                continue;
                            }
                        }

                        //The above weighting strategy works with this mindset:
                        //If it's an all incentive seed, we just need to manage the incentive locations
                        //If it's an all loose seed, we just need to manage the nonincentive locations
                        //If it's something in between, we switch between those two ideas.
                        //It works, because a nonincentive item goes into a nonincentive chest and can't negatively impact the incentive locations.
                        //and vice versa.
                        //There is a flaw in this simplistic approach. When placing a nonincentive, it could additionally weigh in if it's desired for incentive placment.
                        //But that's a thing for another time to think through

                        //remove the candidates with weight 0(should be unneccesary, but anyway)
                        weightedCandidates = weightedCandidates.Where(w => w.weight > 0).ToList();

                        //shuffle the list(should be totally unneccesary, but it makes me more comfortable)
                        weightedCandidates.Shuffle(rng);

                        //randomly select an item from the weighted list
                        Item nextPlacment = Item.None;

                        float sum = weightedCandidates.Sum(i => i.weight);

                        var r = rng.Next() / (float)uint.MaxValue * sum;

                        sum = 0.0f;
                        foreach (var s in weightedCandidates)
                        {
                            sum += s.weight;
                            if (r <= sum)
                            {
                                nextPlacment = s.item;
                                break;
                            }
                        }

                        //if we didn't find one, just select a random one(this shouldn't happen, but safe is safe)
                        if (nextPlacment == Item.None)
                        {
                            nextPlacment = weightedCandidates.PickRandom(rng).item;
                        }

                        //place the item(same code as in GuidedPlacement to have compatibility with other flags)
                        if (incentives.Contains(nextPlacment))
                        {
                            var rewardSources = accessibleSources.Where(s => incentiveLocationPool.Contains(s)).ToList();

                            //can't happen per definition. We already know, that there is a location available for the item(but in case anything unexpectedly goes wrong sure)
                            if (rewardSources.Count == 0)
                            {
                                continue;
                            }

                            var rewardSource = balancedPicker.Pick(rewardSources, false, _flags.LooseItemsSpreadPlacement, false, rng);
                            placedItems.Add(NewItemPlacement(rewardSource, nextPlacment));
                        }
                        else
                        {
                            var rewardSources = accessibleSources.Where(s => unincentivizedLocationPool.Contains(s)).ToList();
                            if (rewardSources.Count == 0)
                            {
                                continue;
                            }

                            var rewardSource = balancedPicker.Pick(rewardSources, _flags.LooseItemsForwardPlacement, _flags.LooseItemsSpreadPlacement, false, rng);
                            placedItems.Add(NewItemPlacement(rewardSource, nextPlacment));
                        }

                        //remove the item from the lists of items to place
                        allPlacements.Remove(nextPlacment);
                        allKeyItems.Remove(nextPlacment);

                        if (nextPlacment == Item.Ship)
                        {
                            BuildLogic(allRewardSources);
                        }

                        //we placed an item so we should randomly select incentive/nonincentive next cycle
                        state = PlacementState.Normal;

                        //we're finished
                        if (allPlacements.Count == 0)
                        {
                            break;
                        }
                    }

                    //we couldn't place all items or encountered a placement problem
                    if (allPlacements.Count > 0 || placementFailed)
                    {
                        continue;
                    }
                }

                //place the shards(almost same code as in GuidedPlacement)
                if (!placementFailed && shards.Count > 0)
                {
                    var leftoverItemLocations = GetAllAccessibleRewardSources(preBlackOrbLocationPool, placedItems);

                    foreach (var shard in shards)
                    {
                        if (!leftoverItemLocations.Any())
                        {
                            placementFailed = true;
                            break;
                        }

                        placedItems.Add(NewItemPlacement(leftoverItemLocations.SpliceRandom(rng), shard));
                    }
                }

                //finally check the placement(if we arrive here, it's safe, but let's do it anyway in case something goes wrong)
            } while (placementFailed || !_checker.CheckSanity(placedItems, null, _flags).Complete);

            return(new ItemPlacementResult {
                PlacedItems = placedItems, RemainingTreasures = treasurePool
            });
        }