public static RandomizerOptions Parse(IEnumerable <string> args, bool Sekiro, Predicate <string> optionsFilter = null)
        {
            RandomizerOptions options = new RandomizerOptions(Sekiro);
            uint          seed        = 0;
            uint          seed2       = 0;
            int           difficulty  = 0;
            List <string> preset      = new List <string>();
            string        op          = null;
            int           numIndex    = 0;

            foreach (string arg in args)
            {
                if (arg == "--preset")
                {
                    op = "preset";
                    continue;
                }
                else if (arg.StartsWith("--"))
                {
                    op = null;
                }
                if (op == "preset")
                {
                    preset.Add(arg);
                }
                else if (uint.TryParse(arg, out uint num))
                {
                    if (numIndex == 0)
                    {
                        difficulty = (int)num;
                    }
                    else if (numIndex == 1)
                    {
                        seed = num;
                    }
                    else if (numIndex == 2)
                    {
                        seed2 = num;
                    }
                    numIndex++;
                }
                else
                {
                    if (optionsFilter != null && !optionsFilter(arg))
                    {
                        continue;
                    }
                    options[arg] = true;
                }
            }
            options.Difficulty = difficulty;
            options.Seed       = seed;
            options.Seed2      = seed2;
            if (preset.Count > 0)
            {
                options.Preset = string.Join(" ", preset);
            }
            return(options);
        }
Exemplo n.º 2
0
        public SekiroForm()
        {
            InitializeComponent();
            // One-time initialization for errors and things
            if (!MiscSetup.CheckRequiredSekiroFiles(out string req))
            {
                SetError(req, true);
            }
            else
            {
                SetWarning();
            }
            SetStatus(null);
            presetL.Text         = "";
            enemyseed.GotFocus  += enemyseed_TextChanged;
            enemyseed.LostFocus += enemyseed_TextChanged;

            // The rest of initialization
            RandomizerOptions initialOpts = new RandomizerOptions(true);

            SetControlFlags(this, initialOpts);
            defaultOpts = initialOpts.FullString();

            string existingOpts = Settings.Default.Options;

            if (string.IsNullOrWhiteSpace(existingOpts))
            {
                options.Difficulty = difficulty.Value;
                SetControlFlags(this);
                // Seed and checkboxes will be updated in UpdateEnabled
            }
            else
            {
                SetOptionsString(existingOpts);
                if (options.Seed != 0)
                {
                    defaultReroll.Enabled = true;
                    defaultReroll.Checked = false;
                }
                // Enemy seed and checkboxes will be updated in UpdateEnabled
            }
            SetStatus(null);

            UpdateEnabled();
            UpdateLabels();

            void parentImage(PictureBox child)
            {
                child.Location = new Point(child.Location.X - title.Location.X, child.Location.Y - title.Location.Y);
                child.Parent   = title;
            }

            parentImage(mascot);
            parentImage(itemPic);
            parentImage(catPic);
            RefreshImage();
        }
Exemplo n.º 3
0
        public MainForm()
        {
            InitializeComponent();
            initialColor = BackColor;

            if (!MiscSetup.CheckRequiredDS3Files(out string req))
            {
                SetError(req, true);
            }
            else
            {
                SetWarning();
            }
            SetStatus(null);

            // The rest of initialization
            RandomizerOptions initialOpts = new RandomizerOptions(false);

            SetControlFlags(this, initialOpts);
            defaultOpts = initialOpts.FullString();

            string existingOpts = Settings.Default.Options;

            if (string.IsNullOrWhiteSpace(existingOpts))
            {
                options.Difficulty = difficulty.Value;
                SetControlFlags(this);
            }
            else
            {
                SetOptionsString(existingOpts);
                if (options.Seed != 0)
                {
                    defaultReroll.Enabled = true;
                    defaultReroll.Checked = false;
                }
            }
            SetStatus(null);

            UpdateEnabled();
            UpdateLabels();
        }
Exemplo n.º 4
0
        private void SetControlFlags(Control control, RandomizerOptions customOpt = null)
        {
            RandomizerOptions getOpt = customOpt ?? options;

            if (control is RadioButton radio)
            {
                getOpt[control.Name] = radio.Checked;
            }
            else if (control is CheckBox check)
            {
                getOpt[control.Name] = check.Checked;
            }
            else
            {
                foreach (Control sub in control.Controls)
                {
                    SetControlFlags(sub, customOpt);
                }
            }
        }
Exemplo n.º 5
0
        public static void SekiroCommonPass(GameData game, Events events, RandomizerOptions opt)
        {
            Dictionary <string, PARAM> Params = game.Params;

            // Snap (for convenience, but can also softlock the player)
            if (opt["snap"])
            {
                Params["EquipParamGoods"][3980]["goodsUseAnim"].Value = (sbyte)84;
            }
            // No tutorials
            if (Params.ContainsKey("MenuTutorialParam"))
            {
                Params["MenuTutorialParam"].Rows = Params["MenuTutorialParam"].Rows.Where(r => r.ID == 0).ToList();
            }
            // Memos pop up and don't just disappear mysteriously
            Params["EquipParamGoods"][9221]["Unk20"].Value = (byte)6;
            Params["EquipParamGoods"][9223]["Unk20"].Value = (byte)6;
            Params["EquipParamGoods"][9225]["Unk20"].Value = (byte)6;

            // These are just always deleted
            HashSet <string> deleteCommands = new HashSet <string>
            {
                "Show Tutorial Text", "Show Hint Box", "Show Small Hint Box", "Award Achievement"
            };
            HashSet <int> deleteEvents = new HashSet <int>
            {
                // Putting away sword in areas
                20006200,
            };

            // Slowless slow walk
            if (opt["headlesswalk"])
            {
                deleteEvents.Add(20005431);
            }

            foreach (KeyValuePair <string, EMEVD> entry in game.Emevds)
            {
                foreach (EMEVD.Event e in entry.Value.Events)
                {
                    bool commonInit  = entry.Key == "common" && e.ID == 0;
                    int  maxPermSlot = 0;
                    for (int i = 0; i < e.Instructions.Count; i++)
                    {
                        Instr instr  = events.Parse(e.Instructions[i]);
                        bool  delete = false;
                        if (instr.Init)
                        {
                            if (deleteEvents.Contains(instr.Callee))
                            {
                                delete = true;
                            }
                            else if (commonInit && instr.Callee == 750 && instr.Offset == 2)
                            {
                                maxPermSlot = Math.Max(maxPermSlot, (int)instr[0]);
                            }
                        }
                        else
                        {
                            if (deleteCommands.Contains(instr.Name))
                            {
                                delete = true;
                            }
                        }
                        if (delete)
                        {
                            EMEVD.Instruction newInstr = new EMEVD.Instruction(1014, 69);
                            e.Instructions[i] = newInstr;
                            // Just in case...
                            e.Parameters = e.Parameters.Where(p => p.InstructionIndex != i).ToList();
                        }
                    }
                    // Add permanent shop placement flags. Also.... abuse this for headless ape bestowal lot, if enemy rando is enabled.
                    if (opt["enemy"] && opt["bosses"] && commonInit)
                    {
                        entry.Value.Events[0].Instructions.Add(new EMEVD.Instruction(2000, 0, new List <object> {
                            maxPermSlot + 1, (uint)750, (uint)9307, (uint)9314
                        }));
                    }
                }
            }
        }
