public void WriteList(GameData game, Dictionary <int, EnemyInfo> fullInfos)
        {
            // Generate things
            HashSet <int> allBonfires = new HashSet <int>
            {
                1001950,  // Dragonspring - Hirata Estate
                1001951,  // Estate Path
                1001952,  // Bamboo Thicket Slope
                1001953,  // Hirata Estate - Main Hall
                1001955,  // Hirata Audience Chamber
                1001954,  // Hirata Estate - Hidden Temple
                1101950,  // Dilapidated Temple
                1101956,  // Ashina Outskirts
                1101951,  // Outskirts Wall - Gate Path
                1101952,  // Outskirts Wall - Stairway
                1101953,  // Underbridge Valley
                1101954,  // Ashina Castle Gate Fortress
                1101955,  // Ashina Castle Gate
                1101957,  // Flames of Hatred
                1111950,  // Ashina Castle
                1111951,  // Upper Tower - Antechamber
                1111957,  // Upper Tower - Ashina Dojo
                1111952,  // Castle Tower Lookout
                1111953,  // Upper Tower - Kuro's Room
                1111956,  // Old Grave
                1111954,  // Great Serpent Shrine
                1111955,  // Abandoned Dungeon Entrance
                1121951,  // Ashina Reservoir
                1121950,  // Near Secret Passage
                1301950,  // Underground Waterway
                1301951,  // Bottomless Hole
                1701955,  // Ashina Depths
                1701954,  // Poison Pool
                1701956,  // Guardian Ape's Burrow
                1501950,  // Hidden Forest
                1501951,  // Mibu Village
                1501952,  // Water Mill
                1501953,  // Wedding Cave Door
                1701957,  // Under-Shrine Valley
                1701950,  // Sunken Valley
                1701951,  // Gun Fort
                1701952,  // Riven Cave
                1701958,  // Bodhisattva Valley
                1701953,  // Guardian Ape's Watering Hole
                2001950,  // Senpou Temple,  Mt. Kongo
                2001951,  // Shugendo
                2001952,  // Temple Grounds
                2001953,  // Main Hall
                2001954,  // Inner Sanctum
                2001955,  // Sunken Valley Cavern
                2001956,  // Bell Demon's Temple
                2501950,  // Fountainhead Palace
                2501951,  // Vermilion Bridge
                2501956,  // Mibu Manor
                2501952,  // Flower Viewing Stage
                2501953,  // Great Sakura
                2501954,  // Palace Grounds
                2501957,  // Feeding Grounds
                2501958,  // Near Pot Noble
                2501955,  // Sanctuary
            };

            // Probably shouldn't use tuples, but too late now
            List <(int, List <string>, List <int>, List <int>)> paths = new List <(int, List <string>, List <int>, List <int>)>
            {
                // Tutorial
                (1, new List <string> {
                    "ashinareservoir", "ashinacastle"
                },
                 new List <int> {
                    8306, 0
                }, new List <int>
                {
                    1121951,  // Ashina Reservoir
                    1121950,  // Near Secret Passage
                }),
                // First stretch of Ashina Outskirts
                (1, new List <string> {
                    "ashinaoutskirts"
                },
                 new List <int> {
                    8302, 1, 8302, -1, 1100330, 1
                }, new List <int>
                {
                    1101956,  // Ashina Outskirts
                    1101951,  // Outskirts Wall - Gate Path
                    1101952,  // Outskirts Wall - Stairway
                }),
                // Ashina Outskirts up to Blazing Bull
                (1, new List <string> {
                    "ashinaoutskirts", "ashinacastle"
                },
                 new List <int> {
                    8302, 1, 8302, -1, 8301, 1, 8301, -1, 1100330, 1
                }, new List <int>
                {
                    1101952,  // Outskirts Wall - Stairway
                    1101953,  // Underbridge Valley
                    1101954,  // Ashina Castle Gate Fortress
                    1101955,  // Ashina Castle Gate
                    // 1111950,  // Ashina Castle
                }),
                // Hirata 1
                (1, new List <string> {
                    "hirata"
                },
                 new List <int> {
                    1000353, 1, 1005601, 1, 1000301, 1, 1000900, 1
                }, new List <int>
                {
                    1001950,  // Dragonspring - Hirata Estate
                    1001951,  // Estate Path
                    1001952,  // Bamboo Thicket Slope
                    1001953,  // Hirata Estate - Main Hall
                    1001955,  // Hirata Audience Chamber
                    1001954,  // Hirata Estate - Hidden Temple
                }),
                // Ashina Castle to Genichiro
                (2, new List <string> {
                    "ashinacastle"
                },
                 new List <int> {
                    8301, 1, 8302, 1, 8302, -1
                }, new List <int>
                {
                    1111950,  // Ashina Castle
                    1111951,  // Upper Tower - Antechamber
                    1111957,  // Upper Tower - Ashina Dojo
                    1111952,  // Castle Tower Lookout
                }),
                // Ashina Castle to Reservoir to Dungeon
                (2, new List <string> {
                    "ashinareservoir"
                },
                 new List <int> {
                    8302, 1, 1120300, 0
                }, new List <int>
                {
                    1111950,  // Ashina Castle
                    1121951,  // Ashina Reservoir
                    1301951,  // Bottomless Hole
                }),
                // Dungeon
                (2, new List <string> {
                    "dungeon"
                },
                 new List <int> {
                }, new List <int>
                {
                    1111955,  // Abandoned Dungeon Entrance
                    1301950,  // Underground Waterway
                    1301951,  // Bottomless Hole
                }),
                // Senpou temple
                (2, new List <string> {
                    "senpou"
                },
                 new List <int> {
                }, new List <int>
                {
                    2001950,  // Senpou Temple,  Mt. Kongo
                    2001951,  // Shugendo
                    2001952,  // Temple Grounds
                    2001953,  // Main Hall
                }),
                // Hidden Forest to Water Mill
                (3, new List <string> {
                    "mibuvillage"
                },
                 new List <int> {
                    1700850, 1, 1700520, 1
                }, new List <int>
                {
                    1501950,  // Hidden Forest
                    1501951,  // Mibu Village
                    1501952,  // Water Mill
                }),
                // End of Ashina Depths
                (3, new List <string> {
                    "mibuvillage"
                },
                 new List <int> {
                }, new List <int>
                {
                    1501952,  // Water Mill
                    1501953,  // Wedding Cave Door
                }),
                // Most of Sunken Valley
                (3, new List <string> {
                    "ashinacastle", "sunkenvalley"
                },
                 new List <int> {
                    8301, 1, 8301, -1, 8302, 1, 8302, -1
                }, new List <int>
                {
                    1111952,  // Castle Tower Lookout
                    1111956,  // Old Grave
                    1111954,  // Great Serpent Shrine
                    1701957,  // Under-Shrine Valley
                    1701950,  // Sunken Valley
                    1701951,  // Gun Fort
                    1701952,  // Riven Cave
                    1701958,  // Bodhisattva Valley
                    1701953,  // Guardian Ape's Watering Hole
                }),
                // Sunken Valley to Poison Pool path
                (3, new List <string> {
                    "sunkenvalley"
                },
                 new List <int> {
                    1700850, 0, 1700520, 0
                }, new List <int>
                {
                    1701958,  // Bodhisattva Valley
                    1701954,  // Poison Pool
                    1701956,  // Guardian Ape's Burrow
                }),
                // Ashina Castle Revisited, also down to Masanaga
                (4, new List <string> {
                    "ashinacastle"
                },
                 new List <int> {
                    8301, 0, 8302, 1, 8302, -1
                }, new List <int>
                {
                    1111955,  // Abandoned Dungeon Entrance
                    1111950,  // Ashina Castle
                    1111951,  // Upper Tower - Antechamber
                    1111957,  // Upper Tower - Ashina Dojo
                    1111952,  // Castle Tower Lookout
                    1111956,  // Old Grave
                    1111954,  // Great Serpent Shrine
                }),
                // Fountainhead
                (5, new List <string> {
                    "fountainhead"
                },
                 new List <int> {
                }, new List <int>
                {
                    2501950,  // Fountainhead Palace
                    2501951,  // Vermilion Bridge
                    2501956,  // Mibu Manor
                    2501952,  // Flower Viewing Stage
                    2501958,  // Near Pot Noble
                    2501953,  // Great Sakura
                    2501954,  // Palace Grounds
                    2501955,  // Sanctuary
                }),
                // Hirata Revisited
                (5, new List <string> {
                    "hirata"
                },
                 new List <int> {
                    1000353, 0, 1005601, 0, 1000301, 0, 1000900, 0
                }, new List <int>
                {
                    1001952,  // Bamboo Thicket Slope
                    1001953,  // Hirata Estate - Main Hall
                    1001955,  // Hirata Audience Chamber
                    1001954,  // Hirata Estate - Hidden Temple
                }),
                // Ashina Castle End to Outskirts
                (5, new List <string> {
                    "ashinacastle", "ashinaoutskirts"
                },
                 new List <int> {
                    8302, 0
                }, new List <int>
                {
                    1111953,  // Upper Tower - Kuro's Room
                    1111956,  // Old Grave
                    1101952,  // Outskirts Wall - Stairway
                    1101951,  // Outskirts Wall - Gate Path
                }),
                // Ashina Castle End to Reservoir
                (5, new List <string> {
                    "ashinacastle", "ashinareservoir"
                },
                 new List <int> {
                    8302, 0
                }, new List <int>
                {
                    1111953,  // Upper Tower - Kuro's Room
                    1111957,  // Upper Tower - Ashina Dojo
                    1111951,  // Upper Tower - Antechamber
                    1111950,  // Ashina Castle
                    1121951,  // Ashina Reservoir
                    1121950,  // Near Secret Passage
                }),
            };
            FMG bonfires = new GameEditor(GameSpec.FromGame.SDT).LoadBnd(@"C:\Program Files (x86)\Steam\steamapps\common\Sekiro\msg\engus\menu.msgbnd.dcx", (p, n) => FMG.Read(p))["NTC_\u30e1\u30cb\u30e5\u30fc\u30c6\u30ad\u30b9\u30c8"];
            Dictionary <int, string> names = new Dictionary <int, string>();

            foreach (PARAM.Row r in game.Params["BonfireWarpParam"].Rows)
            {
                // break;
                int    entity  = (int)r["WarpEventId"].Value;
                string bonfire = bonfires[(int)r["BonfireNameId"].Value];
                if (bonfire != null && entity > 0)
                {
                    names[entity] = bonfire;
                    // Console.WriteLine($"{entity},  // {bonfire}");
                }
            }
            Dictionary <int, Vector3> points = new Dictionary <int, Vector3>();

            // Find location of all bonfires
            foreach (KeyValuePair <string, MSBS> entry in game.Smaps)
            {
                if (!game.Locations.ContainsKey(entry.Key))
                {
                    continue;
                }
                string map = game.Locations[entry.Key];
                MSBS   msb = entry.Value;
                foreach (MSBS.Part.Object e in msb.Parts.Objects)
                {
                    if (allBonfires.Contains(e.EntityID))
                    {
                        points[e.EntityID] = e.Position;
                    }
                }
            }
            string pathText(int p)
            {
                int first = paths[p].Item4.First();
                int last  = paths[p].Item4.Last();

                return($"#{paths[p].Item1} {names[first]}->{names[last]}");
            }

            bool investigateScaling = false;
            List <List <EnemyClass> > typeGroups = new List <List <EnemyClass> >
            {
                new List <EnemyClass> {
                    EnemyClass.Boss, EnemyClass.TutorialBoss
                },
                new List <EnemyClass> {
                    EnemyClass.Miniboss
                },
                new List <EnemyClass> {
                    EnemyClass.Basic, EnemyClass.FoldingMonkey, EnemyClass.OldDragon
                }
            };
            List <EnemyClass>           types = typeGroups.SelectMany(c => c).ToList();
            Dictionary <int, EnemyInfo> infos = fullInfos.Values.Where(e => types.Contains(e.Class)).ToDictionary(e => e.ID, e => e);

            if (!investigateScaling)
            {
                infos.Remove(1110920);
                infos.Remove(1110900);
                infos.Remove(1120800);
            }
            Dictionary <int, List <int> > possiblePaths = new Dictionary <int, List <int> >();
            bool explainCat = false;

            for (int i = 0; i < paths.Count; i++)
            {
                if (explainCat)
                {
                    Console.WriteLine($"--- Processing {pathText(i)}");
                }
                (int section, List <string> maps, List <int> cond, List <int> order) = paths[i];
                Dictionary <int, List <int> > eventFlags = new Dictionary <int, List <int> >();
                HashSet <int> excludeEntity = new HashSet <int>();
                HashSet <int> expectEntity  = new HashSet <int>();
                HashSet <int> getEntity     = new HashSet <int>();
                for (int j = 0; j < cond.Count; j += 2)
                {
                    int check = cond[j];
                    int val   = cond[j + 1];
                    if (check >= 1000000)
                    {
                        if (val == 0)
                        {
                            expectEntity.Add(check);
                        }
                        else
                        {
                            excludeEntity.Add(check);
                        }
                    }
                    else
                    {
                        AddMulti(eventFlags, check, val);
                    }
                }
                foreach (KeyValuePair <string, MSBS> entry in game.Smaps)
                {
                    if (!game.Locations.ContainsKey(entry.Key))
                    {
                        continue;
                    }
                    string map = game.Locations[entry.Key];
                    MSBS   msb = entry.Value;

                    if (!maps.Contains(map))
                    {
                        continue;
                    }
                    foreach (MSBS.Part.Enemy e in msb.Parts.Enemies)
                    {
                        if (!infos.ContainsKey(e.EntityID))
                        {
                            continue;
                        }
                        points[e.EntityID] = e.Position;
                        names[e.EntityID]  = game.ModelName(e.ModelName);
                        List <int> ids = new List <int> {
                            e.EntityID
                        };
                        ids.AddRange(e.EntityGroupIDs.Where(id => id > 0));
                        if (excludeEntity.Overlaps(ids))
                        {
                            if (explainCat)
                            {
                                Console.WriteLine($"excluded: {string.Join(",", ids)} from {string.Join(",", excludeEntity)}");
                            }
                            continue;
                        }
                        else if (expectEntity.Overlaps(ids))
                        {
                            getEntity.UnionWith(ids);
                        }
                        else if (eventFlags.Count > 0)
                        {
                            // If not explicitly expected, do a check for game progression
                            Dictionary <int, int> flags = new Dictionary <int, int>();
                            if (e.EventFlagID != -1)
                            {
                                flags[e.EventFlagID] = e.EventFlagCompareState;
                            }
                            if (e.UnkT48 != -1)
                            {
                                flags[e.UnkT48] = e.UnkT4C;
                            }
                            if (e.UnkT50 != -1)
                            {
                                flags[e.UnkT50] = 1;
                            }
                            bool mismatch = false;
                            foreach (KeyValuePair <int, List <int> > flagPair in eventFlags)
                            {
                                int        flag = flagPair.Key;
                                List <int> cmps = flagPair.Value;
                                int        cmp  = flags.TryGetValue(flag, out int tmp) ? tmp : -1;
                                if (explainCat && e.EntityID == 9999999)
                                {
                                    Console.WriteLine($"for {e.EntityID} expected {flag} = {string.Join(",", cmps)}, found result {cmp}");
                                }
                                if (!cmps.Contains(cmp))
                                {
                                    if (explainCat)
                                    {
                                        Console.WriteLine($"excluded: {string.Join(",", ids)} with {flag} = {cmp} (not {string.Join(",", cmps)})");
                                    }
                                    mismatch = true;
                                }
                            }
                            if (mismatch)
                            {
                                continue;
                            }
                        }
                        if (explainCat)
                        {
                            Console.WriteLine($"added: {string.Join(",", ids)}");
                        }
                        AddMulti(possiblePaths, e.EntityID, i);
                    }
                }
                List <int> missing = expectEntity.Except(getEntity).ToList();
                if (missing.Count > 0)
                {
                    throw new Exception($"Missing {string.Join(",", missing)} in {string.Join(",", maps)}");
                }
            }
            // Hardcode headless into Senpou, because it is out of the way and sort of a singleton
            possiblePaths[1100330] = new List <int> {
                7
            };

            Console.WriteLine("Categories");
            Dictionary <int, (int, float)> chosenPath = new Dictionary <int, (int, float)>();

            foreach (EnemyInfo info in infos.Values)
            {
                if (!possiblePaths.TryGetValue(info.ID, out List <int> pathList))
                {
                    throw new Exception($"{info.ID} has no categorization: {info.DebugText}");
                }
                if (paths[pathList[0]].Item2.Contains("hirata"))
                {
                    // If Hirata, greedily choose pre-revisited Hirata
                    pathList = new List <int> {
                        pathList[0]
                    };
                }
                float   score = float.PositiveInfinity;
                Vector3 pos   = points[info.ID];
                foreach (int path in pathList)
                {
                    (int section, List <string> maps, List <int> cond, List <int> order) = paths[path];
                    for (int i = 0; i < order.Count - 1; i++)
                    {
                        Vector3 p1    = points[order[i]];
                        Vector3 p2    = points[order[i + 1]];
                        float   dist1 = Vector3.Distance(p1, pos);
                        float   dist2 = Vector3.Distance(p2, pos);
                        float   dist  = dist1 + dist2;
                        if (info.ID == 9999999)
                        {
                            Console.WriteLine($"Found dist {dist1} to {names[order[i]]}, and {dist2} to {names[order[i + 1]]}. TOTAL {dist}");
                        }
                        if (dist < score)
                        {
                            score = dist;
                            chosenPath[info.ID] = (path, i + Vector3.Distance(p1, pos) / dist);
                        }
                    }
                }
                if (float.IsInfinity(score))
                {
                    throw new Exception($"{info.ID} with paths {string.Join(",", pathList.Select(pathText))} had nothing checked for it");
                }
            }

            // Put bosses in phase order
            foreach (int id in chosenPath.Keys.ToList())
            {
                EnemyInfo info = infos[id];
                if (info.Class == EnemyClass.Boss && info.OwnedBy != 0)
                {
                    chosenPath[id] = chosenPath[info.OwnedBy];
                }
            }

            if (investigateScaling)
            {
                Dictionary <int, MSBS.Part.Enemy> enemies = new Dictionary <int, MSBS.Part.Enemy>();
                foreach (KeyValuePair <string, MSBS> entry in game.Smaps)
                {
                    if (!game.Locations.ContainsKey(entry.Key))
                    {
                        continue;
                    }
                    string map = game.Locations[entry.Key];
                    MSBS   msb = entry.Value;
                    foreach (MSBS.Part.Enemy e in msb.Parts.Enemies)
                    {
                        enemies[e.EntityID] = e;
                    }
                }

                // Exclude these from scaling considerations, since they are not really part of the area (meant for when visiting later)
                HashSet <int> phantomGroups = new HashSet <int>
                {
                    // Ashina phantoms
                    1505201, 1505211, 1705200, 1705201, 2005200, 2005201,
                    // Sunken Valley phantoms
                    1505202, 1505212, 2005210, 2005211,
                    // Mibu Village phantoms
                    1705220, 1705221, 2005220, 2005221,
                };

                // haveSoulRate Unk85: NG+ only
                // EventFlagId: used for scaling speffect
                // There are these overall groups: vitality, damage, experience, cash. (is there haveSoulRate for cash/xp? maybe it's Unk85)
                List <string> scaleSp   = "maxHpRate maxStaminaCutRate physAtkPowerRate magicAtkPowerRate fireAtkPowerRate thunderAtkPowerRate staminaAttackRate darkAttackPowerRate NewGameBonusUnk".Split(' ').ToList();
                List <string> scaleNpc  = "Hp getSoul stamina staminaRecoverBaseVal Experience".Split(' ').ToList();
                List <string> allFields = scaleSp.Concat(scaleNpc).ToList();
                // Disp: ModelDispMask0 -> ModelDispMask31
                // Npc param has GameClearSpEffectID
                Dictionary <(string, int, int), List <float> > allScales = new Dictionary <(string, int, int), List <float> >();
                Dictionary <int, int> allSections = new Dictionary <int, int>();
                foreach (List <EnemyClass> typeGroup in typeGroups)
                {
                    // Consider two enemies the same if they have the same think id, or same disp mask
                    // Or for minibosses, if they are just the same model, that's probably fine
                    Dictionary <string, List <int> > thinks = new Dictionary <string, List <int> >();
                    Dictionary <string, List <int> > masks  = new Dictionary <string, List <int> >();
                    Dictionary <string, List <int> > bosses = new Dictionary <string, List <int> >();
                    List <string>         order             = new List <string>();
                    Dictionary <int, int> sections          = new Dictionary <int, int>();
                    foreach (KeyValuePair <int, (int, float)> entry in chosenPath.OrderBy(e => (e.Value, e.Key)))
                    {
                        int       id   = entry.Key;
                        EnemyInfo info = infos[id];
                        if (!typeGroup.Contains(info.Class))
                        {
                            continue;
                        }
                        MSBS.Part.Enemy e       = enemies[id];
                        int             path    = entry.Value.Item1;
                        int             section = paths[path].Item1;
                        sections[id]    = section;
                        allSections[id] = section;
                        if (e.EntityGroupIDs.Any(g => phantomGroups.Contains(g)))
                        {
                            continue;
                        }
                        string model = game.ModelName(e.ModelName);
                        if (typeGroup.Contains(EnemyClass.Miniboss) || typeGroup.Contains(EnemyClass.Boss))
                        {
                            AddMulti(bosses, model, id);
                            continue;
                        }
                        string think = $"{model} {e.ThinkParamID}";
                        AddMulti(thinks, think, id);
                        PARAM.Row npc = game.Params["NpcParam"][e.NPCParamID];
                        if (e.NPCParamID > 0 && npc != null)
                        {
                            uint mask = 0;
                            for (int i = 0; i < 32; i++)
                            {
                                if ((byte)npc[$"ModelDispMask{i}"].Value == 1)
                                {
                                    mask |= ((uint)1 << i);
                                }
                            }
                            string dispMask = $"{model} 0x{mask:X8}";
                            AddMulti(masks, dispMask, id);
                        }
                    }
                    foreach (KeyValuePair <string, List <int> > entry in thinks.Concat(masks.Concat(bosses)))
                    {
                        if (entry.Value.Count == 1)
                        {
                            continue;
                        }
                        List <int> secs = entry.Value.Select(i => sections[i]).Distinct().ToList();
                        if (secs.Count == 1)
                        {
                            continue;
                        }

                        Console.WriteLine($"{entry.Key}: {string.Join(",", entry.Value.Select(i => $"{i}[{sections[i]}]"))}");
                        SortedDictionary <string, List <(int, float)> > fieldValues = new SortedDictionary <string, List <(int, float)> >();
                        foreach (int id in entry.Value)
                        {
                            MSBS.Part.Enemy e   = enemies[id];
                            PARAM.Row       npc = game.Params["NpcParam"][e.NPCParamID];
                            if (e.NPCParamID == 0 || npc == null)
                            {
                                continue;
                            }
                            Dictionary <string, float> values = new Dictionary <string, float>();
                            foreach (string f in scaleNpc)
                            {
                                values[f] = float.Parse(npc[f].Value.ToString());
                            }
                            int       spVal = (int)npc["EventFlagId"].Value; // GameClearSpEffectID is for NG+ only, or time-of-day only, or something like that
                            PARAM.Row sp    = game.Params["SpEffectParam"][spVal];
                            if (spVal > 0 && sp != null)
                            {
                                foreach (string f in scaleSp)
                                {
                                    values[f] = float.Parse(sp[f].Value.ToString());
                                }
                            }
                            foreach (KeyValuePair <string, float> val in values)
                            {
                                AddMulti(fieldValues, val.Key, (sections[id], val.Value));
                            }
                        }
                        foreach (KeyValuePair <string, List <(int, float)> > val in fieldValues)
                        {
                            // Console.WriteLine($"  {val.Key}: {string.Join(", ", val.Value.OrderBy(v => v).Select(v => $"[{v.Item1}]{v.Item2}"))}");
                            Dictionary <int, float> bySection = val.Value.GroupBy(v => v.Item1).ToDictionary(g => g.Key, g => g.Select(v => v.Item2).Average());
                            List <string>           sorts     = new List <string>();
                            foreach (int i in bySection.Keys)
                            {
                                foreach (int j in bySection.Keys)
                                {
                                    if (i >= j)
                                    {
                                        continue;
                                    }
                                    float ratio = bySection[j] / bySection[i];
                                    if (float.IsNaN(ratio) || float.IsInfinity(ratio) || ratio == 1 || ratio == 0)
                                    {
                                        continue;
                                    }
                                    sorts.Add($"{i}{j}: {ratio:f3}x");
                                    AddMulti(allScales, (val.Key, i, j), ratio);
                                    // Can be used for complete table, but easier to leave out for lower diagonal
                                    // AddMulti(allScales, (val.Key, j, i), 1 / ratio);
                                }
                            }
                            if (sorts.Count > 0)
                            {
                                Console.WriteLine($"  {val.Key}: {string.Join(", ", sorts)}");
                            }
                        }
                    }
                }
                foreach (string field in allFields)
                {
                    Console.WriteLine($"-- {field} ({allScales.Where(k => k.Key.Item1 == field).Sum(e => e.Value.Count)})");
                    for (int i = 1; i <= 5; i++)
                    {
                        // row: the target class. column: the source class. value: how much to multiply to place the source in the target.
                        Console.WriteLine("  " + string.Join(" ", Enumerable.Range(1, 5).Select(j => allScales.TryGetValue((field, j, i), out List <float> floats) ? $"{floats.Average():f5}," : "        ")));
                    }
                }
                foreach (EnemyInfo info in fullInfos.Values)
                {
                    if (!allSections.ContainsKey(info.ID) && info.Class == EnemyClass.Helper && allSections.TryGetValue(info.OwnedBy, out int section))
                    {
                        allSections[info.ID] = section;
                    }
                }
                foreach (KeyValuePair <int, int> entry in allSections.OrderBy(e => (e.Value, e.Key)))
                {
                    Console.WriteLine($"  {entry.Key}: {entry.Value}");
                }
            }

            bool debugOutput = false;

            foreach (List <EnemyClass> typeGroup in typeGroups)
            {
                List <string> order = new List <string>();
                foreach (KeyValuePair <int, (int, float)> entry in chosenPath.OrderBy(e => (e.Value, e.Key)))
                {
                    int       id   = entry.Key;
                    EnemyInfo info = infos[id];
                    if (!typeGroup.Contains(info.Class))
                    {
                        continue;
                    }
                    if (debugOutput)
                    {
                        Console.WriteLine($"{info.DebugText}\n- {pathText(entry.Value.Item1)}, progress {entry.Value.Item2}\n");
                    }
                    order.Add($"{info.ExtraName ?? names[id]} {id}");
                }
                for (int i = 0; i < order.Count; i++)
                {
                    if (!debugOutput)
                    {
                        Console.WriteLine($"  {order[i]}: {order[order.Count - 1 - i]}");
                    }
                }
            }
        }
