private void LoadScripts() { Emevds = Editor.Load("Base", path => EMEVD.Read(path), "*.emevd.dcx"); MaybeOverrideFromModDir(Emevds, name => $@"event\{name}.emevd.dcx", path => EMEVD.Read(path)); List <string> missing = Locations.Keys.Concat(new[] { "common", "common_func" }).Except(Emevds.Keys).ToList(); if (missing.Count != 0) { throw new Exception($@"Missing emevds in dist\Base: {string.Join(", ", missing)}"); } }
public EventScripter(string file, InstructionDocs docs, EMEVD evd = null) { EMEVDPath = file; this.docs = docs; EVD = evd ?? EMEVD.Read(file); if (File.Exists(file.Replace(".emevd", ".emeld"))) { try { ELD = EMELD.Read(file.Replace(".emevd", ".emeld")); } catch { } } InitAll(); }
public Assignment SplitAll() { // Add skills as item drops in the world. Esoteric texts still work, but they are removed from randomizer pool. Assignment ret = new Assignment(); // First, create skill items, including editing item fmgs SortedDictionary <int, ItemKey> newSkills = new SortedDictionary <int, ItemKey>(); SortedDictionary <int, ItemKey> oldSkills = new SortedDictionary <int, ItemKey>(); Dictionary <int, ItemKey> texts = new Dictionary <int, ItemKey> { [0] = game.ItemForName("Shinobi Esoteric Text"), [1] = game.ItemForName("Prosthetic Esoteric Text"), [2] = game.ItemForName("Ashina Esoteric Text"), [3] = game.ItemForName("Senpou Esoteric Text"), [4] = game.ItemForName("Mushin Esoteric Text"), }; // Note: there are some events in common which can be used to detect skills which are already granted by emevd. // But they all have a text of -1 so it's unnecessary to scan emevd for this. // For example: // Initialize Event (Event Slot ID: 4, Event ID: 450, Parameters: 6719){, 3, 2450, 620} // Initialize Event (Event Slot ID: 0, Event ID: 460, Parameters: 6710){, 2470, 600} PARAM.Row baseGood = game.Params["EquipParamGoods"][2470]; int baseId = 6405; FMG itemName = game.ItemFMGs["アイテム名"]; FMG itemDesc = game.ItemFMGs["アイテム説明"]; FMG weaponName = game.ItemFMGs["武器名"]; FMG weaponDesc = game.ItemFMGs["武器説明"]; SortedDictionary <ItemKey, string> gameNames = game.Names(); bool explain = false; HashSet <int> copiedWeapons = new HashSet <int>(); foreach (PARAM.Row r in game.Params["SkillParam"].Rows) { int skillId = (int)r.ID; if (skillId >= 700) { continue; } int text = (byte)r["Unk7"].Value; if (!texts.ContainsKey(text)) { continue; } int descItem = (int)r["SkilLDescriptionId"].Value; if (copiedWeapons.Contains(descItem)) { continue; } copiedWeapons.Add(descItem); PARAM.Row weaponRow = game.Params["EquipParamWeapon"][descItem]; int sortId = (int)weaponRow["sortId"].Value; short iconId = (short)weaponRow["iconId"].Value; int good = baseId++; PARAM.Row newGood = game.AddRow("EquipParamGoods", good); GameEditor.CopyRow(baseGood, newGood); ItemKey goodKey = new ItemKey(ItemType.GOOD, good); newSkills[skillId] = goodKey; gameNames[goodKey] = weaponName[descItem]; itemName[good] = weaponName[descItem]; itemDesc[good] = weaponDesc[descItem]; newGood["sortId"].Value = sortId; newGood["iconId"].Value = iconId; // These should be set in base row, but do this just in case // Don't show up in inventory newGood["goodsType"].Value = (byte)7; // But pop up on acquisition newGood["Unk20"].Value = (byte)6; if (explain) { Console.WriteLine($"-- {r.ID} -> {good}: {descItem}, {weaponName[descItem]}"); } ret.Assign[new ItemKey(ItemType.WEAPON, descItem)] = goodKey; } game.Params["EquipParamGoods"].Rows.Sort((a, b) => a.ID.CompareTo(b.ID)); // Second, add event scripting to grant skills, with new common_func EMEVD common = game.Emevds["common"]; int grantId = 11615600; EMEVD.Event grantEv = new EMEVD.Event(grantId); List <string> grantCmds = new List <string> { "IF Player Has/Doesn't Have Item (0,3,X0_4,1)", "Grant Skill (X4_4)" }; for (int i = 0; i < grantCmds.Count; i++) { (EMEVD.Instruction ins, List <EMEVD.Parameter> ps) = events.ParseAddArg(grantCmds[i], i); grantEv.Instructions.Add(ins); grantEv.Parameters.AddRange(ps); } common.Events.Add(grantEv); int slot = 0; foreach (KeyValuePair <int, ItemKey> entry in newSkills) { common.Events[0].Instructions.Add(new EMEVD.Instruction(2000, 0, new List <object> { slot++, grantId, entry.Value.ID, entry.Key })); } // Third, edit drops // Remove text drops ann.ItemGroups["remove"].AddRange(texts.Values); // Add skill drops foreach (ItemKey item in newSkills.Values) { data.AddLocationlessItem(item); } // Copy restrictions from the weapons over to the goods ann.CopyRestrictions(ret.Assign); foreach (KeyValuePair <ItemKey, ItemKey> entry in ret.Assign) { ItemKey weapon = entry.Key; ItemKey good = entry.Value; // Mikiri Counter in hint log if (weapon.ID == 200300) { ann.ItemGroups["upgradehints"].Add(good); } // Carp scalesmen if (!ann.ExcludeTags.ContainsKey(weapon)) { ann.ItemGroups["premium"].Add(good); } } // For balancing Dancing Dragon Mask, greatly reduce enemy xp drops // All NG bosses together give 93k XP. This gives enough for 45 skill points. // So allow 15 levels/3 AP upgrades, or 9 XP. (Next threshhold: 4 AP upgrades, 13k XP) // (Or not, since only got 9 levels in a full run, just double it.) foreach (PARAM.Row row in game.Params["NpcParam"].Rows) { row["Experience"].Value = (int)row["Experience"].Value / 5; } foreach (PARAM.Row row in game.Params["GameAreaParam"].Rows) { row["BonusExperience"].Value = (int)row["BonusExperience"].Value / 5; } // Also in this mode, acquire skills option is removed from Sculptor's Idols, in case it has been there from previous runs. Done in PermutationWriter. return(ret); }
public void GenerateAbyssBattleMSB(int mapLevel, List <int> bossCategoriesUsed) { // Basically, create thin New Londo MSB containing boss, then enable "done" flag. // EMEVD handles the rest. Map newLondo = Maps.GetMap("NewLondoRuins"); MSB1 msb = MSB1.Read(ModMSBPath + $"{newLondo.MsbName}.msb"); EMEVD emevd = EMEVD.Read(ModEMEVDPath + $"{newLondo.EmevdName}.emevd.dcx"); EMEVD.Event constructor = emevd.Events.Where(evt => evt.ID == 0).First(); HashSet <int> enemyModels = new HashSet <int>(); HashSet <int> logicGoalIDs = new HashSet <int>(); HashSet <int> battleGoalIDs = new HashSet <int>(); MapPointManager pointManager = new MapPointManager(newLondo.Name, Rand); // ABYSS BOSS int bossEntityID = newLondo.GetBossID(0); if (!Maps.BossLocations.Keys.Contains(bossEntityID)) { throw new Exception($"Could not find information about location for (non-twin) boss ID {bossEntityID}."); } (GamePoint bossPoint, int emevdIndex) = Maps.BossLocations[bossEntityID]; Boss boss = EnemyGenerator.GetRandomBoss(Rand, bossPoint.ArenaSize, bossCategoriesUsed); Enemy bossEnemy = EnemyGenerator.GetEnemy(boss.Name); MSB1.Part.Enemy bossPart = bossEnemy.GetMSBPart("Abyss Boss", bossEntityID, bossPoint, bossPoint.Angle, mapLevel, isRedPhantom: true); msb.Parts.Enemies.Add(bossPart); enemyModels.Add(bossEnemy.ModelID); logicGoalIDs.Add(Mod.GPARAM.AI[bossPart.ThinkParamID].LogicID); battleGoalIDs.Add(Mod.GPARAM.AI[bossPart.ThinkParamID].BattleID); // TWIN BOSSES: // If arena is Large or Giant, there's a chance of a twin boss (capping their combined aggression). // Mornstein ring makes it 100% chance. Boss twinBoss = null; if ((Roll(TwinBossOdds) || Run.GetFlag(GameFlag.MornsteinRingFlag)) && Maps.BossLocations.ContainsKey(bossEntityID + 1) && bossPoint.ArenaSize.In(ArenaSize.Large, ArenaSize.Giant) && boss.AggressionLevel < 5) { (GamePoint twinBossPoint, _) = Maps.BossLocations[bossEntityID + 1]; List <Boss> twinOptions = new List <Boss>(EnemyGenerator.BossList.Where( twin => twin.Category != boss.Category && twin.AggressionLevel + boss.AggressionLevel <= 5 && twin.RequiredArenaSize <= bossPoint.ArenaSize)); twinBoss = twinOptions.GetRandomElement(Rand); Enemy twinBossEnemy = EnemyGenerator.GetEnemy(twinBoss.Name); var twinPart = twinBossEnemy.GetMSBPart($"Abyss Boss Twin", bossEntityID + 1, twinBossPoint, twinBossPoint.Angle, mapLevel, isRedPhantom: true); msb.Parts.Enemies.Add(twinPart); logicGoalIDs.Add(Mod.GPARAM.AI[twinPart.ThinkParamID].LogicID); battleGoalIDs.Add(Mod.GPARAM.AI[twinPart.ThinkParamID].BattleID); Run.BossCategoriesUsed.Add(twinBoss.Category); enemyModels.Add(twinBossEnemy.ModelID); Run.EnableFlag(newLondo.GetBossTwinFlag(0)); } else { Run.DisableFlag(newLondo.GetBossTwinFlag(0)); } // Inject name and item lot reward into EMEVD constructor. EMEVD.Instruction bossBattleCall = constructor.Instructions[emevdIndex]; List <byte> argData = bossBattleCall.ArgData.ToList(); int itemLotID = bossEnemy.RedPhantomItemLotParamID; argData[32] = (byte)itemLotID; argData[33] = (byte)(itemLotID >> 0x08); argData[34] = (byte)(itemLotID >> 0x10); argData[35] = (byte)(itemLotID >> 0x18); argData[52] = (byte)boss.NameTextID; argData[53] = (byte)(boss.NameTextID >> 0x08); if (twinBoss != null) { argData[54] = (byte)twinBoss.NameTextID; argData[55] = (byte)(twinBoss.NameTextID >> 0x08); } bossBattleCall.ArgData = argData.ToArray(); #if DEBUG Console.WriteLine($" Updating models..."); #endif UpdateMSBModels(msb); #if DEBUG Console.WriteLine($" Writing MSB..."); #endif msb.Write(Mod.GameDir + $@"map\MapStudio\{newLondo.MsbName}.msb"); // No '.rls' copy needed. #if DEBUG Console.WriteLine($" Writing EMEVD..."); #endif emevd.Write(Mod.GameDir + $@"event\{newLondo.EmevdName}.emevd.dcx"); #if DEBUG Console.WriteLine($" Writing LUABND..."); #endif WriteMapLUABND(newLondo.EmevdName, battleGoalIDs, logicGoalIDs); #if DEBUG Console.WriteLine($" Writing FFXBND..."); #endif WriteMapFFXBND(newLondo.MapID[0], enemyModels); }
public void GenerateMapData(Map map, double redPhantomOdds) { // Generate and write MSB, LUABND, and tweaked EMEVD for given map. #if DEBUG Console.WriteLine($"GENERATING MAP: {map.Name}"); #endif MSB1 msb = MSB1.Read(ModMSBPath + $"{map.MsbName}.msb"); EMEVD emevd = EMEVD.Read(ModEMEVDPath + $"{map.EmevdName}.emevd.dcx"); EMEVD.Event constructor = emevd.Events.Where(evt => evt.ID == 0).First(); HashSet <int> enemyModels = new HashSet <int>(); HashSet <int> logicGoalIDs = new HashSet <int>(map.DefaultLogicScriptIDs); HashSet <int> battleGoalIDs = new HashSet <int>(map.DefaultBattleScriptIDs); MapPointManager pointManager = new MapPointManager(map.Name, Rand); int rarePhantomCount = 0; int basicPhantomCount = 0; string regionLabel; for (int i = 0; i < map.BossCount; i++) { // BOSSES int bossEntityID = map.GetBossID(i); if (!Maps.BossLocations.Keys.Contains(bossEntityID)) { throw new Exception($"Could not find information about location for (non-twin) boss ID {bossEntityID}."); } (GamePoint bossPoint, int emevdIndex) = Maps.BossLocations[bossEntityID]; Boss boss = EnemyGenerator.GetRandomBoss(Rand, bossPoint.ArenaSize, Run.BossCategoriesUsed); #if DEBUG Console.WriteLine($"Boss {i}: {boss.Name}"); #endif Enemy bossEnemy = EnemyGenerator.GetEnemy(boss.Name); bool isRedPhantom = map.Name == "ChasmOfTheAbyss" || boss.AlwaysRedPhantom; // Red phantom bosses are otherwise fought in Abyss battles only. var part = bossEnemy.GetMSBPart($"Boss {i}", bossEntityID, bossPoint, bossPoint.Angle, Run.MapLevel, isRedPhantom); msb.Parts.Enemies.Add(part); logicGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].LogicID); battleGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].BattleID); Run.BossCategoriesUsed.Add(boss.Category); enemyModels.Add(bossEnemy.ModelID); // TWIN BOSSES: // If arena is Large or Giant, there's a chance of a twin boss (capping their combined aggression). // Mornstein's ring makes it 100% chance whenever possible. Boss twinBoss = null; if ((Roll(TwinBossOdds) || Run.GetFlag(GameFlag.MornsteinRingFlag)) && Maps.BossLocations.ContainsKey(bossEntityID + 1) && bossPoint.ArenaSize.In(ArenaSize.Large, ArenaSize.Giant) && boss.AggressionLevel < 5) { (GamePoint twinBossPoint, _) = Maps.BossLocations[bossEntityID + 1]; List <Boss> twinOptions = new List <Boss>(EnemyGenerator.BossList.Where( twin => twin.Category != boss.Category && twin.AggressionLevel + boss.AggressionLevel <= 5 && twin.RequiredArenaSize <= bossPoint.ArenaSize)); twinBoss = twinOptions.GetRandomElement(Rand); Enemy twinBossEnemy = EnemyGenerator.GetEnemy(twinBoss.Name); bool twinIsRedPhantom = map.Name == "ChasmOfTheAbyss" || twinBoss.AlwaysRedPhantom; // Red phantom bosses are otherwise fought in Abyss battles only. var twinPart = twinBossEnemy.GetMSBPart($"Boss {i} Twin", bossEntityID + 1, twinBossPoint, twinBossPoint.Angle, Run.MapLevel, twinIsRedPhantom); msb.Parts.Enemies.Add(twinPart); logicGoalIDs.Add(Mod.GPARAM.AI[twinPart.ThinkParamID].LogicID); battleGoalIDs.Add(Mod.GPARAM.AI[twinPart.ThinkParamID].BattleID); Run.BossCategoriesUsed.Add(twinBoss.Category); enemyModels.Add(twinBossEnemy.ModelID); Run.EnableFlag(map.GetBossTwinFlag(i)); } else { Run.DisableFlag(map.GetBossTwinFlag(i)); // e.g. may have been set for New Londo Abyss boss previously } if (map.Name != "Lost Izalith") { // Inject name and item lot reward into EMEVD constructor. EMEVD.Instruction bossBattleCall = constructor.Instructions[emevdIndex]; List <byte> argData = bossBattleCall.ArgData.ToList(); int itemLotID = isRedPhantom ? bossEnemy.RedPhantomItemLotParamID : bossEnemy.ItemLotParamID; // twin item lot ignored argData[32] = (byte)itemLotID; argData[33] = (byte)(itemLotID >> 0x08); argData[34] = (byte)(itemLotID >> 0x10); argData[35] = (byte)(itemLotID >> 0x18); argData[52] = (byte)boss.NameTextID; argData[53] = (byte)(boss.NameTextID >> 0x08); if (twinBoss != null) { argData[54] = (byte)twinBoss.NameTextID; argData[55] = (byte)(twinBoss.NameTextID >> 0x08); } bossBattleCall.ArgData = argData.ToArray(); } } if (Roll(InvaderOdds)) { // INVADER if (map.Name == "Blighttown") { regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty"; } else { regionLabel = ""; } int invaderIndex = Run.InvadersAvailable.GetRandomElement(Rand); int invaderParamID = 7000 + 10 * invaderIndex; int invaderEntityID = map.BaseNPCEntityID + 50; GamePoint spawnPoint = pointManager.CheckOutRandomPoint("Invader", (int)ChrSize.Normal); GamePoint triggerPoint = pointManager.CheckOutRandomPointWithinDistance("Invader Trigger", spawnPoint, 20.0, 40.0); float angle = MapPointManager.GetFacingPoint(spawnPoint, triggerPoint); MSB1.Part.Enemy invader = GetInvaderPart(invaderEntityID, spawnPoint, angle, invaderParamID, Run.MapLevel); msb.Parts.Enemies.Add(invader); Run.InvadersAvailable.Remove(invaderIndex); Run.InvadersUsed.Add(invaderIndex); Run.EnableFlag(GameFlag.InvaderUsedBaseFlag + invaderIndex); MSB1.Region triggerRegion = new MSB1.Region() { Name = "Invader Trigger", Position = triggerPoint.Position - new Vector3(0.0f, 1.0f, 0.0f), // 1.0 units below point Shape = new MSB1.Shape.Box() { Width = 20.0f, Depth = 20.0f, Height = 10.0f }, EntityID = map.InvaderTriggerRegionID, }; msb.Regions.Regions.Add(triggerRegion); MSB1.Region spawnRegion = new MSB1.Region() { Name = "Invader Spawn", Position = spawnPoint.Position, Shape = new MSB1.Shape.Point(), EntityID = map.InvaderSpawnPointID, }; msb.Regions.Regions.Add(spawnRegion); } if (Roll(VeryRareEnemyOdds * (Run.GetFlag(GameFlag.MornsteinRingFlag) ? 2 : 1))) { // VERY RARE ENEMY if (map.Name == "Blighttown") { regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty"; } else { regionLabel = ""; } bool isRedPhantom = Roll(redPhantomOdds); int entityID = map.BaseEntityID + VeryRareEnemyOffset; Enemy enemy = EnemyGenerator.GetRandomEnemyWithRarity(Rand, EnemyRarity.VeryRare, Run.MapLabels[map]); GamePoint point = pointManager.CheckOutRandomPoint("VeryRare Enemy", (int)enemy.Size); // don't care about region label if (map.Name == "NewLondoRuins" && point.RegionLabel == "Lower") { entityID += NewLondoVeryRareEnemyOffset; } var part = enemy.GetMSBPart("VeryRare Enemy 1", entityID, point, GetRandomAngle(), Run.MapLevel, isRedPhantom); msb.Parts.Enemies.Add(part); logicGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].LogicID); battleGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].BattleID); enemyModels.Add(enemy.ModelID); } if (!map.Name.In("PaintedWorld", "ChasmOfTheAbyss", "NewLondoRuins") && (Run.GetFlag(GameFlag.LoganRingFlag) || Roll(AbyssPortalOdds))) { // ABYSS PORTAL GamePoint point = pointManager.CheckOutRandomPoint("Abyss Portal", 0); MSB1.Part.Player portalWarpBack = new MSB1.Part.Player() { Name = "Portal Return", Position = point.Position, Rotation = new Vector3(0.0f, GetRandomAngle(), 0.0f), EntityID = map.PortalWarpPointID, IsShadowSrc = 1, IsShadowDest = 1, ModelName = "c0000", }; msb.Parts.Players.Add(portalWarpBack); MSB1.Region portalPoint = new MSB1.Region() { Name = "Portal VFX Point", Position = point.Position + new Vector3(0.0f, 2.0f, 0.0f), Shape = new MSB1.Shape.Point(), EntityID = -1, }; msb.Regions.Regions.Add(portalPoint); MSB1.Event.SFX portalVFX = new MSB1.Event.SFX() { Name = "Portal VFX", FFXID = 120027, RegionName = "Portal VFX Point", PartName = point.CollisionName, EntityID = map.PortalFXID, }; msb.Events.SFXs.Add(portalVFX); MSB1.Part.Enemy portalTrigger = new MSB1.Part.Enemy() { ModelName = "c1000", Name = "Portal Trigger", Position = point.Position + new Vector3(0.0f, 0.5f, 0.0f), EntityID = map.PortalTriggerCharacterID, NPCParamID = 100000, TalkID = 0, CollisionName = point.CollisionName, ThinkParamID = 1, }; msb.Parts.Enemies.Add(portalTrigger); } if (!Run.MapsVisited.Contains(Maps.GetMap("PaintedWorld")) && Roll(PaintingOdds)) { // Painting appears, in a random choice of its possible positions (pre-existing in base MSB). // TODO: 3-4 positions per map. Wherever it fits. Proper collision assignment, etc. Also prompt regions for simplicity. } foreach (Merchant merchant in CharacterGenerator.MerchantList) { // MERCHANTS if (Run.GetFlag(merchant.DeadFlag)) { continue; // Merchant is dead and cannot appear again. } if (map.Name == "Blighttown") { regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty"; } else { regionLabel = ""; } double merchantOdds = merchant.Name == "Marvelous Chester" ? ChesterOdds : MerchantOdds; if (Roll(merchantOdds)) { if (map.Name == "NewLondoRuins") { regionLabel = "Upper"; // no merchants in lower } GamePoint point = pointManager.CheckOutRandomPointCustomNearbyRadius("Merchant", 2.0, 5, regionLabel); var part = merchant.GetPart(point, pointManager.GetAngleFacingNearestPoint(point), map.TalkIDBase); msb.Parts.Enemies.Add(part); logicGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].LogicID); battleGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].BattleID); } } int chestCount = Math.Max(0, Rand.Next(map.ChestCount - 2, map.ChestCount)); List <int> usedChestTreasure = new List <int>(); for (int i = 0; i < chestCount; i++) { // CHESTS if (map.Name == "Blighttown") { regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty"; } else { regionLabel = ""; } int chestID = map.BaseChestEntityID + i; GamePoint point = pointManager.CheckOutRandomPointCustomNearbyRadius("Chest", 2.0, 5, regionLabel); float angle = pointManager.GetAngleFacingNearestPoint(point); var part = GetChestPart(i, chestID, angle, point); msb.Parts.Objects.Add(part); var objAct = new MSB1.Event.ObjAct() { Name = $"Chest ObjAct {i}", ObjActEntityID = map.BaseChestObjActFlagID + i, ObjActPartName = $"Chest {i}", EventFlagID = 0, }; msb.Events.ObjActs.Add(objAct); var treasure = new MSB1.Event.Treasure() { Name = $"Chest Treasure {i}", TreasurePartName = $"Chest {i}", PartName = point.CollisionName, InChest = true, StartDisabled = true, // can't get the treasure to disable itself based on 'InChest' alone... }; // Choose random treasure in unlocked range. treasure.ItemLots[0] = GetChestTreasureID(map.BaseChestItemLotID, usedChestTreasure); usedChestTreasure.Add(treasure.ItemLots[0]); msb.Events.Treasures.Add(treasure); } if (Roll(MimicOdds)) { // MIMIC if (map.Name == "Blighttown") { regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty"; } else { regionLabel = ""; } bool isRedPhantom = Roll(redPhantomOdds); int mimicEntityID = map.BaseEntityID + MimicOffset; if (map.Name == "NewLondoRuins") { regionLabel = "Upper"; // no Mimics in lower } Enemy mimic = EnemyGenerator.GetEnemy("Mimic"); GamePoint point = pointManager.CheckOutRandomPoint("Mimic", (int)mimic.Size, regionLabel); float angle = pointManager.GetAngleFacingNearestPoint(point); var part = mimic.GetMSBPart("Mimic", mimicEntityID, point, angle, Run.MapLevel, isRedPhantom); msb.Parts.Enemies.Add(part); logicGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].LogicID); battleGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].BattleID); enemyModels.Add(mimic.ModelID); MSB1.Region mimicNest = new MSB1.Region() { Name = "Mimic Nest", Position = new Vector3(point.Position.X, point.Position.Y - 0.1f, point.Position.Z), Rotation = new Vector3(0.0f, angle, 0.0f), Shape = new MSB1.Shape.Box() { Width = 5.0f, Depth = 5.0f, Height = 2.0f }, EntityID = mimicEntityID + 5, }; msb.Regions.Regions.Add(mimicNest); } int itemCount = Math.Max(0, Rand.Next(map.ItemCorpseCount - 5, map.ItemCorpseCount)); List <int> usedCorpseTreasure = new List <int>(); for (int i = 0; i < itemCount; i++) { // ITEM CORPSES if (map.Name == "Blighttown") { regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty"; } else { regionLabel = ""; } GamePoint point = pointManager.CheckOutRandomPointWeightedInverseNearby($"Item Corpse {i}", 0, regionLabel); int pose = Objects.CorpsePoses.GetRandomElement(Rand); float poseOffset = Objects.CorpsePoseYOffsets[pose]; MSB1.Part.Object corpse = new MSB1.Part.Object() { ModelName = "o0500", // Not using o0502 or o0504, as they have different pose enums. Name = $"Item Corpse {i}", CollisionName = point.CollisionName, Position = new Vector3(point.Position.X, point.Position.Y + poseOffset, point.Position.Z), Rotation = new Vector3(0.0f, GetRandomAngle(), 0.0f), EntityID = -1, // not needed ObjectPose = (short)pose, }; corpse.ClearDrawGroups(); corpse.ClearDispGroups(); msb.Parts.Objects.Add(corpse); MSB1.Event.Treasure treasure = new MSB1.Event.Treasure() { Name = $"Item Corpse Treasure {i}", TreasurePartName = $"Item Corpse {i}", PartName = point.CollisionName, InChest = false, }; // Assign random corpse treasure from unlocked ranges. treasure.ItemLots[0] = GetCorpseTreasureID(map.BaseCorpseItemLotID, usedCorpseTreasure); usedCorpseTreasure.Add(treasure.ItemLots[0]); msb.Events.Treasures.Add(treasure); } int rareCount = Math.Max(0, Rand.Next(map.RareEnemyCount - 3, map.RareEnemyCount)); List <Enemy> usedRareEnemies = new List <Enemy>(); for (int i = 0; i < rareCount; i++) { // RARE ENEMIES if (map.Name == "Blighttown") { regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty"; } else if (map.Name == "NewLondoRuins" && i == 0) { regionLabel = "Upper"; // first rare enemy is always in Upper New Londo Ruins (for Holy Sigil drop) } else { regionLabel = ""; } Enemy enemy; if (usedRareEnemies.Count > MaxRareEnemyTypeCount) { enemy = usedRareEnemies.GetRandomElement(Rand); } else { enemy = EnemyGenerator.GetRandomEnemyWithRarity(Rand, EnemyRarity.Rare, Run.MapLabels[map]); usedRareEnemies.Add(enemy); } bool isRedPhantom = rarePhantomCount < 15 && Roll(redPhantomOdds); GamePoint point = pointManager.CheckOutRandomPoint("Rare Enemy", (int)enemy.Size, regionLabel); int entityID = map.BaseEntityID + RareEnemyOffset + i; if (map.Name == "NewLondoRuins" && point.RegionLabel == "Lower") { entityID += NewLondoLowerRareEnemyOffset; } MSB1.Part.Enemy enemyPart = enemy.GetMSBPart($"Rare Enemy {i}", entityID, point, GetRandomAngle(), Run.MapLevel, isRedPhantom); msb.Parts.Enemies.Add(enemyPart); logicGoalIDs.Add(Mod.GPARAM.AI[enemyPart.ThinkParamID].LogicID); battleGoalIDs.Add(Mod.GPARAM.AI[enemyPart.ThinkParamID].BattleID); enemyModels.Add(enemy.ModelID); if (isRedPhantom) { rarePhantomCount++; } } int basicEnemyCount = Math.Max(0, Rand.Next(map.BasicEnemyCount - 5, map.BasicEnemyCount)); int basicNonPhantomCount = 0; List <Enemy> usedCommonEnemyTypes = new List <Enemy>(); List <Enemy> usedUncommonEnemyTypes = new List <Enemy>(); for (int i = 0; i < basicEnemyCount; i++) { // BASIC (COMMON/UNCOMMON) ENEMIES if (map.Name == "Blighttown") { regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty"; } else { regionLabel = ""; } Enemy enemy; EnemyRarity rarity = Roll(UncommonEnemyOdds) ? EnemyRarity.Uncommon : EnemyRarity.Common; if (rarity == EnemyRarity.Common) { if (usedCommonEnemyTypes.Count > MaxCommonEnemyTypeCount) { enemy = usedCommonEnemyTypes.GetRandomElement(Rand); } else { enemy = EnemyGenerator.GetRandomEnemyWithRarity(Rand, rarity, Run.MapLabels[map]); usedCommonEnemyTypes.Add(enemy); } } else { if (usedUncommonEnemyTypes.Count > MaxUncommonEnemyTypeCount) { enemy = usedUncommonEnemyTypes.GetRandomElement(Rand); } else { enemy = EnemyGenerator.GetRandomEnemyWithRarity(Rand, rarity, Run.MapLabels[map]); usedUncommonEnemyTypes.Add(enemy); } } bool isRedPhantom = basicPhantomCount < 15 && Roll(redPhantomOdds); int entityID = map.BaseEntityID + (isRedPhantom ? BasicPhantomOffset + basicPhantomCount : BasicEnemyOffset + basicNonPhantomCount); GamePoint point = pointManager.CheckOutRandomPoint("Basic Enemy", (int)enemy.Size, regionLabel); if (map.Name == "NewLondoRuins" && point.RegionLabel == "Lower") { entityID += isRedPhantom ? NewLondoLowerBasicPhantomOffset : NewLondoLowerBasicEnemyOffset; } MSB1.Part.Enemy enemyPart = enemy.GetMSBPart($"Basic Enemy {i}", entityID, point, GetRandomAngle(), Run.MapLevel, isRedPhantom); msb.Parts.Enemies.Add(enemyPart); logicGoalIDs.Add(Mod.GPARAM.AI[enemyPart.ThinkParamID].LogicID); battleGoalIDs.Add(Mod.GPARAM.AI[enemyPart.ThinkParamID].BattleID); enemyModels.Add(enemy.ModelID); if (isRedPhantom) { basicPhantomCount++; } else { basicNonPhantomCount++; } } int vagrantCount = Rand.Next(3); for (int i = 0; i < vagrantCount; i++) { // VAGRANTS (max of 2) if (map.Name == "Blighttown") { regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty"; } else { regionLabel = ""; } if (map.Name == "NewLondoRuins") { regionLabel = "Upper"; // No Vagrants in Lower New Londo } bool isRedPhantom = Run.GetFlag(GameFlag.MornsteinRingFlag) ? Roll(0.2) : Roll(0.1); int entityID = map.BaseEntityID + VagrantOffset + i; Enemy enemy = EnemyGenerator.GetEnemy(Roll(0.5) ? "Good Vagrant" : "Evil Vagrant"); GamePoint point = pointManager.CheckOutRandomPoint("Vagrant", (int)enemy.Size, regionLabel); var part = enemy.GetMSBPart($"Vagrant {i}", entityID, point, GetRandomAngle(), Run.MapLevel, isRedPhantom); msb.Parts.Enemies.Add(part); logicGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].LogicID); battleGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].BattleID); enemyModels.Add(enemy.ModelID); } #if DEBUG Console.WriteLine($" Updating models..."); #endif UpdateMSBModels(msb); // Output MSB. Also output a '.rls' copy, so it can be reloaded and modified at bonfire creation. #if DEBUG Console.WriteLine($" Writing MSB..."); #endif msb.Write(Mod.GameDir + $@"map\MapStudio\{map.MsbName}.msb"); msb.Write(Mod.GameDir + $@"map\MapStudio\{map.MsbName}.msb.rls"); // file to modify for bonfire creation #if DEBUG Console.WriteLine($" Writing EMEVD..."); #endif emevd.Write(Mod.GameDir + $@"event\{map.EmevdName}.emevd.dcx"); #if DEBUG Console.WriteLine($" Writing LUABND..."); #endif WriteMapLUABND(map.EmevdName, battleGoalIDs, logicGoalIDs); #if DEBUG Console.WriteLine($" Writing FFXBND..."); #endif WriteMapFFXBND(map.MapID[0], enemyModels); }
// Usages // Usages for display names of these symbols: instruction name, enum name, enum value private Dictionary <string, Usages> GetSymbolUsages(string game, string emevdDir, InstructionDocs docs) { Dictionary <string, HashSet <string> > symbolsByFile = new Dictionary <string, HashSet <string> >(); HashSet <string> allSymbols = new HashSet <string>(); interestingEmevds.TryGetValue(game, out Regex mainRegex); List <string> files = new List <string>(); Console.WriteLine($"------ Usages from [{emevdDir}]"); foreach (string emevdPath in Directory.GetFiles(emevdDir)) { if (mainRegex != null && !mainRegex.Match(Path.GetFileName(emevdPath)).Success) { continue; } string name = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(emevdPath)); files.Add(name); Console.WriteLine($"--- {name}"); HashSet <string> symbols = symbolsByFile[name] = new HashSet <string>(); EMEVD emevd = EMEVD.Read(emevdPath); foreach (EMEVD.Event evt in emevd.Events) { for (int insIndex = 0; insIndex < evt.Instructions.Count; insIndex++) { // This is all very best-effort EMEVD.Instruction ins = evt.Instructions[insIndex]; EMEDF.InstrDoc doc = docs.DOC[ins.Bank]?[ins.ID]; if (doc == null) { continue; } symbols.Add(doc.DisplayName); Dictionary <EMEVD.Parameter, string> paramNames = docs.ParamNames(evt); try { // A slight abuse of this function, ignoring the returned list docs.UnpackArgsWithParams(ins, insIndex, doc, paramNames, (argDoc, val) => { if (argDoc.GetDisplayValue(val) is string displayStr) { symbols.Add(displayStr); } return(val); }); // Also add a usage if the enum is present at all, even if parameterized foreach (EMEDF.ArgDoc argDoc in doc.Arguments) { if (argDoc.EnumDoc != null && argDoc.EnumName != "BOOL") { symbols.Add(argDoc.EnumDoc.DisplayName); } } } catch { } } } allSymbols.UnionWith(symbols); } Dictionary <string, Usages> symbolUsages = new Dictionary <string, Usages>(); List <string> primaryFiles = null; if (secondaryEmevd.TryGetValue(game, out Regex secondaryRegex)) { primaryFiles = files.Where(f => !secondaryRegex.Match(f).Success).ToList(); } foreach (string symbol in allSymbols) { List <string> matchFiles = files.Where(f => symbolsByFile[f].Contains(symbol)).ToList(); List <string> target = files; if (primaryFiles != null) { List <string> primaryMatchFiles = matchFiles.Intersect(primaryFiles).ToList(); if (primaryMatchFiles.Count > 0) { matchFiles = primaryMatchFiles; target = primaryFiles; } } // Combining PTDE and DS1R is done after. symbolUsages[symbol] = new Usages { Files = matchFiles, AllFiles = target }; } return(symbolUsages); }
public Result FindItems(RandomizerOptions opt, AnnotationData ann, Graph g, Events events, string gameDir, FromGame game) { Dictionary <string, string> itemsById = ann.KeyItems.ToDictionary(item => item.ID, item => item.Name); Dictionary <string, List <string> > itemAreas = g.ItemAreas; if (ann.LotLocations != null) { GameEditor editor = new GameEditor(game); editor.Spec.GameDir = gameDir; Dictionary <string, PARAM.Layout> layouts = editor.LoadLayouts(); Dictionary <string, PARAM> Params = editor.LoadParams(layouts); Dictionary <int, PARAM.Row> lots = Params["ItemLotParam"].Rows.ToDictionary(r => (int)r.ID, r => r); foreach (KeyValuePair <int, string> entry in ann.LotLocations) { int lot = entry.Key; if (!g.Areas.ContainsKey(entry.Value)) { throw new Exception($"Internal error in lot config for {entry.Key}: {entry.Value} does not exist"); } while (true) { // It's also fine to not have a lot defined as long as all key items are found if (!lots.TryGetValue(lot, out PARAM.Row row)) { break; } for (int i = 1; i <= 8; i++) { int item = (int)row[$"lotItemId0{i}"].Value; if (item == 0) { continue; } int category = (int)row[$"lotItemCategory0{i}"].Value; category = Universe.LotTypes.TryGetValue((uint)category, out int value) ? value : -1; if (category == -1) { continue; } string id = $"{category}:{item}"; if (opt["debuglots"]) { Console.WriteLine($"{entry.Key} in {entry.Value} has {id}"); } if (itemsById.TryGetValue(id, out string name)) { if (itemAreas[name].Count > 0 && itemAreas[name][0] != entry.Value) { throw new Exception($"Item {name} found in both {itemAreas[name][0]} and {entry.Value}"); } itemAreas[name] = new List <string> { entry.Value }; } } lot++; } } } if (ann.Locations != null) { // It's a bit hacky, but should work from anywhere, probably GameEditor editor = new GameEditor(game); editor.Spec.GameDir = $@"fogdist"; editor.Spec.LayoutDir = $@"fogdist\Layouts"; editor.Spec.NameDir = $@"fogdist\Names"; Dictionary <string, PARAM.Layout> layouts = editor.LoadLayouts(); int dragonFlag = -1; Dictionary <string, PARAM> Params = editor.LoadParams(@"fogdist\Base\Data0.bdt", layouts, true); if (gameDir != null) { string paramPath = $@"{gameDir}\Data0.bdt"; if (File.Exists(paramPath)) { Params = editor.LoadParams(paramPath, layouts, true); } string commonEmevdPath = $@"{gameDir}\event\common.emevd.dcx"; if (File.Exists(commonEmevdPath)) { EMEVD commonEmevd = EMEVD.Read(commonEmevdPath); EMEVD.Event flagEvent = commonEmevd.Events.Find(e => e.ID == 13000904); if (flagEvent != null) { Events.Instr check = events.Parse(flagEvent.Instructions[1]); dragonFlag = (int)check[3]; if (opt["debuglots"]) { Console.WriteLine($"Dragon flag: {dragonFlag}"); } } } } Dictionary <int, PARAM.Row> lots = Params["ItemLotParam"].Rows.ToDictionary(r => (int)r.ID, r => r); Dictionary <int, PARAM.Row> shops = Params["ShopLineupParam"].Rows.ToDictionary(r => (int)r.ID, r => r); void setArea(string itemName, List <string> areas) { if (opt["debuglots"]) { Console.WriteLine($"-- name: {itemName}"); } if (itemAreas[itemName].Count > 0 && !itemAreas[itemName].SequenceEqual(areas)) { throw new Exception($"Item {itemName} found in both {string.Join(",", itemAreas[itemName])} and {string.Join(",", areas)}"); } itemAreas[itemName] = areas; } foreach (KeyItemLoc loc in ann.Locations.Items) { List <string> areas = loc.Area.Split(' ').ToList(); if (!areas.All(a => g.Areas.ContainsKey(a) || itemAreas.ContainsKey(a))) { // Currently happens with multi-area intersection lots/shops throw new Exception($"Warning: Areas not found for {loc.Area} - {loc.DebugText[0]}"); } List <int> lotIds = loc.Lots == null ? new List <int>() : loc.Lots.Split(' ').Select(i => int.Parse(i)).ToList(); foreach (int baseLot in lotIds) { int lot = baseLot; while (true) { // It's also fine to not have a lot defined as long as all key items are found if (!lots.TryGetValue(lot, out PARAM.Row row)) { break; } for (int i = 1; i <= 8; i++) { int item = (int)row[$"ItemLotId{i}"].Value; if (item == 0) { continue; } uint category = (uint)row[$"LotItemCategory0{i}"].Value; if (!Universe.LotTypes.TryGetValue(category, out int catVal)) { continue; } string id = $"{catVal}:{item}"; if (opt["debuglots"]) { Console.WriteLine($"lot {lot} in {loc.Area} has {id}"); } if (itemsById.TryGetValue(id, out string name)) { setArea(name, areas); } } if (dragonFlag > 0 && (int)row["getItemFlagId"].Value == dragonFlag) { setArea("pathofthedragon", areas); } lot++; } } List <int> shopIds = loc.Shops == null ? new List <int>() : loc.Shops.Split(' ').Select(i => int.Parse(i)).ToList(); foreach (int shopId in shopIds) { // Not as fine for a shop to be missing, but also whatever if (!shops.TryGetValue(shopId, out PARAM.Row row)) { continue; } int item = (int)row["EquipId"].Value; int catVal = (byte)row["equipType"].Value; string id = $"{catVal}:{item}"; if (opt["debuglots"]) { Console.WriteLine($"shop {shopId} in {loc.Area} has {id}"); } if (itemsById.TryGetValue(id, out string name)) { setArea(name, areas); } if (dragonFlag > 0 && (int)row["EventFlag"].Value == dragonFlag) { setArea("pathofthedragon", areas); } } } } // lots:.*[1-9]\r // Iterative approach for items which depend simply on other items // Recursion would look a lot nicer but lazy bool itemExpanded; do { itemExpanded = false; foreach (KeyValuePair <string, List <string> > entry in itemAreas) { foreach (string dep in entry.Value.ToList()) { if (itemAreas.TryGetValue(dep, out List <string> deps)) { entry.Value.Remove(dep); entry.Value.AddRange(deps); itemExpanded = true; } } } }while (itemExpanded); if (opt["explain"] || opt["debuglots"]) { foreach (Item item in ann.KeyItems) { Console.WriteLine($"{item.Name} {item.ID}: default {item.Area}, found [{string.Join(", ", itemAreas[item.Name])}]"); } } // Collect items in graph SortedSet <string> itemRecord = new SortedSet <string>(); bool randomized = false; foreach (Item item in ann.KeyItems) { if (itemAreas[item.Name].Count == 0) { if (item.HasTag("randomonly")) { itemAreas[item.Name] = new List <string> { item.Area }; } else if (item.HasTag("hard") && !opt["hard"]) { continue; } else { throw new Exception($"Couldn't find {item.Name} in item lots"); } } List <string> areas = itemAreas[item.Name]; foreach (string area in areas) { g.Nodes[area].Items.Add(item.Name); } if (!item.HasTag("randomonly")) { if (areas.Count > 1 || areas[0] != item.Area) { randomized = true; } itemRecord.Add($"{item.Name}={string.Join(",", areas)}"); } } return(new Result { Randomized = randomized, ItemHash = (RandomizerOptions.JavaStringHash($"{string.Join(";", itemRecord)}") % 99999).ToString().PadLeft(5, '0') }); }
private bool OpenEMEVDFile( string fileName, string gameDocs, EMEVD evd = null, string jsText = null, bool isFancy = false, Dictionary <string, string> extraFields = null) { // Can reuse docs if for the same game if (!AllDocs.TryGetValue(gameDocs, out InstructionDocs docs)) { docs = AllDocs[gameDocs] = new InstructionDocs(gameDocs); } ScriptSettings settings = new ScriptSettings(docs, extraFields); EventScripter scripter = new EventScripter(fileName, docs, evd); string fileVersion = ProgramVersion.VERSION; if (jsText == null) { try { if (isFancy && docs.Translator != null) { jsText = new FancyEventScripter(scripter, docs, settings.CFGOptions).Unpack(); } else { jsText = scripter.Unpack(); } } catch (Exception ex) { // Also try to do it in compatibility mode, for emevd files which are no longer allowed, such as changing EMEDFs. try { jsText = scripter.Unpack(compatibilityMode: true); } catch { // If this also fails, we only care about the original exception. } if (jsText == null) { MessageBox.Show(ex.Message); } else { StringBuilder sb = new StringBuilder(); sb.AppendLine(ex.Message); sb.AppendLine("Proceed anyway? You will have to fix instruction arguments before resaving."); DialogResult result = MessageBox.Show(sb.ToString(), "Error", MessageBoxButtons.YesNoCancel); if (result != DialogResult.Yes) { jsText = null; } } if (jsText == null) { return(false); } } } else { fileVersion = extraFields != null && extraFields.TryGetValue("version", out string version) ? version : null; } if (Editor != null) { display.Panel2.Controls.Clear(); SharedControls.RemoveEditor(Editor); Editor.Dispose(); } Editor = new EditorGUI(SharedControls, scripter, docs, settings, fileVersion, jsText); SharedControls.AddEditor(Editor); SharedControls.RefreshGlobalStyles(); display.Panel2.Controls.Add(Editor); // PerformLayout(); Text = $"DARKSCRIPT 3 - {scripter.FileName}"; // Notify about possible compatibility issues int versionCmp = ProgramVersion.CompareVersions(ProgramVersion.VERSION, fileVersion); if (versionCmp > 0) { SharedControls.SetStatus("Note: File was previously saved using an earlier version of DarkScript3"); } else if (versionCmp < 0) { SharedControls.SetStatus("Note: File was previously saved using an newer version of DarkScript3. Please update!"); } return(true); }
private bool OpenJSFile(string fileName) { string org = fileName.Substring(0, fileName.Length - 3); string text = File.ReadAllText(fileName); Dictionary <string, string> headers = GetHeaderValues(text); List <string> emevdFileHeaders = new List <string> { "docs", "compress", "game", "string", "linked" }; EMEVD evd; string docs; if (emevdFileHeaders.All(name => headers.ContainsKey(name))) { docs = headers["docs"]; if (!Enum.TryParse(headers["compress"], out DCX.Type compression)) { // TODO look at SekiroDFLT if (Enum.TryParse(headers["compress"], out DCX.DefaultType defaultComp)) { compression = (DCX.Type)defaultComp; } else { throw new Exception($"Unknown compression type in file header {headers["compress"]}"); } } if (!Enum.TryParse(headers["game"], out EMEVD.Game game)) { throw new Exception($"Unknown game type in file header {headers["game"]}"); } string linked = headers["linked"].TrimStart('[').TrimEnd(']'); evd = new EMEVD() { Compression = compression, Format = game, StringData = Encoding.Unicode.GetBytes(headers["string"]), LinkedFileOffsets = Regex.Split(linked, @"\s*,\s*") .Where(o => !string.IsNullOrWhiteSpace(o)) .Select(o => long.Parse(o)) .ToList() }; } else if (!File.Exists(org)) { MessageBox.Show($"{fileName} requires either a corresponding emevd file or JS headers to open"); return(false); } else { evd = null; docs = ChooseGame(); if (docs == null) { return(false); } } text = Regex.Replace(text, @"(^|\n)\s*// ==EMEVD==(.|\n)*// ==/EMEVD==", ""); return(OpenEMEVDFile(org, docs, evd: evd, jsText: text.Trim(), extraFields: headers)); }
public void WriteEventConfig(AnnotationData ann, Events events, RandomizerOptions opt) { GameEditor editor = new GameEditor(FromGame.DS3); editor.Spec.GameDir = "fogdist"; Dictionary <string, MSB3> maps = editor.Load(@"Base", path => ann.Specs.ContainsKey(GameEditor.BaseName(path)) ? MSB3.Read(path) : null, "*.msb.dcx"); Dictionary <string, EMEVD> emevds = editor.Load(@"Base", path => ann.Specs.ContainsKey(GameEditor.BaseName(path)) || path.Contains("common") ? EMEVD.Read(path) : null, "*.emevd.dcx"); void deleteEmpty <K, V>(Dictionary <K, V> d) { foreach (K key in d.Keys.ToList()) { if (d[key] == null) { d.Remove(key); } } } // Should this be in GameEditor? deleteEmpty(maps); deleteEmpty(emevds); editor.Spec.NameDir = @"fogdist\Names"; Dictionary <string, string> modelNames = editor.LoadNames("ModelName", n => n); SortedDictionary <int, string> chars = new SortedDictionary <int, string>(editor.LoadNames("CharaInitParam", n => int.Parse(n))); Dictionary <string, List <string> > description = new Dictionary <string, List <string> >(); Dictionary <int, string> entityNames = new Dictionary <int, string>(); Dictionary <int, List <int> > groupIds = new Dictionary <int, List <int> >(); Dictionary <(string, string), MSB3.Event.ObjAct> objacts = new Dictionary <(string, string), MSB3.Event.ObjAct>(); HashSet <int> highlightIds = new HashSet <int>(); HashSet <int> selectIds = new HashSet <int>(); foreach (Entrance e in ann.Warps.Concat(ann.Entrances)) { int id = e.ID; AddMulti(description, id.ToString(), (ann.Warps.Contains(e) ? "" : "fog gate ") + e.Text); selectIds.Add(e.ID); highlightIds.Add(e.ID); } HashSet <string> gameObjs = new HashSet <string>(); foreach (GameObject obj in ann.Objects) { if (int.TryParse(obj.ID, out int id)) { AddMulti(description, id.ToString(), obj.Text); selectIds.Add(id); highlightIds.Add(id); } else { gameObjs.Add($"{obj.Area}_{obj.ID}"); } } Dictionary <string, Dictionary <string, FMG> > fmgs = new GameEditor(FromGame.DS3).LoadBnds($@"msg\engus", (data, name) => FMG.Read(data), ext: "*_dlc2.msgbnd.dcx"); void addFMG(FMG fmg, string desc) { foreach (FMG.Entry e in fmg.Entries) { if (e.ID > 25000 && !string.IsNullOrWhiteSpace(e.Text)) { highlightIds.Add(e.ID); AddMulti(description, e.ID.ToString(), desc + " " + "\"" + e.Text.Replace("\r", "").Replace("\n", "\\n") + "\""); } } } addFMG(fmgs["item_dlc2"]["NPC名"], "name"); addFMG(fmgs["menu_dlc2"]["イベントテキスト"], "text"); foreach (KeyValuePair <string, MSB3> entry in maps) { string map = ann.Specs[entry.Key].Name; MSB3 msb = entry.Value; foreach (MSB3.Part e in msb.Parts.GetEntries()) { string shortName = $"{map}_{e.Name}"; if (modelNames.TryGetValue(e.ModelName, out string modelDesc)) { if (e is MSB3.Part.Enemy en && modelDesc == "Human NPC" && en.CharaInitID > 0) { modelDesc = CharacterName(chars, en.CharaInitID); } else if (e is MSB3.Part.Player) { modelDesc = "Warp Point"; } AddMulti(description, shortName, modelDesc); } AddMulti(description, shortName, $"{map} {e.GetType().Name.ToString().ToLowerInvariant()} {e.Name}"); // {(e.EventEntityID > 0 ? $" {e.EventEntityID}" : "")} if (e.EventEntityID > 10) { highlightIds.Add(e.EventEntityID); string idStr = e.EventEntityID.ToString(); if (description.ContainsKey(idStr)) { AddMulti(description, shortName, description[idStr]); } description[idStr] = description[shortName]; if (e is MSB3.Part.Player || e.ModelName == "o000100") { selectIds.Add(e.EventEntityID); } if (selectIds.Contains(e.EventEntityID)) { gameObjs.Add(shortName); } foreach (int id in e.EventEntityGroups) { if (id > 0) { AddMulti(groupIds, id, e.EventEntityID); highlightIds.Add(id); } } } } foreach (MSB3.Region r in msb.Regions.GetEntries()) { if (r.EventEntityID < 1000000) { continue; } AddMulti(description, r.EventEntityID.ToString(), $"{map} {r.GetType().Name.ToLowerInvariant()} region {r.Name}"); highlightIds.Add(r.EventEntityID); } foreach (MSB3.Event e in msb.Events.GetEntries()) { if (e is MSB3.Event.ObjAct oa) { // It can be null, basically for commented out objacts string part = oa.PartName ?? oa.PartName2; if (part == null) { continue; } string desc = description.TryGetValue($"{map}_{part}", out List <string> p) ? string.Join(" - ", p) : throw new Exception($"{map} {oa.Name}"); objacts[(map, part)] = oa;