Пример #1
0
        public void Randomize(RandomizerOptions options, Action <string> notify = null, string outPath = null, bool sekiro = false, Preset preset = null, bool encrypted = true)
        {
            // sekiro = false;
            string distDir = sekiro ? "dists" : "dist";

            if (!Directory.Exists(distDir))
            {
                // From Release/Debug dirs
                distDir = $@"..\..\..\{distDir}";
            }
            if (!Directory.Exists(distDir))
            {
                throw new Exception("Missing data directory");
            }
            if (outPath == null)
            {
                outPath = Directory.GetCurrentDirectory();
            }

            Console.WriteLine($"Options and seed: {options}");
            Console.WriteLine();
            int seed = (int)options.Seed;

            notify?.Invoke("Loading game data");
            string modDir = null;

            if (options["mergemods"])
            {
                string        modPath    = sekiro ? "mods" : "mod";
                DirectoryInfo modDirInfo = new DirectoryInfo($@"{outPath}\..\{modPath}");
                if (!modDirInfo.Exists)
                {
                    throw new Exception($"Can't merge mods: {modDirInfo.FullName} not found");
                }
                modDir = modDirInfo.FullName;
                if (new DirectoryInfo(outPath).FullName == modDir)
                {
                    throw new Exception($"Can't merge mods: already running from 'mods' directory");
                }
            }
            GameData game = new GameData(distDir, sekiro);

            game.Load(modDir);
            // game.SearchParamInt(15200090); return;
            if (modDir != null)
            {
                Console.WriteLine();
            }

            // Prologue
            if (options["enemy"])
            {
                Console.WriteLine("Ctrl+F 'Boss placements' or 'Miniboss placements' or 'Basic placements' to see enemy placements.");
            }
            if (options["item"] || !sekiro)
            {
                Console.WriteLine("Ctrl+F 'Hints' to see item placement hints, or Ctrl+F for a specific item name.");
            }
            Console.WriteLine();
#if !DEBUG
            for (int i = 0; i < 50; i++)
            {
                Console.WriteLine();
            }
#endif

            // Slightly different high-level algorithm for each game. As always, can try to merge more in the future.
            if (sekiro)
            {
                Events      events = new Events(@"dists\Base\sekiro-common.emedf.json");
                EventConfig eventConfig;
                using (var reader = File.OpenText("dists/Base/events.txt"))
                {
                    IDeserializer deserializer = new DeserializerBuilder().Build();
                    eventConfig = deserializer.Deserialize <EventConfig>(reader);
                }

                EnemyLocations locations = null;
                if (options["enemy"])
                {
                    notify?.Invoke("Randomizing enemies");
                    locations = new EnemyRandomizer(game, events, eventConfig).Run(options, preset);
                    if (!options["enemytoitem"])
                    {
                        locations = null;
                    }
                }
                if (options["item"])
                {
                    notify?.Invoke("Randomizing items");
                    SekiroLocationDataScraper scraper = new SekiroLocationDataScraper();
                    LocationData   data = scraper.FindItems(game);
                    AnnotationData anns = new AnnotationData(game, data);
                    anns.Load(options);
                    anns.AddEnemyLocations(locations);

                    SkillSplitter.Assignment split = null;
                    if (!options["norandom_skills"] && options["splitskills"])
                    {
                        split = new SkillSplitter(game, data, anns, events).SplitAll();
                    }

                    Permutation perm = new Permutation(game, data, anns, explain: false);
                    perm.Logic(new Random(seed), options, preset);

                    notify?.Invoke("Editing game files");
                    PermutationWriter write = new PermutationWriter(game, data, anns, events, eventConfig);
                    write.Write(new Random(seed + 1), perm, options);
                    if (!options["norandom_skills"])
                    {
                        SkillWriter skills = new SkillWriter(game, data, anns);
                        skills.RandomizeTrees(new Random(seed + 2), perm, split);
                    }
                    if (options["edittext"])
                    {
                        HintWriter hints = new HintWriter(game, data, anns);
                        hints.Write(options, perm);
                    }
                }
                MiscSetup.SekiroCommonPass(game, events, options);

                notify?.Invoke("Writing game files");
                if (!options["dryrun"])
                {
                    game.SaveSekiro(outPath);
                }
                return;
            }
            else
            {
                Events events = new Events(@"dist\Base\ds3-common.emedf.json");

                LocationDataScraper scraper = new LocationDataScraper(logUnused: false);
                LocationData        data    = scraper.FindItems(game);
                AnnotationData      ann     = new AnnotationData(game, data);
                ann.Load(options);
                ann.AddSpecialItems();

                notify?.Invoke("Randomizing");
                Random      random      = new Random(seed);
                Permutation permutation = new Permutation(game, data, ann, explain: false);
                permutation.Logic(random, options, null);

                notify?.Invoke("Editing game files");
                random = new Random(seed + 1);
                PermutationWriter writer = new PermutationWriter(game, data, ann, events, null);
                writer.Write(random, permutation, options);
                random = new Random(seed + 2);
                CharacterWriter characters = new CharacterWriter(game, data);
                characters.Write(random, options);

                notify?.Invoke("Writing game files");
                if (!options["dryrun"])
                {
                    game.SaveDS3(outPath, encrypted);
                }
            }
        }
        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 CharacterWriter(GameData game, LocationData data)
 {
     this.game = game;
     this.data = data;
 }
