public LocationData FindItems(GameData game)
        {
            SortedDictionary <int, List <EntityId> > usedItemLots = FindItemLots(game);

            PARAM shops     = game.Param("ShopLineupParam");
            PARAM itemLots  = game.Param("ItemLotParam");
            PARAM materials = game.Param("EquipMtrlSetParam");

            LocationData data = new LocationData();

            // Run through all item lots in the game in order, extract all the data.
            LocationKey prevLocation = null;

            foreach (KeyValuePair <int, List <EntityId> > entry in usedItemLots)
            {
                int itemLot = entry.Key;
                if (prevLocation != null)
                {
                    if (!crowLots.Contains(prevLocation.ID))
                    {
                        prevLocation.MaxSlots = itemLot - prevLocation.ID - 1;
                        if (prevLocation.MaxSlots < 1)
                        {
                            Warn($"Overlapping slots at {itemLot}");
                        }
                    }
                    prevLocation = null;
                }
                List <EntityId> entities = entry.Value;
                string          locs     = String.Join(", ", entities.Select(e => game.EntityName(e) + $" {e}"));
                if (itemLots[itemLot] == null)
                {
                    string text = game.LotName(itemLot);
                    // These are fine - no-ops to game
                    // Console.WriteLine($"MISSING connected item lot!! {itemLot}: {text} @ {locs}");
                    continue;
                }
                LocationKey baseLocation = null;
                while (itemLots[itemLot] != null)
                {
                    bool   isBase = itemLot == entry.Key;
                    string text   = game.LotName(itemLot);

                    PARAM.Row row         = itemLots[itemLot];
                    int       eventFlag   = (int)row["getItemFlagId"].Value;
                    int       totalPoints = 0;
                    for (int i = 1; i <= 8; i++)
                    {
                        totalPoints += (short)row[$"LotItemBasePoint0{i}"].Value;
                    }
                    for (int i = 1; i <= 8; i++)
                    {
                        int id = (int)row[$"ItemLotId{i}"].Value;
                        if (id != 0)
                        {
                            uint    type     = (uint)row[$"LotItemCategory0{i}"].Value;
                            int     points   = (short)row[$"LotItemBasePoint0{i}"].Value;
                            int     quantity = (byte)row[$"LotItemNum{i}"].Value;
                            ItemKey item     = new ItemKey(LocationData.LotTypes[type], id);
                            string  itemText = $"{itemLot}[{locs}]";
                            // Check out script about CC, btw
                            List <string> info = new List <string>();
                            if (quantity > 1)
                            {
                                info.Add($"{quantity}");
                            }
                            if (points != totalPoints)
                            {
                                info.Add($"{100.0 * points / totalPoints}%");
                            }
                            if (info.Count() > 0)
                            {
                                itemText += $" ({string.Join(",", info)})";
                            }
                            if (quantity <= 0 && logUnused)
                            {
                                Console.WriteLine($"There is 0! of {itemText}");
                            }
                            ItemScope scope;
                            if (eventFlag != -1)
                            {
                                if (equivalentEvents.ContainsKey(eventFlag))
                                {
                                    eventFlag = equivalentEvents[eventFlag];
                                }
                                scope = new ItemScope(ScopeType.EVENT, eventFlag);
                            }
                            else
                            {
                                // One time drops that directly award, that aren't covered by event flags. Mostly crystal lizards.
                                if (entities.Count() == 1 && entityItemLots.ContainsKey(entities[0].EventEntityID) && entityItemLots[entities[0].EventEntityID] == entry.Key)
                                {
                                    scope = new ItemScope(ScopeType.ENTITY, entities[0].EventEntityID);
                                }
                                // Non-respawning enemies with drops which can be missed. These are reused between different entities, so can drop multiple times.
                                else if (entities.All(e => nonRespawningEntities.Contains(e.EventEntityID)))
                                {
                                    scope = new ItemScope(ScopeType.ENTITY, entities.Select(e => e.EventEntityID).Min());
                                }
                                else
                                {
                                    int model = entities.Select(e => e.GetModelID()).Min();
                                    if (model == -1)
                                    {
                                        if (logUnused)
                                        {
                                            // Ideally this should not be randomized, but can't check here
                                            Console.WriteLine($"Infinite item {itemLot} with no event flags, entity flags, or models: {itemText}");
                                        }
                                        continue;
                                    }
                                    scope = new ItemScope(ScopeType.MODEL, model);
                                }
                            }
                            LocationKey location = new LocationKey(LocationType.LOT, itemLot, itemText, entities, quantity, baseLocation);
                            data.AddLocation(item, scope, location);
                            if (baseLocation == null)
                            {
                                baseLocation = location;
                            }
                        }
                    }
                    itemLot++;
                    if (crowLots.Contains(itemLot - 1))
                    {
                        break;
                    }
                }
                prevLocation = baseLocation;
            }
            foreach (PARAM.Row row in shops.Rows)
            {
                int    shopID   = (int)row.ID;
                string shopName = shopSplits[GetShopType(shopID)];
                if (shopName == null)
                {
                    continue;
                }
                int     qwc          = (int)row["qwcID"].Value;
                int     type         = (byte)row["equipType"].Value;
                int     id           = (int)row["EquipId"].Value;
                int     quantity     = (short)row["sellQuantity"].Value;
                int     eventFlag    = (int)row["EventFlag"].Value;
                int     material     = (int)row["mtrlId"].Value;
                string  quantityText = quantity == -1 ? "" : $" ({quantity})";
                string  qwcText      = qwc == -1 ? "" : $" {game.QwcName(qwc)}";
                ItemKey item         = new ItemKey((ItemType)type, id);
                string  text         = $"{shopName}{qwcText}{quantityText}";
                text = $"{shopID}[{text}]";
                LocationKey location = new LocationKey(LocationType.SHOP, shopID, text, new List <EntityId>(), quantity, null);
                ItemScope   scope;
                if (eventFlag != -1)
                {
                    if (equivalentEvents.ContainsKey(eventFlag))
                    {
                        eventFlag = equivalentEvents[eventFlag];
                    }
                    if (quantity <= 0)
                    {
                        Warn($"No quantity for event flag shop entry {text}");
                    }
                    ScopeType scopeType = ScopeType.EVENT;
                    if (restrictiveQwcs.Contains(qwc))
                    {
                        // In DS3, if item becomes unavailable at some point, that is because it returns in infinite form
                        scopeType = ScopeType.SHOP_INFINITE_EVENT;
                    }
                    scope = new ItemScope(scopeType, eventFlag);
                }
                else if (material != -1)
                {
                    int materialItem = (int)materials[material]["MaterialId01"].Value;
                    scope = new ItemScope(ScopeType.MATERIAL, materialItem);
                }
                else
                {
                    scope = new ItemScope(ScopeType.SHOP_INFINITE, -1);
                }
                data.AddLocation(item, scope, location);
            }
            // Merge infinite and finite shops. Mostly done via heuristic (when event and infinite both exist)
            ItemScope infiniteKey = new ItemScope(ScopeType.SHOP_INFINITE, -1);

            foreach (ItemLocations locations in data.Data.Values)
            {
                foreach (ItemLocation restrict in locations.Locations.Values.Where(loc => loc.Scope.Type == ScopeType.SHOP_INFINITE_EVENT).ToList())
                {
                    if (locations.Locations.ContainsKey(infiniteKey))
                    {
                        // Combine infinite shops into event
                        ItemLocation infinite = locations.Locations[infiniteKey];
                        restrict.Keys.AddRange(infinite.Keys);
                        locations.Locations.Remove(infiniteKey);
                    }
                    else
                    {
                        Warn($"No 1:1 match between event shops and infinite shops for {restrict}");
                        // No infinite shops, turn this into a regular event shop. (Doesn't happen in base DS3)
                        ItemLocation eventLoc = new ItemLocation(new ItemScope(ScopeType.EVENT, restrict.Scope.ID));
                        eventLoc.Keys.AddRange(restrict.Keys);
                        locations.Locations[eventLoc.Scope] = eventLoc;
                        locations.Locations.Remove(restrict.Scope);
                    }
                }
            }
            // Now calculate all location scopes - distinct item sources.
            // This is the main key for the annotations file, so scopes can be marked as missable or not, assigned logical areas, etc.
            // It is also used as the target for randomizing an item, because if several locations are equivalent, all should contain the same item.
            List <ScopeType> uniqueTypes = new List <ScopeType> {
                ScopeType.EVENT, ScopeType.ENTITY, ScopeType.MATERIAL
            };

            foreach (KeyValuePair <ItemKey, ItemLocations> entry in data.Data)
            {
                ItemKey item   = entry.Key;
                int     unique = 0;
                foreach (KeyValuePair <ItemScope, ItemLocation> entry2 in entry.Value.Locations)
                {
                    ItemScope    scope = entry2.Key;
                    ItemLocation loc   = entry2.Value;
                    int          id    = uniqueTypes.Contains(scope.Type) ? scope.ID : -1;
                    unique = unique == -1 ? -1 : (id == -1 ? -1 : unique + 1);
                    SortedSet <int> shopIds  = new SortedSet <int>(loc.Keys.Where(k => k.Type == LocationType.SHOP).Select(k => GetShopType(k.ID)));
                    SortedSet <int> shopQwcs = new SortedSet <int>(loc.Keys.Where(k => k.Type == LocationType.SHOP).Select(k => ((int)shops[k.ID]["qwcID"].Value, (int)shops[k.ID]["EventFlag"].Value))
                                                                   .Where(e => e.Item1 != -1 && (!restrictiveQwcs.Contains(e.Item1) || e.Item2 == -1))
                                                                   .Select(e => e.Item1));
                    SortedSet <int> allShop = new SortedSet <int>(shopIds.Union(shopQwcs));
                    if (shopIds.Count() + shopQwcs.Count() != allShop.Count())
                    {
                        Warn($"Overlapping qwc/shop ids for location {loc}");
                    }
                    SortedSet <int> modelBase     = scope.Type == ScopeType.MODEL ? new SortedSet <int>(loc.Keys.Select(k => k.BaseID)) : new SortedSet <int>();
                    bool            onlyShops     = loc.Keys.All(k => k.Type == LocationType.SHOP) && allShop.Count() > 0;
                    LocationScope   locationScope = new LocationScope(scope.Type, id, allShop, modelBase, onlyShops);
                    data.AddLocationScope(item, scope, locationScope);
                }
                entry.Value.Unique = unique > 0 && item.Type != ItemType.ARMOR;
            }

            if (logUnused)
            {
                foreach (KeyValuePair <ItemKey, string> entry in game.Names())
                {
                    ItemKey item = entry.Key;
                    if (item.Type == 0)
                    {
                        item = new ItemKey(item.Type, item.ID - (item.ID % 10000));
                    }
                    if (!data.Data.ContainsKey(item))
                    {
                        // Mostly pulls up old DS1 items and gestures.
                        Console.WriteLine($"Unused item {item.Type}-{entry.Key.ID}: {entry.Value}");
                    }
                }
            }
            return(data);
        }
        private SortedDictionary <int, List <EntityId> > FindItemLots(GameData game)
        {
            PARAM itemLots = game.Param("ItemLotParam");
            PARAM npcs     = game.Param("NpcParam");

            SortedDictionary <int, List <EntityId> > usedItemLots = new SortedDictionary <int, List <EntityId> >();

            // TODO: Merge in Sekiro map scraper, which is a fair bit more sophisticated.
            Dictionary <EntityId, EntityId>    objects      = new Dictionary <EntityId, EntityId>();
            Dictionary <int, List <EntityId> > usedNpcs     = new Dictionary <int, List <EntityId> >();
            Dictionary <int, List <EntityId> > usedEntities = new Dictionary <int, List <EntityId> >();

            foreach (KeyValuePair <string, MSB3> entry in game.Maps)
            {
                string location = game.Locations[entry.Key];
                foreach (MSB3.Part.Object part in entry.Value.Parts.Objects)
                {
                    EntityId id = new EntityId(location, part.Name, part.EventEntityID);
                    objects[id] = id;
                    if (part.EventEntityID > 0)
                    {
                        AddMulti(usedEntities, part.EventEntityID, id);
                    }
                }
                foreach (MSB3.Part.Enemy enemy in entry.Value.Parts.Enemies)
                {
                    EntityId id = new EntityId(location, enemy.Name, enemy.EventEntityID, enemy.NPCParamID, enemy.CharaInitID);
                    objects[id] = id;
                    if (enemy.NPCParamID > 0)
                    {
                        AddMulti(usedNpcs, enemy.NPCParamID, id);
                    }
                    if (enemy.EventEntityID > 0)
                    {
                        AddMulti(usedEntities, enemy.EventEntityID, id);
                    }
                }
            }
            foreach (KeyValuePair <string, MSB3> entry in game.Maps)
            {
                string location = game.Locations[entry.Key];
                foreach (MSB3.Event.Treasure treasure in entry.Value.Events.Treasures)
                {
                    if (treasure.PartName2 != null)
                    {
                        EntityId id = new EntityId(location, treasure.PartName2);
                        if (!objects.ContainsKey(id))
                        {
                            if (logUnused)
                            {
                                Console.WriteLine($"Missing entity for treasure {treasure.Name} with entity {treasure.PartName2} and lot {treasure.ItemLot1}");
                            }
                            continue;
                        }
                        AddMulti(usedItemLots, treasure.ItemLot1, objects[id]);
                    }
                }
            }
            foreach (PARAM.Row row in npcs.Rows)
            {
                int        npcID = (int)row.ID;
                PARAM.Cell cell  = row["ItemLotId1"];
                if (cell == null || (int)cell.Value == -1)
                {
                    continue;
                }
                int itemLot = (int)cell.Value;
                if (itemLots[itemLot] == null)
                {
                    if (logUnused)
                    {
                        Console.WriteLine($"Invalid NPC lot item for {npcID} with lot {itemLot}");
                    }
                    continue;
                }
                if (!usedNpcs.ContainsKey(npcID))
                {
                    if (logUnused)
                    {
                        Console.WriteLine($"Unused NPC: {npcID}");
                    }
                    continue;
                }
                AddMulti(usedItemLots, itemLot, usedNpcs[npcID]);
            }
            foreach (KeyValuePair <int, int> entry in entityItemLots)
            {
                int        entityId = entry.Key;
                List <int> itemLot  = new List <int> {
                    entry.Value
                };
                if (additionalEntityItemLots.ContainsKey(entityId))
                {
                    itemLot.AddRange(additionalEntityItemLots[entityId]);
                }
                if (!usedEntities.ContainsKey(entityId))
                {
                    Warn($"Missing NPC {entityId} with item lot {String.Join(", ", itemLot)}");
                    continue;
                }
                List <EntityId> id = usedEntities[entityId];
                foreach (int lot in itemLot)
                {
                    if (logUnused && (int)itemLots[lot]["getItemFlagId"].Value == -1 && id[0].GetModelID() != 2150)
                    {
                        Warn($"Eventless entity drop, not crystal lizard, for {String.Join(", ", id)} item lot {lot}");
                    }
                    AddMulti(usedItemLots, lot, id);
                }
            }
            foreach (int itemLot in Enumerable.Concat(talkLots, crowLots))
            {
                if ((int)itemLots[itemLot]["getItemFlagId"].Value == -1)
                {
                    Warn($"No event id attached to talk event {itemLot}");
                }
                AddMulti(usedItemLots, itemLot, new EntityId("", "from talk"));
            }
            if (logUnused)
            {
                int lastLot = 0;
                foreach (PARAM.Row lot in itemLots.Rows)
                {
                    int itemLot = (int)lot.ID;
                    if (itemLot == lastLot + 1)
                    {
                        // Don't require groups of item lots to be connected, only the base lot
                        lastLot = itemLot;
                        continue;
                    }
                    if (!usedItemLots.ContainsKey(itemLot))
                    {
                        Console.WriteLine($"Unconnected item lot {itemLot}: {game.LotName(itemLot)}");
                    }
                    lastLot = itemLot;
                }
            }
            return(usedItemLots);
        }
        private ItemLocs FindItemLocs(GameData game)
        {
            PARAM itemLots = game.Param("ItemLotParam");
            PARAM npcs     = game.Param("NpcParam");

            ItemLocs ret = new ItemLocs
            {
                usedItemLots     = new SortedDictionary <int, List <EntityId> >(),
                usedBaseShops    = new SortedDictionary <int, List <EntityId> >(),
                baseLotsToCreate = new Dictionary <int, int>(),
                // TODO: See if we can have a reasaonable processing step
                newEntityLots = new Dictionary <int, int>(),
            };

            Dictionary <EntityId, EntityId>    objects      = new Dictionary <EntityId, EntityId>();
            Dictionary <int, List <EntityId> > usedNpcs     = new Dictionary <int, List <EntityId> >();
            Dictionary <int, List <EntityId> > usedEntities = new Dictionary <int, List <EntityId> >();
            Dictionary <int, List <EntityId> > usedEsds     = new Dictionary <int, List <EntityId> >();

            // Map from item lot to ESD id, hackily extracted from ESD.
            Dictionary <int, HashSet <int> > talkItemLocations = new Dictionary <int, HashSet <int> >();
            // This is a bit intensive. Ideally just dump this somewhere else
            HashSet <int> debugEsd = new HashSet <int>();

            IEnumerable <ESD.Condition> GetCommands(List <ESD.Condition> condList) => Enumerable.Concat(condList, condList.SelectMany(cond => GetCommands(cond.Subconditions)));

            foreach (KeyValuePair <string, Dictionary <string, ESD> > entry in game.Talk)
            {
                string location = game.Locations.ContainsKey(entry.Key) ? game.Locations[entry.Key] : "";
                // 62210
                foreach (KeyValuePair <string, ESD> esdEntry in entry.Value)
                {
                    ESD esd   = esdEntry.Value;
                    int esdId = int.Parse(esdEntry.Key.Substring(1));
                    foreach ((int, int, ESD.State)stateDesc in esd.StateGroups.SelectMany(stateGroup => stateGroup.Value.Select(state => (stateGroup.Key, state.Key, state.Value))))
                    {
                        (int groupId, int id, ESD.State state) = stateDesc;
                        foreach (ESD.CommandCall cmd in new[] { state.EntryCommands, state.WhileCommands, state.ExitCommands, GetCommands(state.Conditions).SelectMany(c => c.PassCommands) }.SelectMany(c => c))
                        {
                            foreach (byte[] arg in cmd.Arguments)
                            {
                                if (arg.Length == 6 && arg[0] == 0x82 && arg[5] == 0xA1)
                                {
                                    int opt = BitConverter.ToInt32(arg, 1);
                                    if (opt >= 60000 && opt <= 69900 && itemLots[opt] != null)
                                    {
                                        AddMulti(talkItemLocations, opt, esdId);
                                    }
                                }
                            }
                        }
                    }
                }
            }
            bool logEntities = false;

            foreach (KeyValuePair <string, MSBS> entry in game.Smaps)
            {
                string location = game.Locations[entry.Key];
                MSBS   msb      = entry.Value;
                Dictionary <string, int> partEsds = new Dictionary <string, int>();
                List <MSBS.Part.Enemy>   enemies  = msb.Parts.Enemies;
                // TODO: Update SoulsFormat and migrate to new names
                foreach (MSBS.Event.Talk ev in entry.Value.Events.Talks)
                {
                    for (int i = 0; i < 2; i++)
                    {
                        string part  = ev.EnemyNames[i];
                        int    esdId = ev.TalkIDs[i];
                        if (esdId < 0 || part == null)
                        {
                            continue;
                        }
                        partEsds[part] = esdId;
                    }
                }
                foreach (MSBS.Entry obj in entry.Value.Parts.GetEntries())
                {
                    MSBS.Part part = obj as MSBS.Part;
                    if (part == null)
                    {
                        continue;
                    }
                    EntityId   id;
                    int        esdId    = 0;
                    List <int> groupIDs = part.EntityGroupIDs.Where(groupID => groupID > 0).ToList();
                    if (part is MSBS.Part.Enemy enemy)
                    {
                        esdId = partEsds.ContainsKey(enemy.Name) ? partEsds[enemy.Name] : -1;
                        id    = new EntityId(location, enemy.Name, enemy.EntityID, enemy.NPCParamID, enemy.CharaInitID, groupIDs);
                    }
                    else if (part is MSBS.Part.Object || logEntities)
                    {
                        id = new EntityId(location, part.Name, part.EntityID, GroupIds: groupIDs);
                    }
                    else
                    {
                        continue;
                    }
                    objects[id] = id;
                    if (id.EventEntityID > 0)
                    {
                        AddMulti(usedEntities, id.EventEntityID, id);
                    }
                    foreach (int groupID in groupIDs)
                    {
                        AddMulti(usedEntities, groupID, id);
                    }
                    if (id.NPCParamID > 0)
                    {
                        AddMulti(usedNpcs, id.NPCParamID, id);
                    }
                    if (esdId > 0)
                    {
                        if (debugEsd.Contains(esdId))
                        {
                            Console.WriteLine($"ESD {esdId} belongs to {game.EntityName(id, true)} in {location}, entity id {id.EventEntityID}");
                        }
                        AddMulti(usedEsds, esdId, id);
                    }
                }
            }
            if (logEntities)
            {
                foreach (KeyValuePair <int, List <EntityId> > entry in usedEntities.OrderBy(e => e.Key))
                {
                    Console.WriteLine($"{entry.Key}: {string.Join(", ", entry.Value.Select(e => $"{game.EntityName(e, true)} in {e.MapName}"))}");
                }
            }
            List <EntityId> unusedEsd = new List <EntityId> {
                new EntityId("", "Unknown Dialogue")
            };

            foreach (KeyValuePair <int, HashSet <int> > entry in talkItemLocations)
            {
                List <EntityId> talkIds;
                if (addUnused)
                {
                    talkIds = entry.Value.SelectMany(esd => usedEsds.ContainsKey(esd) ? usedEsds[esd] : unusedEsd).ToList();
                }
                else
                {
                    talkIds = entry.Value.Where(esd => usedEsds.ContainsKey(esd)).SelectMany(esd => usedEsds[esd]).ToList();
                    if (talkIds.Count == 0)
                    {
                        continue;
                    }
                }
                AddMulti(ret.usedItemLots, entry.Key, talkIds);
            }
            foreach (KeyValuePair <int, int> entry in shopEsds)
            {
                if (!usedEsds.ContainsKey(entry.Value) && !addUnused)
                {
                    continue;
                }
                AddMulti(ret.usedBaseShops, entry.Key, usedEsds.ContainsKey(entry.Value) ? usedEsds[entry.Value] : unusedEsd);
            }
            foreach (KeyValuePair <string, MSBS> entry in game.Smaps)
            {
                string location = game.Locations[entry.Key];
                foreach (MSBS.Event.Treasure treasure in entry.Value.Events.Treasures)
                {
                    if (treasure.TreasurePartName != null)
                    {
                        EntityId id = new EntityId(location, treasure.TreasurePartName);
                        if (!objects.ContainsKey(id))
                        {
                            if (logUnused)
                            {
                                Console.WriteLine($"Missing entity for treasure {treasure.Name} with entity {treasure.TreasurePartName} and lot {treasure.ItemLotID}");
                            }
                            continue;
                        }
                        AddMulti(ret.usedItemLots, treasure.ItemLotID, objects[id]);
                    }
                }
            }
            foreach (PARAM.Row row in npcs.Rows)
            {
                int        npcID = (int)row.ID;
                PARAM.Cell cell  = row["ItemLotId1"];
                if (cell == null || (int)cell.Value == -1)
                {
                    continue;
                }
                int itemLot = (int)cell.Value;
                if (itemLots[itemLot] == null)
                {
                    if (logUnused)
                    {
                        Console.WriteLine($"Invalid NPC lot item for {npcID} with lot {itemLot}");
                    }
                    continue;
                }
                if (!usedNpcs.ContainsKey(npcID))
                {
                    if (logUnused)
                    {
                        Console.WriteLine($"Unused NPC: {npcID}");
                    }
                    if (addUnused)
                    {
                        AddMulti(ret.usedItemLots, itemLot, new EntityId("", "Unused NPC"));
                    }
                    continue;
                }
                AddMulti(ret.usedItemLots, itemLot, usedNpcs[npcID]);
            }
            foreach (KeyValuePair <int, int> entry in entityItemLots)
            {
                int        entityId = entry.Key;
                List <int> itemLot  = new List <int> {
                    entry.Value
                };
                if (additionalEntityItemLots.ContainsKey(entityId))
                {
                    itemLot.AddRange(additionalEntityItemLots[entityId]);
                }
                List <EntityId> id;
                if (usedEntities.ContainsKey(entityId))
                {
                    id = usedEntities[entityId];
                }
                else
                {
                    Console.WriteLine($"XX Missing entity {entityId} with item lot {String.Join(", ", itemLot)}");
                    id = new List <EntityId> {
                        new EntityId("", "from entity")
                    };
                }

                foreach (int lot in itemLot)
                {
                    if (logUnused && (int)itemLots[lot]["getItemFlagId"].Value == -1 && id[0].GetModelID() != 2150)
                    {
                        Console.WriteLine($"Eventless entity drop, not crystal lizard, for {String.Join(", ", id)} item lot {lot}");
                    }
                    AddMulti(ret.usedItemLots, lot, id);
                }
            }
            Dictionary <int, int> syntheticLotBase = new Dictionary <int, int>();

            foreach (KeyValuePair <int, int> entry in treasureCarpDrops)
            {
                int             entityId = entry.Key;
                int             baseLot  = entry.Value;
                List <EntityId> id;
                if (usedEntities.ContainsKey(entityId))
                {
                    id = usedEntities[entityId];
                }
                else
                {
                    Console.WriteLine($"XX Missing entity {entityId} with item lot {baseLot}");
                    id = new List <EntityId> {
                        new EntityId("", "from entity")
                    };
                }
                if (syntheticLotBase.ContainsKey(baseLot))
                {
                    syntheticLotBase[baseLot] += 5;
                }
                else
                {
                    syntheticLotBase[baseLot] = baseLot;
                }
                int itemLot = syntheticLotBase[baseLot];
                ret.baseLotsToCreate[itemLot] = baseLot;
                // TODO: See if this info can be extracted anywhere else
                ret.newEntityLots[entityId] = itemLot;
                AddMulti(ret.usedItemLots, itemLot, id);
            }
            foreach (KeyValuePair <int, string> entry in scriptLots)
            {
                int itemLot = entry.Key;
                if ((int)itemLots[itemLot]["getItemFlagId"].Value == -1)
                {
                    // TODO: Make sure there are all classified...
                    // Console.WriteLine($"XX No event id attached to script event {itemLot}");
                }
                AddMulti(ret.usedItemLots, itemLot, new EntityId(entry.Value, "Scripted"));
            }
            foreach (int itemLot in talkLots)
            {
                if ((int)itemLots[itemLot]["getItemFlagId"].Value == -1)
                {
                    Console.WriteLine($"XX No event id attached to talk event {itemLot}");
                }
                AddMulti(ret.usedItemLots, itemLot, new EntityId("", "from talk"));
            }
            int  lastLot       = 0;
            bool lastConnected = false;

            foreach (PARAM.Row lot in itemLots.Rows)
            {
                int  itemLot   = (int)lot.ID;
                bool connected = (byte)lot["LotItemNum1"].Value == 1;
                if (itemLot == lastLot + 1)
                {
                    // Don't require groups of item lots to be connected, only the base lot
                }
                else if (connected && lastConnected && (itemLot - lastLot) % 10 == 0 && (itemLot - lastLot) <= 40)
                {
                    // This is also fine.... aaaa. Bell Demon drops.
                }
                else
                {
                    if (!ret.usedItemLots.ContainsKey(itemLot))
                    {
                        if (logUnused)
                        {
                            Console.WriteLine($"Unconnected item lot {itemLot}: {game.LotName(itemLot)}");
                        }
                        if (addUnused)
                        {
                            AddMulti(ret.usedItemLots, itemLot, new EntityId("", "unknown"));
                        }
                    }
                }
                lastLot       = itemLot;
                lastConnected = connected;
            }
            return(ret);
        }
        public LocationData FindItems(GameData game)
        {
            ItemLocs allLocs = FindItemLocs(game);
            SortedDictionary <int, List <EntityId> > usedItemLots  = allLocs.usedItemLots;
            SortedDictionary <int, List <EntityId> > usedBaseShops = allLocs.usedBaseShops;

            PARAM shops     = game.Param("ShopLineupParam");
            PARAM itemLots  = game.Param("ItemLotParam");
            PARAM materials = game.Param("EquipMtrlSetParam");
            PARAM npcs      = game.Param("NpcParam");

            // First we may have to create lots - easier to do this at the start than keep data in side channels all the way through
            int baseEvent = 52500960;

            foreach (KeyValuePair <int, int> toCreate in allLocs.baseLotsToCreate)
            {
                PARAM.Row baseRow = itemLots[toCreate.Value];
                int       newLot  = toCreate.Key;
                PARAM.Row row     = itemLots[newLot];
                if (row == null)
                {
                    row = game.AddRow("ItemLotParam", newLot);
                    foreach (PARAM.Cell newCell in row.Cells)
                    {
                        newCell.Value = baseRow[newCell.Def.InternalName].Value;
                    }
                }
                // TODO: Re-enable this with flag id validation
                row["getItemFlagId"].Value = baseEvent;
                baseEvent++;
            }

            LocationData data = new LocationData();

            data.NewEntityLots = allLocs.newEntityLots;

            LocationKey prevLocation = null;

            foreach (KeyValuePair <int, List <EntityId> > entry in usedItemLots)
            {
                int itemLot = entry.Key;
                if (prevLocation != null)
                {
                    // TODO: If event flag is tracked in script, allow 1 maxslot
                    prevLocation.MaxSlots = Math.Max(Math.Min(itemLot - prevLocation.ID - 1, 8), 1);
                    if (prevLocation.MaxSlots < 1)
                    {
                        Console.WriteLine($"XX Overlapping slots at {itemLot}");
                    }
                    prevLocation = null;
                }
                List <EntityId> entities = entry.Value;
                string          locs     = string.Join(", ", entities.Select(e => game.EntityName(e, true) + (e.MapName == "" ? "" : " " + e.MapName)));
                if (itemLots[itemLot] == null)
                {
                    string text = game.LotName(itemLot);
                    // These are fine - no-ops to game
                    if (logUnused)
                    {
                        Console.WriteLine($"MISSING connected item lot!! {itemLot}: {text} @ {locs}");
                    }
                    continue;
                }
                LocationKey baseLocation = null;
                while (itemLots[itemLot] != null)
                {
                    bool   isBase = itemLot == entry.Key;
                    string text   = game.LotName(itemLot);

                    PARAM.Row row         = itemLots[itemLot];
                    int       clearCount  = (sbyte)row["ClearCount"].Value;
                    int       eventFlag   = (int)row["getItemFlagId"].Value;
                    int       totalPoints = 0;
                    for (int i = 1; i <= 8; i++)
                    {
                        totalPoints += (short)row[$"LotItemBasePoint0{i}"].Value;
                    }
                    List <string> itemLotOutput = new List <string>();
                    for (int i = 1; i <= 8; i++)
                    {
                        int id = (int)row[$"ItemLotId{i}"].Value;
                        if (id != 0)
                        {
                            uint type     = (uint)row[$"LotItemCategory0{i}"].Value;
                            int  points   = (short)row[$"LotItemBasePoint0{i}"].Value;
                            int  quantity = (ushort)row[$"NewLotItemNum{i}"].Value;
                            if (type == 0xFFFFFFFF)
                            {
                                continue;
                            }
                            ItemKey       item     = new ItemKey(LocationData.LotTypes[type], id);
                            string        itemText = $"{itemLot}[{locs}]";
                            List <string> lotInfo  = new List <string>();
                            if (quantity > 1)
                            {
                                lotInfo.Add($"{quantity}x");
                            }
                            if (points != totalPoints)
                            {
                                lotInfo.Add($"{100.0 * points / totalPoints:0.##}%");
                            }
                            PARAM.Row itemRow = game.Item(item);
                            if (itemRow != null && item.Type == ItemType.GOOD && (byte)itemRow["goodsType"].Value == 7)
                            {
                                // Items which are not shown in inventory menu for various reasons, but having them still does something.
                                lotInfo.Add($"(hidden)");
                            }
                            itemLotOutput.Add($"{game.Name(item)} " + string.Join(" ", lotInfo));
                            if (lotInfo.Count() > 0)
                            {
                                itemText += $" {string.Join(", ", lotInfo)}";
                            }
                            if (quantity <= 0)
                            {
                                Console.WriteLine($"XX There is 0! of {itemText}");
                            }
                            ItemScope scope;
                            if (eventFlag != -1)
                            {
                                if (equivalentEvents.ContainsKey(eventFlag))
                                {
                                    eventFlag = equivalentEvents[eventFlag];
                                }
                                scope = new ItemScope(ScopeType.EVENT, eventFlag);
                                // Note this doesn't necessarily have to be slot 1. But it should be only one slot...
                                if (points != totalPoints)
                                {
                                    Console.WriteLine($"Has event flag? But random? {itemText}");
                                }
                            }
                            else
                            {
                                // One time drops that directly award, that aren't covered by event flags. Mostly crystal lizards.
                                if (entities.Count() == 1 && entityItemLots.ContainsKey(entities[0].EventEntityID) && entityItemLots[entities[0].EventEntityID] == entry.Key)
                                {
                                    scope = new ItemScope(ScopeType.ENTITY, entities[0].EventEntityID);
                                }
                                // Non-respawning enemies with drops which can be missed. These are reused between different entities, so can drop multiple times.
                                else if (entities.All(e => nonRespawningEntities.Contains(e.EventEntityID)))
                                {
                                    scope = new ItemScope(ScopeType.ENTITY, entities.Select(e => e.EventEntityID).Min());
                                }
                                else
                                {
                                    int model = entities.Select(e => e.GetModelID()).Min();
                                    // Infinite guaranteed or scripted drops are not randomized unless specifically added to entityItemLots
                                    if (model == -1 || points == totalPoints)
                                    {
                                        if (logUnused)
                                        {
                                            Console.WriteLine($"XX Item {game.Name(item)} {itemLot} has no associated event, but is guaranteed or global: {itemText}");
                                        }
                                        continue;
                                    }
                                    scope = new ItemScope(ScopeType.MODEL, model);
                                }
                            }
                            LocationKey location = new LocationKey(LocationType.LOT, itemLot, itemText, entities, quantity, baseLocation);
                            data.AddLocation(item, scope, location);
                            if (baseLocation == null)
                            {
                                baseLocation = location;
                            }
                        }
                    }
                    // Write out the info. Some deduplication of sources to make prettier output.
                    string lotOutput = string.Join(", ", itemLotOutput);
                    bool   simple    = false;
                    string text2;
                    if (simple)
                    {
                        SortedSet <string> locations = new SortedSet <string>(entities.Select(e => game.LocationNames[e.MapName]));
                        SortedSet <string> models    = new SortedSet <string>(entities.Select(e => e.EntityName.StartsWith("o") ? "Treasure" : game.EntityName(e)));
                        text2 = $"{string.Join(", ", models)}: {string.Join(", ", locations)}";
                        if (models.All(x => x == "unknown"))
                        {
                            text2 = "Unused/Unknown";
                        }
                        if (models.All(x => x == "Unused NPC"))
                        {
                            text2 = "Unused NPC";
                        }
                        else if (models.Any(x => x == "Unused NPC"))
                        {
                            models.Remove("Unused NPC"); if (locations.Count > 1)
                            {
                                locations.Remove("Global");
                            }
                            text2 = $"{string.Join(", ", models)}: {string.Join(", ", locations)}";
                        }
                    }
                    else
                    {
                        // e.NPCParamID > -1 ? $" #{e.NPCParamID}" : ""
                        // SortedSet<string> models = new SortedSet<string>(entities.Select(e => e.EntityName.StartsWith("o") ? $"Treasure in {e.MapName}" : $"{game.ModelName(e, true)} in {e.MapName}"));
                        SortedSet <string> models = new SortedSet <string>(entities.Select(e => game.EntityName(e, true) + (e.MapName == "" ? "" : $" in {e.MapName}")));
                        text2 = $"{string.Join(", ", models)}";
                    }
                    // Console.WriteLine($"{itemLot} [{text2}] {lotOutput}");

                    if (itemLot == 2014)
                    {
                        break;                   // Unused, and item lot immediately after it is used. Won't be an issue once. ... ??
                    }
                    // Try to navigate resource drops (affected by Bell Demon).
                    if ((byte)row["LotItemNum1"].Value == 1)
                    {
                        int curOffset = itemLot % 100;
                        int curBase   = itemLot / 100 * 100;
                        int offset;
                        for (offset = curOffset + 10; offset <= 50; offset += 10)
                        {
                            PARAM.Row offRow = itemLots[curBase + offset];
                            if (offRow != null && (byte)offRow["LotItemNum1"].Value == 1)
                            {
                                break;
                            }
                        }
                        if (offset <= 50)
                        {
                            itemLot = curBase + offset;
                        }
                        else
                        {
                            itemLot++;
                        }
                    }
                    else
                    {
                        itemLot++;
                    }
                }
                prevLocation = baseLocation;
            }
            if (prevLocation != null)
            {
                prevLocation.MaxSlots = 5;
            }
            SortedDictionary <int, List <string> > qwcs      = new SortedDictionary <int, List <string> >();
            Dictionary <int, LocationKey>          baseShops = new Dictionary <int, LocationKey>();

            foreach (PARAM.Row row in shops.Rows)
            {
                int    shopID   = (int)row.ID;
                int    baseShop = GetShopType(shopID);
                string shopName = shopSplits[baseShop];
                if (shopName == null)
                {
                    if (!addUnused)
                    {
                        continue;
                    }
                    shopName = "Unknown shop";
                }
                if (shopID >= 9000000)
                {
                    continue;
                }
                int qwc = (int)row["qwcID"].Value;

                int     type         = (byte)row["equipType"].Value;
                int     id           = (int)row["EquipId"].Value;
                int     quantity     = (short)row["sellQuantity"].Value;
                int     eventFlag    = (int)row["EventFlag"].Value;
                int     material     = (int)row["mtrlId"].Value;
                int     value        = (int)row["value"].Value;
                float   priceRate    = (float)row["PriceRate"].Value;
                string  quantityText = quantity == -1 ? "" : $" ({quantity})"; // (unlimited)
                string  qwcText      = qwc == -1 ? "" : $" {game.QwcName(qwc)}";
                string  costText     = "";
                ItemKey item         = new ItemKey((ItemType)type, id);
                if (material != -1)
                {
                    PARAM.Row matRow        = materials[material];
                    int       materialQuant = (sbyte)matRow["ItemNum01"].Value;
                    int       materialItem  = (int)matRow["MaterialId01"].Value;
                    costText = $" for {materialQuant} {game.Name(new ItemKey(ItemType.GOOD, materialItem))}";
                }
                if (value != 0 || costText == "")
                {
                    int actualCost = value;
                    if (actualCost == -1)
                    {
                        actualCost = (int)game.Item(item)["shopId"].Value;
                    }
                    if (priceRate != 0)
                    {
                        actualCost = (int)(actualCost * priceRate);
                    }
                    costText = costText == "" ? $" for {actualCost} Sen" : $"{costText} and {actualCost} Sen";
                }
                string shopText = $"{shopName}{qwcText}{quantityText}{costText} - event {eventFlag}";
                string text     = $"{shopID}[{shopText}]";
                // Console.WriteLine($"{shopID} [{shopName}{qwcText}] {game.Name(item)}{quantityText}{costText}");
                LocationKey location = new LocationKey(
                    LocationType.SHOP, shopID, text,
                    usedBaseShops.ContainsKey(baseShop) ? usedBaseShops[baseShop] : new List <EntityId>(),
                    quantity,
                    null); // try not to use base shops - baseShops.ContainsKey(baseShop) ? baseShops[baseShop] : null);
                if (shopID == baseShop)
                {
                    baseShops[baseShop] = location;
                }
                ItemScope scope;
                AddMulti(qwcs, qwc, $"{game.Name(item)}: {text}");
                if (eventFlag != -1)
                {
                    if (equivalentEvents.ContainsKey(eventFlag))
                    {
                        eventFlag = equivalentEvents[eventFlag];
                    }
                    if (quantity <= 0)
                    {
                        Console.WriteLine("XX No quantity for event flag shop entry {text}");
                    }
                    ScopeType scopeType = ScopeType.EVENT;
                    if (restrictiveQwcs.Contains(qwc))
                    {
                        // If item becomes unavailable at some point, it returns in infinite form
                        scopeType = ScopeType.SHOP_INFINITE_EVENT;
                    }
                    scope = new ItemScope(scopeType, eventFlag);
                }
                else if (material != -1)
                {
                    int materialItem = (int)materials[material]["MaterialId01"].Value;
                    scope = new ItemScope(ScopeType.MATERIAL, materialItem);
                }
                else
                {
                    scope = new ItemScope(ScopeType.SHOP_INFINITE, -1);
                }
                data.AddLocation(item, scope, location);
            }
            // Merge infinite and finite shops. Mostly done via heuristic (when event and infinite both exist), with exception of one event
            ItemScope infiniteKey = new ItemScope(ScopeType.SHOP_INFINITE, -1);

            foreach (ItemLocations locations in data.Data.Values)
            {
                foreach (ItemLocation restrict in locations.Locations.Values.Where(loc => loc.Scope.Type == ScopeType.SHOP_INFINITE_EVENT).ToList())
                {
                    if (locations.Locations.ContainsKey(infiniteKey))
                    {
                        // Combine infinite shops into event
                        ItemLocation infinite = locations.Locations[infiniteKey];
                        restrict.Keys.AddRange(infinite.Keys);
                        locations.Locations.Remove(infiniteKey);
                    }
                    else
                    {
                        Console.WriteLine($"XX: No 1:1 match between event shops and infinite shops for {restrict}");
                        // No infinite shops, turn this into a regular event shop. (Doesn't happen in base DS3)
                        ItemLocation eventLoc = new ItemLocation(new ItemScope(ScopeType.EVENT, restrict.Scope.ID));
                        eventLoc.Keys.AddRange(restrict.Keys);
                        locations.Locations[eventLoc.Scope] = eventLoc;
                        locations.Locations.Remove(restrict.Scope);
                    }
                }
            }
            // Now can find all location scopes
            List <ScopeType> uniqueTypes = new List <ScopeType> {
                ScopeType.EVENT, ScopeType.ENTITY, ScopeType.MATERIAL
            };

            foreach (KeyValuePair <ItemKey, ItemLocations> entry in data.Data)
            {
                int unique = 0;
                foreach (KeyValuePair <ItemScope, ItemLocation> entry2 in entry.Value.Locations)
                {
                    ItemScope    scope = entry2.Key;
                    ItemLocation loc   = entry2.Value;
                    int          id    = uniqueTypes.Contains(scope.Type) ? scope.ID : -1;
                    unique = unique == -1 ? -1 : (id == -1 ? -1 : unique + 1);
                    SortedSet <int> shopIds  = new SortedSet <int>(loc.Keys.Where(k => k.Type == LocationType.SHOP).Select(k => GetShopType(k.ID)));
                    SortedSet <int> shopQwcs = new SortedSet <int>(loc.Keys.Where(k => k.Type == LocationType.SHOP).Select(k => (int)shops[k.ID]["qwcID"].Value)
                                                                   .Select(qwc => equivalentEvents.ContainsKey(qwc) ? equivalentEvents[qwc] : qwc)
                                                                   .Where(qwc => qwc != -1 && !restrictiveQwcs.Contains(qwc)));
                    SortedSet <int> allShop = new SortedSet <int>(shopIds.Union(shopQwcs));
                    if (shopIds.Count() + shopQwcs.Count() != allShop.Count())
                    {
                        Console.WriteLine($"XX Overlapping qwc/shop ids for location {loc}");
                    }
                    SortedSet <int> modelBase     = scope.Type == ScopeType.MODEL ? new SortedSet <int>(loc.Keys.Select(k => k.BaseID)) : new SortedSet <int>();
                    bool            onlyShops     = loc.Keys.All(k => k.Type == LocationType.SHOP) && allShop.Count() > 0;
                    LocationScope   locationScope = new LocationScope(scope.Type, id, allShop, modelBase, onlyShops);
                    data.AddLocationScope(entry.Key, scope, locationScope);
                    loc.LocScope = locationScope;
                }
                entry.Value.Unique = unique > 0;
            }

            if (logUnused)
            {
                Console.WriteLine("---------------------------------------------------------------------------");
                foreach (KeyValuePair <ItemKey, string> entry in game.Names())
                {
                    ItemKey item = entry.Key;
                    if (item.Type == 0)
                    {
                        item = new ItemKey(item.Type, item.ID - (item.ID % 10000));
                    }
                    if (!data.Data.ContainsKey(item))
                    {
                        // Mostly pulls up old DS1 items, crow items, and gestures.
                        Console.WriteLine($"Unused item {item.Type}-{entry.Key.ID}: {entry.Value}");
                    }
                }
            }

            return(data);
        }
        public void Write(Random random, RandomizerOptions options)
        {
            // Collect game items
            // For armor: headEquip/bodyEquip/armEquip/legEquip booleans. weight float
            // For weapons: weight float.
            // Requirements: correctStrength/correctAgility/corretMagic/corretFaith float.
            // Types: displayTypeId (maps to MenuValueTableParam, in FMGs).
            // enablePyromancy/enablePyromancy/enableMiracle bool? Check attack types other than isBlowAttackType for whether a proper weapon
            // rightHandEquipable/leftHandEquipable bool (bothHandEquipable)?
            // arrowSlotEquipable/boltSlotEquipable bool for bows. bool DisableShoot for greatbow
            // enableGuard for shield
            // Arrows/Bolts: eh a bit tricky. weaponCategory 13/14 for arrow/bolt, and bool DisableShoot for greatbow
            // Spells: in Magic table. requirementIntellect, requirementFaith. ezStateBehaviorType - 0 magic, 2 pyro, 1 miracle
            Dictionary <EquipCategory, List <ItemKey> > items = new Dictionary <EquipCategory, List <ItemKey> >();
            Dictionary <ItemKey, float>   weights             = new Dictionary <ItemKey, float>();
            Dictionary <ItemKey, StatReq> requirements        = new Dictionary <ItemKey, StatReq>();
            HashSet <ItemKey>             crossbows           = new HashSet <ItemKey>();
            PARAM magics  = game.Param("Magic");
            bool  twoHand = options["startingtwohand"];

            foreach (ItemKey key in data.Data.Keys)
            {
                if (key.Type == ItemType.WEAPON)
                {
                    PARAM.Row     row            = game.Item(key);
                    EquipCategory mainCat        = EquipCategory.WEAPON;
                    int           weaponCategory = (byte)row["weaponCategory"].Value;
                    if (weaponCategories.ContainsKey(weaponCategory))
                    {
                        mainCat = weaponCategories[weaponCategory];
                    }
                    if ((byte)row["enableGuard"].Value == 1)
                    {
                        mainCat = EquipCategory.SHIELD;
                    }
                    if (mainCat == EquipCategory.BOW || mainCat == EquipCategory.ARROW || mainCat == EquipCategory.BOLT)
                    {
                        // Disable greatbow for starting - requirements too far off
                        if ((byte)row["DisableShoot"].Value == 1)
                        {
                            continue;
                        }
                    }
                    if (mainCat == EquipCategory.BOW)
                    {
                        if ((byte)row["boltSlotEquipable"].Value == 1)
                        {
                            crossbows.Add(key);
                        }
                    }
                    if (mainCat != EquipCategory.UNSET)
                    {
                        AddMulti(items, mainCat, key);
                    }
                    if ((byte)row["enableMagic"].Value == 1)
                    {
                        AddMulti(items, EquipCategory.CATALYST, key);
                    }
                    if ((byte)row["enableMiracle"].Value == 1)
                    {
                        AddMulti(items, EquipCategory.TALISMAN, key);
                    }
                    if ((byte)row["enablePyromancy"].Value == 1)
                    {
                        AddMulti(items, EquipCategory.FLAME, key);
                    }
                    int str = (byte)row["properStrength"].Value;
                    // Add two hand adjustment for weapons. Note this doesn't work exactly for casting items, but does not affect casting.
                    if (twoHand && (byte)row["Unk14"].Value == 0 && (mainCat == EquipCategory.WEAPON || mainCat == EquipCategory.UNSET))
                    {
                        str = (int)Math.Ceiling(str / 1.5);
                    }
                    requirements[key] = new StatReq
                    {
                        Str = (sbyte)str,
                        Dex = (sbyte)(byte)row["properAgility"].Value,
                        Mag = (sbyte)(byte)row["properMagic"].Value,
                        Fai = (sbyte)(byte)row["properFaith"].Value,
                    };
                    weights[key] = (float)row["weight"].Value;
                }
                else if (key.Type == ItemType.ARMOR)
                {
                    PARAM.Row row = game.Item(key);
                    for (int i = 0; i < 4; i++)
                    {
                        if ((byte)row[armorTypes[i]].Value == 1)
                        {
                            AddMulti(items, armorCats[i], key);
                            weights[key] = (float)row["weight"].Value;
                            break;
                        }
                    }
                }
                else if (key.Type == ItemType.GOOD)
                {
                    PARAM.Row magic = magics[key.ID];
                    // Exclude Spook and Tears of Denial as they can be a key item, useful though they are
                    if (magic != null && key.ID != 1354000 && key.ID != 3520000)
                    {
                        int magicCat = (byte)magic["ezStateBehaviorType"].Value;
                        AddMulti(items, magicTypes[magicCat], key);
                        requirements[key] = new StatReq
                        {
                            Str = 0,
                            Dex = 0,
                            Mag = (sbyte)(byte)magic["requirementIntellect"].Value,
                            Fai = (sbyte)(byte)magic["requirementFaith"].Value,
                            Att = (sbyte)(byte)magic["slotLength"].Value,
                        };
                    }
                }
            }
            // Generate some armor sets. One downside of this approach is that each piece is represented only once - but it is just one shuffle per category, and tends to result in a similar distribution to normal.
            List <List <ItemKey> > weightedArmors = new List <List <ItemKey> >();

            for (int i = 0; i < 4; i++)
            {
                weightedArmors.Add(WeightedShuffle(random, items[armorCats[i]], item => 1 / weights[item]));
            }
            List <ArmorSet> armors    = new List <ArmorSet>();
            int             maxArmors = weightedArmors.Select(rank => rank.Count).Min();

            for (int num = 0; num < maxArmors; num++)
            {
                ArmorSet armor = new ArmorSet();
                for (int i = 0; i < 4; i++)
                {
                    ItemKey item = weightedArmors[i][num];
                    armor.Ids[i]  = item.ID;
                    armor.Weight += weights[item];
                }
                armors.Add(armor);
            }
            armors.Sort((a, b) => a.Weight.CompareTo(b.Weight));

            PARAM chara = game.Param("CharaInitParam");
            // Just for testing ;)
            bool cheat = false;

            for (int i = 0; i < 10; i++)
            {
                PARAM.Row row = chara[startId + i];
                // First, always fudge magic to 10, so that Orbeck quest is possible.
                if ((sbyte)row["baseMag"].Value < 10)
                {
                    row["baseMag"].Value = (sbyte)10;
                }
                if (cheat)
                {
                    foreach (string stat in stats)
                    {
                        row[$"base{stat}"].Value = (sbyte)90;
                    }
                }
                // Then, see stat diffs for weapons/spells/catalysts, and fudge if necessary
                CharacterClass chClass = classes[i];
                int            attStat = (sbyte)row["baseWil"].Value;
                StatReq        chReqs  = new StatReq
                {
                    Str = (sbyte)row["baseStr"].Value,
                    Dex = (sbyte)row["baseDex"].Value,
                    Mag = (sbyte)row["baseMag"].Value,
                    Fai = (sbyte)row["baseFai"].Value,
                    Att = (sbyte)(attStat < 10 ? 0 : attStat < 14 ? 1 : 2),
                };
                StatReq dynamicReqs      = chReqs;
                double  fudgeFactor      = 1.5;
                float   weaponWeight     = 0f;
                int     attSlots         = 0;
                bool    crossbowSelected = false;
                Console.WriteLine($"Randomizing starting equipment for {chClass.Name}");
                foreach (KeyValuePair <string, EquipCategory> entry in baseStart.Concat(chClass.Start))
                {
                    EquipCategory cat = entry.Value;
                    // TODO: If a catalyst etc also doubles as a weapon, maybe skip its slot.
                    // This crossbow/bow logic relies on iteration order - try to make the order fixed...
                    if ((cat == EquipCategory.ARROW && crossbowSelected) || (cat == EquipCategory.BOLT && !crossbowSelected))
                    {
                        continue;
                    }
                    Dictionary <ItemKey, int> statDiffs  = items[entry.Value].ToDictionary(item => item, item => requirements[item].Eligible(dynamicReqs));
                    List <ItemKey>            candidates = items[entry.Value];
                    if (cat == EquipCategory.SHIELD || chClass.Name == "Deprived")
                    {
                        candidates = candidates.Where(item => statDiffs[item] >= 0).ToList();
                    }
                    if (cat == EquipCategory.SORCERY || cat == EquipCategory.MIRACLE || cat == EquipCategory.PYROMANCY)
                    {
                        // Fit within attunement slots. Alternatively could increase attunement, but that unbalances things potentially.
                        // Unfortunately means that pyromancer can't start with Chaos Bed Vestiges. Maybe for the best.
                        if (attSlots == chReqs.Att)
                        {
                            row[entry.Key].Value = -1;
                            continue;
                        }
                        candidates = candidates.Where(item => attSlots + requirements[item].Att <= chReqs.Att).ToList();
                    }
                    // Select weapon and adjust stats if necessary
                    List <ItemKey> weightKeys = WeightedShuffle(random, candidates, item =>
                    {
                        int diff = statDiffs[item];
                        if (diff >= 4)
                        {
                            return((float)Math.Pow(2, -4 * (Math.Min(diff, 20) / 20.0)));
                        }
                        if (diff >= 0)
                        {
                            return(2);
                        }
                        return((float)Math.Pow(fudgeFactor, diff));
                    });
                    ItemKey selected = weightKeys[0];
                    items[entry.Value].Remove(selected);
                    if (statDiffs[selected] < 0)
                    {
                        dynamicReqs.Adjust(requirements[selected]);
                        fudgeFactor *= -statDiffs[selected];
                    }
                    row[entry.Key].Value = selected.ID;
                    if (weights.ContainsKey(selected))
                    {
                        weaponWeight += weights[selected];
                    }
                    attSlots = requirements[selected].Att;
                    Console.WriteLine($"  {entry.Key} is now {game.Name(selected)}, meets requirements by {statDiffs[selected]}");
                }
                int statChange = dynamicReqs.Eligible(chReqs);
                if (statChange < 0)
                {
                    row["baseStr"].Value = dynamicReqs.Str;
                    row["baseDex"].Value = dynamicReqs.Dex;
                    row["baseMag"].Value = dynamicReqs.Mag;
                    row["baseFai"].Value = dynamicReqs.Fai;
                    row["soulLvl"].Value = (short)((short)row["soulLvl"].Value - statChange);
                }
                // Armor time
                float           totalWeight   = 40 + (sbyte)row["baseDurability"].Value;
                List <ArmorSet> availableSets = armors.TakeWhile(armor => armor.Weight + weaponWeight < totalWeight * 0.69f).ToList();
                if (availableSets.Count == 0)
                {
                    availableSets = new List <ArmorSet> {
                        armors[0]
                    }
                }
                ;
                ArmorSet selectedArmor = Choice(random, availableSets);
                armors.Remove(selectedArmor);
                Console.WriteLine($"  Armor: {string.Join(", ", selectedArmor.Ids.Select(id => game.Name(new ItemKey(ItemType.ARMOR, id))))}");
                Console.WriteLine($"  Weight: weapons {weaponWeight:0.##} + armor {selectedArmor.Weight:0.##} / {totalWeight:0.##} = {100*(weaponWeight+selectedArmor.Weight)/totalWeight:0.##}%");
                for (int j = 0; j < 4; j++)
                {
                    if ((int)row[armorSlots[j]].Value != -1)
                    {
                        row[armorSlots[j]].Value = selectedArmor.Ids[j];
                    }
                }

                if (cheat)
                {
                    PARAM         reinforce       = game.Param("ReinforceParamWeapon");
                    HashSet <int> reinforceLevels = new HashSet <int>(reinforce.Rows.Select(r => (int)r.ID));
                    foreach (string wep in weaponSlots)
                    {
                        int id = (int)row[wep].Value;
                        if (id != -1)
                        {
                            id = id - (id % 100);
                            PARAM.Row item        = game.Item(new ItemKey(ItemType.WEAPON, id));
                            int       reinforceId = (short)item["reinforceTypeId"].Value;
                            while (reinforceLevels.Contains(reinforceId + 5))
                            {
                                reinforceId += 5;
                                id          += 5;
                            }
                            row[wep].Value = id;
                        }
                    }
                }
            }
            // Now, have fun with NPCs
            Dictionary <string, ArmorSet>      npcArmors  = new Dictionary <string, ArmorSet>();
            Func <ItemType, PARAM.Cell, float> cellWeight = (type, cell) =>
            {
                int id = (int)cell.Value;
                if (id == -1)
                {
                    return(0);
                }
                ItemKey key = new ItemKey(type, id);
                if (!weights.ContainsKey(key))
                {
                    return(0);
                }
                return(weights[key]);
            };

            foreach (PARAM.Row row in chara.Rows.Where(r => r.ID > startId + 10))
            {
                string name = game.CharacterName((int)row.ID);
                if (name == "?CHARACTER?")
                {
                    continue;
                }
                ArmorSet selectedArmor;
                if (!npcArmors.ContainsKey(name))
                {
                    float weaponWeight = weaponSlots.Select(slot => cellWeight(ItemType.WEAPON, row[slot])).Sum();
                    float armorWeight  = armorSlots.Select(slot => cellWeight(ItemType.ARMOR, row[slot])).Sum();
                    float weightLimit  = weaponWeight + armorWeight;
                    float totalWeight  = 40 + (sbyte)row["baseDurability"].Value;
                    int   armorLimit   = armors.FindIndex(armor => armor.Weight + weaponWeight > weightLimit);
                    if (armorLimit == -1)
                    {
                        armorLimit = armors.Count - 1;
                    }
                    armorLimit    = Math.Min(20, armorLimit);
                    selectedArmor = npcArmors[name] = armors[random.Next(armorLimit)];
                    armors.Remove(selectedArmor);
                    Console.WriteLine($"Armor for {name}: {100 * weightLimit / totalWeight:0.##}% -> {100 * (selectedArmor.Weight + weaponWeight) / totalWeight:0.##}%: {string.Join(", ", selectedArmor.Ids.Select(id => game.Name(new ItemKey(ItemType.ARMOR, id))))}");
                }
                selectedArmor = npcArmors[name];
                for (int j = 0; j < 4; j++)
                {
                    if ((int)row[armorSlots[j]].Value != -1)
                    {
                        row[armorSlots[j]].Value = selectedArmor.Ids[j];
                    }
                }
            }
        }
    }