public List <IRewardSource> PlaceSaneItems(MT19337 rng, FF1Rom rom)
        {
            _rom = rom;

            var incentivePool = _incentivesData.IncentiveItems.Where(x => _allTreasures.Contains(x)).ToList();
            var forcedItems   = _incentivesData.ForcedItemPlacements.ToList();
            var removedItems  = _incentivesData.RemovedItems.ToList();

            var unincentivizedQuestItems =
                ItemLists.AllQuestItems
                .Where(x => !incentivePool.Contains(x) &&
                       _allTreasures.Contains(x) &&
                       !forcedItems.Any(y => y.Item == x) &&
                       !removedItems.Contains(x))
                .ToList();

            var shards = new List <Item>();

            var treasurePool = _allTreasures.ToList();

            bool jingleGuaranteedDefenseItem = true;
            bool jingleGuaranteedPowerItem   = true;

            if (_flags.GuaranteedDefenseItem != GuaranteedDefenseItem.None && !(_flags.ItemMagicMode == ItemMagicMode.None) && !incentivePool.Contains(Item.PowerRod))
            {
                unincentivizedQuestItems.Add(Item.PowerRod);
                jingleGuaranteedDefenseItem = false;
            }

            if (_flags.GuaranteedPowerItem != GuaranteedPowerItem.None && !(_flags.ItemMagicMode == ItemMagicMode.None) && !incentivePool.Contains(Item.PowerGauntlets))
            {
                unincentivizedQuestItems.Add(Item.PowerGauntlets);
                jingleGuaranteedPowerItem = false;
            }

            foreach (var incentive in incentivePool)
            {
                treasurePool.Remove(incentive);
            }
            foreach (var placement in forcedItems)
            {
                treasurePool.Remove(placement.Item);
            }
            foreach (var questItem in unincentivizedQuestItems)
            {
                treasurePool.Remove(questItem);
            }
            foreach (var item in removedItems)
            {
                treasurePool.Remove(item);
            }
            while (treasurePool.Remove(Item.Shard))
            {
                shards.Add(Item.Shard);
            }

            ItemPlacementContext ctx = new ItemPlacementContext
            {
                Forced         = forcedItems,
                Incentivized   = incentivePool,
                Unincentivized = unincentivizedQuestItems,
                Shards         = shards,
                Removed        = removedItems,
                AllTreasures   = treasurePool.ToList(),
            };

            ItemPlacementResult  result      = DoSanePlacement(rng, ctx);
            List <IRewardSource> placedItems = result.PlacedItems;

            treasurePool = result.RemainingTreasures;

            //setup jingle for "incentive treasures", the placed items should just be key items, loose incentive items
            if (_flags.IncentiveChestItemsFanfare)
            {
                foreach (var placedItem in placedItems)
                {
                    //dont make shards jingle that'd be annoying
                    //dont make free items that get replaced, aka cabins, jingle
                    if (placedItem is TreasureChest && placedItem.Item != Item.Shard &&
                        placedItem.Item != ReplacementItem)
                    {
                        if ((placedItem.Item == Item.PowerGauntlets && !jingleGuaranteedPowerItem) || (placedItem.Item == Item.PowerRod && !jingleGuaranteedDefenseItem))
                        {
                            continue;
                        }

                        rom.Put(placedItem.Address - FF1Rom.TreasureOffset + FF1Rom.TreasureJingleOffset, new byte[] { (byte)placedItem.Item });
                    }
                }
            }

            // 8. Place all remaining unincentivized treasures or incentivized non-quest items that weren't placed
            var itemLocationPool = _incentivesData.AllValidItemLocations.ToList();

            itemLocationPool = itemLocationPool.Where(x => !x.IsUnused && !placedItems.Any(y => y.Address == x.Address)).ToList();

            MoreConsumableChests.Work(_flags, treasurePool, rng);

            if ((bool)_flags.GuaranteedMasamune && !incentivePool.Contains(Item.Masamune) && (bool)_flags.SendMasamuneHome)
            {
                // Remove Masamune from treasure pool (This will also causes Masamune to not be placed by RandomLoot)
                treasurePool.Remove(Item.Masamune);
            }

            // Use cabins to balance item population
            int poolDifference = itemLocationPool.Count() - treasurePool.Count();

            if (poolDifference < 0)
            {
                for (int i = 0; i > poolDifference; i--)
                {
                    treasurePool.Remove(Item.Cabin);
                }
            }
            else if (poolDifference > 0)
            {
                treasurePool.AddRange(Enumerable.Repeat(Item.Cabin, poolDifference));
            }

            Debug.Assert(treasurePool.Count() == itemLocationPool.Count());


            List <IRewardSource> normalTreasures = new();

            var randomizeTreasureMode = (_flags.RandomizeTreasure == RandomizeTreasureMode.Random) ? (RandomizeTreasureMode)rng.Between(0, 2) : _flags.RandomizeTreasure;

            if (randomizeTreasureMode != RandomizeTreasureMode.None)
            {
                IItemGenerator generator;

                // We want to leave out anything incentivized (and thus already placed), but
                // add all the other stuff that you can't find in vanilla.
                var randomTreasure = treasurePool.ToList();

                if (randomizeTreasureMode == RandomizeTreasureMode.DeepDungeon)
                {
                    generator = new DeepDungeonItemGenerator(itemLocationPool, rom.UnusedGoldItems, removedItems, normalTreasures, (_flags.GameMode == GameModes.DeepDungeon), _flags.Etherizer, rom);
                }
                else
                {
                    generator = new ItemGenerator(randomTreasure, rom.UnusedGoldItems, removedItems, _flags.WorldWealth);
                }

                treasurePool = treasurePool.Select(treasure => generator.GetItem(rng)).ToList();
            }

            if ((bool)_flags.BetterTrapChests && randomizeTreasureMode != RandomizeTreasureMode.DeepDungeon)
            {
                // First we'll make a list of all 'notable' treasure.
                var notableTreasureList = new List <Item>()
                                          .Concat(ItemLists.UberTier)
                                          .Concat(ItemLists.LegendaryWeaponTier)
                                          .Concat(ItemLists.LegendaryArmorTier)
                                          .Concat(ItemLists.RareWeaponTier)
                                          .Concat(ItemLists.RareArmorTier);
                // Convert the list to a HashSet since we'll be doing lookups in it.
                var notableTreasure = new HashSet <Item>(notableTreasureList);

                // We sort the treasure pool based on value (sort of) and pull out the highest ranked ones to put
                // in the trap chests we picked out.
                var notableTreasurePool = treasurePool.Where(item => notableTreasure.Contains(item)).ToList();

                // Since some chests might be incentivized, remove those that aren't in the pool.
                var trapChestPool = TrapChests.Where(chest => itemLocationPool.Contains(chest));

                foreach (var chest in trapChestPool)
                {
                    // It seems unlikely that is possible, but just in case.
                    if (!notableTreasurePool.Any())
                    {
                        break;
                    }

                    // Pick a random treasure and place it.
                    var treasure = notableTreasurePool.SpliceRandom(rng);
                    placedItems.Add(NewItemPlacement(chest, treasure));

                    // Since it was placed, remove both the item and location from the remaining pool.
                    treasurePool.Remove(treasure);
                    itemLocationPool.Remove(chest);
                }

                // This should still be true at the end, so make sure it is
                Debug.Assert(treasurePool.Count() == itemLocationPool.Count());
            }

            if (randomizeTreasureMode == RandomizeTreasureMode.DeepDungeon && _flags.DeepDungeonGenerator == DeepDungeonGeneratorMode.Progressive)
            {
                placedItems.AddRange(normalTreasures);
            }
            else
            {
                treasurePool.Shuffle(rng);
                itemLocationPool.Shuffle(rng);

                var leftovers = treasurePool.Zip(itemLocationPool, (treasure, location) => NewItemPlacement(location, treasure));
                placedItems.AddRange(leftovers);
            }

            //chest order placement
            var chestOrderPool = treasurePool;

            if (_flags.ShardHunt)
            {
                //add the shards back into the chest order pool
                chestOrderPool = chestOrderPool.Concat(shards).ToList();
            }

            chestOrderPool.Shuffle(rng);

            for (int i = 0; i < chestOrderPool.Count; i++)
            {
                rom.Put(FF1Rom.TreasureChestOrderOffset + i, new byte[] { (byte)chestOrderPool[i] });
            }

            return(placedItems);
        }
