using CodeWalker.Core.Utils; using CodeWalker.GameFiles; using Collections.Pooled; using CommunityToolkit.HighPerformance; using Microsoft.Extensions.ObjectPool; using SharpDX; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; namespace CodeWalker.World { public class Space { public LinkedList TemporaryEntities = new LinkedList(); public LinkedList PersistentEntities = new LinkedList(); public List EnabledEntities = new List(); //built each frame private GameFileCache GameFileCache = null; public SpaceMapDataStore MapDataStore; public SpaceBoundsStore BoundsStore; private Dictionary interiorLookup = new Dictionary(); private Dictionary interiorManifest = new Dictionary(); private Dictionary interiorProxies = new Dictionary(); private Dictionary dataGroupDict = new Dictionary(); private Dictionary nodedict = new Dictionary(); private Dictionary boundsdict = new Dictionary(); private Dictionary usedboundsdict = new Dictionary(); private Dictionary ymaptimes = new Dictionary(); private Dictionary ymapweathertypes = new Dictionary(); public bool Inited = false; public SpaceNodeGrid NodeGrid; private Dictionary AllYnds = new Dictionary(); public SpaceNavGrid NavGrid; public List Collisions = new List(); private bool[] CollisionLayers = new[] { true, false, false }; private int CurrentHour; private MetaHash CurrentWeather; public async Task InitAsync(GameFileCache gameFileCache, Action? updateStatus) { using var _ = new DisposableTimer("Space Init"); GameFileCache = gameFileCache; var task = Task.Run(() => InitNodeGrid("Space Init")); updateStatus?.Invoke("Scanning manifests..."); InitManifestData("Space Init"); updateStatus?.Invoke("Scanning caches..."); await InitCacheDataAsync("Space Init"); updateStatus?.Invoke("Building map data store..."); InitMapDataStore("Space Init"); updateStatus?.Invoke("Building bounds store..."); InitBoundsStore("Space Init"); updateStatus?.Invoke("Loading nav meshes..."); InitNavGrid("Space Init"); await task; Inited = true; updateStatus?.Invoke("World initialised."); } private void InitManifestData([CallerMemberName] string callerName = "") { using var _ = new DisposableTimer($"{callerName} -> {nameof(InitManifestData)}"); interiorLookup.Clear(); interiorManifest.Clear(); ymaptimes.Clear(); ymapweathertypes.Clear(); dataGroupDict.Clear(); var manifests = GameFileCache.AllManifests; foreach (var manifest in manifests.AsSpan()) { //build interior lookup - maps child->parent interior bounds foreach(var interior in manifest.Interiors) { var intname = interior.Interior.Name; interiorManifest[intname] = interior; if (interior.Bounds != null) { foreach (var intbound in interior.Bounds) { interiorLookup[intbound] = intname; } } } //these appear to be all the dynamic "togglable" ymaps.... if (manifest.MapDataGroups.Length > 0) { foreach(var mapgroup in manifest.MapDataGroups) { if (mapgroup.HoursOnOff != 0) { ymaptimes[mapgroup.Name] = mapgroup.HoursOnOff; } if (mapgroup.WeatherTypes.Length > 0) { ymapweathertypes[mapgroup.Name] = mapgroup.WeatherTypes; } if (dataGroupDict.ContainsKey(mapgroup.DataGroup.Name)) { if (mapgroup.Bounds.Length == 0) { continue; } } dataGroupDict[mapgroup.DataGroup.Name] = mapgroup; } } } //YmfMapDataGroups string //StringBuilder sb = new StringBuilder(); //foreach (var dg in dataGroupDict.Values) //{ // sb.AppendLine(dg.ToString()); // if (dg.Bounds != null) // { // foreach (var ybnh in dg.Bounds) // { // sb.AppendLine(" " + ybnh.ToString()); // } // } //} //string str = sb.ToString(); } private async Task InitCacheDataAsync([CallerMemberName] string callerName = "") { //build the grid from the cached data using var _ = new DisposableTimer($"{callerName} -> {nameof(InitCacheDataAsync)}"); var caches = GameFileCache.AllCacheFiles; nodedict = new Dictionary(6000); //List intlist = new List(); boundsdict = new Dictionary(10000); usedboundsdict = new Dictionary(10000); interiorProxies = new Dictionary(1000); //Dictionary filedates = new Dictionary(1500); //Dictionary filedates2 = new Dictionary(1500); using(new DisposableTimer("caches loop")) { foreach (var cache in caches) { //foreach (var filedate in cache.FileDates) //{ // if (filedates.TryGetValue(filedate.FileName, out CacheFileDate? exdate)) // { // if (filedate.TimeStamp >= exdate.TimeStamp) // { // filedates[filedate.FileName] = filedate; // } // } // else // { // filedates[filedate.FileName] = filedate; // } // if (filedates2.TryGetValue(filedate.FileID, out exdate)) // { // if (filedate.TimeStamp >= exdate.TimeStamp) // { // filedates2[filedate.FileID] = filedate; // } // } // else // { // filedates2[filedate.FileID] = filedate; // } //} foreach (var node in cache.AllMapNodes) { if (!GameFileCache.YmapDict.ContainsKey(node.Name)) continue; nodedict[node.Name] = node; } foreach (var intprx in cache.AllCInteriorProxies) { //these might need to go into the grid. which grid..? //but might need to map back to the bounds store... this has more info though! SpaceBoundsKey key = new SpaceBoundsKey(intprx.Name, intprx.Position); //if (interiorProxies.ContainsKey(key)) //{ }//updates/dlc hit here interiorProxies[key] = intprx; } foreach (var item in cache.AllBoundsStoreItems) { if (!GameFileCache.YbnDict.ContainsKey(item.Name)) continue; if (!interiorLookup.TryGetValue(item.Name, out var inthash)) { SpaceBoundsKey key = new SpaceBoundsKey(item.Name, item.Min); boundsdict[key] = item; } //else //{ // intlist.Add(item);//handle it later? use the parent for a dict? //} usedboundsdict[item.Name] = item; } } } using var ymapTimer = new DisposableTimer("ymaps", false); using var ybnTimer = new DisposableTimer("ybns", false); using(new DisposableTimer("maprpfs.Values")) { //try and generate the cache data for uncached ymaps... mainly for mod maps! var maprpfs = GameFileCache.ActiveMapRpfFiles; foreach (var maprpf in maprpfs.Values) { foreach (var entry in maprpf.AllEntries) { if (entry.IsExtension(".ymap")) { ymapTimer.Stopwatch.Start(); if (!nodedict.ContainsKey(new MetaHash(entry.ShortNameHash))) { //non-cached ymap. mostly only mods... but some interesting test things also var ymap = await RpfManager.GetFileAsync(entry); if (ymap is not null) { MapDataStoreNode dsn = new MapDataStoreNode(ymap); if (dsn.Name != 0) { nodedict[dsn.Name] = dsn;//perhaps should add as entry.ShortNameHash? } } } ymapTimer.Stopwatch.Stop(); } if (entry.IsExtension(".ybn")) { ybnTimer.Stopwatch.Start(); MetaHash ehash = new MetaHash(entry.ShortNameHash); if (!usedboundsdict.ContainsKey(ehash)) { if (!interiorLookup.ContainsKey(ehash)) { //exterior ybn's that aren't already cached... only noncached modded bounds hit here... //load the ybn and cache its extents. var ybn = await RpfManager.GetFileAsync(entry); if (ybn is not null) { BoundsStoreItem item = new BoundsStoreItem(ybn.Bounds); item.Name = ehash; SpaceBoundsKey key = new SpaceBoundsKey(ehash, item.Min); boundsdict[key] = item; } } } ybnTimer.Stopwatch.Stop(); } } } } Console.WriteLine($"nodedict: {nodedict.Count}; boundsdict: {boundsdict.Count}; usedboundsdict: {usedboundsdict.Count}; interiorProxies: {interiorProxies.Count}"); } private void InitMapDataStore([CallerMemberName] string callerName = "") { using var _ = new DisposableTimer($"{callerName} -> {nameof(InitMapDataStore)}"); MapDataStore = new SpaceMapDataStore(); MapDataStore.Init(nodedict.Values); } private void InitBoundsStore([CallerMemberName] string callerName = "") { using var _ = new DisposableTimer($"{callerName} -> {nameof(InitBoundsStore)}"); BoundsStore = new SpaceBoundsStore(); BoundsStore.Init(boundsdict.Values); } private Dictionary GetYndEntries() { using var addRpfYndTimer = new DisposableTimer($"{nameof(InitNodeGrid)} -> AddRpfYnds"); var rpfman = GameFileCache.RpfMan; Dictionary yndentries = new Dictionary(); foreach (var rpffile in GameFileCache.BaseRpfs.AsSpan()) //load nodes from base rpfs { AddRpfYnds(rpffile, yndentries); } if (GameFileCache.EnableDlc) { var updrpf = rpfman.FindRpfFile("update\\update.rpf"); //load nodes from patch area... if (updrpf is not null) { foreach (var rpffile in updrpf.Children.Span) { AddRpfYnds(rpffile, yndentries); } } foreach (var dlcrpf in GameFileCache.DlcActiveRpfs.AsSpan()) //load nodes from current dlc rpfs { if (dlcrpf.Path.StartsWith("x64", StringComparison.OrdinalIgnoreCase)) continue; //don't override update.rpf YNDs with x64 ones! *hack foreach (var rpffile in dlcrpf.Children.Span) { AddRpfYnds(rpffile, yndentries); } } } return yndentries; } private async Task InitNodeGrid([CallerMemberName] string callerName = "") { using var _ = new DisposableTimer($"{callerName} -> {nameof(InitNodeGrid)}"); NodeGrid = new SpaceNodeGrid(); AllYnds.Clear(); var yndentries = GetYndEntries(); var addRpfYndTimer = new DisposableTimer($"{nameof(InitNodeGrid)} -> BuildNodeGrid"); Vector3 corner = new Vector3(-8192, -8192, -2048); Vector3 cellsize = new Vector3(512, 512, 4096); await Parallel.ForAsync(0, NodeGrid.CellCountX, async (x, cancellationToken) => { for (int y = 0; y < NodeGrid.CellCountY; y++) { var cell = NodeGrid.Cells[x, y]; string fname = $"nodes{cell.ID}.ynd"; uint fnhash = JenkHash.GenHash(fname); if (yndentries.TryGetValue(fnhash, out RpfFileEntry? fentry)) { cell.Ynd = await RpfManager.GetFileAsync(fentry); cell.Ynd.BBMin = corner + (cellsize * new Vector3(x, y, 0)); cell.Ynd.BBMax = cell.Ynd.BBMin + cellsize; cell.Ynd.CellX = x; cell.Ynd.CellY = y; cell.Ynd.Loaded = true; lock(AllYnds) { AllYnds[fnhash] = cell.Ynd; } #region node flags test //if (cell.Ynd == null) continue; //if (cell.Ynd.NodeDictionary == null) continue; //if (cell.Ynd.NodeDictionary.Nodes == null) continue; //var na = cell.Ynd.NodeDictionary.Nodes; //for (int i = 0; i < na.Length; i++) //{ // var node = na[i]; // int nodetype = node.Unk25Type & 7; // int linkcount = node.Unk25Type >> 3; // int nxtlink = node.LinkID + linkcount; // if (i < na.Length - 1) // { // var nxtnode = na[i + 1]; // if (nxtnode.LinkID != nxtlink) // { } // } // else // { // if (nxtlink != cell.Ynd.NodeDictionary.LinksCount) // { } // } // switch (node.Flags0) // { // case 0: // case 1: // case 2: // case 8: // case 10: // case 32: // case 34: // case 35: // case 40: // case 42: // case 66: // case 98: // case 129: // case 130: // case 162: // case 194: // case 226: // break; // default: // break; // } // switch (node.Flags1) // { // case 0: // case 1: // case 2: // case 3: // case 4: // case 16: // case 80: // case 112: // case 120: // case 121: // case 122: // case 128: // case 129: // case 136: // case 144: // case 152: // case 160: // break; // default: // break; // } //} #endregion } } }); addRpfYndTimer.Dispose(); addRpfYndTimer = new DisposableTimer($"{nameof(InitNodeGrid)} -> BuildYndData"); //join the dots.... //StringBuilder sb = new StringBuilder(); var allYnds = AllYnds.Values.ToArray(); Parallel.ForEach(allYnds, BuildYndData); //foreach (var ynd in AllYnds.Values) //{ // BuildYndData(ynd); // //sb.Append(ynd.nodestr); //} addRpfYndTimer.Dispose(); //string str = sb.ToString(); } public void PatchYndFile(YndFile ynd) { //ideally we should be able to revert to the vanilla ynd's after closing the project window, //but codewalker can always just be restarted, so who cares really NodeGrid.UpdateYnd(ynd); } private static void AddRpfYnds(RpfFile rpffile, Dictionary yndentries) { if (rpffile.AllEntries is null) return; foreach (var entry in rpffile.AllEntries.Span) { if (entry is RpfFileEntry fentry) { if (fentry.IsExtension(".ynd")) { yndentries[entry.NameHash] = fentry; } } } } public void BuildYndLinks(YndFile ynd) { var ynodes = ynd.Nodes; var nodes = ynd.NodeDictionary?.Nodes; var links = ynd.NodeDictionary?.Links; if (ynodes is null || nodes is null || links is null) return; int nodecount = ynodes.Length; //build the links arrays. var tlinks = PooledListPool.Shared.Get(); var nlinks = PooledListPool.Shared.Get(); tlinks.Clear(); for (int i = 0; i < nodecount; i++) { nlinks.Clear(); var node = ynodes[i]; var linkid = node.LinkID; for (int l = 0; l < node.LinkCount; l++) { var llid = linkid + l; if (llid >= links.Length) continue; var link = links[llid]; YndNode? tnode; if (link.AreaID == node.AreaID) { if (link.NodeID >= ynodes.Length) continue; tnode = ynodes[link.NodeID]; } else { tnode = NodeGrid.GetYndNode(link.AreaID, link.NodeID); if (tnode == null) continue; //if ((Math.Abs(tnode.Ynd.CellX - ynd.CellX) > 1) || (Math.Abs(tnode.Ynd.CellY - ynd.CellY) > 1)) //{ /*continue;*/ } //non-adjacent cell? seems to be the carrier problem... } YndLink yl = new YndLink(ynd, node, tnode, link); tlinks.Add(yl); nlinks.Add(yl); } node.Links = nlinks.ToArray(); } ynd.Links = tlinks.ToArray(); PooledListPool.Shared.Return(tlinks); PooledListPool.Shared.Return(nlinks); } public void BuildYndVerts(YndFile ynd, YndNode[]? selectedNodes = null) { var laneColour = 4294901760; // (uint) new Color4(0f, 0f, 1f, 1f).ToRgba(); var ynodes = ynd.Nodes; if (ynodes is null) return; int nodecount = ynodes.Length; //build the main linked vertex array (used by the renderable to draw the lines). var tverts = PooledListPool.Shared.Get(); tverts.Clear(); for (int i = 0; i < nodecount; i++) { var node = ynodes[i]; if (node.Links is null) continue; var nvert = new EditorVertex(node.Position, (uint)node.ColourRgba); foreach(var yl in node.Links) { var laneDir = yl.GetDirection(); var laneDirCross = Vectors.Cross(in laneDir, in Vector3.UnitZ); var laneWidth = yl.GetLaneWidth(); var laneHalfWidth = laneWidth / 2; var offset = yl.IsTwoWay() ? yl.LaneOffset * laneWidth - laneHalfWidth : yl.LaneOffset - yl.LaneCountForward * laneWidth / 2f + laneHalfWidth; var iOffset = yl.IsTwoWay() ? 1 : 0; var tnode = yl.Node2; if (tnode is null) continue; //invalid links could hit here var tvert = new EditorVertex(tnode.Position, (uint)tnode.ColourRgba); tverts.Add(nvert); tverts.Add(tvert); // Add lane display for (int j = iOffset; j < yl.LaneCountForward + iOffset; j++) { var vertOffset = laneDirCross * (offset + laneWidth * j); vertOffset.Z = 0.1f; var lvert1 = new EditorVertex(nvert.Position + vertOffset, laneColour); var lvert2 = new EditorVertex(tvert.Position + vertOffset, laneColour); tverts.Add(lvert1); tverts.Add(lvert2); // Arrow var apos = lvert1.Position + laneDir * yl.LinkLength / 2; const float asize = 0.5f; const float negasize = asize * -1f; tverts.Add(new EditorVertex(apos, laneColour)); tverts.Add(new EditorVertex(apos + laneDir * negasize + laneDirCross * asize, laneColour)); tverts.Add(new EditorVertex(apos, laneColour)); tverts.Add(new EditorVertex(apos + laneDir * negasize + laneDirCross * negasize, laneColour)); } } } ynd.LinkedVerts = tverts.ToArray(); PooledListPool.Shared.Return(tverts); ynd.UpdateTriangleVertices(selectedNodes); } public void BuildYndJuncs(YndFile ynd) { //attach the junctions to the nodes. var yjuncs = ynd.Junctions; if (yjuncs != null) { var junccount = yjuncs.Length; for (int i = 0; i < junccount; i++) { var junc = yjuncs[i]; if (!NodeGrid.TryGetCell(junc.RefData.AreaID, out var cell) || cell?.Ynd?.Nodes is null) continue; var jynd = cell.Ynd; if (cell.Ynd != ynd) //junc in different ynd..? no hits here, except ynds in project.. { if (cell.Ynd.AreaID == ynd.AreaID) { jynd = ynd; } else { } } if (junc.RefData.NodeID >= jynd.Nodes.Length) continue; var jnode = jynd.Nodes[junc.RefData.NodeID]; jnode.Junction = junc; jnode.HasJunction = true; } } } public void BuildYndData(YndFile ynd) { ArgumentNullException.ThrowIfNull(ynd, nameof(ynd)); BuildYndLinks(ynd); BuildYndJuncs(ynd); BuildYndVerts(ynd); } public YndFile[] GetYndFilesThatDependOnYndFile(YndFile file) { return AllYnds.Values.Where(y => y.Links.Any(l => l.Node2.AreaID == file.AreaID)).ToArray(); } public void MoveYndArea(YndFile ynd, int desiredX, int desiredY) { var xDir = Math.Min(1, Math.Max(-1, desiredX - ynd.CellX)); var yDir = Math.Min(1, Math.Max(-1, desiredY - ynd.CellY)); var x = desiredX; var y = desiredY; if (xDir != 0) { while (x >= 0 && x <= 31) { if (NodeGrid.Cells[x, y].Ynd == null) { break; } x += xDir; } } if (yDir != 0) { while (y >= 0 && y <= 31) { if (NodeGrid.Cells[x, y].Ynd == null) { break; } y += yDir; } } ynd.CellX = x; ynd.CellY = y; var areaId = y * 32 + x; ynd.AreaID = areaId; ynd.Name = $"nodes{areaId}"; NodeGrid.UpdateYnd(ynd); } public void RecalculateAllYndIndices() { foreach (var yndFile in AllYnds.Values) { yndFile.RecalculateNodeIndices(); } } private void InitNavGrid([CallerMemberName] string callerName = "") { using var _ = new DisposableTimer($"{callerName} -> InitNavGrid"); NavGrid = new SpaceNavGrid(); var rpfman = GameFileCache.RpfMan; Dictionary ynventries = new Dictionary(); foreach (var rpffile in GameFileCache.BaseRpfs) //load navmeshes from base rpfs { AddRpfYnvs(rpffile, ynventries); } if (GameFileCache.EnableDlc) { var updrpf = rpfman.FindRpfFile("update\\update.rpf"); //load navmeshes from patch area... if (updrpf != null) { foreach (var rpffile in updrpf.Children) { AddRpfYnvs(rpffile, ynventries); } } foreach (var dlcrpf in GameFileCache.DlcActiveRpfs) //load navmeshes from current dlc rpfs { foreach (var rpffile in dlcrpf.Children) { AddRpfYnvs(rpffile, ynventries); } } } for (int x = 0; x < NavGrid.CellCountX; x++) { for (int y = 0; y < NavGrid.CellCountY; y++) { var cell = NavGrid.Cells[x, y]; string fname = "navmesh[" + cell.FileX.ToString() + "][" + cell.FileY.ToString() + "].ynv"; uint fnhash = JenkHash.GenHash(fname); RpfFileEntry fentry = null; if (ynventries.TryGetValue(fnhash, out fentry)) { cell.YnvEntry = fentry as RpfResourceFileEntry; //cell.Ynv = rpfman.GetFile(fentry); } } } } private void AddRpfYnvs(RpfFile rpffile, Dictionary ynventries) { if (rpffile.AllEntries is null) return; foreach (var entry in rpffile.AllEntries.Span) { if (entry is RpfFileEntry fentry) { if (entry.IsExtension(".ynv")) { ynventries[entry.NameHash] = fentry; } } } } public void Update(float elapsed) { if (!Inited) return; if (BoundsStore is null) return; if (elapsed > 0.1f) elapsed = 0.1f; Collisions.Clear(); EnabledEntities.Clear(); foreach (var e in PersistentEntities) { if (e.Enabled) EnabledEntities.Add(e); } foreach (var e in TemporaryEntities) { if (e.Enabled) EnabledEntities.Add(e); } float gravamt = -9.8f; Vector3 dvgrav = new Vector3(0, 0, gravamt * elapsed); //gravity acceleration vector dvgrav += (0.5f * dvgrav * elapsed); //v = ut+0.5at^2 ! float minvel = 0.5f; // stop bouncing when slow... foreach (var e in EnabledEntities) { if (!e.Enabled) continue; e.Velocity += dvgrav; //apply gravity e.Momentum = e.Velocity * e.Mass; e.Age += elapsed; e.PreUpdate(elapsed); if (e.EnableCollisions) { var coll = FindFirstCollision(e, elapsed); if (coll.Hit) { Collisions.Add(coll); float argvel = Math.Abs((e.Velocity - dvgrav).Length()); if (e.WasColliding && (argvel < minvel)) { e.Velocity = Vector3.Zero; e.Momentum = Vector3.Zero; } else { e.Position = coll.PrePos; //move to the last known position before collision //bounce... int maxbounce = 5; int curbounce = 0; float trem = 1.0f - coll.PreT; while (trem > 0) { float vl = e.Velocity.Length(); float erem = elapsed * trem; float drem = vl * erem; Vector3 hitn = coll.SphereHit.Normal; Vector3 bdir = Vector3.Reflect(coll.HitVelDir, hitn); Vector3 newvel = bdir * (vl * 0.5f); //restitution/bouncyness e.Velocity = newvel; coll = FindFirstCollision(e, erem); if (!coll.Hit) { e.Position = coll.HitPos;//no hit, all done break; } Collisions.Add(coll); e.Position = coll.PrePos; trem = Math.Max(trem * (1.0f - coll.PreT), 0); curbounce++; if (curbounce >= maxbounce) { e.Position = coll.HitPos; break; } //if ((coll.PreT <= 0))// || (coll.SphereHit.Normal == hitn)) //{ // //ae.Velocity = Vector3.Zero; //same collision twice? abort? // break; //} } e.Momentum = e.Velocity * e.Mass; } e.WasColliding = true; } else { e.Position = coll.HitPos; //hit pos is the end pos if no hit e.WasColliding = false; } } if (e.EntityDef != null) { e.EntityDef.Position = e.Position; } if ((e.Lifetime > 0.0f) && (e.Age > e.Lifetime)) { TemporaryEntities.Remove(e); } } } public SpaceEntityCollision FindFirstCollision(Entity e, float elapsed) { SpaceEntityCollision r = new SpaceEntityCollision(); r.Entity = e; Vector3 pos = e.Position; Vector3 sphpos = pos + e.Center; Vector3 disp = e.Velocity * elapsed; float absdisp = disp.Length(); r.HitVelDir = Vector3.Normalize(disp); r.HitPos = pos + disp; r.HitVel = e.Velocity; r.HitT = 1.0f; r.PreT = 0.0f; r.PrePos = pos; BoundingSphere sph = new BoundingSphere(r.HitPos + e.Center, e.Radius); r.SphereHit = SphereIntersect(ref sph, CollisionLayers); if (!r.SphereHit.Hit) { if (absdisp > e.Radius) //fast-moving... do a ray test to make sure it's not tunnelling { Ray rayt = new Ray(sphpos, r.HitVelDir); float rayl = absdisp + e.Radius * 4.0f; //include some extra incase of glancing hit var rayhit = RayIntersect(ref rayt, rayl); if (rayhit.Hit) //looks like it is tunnelling... need to find the sphere hit point { sph.Center = rayhit.Position - (r.HitVelDir*Math.Min(e.Radius*0.5f, rayhit.HitDist)); float hitd = rayhit.HitDist; r.HitT = hitd / absdisp; if (r.HitT > 1.0f) { r.HitT = 1.0f; sph.Center = r.HitPos + e.Center; //this really shouldn't happen... but just in case of glancing hit.. } r.SphereHit = SphereIntersect(ref sph, CollisionLayers); //this really should be a hit! } } } if (r.SphereHit.Hit) { int maxiter = 6;//(would be better to iterate until error within tolerance..) int curiter = 0; float curt = r.HitT * 0.5f; float step = curt * 0.5f; float minstep = 0.05f; while (curiter < maxiter) //iterate to find a closer hit time... improve this! { sph.Center = sphpos + disp * curt; var tcollres = SphereIntersect(ref sph, CollisionLayers); if (tcollres.Hit) { r.HitT = curt; r.HitPos = sph.Center - e.Center; r.SphereHit = tcollres; //only use the best hit (ignore misses) r.HitNumber = curiter; } else { r.PreT = curt; r.PrePos = sph.Center - e.Center; } curiter++; if (curiter < maxiter) { curt += step * (tcollres.Hit ? -1.0f : 1.0f); step *= 0.5f; } if (absdisp * step < minstep) { break; } } } r.Hit = r.SphereHit.Hit; return r; } public void AddTemporaryEntity(Entity e) { e.Space = this; while (TemporaryEntities.Count > 100) { TemporaryEntities.RemoveFirst();//don't be too laggy } TemporaryEntities.AddLast(e); } public void AddPersistentEntity(Entity e) { e.Space = this; PersistentEntities.AddLast(e); } public void RemovePersistentEntity(Entity e) { PersistentEntities.Remove(e); } private bool IsYmapAvailable(uint ymaphash, int hour, MetaHash weather) { if (hour >= 0 && hour <= 23) { if (ymaptimes.TryGetValue(ymaphash, out var ymaptime)) { uint mask = 1u << hour; if ((ymaptime & mask) == 0) return false; } } if (weather.Hash != 0) { if (ymapweathertypes.TryGetValue(ymaphash, out var weathers)) { foreach(var _weather in weathers) { if (_weather == weather) return true; } return false; } } return true; } public void GetVisibleYmaps(Camera cam, int hour, MetaHash weather, Dictionary ymaps) { if (!Inited || MapDataStore is null) return; CurrentHour = hour; CurrentWeather = weather; var items = MapDataStore.GetItems(in cam.Position); foreach(var item in items.AsSpan()) { if (item is null) { continue; } var hash = item.Name; if (!ymaps.ContainsKey(hash)) { var ymap = (hash > 0) ? GameFileCache.GetYmap(hash) : null; while (ymap is not null && ymap.Loaded) { if (!IsYmapAvailable(hash, hour, weather)) { break; } ymaps[hash] = ymap; hash = ymap._CMapData.parent; if (ymaps.ContainsKey(hash)) { break; } ymap = (hash > 0) ? GameFileCache.GetYmap(hash) : null; } } } } public void GetVisibleBounds(Camera cam, int gridrange, bool[] layers, List boundslist) { if (!Inited) return; if (BoundsStore is null) return; float dist = 50.0f * gridrange; var pos = cam.Position; var min = pos - dist; var max = pos + dist; var items = BoundsStore.GetItems(in min, in max, layers); boundslist.AddRange(items); } public void GetVisibleYnds(Camera cam, List ynds) { if (!Inited) return; if (NodeGrid is null) return; //int x = 9; //int y = 15; //== nodes489.ynd //ynds.Add(NodeGrid.Cells[x, y].Ynd); ynds.AddRange(AllYnds.Values); } public void GetVisibleYnvs(Camera cam, int gridrange, List ynvs) { if (!Inited) return; if (NavGrid is null) return; ynvs.Clear(); var pos = NavGrid.GetCellPos(cam.Position); int minx = Math.Min(Math.Max(pos.X - gridrange, 0), NavGrid.CellCountX-1); int maxx = Math.Min(Math.Max(pos.X + gridrange, 0), NavGrid.CellCountX-1); int miny = Math.Min(Math.Max(pos.Y - gridrange, 0), NavGrid.CellCountY-1); int maxy = Math.Min(Math.Max(pos.Y + gridrange, 0), NavGrid.CellCountY-1); for (int x = minx; x <= maxx; x++) { for (int y = miny; y <= maxy; y++) { var cell = NavGrid.GetCell(new Vector2I(x, y)); if ((cell != null) && (cell.YnvEntry != null)) { var hash = cell.YnvEntry.ShortNameHash; var ynv = (hash > 0) ? GameFileCache.GetYnv(hash) : null; if (ynv != null && ynv.Loaded) { ynvs.Add(ynv); } } } } } public SpaceRayIntersectResult RayIntersect(ref Ray ray, float maxdist = float.MaxValue, bool[]? layers = null) { var res = new SpaceRayIntersectResult(); if (GameFileCache is null) return res; bool testcomplete = true; res.HitDist = maxdist; var box = new BoundingBox(); float boxhitdisttest; if (BoundsStore is null || MapDataStore is null) return res; var boundslist = BoundsStore.GetItems(ref ray, layers); var mapdatalist = MapDataStore.GetItems(ref ray); for (int i = 0; i < boundslist.Count; i++) { var bound = boundslist[i]; box.Minimum = bound.Min; box.Maximum = bound.Max; if (ray.Intersects(ref box, out boxhitdisttest)) { if (boxhitdisttest > res.HitDist) { continue; } //already a closer hit YbnFile? ybn = GameFileCache.GetYbn(bound.Name); if (ybn is null) { continue; } //ybn not found? if (!ybn.Loaded) { testcomplete = false; continue; } //ybn not loaded yet... var b = ybn.Bounds; if (b is null) { continue; } var bhit = b.RayIntersect(ref ray, res.HitDist); if (bhit.Hit) { bhit.HitYbn = ybn; } res.TryUpdate(in bhit); } } for (int i = 0; i < mapdatalist.Count; i++) { var mapdata = mapdatalist[i]; if (mapdata == null) { continue; } if ((mapdata.ContentFlags & 1) == 0) { continue; } //only test HD ymaps box.Minimum = mapdata.entitiesExtentsMin; box.Maximum = mapdata.entitiesExtentsMax; if (ray.Intersects(ref box, out boxhitdisttest)) { if (boxhitdisttest > res.HitDist) { continue; } //already a closer hit var hash = mapdata.Name; var ymap = (hash > 0) ? GameFileCache.GetYmap(hash) : null; if (ymap is not null && ymap.Loaded && ymap.AllEntities.Length > 0) { if (!IsYmapAvailable(hash, CurrentHour, CurrentWeather)) { continue; } foreach(var ent in ymap.AllEntities) { if (!EntityCollisionsEnabled(ent)) { continue; } box.Minimum = ent.BBMin; box.Maximum = ent.BBMax; if (ray.Intersects(ref box, out boxhitdisttest)) { if (boxhitdisttest > res.HitDist) { continue; } //already a closer hit if (ent.IsMlo) { var ihit = RayIntersectInterior(ref ray, ent, res.HitDist); res.TryUpdate(in ihit); } else { var ehit = RayIntersectEntity(ref ray, ent, res.HitDist); res.TryUpdate(in ehit); } } } } else if (ymap is not null && !ymap.Loaded) { testcomplete = false; } } } if (res.Hit) { res.Position = ray.Position + ray.Direction * res.HitDist; } res.TestComplete = testcomplete; return res; } public SpaceRayIntersectResult RayIntersectEntity(ref Ray ray, YmapEntityDef ent, float maxdist = float.MaxValue) { var res = new SpaceRayIntersectResult(); res.HitDist = maxdist; var drawable = GameFileCache.TryGetDrawable(ent.Archetype); if (drawable != null) { var eori = ent.Orientation; var eorinv = Quaternion.Invert(ent.Orientation); var eray = new Ray(); eray.Position = eorinv.Multiply(ray.Position - ent.Position); eray.Direction = eorinv.Multiply(in ray.Direction); if ((drawable is Drawable sdrawable) && (sdrawable.Bound != null)) { var dhit = sdrawable.Bound.RayIntersect(ref eray, res.HitDist); if (dhit.Hit) { dhit.Position = eori.Multiply(in dhit.Position) + ent.Position; dhit.Normal = eori.Multiply(in dhit.Normal); } res.TryUpdate(in dhit); } else if (drawable is FragDrawable fdrawable) { if (fdrawable.Bound is not null) { var fhit = fdrawable.Bound.RayIntersect(ref eray, res.HitDist); if (fhit.Hit) { fhit.Position = eori.Multiply(in fhit.Position) + ent.Position; fhit.Normal = eori.Multiply(in fhit.Normal); } res.TryUpdate(in fhit); } var fbound = fdrawable.OwnerFragment?.PhysicsLODGroup?.PhysicsLOD1?.Bound; if (fbound is not null) { var fhit = fbound.RayIntersect(ref eray, res.HitDist);//TODO: these probably have extra transforms..! if (fhit.Hit) { fhit.Position = eori.Multiply(in fhit.Position) + ent.Position; fhit.Normal = eori.Multiply(in fhit.Normal); } res.TryUpdate(in fhit); } } } if (res.Hit) { res.HitEntity = ent; } return res; } public SpaceRayIntersectResult RayIntersectInterior(ref Ray ray, YmapEntityDef mlo, float maxdist = float.MaxValue) { var res = new SpaceRayIntersectResult(); res.HitDist = maxdist; if (mlo.Archetype is null) { return res; } var iori = mlo.Orientation; var iorinv = Quaternion.Invert(mlo.Orientation); var iray = new Ray(); iray.Position = iorinv.Multiply(ray.Position - mlo.Position); iray.Direction = iorinv.Multiply(in ray.Direction); var hash = mlo.Archetype.Hash; var ybn = GameFileCache.GetYbn(hash); if (ybn is not null && ybn.Loaded) { var ihit = ybn.Bounds.RayIntersect(ref iray, res.HitDist); if (ihit.Hit) { ihit.HitYbn = ybn; ihit.HitEntity = mlo; ihit.Position = iori.Multiply(in ihit.Position) + mlo.Position; ihit.Normal = iori.Multiply(in ihit.Normal); } res.TryUpdate(in ihit); } var mlodat = mlo.MloInstance; if (mlodat is null) { return res; } var box = new BoundingBox(); float boxhitdisttest; if (mlodat.Entities != null && mlodat.Entities.Length > 0) { foreach(var intent in mlodat.Entities) { if (intent.Archetype is null) continue; //missing archetype... if (!EntityCollisionsEnabled(intent)) { continue; } box.Minimum = intent.BBMin; box.Maximum = intent.BBMax; if (ray.Intersects(ref box, out boxhitdisttest)) { if (boxhitdisttest > res.HitDist) { continue; } //already a closer hit var ehit = RayIntersectEntity(ref ray, intent, res.HitDist); res.TryUpdate(in ehit); } } } if (mlodat.EntitySets is not null && mlodat.EntitySets.Length > 0) { foreach(var entityset in mlodat.EntitySets) { if (!entityset.Visible) continue; var entities = entityset.Entities; if (entities is null) continue; for (int i = 0; i < entities.Count; i++) //should really improve this by using rooms! { var intent = entities[i]; if (intent.Archetype == null) continue; //missing archetype... if (!EntityCollisionsEnabled(intent)) { continue; } box.Minimum = intent.BBMin; box.Maximum = intent.BBMax; if (ray.Intersects(ref box, out boxhitdisttest)) { if (boxhitdisttest > res.HitDist) { continue; } //already a closer hit var ehit = RayIntersectEntity(ref ray, intent, res.HitDist); res.TryUpdate(in ehit); } } } } return res; } public SpaceSphereIntersectResult SphereIntersect(ref BoundingSphere sph, bool[] layers = null) { var res = new SpaceSphereIntersectResult(); if (GameFileCache == null) return res; bool testcomplete = true; Vector3 sphmin = sph.Center - sph.Radius; Vector3 sphmax = sph.Center + sph.Radius; var box = new BoundingBox(); if (BoundsStore is null || MapDataStore is null) return res; var boundslist = BoundsStore.GetItems(in sphmin, in sphmax, layers); var mapdatalist = MapDataStore.GetItems(in sphmin, in sphmax); for (int i = 0; i < boundslist.Count; i++) { var bound = boundslist[i]; box.Minimum = bound.Min; box.Maximum = bound.Max; if (sph.Intersects(ref box)) { YbnFile? ybn = GameFileCache.GetYbn(bound.Name); if (ybn is null) continue; //ybn not found? if (!ybn.Loaded) { testcomplete = false; continue; } //ybn not loaded yet... var b = ybn.Bounds; if (b is null) continue; var bhit = b.SphereIntersect(ref sph); res.TryUpdate(ref bhit); } } for (int i = 0; i < mapdatalist.Count; i++) { var mapdata = mapdatalist[i]; if ((mapdata.ContentFlags & 1) == 0) { continue; } //only test HD ymaps box.Minimum = mapdata.entitiesExtentsMin; box.Maximum = mapdata.entitiesExtentsMax; if (sph.Intersects(ref box)) { var hash = mapdata.Name; var ymap = (hash > 0) ? GameFileCache.GetYmap(hash) : null; if (ymap is not null && ymap.Loaded && ymap.AllEntities.Length > 0) { if (!IsYmapAvailable(hash, CurrentHour, CurrentWeather)) { continue; } for (int e = 0; e < ymap.AllEntities.Length; e++) { var ent = ymap.AllEntities[e]; if (!EntityCollisionsEnabled(ent)) { continue; } box.Minimum = ent.BBMin; box.Maximum = ent.BBMax; if (sph.Intersects(ref box)) { if (ent.IsMlo) { var ihit = SphereIntersectInterior(ref sph, ent); res.TryUpdate(ref ihit); } else { var ehit = SphereIntersectEntity(ref sph, ent); res.TryUpdate(ref ehit); } } } } else if ((ymap != null) && (!ymap.Loaded)) { testcomplete = false; } } } //if (hit) //{ // hitpos = ray.Position + ray.Direction * itemhitdist; //} res.TestComplete = testcomplete; return res; } public SpaceSphereIntersectResult SphereIntersectEntity(ref BoundingSphere sph, YmapEntityDef ent) { var res = new SpaceSphereIntersectResult(); var drawable = GameFileCache.TryGetDrawable(ent.Archetype); if (drawable != null) { var eori = ent.Orientation; var eorinv = Quaternion.Invert(ent.Orientation); var esph = sph; esph.Center = eorinv.Multiply(sph.Center - ent.Position); if ((drawable is Drawable sdrawable) && (sdrawable.Bound != null)) { var dhit = sdrawable.Bound.SphereIntersect(ref esph); if (dhit.Hit) { dhit.Position = eori.Multiply(in dhit.Position) + ent.Position; dhit.Normal = eori.Multiply(in dhit.Normal); } res.TryUpdate(ref dhit); } else if (drawable is FragDrawable fdrawable) { if (fdrawable.Bound != null) { var fhit = fdrawable.Bound.SphereIntersect(ref esph); if (fhit.Hit) { fhit.Position = eori.Multiply(in fhit.Position) + ent.Position; fhit.Normal = eori.Multiply(in fhit.Normal); } res.TryUpdate(ref fhit); } var fbound = fdrawable.OwnerFragment?.PhysicsLODGroup?.PhysicsLOD1?.Bound; if (fbound != null) { var fhit = fbound.SphereIntersect(ref esph);//TODO: these probably have extra transforms..! if (fhit.Hit) { fhit.Position = eori.Multiply(in fhit.Position) + ent.Position; fhit.Normal = eori.Multiply(in fhit.Normal); } res.TryUpdate(ref fhit); } } } return res; } public SpaceSphereIntersectResult SphereIntersectInterior(ref BoundingSphere sph, YmapEntityDef mlo) { var res = new SpaceSphereIntersectResult(); if (mlo.Archetype == null) { return res; } var iori = mlo.Orientation; var iorinv = Quaternion.Invert(mlo.Orientation); var isph = sph; isph.Center = iorinv.Multiply(sph.Center - mlo.Position); var hash = mlo.Archetype.Hash; var ybn = GameFileCache.GetYbn(hash); if ((ybn != null) && (ybn.Loaded)) { var ihit = ybn.Bounds.SphereIntersect(ref isph); if (ihit.Hit) { ihit.Position = iori.Multiply(in ihit.Position) + mlo.Position; ihit.Normal = iori.Multiply(in ihit.Normal); } res.TryUpdate(ref ihit); } var mlodat = mlo.MloInstance; if (mlodat == null) { return res; } var box = new BoundingBox(); if (mlodat.Entities != null) { for (int j = 0; j < mlodat.Entities.Length; j++) //should really improve this by using rooms! { var intent = mlodat.Entities[j]; if (intent.Archetype == null) continue; //missing archetype... if (!EntityCollisionsEnabled(intent)) { continue; } box.Minimum = intent.BBMin; box.Maximum = intent.BBMax; if (sph.Intersects(ref box)) { var ehit = SphereIntersectEntity(ref sph, intent); res.TryUpdate(ref ehit); } } } if (mlodat.EntitySets != null) { for (int e = 0; e < mlodat.EntitySets.Length; e++) { var entityset = mlodat.EntitySets[e]; if (!entityset.Visible) continue; var entities = entityset.Entities; if (entities == null) continue; for (int i = 0; i < entities.Count; i++) //should really improve this by using rooms! { var intent = entities[i]; if (intent.Archetype == null) continue; //missing archetype... if (!EntityCollisionsEnabled(intent)) { continue; } box.Minimum = intent.BBMin; box.Maximum = intent.BBMax; if (sph.Intersects(ref box)) { var ehit = SphereIntersectEntity(ref sph, intent); res.TryUpdate(ref ehit); } } } } return res; } private bool EntityCollisionsEnabled(YmapEntityDef ent) { if ((ent._CEntityDef.lodLevel != rage__eLodType.LODTYPES_DEPTH_ORPHANHD) && (ent._CEntityDef.lodLevel != rage__eLodType.LODTYPES_DEPTH_HD)) { return false; } //only test HD entities if ((ent._CEntityDef.flags & 4) > 0) { return false; } //embedded collisions disabled return true; } } public struct SpaceBoundsKey { public MetaHash Name { get; set; } public Vector3 Position { get; set; } public SpaceBoundsKey(MetaHash name, Vector3 position) { Name = name; Position = position; } } public class SpaceMapDataStore { public SpaceMapDataStoreNode RootNode; public int SplitThreshold = 10; public List VisibleItems = new List(); public void Init(IEnumerable rootnodes) { RootNode = new SpaceMapDataStoreNode(); RootNode.Owner = this; foreach (var item in rootnodes) { RootNode.Add(item); } RootNode.TrySplit(SplitThreshold); } public List GetItems(in Vector3 p) //get items at a point, using the streaming extents { VisibleItems.Clear(); RootNode?.GetItems(in p, VisibleItems); return VisibleItems; } public List GetItems(in Vector3 min, in Vector3 max) //get items intersecting a box, using the entities extents { VisibleItems.Clear(); RootNode?.GetItems(in min, in max, VisibleItems); return VisibleItems; } public List GetItems(ref Ray ray) //get items intersecting a ray, using the entities extents { VisibleItems.Clear(); RootNode?.GetItems(ref ray, VisibleItems); return VisibleItems; } } public class SpaceMapDataStoreNode { public SpaceMapDataStore? Owner = null; public SpaceMapDataStoreNode[]? Children = null; public List? Items = null; public Vector3 BBMin = new Vector3(float.MaxValue); public Vector3 BBMax = new Vector3(float.MinValue); public int Depth = 0; public void Add(MapDataStoreNode item) { Items ??= new List(); Vectors.Min(in BBMin, item.streamingExtentsMin, out BBMin); Vectors.Max(in BBMax, item.streamingExtentsMax, out BBMax); Items.Add(item); } public void TrySplit(int threshold) { if (Items is null || Items.Count <= threshold) return; Children = new SpaceMapDataStoreNode[4]; var newItems = new List(); var ncen = (BBMax + BBMin) * 0.5f; var next = (BBMax - BBMin) * 0.5f; var nsiz = Math.Max(next.X, next.Y); var nsizh = nsiz * 0.5f; foreach (var item in Items) { var imin = item.streamingExtentsMin; var imax = item.streamingExtentsMax; var icen = (imax + imin) * 0.5f; var iext = (imax - imin) * 0.5f; var isiz = Math.Max(iext.X, iext.Y); if (isiz >= nsizh) { newItems.Add(item); } else { var cind = ((icen.X > ncen.X) ? 1 : 0) + ((icen.Y > ncen.Y) ? 2 : 0); var c = Children[cind]; if (c is null) { c = new SpaceMapDataStoreNode(); c.Owner = Owner; c.Depth = Depth + 1; Children[cind] = c; } c.Add(item); } } for (int i = 0; i < 4; i++) { Children[i]?.TrySplit(threshold); } Items = newItems; } public void GetItems(in Vector3 p, List items) //get items at a point, using the streaming extents { if (p.X >= BBMin.X && p.X <= BBMax.X && p.Y >= BBMin.Y && p.Y <= BBMax.Y) { if (Items is not null) { foreach (var item in Items.AsSpan()) { var imin = item.streamingExtentsMin; var imax = item.streamingExtentsMax; if (p.X >= imin.X && p.X <= imax.X && p.Y >= imin.Y && p.Y <= imax.Y) { items.Add(item); } } } if (Children is not null) { foreach(var c in Children) { c?.GetItems(in p, items); } } } } public void GetItems(in Vector3 min, in Vector3 max, List items) //get items intersecting a box, using the entities extents { if ((max.X >= BBMin.X) && (min.X <= BBMax.X) && (max.Y >= BBMin.Y) && (min.Y <= BBMax.Y)) { if (Items is not null) { foreach(var item in Items.AsSpan()) { var imin = item.entitiesExtentsMin; var imax = item.entitiesExtentsMax; if (max.X >= imin.X && min.X <= imax.X && max.Y >= imin.Y && min.Y <= imax.Y) { items.Add(item); } } } if (Children is not null) { foreach(var child in Children) { child?.GetItems(in min, in max, items); } } } } public void GetItems(ref Ray ray, List items) //get items intersecting a ray, using the entities extents { var bb = new BoundingBox(BBMin, BBMax); if (ray.Intersects(ref bb)) { if (Items != null) { foreach(var item in Items.AsSpan()) { bb.Minimum = item.entitiesExtentsMin; bb.Maximum = item.entitiesExtentsMax; if (ray.Intersects(ref bb)) { items.Add(item); } } } if (Children != null) { for (int i = 0; i < 4; i++) { var c = Children[i]; c?.GetItems(ref ray, items); } } } } } public class SpaceBoundsStore { public SpaceBoundsStoreNode RootNode; public int SplitThreshold = 10; public List VisibleItems = new List(); public void Init(IEnumerable items) { RootNode = new SpaceBoundsStoreNode(); RootNode.Owner = this; RootNode.AddRange(items); RootNode.TrySplit(SplitThreshold); } public List GetItems(in Vector3 min, in Vector3 max, bool[]? layers = null) { VisibleItems.Clear(); RootNode?.GetItems(in min, in max, VisibleItems, layers); return VisibleItems; } public List GetItems(ref Ray ray, bool[]? layers = null) { VisibleItems.Clear(); RootNode?.GetItems(ref ray, VisibleItems, layers); return VisibleItems; } } public class SpaceBoundsStoreNode { public SpaceBoundsStore? Owner = null; public SpaceBoundsStoreNode[]? Children = null; public PooledList? Items = null; public Vector3 BBMin = new Vector3(float.MaxValue); public Vector3 BBMax = new Vector3(float.MinValue); public int Depth = 0; public void Add(BoundsStoreItem item) { Items ??= PooledListPool.Shared.Get(); Vectors.Min(in BBMin, in item.Min, out BBMin); Vectors.Max(in BBMax, in item.Max, out BBMax); Items.Add(item); } public void AddRange(IEnumerable items) { Items ??= PooledListPool.Shared.Get(); if (items.TryGetNonEnumeratedCount(out var count)) { Items.EnsureCapacity(Items.Count + count); } foreach (var item in items) { Vectors.Min(in BBMin, in item.Min, out BBMin); Vectors.Max(in BBMax, in item.Max, out BBMax); Items.Add(item); } } public void TrySplit(int threshold) { if (Items is null || Items.Count <= threshold) return; Children = new SpaceBoundsStoreNode[4]; var newItems = PooledListPool.Shared.Get(); var ncen = (BBMax + BBMin) * 0.5f; var next = (BBMax - BBMin) * 0.5f; var nsiz = Math.Max(next.X, next.Y); var nsizh = nsiz * 0.5f; foreach (var item in Items.Span) { ref readonly var imin = ref item.Min; ref readonly var imax = ref item.Max; var icen = (imax + imin) * 0.5f; var iext = (imax - imin) * 0.5f; var isiz = Math.Max(iext.X, iext.Y); if (isiz >= nsizh) { newItems.Add(item); } else { var cind = ((icen.X > ncen.X) ? 1 : 0) + ((icen.Y > ncen.Y) ? 2 : 0); var c = Children[cind]; if (c is null) { c = new SpaceBoundsStoreNode(); c.Owner = Owner; c.Depth = Depth + 1; Children[cind] = c; } c.Add(item); } } for (int i = 0; i < 4; i++) { Children[i]?.TrySplit(threshold); } if (Items is not null) { PooledListPool.Shared.Return(Items); } Items = newItems; } public void GetItems(in Vector3 min, in Vector3 max, List items, bool[]? layers = null) { if (max.X >= BBMin.X && min.X <= BBMax.X && max.Y >= BBMin.Y && min.Y <= BBMax.Y) { if (Items != null) { foreach(var item in Items.Span) { if (layers is not null && item.Layer < 3 && !layers[item.Layer]) { continue; } if (max.X >= item.Min.X && min.X <= item.Max.X && max.Y >= item.Min.Y && min.Y <= item.Max.Y) { items.Add(item); } } } if (Children != null) { for (int i = 0; i < 4; i++) { Children[i]?.GetItems(in min, in max, items, layers); } } } } public void GetItems(ref Ray ray, List items, bool[]? layers = null) { var box = new BoundingBox(BBMin, BBMax); if (ray.Intersects(ref box)) { if (Items is not null) { foreach(var item in Items.Span) { if (layers is not null && item.Layer < 3 && !layers[item.Layer]) { continue; } box = new BoundingBox(item.Min, item.Max); if (ray.Intersects(ref box)) { items.Add(item); } } } if (Children is not null) { for (int i = 0; i < 4; i++) { Children[i]?.GetItems(ref ray, items, layers); } } } } } public class SpaceNodeGrid { //node grid for V paths public SpaceNodeGridCell[,] Cells { get; set; } public float CellSize = 512.0f; public float CellSizeInv; //inverse of the cell size. public int CellCountX = 32; public int CellCountY = 32; public float CornerX = -8192.0f; public float CornerY = -8192.0f; public SpaceNodeGrid() { CellSizeInv = 1.0f / CellSize; Cells = new SpaceNodeGridCell[CellCountX, CellCountY]; for (int x = 0; x < CellCountX; x++) { for (int y = 0; y < CellCountY; y++) { Cells[x, y] = new SpaceNodeGridCell(x, y); } } } public SpaceNodeGridCell? GetCell(int id) { int x = id % CellCountX; int y = id / CellCountX; if (x >= 0 && x < CellCountX && y >= 0 && y < CellCountY) { return Cells[x, y]; } return null; } public bool TryGetCell(int id, [MaybeNullWhen(false)] out SpaceNodeGridCell cell) { int x = id % CellCountX; int y = id / CellCountX; if (x >= 0 && x < CellCountX && y >= 0 && y < CellCountY) { cell = Cells[x, y]; return true; } cell = default; return false; } public SpaceNodeGridCell? GetCellForPosition(in Vector3 position) { var x = (int)((position.X - CornerX) / CellSize); var y = (int)((position.Y - CornerY) / CellSize); if (x >= 0 && x < CellCountX && y >= 0 && y < CellCountY) { return Cells[x, y]; } return null; } public YndNode? GetYndNode(ushort areaid, ushort nodeid) { if (!TryGetCell(areaid, out var cell) || cell?.Ynd?.Nodes is null) { return null; } if (nodeid >= cell.Ynd.Nodes.Length) return null; return cell.Ynd.Nodes[nodeid]; } public void UpdateYnd(YndFile ynd) { for (int xx = 0; xx < Cells.GetLength(0); xx++) { for (int yy = 0; yy < Cells.GetLength(1); yy++) { if (Cells[xx, yy].Ynd == ynd) { Cells[xx, yy].Ynd = null; } } } var x = ynd.CellX; var y = ynd.CellY; Cells[x, y].Ynd = ynd; } } public class SpaceNodeGridCell { public int X; public int Y; public int ID; public YndFile? Ynd; public SpaceNodeGridCell(int x, int y) { X = x; Y = y; ID = y * 32 + x; } } public class SpaceNavGrid { //grid for V navmeshes public SpaceNavGridCell[,] Cells { get; set; } public float CellSize = 150.0f; public float CellSizeInv; //inverse of the cell size. public int CellCountX = 100; public int CellCountY = 100; public float CornerX = -6000.0f;//max = -6000+(100*150) = 9000 public float CornerY = -6000.0f; public SpaceNavGrid() { CellSizeInv = 1.0f / CellSize; Cells = new SpaceNavGridCell[CellCountX, CellCountY]; for (int x = 0; x < CellCountX; x++) { for (int y = 0; y < CellCountY; y++) { Cells[x, y] = new SpaceNavGridCell(x, y); } } } public SpaceNavGridCell GetCell(int id) { int x = id % CellCountX; int y = id / CellCountX; if ((x >= 0) && (x < CellCountX) && (y >= 0) && (y < CellCountY)) { return Cells[x, y]; } return null; } public Vector3 GetCellRel(Vector3 p)//float value in cell coords { return (p - new Vector3(CornerX, CornerY, 0)) * CellSizeInv; } public Vector2I GetCellPos(Vector3 p) { Vector3 ind = (p - new Vector3(CornerX, CornerY, 0)) * CellSizeInv; int x = (int)ind.X; int y = (int)ind.Y; x = (x < 0) ? 0 : (x >= CellCountX) ? CellCountX-1 : x; y = (y < 0) ? 0 : (y >= CellCountY) ? CellCountY-1 : y; return new Vector2I(x, y); } public SpaceNavGridCell GetCell(Vector2I g) { var cell = Cells[g.X, g.Y]; if (cell == null) { //cell = new SpaceNavGridCell(g.X, g.Y); //Cells[g.X, g.Y] = cell; } return cell; } public SpaceNavGridCell GetCell(Vector3 p) { return GetCell(GetCellPos(p)); } public Vector3 GetCellMin(SpaceNavGridCell cell) { Vector3 c = new Vector3(cell.X, cell.Y, 0); return new Vector3(CornerX, CornerY, 0) + (c * CellSize); } public Vector3 GetCellMax(SpaceNavGridCell cell) { return GetCellMin(cell) + new Vector3(CellSize, CellSize, 0.0f); } } public class SpaceNavGridCell { public int X; public int Y; public int ID; public int FileX; public int FileY; public RpfResourceFileEntry YnvEntry; public YnvFile Ynv; public SpaceNavGridCell(int x, int y) { X = x; Y = y; ID = y * 100 + x; FileX = x * 3; FileY = y * 3; } } public struct SpaceRayIntersectResult { public bool Hit; public float HitDist; public BoundVertexRef HitVertex; public BoundPolygon HitPolygon; public Bounds HitBounds; public YbnFile HitYbn; public YmapEntityDef HitEntity; public Vector3 Position; public Vector3 Normal; public int TestedNodeCount; public int TestedPolyCount; public bool TestComplete; public BoundMaterial_s Material; public void TryUpdate(in SpaceRayIntersectResult r) { if (r.Hit) { Hit = true; HitDist = r.HitDist; HitVertex = r.HitVertex; HitPolygon = r.HitPolygon; HitBounds = r.HitBounds; HitYbn = r.HitYbn; HitEntity = r.HitEntity; Material = r.Material; Position = r.Position; Normal = r.Normal; } TestedNodeCount += r.TestedNodeCount; TestedPolyCount += r.TestedPolyCount; } } public struct SpaceSphereIntersectResult { public bool Hit; public float HitDist; public BoundPolygon HitPolygon; public Vector3 Position; public Vector3 Normal; public int TestedNodeCount; public int TestedPolyCount; public bool TestComplete; public void TryUpdate(ref SpaceSphereIntersectResult r) { if (r.Hit) { Hit = true; HitPolygon = r.HitPolygon; Normal = r.Normal; } TestedPolyCount += r.TestedPolyCount; TestedNodeCount += r.TestedNodeCount; } } public struct SpaceEntityCollision { public Entity Entity; //the entity owning this collision public Entity Entity2; //second entity, if this is a collision between two entities public SpaceSphereIntersectResult SphereHit; //details of the sphere intersection point public Vector3 PrePos; //last known position before hit public float PreT; //last known T before hit public float HitT; //fraction of the frame (0-1) public Vector3 HitPos; //position of the sphere center at hit point public Quaternion HitRot; //rotation of the entity at hit point public Vector3 HitVel; //velocity of the entity for this hit public Vector3 HitAngVel; //angular velocity of the entity for this hit public int HitNumber; //count of previous iterations public bool Hit; public Vector3 HitVelDir; } }