diff --git a/JortPob/Layout.cs b/JortPob/Layout.cs index d44f030..a35afa7 100644 --- a/JortPob/Layout.cs +++ b/JortPob/Layout.cs @@ -88,92 +88,216 @@ public Layout(Cache cache, ESM esm, Paramanager param, TextManager text, ScriptM // able refs is objects targeted by Enable, Disable, and GetDisabled var (allCalls, allReferences, toggleableReferences) = esm.GetScriptReferences(); Lort.TaskIterate(); // Progress bar update + + SetFollowerFlags(esm, allCalls); - /* MSB promotion pre-process step */ - /* Goal of this step is to identify all characters that have a "Follow" aipackage or a "AiFollow" call that target them. */ - void PreProcessFollow(Cell cell) + /* Subdivide all cell content into tiles */ + PrepareCellTilesAndEmitters(cache, esm, scriptManager, allReferences); + Lort.TaskIterate(); // Progress bar update + + /* Render an ASCII image of the tiles for verification! */ + Lort.Log("Drawing ASCII art of worldspace map...", Lort.Type.Debug); + RenderWorldspaceAscii(); + Lort.TaskIterate(); // Progress bar update + + PrepareInteriorCells(cache, esm, scriptManager); + Lort.TaskIterate(); // Progress bar update + + /* Resolve load doors and travel npcs */ + ResolveDoorsAndWarps(esm, scriptManager); + Lort.TaskIterate(); // Progress bar update + + /* default location name value for interiors */ + foreach (InteriorGroup group in interiors) { - foreach (Content content in cell.contents) + int textId = int.Parse($"{group.map:D2}{group.area:D2}0"); + text.SetLocation(textId, "Interior"); + } + Lort.TaskIterate(); // Progress bar update + + PrecomputeNpcWitnesses(); + Lort.TaskIterate(); // Progress bar update + + /* Statically resolve shop inventories for npcs (also creatures too) */ + ResolveShops(esm); + + Lort.TaskIterate(); // Progress bar update + + /* Part 2 of Papyrus preprocess */ + /* This assigns entity ids to objects that have scripts referencing them */ + /* It also in some cases creates flags and events for objects that require them. */ + /* Also notably we setup disable/enable flags here as well */ + void PreprocessContent(BaseScript areaScript, IEnumerable contents) + { + foreach (Content content in contents) { - if (content is CharacterContent cc) + string contentId = content.id.ToLower().Trim(); + if (allReferences.Contains(contentId) || content is CharacterContent || content is ItemContent || content is DoorContent || content.papyrus != null) { - foreach (NpcContent.AiPackage package in cc.packages) + // Create an entity ID for this object so that it can be interacted with via scripts + Script.EntityType entityType; + switch (content) { - if (package.type == CharacterContent.AiPackage.Type.Follow && package.target == "player") { cc.follower = true; return; } + case ItemContent ic: entityType = Script.EntityType.Asset; break; + case CharacterContent cc: entityType = Script.EntityType.Enemy; break; + case StaticContent sc: entityType = Script.EntityType.Asset; break; + case LightContent lc: entityType = Script.EntityType.Region; break; // BTL Light. Scripts on these don't actually work rn + default: throw new Exception("Invalid content type for script preprocess"); + } + content.entity = areaScript.CreateEntity(entityType, $"{content.type}::{content.id}"); + + // talkable characters always get disable flags for simplicity. statically resolving dialog triggered self-disable calls is slow as hell + if (content is NpcContent || (content is CreatureContent creatureContent && esm.HasDialog(creatureContent)) || toggleableReferences.Contains(contentId)) + { + // Object disabled flag + areaScript.CreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Bit, Script.Flag.Designation.Disabled, content); } } + + switch (content) + { + case LightContent lc: + { + // BTL lights likely can't have scripts (havent checked) so just continue + break; + } + case ItemContent item: + { + // Item scripts are registered during MSB generation. This is due to the fact that they are tied in closely with params that are generated at that point. + break; + } + case StaticContent statik: + { + areaScript.RegisterStaticDisable(statik); + break; + } + case CharacterContent character: + { + // Dead by type list. + // So morrowind uses a weird system where it keeps a count of each "type" of npc/creature is killed + // For most npcs this count will only ever be 0 or 1 since there is only one of that npc in the world + // But for like rats and shit it keeps count so it knows you've killed 10 rats or whatever + // So to emulate this system we will have a seperate counter flag for each record type of creature or npc + Script.Flag countFlag = scriptManager.common.GetOrCreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Byte, Script.Flag.Designation.DeadCount, character.id); + + // Humanoid NPCs or creatures that can talk + if (character is NpcContent || (character is CreatureContent creatureContent && !character.dead && esm.HasDialog(creatureContent))) + { + // Pre-Dead npcs get a special script to have their body just lay there. They do not get any flags or ESD stuff built + if (character is NpcContent npcContent && character.dead) { areaScript.RegisterDeadNpc(npcContent); } + // Create a bunch of stuff needed for NPCs to work + else + { + /* Create various flags requried for NPCs */ + Script.Flag firstGreet = areaScript.CreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Bit, Script.Flag.Designation.TalkedToPc, character); + Script.Flag disposition = areaScript.CreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Byte, Script.Flag.Designation.Disposition, character, (uint)character.disposition); + Script.Flag pickpocketedFlag = areaScript.CreateFlag(Script.Flag.Category.Temporary, Script.Flag.Type.Bit, Script.Flag.Designation.Pickpocketed, character); + Script.Flag thiefFlag = areaScript.CreateFlag(Script.Flag.Category.Temporary, Script.Flag.Type.Bit, Script.Flag.Designation.ThiefCrime, character); + + /* Register some scripts for NPCs */ + areaScript.RegisterCharacter(param, character, countFlag); + areaScript.RegisterNpcHostility(character); // setup hostility flag/event + } + } + // Regular creatures + else + { + // Dead creatures not supported rn + CreatureContent creature = content as CreatureContent; + if (creature.dead) { throw new Exception("Pre-Dead creatures not supported yet!"); } + else { areaScript.RegisterCharacter(param, creature, countFlag); } + } + break; + } + default: throw new Exception("Invalid content type for script preprocess"); + } } } - foreach (Cell cell in esm.exterior) { PreProcessFollow(cell); } // handle ai packages part - foreach (Cell cell in esm.interior) { PreProcessFollow(cell); } - void SetFollowerFlagByRecordId(Cell cell, string id) + foreach (BaseTile tile in AllTiles) { PreprocessContent(scriptManager.GetScript(tile), tile.GetAllContent()); } + foreach (InteriorGroup group in interiors) { - foreach (Content content in cell.contents) + foreach (InteriorGroup.Chunk chunk in group.chunks) { PreprocessContent(scriptManager.GetScript(group), chunk.GetAllContent()); } + } + Lort.TaskIterate(); // Progress bar update + + /* Preprocess papyrus calls like 'Position' that need a region placed at a location in the world */ + /* Also preprocess Position and PositionCell calls for npcs by creating "PhasedNPCs" */ + PrepareScriptedPositions(cache, esm, param, scriptManager); + + /* Process character aipackage positions */ + foreach (Tile tile in tiles) { tile.ProcessTravelPoints(scriptManager); } + foreach (InteriorGroup group in interiors) { group.ProcessTravelPositions(scriptManager); } + + /* Generate map point placements */ + AddMapPointsOfInterest(esm, scriptManager); + Lort.TaskIterate(); // Progress bar update + } + + private void PrecomputeNpcWitnesses() + { + /* Handle npc hasWitness flag */ // this check is very similar to and patially entwined with Script.GenerateCrimeEvents() and the ESD state HANDLECRIME + /* We can't determine witnesses at runtime so we just do some test now and determine if an npc has witneses to report crimes to */ + void CheckWitnesses(List npcs) + { + foreach (NpcContent npc in npcs) { - if (content is CharacterContent cc && content.id.Trim().ToLower() == id.Trim().ToLower()) + if (npc.IsHostile()) { npc.witness = CharacterContent.Witness.None; continue; } // if an npc is naturally hostile to the player they don't report crimes lmao + if (npc.IsGuard()) { npc.witness = CharacterContent.Witness.Guard; continue; } + if (npc.alarm >= 50) { npc.witness = CharacterContent.Witness.Citizen; } + foreach (NpcContent other in npcs) { - cc.follower = true; + if (npc == other) { continue; } // dont' self succ + + // guards get bonus range because I said so + if (other.IsGuard() && System.Numerics.Vector3.Distance(npc.position, other.position) < 50) { npc.witness = CharacterContent.Witness.Guard; break; } + if (other.alarm >= 50 && System.Numerics.Vector3.Distance(npc.position, other.position) < 15) { npc.witness = CharacterContent.Witness.Citizen; } } } } - foreach (Papyrus.Call call in allCalls) // Reusing "allCalls" list from above + + foreach (Tile tile in tiles) { CheckWitnesses(tile.npcs); } + foreach (InteriorGroup group in interiors) { + foreach (InteriorGroup.Chunk chunk in group.chunks) { CheckWitnesses(chunk.npcs); } + } + } + + private void ResolveDoorsAndWarps(ESM esm, ScriptManager scriptManager) + { + foreach (InteriorGroup group in interiors) { - // Grab record reference from target if exists - if (call.target != null && call.parameters.Count() > 0 && call.parameters[0].ToLower().Trim() == "player") + foreach (InteriorGroup.Chunk chunk in group.chunks) { - switch (call.type) + foreach (DoorContent door in chunk.doors) { - case Papyrus.Call.Type.AiFollow: - case Papyrus.Call.Type.AiFollowCell: - case Papyrus.Call.Type.AiEscort: - case Papyrus.Call.Type.AiEscortCell: - foreach (Cell cell in esm.exterior) { SetFollowerFlagByRecordId(cell, call.target); } // handle papyrus call part - foreach (Cell cell in esm.interior) { SetFollowerFlagByRecordId(cell, call.target); } - break; - default: break; + RegisterDoorWarp(door, scriptManager); + if (door.warp != null) { door.entity = scriptManager.GetScript(group).CreateEntity(Script.EntityType.Asset, $"DoorEntry::{door.cell.name}->{door.warp.cell}"); } + } + + foreach (NpcContent npc in chunk.npcs) + { + RegisterNpcWarp(esm, scriptManager, npc); } } } - - /* Subdivide all cell content into tiles */ - foreach (Cell cell in esm.exterior) + foreach (Tile tile in tiles) { - HugeTile huge = GetHugeTile(cell.center); - TerrainInfo terrain = cache.GetTerrain(cell.coordinate); - if (terrain != null) + foreach (DoorContent door in tile.doors) { - if (huge != null) { huge.AddTerrain(cell.center, terrain); } - else { Lort.Log($" ## WARNING ## Terrain fell outside of reality [{cell.coordinate.x}, {cell.coordinate.y}] -- {cell.region} :: B02", Lort.Type.Debug); } + RegisterDoorWarp(door, scriptManager); + if (door.warp != null) { door.entity = scriptManager.GetScript(tile).CreateEntity(Script.EntityType.Asset, $"DoorExit::{door.cell.name}->exterior[{door.warp.x},{door.warp.y}]"); } } - if (huge != null) + foreach (NpcContent npc in tile.npcs) { - huge.AddCell(scriptManager, cell); - - foreach (Content content in cell.contents) - { - if (content is AssetContent assetContent) - { - /* If an assetcontent has emitter nodes, we convert it to an emittercontent */ - /* We can't really do this earlier than this point sadly because we need both the ESM loaded and cache built to be able to catch this corner case */ - /* So we do it here */ - if (cache.GetModel(assetContent.mesh)?.HasEmitter() == true) - { - cache.AddConvertedEmitter(assetContent.ConvertToEmitter()); - } - } - - huge.AddContent(cache, cell, content, allReferences.Contains(content.id.ToLower().Trim())); - } + RegisterNpcWarp(esm, scriptManager, npc, tile); } - else { Lort.Log($" ## WARNING ## Cell fell outside of reality [{cell.coordinate.x}, {cell.coordinate.y}] -- {cell.name} :: B02", Lort.Type.Debug); } } - Lort.TaskIterate(); // Progress bar update + } - /* Render an ASCII image of the tiles for verification! */ - Lort.Log("Drawing ASCII art of worldspace map...", Lort.Type.Debug); + private void RenderWorldspaceAscii() + { for (int y = 28; y < 66; y++) { string line = ""; @@ -188,10 +312,10 @@ void SetFollowerFlagByRecordId(Cell cell, string id) } Lort.Log(line, Lort.Type.Debug); } - Lort.TaskIterate(); // Progress bar update - + } - /* Pre-sort interior cells by the number of beds they have (to avoid the 19 bed limit per msb) */ + private void PrepareInteriorCells(Cache cache, ESM esm, ScriptManager scriptManager) + { int partition = (int)Math.Ceiling(esm.interior.Count / (float)interiors.Count); List[] cellPreSort = new List[interiors.Count]; @@ -199,6 +323,7 @@ void SetFollowerFlagByRecordId(Cell cell, string id) for (int i = 0; i < cellPreSort.Length; i++) { cellPreSort[i] = new(); } // initialize + /* Pre-sort interior cells by the number of beds they have (to avoid the 19 bed limit per msb) */ foreach (Cell cell in esm.interior) { int beds = cell.BedCount(); @@ -215,8 +340,7 @@ void SetFollowerFlagByRecordId(Cell cell, string id) if (fail) { throw new Exception("Too many beds! Oh god so many beds! Help!"); } } - - /* Subdivide all interior cells into groups */ + for (int i = 0; i < cellPreSort.Length; i++) { InteriorGroup group = interiors[i]; @@ -229,376 +353,235 @@ void SetFollowerFlagByRecordId(Cell cell, string id) scriptManager.areas.Add(cell, script.CreateEntity(Script.EntityType.Region, $"cell {cell.name}")); } } - Lort.TaskIterate(); // Progress bar update + } - /* Resolve load doors and travel npcs */ - foreach (InteriorGroup group in interiors) + private void AddMapPointsOfInterest(ESM esm, ScriptManager scriptManager) + { + Dictionary> mapPoints = new(); + + // collect em all + foreach (Cell cell in esm.exterior) { - foreach (InteriorGroup.Chunk chunk in group.chunks) + if(!string.IsNullOrEmpty(cell.name)) { - foreach (DoorContent door in chunk.doors) - { - RegisterDoorWarp(door, scriptManager); - if (door.warp != null) { door.entity = scriptManager.GetScript(group).CreateEntity(Script.EntityType.Asset, $"DoorEntry::{door.cell.name}->{door.warp.cell}"); } - } + Landscape landscape = esm.GetLandscape(cell.coordinate); + Vector3 center; + if (landscape == null) { center = new(); } + else { center = cell.center + new Vector3(0f, landscape.GetHeightAverage(), 0f); } - foreach (NpcContent npc in chunk.npcs) - { - RegisterNpcWarp(esm, scriptManager, npc); - } + if (mapPoints.ContainsKey(cell.name)) { mapPoints[cell.name].Add(center); } + else { mapPoints.Add(cell.name, new() { center }); } } } - foreach (Tile tile in tiles) + // merge similar + HashSet pointNames = new(); + List importants = new(); + foreach(var kvp in mapPoints) { - foreach (DoorContent door in tile.doors) + string name = kvp.Key; // name + Vector3 center = new(); // average of all points with same name + float radius = Const.CELL_SIZE / 2; // minimum size for radius of map point is 1 cell + + Vector3 first = kvp.Value.First(); + foreach(Vector3 pos in kvp.Value) { - RegisterDoorWarp(door, scriptManager); - if (door.warp != null) { door.entity = scriptManager.GetScript(tile).CreateEntity(Script.EntityType.Asset, $"DoorExit::{door.cell.name}->exterior[{door.warp.x},{door.warp.y}]"); } + radius = Math.Max(radius, Vector3.Distance(first, pos)); + center += pos; } - foreach (NpcContent npc in tile.npcs) + center *= (1f / kvp.Value.Count()); + + MapPoint.Icon icon = Override.GetMapIcon(name); + if (icon == MapPoint.Icon.None) { continue; } // skip these + Script.Flag discoverFlag = scriptManager.common.CreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Bit, Script.Flag.Designation.DiscoverLocation, name); + Layout.MapPoint mapPoint = new(name, center, radius, true, discoverFlag, icon); + Tile tile = GetTile(center); + tile.AddMapPoint(mapPoint); + importants.Add(mapPoint); + pointNames.Add(name.ToLower().Trim()); + } + + foreach(Tile tile in tiles) + { + foreach(DoorContent door in tile.doors) { - RegisterNpcWarp(esm, scriptManager, npc, tile); + if(door.warp != null) + { + string name = door.warp.cell; + if (name.Contains(",")) { name = name.Split(",")[0].Trim(); } // Split area sub names so we just have the main area name. Changes things like "Shipwreck, Upper Level" to just "Shipwreck" + MapPoint.Icon icon = Override.GetMapIcon(name); + + if (icon == MapPoint.Icon.None) { continue; } // skip these + + // checks if a position is inside of one of the important map points we created above. skip these too! + if (importants.Any(p => Vector3.Distance(door.position, p.position) <= p.radius)) { continue; } + + // see if map point has already been made. some areas have multiple entrances or exits + var lowerName = name.ToLower().Trim(); + if(pointNames.Contains(lowerName)) { continue; } + pointNames.Add(lowerName); + + const float UNIMPORTANT_SIZE_MODIFIER = 0.3f; + Script.Flag discoverFlag = scriptManager.common.CreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Bit, Script.Flag.Designation.DiscoverLocation, name); // if 2 doors go to the same interior we share the flag + Layout.MapPoint mapPoint = new(name, door.position, Const.CELL_SIZE * UNIMPORTANT_SIZE_MODIFIER, false, discoverFlag, icon); + tile.AddMapPoint(mapPoint); + } } } - Lort.TaskIterate(); // Progress bar update + } - /* default location name value for interiors */ - foreach (InteriorGroup group in interiors) + private void PrepareScriptedPositions(Cache cache, ESM esm, Paramanager param, ScriptManager scriptManager) + { + Dictionary phases = new(); + + foreach (Tile tile in tiles) { - int textId = int.Parse($"{group.map:D2}{group.area:D2}0"); - text.SetLocation(textId, "Interior"); + List allContent = tile.GetAllContent().ToList(); + foreach(Content content in allContent) + { + if (content is PhasedNpcContent) { continue; } // skip phased npcs as we don't want to re-process them + + Papyrus papyrus = esm.GetPapyrus(content.papyrus); + if (papyrus == null) { continue; } // no script or failed to get script, skip + + List calls = papyrus.GetCalls(); + PreProcessScriptedPositions(content, calls, scriptManager, param, phases, esm, cache); + } } - Lort.TaskIterate(); // Progress bar update - /* Handle npc hasWitness flag */ // this check is very similar to and patially entwined with Script.GenerateCrimeEvents() and the ESD state HANDLECRIME - /* We can't determine witnesses at runtime so we just do some test now and determine if an npc has witneses to report crimes to */ - void CheckWitnesses(List npcs) + foreach (InteriorGroup group in interiors) { - foreach (NpcContent npc in npcs) + foreach(InteriorGroup.Chunk chunk in group.chunks) { - if (npc.IsHostile()) { npc.witness = CharacterContent.Witness.None; break; } // if an npc is naturally hostile to the player they don't report crimes lmao - if (npc.IsGuard()) { npc.witness = CharacterContent.Witness.Guard; continue; } - if (npc.alarm >= 50) { npc.witness = CharacterContent.Witness.Citizen; } - foreach (NpcContent other in npcs) + List allContent = chunk.GetAllContent().ToList(); + foreach (Content content in allContent) { - if (npc == other) { continue; } // dont' self succ + if (content is PhasedNpcContent) { continue; } // skip phased npcs as we don't want to re-process them - // guards get bonus range because I said so - if (other.IsGuard() && System.Numerics.Vector3.Distance(npc.position, other.position) < 50) { npc.witness = CharacterContent.Witness.Guard; break; } - if (other.alarm >= 50 && System.Numerics.Vector3.Distance(npc.position, other.position) < 15) { npc.witness = CharacterContent.Witness.Citizen; } + Papyrus papyrus = esm.GetPapyrus(content.papyrus); + if (papyrus == null) { continue; } // no script or failed to get script, skip + + List calls = papyrus.GetCalls(); + PreProcessScriptedPositions(content, calls, scriptManager, param, phases, esm, cache); } } } - foreach (Tile tile in tiles) { CheckWitnesses(tile.npcs); } - foreach (InteriorGroup group in interiors) { - foreach (InteriorGroup.Chunk chunk in group.chunks) { CheckWitnesses(chunk.npcs); } + foreach(Papyrus papyrus in esm.scripts) + { + foreach(Papyrus.Call call in papyrus.GetCalls(Papyrus.Call.Type.StartScript)) + { + Papyrus subscript = esm.GetPapyrus(call.parameters[0]); + if (subscript == null) { continue; } // no script or failed to get script, skip + + List calls = subscript.GetCalls(); + PreProcessScriptedPositions(null, calls, scriptManager, param, phases, esm, cache); + } } - Lort.TaskIterate(); // Progress bar update - /* Statically resolve shop inventories for npcs (also creatures too) */ - void ResolveShop(CharacterContent npc) + foreach (Dialog.DialogRecord dialog in esm.dialog) { - if (!npc.HasBarter()) { return; } // nope! + List calls = dialog.GetCalls(); + PreProcessScriptedPositions(null, calls, scriptManager, param, phases, esm, cache); // null here means we cannot process self reference calls. will print error if we run into one + } + } - bool WillBarter(CharacterContent npc, ESM.Type type) + private void PreProcessScriptedPositions(Content content, List calls, ScriptManager scriptManager, Paramanager param, Dictionary phases, ESM esm, Cache cache) + { + void ReplaceNpc(NpcContent original, PhasedNpcContent replacement) + { + // Find any registration scripts pointing at the original content and murder them. This is kind of a bandaid fix for some unanticipated side effects. It gets the job done but ew. + void RemoveOriginalNpcRegistration(BaseScript script) // @TODO: may be worth it to simply move this stage up a little bit in this constructor stack to avoid this even happening { - switch (type) + for(int i=0;i shopInv = new(); + uint arg1 = BitConverter.ToUInt32(instruction.ArgData, 4); + uint arg2; - void AddOrIncrement(List<(string id, int quantity)> list, (string id, int quantity) tuple) - { - for (int i = 0; i < list.Count(); i++) - { - (string id, int quantity) entry = list[i]; - if (entry.id.ToLower() == tuple.id.ToLower()) { - list.RemoveAt(i); - list.Add((entry.id, entry.quantity + tuple.quantity)); // can't increment value in a tuple because fuck - return; + // all of these ended up being at the 12 byte offset but im leaving this structure incase we ever need to change things. + if (arg1 == scriptManager.common.events[ScriptCommon.Event.NpcHostilityHandler]) + { + arg2 = BitConverter.ToUInt32(instruction.ArgData, 12); + } + else if (arg1 == scriptManager.common.events[ScriptCommon.Event.SpawnHandler]) + { + arg2 = BitConverter.ToUInt32(instruction.ArgData, 12); + } + else if (arg1 == scriptManager.common.events[ScriptCommon.Event.SpawnHandlerDisableable]) + { + arg2 = BitConverter.ToUInt32(instruction.ArgData, 12); } + else { arg2 = 0; } + + // Delete if matches + if (arg2 != 0 && arg2 == original.entity) { script.init.Instructions.RemoveAt(i--); } } - list.Add(tuple); } - foreach (ItemContent item in cell.items) // add loose items this npc owns + // Find this content and replace it with a phased copy. Searches for matching reference, not by id or anything + bool DoReplacement(BaseScript script, NpcContent original, List npcs) { - if (item.ownerNpc == npc.id) + foreach (NpcContent c in npcs) { - if (WillBarter(npc, item.type)) + if (original == c) { - AddOrIncrement(shopInv, (item.id, 1)); + RemoveOriginalNpcRegistration(script); + npcs.Replace(original, replacement); + scriptManager.AddRoute(replacement, original); + script.RegisterCharacter(param, replacement, scriptManager.common.GetOrCreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Byte, Script.Flag.Designation.DeadCount, replacement.id)); + script.RegisterNpcHostility(replacement); + return true; } } + return false; } - foreach (ContainerContent container in cell.containers) // add containers this npc owns + + foreach(BaseTile t in AllTiles) { - if (container.ownerNpc == npc.id) - { - foreach ((string id, int quantity) tuple in container.inventory) - { - Record record = esm.FindRecordById(tuple.id); - if (WillBarter(npc, record.type)) - { - AddOrIncrement(shopInv, tuple); - } - } - } + BaseScript script = scriptManager.GetScript(t); + if(DoReplacement(script, original, t.npcs)) { return; } } - foreach ((string id, int quantity) tuple in npc.inventory) // add own inventory to potential barter + foreach (InteriorGroup g in interiors) { - Record record = esm.FindRecordById(tuple.id); - if (WillBarter(npc, record.type)) + foreach (InteriorGroup.Chunk c in g.chunks) { - AddOrIncrement(shopInv, tuple); + BaseScript script = scriptManager.GetScript(g); + if (DoReplacement(script, original, c.npcs)) { return; } } } - if (shopInv.Count() > 0) { npc.barter = shopInv; } - } - foreach (Tile tile in tiles) - { - foreach (NpcContent npc in tile.npcs) { ResolveShop(npc); } - foreach (CreatureContent creature in tile.creatures) { ResolveShop(creature); } + throw new Exception("Failed to replace NPC during phased npc preprocess stage!"); // shouldn't happen in theory } - foreach (InteriorGroup group in interiors) + + void HandleNpcPhase(BaseTile t, InteriorGroup.Chunk chunk, Vector3 position, float rotation, Content content, Papyrus.Call call) { - foreach (InteriorGroup.Chunk chunk in group.chunks) + // Quick checks before we do anything + if (content == null && call.target == null) { Lort.Log($" ## Cannot handle self reference position call from empty context! '{call.RAW}' Bad!", Lort.Type.Debug); return; } + + // Find our target + Content target = null; + if(call.target == null) { target = content; } + else { - foreach (NpcContent npc in chunk.npcs) { ResolveShop(npc); } - foreach (CreatureContent creature in chunk.creatures) { ResolveShop(creature); } + // See if our target is already phased + foreach(var (n, _) in phases) + { + if(n.id.ToLower() == call.target) { target = n; break; } + } + + // Otherwise look for it + if (target == null) { target = FindScriptReference(content, call.target); } } - } - Lort.TaskIterate(); // Progress bar update + if (target == null) { return; } // Failed to find ref, likely a partial build where it doesn't exist + if (target is not NpcContent) { Lort.Log($" ## Position and PositionCell calls do not support '{target.type}' content. call = '{call.RAW}'", Lort.Type.Debug); return; } - /* Part 2 of Papyrus preprocess */ - /* This assigns entity ids to objects that have scripts referencing them */ - /* It also in some cases creates flags and events for objects that require them. */ - /* Also notably we setup disable/enable flags here as well */ - void PreprocessContent(BaseScript areaScript, IEnumerable contents) - { - foreach (Content content in contents) - { - string contentId = content.id.ToLower().Trim(); - if (allReferences.Contains(contentId) || content is CharacterContent || content is ItemContent || content is DoorContent || content.papyrus != null) - { - // Create an entity ID for this object so that it can be interacted with via scripts - Script.EntityType entityType; - switch (content) - { - case ItemContent ic: entityType = Script.EntityType.Asset; break; - case CharacterContent cc: entityType = Script.EntityType.Enemy; break; - case StaticContent sc: entityType = Script.EntityType.Asset; break; - case LightContent lc: entityType = Script.EntityType.Region; break; // BTL Light. Scripts on these don't actually work rn - default: throw new Exception("Invalid content type for script preprocess"); - } - content.entity = areaScript.CreateEntity(entityType, $"{content.type}::{content.id}"); - - // talkable characters always get disable flags for simplicity. statically resolving dialog triggered self-disable calls is slow as hell - if (content is NpcContent || (content is CreatureContent && esm.HasDialog((CreatureContent)content)) || toggleableReferences.Contains(contentId)) - { - // Object disabled flag - Script.Flag disableFlag = areaScript.CreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Bit, Script.Flag.Designation.Disabled, content); - } - } - - switch (content) - { - case LightContent lc: - { - // BTL lights likely can't have scripts (havent checked) so just continue - break; - } - case ItemContent item: - { - // Item scripts are registered during MSB generation. This is due to the fact that they are tied in closely with params that are generated at that point. - break; - } - case StaticContent statik: - { - areaScript.RegisterStaticDisable(statik); - break; - } - case CharacterContent character: - { - // Dead by type list. - // So morrowind uses a weird system where it keeps a count of each "type" of npc/creature is killed - // For most npcs this count will only ever be 0 or 1 since there is only one of that npc in the world - // But for like rats and shit it keeps count so it knows you've killed 10 rats or whatever - // So to emulate this system we will have a seperate counter flag for each record type of creature or npc - Script.Flag countFlag = scriptManager.common.GetOrCreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Byte, Script.Flag.Designation.DeadCount, character.id); - - // Humanoid NPCs or creatures that can talk - if (character is NpcContent || (character is CreatureContent && !character.dead && esm.HasDialog((CreatureContent)character))) - { - // Pre-Dead npcs get a special script to have their body just lay there. They do not get any flags or ESD stuff built - if (character is NpcContent && character.dead) { areaScript.RegisterDeadNpc((NpcContent)character); } - // Create a bunch of stuff needed for NPCs to work - else - { - /* Create various flags requried for NPCs */ - Script.Flag firstGreet = areaScript.CreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Bit, Script.Flag.Designation.TalkedToPc, character); - Script.Flag disposition = areaScript.CreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Byte, Script.Flag.Designation.Disposition, character, (uint)character.disposition); - Script.Flag pickpocketedFlag = areaScript.CreateFlag(Script.Flag.Category.Temporary, Script.Flag.Type.Bit, Script.Flag.Designation.Pickpocketed, character); - Script.Flag thiefFlag = areaScript.CreateFlag(Script.Flag.Category.Temporary, Script.Flag.Type.Bit, Script.Flag.Designation.ThiefCrime, character); - - /* Register some scripts for NPCs */ - areaScript.RegisterCharacter(param, character, countFlag); - areaScript.RegisterNpcHostility(character); // setup hostility flag/event - } - } - // Regular creatures - else - { - // Dead creatures not supported rn - CreatureContent creature = content as CreatureContent; - if (creature.dead) { throw new Exception("Pre-Dead creatures not supported yet!"); } - else { areaScript.RegisterCharacter(param, creature, countFlag); } - } - break; - } - default: throw new Exception("Invalid content type for script preprocess"); - } - } - } - - foreach (BaseTile tile in AllTiles) { PreprocessContent(scriptManager.GetScript(tile), tile.GetAllContent()); } - foreach (InteriorGroup group in interiors) - { - foreach (InteriorGroup.Chunk chunk in group.chunks) { PreprocessContent(scriptManager.GetScript(group), chunk.GetAllContent()); } - } - Lort.TaskIterate(); // Progress bar update - - /* Preprocess papyrus calls like 'Position' that need a region placed at a location in the world */ - /* Also preprocess Position and PositionCell calls for npcs by creating "PhasedNPCs" */ - Dictionary phases = new(); - - void ReplaceNpc(NpcContent original, PhasedNpcContent replacement) - { - // Find any registration scripts pointing at the original content and murder them. This is kind of a bandaid fix for some unanticipated side effects. It gets the job done but ew. - void RemoveOriginalNpcRegistration(BaseScript script) // @TODO: may be worth it to simply move this stage up a little bit in this constructor stack to avoid this even happening - { - for(int i=0;i npcs) - { - foreach (NpcContent c in npcs) - { - if (original == c) - { - RemoveOriginalNpcRegistration(script); - npcs.Replace(original, replacement); - scriptManager.AddRoute(replacement, original); - script.RegisterCharacter(param, replacement, scriptManager.common.GetOrCreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Byte, Script.Flag.Designation.DeadCount, replacement.id)); - script.RegisterNpcHostility(replacement); - return true; - } - } - return false; - } - - foreach(BaseTile t in AllTiles) - { - BaseScript script = scriptManager.GetScript(t); - if(DoReplacement(script, original, t.npcs)) { return; } - } - - foreach (InteriorGroup g in interiors) - { - foreach (InteriorGroup.Chunk c in g.chunks) - { - BaseScript script = scriptManager.GetScript(g); - if (DoReplacement(script, original, c.npcs)) { return; } - } - } - - throw new Exception("Failed to replace NPC during phased npc preprocess stage!"); // shouldn't happen in theory - } - - void HandleNpcPhase(BaseTile t, InteriorGroup.Chunk chunk, Vector3 position, float rotation, Content content, Papyrus.Call call) - { - // Quick checks before we do anything - if (content == null && call.target == null) { Lort.Log($" ## Cannot handle self reference position call from empty context! '{call.RAW}' Bad!", Lort.Type.Debug); return; } - - // Find our target - Content target = null; - if(call.target == null) { target = content; } - else - { - // See if our target is already phased - foreach(var (n, _) in phases) - { - if(n.id.ToLower() == call.target) { target = n; break; } - } - - // Otherwise look for it - if (target == null) { target = FindScriptReference(content, call.target); } - } - if (target == null) { return; } // Failed to find ref, likely a partial build where it doesn't exist - if (target is not NpcContent) { Lort.Log($" ## Position and PositionCell calls do not support '{target.type}' content. call = '{call.RAW}'", Lort.Type.Debug); return; } - - // Determine if npc is msb promoted or not and set our target tile if so - BaseTile tile; - if (t != null && target is CharacterContent cc && cc.follower == true) + // Determine if npc is msb promoted or not and set our target tile if so + BaseTile tile; + if (t != null && target is CharacterContent cc && cc.follower == true) { tile = GetHugeTile(position); } @@ -659,238 +642,299 @@ void HandleNpcPhase(BaseTile t, InteriorGroup.Chunk chunk, Vector3 position, flo script.RegisterCharacter(param, pnpc, scriptManager.common.GetOrCreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Byte, Script.Flag.Designation.DeadCount, pnpc.id)); script.RegisterNpcHostility(pnpc); } - - void PreProcessScriptedPositions(Content content, List calls) + + foreach (Papyrus.Call call in calls) { - foreach (Papyrus.Call call in calls) + switch (call.type) { - switch (call.type) + case Papyrus.Call.Type.Position: { - case Papyrus.Call.Type.Position: - { - // Find tile this position call is pointing too - Vector3 position = Utility.Vector3FromParameters(call.parameters) * Const.GLOBAL_SCALE; - float rot = float.Parse(call.parameters[3]); - Tile target = GetTile(position); - if (target == null) { Lort.Log($"Failed to place region for preprocessed Position call -> '{call.RAW}'", Lort.Type.Debug); break; } - - // Add a point at this position that will become a region we can use in scripts later to warp the player around - if (call.target == "player") - { - BaseScript script = scriptManager.GetScript(target); - target.AddScriptedPosition(script, position, rot); - } - // Create a phased npc - else - { - HandleNpcPhase(target, null, position, rot, content, call); - } - break; - } - case Papyrus.Call.Type.PositionCell: - { - Vector3 position = Utility.Vector3FromParameters(call.parameters) * Const.GLOBAL_SCALE; - float rot = float.Parse(call.parameters[3]); - string name = call.parameters[4]; - InteriorGroup.Chunk target = FindChunk(name); - if (target == null) { Lort.Log($"Failed to place region for preprocessed PositionCell call -> '{call.RAW}'", Lort.Type.Debug); break; } - - // Add a point at this position that will become a region we can use in scripts later to warp the player around - if (call.target == "player") - { - BaseScript script = scriptManager.GetScript(target.group); - target.AddScriptedPosition(script, position, rot); - } - // Create a phased npc - else - { - HandleNpcPhase(null, target, position, rot, content, call); - } - break; - } - case Papyrus.Call.Type.AiEscort: - case Papyrus.Call.Type.AiFollow: - { - Vector3 position = Utility.Vector3FromParameters(call.parameters, 2) * Const.GLOBAL_SCALE; - Tile target = GetTile(position); - if (target == null) { Lort.Log($"Failed to place region for preprocessed AiFollow call -> '{call.RAW}'", Lort.Type.Debug); break; } - - BaseScript script = scriptManager.GetScript(target); - target.AddTravelPoint(script, position, 5f); - break; - } - case Papyrus.Call.Type.AiEscortCell: - case Papyrus.Call.Type.AiFollowCell: - { - Vector3 position = Utility.Vector3FromParameters(call.parameters, 3) * Const.GLOBAL_SCALE; - string name = call.parameters[1]; - InteriorGroup.Chunk target = FindChunk(name); - if (target == null) { Lort.Log($"Failed to place region for preprocessed AiFollowCell call -> '{call.RAW}'", Lort.Type.Debug); break; } + // Find tile this position call is pointing too + Vector3 position = Utility.Vector3FromParameters(call.parameters) * Const.GLOBAL_SCALE; + float rot = float.Parse(call.parameters[3]); + Tile target = GetTile(position); + if (target == null) { Lort.Log($"Failed to place region for preprocessed Position call -> '{call.RAW}'", Lort.Type.Debug); break; } + + // Add a point at this position that will become a region we can use in scripts later to warp the player around + if (call.target == "player") + { + BaseScript script = scriptManager.GetScript(target); + target.AddScriptedPosition(script, position, rot); + } + // Create a phased npc + else + { + HandleNpcPhase(target, null, position, rot, content, call); + } + break; + } + case Papyrus.Call.Type.PositionCell: + { + Vector3 position = Utility.Vector3FromParameters(call.parameters) * Const.GLOBAL_SCALE; + float rot = float.Parse(call.parameters[3]); + string name = call.parameters[4]; + InteriorGroup.Chunk target = FindChunk(name); + if (target == null) { Lort.Log($"Failed to place region for preprocessed PositionCell call -> '{call.RAW}'", Lort.Type.Debug); break; } + + // Add a point at this position that will become a region we can use in scripts later to warp the player around + if (call.target == "player") + { + BaseScript script = scriptManager.GetScript(target.group); + target.AddScriptedPosition(script, position, rot); + } + // Create a phased npc + else + { + HandleNpcPhase(null, target, position, rot, content, call); + } + break; + } + case Papyrus.Call.Type.AiEscort: + case Papyrus.Call.Type.AiFollow: + { + Vector3 position = Utility.Vector3FromParameters(call.parameters, 2) * Const.GLOBAL_SCALE; + Tile target = GetTile(position); + if (target == null) { Lort.Log($"Failed to place region for preprocessed AiFollow call -> '{call.RAW}'", Lort.Type.Debug); break; } - BaseScript script = scriptManager.GetScript(target.group); - target.AddTravelPoint(script, position, 5f); - break; - } - case Papyrus.Call.Type.AiTravel: - { - Content target; - if(content == null) { target = FindScriptReference(null, call.target); } // attempt to resolve a dialog based call ref if possible, no guarantees though - else { target = content; } + BaseScript script = scriptManager.GetScript(target); + target.AddTravelPoint(script, position, 5f); + break; + } + case Papyrus.Call.Type.AiEscortCell: + case Papyrus.Call.Type.AiFollowCell: + { + Vector3 position = Utility.Vector3FromParameters(call.parameters, 3) * Const.GLOBAL_SCALE; + string name = call.parameters[1]; + InteriorGroup.Chunk target = FindChunk(name); + if (target == null) { Lort.Log($"Failed to place region for preprocessed AiFollowCell call -> '{call.RAW}'", Lort.Type.Debug); break; } + + BaseScript script = scriptManager.GetScript(target.group); + target.AddTravelPoint(script, position, 5f); + break; + } + case Papyrus.Call.Type.AiTravel: + { + Content target; + if(content == null) { target = FindScriptReference(null, call.target); } // attempt to resolve a dialog based call ref if possible, no guarantees though + else { target = content; } - Vector3 position = Utility.Vector3FromParameters(call.parameters) * Const.GLOBAL_SCALE; - if(target == null || target.cell.IsExterior()) // failed reference resolve defaults to overworld - { - Tile t = GetTile(position); - if (t == null) { Lort.Log($"Failed to place region for preprocessed AiTravel call -> '{call.RAW}'", Lort.Type.Debug); break; } + Vector3 position = Utility.Vector3FromParameters(call.parameters) * Const.GLOBAL_SCALE; + if(target == null || target.cell.IsExterior()) // failed reference resolve defaults to overworld + { + Tile t = GetTile(position); + if (t == null) { Lort.Log($"Failed to place region for preprocessed AiTravel call -> '{call.RAW}'", Lort.Type.Debug); break; } - BaseScript script = scriptManager.GetScript(t); - t.AddTravelPoint(script, position, Const.PATH_REGION_SIZE); - } - else - { - string name = target.cell.name; - InteriorGroup.Chunk c = FindChunk(name); - if (c == null) { Lort.Log($"Failed to place region for preprocessed AiTravel call -> '{call.RAW}'", Lort.Type.Debug); break; } + BaseScript script = scriptManager.GetScript(t); + t.AddTravelPoint(script, position, Const.PATH_REGION_SIZE); + } + else + { + string name = target.cell.name; + InteriorGroup.Chunk c = FindChunk(name); + if (c == null) { Lort.Log($"Failed to place region for preprocessed AiTravel call -> '{call.RAW}'", Lort.Type.Debug); break; } - BaseScript script = scriptManager.GetScript(c.group); - c.AddTravelPoint(script, position, Const.PATH_REGION_SIZE); - } - break; - } - default: break; // do nothing + BaseScript script = scriptManager.GetScript(c.group); + c.AddTravelPoint(script, position, Const.PATH_REGION_SIZE); + } + break; } + default: break; // do nothing } } + } - foreach (Tile tile in tiles) + private void ResolveShops(ESM esm) + { + void SetupShop(CharacterContent npc) { - List allContent = tile.GetAllContent().ToList(); - foreach(Content content in allContent) + if (!npc.HasBarter()) { return; } // nope! + + bool WillBarter(CharacterContent npc, ESM.Type type) { - if (content is PhasedNpcContent) { continue; } // skip phased npcs as we don't want to re-process them + switch (type) + { + case ESM.Type.Armor: + return npc.services.Contains(CharacterContent.Service.BartersArmor); + case ESM.Type.Book: + return npc.services.Contains(CharacterContent.Service.BartersBooks); + case ESM.Type.Clothing: + return npc.services.Contains(CharacterContent.Service.BartersClothing); + case ESM.Type.Ingredient: + return npc.services.Contains(CharacterContent.Service.BartersIngredients); + case ESM.Type.Light: + return npc.services.Contains(CharacterContent.Service.BartersLights); + case ESM.Type.MiscItem: + return npc.services.Contains(CharacterContent.Service.BartersMiscItems); + case ESM.Type.Weapon: + return npc.services.Contains(CharacterContent.Service.BartersWeapons); + case ESM.Type.Probe: + return npc.services.Contains(CharacterContent.Service.BartersProbes); + case ESM.Type.Lockpick: + return npc.services.Contains(CharacterContent.Service.BartersLockpicks); + case ESM.Type.RepairItem: + return npc.services.Contains(CharacterContent.Service.BartersRepairItems); + case ESM.Type.Alchemy: + return npc.services.Contains(CharacterContent.Service.BartersAlchemy); + case ESM.Type.Apparatus: + return npc.services.Contains(CharacterContent.Service.BartersApparatus); + default: + return false; + } + } - Papyrus papyrus = esm.GetPapyrus(content.papyrus); - if (papyrus == null) { continue; } // no script or failed to get script, skip + Cell cell = npc.cell; + List<(string id, int quantity)> shopInv = new(); - List calls = papyrus.GetCalls(); - PreProcessScriptedPositions(content, calls); + void AddOrIncrement(List<(string id, int quantity)> list, (string id, int quantity) tuple) + { + for (int i = 0; i < list.Count(); i++) + { + (string id, int quantity) entry = list[i]; + if (entry.id.ToLower() == tuple.id.ToLower()) { + list.RemoveAt(i); + list.Add((entry.id, entry.quantity + tuple.quantity)); // can't increment value in a tuple because fuck + return; + } + } + list.Add(tuple); } - } - foreach (InteriorGroup group in interiors) - { - foreach(InteriorGroup.Chunk chunk in group.chunks) + foreach (ItemContent item in cell.items) // add loose items this npc owns { - List allContent = chunk.GetAllContent().ToList(); - foreach (Content content in allContent) + if (item.ownerNpc == npc.id) { - if (content is PhasedNpcContent) { continue; } // skip phased npcs as we don't want to re-process them - - Papyrus papyrus = esm.GetPapyrus(content.papyrus); - if (papyrus == null) { continue; } // no script or failed to get script, skip + if (WillBarter(npc, item.type)) + { + AddOrIncrement(shopInv, (item.id, 1)); + } + } + } + foreach (ContainerContent container in cell.containers) // add containers this npc owns + { + if (container.ownerNpc == npc.id) + { + foreach ((string id, int quantity) tuple in container.inventory) + { + Record record = esm.FindRecordById(tuple.id); + if (WillBarter(npc, record.type)) + { + AddOrIncrement(shopInv, tuple); + } + } + } + } - List calls = papyrus.GetCalls(); - PreProcessScriptedPositions(content, calls); + foreach ((string id, int quantity) tuple in npc.inventory) // add own inventory to potential barter + { + Record record = esm.FindRecordById(tuple.id); + if (WillBarter(npc, record.type)) + { + AddOrIncrement(shopInv, tuple); } } + if (shopInv.Count() > 0) { npc.barter = shopInv; } } - foreach(Papyrus papyrus in esm.scripts) + foreach (Tile tile in tiles) { - foreach(Papyrus.Call call in papyrus.GetCalls(Papyrus.Call.Type.StartScript)) + foreach (NpcContent npc in tile.npcs) { SetupShop(npc); } + foreach (CreatureContent creature in tile.creatures) { SetupShop(creature); } + } + foreach (InteriorGroup group in interiors) + { + foreach (InteriorGroup.Chunk chunk in group.chunks) { - Papyrus subscript = esm.GetPapyrus(call.parameters[0]); - if (subscript == null) { continue; } // no script or failed to get script, skip - - List calls = subscript.GetCalls(); - PreProcessScriptedPositions(null, calls); + foreach (NpcContent npc in chunk.npcs) { SetupShop(npc); } + foreach (CreatureContent creature in chunk.creatures) { SetupShop(creature); } } } + } - foreach (Dialog.DialogRecord dialog in esm.dialog) + private void PrepareCellTilesAndEmitters(Cache cache, ESM esm, ScriptManager scriptManager, HashSet allReferences) + { + foreach (Cell cell in esm.exterior) { - List calls = dialog.GetCalls(); - PreProcessScriptedPositions(null, calls); // null here means we cannot process self reference calls. will print error if we run into one - } + HugeTile huge = GetHugeTile(cell.center); + TerrainInfo terrain = cache.GetTerrain(cell.coordinate); + if (terrain != null) + { + if (huge != null) { huge.AddTerrain(cell.center, terrain); } + else { Lort.Log($" ## WARNING ## Terrain fell outside of reality [{cell.coordinate.x}, {cell.coordinate.y}] -- {cell.region} :: B02", Lort.Type.Debug); } + } - /* Process character aipackage positions */ - foreach (Tile tile in tiles) { tile.ProcessTravelPoints(scriptManager); } - foreach (InteriorGroup group in interiors) { group.ProcessTravelPositions(scriptManager); } + if (huge != null) + { + huge.AddCell(scriptManager, cell); - /* Generate map point placements */ - Dictionary> mapPoints = new(); + foreach (Content content in cell.contents) + { + if (content is AssetContent assetContent) + { + /* If an assetcontent has emitter nodes, we convert it to an emittercontent */ + /* We can't really do this earlier than this point sadly because we need both the ESM loaded and cache built to be able to catch this corner case */ + /* So we do it here */ + if (cache.GetModel(assetContent.mesh)?.HasEmitter() == true) + { + cache.AddConvertedEmitter(assetContent.ConvertToEmitter()); + } + } - // collect em all - foreach (Cell cell in esm.exterior) + huge.AddContent(cache, cell, content, allReferences.Contains(content.id.ToLower().Trim())); + } + } + else { Lort.Log($" ## WARNING ## Cell fell outside of reality [{cell.coordinate.x}, {cell.coordinate.y}] -- {cell.name} :: B02", Lort.Type.Debug); } + } + } + + private static void SetFollowerFlags(ESM esm, List allCalls) + { + /* MSB promotion pre-process step */ + /* Goal of this step is to identify all characters that have a "Follow" aipackage or a "AiFollow" call that target them. */ + void PreProcessFollow(Cell cell) { - if(!string.IsNullOrEmpty(cell.name)) + foreach (Content content in cell.contents) { - Landscape landscape = esm.GetLandscape(cell.coordinate); - Vector3 center; - if (landscape == null) { center = new(); } - else { center = cell.center + new Vector3(0f, landscape.GetHeightAverage(), 0f); } - - if (mapPoints.ContainsKey(cell.name)) { mapPoints[cell.name].Add(center); } - else { mapPoints.Add(cell.name, new() { center }); } + if (content is CharacterContent cc) + { + foreach (NpcContent.AiPackage package in cc.packages) + { + if (package.type == CharacterContent.AiPackage.Type.Follow && package.target == "player") { cc.follower = true; return; } + } + } } } + foreach (Cell cell in esm.exterior) { PreProcessFollow(cell); } // handle ai packages part + foreach (Cell cell in esm.interior) { PreProcessFollow(cell); } - // merge similar - HashSet pointNames = new(); - List importants = new(); - foreach(var kvp in mapPoints) + void SetFollowerFlagByRecordId(Cell cell, string id) { - string name = kvp.Key; // name - Vector3 center = new(); // average of all points with same name - float radius = Const.CELL_SIZE / 2; // minimum size for radius of map point is 1 cell - - Vector3 first = kvp.Value.First(); - foreach(Vector3 pos in kvp.Value) + foreach (Content content in cell.contents) { - radius = Math.Max(radius, Vector3.Distance(first, pos)); - center += pos; + if (content is CharacterContent cc && content.id.Trim().ToLower() == id.Trim().ToLower()) + { + cc.follower = true; + } } - - center *= (1f / kvp.Value.Count()); - - MapPoint.Icon icon = Override.GetMapIcon(name); - if (icon == MapPoint.Icon.None) { continue; } // skip these - Script.Flag discoverFlag = scriptManager.common.CreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Bit, Script.Flag.Designation.DiscoverLocation, name); - Layout.MapPoint mapPoint = new(name, center, radius, true, discoverFlag, icon); - Tile tile = GetTile(center); - tile.AddMapPoint(mapPoint); - importants.Add(mapPoint); - pointNames.Add(name.ToLower().Trim()); } - foreach(Tile tile in tiles) + foreach (Papyrus.Call call in allCalls) // Reusing "allCalls" list from above { - foreach(DoorContent door in tile.doors) + // Grab record reference from target if exists + if (call.target != null && call.parameters.Count() > 0 && call.parameters[0].ToLower().Trim() == "player") { - if(door.warp != null) + switch (call.type) { - string name = door.warp.cell; - if (name.Contains(",")) { name = name.Split(",")[0].Trim(); } // Split area sub names so we just have the main area name. Changes things like "Shipwreck, Upper Level" to just "Shipwreck" - MapPoint.Icon icon = Override.GetMapIcon(name); - - if (icon == MapPoint.Icon.None) { continue; } // skip these - - // checks if a position is inside of one of the important map points we created above. skip these too! - if (importants.Any(p => Vector3.Distance(door.position, p.position) <= p.radius)) { continue; } - - // see if map point has already been made. some areas have multiple entrances or exits - var lowerName = name.ToLower().Trim(); - if(pointNames.Contains(lowerName)) { continue; } - pointNames.Add(lowerName); - - const float UNIMPORTANT_SIZE_MODIFIER = 0.3f; - Script.Flag discoverFlag = scriptManager.common.CreateFlag(Script.Flag.Category.Saved, Script.Flag.Type.Bit, Script.Flag.Designation.DiscoverLocation, name); // if 2 doors go to the same interior we share the flag - Layout.MapPoint mapPoint = new(name, door.position, Const.CELL_SIZE * UNIMPORTANT_SIZE_MODIFIER, false, discoverFlag, icon); - tile.AddMapPoint(mapPoint); + case Papyrus.Call.Type.AiFollow: + case Papyrus.Call.Type.AiFollowCell: + case Papyrus.Call.Type.AiEscort: + case Papyrus.Call.Type.AiEscortCell: + foreach (Cell cell in esm.exterior) { SetFollowerFlagByRecordId(cell, call.target); } // handle papyrus call part + foreach (Cell cell in esm.interior) { SetFollowerFlagByRecordId(cell, call.target); } + break; + default: break; } } } - Lort.TaskIterate(); // Progress bar update } private void RegisterDoorWarp(DoorContent door, ScriptManager scriptManager)