Example #2
0
        public void ProcessEnemyPreset(GameData game, Dictionary <int, EnemyInfo> infos, List <EnemyCategory> cats, Dictionary <int, EnemyData> defaultData)
        {
            // Process enemy names
            HashSet <string> eligibleNames = new HashSet <string>();

            foreach (EnemyCategory cat in cats)
            {
                eligibleNames.Add(cat.Name);
                if (cat.Instance != null)
                {
                    eligibleNames.UnionWith(cat.Instance);
                }
                if (cat.Partition != null)
                {
                    eligibleNames.UnionWith(cat.Partition);
                }
                if (cat.Partial != null)
                {
                    eligibleNames.UnionWith(cat.Partial);
                }
            }
            Dictionary <int, string>         primaryName    = new Dictionary <int, string>();
            Dictionary <string, List <int> > enemiesForName = new Dictionary <string, List <int> >();
            bool debugNames = false;
            // Guardian Ape is both a boss and a helper, so try to avoid the helper ape getting pulled into the category
            HashSet <string> bossNames = new HashSet <string>(infos.Values.Where(i => i.Class == EnemyClass.Boss && i.ExtraName != null).Select(i => i.ExtraName));

            foreach (EnemyInfo info in infos.Values)
            {
                // Do not let some enemies be randomized at this point, many will prevent the game from being completeable.
                if (info.Class == EnemyClass.None)
                {
                    continue;
                }
                List <string> names = new List <string>();
                // Add all names. The first name added will be the primary name.
                if (info.ExtraName != null)
                {
                    names.Add(info.ExtraName);
                }
                if (defaultData.TryGetValue(info.ID, out EnemyData data))
                {
                    string model = game.ModelName(data.Model);
                    if (info.Class != EnemyClass.Boss && info.Category != null)
                    {
                        foreach (string cat in Regex.Split(info.Category, @"\s*;\s*"))
                        {
                            names.Add($"{cat} {model}");
                        }
                    }
                    if (info.Class == EnemyClass.Boss ? info.ExtraName == null : !bossNames.Contains(model))
                    {
                        names.Add(model);
                    }
                    if (info.Class == EnemyClass.Miniboss || info.Class == EnemyClass.Basic)
                    {
                        names.Add($"{info.Class} {model}");
                    }
                }
                names.RemoveAll(n =>
                {
                    if (!eligibleNames.Contains(n))
                    {
                        if (debugNames)
                        {
                            Console.WriteLine($"Name removed for {info.ID}: [{n}]");
                        }
                        return(true);
                    }
                    return(false);
                });
                if (names.Count > 0)
                {
                    primaryName[info.ID] = names[0];
                }
                names = names.SelectMany(n => new[] { n, $"{n} {info.ID}" }).ToList();
                names.Add(info.ID.ToString());
                if (info.Class == EnemyClass.Boss || info.Class == EnemyClass.Miniboss || info.Class == EnemyClass.Basic)
                {
                    names.Add($"{info.Class}");
                }
                if (info.Class != EnemyClass.Helper)
                {
                    // This is mainly used for "Oops All Any" so it should not include unkillable helpers
                    // like Immortal Centipede or Corrupted Monk Illusion.
                    names.Add($"Any");
                }
                if (debugNames)
                {
                    Console.WriteLine($"-- Names: {string.Join("; ", names)}");
                }
                foreach (string name in names)
                {
                    AddMulti(enemiesForName, name, info.ID);
                }
            }
            bool generateEnemyList = false;

            if (generateEnemyList)
            {
                foreach (EnemyClass c in new[] { EnemyClass.Boss, EnemyClass.TutorialBoss, EnemyClass.Miniboss, EnemyClass.FoldingMonkey, EnemyClass.Basic })
                {
                    string map = null;
                    foreach (EnemyInfo info in infos.Values)
                    {
                        if (info.Class == c && primaryName.TryGetValue(info.ID, out string name))
                        {
                            string enemyMap = game.LocationNames[game.Locations[defaultData[info.ID].Map]];
                            if (map != enemyMap)
                            {
                                map = enemyMap;
                                Console.WriteLine($"  # {map}");
                            }
                            Console.WriteLine($"  {name} {info.ID}: any");
                        }
                    }
                    Console.WriteLine();
                }
            }
            foreach (EnemyCategory cat in cats)
            {
                if (cat.Contains == null)
                {
                    continue;
                }
                List <int> combinedIds = new List <int>();
                foreach (string sub in cat.Contains)
                {
                    if (enemiesForName.TryGetValue(sub, out List <int> specialIds))
                    {
                        combinedIds.AddRange(specialIds);
                    }
                }
                if (combinedIds.Count > 0)
                {
                    enemiesForName[cat.Name] = combinedIds;
                }
            }

            // Process the config with these names
            List <string> errors = new List <string>();

            List <int> getIds(string name)
            {
                if (!enemiesForName.TryGetValue(name, out List <int> ids))
                {
                    string findId = "";
                    if (int.TryParse(name.Split(' ').Last(), out int id))
                    {
                        if (primaryName.TryGetValue(id, out string name2))
                        {
                            findId = $". Did you mean {name2} {id}?";
                        }
                        else
                        {
                            List <string> alts = enemiesForName.Select(e => e.Key).Where(e => e.EndsWith(id.ToString())).ToList();
                            if (alts.Count > 0)
                            {
                                findId = $". Did you mean {string.Join(", ", alts)}?";
                            }
                        }
                    }
                    errors.Add($"Unrecognized enemy name \"{name}\"{findId}");
                    return(new List <int>());
                }
                return(ids.ToList());
            }

            List <List <int> > getMultiIds(string name)
            {
                List <List <int> > ids = new List <List <int> >();

                foreach (string n in Regex.Split(name, @"\s*;\s*").ToList())
                {
                    ids.Add(getIds(n));
                }
                return(ids);
            }

            // Fill in non-randomized ids. The individual enemy config can also add to this.
            if (DontRandomize != null && DontRandomize.ToLowerInvariant() != "none")
            {
                DontRandomizeIDs.UnionWith(getMultiIds(DontRandomize).SelectMany(i => i));
            }
            if (RemoveSource != null && RemoveSource.ToLowerInvariant() != "none")
            {
                RemoveSourceIDs.UnionWith(getMultiIds(RemoveSource).SelectMany(i => i));
            }

            // Process the specific enemy map config
            bool debug = false;

            if (Enemies != null)
            {
                foreach (KeyValuePair <string, string> entry in Enemies)
                {
                    // For now, validate the config before checking if we can continue. This could be relaxed in the future, or in release builds.
                    List <int> targets = getIds(entry.Key);
                    if (targets.Count > 1 && debug)
                    {
                        Console.WriteLine($"Note: Enemy assigment {entry.Key}: {entry.Value} produced {targets.Count} targets");
                    }
                    if (entry.Value.ToLowerInvariant() == "any")
                    {
                        continue;
                    }
                    else if (entry.Value.ToLowerInvariant() == "norandom")
                    {
                        DontRandomizeIDs.UnionWith(targets);
                        continue;
                    }
                    List <int> sources = getIds(entry.Value);
                    if (sources.Count > 0)
                    {
                        // Allow the primary key to not be a unique enemy. This may produce some weird results.
                        foreach (int target in targets)
                        {
                            AddMulti(EnemyIDs, target, sources);
                        }
                    }
                }
            }

            bool poolFilter(int id)
            {
                return(!DontRandomizeIDs.Contains(id) && !RemoveSourceIDs.Contains(id));
            }

            // If oops all mode, fill in oops all ids. And copy them to pools.
            if (OopsAll != null && OopsAll.ToLowerInvariant() != "none")
            {
                OopsAllIDs.AddRange(getMultiIds(OopsAll).SelectMany(i => i).Where(poolFilter).Distinct());
                if (debug)
                {
                    Console.WriteLine($"Oops All: {string.Join("; ", OopsAllIDs.Select(i => primaryName.TryGetValue(i, out string n) ? n : i.ToString()))}");
                }
            }

            // Pool filtering
            int filterMulti(List <List <int> > groups, Predicate <int> filter)
            {
                int removed      = 0;
                int groupRemoved = groups.RemoveAll(group =>
                {
                    removed += group.RemoveAll(i => !filter(i));
                    return(group.Count == 0);
                });

                removed += groupRemoved;
                return(removed);
            }

            // For all enemy groups, fill in their ids
            void processPool(PoolAssignment pool, string type)
            {
                if (pool.Weight < 0)
                {
                    pool.Weight = 0;
                    errors.Add($"Pool for {type} \"{pool.Pool}\" must specify a positive Weight");
                }
                if (pool.Pool == null)
                {
                    errors.Add($"Pool for {type} must include a Pool specification");
                    pool.Weight = 0;
                    return;
                }
                if (pool.Pool.ToLowerInvariant() == "default")
                {
                    return;
                }
                pool.PoolGroups = getMultiIds(pool.Pool);
                filterMulti(pool.PoolGroups, poolFilter);
                if (pool.PoolGroups.Count == 0)
                {
                    pool.Weight = 0;
                }
            }

            List <PoolAssignment> processPools(List <PoolAssignment> pools, string type)
            {
                if (pools == null || pools.Count == 1 && pools[0].Pool.ToLowerInvariant() == "default")
                {
                    if (OopsAllIDs.Count > 0)
                    {
                        return(new List <PoolAssignment>
                        {
                            new PoolAssignment
                            {
                                Weight = 100,
                                Pool = OopsAll,
                                PoolGroups = new List <List <int> > {
                                    OopsAllIDs
                                },
                            },
                        });
                    }
                    else
                    {
                        return(null);
                    }
                }
                foreach (PoolAssignment pool in pools)
                {
                    processPool(pool, type);
                }
                return(pools);
            }

            Boss          = processPools(Boss, "Boss");
            Miniboss      = processPools(Miniboss, "Miniboss");
            Basic         = processPools(Basic, "Basic");
            Add           = processPools(Add, "Add");
            FoldingMonkey = processPools(FoldingMonkey, "FoldingMonkey");
            // Also copy 'basic' into 'add' if not specified, removing multi-phase enemies where possible
            if (Add == null && Basic != null)
            {
                Add = Basic.Select(p => p.Copy()).ToList();
                int removed = 0;
                foreach (PoolAssignment pool in Add)
                {
                    if (pool.PoolGroups.Count != 0)
                    {
                        removed += filterMulti(pool.PoolGroups, i => (infos[i].Class != EnemyClass.Boss && infos[i].Class != EnemyClass.Miniboss) || infos[i].HasTag("reasonable"));
                        if (pool.PoolGroups.Count == 0)
                        {
                            pool.Weight = 0;
                        }
                    }
                }
                if (removed == 0)
                {
                    Add = null;
                }
            }

            HandleErrors(errors);
        }