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);
                }
            }
        }
Example #2
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}");
                }
            }
        }