Пример #4
0
        public KeyItemsPermutation(RandomizerOptions options, LocationData data, AnnotationData ann, EnemyLocations enemies, bool explain)
        {
            this.data    = data;
            this.ann     = ann;
            this.explain = explain;

            Dictionary <string, bool> config      = ann.GetConfig(options.GetLogicOptions());
            Dictionary <string, Expr> configExprs = config.ToDictionary(e => e.Key, e => e.Value ? Expr.TRUE : Expr.FALSE);

            Dictionary <LocationScope, (UniqueCategory, int)>      counts     = ann.GetUniqueCounts();
            Dictionary <string, Dictionary <UniqueCategory, int> > areaCounts = ann.AllAreas.ToDictionary(e => e.Key, e =>
            {
                Dictionary <UniqueCategory, int> dict = Node.EmptyCounts();
                foreach (LocationScope scope in e.Value.Where(s => counts.ContainsKey(s)))
                {
                    (UniqueCategory cat, int count) = counts[scope];
                    dict[cat] += count;
                }
                return(dict);
            });

            Dictionary <string, string> equivalentGraph = new Dictionary <string, string>();

            void processDependencies(AreaAnnotation area, ISet <string> frees, bool assignItems)
            {
                string           name           = area.Name;
                HashSet <string> dependentAreas = new HashSet <string>();
                bool             other          = false;

                foreach (string free in frees)
                {
                    if (ann.Items.ContainsKey(free))
                    {
                        items.Add(free);
                        if (assignItems)
                        {
                            if (itemEvents.ContainsKey(free))
                            {
                                throw new Exception($"Internal error: {free} activates multiple events");
                            }
                            AddMulti(itemEvents, free, name);
                            if (area.AlwaysBefore != null)
                            {
                                AddMulti(itemEvents, free, area.AlwaysBefore);
                            }
                        }
                        other = true;
                    }
                    else if (ann.Areas.ContainsKey(free))
                    {
                        dependentAreas.Add(free);
                    }
                    else if (ann.Events.ContainsKey(free))
                    {
                        if (ann.EventAreas.TryGetValue(free, out string evArea))
                        {
                            dependentAreas.Add(evArea);
                        }
                        else
                        {
                            other = true;
                        }
                    }
                    else
                    {
                        throw new Exception($"Internal error: Unknown dependency {free} in requirements for {area.Name}");
                    }
                }
                if (dependentAreas.Count == 1 && !other)
                {
                    equivalentGraph[name] = dependentAreas.First();
                    if (explain)
                    {
                        Console.WriteLine($"Collapsed events for key item generation: {name} -> {frees.First()}");
                    }
                }
                // This is used for equivalence graph things. Should probably use this information in weight groups instead of actually combining the areas
                AddMulti(combinedAreas, name, name);
                // Weight base is used to specify that a key item, if placed in the base area, should also apply to this other area.
                AddMulti(combinedWeights, name, name);
                if (area.WeightBase != null)
                {
                    AddMulti(combinedWeights, area.WeightBase, name);
                }
            }

            foreach (AreaAnnotation ev in ann.Events.Values)
            {
                string name = ev.Name;
                Expr   req  = ev.ReqExpr.Substitute(configExprs).Simplify();
                if (req.IsFalse())
                {
                    // Can happen with DLC
                    unusedAreas.Add(ev.Name);
                    continue;
                }
                processDependencies(ev, req.FreeVars(), true);
                // Events are not dynamically placed anywhere, nor is anything placed inside of them, so they are always added to the graph upfront
                nodes[name] = new Node
                {
                    Name        = name,
                    Req         = req,
                    Counts      = Node.EmptyCounts(),
                    CumKeyCount = -1,
                };
            }
            foreach (AreaAnnotation area in ann.Areas.Values)
            {
                string name = area.Name;
                Expr   req  = area.ReqExpr.Substitute(configExprs).Simplify();
                if (req.IsFalse())
                {
                    // Can happen with DLC
                    unusedAreas.Add(area.Name);
                    continue;
                }
                // Proper aliases are already represented using the BaseArea slot property, skip those
                if (ann.AreaAliases[name] != name)
                {
                    continue;
                }
                processDependencies(area, req.FreeVars(), false);
                // This is where we used to skip combined areas in DS3, but now weight bases are added automatically
                nodes[name] = new Node
                {
                    Name        = name,
                    Counts      = areaCounts[name],
                    Req         = req,
                    Weight      = 1,
                    CumKeyCount = -1
                };
                areas.Add(name);
            }
            // Quick collapse of equivalence graph
            Dictionary <string, string> equivalent = new Dictionary <string, string>();

            string getBaseName(string name)
            {
                if (equivalent.ContainsKey(name))
                {
                    return(equivalent[name]);
                }
                else if (equivalentGraph.ContainsKey(name))
                {
                    string root = getBaseName(equivalentGraph[name]);
                    equivalent[name] = root;
                    AddMulti(combinedAreas, root, name);
                    return(root);
                }
                else
                {
                    return(name);
                }
            };
            foreach (KeyValuePair <string, string> equivalence in equivalentGraph)
            {
                getBaseName(equivalence.Key);
            }
            // TODO: Remove combinedAreas
            foreach (KeyValuePair <string, List <string> > entry in combinedAreas)
            {
                foreach (string alias in entry.Value)
                {
                    if (alias != entry.Key)
                    {
                        AddMulti(combinedWeights, entry.Key, alias);
                    }
                }
            }
            foreach (KeyValuePair <string, HashSet <string> > entry in combinedWeights.Where(e => e.Value.Count > 1).ToList())
            {
                foreach (string sharedArea in entry.Value.ToList())
                {
                    entry.Value.UnionWith(combinedWeights[sharedArea]);
                    combinedWeights[sharedArea] = entry.Value;
                }
            }
            if (explain)
            {
                HashSet <string> explained = new HashSet <string>();
                foreach (KeyValuePair <string, HashSet <string> > entry in combinedWeights)
                {
                    if (explained.Contains(entry.Key))
                    {
                        continue;
                    }
                    Console.WriteLine($"Combined group: [{string.Join(",", entry.Value)}]");
                    explained.UnionWith(entry.Value);
                }
            }

            // TODO: Make the dictionaries less of a slog to produce
            combinedAreas = combinedWeights.ToDictionary(e => e.Key, e => e.Value.ToList());

            // Last step - calculate rough measures of area difficulty, in terms of minimal number of items required for the area
            int getCumulativeCounts(string name)
            {
                Node node = nodes[name];

                if (node.CumKeyCount != -1)
                {
                    return(node.KeyCount + node.CumKeyCount);
                }
                List <string> deps  = node.Req.FreeVars().Where(free => areas.Contains(free) || ann.Events.ContainsKey(free)).ToList();
                int           count = deps.Select(free => getCumulativeCounts(free)).DefaultIfEmpty().Max();

                node.CumKeyCount = count;
                return(node.KeyCount + count);
            };
            foreach (Node node in nodes.Values)
            {
                if (unusedAreas.Contains(node.Name))
                {
                    continue;
                }
                getCumulativeCounts(node.Name);
                if (explain)
                {
                    Console.WriteLine($"{node.Name} ({node.Counts[UniqueCategory.KEY_SHOP]} shop / {node.KeyCount} area / ({node.Counts[UniqueCategory.QUEST_LOT]} quest / {node.CumKeyCount} cumulative): {node.Req}");
                }
            }
        }
Пример #5
0
 public SkillWriter(GameData game, LocationData data, AnnotationData ann)
 {
     this.game = game;
     this.data = data;
     this.ann  = ann;
 }