Exemplo n.º 6
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);
                }
            }
        }
Exemplo n.º 7
0
        private void SetOptionsString(string defaultOpts)
        {
            HashSet <string> validOptions = new HashSet <string>();

            GetAllControlNames(this, validOptions);
            bool isValidOption(string s)
            {
                if (validOptions.Contains(s))
                {
                    return(true);
                }
                if (uint.TryParse(s, out var ignored))
                {
                    return(true);
                }
                return(false);
            }

            previousOpts = new HashSet <string>(defaultOpts.Split(' '));
            options      = RandomizerOptions.Parse(previousOpts, true, isValidOption);

            // New defaults
            if (previousOpts.Contains("v1"))
            {
                options["veryearlyhirata"] = true;
                options["openstart"]       = true;
            }
            if (previousOpts.Contains("v1") || previousOpts.Contains("v2"))
            {
                options["scale"]    = true;
                options["edittext"] = true;
            }

            simultaneousUpdate = true;
            InsertControlFlags(this);
            difficulty.Value = options.Difficulty;
            if (!enemytoitem.Checked)
            {
                defaultAllowReroll.Checked = true;
            }
            simultaneousUpdate = false;

            fixedseed.Text = options.Seed == 0 ? "" : $"{options.Seed}";
            enemyseed.Text = options.Seed2 == 0 || options.Seed == options.Seed2 ? "" : $"{options.Seed2}";
            // Also need to set enemy seed. This may be done by enemyseed text change handler?

            if (options.Preset == null)
            {
                SetPreset(null);
            }
            else
            {
                try
                {
                    Preset preset = LoadPreset(options.Preset, extractOopsAll: true);
                    SetPreset(preset);
                }
                catch (Exception e)
                {
                    Console.WriteLine(e);
                    SetPreset(null);
                }
            }
        }
Exemplo n.º 8
0
        private async void randomize_Click(object sender, EventArgs e)
        {
            if (working)
            {
                return;
            }
            SetWarning();
            Random seedRandom = new Random();

            if (!defaultReroll.Checked && fixedseed.Text.Trim() != "")
            {
                if (uint.TryParse(fixedseed.Text.Trim(), out uint seed))
                {
                    options.Seed = seed;
                }
                else
                {
                    SetStatus("Invalid fixed seed", true);
                    return;
                }
            }
            else
            {
                options.Seed = (uint)seedRandom.Next();
            }
            bool newEnemySeed = false;

            if (!defaultRerollEnemy.Checked && enemyseed.Text.Trim() != "" && enemyseed.Text != enemySeedPlaceholder)
            {
                if (uint.TryParse(enemyseed.Text.Trim(), out uint seed))
                {
                    options.Seed2 = seed;
                }
                else
                {
                    SetStatus("Invalid enemy seed", true);
                    return;
                }
            }
            // TODO: What is exact condition to use for when rerolling should be allowed? What is condition for enemySeedPlaceholder about to be set?
            else if (defaultRerollEnemy.Checked && defaultRerollEnemy.Enabled)
            {
                options.Seed2 = (uint)seedRandom.Next();
                newEnemySeed  = true;
            }
            else
            {
                options.Seed2 = 0;
            }
            SaveOptions();
            RandomizerOptions rand = options.Copy();

            working = true;
            string buttonText = randomize.Text;

            randomize.Text      = $"Running...";
            randomize.BackColor = Color.LightYellow;
            fixedseed.Text      = rand.Seed.ToString();
            if (newEnemySeed)
            {
                enemyseed.Text      = rand.Seed2.ToString();
                enemyseed.ForeColor = SystemColors.WindowText;
            }

            bool       success    = false;
            Randomizer randomizer = new Randomizer();
            await Task.Factory.StartNew(() => {
                Directory.CreateDirectory("spoiler_logs");
                string seed2      = rand.Seed2 == 0 || rand.Seed2 == rand.Seed ? "" : $"_{rand.Seed2}";
                string runId      = $"{DateTime.Now.ToString("yyyy-MM-dd_HH.mm.ss")}_log_{rand.Seed}{seed2}_{rand.ConfigHash()}.txt";
                TextWriter log    = File.CreateText($@"spoiler_logs\{runId}");
                TextWriter stdout = Console.Out;
                Console.SetOut(log);
                try
                {
                    randomizer.Randomize(rand, status => { SetStatus(status); }, sekiro: true, preset: selectedPreset);
                    SetStatus($"Done! Hints and spoilers in spoiler_logs directory as {runId} - Restart your game!!", success: true);
                    success = true;
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                    SetError($"Error encountered: {ex.Message}\r\nIt may work to try again with a different seed. See most recent file in spoiler_logs directory for the full error.");
                    SetStatus($"Error! Partial log in spoiler_logs directory as {runId}", true);
                }
                finally
                {
                    log.Close();
                    Console.SetOut(stdout);
                }
            });

            randomize.Text      = buttonText;
            randomize.BackColor = SystemColors.Control;
            working             = false;
            if (success)
            {
                RefreshImage();
            }
        }