예제 #2
0
        public List <IRewardSource> PlaceSaneItems(MT19337 rng)
        {
            var incentivePool = _incentivesData.IncentiveItems.Where(x => _allTreasures.Contains(x)).ToList();
            var forcedItems   = _incentivesData.ForcedItemPlacements.ToList();

            var unincentivizedQuestItems =
                ItemLists.AllQuestItems
                .Where(x => !incentivePool.Contains(x) &&
                       _allTreasures.Contains(x) &&
                       !forcedItems.Any(y => y.Item == x))
                .ToList();

            var treasurePool = _allTreasures.ToList();

            if ((bool)_flags.GuaranteedRuseItem)
            {
                unincentivizedQuestItems.Add(Item.PowerRod);
            }
            foreach (var incentive in incentivePool)
            {
                treasurePool.Remove(incentive);
            }
            foreach (var placement in forcedItems)
            {
                treasurePool.Remove(placement.Item);
            }
            foreach (var questItem in unincentivizedQuestItems)
            {
                treasurePool.Remove(questItem);
            }
            while (treasurePool.Remove(Item.Shard))
            {
                unincentivizedQuestItems.Add(Item.Shard);
            }

            ItemPlacementContext ctx = new ItemPlacementContext
            {
                Forced         = forcedItems,
                Incentivized   = incentivePool,
                Unincentivized = unincentivizedQuestItems,
                AllTreasures   = treasurePool.ToList(),
            };

            ItemPlacementResult result = DoSanePlacement(rng, ctx);
            var placedItems            = result.PlacedItems;

            treasurePool = result.RemainingTreasures;

            if ((bool)_flags.FreeBridge)
            {
                placedItems = placedItems.Select(x => x.Item != Item.Bridge ? x : NewItemPlacement(x, ReplacementItem)).ToList();
            }
            if ((bool)_flags.FreeAirship)
            {
                placedItems = placedItems.Select(x => x.Item != Item.Floater ? x : NewItemPlacement(x, ReplacementItem)).ToList();
            }
            if ((bool)_flags.FreeShip)
            {
                placedItems = placedItems.Select(x => x.Item != Item.Ship ? x : NewItemPlacement(x, ReplacementItem)).ToList();
            }
            if ((bool)_flags.FreeCanal)
            {
                placedItems = placedItems.Select(x => x.Item != Item.Canal ? x : NewItemPlacement(x, ReplacementItem)).ToList();
            }
            if ((bool)_flags.FreeCanoe)
            {
                placedItems = placedItems.Select(x => x.Item != Item.Canoe ? x : NewItemPlacement(x, ReplacementItem)).ToList();
            }
            if ((bool)_flags.FreeLute)
            {
                placedItems = placedItems.Select(x => x.Item != Item.Lute ? x : NewItemPlacement(x, ReplacementItem)).ToList();
            }
            if ((bool)_flags.FreeTail || (bool)_flags.NoTail)
            {
                placedItems = placedItems.Select(x => x.Item != Item.Tail ? x : NewItemPlacement(x, ReplacementItem)).ToList();
            }

            if (_flags.Spoilers || Debugger.IsAttached)
            {
                Console.WriteLine($"ItemPlacement::PlaceSaneItems required {_sanityCounter} iterations.");
                Console.WriteLine("");
                Console.WriteLine("Item     Entrance  ->  Floor  ->  Source                             Requirements");
                Console.WriteLine("----------------------------------------------------------------------------------------------------");

                var sorted = placedItems.Where(item => item.Item != Item.Shard).ToList();
                sorted.Sort((IRewardSource lhs, IRewardSource rhs) => lhs.Item.ToString().CompareTo(rhs.Item.ToString()));
                sorted.ForEach(item =>
                {
                    if (_overworldMap.FullLocationRequirements.TryGetValue(item.MapLocation, out var flr))
                    {
                        var overworldLocation = item.MapLocation.ToString();
                        if (_overworldMap.OverriddenOverworldLocations != null && _overworldMap.OverriddenOverworldLocations.TryGetValue(item.MapLocation, out var overriden))
                        {
                            overworldLocation = overriden.ToString();
                        }

                        var itemStr = item.Item.ToString().PadRight(9);
                        var locStr  = $"{overworldLocation} -> {item.MapLocation} -> {item.Name} ".PadRight(60);
                        var changes = $"({String.Join(" OR ", flr.Item1.Select(mapChange => mapChange.ToString()).ToArray())})";
                        var reqs    = flr.Item2.ToString().CompareTo("None") == 0 ? "" : $" AND {flr.Item2.ToString()}";
                        Console.WriteLine($"{itemStr}{locStr}{changes}{reqs}");
                    }
                });
            }

            // 8. Place all remaining unincentivized treasures or incentivized non-quest items that weren't placed
            var itemLocationPool = _incentivesData.AllValidItemLocations.ToList();

            itemLocationPool = itemLocationPool.Where(x => !x.IsUnused && !placedItems.Any(y => y.Address == x.Address)).ToList();

            if ((bool)_flags.NoMasamune)
            {
                // Remove Masamune chest from shuffle
                treasurePool.Remove(Item.Masamune);
                treasurePool.Add(Item.Cabin);
            }
            else if ((bool)_flags.GuaranteedMasamune)
            {
                // Remove Masamune chest from shuffle, Remove Cabin from item pool
                itemLocationPool = itemLocationPool.Where(x => !x.Equals(ItemLocations.ToFRMasmune)).ToList();
                treasurePool.Remove(Item.Cabin);

                // Send Masamune Home is ignored when Masamune is incentivized
                if (!incentivePool.Contains(Item.Masamune))
                {
                    if ((bool)_flags.SendMasamuneHome)
                    {
                        // Remove Masamune from treasure pool (This will also causes Masamune to not be placed by RandomLoot)
                        treasurePool.Remove(Item.Masamune);
                        treasurePool.Add(Item.Cabin);
                    }
                }
            }

            foreach (var placedItem in placedItems)
            {
                incentivePool.Remove(placedItem.Item);
            }
            treasurePool.AddRange(incentivePool);

            Debug.Assert(treasurePool.Count() == itemLocationPool.Count());

            if ((bool)_flags.RandomLoot)
            {
                // We want to leave out anything incentivized (and thus already placed), but
                // add all the other stuff that you can't find in vanilla.
                var randomTreasure = treasurePool.ToList();
                randomTreasure.AddRange(ItemLists.CommonWeaponTier);
                randomTreasure.AddRange(ItemLists.CommonArmorTier);
                randomTreasure.Add(Item.CatClaw);
                randomTreasure.Add(Item.SteelArmor);

                ItemGenerator generator = new ItemGenerator(randomTreasure, _flags.WorldWealth);
                treasurePool = treasurePool.Select(treasure => generator.GetItem(rng)).ToList();
            }

            if ((bool)_flags.BetterTrapChests)
            {
                // First we'll make a list of all 'notable' treasure.
                var notableTreasureList = new List <Item>()
                                          .Concat(ItemLists.UberTier)
                                          .Concat(ItemLists.LegendaryWeaponTier)
                                          .Concat(ItemLists.LegendaryArmorTier)
                                          .Concat(ItemLists.RareWeaponTier)
                                          .Concat(ItemLists.RareArmorTier);
                // Convert the list to a HashSet since we'll be doing lookups in it.
                var notableTreasure = new HashSet <Item>(notableTreasureList);

                // We sort the treasure pool based on value (sort of) and pull out the highest ranked ones to put
                // in the trap chests we picked out.
                var notableTreasurePool = treasurePool.Where(item => notableTreasure.Contains(item)).ToList();

                // Since some chests might be incentivized, remove those that aren't in the pool.
                var trapChestPool = TrapChests.Where(chest => itemLocationPool.Contains(chest));

                foreach (var chest in trapChestPool)
                {
                    // It seems unlikely that is possible, but just in case.
                    if (!notableTreasurePool.Any())
                    {
                        break;
                    }

                    // Pick a random treasure and place it.
                    var treasure = notableTreasurePool.SpliceRandom(rng);
                    placedItems.Add(NewItemPlacement(chest, treasure));

                    // Since it was placed, remove both the item and location from the remaining pool.
                    treasurePool.Remove(treasure);
                    itemLocationPool.Remove(chest);
                }

                // This should still be true at the end, so make sure it is
                Debug.Assert(treasurePool.Count() == itemLocationPool.Count());
            }

            treasurePool.Shuffle(rng);
            itemLocationPool.Shuffle(rng);

            var leftovers = treasurePool.Zip(itemLocationPool, (treasure, location) => NewItemPlacement(location, treasure));

            placedItems.AddRange(leftovers);
            return(placedItems);
        }
