public static void Work(IItemPlacementFlags flags, List <Item> treasurePool, MT19337 rng) { int count = treasurePool.Count; var consumableChestSet = ConsumableChestSets[flags.MoreConsumableChests]; var extConsumableChestSet = flags.ExtConsumableSet != ExtConsumableSet.None ? ExtConsumableChestSets[flags.ExtConsumableChests] : ExtConsumableChestSets[ExtConsumableChestSet.None]; if (flags.MoreConsumableChests == ConsumableChestSet.Random || flags.MoreConsumableChests == ConsumableChestSet.RandomLow) { consumableChestSet = ( Tents : rng.Between(0, consumableChestSet.Tents), Cabins : rng.Between(0, consumableChestSet.Cabins), Houses : rng.Between(0, consumableChestSet.Houses), Heals : rng.Between(0, consumableChestSet.Heals), Pures : rng.Between(0, consumableChestSet.Pures), Softs : rng.Between(0, consumableChestSet.Softs) ); } if (flags.ExtConsumableSet != ExtConsumableSet.None && (flags.ExtConsumableChests == ExtConsumableChestSet.Random || flags.ExtConsumableChests == ExtConsumableChestSet.RandomLow)) { extConsumableChestSet = ( WoodenNunchucks : rng.Between(0, extConsumableChestSet.WoodenNunchucks), SmallKnives : rng.Between(0, extConsumableChestSet.SmallKnives), WoodenRods : rng.Between(0, extConsumableChestSet.WoodenRods), Rapiers : rng.Between(0, extConsumableChestSet.Rapiers) ); } int requestedchests = consumableChestSet.Tents + consumableChestSet.Cabins + consumableChestSet.Houses + consumableChestSet.Heals + consumableChestSet.Pures + consumableChestSet.Softs + extConsumableChestSet.WoodenNunchucks + extConsumableChestSet.SmallKnives + extConsumableChestSet.WoodenRods + extConsumableChestSet.Rapiers; int removedchests = 0; RemoveConsumableChests(flags, treasurePool, ref removedchests); RemoveGoldChests(treasurePool, requestedchests, ref removedchests); if (flags.ExtConsumableSet != ExtConsumableSet.None) { AddConsumableChests(treasurePool, extConsumableChestSet.WoodenNunchucks, Item.WoodenNunchucks, ref removedchests); AddConsumableChests(treasurePool, extConsumableChestSet.SmallKnives, Item.SmallKnife, ref removedchests); AddConsumableChests(treasurePool, extConsumableChestSet.WoodenRods, Item.WoodenRod, ref removedchests); AddConsumableChests(treasurePool, extConsumableChestSet.Rapiers, Item.Rapier, ref removedchests); } AddConsumableChests(treasurePool, consumableChestSet.Tents, Item.Tent, ref removedchests); AddConsumableChests(treasurePool, consumableChestSet.Cabins, Item.Cabin, ref removedchests); AddConsumableChests(treasurePool, consumableChestSet.Houses, Item.House, ref removedchests); AddConsumableChests(treasurePool, consumableChestSet.Heals, Item.Heal, ref removedchests); AddConsumableChests(treasurePool, consumableChestSet.Pures, Item.Pure, ref removedchests); AddConsumableChests(treasurePool, consumableChestSet.Softs, Item.Soft, ref removedchests); AddGoldChests(treasurePool, removedchests); if (treasurePool.Count != count) { throw new Exception("Sorry, I f****d it up!"); } }
public static readonly List <int> UsedTreasureIndices = Enumerable.Range(0, 256).Except(UnusedTreasureIndices).ToList(); // This maps a compacted list back to the game's array, skipping the unused slots. public List <IRewardSource> ShuffleTreasures(MT19337 rng, IItemPlacementFlags flags, IncentiveData incentivesData, ItemShopSlot caravanItemLocation, Dictionary <MapLocation, List <MapChange> > mapLocationRequirements) { var vanillaNPCs = !flags.NPCItems && !flags.NPCFetchItems; if (!vanillaNPCs) { EnableBridgeShipCanalAnywhere(); EnableNPCsGiveAnyItem(); // This extends Vampire's routine to set a flag for Sarda, but it also clobers Sarda's routine if (!flags.EarlySarda) { Put(0x393E1, Blob.FromHex("207F90A51160")); } } var treasureBlob = Get(TreasureOffset, TreasureSize * TreasureCount); var treasurePool = UsedTreasureIndices.Select(x => (Item)treasureBlob[x]) .Concat(ItemLists.AllNonTreasureChestItems).ToList(); if (flags.ShardHunt) { treasurePool = treasurePool.Select(ShardHuntTreasureSelector).ToList(); int shardsAdded = treasurePool.Count(item => item == Item.Shard); Debug.Assert(shardsAdded == TotalOrbsToInsert); } var placedItems = ItemPlacement.PlaceSaneItems(rng, flags, incentivesData, treasurePool, caravanItemLocation, mapLocationRequirements); if (flags.FreeBridge) { placedItems = placedItems.Select(x => x.Item != Item.Bridge ? x : ItemPlacement.NewItemPlacement(x, ReplacementItem)).ToList(); } if (flags.FreeAirship) { placedItems = placedItems.Select(x => x.Item != Item.Floater ? x : ItemPlacement.NewItemPlacement(x, ReplacementItem)).ToList(); } // Output the results to the ROM foreach (var item in placedItems.Where(x => !x.IsUnused && x.Address < 0x80000 && (!vanillaNPCs || x is TreasureChest))) { //Debug.WriteLine(item.SpoilerText); item.Put(this); } MoveShipToRewardSource(placedItems.Find(reward => reward.Item == Item.Ship)); return(placedItems); }
public static readonly List <int> UsedTreasureIndices = Enumerable.Range(0, 256).Except(UnusedTreasureIndices).ToList(); // This maps a compacted list back to the game's array, skipping the unused slots. public List <IRewardSource> ShuffleTreasures(MT19337 rng, IItemPlacementFlags flags, IncentiveData incentivesData, ItemShopSlot caravanItemLocation, OverworldMap overworldMap, TeleportShuffle teleporters) { Dictionary <MapLocation, Tuple <List <MapChange>, AccessRequirement> > fullFloorRequirements = overworldMap.FullLocationRequirements; Dictionary <MapLocation, OverworldTeleportIndex> overridenOverworld = overworldMap.OverriddenOverworldLocations; var vanillaNPCs = !(flags.NPCItems ?? false) && !(flags.NPCFetchItems ?? false); if (!vanillaNPCs) { EnableBridgeShipCanalAnywhere(); EnableNPCsGiveAnyItem(); // This extends Vampire's routine to set a flag for Sarda, but it also clobers Sarda's routine if (!(bool)flags.EarlySarda) { Put(0x393E1, Blob.FromHex("207F90A51160")); } } var treasureBlob = Get(TreasureOffset, TreasureSize * TreasureCount); var treasurePool = UsedTreasureIndices.Select(x => (Item)treasureBlob[x]) .Concat(ItemLists.AllNonTreasureChestItems).ToList(); if (flags.ShardHunt) { treasurePool = treasurePool.Select(ShardHuntTreasureSelector).ToList(); int shardsAdded = treasurePool.Count(item => item == Item.Shard); Debug.Assert(shardsAdded == TotalOrbsToInsert); } ItemPlacement placement = ItemPlacement.Create(flags, incentivesData, treasurePool, caravanItemLocation, overworldMap); var placedItems = placement.PlaceSaneItems(rng); // Output the results to the ROM foreach (var item in placedItems.Where(x => !x.IsUnused && x.Address < 0x80000 && (!vanillaNPCs || x is TreasureChest))) { //Debug.WriteLine(item.SpoilerText); item.Put(this); } // Move the ship someplace closer to where it really ends up. if (!(flags.FreeShip ?? false)) { MapLocation shipLocation = placedItems.Find(reward => reward.Item == Item.Ship).MapLocation; if (overridenOverworld != null && overridenOverworld.TryGetValue(shipLocation, out var overworldIndex)) { shipLocation = teleporters.OverworldMapLocations.TryGetValue(overworldIndex, out var vanillaShipLocation) ? vanillaShipLocation : shipLocation; } MoveShipToRewardSource(shipLocation); } return(placedItems); }
public static ItemPlacement Create(IItemPlacementFlags flags, IncentiveData incentivesData, List <Item> allTreasures, ItemShopSlot caravanItemLocation, OverworldMap overworldMap) { ItemPlacement placement; placement = new GuidedItemPlacement(); placement._flags = flags; placement._incentivesData = incentivesData; placement._allTreasures = allTreasures; placement._caravanItemLocation = caravanItemLocation; placement._overworldMap = overworldMap; return(placement); }
public static ItemPlacement Create(IItemPlacementFlags flags, IncentiveData incentivesData, List <Item> allTreasures, ItemShopSlot caravanItemLocation, OverworldMap overworldMap, ISanityChecker checker) { ItemPlacement placement; placement = flags.PredictivePlacement && checker is SanityCheckerV2 ? new PredictivePlacement() : new GuidedItemPlacement(); placement._flags = flags; placement._incentivesData = incentivesData; placement._allTreasures = allTreasures; placement._caravanItemLocation = caravanItemLocation; placement._overworldMap = overworldMap; placement._checker = checker; return(placement); }
public static void Work(IItemPlacementFlags flags, List <Item> treasurePool, MT19337 rng) { //var x = treasurePool.GroupBy(i => i).Select(g => (g.Key, g.Count())).OrderBy(g => (int)g.Key).ToList(); if (flags.MoreConsumableChests == ConsumableChestSet.Vanilla) { return; } int count = treasurePool.Count; var consumableChestSet = ConsumableChestSets[flags.MoreConsumableChests]; if (flags.MoreConsumableChests == ConsumableChestSet.Random || flags.MoreConsumableChests == ConsumableChestSet.RandomLow) { consumableChestSet = ( Tents : rng.Between(0, consumableChestSet.Tents), Cabins : rng.Between(0, consumableChestSet.Cabins), Houses : rng.Between(0, consumableChestSet.Houses), Heals : rng.Between(0, consumableChestSet.Heals), Pures : rng.Between(0, consumableChestSet.Pures), Softs : rng.Between(0, consumableChestSet.Softs) ); } int requestedchests = consumableChestSet.Tents + consumableChestSet.Cabins + consumableChestSet.Houses + consumableChestSet.Heals + consumableChestSet.Pures + consumableChestSet.Softs; int removedchests = 0; RemoveConsumableChests(treasurePool, ref removedchests); RemoveGoldChests(treasurePool, requestedchests, ref removedchests); AddConsumableChests(treasurePool, consumableChestSet.Tents, Item.Tent, ref removedchests); AddConsumableChests(treasurePool, consumableChestSet.Cabins, Item.Cabin, ref removedchests); AddConsumableChests(treasurePool, consumableChestSet.Houses, Item.House, ref removedchests); AddConsumableChests(treasurePool, consumableChestSet.Heals, Item.Heal, ref removedchests); AddConsumableChests(treasurePool, consumableChestSet.Pures, Item.Pure, ref removedchests); AddConsumableChests(treasurePool, consumableChestSet.Softs, Item.Soft, ref removedchests); AddGoldChests(treasurePool, removedchests); if (treasurePool.Count != count) { throw new Exception("Sorry, I f****d it up!"); } }
private static void RemoveConsumableChests(IItemPlacementFlags flags, List <Item> treasurePool, ref int removedchests) { var consumableChests = treasurePool.Where(i => i == Item.Tent || i == Item.Cabin || i == Item.House || i == Item.Heal || i == Item.Pure || i == Item.Soft).ToList(); if (flags.ExtConsumableSet != ExtConsumableSet.None) { consumableChests.Add(Item.WoodenNunchucks); consumableChests.Add(Item.SmallKnife); consumableChests.Add(Item.WoodenRod); consumableChests.Add(Item.Rapier); } foreach (var item in consumableChests) { if (treasurePool.Remove(item)) { removedchests++; } } }
public static ItemPlacement Create(IItemPlacementFlags flags, IncentiveData incentivesData, List <Item> allTreasures, ItemShopSlot caravanItemLocation, OverworldMap overworldMap) { ItemPlacement placement; if (flags.AllowObsoleteVehicles) { placement = new RandomItemPlacement(); } else { placement = new GuidedItemPlacement(); }; placement._flags = flags; placement._incentivesData = incentivesData; placement._allTreasures = allTreasures; placement._caravanItemLocation = caravanItemLocation; placement._overworldMap = overworldMap; return(placement); }
public static List <IRewardSource> PlaceSaneItems(MT19337 rng, IItemPlacementFlags flags, IncentiveData incentivesData, List <Item> allTreasures, ItemShopSlot caravanItemLocation, Dictionary <MapLocation, List <MapChange> > mapLocationRequirements) { 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 startingMapLocations = mapLocationRequirements.Where(x => x.Value.Any(y => y == MapChange.None)).Select(x => x.Key); var earlyMapLocations = mapLocationRequirements.Where(x => x.Value.Any(y => MapChange.Bridge.HasFlag(y))).Select(x => x.Key); 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); } do { sanityCounter++; if (sanityCounter > 10000) { throw new InvalidOperationException("Invalid flag set"); } // 1. (Re)Initialize lists inside of loop placedItems = forcedItems.ToList(); var incentives = incentivePool.ToList(); var nonincentives = unincentivizedQuestItems.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).ToList().PickRandom(rng); nonincentives.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)); var earlyCanoeAvailable = placedItems.Any(x => x.Item == Item.Canoe && earlyMapLocations.Contains(x.MapLocation)); var earlyKeyAvailable = placedItems.Any(x => x.Item == Item.Key && earlyMapLocations.Contains(x.MapLocation)); // 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)); if (!(startingCanoeAvailable && canoeObsoletesBridge) && !startingShipAvailable) { var startingKeyAvailable = earlyKeyAvailable && placedItems.Any(x => x.Item == Item.Key && startingMapLocations.Contains(x.MapLocation)); 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.Remove(itemShopItem); 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, mapLocationRequirements, flags)); // 8. Place all remaining unincentivized treasures or incentivized non-quest items that weren't placed var i = 0; itemLocationPool = itemLocationPool .Where(x => !x.IsUnused && !placedItems.Any(y => y.Address == x.Address)) .ToList(); foreach (var placedItem in placedItems) { incentivePool.Remove(placedItem.Item); } foreach (var unplacedIncentive in incentivePool) { treasurePool.Add(unplacedIncentive); } treasurePool.Shuffle(rng); itemLocationPool.Shuffle(rng); foreach (var remainingTreasure in itemLocationPool) { placedItems.Add(NewItemPlacement(remainingTreasure, treasurePool[i])); i++; } //Debug.WriteLine($"Sanity Check Fails: {sanityCounter}"); return(placedItems); }
public static void RunStats(MT19337 rng, IItemPlacementFlags flags, IncentiveData incentivesData, ItemShopSlot caravanItemLocation, OverworldMap overworldMap) { Dictionary <MapLocation, Tuple <List <MapChange>, AccessRequirement> > fullLocationRequirements = overworldMap.FullLocationRequirements; var placedItems = new List <IRewardSource>(); var treasurePool = ItemLocations.AllTreasures.Where(x => !x.IsUnused).Select(x => x.Item) .Concat(ItemLists.AllNonTreasureChestItems).ToList(); var requirementChecks = ItemLists.AllQuestItems.ToDictionary(x => x, x => 0); requirementChecks[Item.Ribbon] = 0; var requirementsToCheck = new List <Item> { Item.Crown, Item.Crystal, Item.Herb, Item.Tnt, Item.Adamant, Item.Slab, Item.Ruby, Item.Bottle, Item.Floater, Item.Ship, Item.Bridge, Item.Canal }; const int maxIterations = 255; var forcedIceCount = 0; var itemPlacementStats = ItemLists.AllQuestItems.ToDictionary(x => x, x => new List <int>()); itemPlacementStats[Item.Ribbon] = new List <int>(); var itemPlacementZones = ItemLists.AllQuestItems.ToDictionary(x => x, x => new List <string>()); itemPlacementZones[Item.Ribbon] = new List <string>(); long iterations = 0; var timeElapsed = new Stopwatch(); while (iterations < maxIterations) { iterations++; // When Enemy Status Shuffle is turned on Coneria is reduced to 11% chance with other shops spliting the remaining 89% var shopTownSelected = ItemShops.PickRandom(rng); var itemShopItem = new ItemShopSlot(ItemLocations.CaravanItemShop1.Address, $"{Enum.GetName(typeof(MapLocation), shopTownSelected)}Shop", shopTownSelected, Item.Bottle); timeElapsed.Start(); placedItems = ItemPlacement.PlaceSaneItems(rng, flags, incentivesData, treasurePool, itemShopItem, overworldMap); timeElapsed.Stop(); var outputIndexes = placedItems.ToLookup(x => x.Item, x => x); foreach (Item item in itemPlacementStats.Keys) { itemPlacementStats[item].AddRange(outputIndexes[item].Select(x => x.Address).ToList()); } var outputZones = placedItems .ToLookup(x => x.Item, x => Enum.GetName(typeof(MapLocation), x.MapLocation)); foreach (Item item in itemPlacementZones.Keys) { if (!outputZones[item].Any()) { continue; } itemPlacementZones[item].AddRange(outputZones[item]); } var matoyaShip = placedItems.Any(x => x.Address == ItemLocations.Matoya.Address && x.Item == Item.Ship); var crystalIceCave = placedItems.Any(x => x.Item == Item.Crystal && x.Address >= ItemLocations.IceCave1.Address && x.Address <= ItemLocations.IceCaveMajor.Address); var keyIceCave = placedItems.Any(x => x.Item == Item.Key && x.Address >= ItemLocations.IceCave1.Address && x.Address <= ItemLocations.IceCaveMajor.Address); var keyLockedCrystal = placedItems.Any(x => x.Item == Item.Crystal && x.AccessRequirement.HasFlag(AccessRequirement.Key)); var keyLockedShip = placedItems.Any(x => x.Item == Item.Ship && x.AccessRequirement.HasFlag(AccessRequirement.Key)); if ((keyLockedShip && keyIceCave) || (matoyaShip && crystalIceCave) || (matoyaShip && keyLockedCrystal && keyIceCave)) { forcedIceCount++; } foreach (Item item in requirementsToCheck) { if (!ItemPlacement.CheckSanity(placedItems.Where(x => x.Item != item).ToList(), fullLocationRequirements, new Flags { OnlyRequireGameIsBeatable = true })) { requirementChecks[item]++; } } } if (iterations > 10) { Debug.WriteLine(PrintStats(maxIterations, itemPlacementStats, itemPlacementZones, requirementChecks)); Debug.WriteLine($"Forced Early Ice Cave for Ship: {forcedIceCount} out of {maxIterations}"); Debug.WriteLine($"Time per iteration: {1.0 * timeElapsed.ElapsedMilliseconds / iterations}"); } }
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); var leftovers = treasurePool.Zip(itemLocationPool, (treasure, location) => NewItemPlacement(location, treasure)); placedItems.AddRange(leftovers); return(placedItems); }