Exemplo n.º 9
0
        public Assignment AssignItems(Random random, RandomizerOptions options, Preset preset)
        {
            List <string> itemOrder = new List <string>(items);

            // Right now, assign key items in a random order, with endgame items last.
            // We will get more devious runs from assigning later items later, may be worth looking into, especially for game with clear phases like Sekiro has.
            Shuffle(random, itemOrder);
            bool isEndgame(string i) => i.StartsWith("cinder") || i == "secretpassagekey";

            itemOrder = itemOrder.OrderBy(i => isEndgame(i) ? 1 : 0).ToList();

            // In race mode, always put shinobiprosthetic and younglordsbellcharm first, since there is only one ashinaoutskirts_template spot for key item placement logic
            if (ann.RaceModeItems.Count > 0)
            {
                itemOrder = itemOrder.OrderBy(i => i == "shinobiprosthetic" || i == "younglordsbellcharm" ? 0 : 1).ToList();
            }

            Assignment ret = new Assignment();

            // Find which events are required to access other things, and do not give them as much weight as dead ends with placement biases
            foreach (Node node in nodes.Values)
            {
                if (node.Req != null)
                {
                    ret.RequiredEvents.UnionWith(node.Req.FreeVars().Where(v => ann.Events.ContainsKey(v)));
                }
            }
            // First assign ashes for key item placement. Other quest assignments can happen later.
            // TODO: Make ashes its own system, rather than abusing quest system.
            if (ann.ItemGroups.ContainsKey("ashes"))
            {
                List <string> ashesOrder = new List <string>(areas);
                ashesOrder = WeightedShuffle(random, ashesOrder, loc => Math.Min(nodes[loc].KeyCount - nodes[loc].Counts[UniqueCategory.KEY_SHOP], 3));
                int ashesIndex = 0;
                foreach (KeyValuePair <LocationScope, SlotAnnotation> entry in ann.Slots)
                {
                    LocationScope    scope = entry.Key;
                    SlotAnnotation   slot  = entry.Value;
                    HashSet <string> tags  = slot.GetTags();
                    // Unique item unlocks other unique items unconditionally - can add a location for key item. Mainly for ashes.
                    if (slot.QuestReqs != null && !slot.HasAnyTags(ann.NoKeyTags) && scope.UniqueId > 0 && slot.ItemReqs.Count == 1)
                    {
                        ItemKey key = slot.ItemReqs[0];
                        if (ret.Assign.ContainsKey(key))
                        {
                            throw new Exception($"Multiple assignments for {slot.QuestReqs}");
                        }
                        string selected = ashesOrder[ashesIndex++];
                        if (explain)
                        {
                            Console.WriteLine($"Assigning key quest item {slot.QuestReqs} to {selected}");
                        }
                        int ashesCount = data.Location(scope).Count;
                        nodes[selected].AddShopCapacity(false, ashesCount);
                        nodes[selected].AddItem(false, false);
                        ret.Assign[key] = new HashSet <string> {
                            selected
                        };
                        if (ann.AreaEvents.TryGetValue(selected, out List <string> events))
                        {
                            ret.Assign[key].UnionWith(events);
                        }
                    }
                }
            }
            if (explain)
            {
                int raceModeCount = 0;
                foreach (string area in areas)
                {
                    int rmode = nodes[area].Count(false, true);
                    if (rmode > 0)
                    {
                        Console.WriteLine($"RACEMODE {area}: {rmode}");
                        raceModeCount += rmode;
                    }
                }
                Console.WriteLine($"TOTAL: {raceModeCount}");
            }

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

            if (options["norandom"])
            {
                foreach (string item in itemOrder)
                {
                    ItemKey itemKey = ann.Items[item];
                    // Some hardcoding for Path of the Dragon which is not an actual item
                    if (item == "pathofthedragon")
                    {
                        forcemap[item] = "highwall_garden";
                    }
                    else
                    {
                        ItemLocation   loc = data.Data[itemKey].Locations.Values.First();
                        SlotAnnotation sn  = ann.Slot(loc.LocScope);
                        forcemap[item] = sn.Area;
                    }
                }
            }
            else if (preset?.Items != null)
            {
                foreach (KeyValuePair <string, string> entry in preset.Items)
                {
                    forcemap[entry.Key] = entry.Value;
                }
            }

            // Assign key items
            bool  debugChoices             = false;
            float scaling                  = options.GetNum("keyitemchainweight");
            Dictionary <string, Expr> reqs = CollapseReqs();

            foreach (string item in itemOrder)
            {
                List <string> allowedAreas = areas.Where(a => !reqs[a].Needs(item)).ToList();
                if (debugChoices)
                {
                    Console.WriteLine($"\n> {item} not allowed in areas: {string.Join(",", areas.Where(a => !allowedAreas.Contains(a)))}");
                }
                bool             redundant      = allowedAreas.Count == areas.Count;
                HashSet <string> neededForEvent = itemEvents.TryGetValue(item, out HashSet <string> ev) ? ev : null;
                allowedAreas.RemoveAll(a => ann.Areas[a].Until != null && (neededForEvent == null || !neededForEvent.Contains(ann.Areas[a].Until)));

                Dictionary <string, float> specialScaling = new Dictionary <string, float>();
                if (item == "smalllothricbanner")
                {
                    specialScaling = allowedAreas.Where(area => ann.Areas[area].BoringKeyItem).ToDictionary(area => area, _ => 1 / scaling);
                }
                // A bit hacky, but prevent Small Lothric Banner from being so prevalent in Firelink Shrine
                Dictionary <string, float> weights = allowedAreas.ToDictionary(area => area, area => Weight(area) * (specialScaling.TryGetValue(area, out float sp) ? sp : 1));

                if (debugChoices)
                {
                    Console.WriteLine($"> Choices for {item}: " + string.Join(", ", allowedAreas.OrderBy(a => weights[a]).Select(a => $"{a} {weights[a]}")));
                }
                string selected = WeightedChoice(random, allowedAreas, a => weights[a]);

                if (forcemap.TryGetValue(item, out string forced))
                {
                    if (explain && !allowedAreas.Contains(forced))
                    {
                        Console.WriteLine($"Key item {item} put in non-random location {forced} which isn't normally allowed by logic");
                    }
                    selected = forced;
                }
                AddItem(item, selected, forced != null);

                ItemKey itemKey = ann.Items[item];
                ret.Priority.Add(itemKey);
                ret.Assign[itemKey] = new HashSet <string> {
                    selected
                };
                // Areas should include events there. Except for bell charm being dropped by chained ogre, if that option is enabled
                // todo: check this works okay with racemode key items, and nothing else randomized.
                if (!(item == "younglordsbellcharm" && options["earlyhirata"]))
                {
                    if (ann.AreaEvents.TryGetValue(selected, out List <string> events))
                    {
                        ret.Assign[itemKey].UnionWith(events);
                    }
                }
                if (explain || debugChoices)
                {
                    Console.WriteLine($"Adding {item} to {string.Join(",", ret.Assign[itemKey])}");
                }

                // Update weights
                reqs = CollapseReqs();
                // If item was not really needed, don't update weighhts
                if (redundant)
                {
                    if (explain)
                    {
                        Console.WriteLine($"{item} is redundant to another key item (does not uniquely make new areas available)");
                    }
                    continue;
                }
                // Heuristic which forms chains and spreads items across areas
                // Reduce weight for this area, and increase weight for areas which depend on the item
                AdjustWeight(selected, 1 / scaling);
                HashSet <string> addedAreas = new HashSet <string>();
                foreach (string area in areas)
                {
                    if (addedAreas.Contains(combinedWeights[area].First()))
                    {
                        continue;
                    }
                    if (reqs[area].Needs(item))
                    {
                        AdjustWeight(area, scaling);
                        addedAreas.Add(combinedWeights[area].First());
                    }
                }
            }
            // The last placed item has the highest priority
            ret.Priority.Reverse();

            // Now that all key items have been assigned, determine which areas are blocked by other areas.
            // This is used to determine lateness within the game (by # of items encountered up to that point).
            HashSet <string> getIncludedAreas(string name, List <string> path)
            {
                path = path.Concat(new[] { name }).ToList();
                if (!nodes.TryGetValue(name, out Node node))
                {
                    throw new Exception($"Bad options: no way to access area \"{name}\"");
                }
                if (ret.IncludedAreas.ContainsKey(name))
                {
                    if (ret.IncludedAreas[name] == null)
                    {
                        throw new Exception($"Loop from {name} to {node.Req} - path {string.Join(",", path)}");
                    }
                    return(ret.IncludedAreas[name]);
                }
                ret.IncludedAreas[name] = null;
                HashSet <string> result = new HashSet <string>();

                // Add all nodes for now, but remove items later
                if (areas.Contains(name) || ann.Events.ContainsKey(name) || ann.Items.ContainsKey(name))
                {
                    result.Add(name);
                }
                foreach (string free in node.Req.FreeVars())
                {
                    if (!(loops.ContainsKey(name) && loops[name].Contains(free)))
                    {
                        result.UnionWith(getIncludedAreas(free, path));
                    }
                }
                ret.IncludedAreas[name] = result;
                return(result);
            };
            foreach (Node node in nodes.Values)
            {
                getIncludedAreas(node.Name, new List <string>());
                // Redefine weights for quest selection
                node.Weight = 1;
                if (areas.Contains(node.Name))
                {
                    node.CumKeyCount = ret.IncludedAreas[node.Name].Where(n => nodes[n].Counts != null).Select(n => nodes[n].Count(true, true)).Sum();
                    if (explain && false)
                    {
                        Console.WriteLine($"Quest area {node.Name}: {node.Count(true, true)}/{node.CumKeyCount}: {string.Join(",", ret.IncludedAreas[node.Name])}");
                    }
                }
            }

            // Find out which items are required to access the ending, while items are still included in the graph.
            // For hints later.
            // Also do this in DS3 after deciding where to add hints.
            if (ret.IncludedAreas.TryGetValue("ashinareservoir_end", out HashSet <string> requiredForEnd))
            {
                List <ItemKey> requiredItems = requiredForEnd.Where(t => ann.Items.ContainsKey(t)).Select(t => ann.Items[t]).ToList();
                ret.NotRequiredKeyItems.UnionWith(ret.Priority.Except(requiredItems));
            }

            // The above DFS adds both items and areas together, so remove the items.
            foreach (string key in ret.IncludedAreas.Keys.ToList())
            {
                if (ann.Items.ContainsKey(key))
                {
                    ret.IncludedAreas.Remove(key);
                }
                else
                {
                    ret.IncludedAreas[key].RemoveWhere(v => ann.Items.ContainsKey(v));
                }
            }
            foreach (string area in unusedAreas)
            {
                ret.IncludedAreas[area] = new HashSet <string>();
            }

            // Make an area order
            List <string>            areaOrder = areas.OrderBy(a => nodes[a].CumKeyCount).ToList();
            Dictionary <string, int> areaIndex = Enumerable.Range(0, areaOrder.Count()).ToDictionary(i => areaOrder[i], i => i);

            string latestArea(IEnumerable <string> ns)
            {
                return(areaOrder[ns.Select(n => areaIndex.TryGetValue(n, out int i) ? i : throw new Exception($"No order for area {n}")).DefaultIfEmpty().Max()]);
Exemplo n.º 10
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}");
                }
            }
        }
        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];
                    }
                }
            }
        }
    }