예제 #3
0
        private ItemShopSlot ShuffleShopType(ShopType shopType, ushort[] pointers, MT19337 rng, bool randomize = false, IEnumerable <Item> excludeItemsFromRandomShops = null, WorldWealthMode wealth = WorldWealthMode.Normal)
        {
            var shops = GetShops(shopType, pointers);

            bool shopsBlocked;

            List <byte>[] newShops;
            do
            {
                shopsBlocked = false;
                newShops     = new List <byte> [ShopSectionSize];

                var allEntries = shops.SelectMany(list => list).ToList();
                allEntries.Shuffle(rng);

                int entry = 0;
                for (int i = 0; i < ShopSectionSize; i++)
                {
                    newShops[i] = new List <byte>();
                    if (pointers[(int)shopType + i] != ShopNullPointer)
                    {
                        newShops[i].Add(allEntries[entry++]);
                    }
                }

                while (entry < allEntries.Count)
                {
                    var eligibleShops = new List <int>();
                    for (int i = 0; i < newShops.Length; i++)
                    {
                        if (newShops[i].Count > 0 && newShops[i].Count < 5 && !newShops[i].Contains(allEntries[entry]))
                        {
                            eligibleShops.Add(i);
                        }
                    }

                    // We might be unable to place an item in any shop because they're all full, or they already have that item.  Start over.
                    if (eligibleShops.Count == 0)
                    {
                        shopsBlocked = true;
                        break;
                    }

                    int shopIndex = eligibleShops[rng.Between(0, eligibleShops.Count - 1)];
                    newShops[shopIndex].Add(allEntries[entry++]);
                }
            } while (shopsBlocked);

            if (randomize)
            {
                if (shopType == ShopType.Weapon || shopType == ShopType.Armor)
                {
                    // Shuffle up a byte array of random weapons or armor and assign them in place of the existing items.
                    var baseIndex = shopType == ShopType.Armor ? Item.Cloth : Item.WoodenNunchucks;
                    var indeces   = Enumerable.Range((int)baseIndex, 40).Select(i => (Item)i).ToList();
                    foreach (var exclusion in excludeItemsFromRandomShops ?? new List <Item>())
                    {
                        indeces.Remove(exclusion);
                    }

                    ItemGenerator generator = new ItemGenerator(indeces, wealth);
                    for (int i = 0; i < newShops.Length; i++)
                    {
                        newShops[i] = newShops[i].Select(x => (byte)generator.SpliceItem(rng)).ToList();
                    }
                }
            }
            // Zero-terminate the new shops.
            foreach (var newShop in newShops)
            {
                newShop.Add(0);
            }

            ItemShopSlot result  = null;
            var          pointer = pointers[(int)shopType];

            for (int i = 0; i < ShopSectionSize; i++)
            {
                if (newShops[i].Count > 1)
                {
                    var bottle =
                        newShops[i]
                        .Select((item, index) => new { item, index })
                        .FirstOrDefault(x => ((Item)x.item) == Item.Bottle);
                    if (bottle != null)
                    {
                        var location = ShopMapLocationsByIndex[i];
                        result = new ItemShopSlot(ShopPointerBase + pointer + bottle.index,
                                                  $"{Enum.GetName(typeof(MapLocation), location)}Shop{bottle.index + 1}",
                                                  location,
                                                  Item.Bottle);
                    }
                    Put(ShopPointerBase + pointer, newShops[i].ToArray());

                    pointers[(int)shopType + i] = pointer;
                    pointer += (ushort)newShops[i].Count;
                }
            }
            return(result);
        }
