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); }