Exemplo n.º 12
0
        public void Write(RandomizerOptions opt, Permutation permutation)
        {
            // Overall: 9020, and 9200 to 9228
            // Exclude 9209 and 9215 and 9228 bc it's useful
            // Also, 9221 9223 9225 are 'purchase to read'
            // Can replace everything in quotes
            // Or after the first :\n
            // Or after the last \n\n
            // If the message ends with '               -?Name' can leave that. space or ideographic space (3000). multiple whitespace either way
            //
            // Alternatively: replace individual text, like 'moon-view tower'
            // defeat a named enemy
            // defeat a formidable foe
            // defeat a powerful enemy
            FMG itemDesc  = game.ItemFMGs["アイテム説明"];
            FMG eventText = game.MenuFMGs["イベントテキスト"];

            if (opt["writehints"])
            {
                List <int> eventIds = new List <int>
                {
                    12000000, 12000331, 12000021, 12000261, 12000275, 12000321, 12000285,
                    12000241, 12000011, 12000311, 12000231, 12000291, 12000341,
                };
                eventIds.Sort();
                HintData write = new HintData();
                void createHint(int id, string name, string text)
                {
                    Hint hint = new Hint
                    {
                        ID       = id,
                        Name     = name,
                        Versions = new List <HintTemplate>
                        {
                            new HintTemplate
                            {
                                Type = "default",
                                Text = text.Split('\n').ToList(),
                            }
                        },
                    };

                    write.Hints.Add(hint);
                }

                foreach (KeyValuePair <ItemKey, string> entry in game.Names())
                {
                    ItemKey key = entry.Key;
                    if (!(key.Type == ItemType.GOOD && (key.ID == 9020 || key.ID >= 9200 && key.ID <= 9228)))
                    {
                        continue;
                    }
                    createHint(key.ID, entry.Value, itemDesc[key.ID]);
                }
                foreach (int id in eventIds)
                {
                    createHint(id, null, eventText[id]);
                }
                ISerializer serializer = new SerializerBuilder().DisableAliases().Build();
                using (var writer = File.CreateText("hints.txt"))
                {
                    serializer.Serialize(writer, write);
                }
                return;
            }

            IDeserializer deserializer = new DeserializerBuilder().Build();
            HintData      hints;

            using (var reader = File.OpenText("dists/Base/hints.txt"))
            {
                hints = deserializer.Deserialize <HintData>(reader);
            }

            // Preprocess some items
            Dictionary <HintType, TypeHint>            typeNames    = hints.Types.ToDictionary(e => e.Name, e => e);
            Dictionary <ItemCategory, List <ItemKey> > categories   = ((ItemCategory[])Enum.GetValues(typeof(ItemCategory))).ToDictionary(e => e, e => new List <ItemKey>());
            Dictionary <ItemCategory, string>          categoryText = new Dictionary <ItemCategory, string>();

            foreach (ItemHint cat in hints.ItemCategories)
            {
                if (cat.Includes != null)
                {
                    categories[cat.Name].AddRange(cat.Includes.Split(' ').Select(i => ann.Items.TryGetValue(i, out ItemKey key) ? key : throw new Exception($"Unrecognized name {i}")));
                }
                if (cat.IncludesName != null)
                {
                    categories[cat.Name].AddRange(phraseRe.Split(cat.IncludesName).Select(i => game.ItemForName(i)));
                }
                if (cat.Text != null)
                {
                    categoryText[cat.Name] = cat.Text;
                }
            }
            if (opt["earlyhirata"])
            {
                categories[ItemCategory.ExcludeHints].Add(ann.Items["younglordsbellcharm"]);
            }
            categories[ItemCategory.ExcludeHints].AddRange(permutation.NotRequiredKeyItems);

            // TODO: Exclude non-technically-required items... calculate this in key item permutations
            List <ItemKey> allItems = permutation.KeyItems.ToList();

            if (opt["norandom_skills"])
            {
                categories[ItemCategory.ImportantTool].Clear();
            }
            else
            {
                allItems.AddRange(categories[ItemCategory.ImportantTool]);
            }
            allItems.AddRange(categories[ItemCategory.HintFodder]);

            Dictionary <string, ItemHintName> specialItemNames = hints.ItemNames.ToDictionary(n => n.Name, n => n);

            // Process areas
            Dictionary <string, AreaHint> areas = new Dictionary <string, AreaHint>();
            HashSet <string> gameAreas          = new HashSet <string>(ann.Areas.Keys);

            List <string> getAreasForName(string names)
            {
                List <string> nameList = new List <string>();

                foreach (string name in names.Split(' '))
                {
                    if (name.EndsWith("*"))
                    {
                        string        prefix   = name.Substring(0, name.Length - 1);
                        List <string> matching = gameAreas.Where(n => n.StartsWith(prefix)).ToList();
                        if (matching.Count == 0)
                        {
                            throw new Exception($"Unrecognized area in hint config: {name}");
                        }
                        nameList.AddRange(matching);
                    }
                    else
                    {
                        if (!gameAreas.Contains(name))
                        {
                            throw new Exception($"Unrecognized area in hint config: {name}");
                        }
                        nameList.Add(name);
                    }
                }
                return(nameList);
            }

            foreach (AreaHint area in hints.Areas)
            {
                if (area.Name == null || area.Includes == null)
                {
                    throw new Exception($"Missing data in area hint grouping {area.Name}");
                }
                areas[area.Name] = area;
                area.Areas.UnionWith(getAreasForName(area.Includes));
                if (area.Excludes != null)
                {
                    area.Areas.ExceptWith(getAreasForName(area.Excludes));
                }
                if (area.LaterIncludes != null)
                {
                    area.LaterAreas.UnionWith(getAreasForName(area.LaterIncludes));
                    if (!area.LaterAreas.IsSubsetOf(area.Areas))
                    {
                        throw new Exception($"Error in hint config: later areas of {area.Name} are not a subset of all areas");
                    }
                }
                area.EarlyAreas.UnionWith(area.Areas.Except(area.LaterAreas));
                if (area.Parent != null)
                {
                    if (!areas.TryGetValue(area.Parent, out AreaHint parent))
                    {
                        throw new Exception($"Error in hint config: parent of {area.Name} does not exist: {area.Parent}");
                    }
                    area.Parents.Add(parent);
                    area.Parents.UnionWith(parent.Parents);
                }
                if (area.Present != null)
                {
                    area.Types = area.Present.Split(' ').Select(t => (HintType)Enum.Parse(typeof(HintType), t)).ToList();
                }
            }

            bool printText = opt["hinttext"];

            // Process items to search for
            List <ItemCategory> categoryOverrides = new List <ItemCategory> {
                ItemCategory.RequiredKey, ItemCategory.RequiredAbility, ItemCategory.ImportantTool, ItemCategory.HintFodder
            };
            HashSet <string> chests = new HashSet <string> {
                "o005300", "o005400", "o255300"
            };
            List <Placement> placements = new List <Placement>();
            Dictionary <ItemKey, Placement> itemPlacement = new Dictionary <ItemKey, Placement>();

            foreach (ItemKey key in allItems)
            {
                if (categories[ItemCategory.ExcludeHints].Contains(key))
                {
                    continue;
                }
                if (!permutation.SkillAssignment.TryGetValue(key, out ItemKey lookup))
                {
                    lookup = key;
                }
                SlotKey      targetKey = permutation.GetFiniteTargetKey(lookup);
                ItemLocation itemLoc   = data.Location(targetKey);
                if (!ann.Slots.TryGetValue(itemLoc.LocScope, out SlotAnnotation slot))
                {
                    continue;
                }
                ItemCategory category = ItemCategory.RequiredItem;
                foreach (ItemCategory cat in categoryOverrides)
                {
                    // Use the last applicable category
                    if (categories[cat].Contains(key))
                    {
                        category = cat;
                    }
                }
                List <HintType> types = new List <HintType>();
                if (slot.HasTag("boss") || slot.HasTag("bosshint"))
                {
                    types.Add(HintType.Boss);
                    types.Add(HintType.Enemy);
                }
                else if (slot.HasTag("miniboss"))
                {
                    types.Add(HintType.Miniboss);
                    types.Add(HintType.Enemy);
                }
                else if (slot.HasTag("enemyhint"))
                {
                    types.Add(HintType.Enemy);
                }
                else if (slot.HasTag("carp"))
                {
                    types.Add(HintType.Carp);
                }
                else if (itemLoc.Keys.Any(k => k.Type == LocationKey.LocationType.SHOP && k.ID / 100 != 11005))
                {
                    types.Add(HintType.Shop);
                }
                else
                {
                    if (slot.Area.EndsWith("_underwater") || slot.HasTag("underwater"))
                    {
                        types.Add(HintType.Underwater);
                    }
                    if (itemLoc.Keys.Any(k => k.Type == LocationKey.LocationType.LOT && k.Entities.Any(e => chests.Contains(e.ModelName))))
                    {
                        types.Add(HintType.Chest);
                    }
                    types.Add(HintType.Treasure);
                }

                string    name      = game.Name(key);
                Placement placement = new Placement
                {
                    Item         = key,
                    FullName     = name,
                    Category     = category,
                    LateEligible = categories[ItemCategory.LatenessHints].Contains(key),
                    Important    = category != ItemCategory.HintFodder,
                    Area         = slot.Area,
                    Types        = types,
                };
                if (placement.Important)
                {
                    placements.Add(placement);
                }
                itemPlacement[key] = placement;
                if (printText)
                {
                    Console.WriteLine(placement);
                }
            }

            foreach (Hint hint in hints.Hints)  // Lovely
            {
                hint.Types = hint.Versions.Where(v => v.Type != "default").ToDictionary(v => v.Type, v => v);
            }

            // Classify early and late areas
            HashSet <string> early = new HashSet <string>(permutation.IncludedAreas.Where(e => !e.Value.Contains("ashinacastle")).Select(e => e.Key));
            HashSet <string> late  = new HashSet <string>(permutation.IncludedAreas.Where(e => e.Value.Contains("fountainhead_bridge")).Select(e => e.Key));

            // Start hints
            Random      random  = new Random((int)opt.Seed);
            List <Hint> sources = hints.Hints.Where(s => s.Types.Any(e => e.Key != "default")).ToList();

            Shuffle(random, sources);
            sources = sources.OrderBy(s => (s.HasInfix("bad") && s.Types.ContainsKey("hint")) ? 1 : 0).ToList();
            string choose(List <string> items)
            {
                return(items.Count == 1 ? items[0] : Choice(random, items));
            }

            // Process all hint types. There are 20 item locations in the entire game, plus 13 fixed texts, for a total of 33
            Regex format = new Regex(@"\(([^\)]*)\)");

            if (printText)
            {
                Console.WriteLine($"No hint items: {string.Join(", ", categories[ItemCategory.ExcludeHints].Select(k => game.Name(k)))}");
            }
            void addHint(Hint hint, HintTemplate t, Placement mainPlacement, Placement otherPlacement = null)
            {
                string text     = printText ? string.Join(" ", t.Text.Select(l => l.Trim())) : string.Join("\n", t.Text);
                bool   positive = !t.Type.Contains("bad");

                foreach (Match m in format.Matches(text))
                {
                    string    variable = m.Groups[1].Value;
                    string[]  parts    = variable.Split('_');
                    string    kind     = parts[0].ToLowerInvariant();
                    bool      upper    = char.IsUpper(parts[0][0]);
                    string    subkind  = parts.Length > 1 ? parts[1] : null;
                    string    value;
                    Placement placement = kind == "location2" && otherPlacement != null ? otherPlacement : mainPlacement;
                    if (kind == "item")
                    {
                        if (positive)
                        {
                            if (placement.LateHint || !categoryText.ContainsKey(placement.Category))
                            {
                                value = placement.FullName;
                                if (specialItemNames.TryGetValue(value, out ItemHintName vagueName))
                                {
                                    value = choose(vagueName.GetNames());
                                }
                            }
                            else
                            {
                                value = categoryText[placement.Category];
                            }
                        }
                        else
                        {
                            // Shouldn't be used in this context, but fall back
                            value = "nothing";
                        }
                    }
                    else if (kind == "type")
                    {
                        HintType type = placement.Types.FirstOrDefault();
                        value = choose(typeNames[placement.Types[0]].GetNames(subkind));
                    }
                    else if (kind == "location" || kind == "location2")
                    {
                        bool prep;
                        if (subkind == null)
                        {
                            prep = false;
                        }
                        else if (subkind == "preposition")
                        {
                            prep = true;
                        }
                        else
                        {
                            throw new Exception($"Unknown hint config variable {variable}");
                        }
                        if (positive || placement.Types.Count == 0)
                        {
                            value = placement.AreaHint == null ? (positive ? "somewhere" : "anywhere") : choose(placement.AreaHint.GetNames(prep));
                        }
                        else
                        {
                            if (prep || placement.AreaHint != null)
                            {
                                value = (prep ? "from " : "") + choose(typeNames[placement.Types[0]].GetNames("noun")) + " " + (placement.AreaHint == null ? (positive ? "somewhere" : "anywhere") : choose(placement.AreaHint.GetNames(true)));
                            }
                            else
                            {
                                value = value = choose(typeNames[placement.Types[0]].GetNames("gerund"));
                            }
                        }
                    }
                    else
                    {
                        throw new Exception($"Unknown hint variable {variable}");
                    }
                    if (upper)
                    {
                        value = value[0].ToString().ToUpperInvariant() + value.Substring(1);
                    }
                    text = text.Replace($"({variable})", value);
                }
                if (printText)
                {
                    Console.WriteLine(text + "\n");
                }
                if (hint.ID < 10000)
                {
                    itemDesc[hint.ID] = text;
                }
                else
                {
                    eventText[hint.ID] = text;
                }
            }

            AreaHint mostSpecificArea(string name)
            {
                AreaHint selected = null;

                foreach (AreaHint area in hints.Areas)
                {
                    if (area.EarlyAreas.Contains(name))
                    {
                        if (selected == null || area.Parents.Contains(selected))
                        {
                            selected = area;
                        }
                    }
                }
                return(selected);
            }

            T pop <T>(List <T> list)
            {
                T ret = list[list.Count - 1];

                list.RemoveAt(list.Count - 1);
                return(ret);
            }

            // In priority order:
            // Item hints: Find item at (type) and (location). Always filled in. 1 of these.
            HashSet <ItemKey> exactKey = new HashSet <ItemKey>();

            foreach (Hint hint in sources.Where(s => s.Types.ContainsKey("itemhint")).ToList())
            {
                HintTemplate t = hint.Types["itemhint"];
                if (!ann.Items.TryGetValue(t.Req, out ItemKey key))
                {
                    throw new Exception($"Unrecognized name {t.Req}");
                }
                if (!itemPlacement.TryGetValue(key, out Placement placement))
                {
                    continue;
                }
                exactKey.Add(key);
                placement          = placement.Copy();
                placement.AreaHint = mostSpecificArea(placement.Area);
                addHint(hint, t, placement);
                sources.Remove(hint);
            }

            // Location hints: Find (item/item category/nothing) at (location). Always filled in. 2 of these.
            foreach (Hint hint in sources.Where(s => s.Types.ContainsKey("locationhint")).ToList())
            {
                HintTemplate     t        = hint.Types["locationhint"];
                List <string>    reqAreas = t.Req.Split(' ').ToList();
                List <Placement> places   = placements.Where(p => reqAreas.Contains(p.Area)).ToList();
                if (places.Count == 0 && hint.Types.TryGetValue("locationbadhint", out HintTemplate t2))
                {
                    addHint(hint, t2, null);
                }
                else
                {
                    Placement placement = places[0].Copy();
                    addHint(hint, t, placement);
                }
                sources.Remove(hint);
            }

            // Global negative hint: There is nothing at (type). Always include as many as applicable for likely types, treasure/chest/boss/miniboss/enemy/underwater/shop (but more like 1-2)
            List <HintType> present = placements.SelectMany(p => p.Types).Distinct().ToList();
            List <HintType> absent  = new List <HintType>
            {
                HintType.Treasure, HintType.Chest, HintType.Boss, HintType.Miniboss, HintType.Enemy, HintType.Underwater, HintType.Shop
            }
            .Except(present).ToList();

            foreach (Hint hint in sources.Where(s => s.Types.ContainsKey("badhint")).ToList())
            {
                if (absent.Count == 0)
                {
                    break;
                }
                HintType     type = pop(absent);
                HintTemplate t    = hint.Types["badhint"];
                addHint(hint, t, new Placement
                {
                    Types = new List <HintType> {
                        type
                    },
                });
                sources.Remove(hint);
            }

            // Positive hint: There is (item/item category) at (type) in (location). Include one per key item, up to 13, and special items, currently 3.
            List <Placement> toPlace = placements.Where(p => !exactKey.Contains(p.Item)).ToList();

            foreach (Hint hint in sources.Where(s => s.Types.ContainsKey("hint")).ToList())
            {
                if (toPlace.Count == 0)
                {
                    break;
                }
                HintTemplate t         = hint.Types["hint"];
                Placement    placement = pop(toPlace);
                placement          = placement.Copy();
                placement.AreaHint = mostSpecificArea(placement.Area);
                addHint(hint, t, placement);
                sources.Remove(hint);
            }

            // Lateness hint: There is (item) at (type) (late/early). Include one per location item, up to 6.
            toPlace = placements.Where(p => p.LateEligible).ToList();
            foreach (Hint hint in sources.Where(s => s.Types.ContainsKey("hint")).ToList())
            {
                if (toPlace.Count == 0)
                {
                    break;
                }
                HintTemplate t         = hint.Types["hint"];
                Placement    placement = pop(toPlace);
                placement          = placement.Copy();
                placement.LateHint = true;
                string info = early.Contains(placement.Area) ? "an early game location" : (late.Contains(placement.Area) ? "a late game location" : "a mid game location");
                string prep = early.Contains(placement.Area) ? "in the early game" : (late.Contains(placement.Area) ? "in the late game" : "in the mid game");
                placement.AreaHint = new AreaHint {
                    Name = info, Vague = info, VaguePrep = prep
                };
                addHint(hint, t, placement);
                sources.Remove(hint);
            }

            // So far, around 25 hints created, with ~10 left to go. At this point, pick randomly from either of these categories.
            List <AreaHint> withoutRedundantChildren(List <AreaHint> allAreas)
            {
                return(allAreas.Where(area => area.Parents.Count == 0 || !area.Parents.Any(p => allAreas.Contains(p))).ToList());
            }

            List <AreaHint> areasWithoutPlacements(HintType type = HintType.None)
            {
                HashSet <string> importantAreas = new HashSet <string>(placements.Where(p => type == HintType.None || p.Types.Contains(type)).Select(p => p.Area));

                return(hints.Areas.Where(h => (type == HintType.None || h.Types.Contains(type)) && !importantAreas.Overlaps(h.Areas)).ToList());
            }

            // Area negative hint: There is nothing in (location). Can be included for all such areas, but eliminate a hint if its parent also applies.
            Dictionary <HintType, List <AreaHint> > negativeHints = new Dictionary <HintType, List <AreaHint> >();
            List <AreaHint> unimportantAreas = areasWithoutPlacements();

            negativeHints[HintType.None] = withoutRedundantChildren(unimportantAreas);
            foreach (HintType noType in new[] { HintType.Boss, HintType.Miniboss, HintType.Treasure })
            {
                negativeHints[noType] = withoutRedundantChildren(areasWithoutPlacements(noType)).Except(unimportantAreas).ToList();
            }
            negativeHints[HintType.Enemy] = new List <AreaHint>();
            foreach (AreaHint allEnemy in negativeHints[HintType.Boss].Intersect(negativeHints[HintType.Miniboss]).ToList())
            {
                // Add 'no powerful enemy' hints if no boss and no miniboss.
                negativeHints[HintType.Enemy].Add(allEnemy);
                negativeHints[HintType.Boss].Remove(allEnemy);
                negativeHints[HintType.Miniboss].Remove(allEnemy);
            }
            List <Placement> negatives = new List <Placement>();

            foreach (KeyValuePair <HintType, List <AreaHint> > entry in negativeHints)
            {
                List <HintType> types = entry.Key == HintType.None ? new List <HintType>() : new List <HintType> {
                    entry.Key
                };
                foreach (AreaHint area in entry.Value)
                {
                    negatives.Add(new Placement
                    {
                        AreaHint = area,
                        Types    = types,
                    });
                }
            }
            // Area type hint: There is nothing at (type) in (location). Include this for treasure and for miniboss/boss/enemy, eliminating when parent (or parent type) applies.
            Shuffle(random, negatives);
            negatives = negatives.OrderBy(a => a.AreaHint.AreaRank).ToList();
            // If there are at least 4 negative hints, allow up to 3 of the remainder to become fodder.
            List <Hint> negativeHintTemplates = sources.Where(s => s.Types.ContainsKey("badhint") || s.Types.ContainsKey("badhint2")).ToList();

            if (negatives.Count >= 4 && negativeHintTemplates.Count > 4)
            {
                int cutoffIndex = Math.Max(negativeHintTemplates.Count - 3, 4);
                negativeHintTemplates.RemoveRange(cutoffIndex, negativeHintTemplates.Count - cutoffIndex);
            }
            foreach (Hint hint in negativeHintTemplates)
            {
                if (negatives.Count == 0)
                {
                    break;
                }
                HintTemplate t              = hint.Types.ContainsKey("badhint") ? hint.Types["badhint"] : hint.Types["badhint2"];
                Placement    placement      = pop(negatives);
                Placement    otherPlacement = t.Type == "badhint2" && negatives.Count > 0 ? pop(negatives) : null;
                addHint(hint, t, placement, otherPlacement);
                sources.Remove(hint);
            }

            if (printText)
            {
                Console.WriteLine($"{sources.Count} remaining hints: [{string.Join(", ", sources.Select(s => string.Join("/", s.Types.Keys)))}]");
            }
            if (sources.Count > 0)
            {
                // At this point, pull in misc hints for somewhat useful items
                toPlace = categories[ItemCategory.HintFodder].Select(k => itemPlacement[k]).ToList();
                Shuffle(random, toPlace);
                foreach (Hint hint in sources.Where(s => s.Types.ContainsKey("hint")).ToList())
                {
                    if (toPlace.Count == 0)
                    {
                        break;
                    }
                    HintTemplate t         = hint.Types["hint"];
                    Placement    placement = pop(toPlace);
                    placement          = placement.Copy();
                    placement.AreaHint = mostSpecificArea(placement.Area);
                    addHint(hint, t, placement);
                    sources.Remove(hint);
                }
            }

            // Need to figure out which items are strictly required to beat the game
            // Also, for bad hints, find all strictly required items plus key items
            // List all locations which can have hints scoped to them. The entirety of Sunken Valley, or just lower/upper, or burrow
            // Most key items get minimum specificity
            // Required side area items will get early/lateness specificity when that applies
            // Skills/prosthetics will get maximum specificity, maybe even two for Mikiri

            // Mortal Blade is excluded, since it has its own explicit hint
            // Young Lord Bell Charm is excluded if earlyhirata
        }