예제 #4
0
        public static List <IRewardSource> PlaceSaneItems(MT19337 rng,
                                                          IItemPlacementFlags flags,
                                                          IncentiveData incentivesData,
                                                          List <Item> allTreasures,
                                                          ItemShopSlot caravanItemLocation,
                                                          OverworldMap overworldMap)
        {
            Dictionary <MapLocation, Tuple <List <MapChange>, AccessRequirement> > fullLocationRequirements = overworldMap.FullLocationRequirements;
            Dictionary <MapLocation, OverworldTeleportIndex> overridenOverworld = overworldMap.OverriddenOverworldLocations;

            long sanityCounter = 0;
            List <IRewardSource> placedItems;

            var canoeObsoletesBridge    = flags.MapCanalBridge && flags.MapConeriaDwarves;
            var canoeObsoletesShip      = flags.MapCanalBridge ? (flags.MapConeriaDwarves || flags.MapVolcanoIceRiver) : (flags.MapConeriaDwarves && flags.MapVolcanoIceRiver);
            var incentiveLocationPool   = incentivesData.IncentiveLocations.ToList();
            var incentivePool           = incentivesData.IncentiveItems.Where(x => allTreasures.Contains(x)).ToList();
            var forcedItems             = incentivesData.ForcedItemPlacements.ToList();
            var keyLocations            = incentivesData.KeyLocations.ToList();
            var canoeLocations          = incentivesData.CanoeLocations.ToList();
            var bridgeLocations         = incentivesData.BridgeLocations.ToList();
            var shipLocations           = incentivesData.ShipLocations.ToList();
            var itemLocationPool        = incentivesData.AllValidItemLocations.ToList();
            var startingPotentialAccess = overworldMap.StartingPotentialAccess;
            var startingMapLocations    = AccessibleMapLocations(startingPotentialAccess, MapChange.None, fullLocationRequirements);
            var earlyMapLocations       = AccessibleMapLocations(startingPotentialAccess | AccessRequirement.Crystal, MapChange.Bridge, fullLocationRequirements);

            var unincentivizedQuestItems =
                ItemLists.AllQuestItems
                .Where(x => !incentivePool.Contains(x) &&
                       allTreasures.Contains(x) &&
                       !forcedItems.Any(y => y.Item == x))
                .ToList();

            var treasurePool = allTreasures.ToList();

            foreach (var incentive in incentivePool)
            {
                treasurePool.Remove(incentive);
            }
            foreach (var placement in forcedItems)
            {
                treasurePool.Remove(placement.Item);
            }
            foreach (var questItem in unincentivizedQuestItems)
            {
                treasurePool.Remove(questItem);
            }

            var itemShopItem = Item.Bottle;

            while (treasurePool.Remove(Item.Shard))
            {
                unincentivizedQuestItems.Add(Item.Shard);
            }

            // Cache the final unincentivized pool so we can reset it each iteration of the loop.
            IReadOnlyCollection <Item> cachedTreasurePool = new ReadOnlyCollection <Item>(treasurePool.ToList());

            do
            {
                sanityCounter++;
                if (sanityCounter > 500)
                {
                    throw new InsaneException("Sanity Counter exceeds 500 iterations!");
                }
                // 1. (Re)Initialize lists inside of loop
                placedItems = forcedItems.ToList();
                var incentives    = incentivePool.ToList();
                var nonincentives = unincentivizedQuestItems.ToList();
                treasurePool = cachedTreasurePool.ToList();
                incentives.Shuffle(rng);
                nonincentives.Shuffle(rng);

                if (flags.NPCItems)
                {
                    // 2. Place caravan item first because among incentive locations it has the smallest set of possible items
                    var validShopIncentives = incentives
                                              .Where(x => x > Item.None && x <= Item.Soft)
                                              .ToList();
                    if (validShopIncentives.Any() && incentiveLocationPool.Any(x => x.Address == ItemLocations.CaravanItemShop1.Address))
                    {
                        itemShopItem = validShopIncentives.PickRandom(rng);
                        incentives.Remove(itemShopItem);
                    }
                    else
                    {
                        itemShopItem = treasurePool.Concat(nonincentives).Where(x => x > Item.None && x <= Item.Soft && x != Item.Shard).ToList().PickRandom(rng);
                        if (!nonincentives.Remove(itemShopItem))
                        {
                            treasurePool.Remove(itemShopItem);
                        }
                    }
                    placedItems.Add(new ItemShopSlot(caravanItemLocation, itemShopItem));

                    // 3. Place key and canoe next because among incentive locations these have the greatest initial impact
                    if (incentives.Remove(Item.Key) || nonincentives.Remove(Item.Key))
                    {
                        placedItems.Add(NewItemPlacement(keyLocations.PickRandom(rng), Item.Key));
                    }
                    if (incentives.Remove(Item.Canoe) || nonincentives.Remove(Item.Canoe))
                    {
                        placedItems.Add(NewItemPlacement(canoeLocations.Where(x => !placedItems.Any(y => y.Address == x.Address)).ToList().PickRandom(rng), Item.Canoe));
                    }

                    var startingCanoeAvailable =
                        placedItems.Any(x => x.Item == Item.Canoe && startingMapLocations.Contains(x.MapLocation) &&
                                        startingMapLocations.Contains((x as MapObject)?.SecondLocation ?? MapLocation.StartingLocation));
                    var earlyCanoeAvailable =
                        placedItems.Any(x => x.Item == Item.Canoe && earlyMapLocations.Contains(x.MapLocation) &&
                                        earlyMapLocations.Contains((x as MapObject)?.SecondLocation ?? MapLocation.StartingLocation));
                    var earlyKeyAvailable =
                        placedItems.Any(x => x.Item == Item.Key && earlyMapLocations.Contains(x.MapLocation) &&
                                        earlyMapLocations.Contains((x as MapObject)?.SecondLocation ?? MapLocation.StartingLocation));

                    // 4. Place Bridge and Ship next since the valid location lists are so small, unless canoe is available and map edits are applied
                    if (!earlyCanoeAvailable || !canoeObsoletesShip)
                    {
                        var remainingShipLocations =
                            shipLocations
                            .Where(x => !placedItems.Any(y => y.Address == x.Address) &&
                                   (earlyKeyAvailable || !x.AccessRequirement.HasFlag(AccessRequirement.Key)))
                            .ToList();
                        if (!remainingShipLocations.Any())
                        {
                            continue;
                        }
                        if (incentives.Remove(Item.Ship) && remainingShipLocations.Any(x => incentiveLocationPool.Any(y => y.Address == x.Address)))
                        {
                            remainingShipLocations =
                                remainingShipLocations
                                .Where(x => incentiveLocationPool.Any(y => y.Address == x.Address))
                                .ToList();
                        }
                        nonincentives.Remove(Item.Ship);
                        placedItems.Add(NewItemPlacement(remainingShipLocations.PickRandom(rng), Item.Ship));
                    }

                    var startingShipAvailable =
                        placedItems.Any(x => x.Item == Item.Ship && startingMapLocations.Contains(x.MapLocation) &&
                                        startingMapLocations.Contains((x as MapObject)?.SecondLocation ?? MapLocation.StartingLocation));

                    if (!(startingCanoeAvailable && canoeObsoletesBridge) && !startingShipAvailable)
                    {
                        var startingKeyAvailable =
                            earlyKeyAvailable && placedItems.Any(x => x.Item == Item.Key && startingMapLocations.Contains(x.MapLocation) &&
                                                                 startingMapLocations.Contains((x as MapObject)?.SecondLocation ?? MapLocation.StartingLocation));

                        var remainingBridgeLocations =
                            bridgeLocations
                            .Where(x => !placedItems.Any(y => y.Address == x.Address) &&
                                   (startingKeyAvailable || !x.AccessRequirement.HasFlag(AccessRequirement.Key)))
                            .ToList();
                        if (!remainingBridgeLocations.Any())
                        {
                            continue;
                        }
                        if (incentives.Remove(Item.Bridge) && remainingBridgeLocations.Any(x => incentiveLocationPool.Any(y => y.Address == x.Address)))
                        {
                            remainingBridgeLocations =
                                remainingBridgeLocations
                                .Where(x => incentiveLocationPool.Any(y => y.Address == x.Address))
                                .ToList();
                        }
                        nonincentives.Remove(Item.Bridge);
                        placedItems.Add(NewItemPlacement(remainingBridgeLocations.PickRandom(rng), Item.Bridge));
                    }
                }

                // 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 and unincentivized quest items in any other chest before ToFR
                var leftoverItems = incentives.Concat(nonincentives).ToList();
                leftoverItems.Shuffle(rng);
                var leftoverItemLocations =
                    itemLocationPool
                    .Where(x => !ItemLocations.ToFR.Any(y => y.Address == x.Address) &&
                           !x.IsUnused && !placedItems.Any(y => y.Address == x.Address))
                    .ToList();
                foreach (var leftoverItem in leftoverItems)
                {
                    placedItems.Add(NewItemPlacement(leftoverItemLocations.SpliceRandom(rng), leftoverItem));
                }

                // 7. Check sanity and loop if needed
            } while (!CheckSanity(placedItems, fullLocationRequirements, flags));

            if (Debugger.IsAttached)
            {
                Console.WriteLine($"ItemPlacement::PlaceSaneItems required {sanityCounter} iterations.");
                Console.WriteLine("");
                Console.WriteLine("Item     Entrance  ->  Floor  ->  Source                   Requirements");
                Console.WriteLine("------------------------------------------------------------------------------------------");

                var sorted = placedItems.Where(item => item.Item != Item.Shard).ToList();
                sorted.Sort((IRewardSource lhs, IRewardSource rhs) => lhs.Item.ToString().CompareTo(rhs.Item.ToString()));
                sorted.ForEach(item =>
                {
                    if (fullLocationRequirements.TryGetValue(item.MapLocation, out var flr))
                    {
                        var overworldLocation = item.MapLocation.ToString();
                        if (overridenOverworld != null && overridenOverworld.TryGetValue(item.MapLocation, out var overriden))
                        {
                            overworldLocation = overriden.ToString();
                        }

                        var itemStr = item.Item.ToString().PadRight(9);
                        var locStr  = $"{overworldLocation} -> {item.MapLocation} -> {item.Name} ".PadRight(50);
                        var changes = $"{String.Join(" | ", flr.Item1.Select(mapChange => mapChange.ToString()).ToArray())}";
                        var reqs    = flr.Item2.ToString().CompareTo("None") == 0 ? "" : $" AND {flr.Item2.ToString()}";
                        Console.WriteLine($"{itemStr}{locStr}{changes}{reqs}");
                    }
                });
            }

            // 8. Place all remaining unincentivized treasures or incentivized non-quest items that weren't placed
            itemLocationPool = itemLocationPool.Where(x => !x.IsUnused && !placedItems.Any(y => y.Address == x.Address)).ToList();
            foreach (var placedItem in placedItems)
            {
                incentivePool.Remove(placedItem.Item);
            }
            treasurePool.AddRange(incentivePool);

            Debug.Assert(treasurePool.Count() == itemLocationPool.Count());
            treasurePool.Shuffle(rng);
            itemLocationPool.Shuffle(rng);

            if (flags.RandomLoot)
            {
                // We want to leave out anything incentivized (and thus already placed), but
                // add all the other stuff that you can't find in vanilla.
                var randomTreasure = treasurePool.ToList();
                randomTreasure.AddRange(ItemLists.CommonWeaponTier);
                randomTreasure.AddRange(ItemLists.CommonArmorTier);
                randomTreasure.Add(Item.CatClaw);
                randomTreasure.Add(Item.SteelArmor);

                ItemGenerator generator = new ItemGenerator(randomTreasure, flags.WorldWealth);
                treasurePool = treasurePool.Select(treasure => generator.GetItem(rng)).ToList();
            }

            var leftovers = treasurePool.Zip(itemLocationPool, (treasure, location) => NewItemPlacement(location, treasure));

            placedItems.AddRange(leftovers);
            return(placedItems);
        }